From 6574f780dbbea515567385fd89ad6cecd1c828ac Mon Sep 17 00:00:00 2001 From: nytmyr <53322305+nytmyr@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:38:56 -0500 Subject: [PATCH] Bot Rework --- common/classes.h | 2 + .../database_update_manifest_bots.cpp | 340 ++ common/database_schema.h | 1 + common/eqemu_logsys.h | 16 +- common/eqemu_logsys_log_aliases.h | 70 + .../base/base_bot_data_repository.h | 85 +- .../base/base_bot_settings_repository.h | 464 ++ common/repositories/bot_data_repository.h | 40 - common/repositories/bot_settings_repository.h | 50 + common/ruletypes.h | 113 + common/spdat.cpp | 795 ++- common/spdat.h | 100 +- zone/aggro.cpp | 119 + zone/attack.cpp | 7 +- zone/bot.cpp | 4250 +++++++++++++---- zone/bot.h | 338 +- zone/bot_command.cpp | 229 +- zone/bot_command.h | 67 +- zone/bot_commands/actionable.cpp | 69 +- zone/bot_commands/aggressive.cpp | 26 +- zone/bot_commands/appearance.cpp | 2 +- zone/bot_commands/apply_poison.cpp | 1 + zone/bot_commands/attack.cpp | 9 +- zone/bot_commands/behind_mob.cpp | 173 + zone/bot_commands/bind_affinity.cpp | 1 + zone/bot_commands/bot.cpp | 373 +- zone/bot_commands/bot_settings.cpp | 43 + zone/bot_commands/cast.cpp | 301 ++ zone/bot_commands/caster_range.cpp | 9 +- zone/bot_commands/class_race_list.cpp | 113 + zone/bot_commands/click_item.cpp | 4 +- zone/bot_commands/copy_settings.cpp | 366 ++ zone/bot_commands/default_settings.cpp | 473 ++ zone/bot_commands/defensive.cpp | 11 +- zone/bot_commands/follow.cpp | 106 +- zone/bot_commands/guard.cpp | 6 +- zone/bot_commands/hold.cpp | 6 +- zone/bot_commands/illusion_block.cpp | 172 + zone/bot_commands/inventory.cpp | 21 +- zone/bot_commands/max_melee_range.cpp | 172 + zone/bot_commands/mesmerize.cpp | 44 +- zone/bot_commands/pet.cpp | 47 +- zone/bot_commands/pull.cpp | 18 +- zone/bot_commands/release.cpp | 2 +- zone/bot_commands/sit_hp_percent.cpp | 172 + zone/bot_commands/sit_in_combat.cpp | 172 + zone/bot_commands/sit_mana_percent.cpp | 172 + zone/bot_commands/spell.cpp | 4 +- zone/bot_commands/spell_aggro_checks.cpp | 236 + zone/bot_commands/spell_delays.cpp | 242 + zone/bot_commands/spell_engaged_priority.cpp | 240 + zone/bot_commands/spell_holds.cpp | 229 + zone/bot_commands/spell_idle_priority.cpp | 240 + zone/bot_commands/spell_max_hp_pct.cpp | 236 + zone/bot_commands/spell_max_mana_pct.cpp | 236 + zone/bot_commands/spell_max_thresholds.cpp | 243 + zone/bot_commands/spell_min_hp_pct.cpp | 236 + zone/bot_commands/spell_min_mana_pct.cpp | 236 + zone/bot_commands/spell_min_thresholds.cpp | 244 + zone/bot_commands/spell_pursue_priority.cpp | 240 + zone/bot_commands/spell_target_count.cpp | 236 + zone/bot_commands/suspend.cpp | 2 +- zone/bot_commands/taunt.cpp | 19 +- zone/bot_commands/timer.cpp | 9 +- zone/bot_database.cpp | 343 +- zone/bot_database.h | 26 +- zone/bot_structs.h | 5 + zone/botspellsai.cpp | 3785 +++++++-------- zone/client.cpp | 275 +- zone/client.h | 32 + zone/client_bot.cpp | 8 + zone/client_mods.cpp | 6 +- zone/client_packet.cpp | 13 +- zone/command.cpp | 10 + zone/command.h | 5 + zone/entity.cpp | 1 + zone/entity.h | 2 +- zone/exp.cpp | 1 + zone/gm_commands/illusion_block.cpp | 31 + zone/gm_commands/spell_delays.cpp | 215 + zone/gm_commands/spell_holds.cpp | 218 + zone/gm_commands/spell_max_thresholds.cpp | 216 + zone/gm_commands/spell_min_thresholds.cpp | 216 + zone/lua_bot.cpp | 6 - zone/lua_bot.h | 1 - zone/merc.cpp | 4 +- zone/mob.cpp | 825 +++- zone/mob.h | 103 + zone/perl_bot.cpp | 6 - zone/perl_client.cpp | 4 +- zone/perl_npc.cpp | 2 +- zone/questmgr.cpp | 2 +- zone/spell_effects.cpp | 10 +- zone/spells.cpp | 203 +- 94 files changed, 15920 insertions(+), 3952 deletions(-) create mode 100644 common/repositories/base/base_bot_settings_repository.h create mode 100644 common/repositories/bot_settings_repository.h create mode 100644 zone/bot_commands/behind_mob.cpp create mode 100644 zone/bot_commands/bot_settings.cpp create mode 100644 zone/bot_commands/cast.cpp create mode 100644 zone/bot_commands/class_race_list.cpp create mode 100644 zone/bot_commands/copy_settings.cpp create mode 100644 zone/bot_commands/default_settings.cpp create mode 100644 zone/bot_commands/illusion_block.cpp create mode 100644 zone/bot_commands/max_melee_range.cpp create mode 100644 zone/bot_commands/sit_hp_percent.cpp create mode 100644 zone/bot_commands/sit_in_combat.cpp create mode 100644 zone/bot_commands/sit_mana_percent.cpp create mode 100644 zone/bot_commands/spell_aggro_checks.cpp create mode 100644 zone/bot_commands/spell_delays.cpp create mode 100644 zone/bot_commands/spell_engaged_priority.cpp create mode 100644 zone/bot_commands/spell_holds.cpp create mode 100644 zone/bot_commands/spell_idle_priority.cpp create mode 100644 zone/bot_commands/spell_max_hp_pct.cpp create mode 100644 zone/bot_commands/spell_max_mana_pct.cpp create mode 100644 zone/bot_commands/spell_max_thresholds.cpp create mode 100644 zone/bot_commands/spell_min_hp_pct.cpp create mode 100644 zone/bot_commands/spell_min_mana_pct.cpp create mode 100644 zone/bot_commands/spell_min_thresholds.cpp create mode 100644 zone/bot_commands/spell_pursue_priority.cpp create mode 100644 zone/bot_commands/spell_target_count.cpp create mode 100644 zone/gm_commands/illusion_block.cpp create mode 100644 zone/gm_commands/spell_delays.cpp create mode 100644 zone/gm_commands/spell_holds.cpp create mode 100644 zone/gm_commands/spell_max_thresholds.cpp create mode 100644 zone/gm_commands/spell_min_thresholds.cpp diff --git a/common/classes.h b/common/classes.h index 96e5cad26..dc586ceeb 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 48c9aa7f8..afaa9f23e 100644 --- a/common/database/database_update_manifest_bots.cpp +++ b/common/database/database_update_manifest_bots.cpp @@ -161,6 +161,346 @@ 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` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `char_id` INT UNSIGNED NOT NULL, + `bot_id` INT UNSIGNED NOT NULL, + `setting_id` INT UNSIGNED NOT NULL, + `setting_type` INT UNSIGNED NOT NULL, + `value` INT UNSIGNED NOT NULL, + `category_name` VARCHAR(64) NULL DEFAULT '', + `setting_name` VARCHAR(64) NULL DEFAULT '', + PRIMARY KEY (`id`) USING BTREE +) +COLLATE='utf8mb4_general_ci'; + +INSERT INTO bot_settings SELECT NULL, 0, bd.`bot_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 NULL, 0, `bot_id`, 1, 0, `show_helm`, 'BaseSetting', 'ShowHelm' FROM bot_data WHERE `show_helm` != 1; +INSERT INTO bot_settings SELECT NULL, 0, `bot_id`, 2, 0, `follow_distance`, 'BaseSetting', 'FollowDistance' FROM bot_data WHERE `follow_distance` != 184; + +INSERT INTO bot_settings +SELECT NULL, 0, `bot_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 NULL, 0, `bot_id`, 4, 0, `enforce_spell_settings`, 'BaseSetting', 'EnforceSpellSettings' FROM bot_data WHERE `enforce_spell_settings` != 0; +INSERT INTO bot_settings SELECT NULL, 0, `bot_id`, 5, 0, `archery_setting`, 'BaseSetting', 'RangedSetting' FROM bot_data WHERE `archery_setting` != 0; + +INSERT INTO bot_settings +SELECT NULL, 0, `bot_id`, 8, 0, `caster_range`, 'BaseSetting', 'CasterRange' +FROM ( + SELECT `bot_id`, + (CASE + WHEN (`class` IN (1, 7, 19, 16)) THEN 0 + WHEN `class` = 8 THEN 0 + ELSE 90 + END) AS `casterRange`, + `caster_range` + FROM bot_data +) AS `subquery` +WHERE `casterRange` != `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`, '|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`= CASE WHEN LENGTH(`aliases`) > 0 THEN CONCAT(`aliases`, '|cr') ELSE 'cr' END WHERE `bot_command`='casterrange' AND `aliases` NOT LIKE '%cr%'; +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`= '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`= 'targetcount' WHERE `bot_command`='spelltargetcount'; +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%'; +)" + }, + 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.spellid = 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 `spellid` = 14312", + .condition = "empty", + .match = "", + .sql = R"( +-- Class fixes +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3002 WHERE b.`spellid` = 14312; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3002 WHERE b.`spellid` = 14313; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3002 WHERE b.`spellid` = 14314; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3005 WHERE b.`spellid` = 15186; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3005 WHERE b.`spellid` = 15187; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3005 WHERE b.`spellid` = 15188; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14446; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14447; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14467; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14468; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14469; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3003 WHERE b.`spellid` = 14955; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3003 WHERE b.`spellid` = 14956; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14387; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14388; +UPDATE bot_spells_entries b SET b.`npc_spells_id` = 3006 WHERE b.`spellid` = 14389; + +-- Minlevel fixes +UPDATE bot_spells_entries SET `minlevel` = 34 WHERE `spellid` = 1445 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 2 WHERE `spellid` = 229 AND `npc_spells_id` = 3011; +UPDATE bot_spells_entries SET `minlevel` = 13 WHERE `spellid` = 333 AND `npc_spells_id` = 3013; +UPDATE bot_spells_entries SET `minlevel` = 29 WHERE `spellid` = 106 AND `npc_spells_id` = 3013; +UPDATE bot_spells_entries SET `minlevel` = 38 WHERE `spellid` = 754 AND `npc_spells_id` = 3010; +UPDATE bot_spells_entries SET `minlevel` = 58 WHERE `spellid` = 2589 AND `npc_spells_id` = 3003; +UPDATE bot_spells_entries SET `minlevel` = 67 WHERE `spellid` = 5305 AND `npc_spells_id` = 3004; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 14267 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 14268 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 14269 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 23 WHERE `spellid` = 738 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 51 WHERE `spellid` = 1751 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 7 WHERE `spellid` = 734 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 5 WHERE `spellid` = 717 AND `npc_spells_id` = 3008; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 15186 AND `npc_spells_id` = 3005; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 15187 AND `npc_spells_id` = 3005; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 15188 AND `npc_spells_id` = 3005; +UPDATE bot_spells_entries SET `minlevel` = 80 WHERE `spellid` = 14446 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 80 WHERE `spellid` = 14447 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 14467 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 14468 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 79 WHERE `spellid` = 14469 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spellid` = 14955 AND `npc_spells_id` = 3003; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spellid` = 14956 AND `npc_spells_id` = 3003; +UPDATE bot_spells_entries SET `minlevel` = 78 WHERE `spellid` = 14387 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spellid` = 14388 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spellid` = 14389 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spellid` = 14312 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spellid` = 14313 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `minlevel` = 77 WHERE `spellid` = 14314 AND `npc_spells_id` = 3002; + +-- Maxlevel fixes +UPDATE bot_spells_entries SET `maxlevel` = 83 WHERE `spellid` = 14267 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 83 WHERE `spellid` = 14268 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 83 WHERE `spellid` = 14269 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spellid` = 14446 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spellid` = 14447 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spellid` = 14467 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spellid` = 14468 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 84 WHERE `spellid` = 14469 AND `npc_spells_id` = 3006; +UPDATE bot_spells_entries SET `maxlevel` = 81 WHERE `spellid` = 14312 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 81 WHERE `spellid` = 14313 AND `npc_spells_id` = 3002; +UPDATE bot_spells_entries SET `maxlevel` = 81 WHERE `spellid` = 14314 AND `npc_spells_id` = 3002; + +-- Type fixes +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 201; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spellid` = 752; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spellid` = 2117; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 2542; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 2544; +UPDATE bot_spells_entries SET `type` = 6 WHERE `spellid` = 2115; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 1403; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 1405; +UPDATE bot_spells_entries SET `type` = 9 WHERE `spellid` = 289; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 294; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 302; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 521; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 185; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 450; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 186; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 4074; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 195; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 1712; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 1703; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spellid` = 3229; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 3345; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 5509; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 6826; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 270; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 281; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 505; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 526; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 110; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 506; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 162; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 111; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 507; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 527; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 163; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 112; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 1588; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 1573; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 1592; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 1577; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 1578; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spellid` = 1576; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 3386; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 3387; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 4900; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 3395; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 5394; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 5392; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 6827; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spellid` = 5416; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 1437; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 1436; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 5348; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 8008; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 2571; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 370; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spellid` = 1741; +UPDATE bot_spells_entries SET `type` = 17 WHERE `spellid` = 1296; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 270; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 2634; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 2942; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 3462; +UPDATE bot_spells_entries SET `type` = 13 WHERE `spellid` = 6828; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spellid` = 14312; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spellid` = 14313; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spellid` = 14314; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 18392; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 18393; +UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 18394; +UPDATE bot_spells_entries SET `type` = 10 WHERE `spellid` = 15186; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 15187; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 15188; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spellid` = 14446; +UPDATE bot_spells_entries SET `type` = 1 WHERE `spellid` = 14447; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 14467; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 14468; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 14469; +UPDATE bot_spells_entries SET `type` = 0 WHERE `spellid` = 14267; +UPDATE bot_spells_entries SET `type` = 0 WHERE `spellid` = 14268; +UPDATE bot_spells_entries SET `type` = 0 WHERE `spellid` = 14269; +UPDATE bot_spells_entries SET `type` = 10 WHERE `spellid` = 14955; +UPDATE bot_spells_entries SET `type` = 10 WHERE `spellid` = 14956; +UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 14387; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 14388; +UPDATE bot_spells_entries SET `type` = 3 WHERE `spellid` = 14389; +UPDATE bot_spells_entries SET `type` = 4 WHERE `spellid` = 10436; + +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 3440; -- Ro's Illumination [#3440] from DoT [#8] to Debuff [#14] [Should be 0] +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 303; -- Whirl till you hurl [#303] from Nuke [#0] to Debuff [#14] [Should be 0] +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 619; -- Dyn's Dizzying Draught [#619] from Nuke [#0] to Debuff [#14] [Should be 0] + +-- UPDATE bot_spells_entries SET `type` = 14 WHERE `spellid` = 74; -- Mana Sieve [#74] from Nuke [#0] to Debuff [#14] +-- UPDATE bot_spells_entries SET `type` = 6 WHERE `spellid` = 1686; -- Theft of Thought [#1686] from Nuke [#0] to Lifetap [#6] + +-- UPDATE bot_spells_entries SET `type` = 1 WHERE `spellid` = 3694; -- Stoicism [#3694] from In-Combat Buff [#10] to Regular Heal [#1] +-- UPDATE bot_spells_entries SET `type` = 1 WHERE `spellid` = 4899; -- Breath of Trushar [#4899] from In-Combat Buff [#10] to Regular Heal [#1] + +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spellid` = 738; -- Selo's Consonant Chain [#738] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spellid` = 1751; -- Largo's Assonant Binding [#1751] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 8 WHERE `spellid` = 1748; -- Angstlich's Assonance [#1748] from Slow [#13] to DoT [#8] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spellid` = 738; -- Selo's Consonant Chain [#738] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spellid` = 1751; -- Largo's Assonant Binding [#1751] from Slow [#13] to Snare [#7] +-- UPDATE bot_spells_entries SET `type` = 7 WHERE `spellid` = 738; -- Selo's Consonant Chain [#738] from Slow [#13] to Snare [#7] + )" } // -- 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 e0b87b94d..6bf75b6d1 100644 --- a/common/database_schema.h +++ b/common/database_schema.h @@ -415,6 +415,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 8bf474f34..0fa85b75a 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -142,6 +142,13 @@ namespace Logs { EqTime, Corpses, XTargets, + BotSettings, + BotPreChecks, + BotHoldChecks, + BotDelayChecks, + BotThresholdChecks, + BotSpellTypeChecks, + TestDebug, MaxCategoryID /* Don't Remove this */ }; @@ -242,7 +249,14 @@ namespace Logs { "Zoning", "EqTime", "Corpses", - "XTargets" + "XTargets", + "Bot Settings", + "Bot Pre Checks", + "Bot Hold Checks", + "Bot Delay Checks", + "Bot Threshold Checks", + "Bot Spell Type Checks", + "Test Debug" }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 10c9cd98a..fdd33be14 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -844,6 +844,76 @@ OutF(LogSys, Logs::Detail, Logs::XTargets, __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 LogBotPreChecks(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::BotPreChecks))\ + OutF(LogSys, Logs::General, Logs::BotPreChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotPreChecksDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::BotPreChecks))\ + OutF(LogSys, Logs::Detail, Logs::BotPreChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotHoldChecks(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::BotHoldChecks))\ + OutF(LogSys, Logs::General, Logs::BotHoldChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotHoldChecksDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::BotHoldChecks))\ + OutF(LogSys, Logs::Detail, Logs::BotHoldChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotDelayChecks(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::BotDelayChecks))\ + OutF(LogSys, Logs::General, Logs::BotDelayChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotDelayChecksDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::BotDelayChecks))\ + OutF(LogSys, Logs::Detail, Logs::BotDelayChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotThresholdChecks(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::BotThresholdChecks))\ + OutF(LogSys, Logs::General, Logs::BotThresholdChecks, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogBotThresholdChecksDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::BotThresholdChecks))\ + OutF(LogSys, Logs::Detail, Logs::BotThresholdChecks, __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 LogTestDebug(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::General, Logs::TestDebug))\ + OutF(LogSys, Logs::General, Logs::TestDebug, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogTestDebugDetail(message, ...) do {\ + if (LogSys.IsLogEnabled(Logs::Detail, Logs::TestDebug))\ + OutF(LogSys, Logs::Detail, Logs::TestDebug, __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_data_repository.h b/common/repositories/base/base_bot_data_repository.h index 96622c2db..eb0cdcb99 100644 --- a/common/repositories/base/base_bot_data_repository.h +++ b/common/repositories/base/base_bot_data_repository.h @@ -64,13 +64,6 @@ public: 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 @@ public: "poison", "disease", "corruption", - "show_helm", - "follow_distance", - "stop_melee_level", - "expansion_bitmask", - "enforce_spell_settings", - "archery_setting", - "caster_range", }; } @@ -184,13 +170,6 @@ public: "poison", "disease", "corruption", - "show_helm", - "follow_distance", - "stop_melee_level", - "expansion_bitmask", - "enforce_spell_settings", - "archery_setting", - "caster_range", }; } @@ -276,13 +255,7 @@ public: 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 @@ public: 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; } @@ -448,13 +414,6 @@ public: 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 +480,6 @@ public: 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 +554,6 @@ public: 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 +632,6 @@ public: 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 +701,6 @@ public: 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 +820,6 @@ public: 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 +887,6 @@ public: 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 000000000..2018177dc --- /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 id; + uint32_t char_id; + uint32_t bot_id; + 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("id"); + } + + static std::vector Columns() + { + return { + "id", + "char_id", + "bot_id", + "setting_id", + "setting_type", + "value", + "category_name", + "setting_name", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "char_id", + "bot_id", + "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.id = 0; + e.char_id = 0; + e.bot_id = 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.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.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.bot_id = 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(strtoul(row[5], nullptr, 10)) : 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.id)); + v.push_back(columns[1] + " = " + std::to_string(e.char_id)); + v.push_back(columns[2] + " = " + std::to_string(e.bot_id)); + 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.bot_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static BotSettings InsertOne( + Database& db, + BotSettings e + ) + { + std::vector v; + + v.push_back(std::to_string(e.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.bot_id)); + 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.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.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.bot_id)); + 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.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.bot_id = 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(strtoul(row[5], nullptr, 10)) : 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.id = row[0] ? static_cast(strtoul(row[0], nullptr, 10)) : 0; + e.char_id = row[1] ? static_cast(strtoul(row[1], nullptr, 10)) : 0; + e.bot_id = 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(strtoul(row[5], nullptr, 10)) : 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.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.bot_id)); + 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.id)); + v.push_back(std::to_string(e.char_id)); + v.push_back(std::to_string(e.bot_id)); + 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_data_repository.h b/common/repositories/bot_data_repository.h index f6508479f..e8d0038a4 100644 --- a/common/repositories/bot_data_repository.h +++ b/common/repositories/bot_data_repository.h @@ -44,46 +44,6 @@ public: */ // 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 000000000..7cf1d0dbd --- /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 60560345e..e033ffd82 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -381,6 +381,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) @@ -753,6 +756,7 @@ 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.") @@ -782,6 +786,91 @@ 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, PercentChanceToCastNuke, 75, "The chance for a bot to attempt to cast the given spell type in combat. Default 75%.") +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, 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, 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, 250, "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 2000ms.") +RULE_INT(Bots, MinDelayBetweenOutCombatCastAttempts, 125, "The minimum delay in milliseconds between cast attempts while out-of-combat. This is rolled between the min and max. Default 200ms.") +RULE_INT(Bots, MaxDelayBetweenOutCombatCastAttempts, 500, "The maximum delay in milliseconds between cast attempts while out-of-combat. This is rolled between the min and max. Default 500ms.") +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, 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_BOOL(Bots, AllowSpellPulling, 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, 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, 100, "100 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, "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.60, "Multiplier of the max 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.25, "Multiplier of max 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, CastersStayJustOutOfMeleeRange, true, "True Default. If true, caster bots will stay just out of melee range. Otherwise they use Bots:PercentMinCasterRangeDistance.") +RULE_REAL(Bots, PercentMinCasterRangeDistance, 0.60, "Multiplier of the closest caster range at which a bot will stand while casting before trying to adjust. 0.60 Recommended.") +RULE_BOOL(Bots, TauntingBotsFollowTopHate, true, "True Default. If true, bots that are taunting will attempt to stick with whoever currently is top hate.") +RULE_REAL(Bots, DistanceTauntingBotsStickMainHate, 25.00, "If TauntingBotsFollowTopHate is enabled, this is the distance bots will try to stick to whoever currently is Top Hate.") +RULE_BOOL(Bots, DisableSpecialAbilitiesAtMaxMelee, false, "False 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, true, "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, false, "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 the anti-spam system") +RULE_INT(Bots, StatusSpawnLimit, 120, "Minimum status to bypass spawn limit. Default 120.") +RULE_INT(Bots, MinStatusToBypassCreateLimit, 100, "Minimum status to bypass the anti-spam system") +RULE_INT(Bots, StatusCreateLimit, 120, "Minimum status to bypass spawn limit. Default 120.") +RULE_BOOL(Bots, BardsAnnounceCasts, false, "This determines whether or not Bard bots will announce that they're casting songs (Buffs, Heals, Nukes, Slows, etc.) they will always announce Mez.") RULE_CATEGORY_END() RULE_CATEGORY(Chat) @@ -1007,6 +1096,30 @@ 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_STRING(Command, DescriptionColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, DescriptionHeaderColor, "indian_red", "Color for command help windows") +RULE_STRING(Command, AltDescriptionColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, NoteColor, "dark_orange", "Color for command help windows") +RULE_STRING(Command, NoteHeaderColor, "indian_red", "Color for command help windows") +RULE_STRING(Command, AltNoteColor, "dark_orange", "Color for command help windows") +RULE_STRING(Command, ExampleColor, "goldenrod", "Color for command help windows") +RULE_STRING(Command, ExampleHeaderColor, "indian_red", "Color for command help windows") +RULE_STRING(Command, SubExampleColor, "slate_blue", "Color for command help windows") +RULE_STRING(Command, AltExampleColor, "goldenrod", "Color for command help windows") +RULE_STRING(Command, SubAltExampleColor, "goldenrod", "Color for command help windows") +RULE_STRING(Command, OptionColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, OptionHeaderColor, "indian_red", "Color for command help windows") +RULE_STRING(Command, SubOptionColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, AltOptionColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, SubAltOptionColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, ActionableColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, ActionableHeaderColor, "indian_red", "Color for command help windows") +RULE_STRING(Command, AltActionableColor, "light_grey", "Color for command help windows") +RULE_STRING(Command, HeaderColor, "indian_red", "Color for command help windows") +RULE_STRING(Command, SecondaryHeaderColor, "slate_blue", "Color for command help windows") +RULE_STRING(Command, AltHeaderColor, "indian_red", "Color for command help windows") +RULE_STRING(Command, FillerLineColor, "dark_grey", "Color for command help windows") RULE_CATEGORY_END() RULE_CATEGORY(Doors) diff --git a/common/spdat.cpp b/common/spdat.cpp index 3acac1c8b..53bf03f6d 100644 --- a/common/spdat.cpp +++ b/common/spdat.cpp @@ -125,9 +125,26 @@ bool IsMesmerizeSpell(uint16 spell_id) return IsEffectInSpell(spell_id, SE_Mez); } +bool SpellBreaksMez(uint16 spell_id) +{ + if (IsDetrimentalSpell(spell_id) && IsAnyDamageSpell(spell_id)) { + return true; + } + + return false; +} + bool IsStunSpell(uint16 spell_id) { - return IsEffectInSpell(spell_id, SE_Stun); + if (IsEffectInSpell(spell_id, SE_Stun)) { + return true; + } + + if (IsEffectInSpell(spell_id, SE_SpinTarget)) { + return true; + } + + return false; } bool IsSummonSpell(uint16 spell_id) @@ -164,13 +181,7 @@ bool IsDamageSpell(uint16 spell_id) const auto effect_id = spell.effect_id[i]; if ( spell.base_value[i] < 0 && - ( - effect_id == SE_CurrentHPOnce || - ( - effect_id == SE_CurrentHP && - spell.buff_duration < 1 - ) - ) + (effect_id == SE_CurrentHPOnce || effect_id == SE_CurrentHP) ) { return true; } @@ -179,6 +190,62 @@ bool IsDamageSpell(uint16 spell_id) return false; } +bool IsAnyDamageSpell(uint16 spell_id) +{ + 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 && + spell.buff_duration < 1 + ) + ) + ) { + return true; + } + } + + 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 +476,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 +628,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 +655,137 @@ 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) { + //if ( + // spells[spell_id].target_type == ST_Target || + // spells[spell_id].target_type == ST_Self || + // spells[spell_id].target_type == ST_Animal || + // spells[spell_id].target_type == ST_Undead || + // spells[spell_id].target_type == ST_Summoned || + // spells[spell_id].target_type == ST_Tap || + // spells[spell_id].target_type == ST_Pet || + // spells[spell_id].target_type == ST_Corpse || + // spells[spell_id].target_type == ST_Plant || + // spells[spell_id].target_type == ST_Giant || + // spells[spell_id].target_type == ST_Dragon || + // spells[spell_id].target_type == ST_HateList || + // spells[spell_id].target_type == ST_LDoNChest_Cursed || + // spells[spell_id].target_type == ST_Muramite || + // spells[spell_id].target_type == ST_SummonedPet || + // spells[spell_id].target_type == ST_TargetsTarget || + // spells[spell_id].target_type == ST_PetMaster //|| + // //spells[spell_id].target_type == ST_AEBard //TODO needed? + //) { + // return false; + //} + + if (IsAESpell(spell_id) || IsPBAENukeSpell(spell_id) || IsPBAESpell(spell_id) || IsAERainSpell(spell_id) || IsAERainNukeSpell(spell_id) || IsAEDurationSpell(spell_id)) { + return true; + } + + return false; +} + +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 +842,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,6 +1465,7 @@ bool IsCompleteHealSpell(uint16 spell_id) } return false; + } bool IsFastHealSpell(uint16 spell_id) @@ -1386,20 +1587,61 @@ bool IsRegularSingleTargetHealSpell(uint16 spell_id) return false; } -bool IsRegularGroupHealSpell(uint16 spell_id) +bool IsRegularPetHealSpell(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) { + if ( + spells[spell_id].target_type == ST_Pet && + !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 + ) + ) { + return true; + } + } + } + } + + return false; +} + +bool IsRegularGroupHealSpell(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) { @@ -1415,8 +1657,8 @@ bool IsRegularGroupHealSpell(uint16 spell_id) ( spells[spell_id].effect_id[i] == SE_CurrentHP || spells[spell_id].effect_id[i] == SE_CurrentHPOnce - ) - ) { + ) + ) { return true; } } @@ -1429,9 +1671,14 @@ bool IsRegularGroupHealSpell(uint16 spell_id) bool IsGroupCompleteHealSpell(uint16 spell_id) { if ( - IsGroupSpell(spell_id) && - IsCompleteHealSpell(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; } @@ -1441,9 +1688,92 @@ bool IsGroupCompleteHealSpell(uint16 spell_id) bool IsGroupHealOverTimeSpell(uint16 spell_id) { if ( - IsGroupSpell(spell_id) && - IsHealOverTimeSpell(spell_id) && - spells[spell_id].buff_duration < 10 + ( + 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 ( + 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; + } + + return false; +} + +bool IsAnyBuffSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if ( + 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; + } + + return false; +} + +bool IsEscapeSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if ( + 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) ) { return true; } @@ -1464,7 +1794,8 @@ 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) ) { return false; } @@ -1472,6 +1803,22 @@ bool IsDebuffSpell(uint16 spell_id) return true; } +bool IsHateReduxSpell(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if ( + (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) + ) { + return true; + } + + return false; +} + bool IsResistDebuffSpell(uint16 spell_id) { if ( @@ -2383,7 +2730,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 +2777,401 @@ bool AegolismStackingIsArmorClassSpell(uint16 spell_id) { return 0; } + +int8 SpellEffectsCount(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + int8 i = 0; + + for (int i = 0; i < EFFECT_COUNT; i++) { + if (!IsBlankSpellEffect(spell_id, i)) { + ++i; + } + } + + return i; +} + +bool IsLichSpell(uint16 spell_id) +{ + if (!IsValidSpell(spell_id)) { + return false; + } + + if ( + 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 + ) { + return true; + } + + return false; +} + +bool BOT_SPELL_TYPES_DETRIMENTAL(uint16 spellType, uint8 cls) { + switch (spellType) { + 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: + return true; + case BotSpellTypes::InCombatBuff: + if (cls == Class::ShadowKnight) { + return true; + } + + return false; + default: + return false; + } + + return false; +} + +bool BOT_SPELL_TYPES_BENEFICIAL(uint16 spellType, uint8 cls) { + switch (spellType) { + 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::DamageShields: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::Pet: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::ResistBuffs: + case BotSpellTypes::Resurrect: + return true; + case BotSpellTypes::InCombatBuff: + if (cls == Class::ShadowKnight) { + return false; + } + + return true; + default: + return false; + } + + return false; +} + +bool BOT_SPELL_TYPES_OTHER_BENEFICIAL(uint16 spellType) { + switch (spellType) { + 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::DamageShields: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::ResistBuffs: + return true; + default: + return false; + } + + return false; +} + +bool BOT_SPELL_TYPES_INNATE(uint16 spellType) { + switch (spellType) { + 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: + return true; + default: + return false; + } + + return false; +} + +bool IsBotSpellType(uint16 spellType) { + if (BOT_SPELL_TYPES_DETRIMENTAL(spellType) && BOT_SPELL_TYPES_BENEFICIAL(spellType) && BOT_SPELL_TYPES_INNATE(spellType)) { + return true; + } + + return false; +} + +bool IsAEBotSpellType(uint16 spellType) { + switch (spellType) { + 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: + return true; + default: + return false; + } + + return false; +} + +bool IsGroupBotSpellType(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::GroupCures: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupHoTHeals: + return true; + default: + return false; + } + + return false; +} + +bool IsGroupTargetOnlyBotSpellType(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::GroupCures: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::GroupHeals: + return true; + default: + return false; + } + + return false; +} + +bool IsPetBotSpellType(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PetRegularHeals: + case BotSpellTypes::PetCompleteHeals: + case BotSpellTypes::PetFastHeals: + case BotSpellTypes::PetVeryFastHeals: + case BotSpellTypes::PetHoTHeals: + return true; + default: + return false; + } + + return false; +} + +bool IsClientBotSpellType(uint16 spellType) { + switch (spellType) { + 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::DamageShields: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::ResistBuffs: + return true; + default: + return false; + } + + return false; +} + +bool IsHealBotSpellType(uint16 spellType) { + switch (spellType) { + 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 SpellTypeRequiresLoS(uint16 spellType, uint16 cls) { + if (IsAEBotSpellType(spellType)) { // These gather their own targets later + return false; + } + + switch (spellType) { + 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: + return false; + case BotSpellTypes::InCombatBuff: + if (cls && cls == Class::ShadowKnight) { + return true; + } + + return false; + default: + return true; + } + + return true; +} + +bool SpellTypeRequiresTarget(uint16 spellType, uint16 cls) { + switch (spellType) { + case BotSpellTypes::Escape: + if (cls == Class::ShadowKnight) { + return false; + } + + return true; + case BotSpellTypes::Pet: + return false; + default: + return true; + } + + return true; +} + +bool IsValidSpellAndLoS(uint32 spell_id, bool hasLoS) { + if (!IsValidSpell(spell_id)) { + return false; + } + + if (!hasLoS && IsTargetRequiredForSpell(spell_id)) { + return false; + } + + return true; +} + +bool IsInstantHealSpell(uint32 spell_id) { + if (IsRegularSingleTargetHealSpell(spell_id) || IsRegularGroupHealSpell(spell_id) || IsRegularPetHealSpell(spell_id) || IsRegularGroupHealSpell(spell_id) || spell_id == SPELL_COMPLETE_HEAL) { + return true; + } + + return false; +} + +bool IsResurrectSpell(uint16 spell_id) +{ + return IsEffectInSpell(spell_id, SE_Revive); +} + +bool RequiresStackCheck(uint16 spellType) { + switch (spellType) { + 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; +} diff --git a/common/spdat.h b/common/spdat.h index d597c79fa..432774b95 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 @@ -647,14 +651,84 @@ enum SpellTypes : uint32 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 GroupCures = 24; + constexpr uint16 CompleteHeal = 25; + constexpr uint16 FastHeals = 26; + constexpr uint16 VeryFastHeals = 27; + constexpr uint16 GroupHeals = 28; + constexpr uint16 GroupCompleteHeals = 29; + constexpr uint16 GroupHoTHeals = 30; + constexpr uint16 HoTHeals = 31; + constexpr uint16 AENukes = 32; + constexpr uint16 AERains = 33; + constexpr uint16 AEMez = 34; + constexpr uint16 AEStun = 35; + constexpr uint16 AEDebuff = 36; + constexpr uint16 AESlow = 37; + constexpr uint16 AESnare = 38; + constexpr uint16 AEFear = 39; + constexpr uint16 AEDispel = 40; + constexpr uint16 AERoot = 41; + constexpr uint16 AEDoT = 42; + constexpr uint16 AELifetap = 43; + constexpr uint16 PBAENuke = 44; + constexpr uint16 PetBuffs = 45; + constexpr uint16 PetRegularHeals = 46; + constexpr uint16 PetCompleteHeals = 47; + constexpr uint16 PetFastHeals = 48; + constexpr uint16 PetVeryFastHeals = 49; + constexpr uint16 PetHoTHeals = 50; + constexpr uint16 DamageShields = 51; + constexpr uint16 ResistBuffs = 52; + + constexpr uint16 START = BotSpellTypes::Nuke; // Do not remove or change this + constexpr uint16 END = BotSpellTypes::ResistBuffs; // Do not remove this, increment as needed +} 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); +bool BOT_SPELL_TYPES_DETRIMENTAL (uint16 spellType, uint8 cls = 0); +bool BOT_SPELL_TYPES_BENEFICIAL (uint16 spellType, uint8 cls = 0); +bool BOT_SPELL_TYPES_OTHER_BENEFICIAL(uint16 spellType); +bool BOT_SPELL_TYPES_INNATE (uint16 spellType); +bool IsBotSpellType (uint16 spellType); +bool IsAEBotSpellType(uint16 spellType); +bool IsGroupBotSpellType(uint16 spellType); +bool IsGroupTargetOnlyBotSpellType(uint16 spellType); +bool IsPetBotSpellType(uint16 spellType); +bool IsClientBotSpellType(uint16 spellType); +bool IsHealBotSpellType(uint16 spellType); +bool SpellTypeRequiresLoS(uint16 spellType, uint16 cls = 0); +bool SpellTypeRequiresTarget(uint16 spellType, uint16 cls = 0); + // These should not be used to determine spell category.. // They are a graphical affects (effects?) index only // TODO: import sai list @@ -1503,6 +1577,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 +1611,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 +1625,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 hasLoS = 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 +1668,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 +1717,10 @@ 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 spellType); #endif diff --git a/zone/aggro.cpp b/zone/aggro.cpp index 1bc7efa76..1a8c7b929 100644 --- a/zone/aggro.cpp +++ b/zone/aggro.cpp @@ -1278,6 +1278,39 @@ bool Mob::CheckLosFN(glm::vec3 posWatcher, float sizeWatcher, glm::vec3 posTarge return zone->zonemap->CheckLoS(posWatcher, posTarget); } +bool Mob::CheckPositioningLosFN(Mob* other, float posX, float posY, float posZ) { + if (zone->zonemap == nullptr) { + //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 = posX; + myloc.y = posY; + myloc.z = posZ + (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) { @@ -1659,3 +1692,89 @@ void Mob::RogueEvade(Mob *other) return; } +bool Mob::DoLosChecks(Mob* who, Mob* other) { + if (!who->CheckLosFN(other) || !who->CheckWaterLoS(other)) { + if (who->CheckLosCheatExempt(who, other)) { + return true; + } + + if (CheckLosCheat(who, other)) { + return true; + } + + return false; + } + + return true; +} + +bool Mob::CheckLosCheat(Mob* who, Mob* other) { + if (RuleB(Map, CheckForLoSCheat)) { + auto& door_list = entity_list.GetDoorsList(); + for (auto itr : door_list) { + Doors* door = itr.second; + if (door && !door->IsDoorOpen() && (door->GetTriggerType() == 255 || door->GetLockpick() != 0 || door->GetKeyItem() != 0 || door->GetNoKeyring() != 0)) { + if (DistanceNoZ(who->GetPosition(), door->GetPosition()) <= 50) { + auto who_to_door = DistanceNoZ(who->GetPosition(), door->GetPosition()); + auto other_to_door = DistanceNoZ(other->GetPosition(), door->GetPosition()); + auto who_to_other = DistanceNoZ(who->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)) { + LogTestDebug("CheckLosCheat failed at Door [{}], TriggerType [{}], GetLockpick [{}], GetKeyItem [{}], GetNoKeyring [{}]", door->GetDoorID(), door->GetTriggerType(), door->GetLockpick(), door->GetKeyItem(), door->GetNoKeyring()); //deleteme + return false; + } + } + } + } + } + + //if (RuleB(Map, CheckForLoSCheat)) { + // uint8 zone_id = zone->GetZoneID(); + // // ZoneID, target XYZ, my range from target + // //float zone_basic_checks[] = { 6, 36 }; + // //float zone_basic_x_coord[] = { -295, -179.908 }; + // //float zone_basic_y_coord[] = { -18, -630.708 }; + // //float zone_basic_y_coord[] = { 50.97, -69.971 }; + // //float zone_basic_range_check[] = { 21, 10 }; + // //if door and target infront, fail + // //if door and target behind, fail + // + // if (zone_id == 103) { + // Doors* door_to_check = entity_list.FindDoor(8); + // TestDebug("Entered LoSCheat for ZoneID: [{}]", zone_id); //deleteme + // glm::vec4 who_check; who_check.x = 1202; who_check.y = 559; who_check.z = -158.94; + // glm::vec4 other_check; other_check.x = 1291; other_check.y = 559; other_check.z = -158.19; + // float my_distance = DistanceNoZ(who->GetPosition(), who_check); + // float tar_distance = DistanceNoZ(other->GetPosition(), other_check); + // float my_range = 16; + // float tar_range = 75; + // if (my_distance <= my_range && tar_distance <= tar_range && !quest_manager.isdooropen(8)) { + // TestDebug("Door is NOT open"); //deleteme + // TestDebug("LoSCheat failed"); //deleteme + // return false; + // } + // TestDebug("LoS Check for ZoneID: [{}] was [{}] units for [{}], [{}] units for [{}]", zone_id, my_distance, who->GetCleanName(), tar_distance, other->GetCleanName()); //deleteme + // } + //} + return true; +} + +bool Mob::CheckLosCheatExempt(Mob* who, Mob* other) { + if (RuleB(Map, EnableLoSCheatExemptions)) { + glm::vec4 exempt_check_who; + glm::vec4 exempt_check_other; + /* This is an exmaple of how to configure exemptions for LoS checks. + if (zone->GetZoneID() == 222) { //PoEarthB + exempt_check_who.x = 2051; exempt_check_who.y = 407; exempt_check_who.z = -219; //Middle of councilman spawns + //check to be sure the player and the target are in the pit to PoEarthB + //if the player is inside the cove they cannot be higher than the ceiling (no exploiting from uptop) + //otherwise they can pass LoS checks even if they don't have true LoS + if (who->GetZ() <= -171 && other->GetZ() <= -171 && DistanceNoZ(other->GetPosition(), exempt_check_who) <= 800 && DistanceNoZ(who->GetPosition(), exempt_check_who) <= 800) { + return true; + } + } + */ + } + + return false; +} diff --git a/zone/attack.cpp b/zone/attack.cpp index e2d7cdf17..0d8e420ca 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; } diff --git a/zone/bot.cpp b/zone/bot.cpp index aa746e767..0b7fd0cc8 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -69,23 +69,20 @@ 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(); + auto_save_timer.Disable(); + m_rogue_evade_timer.Disable(); + m_monk_evade_timer.Disable(); m_auto_defend_timer.Disable(); SetGuardFlag(false); SetHoldFlag(false); @@ -98,12 +95,12 @@ 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); + ping_timer.Disable(); + + LoadDefaultBotSettings(); + SetCastedSpellType(UINT16_MAX); + SetCommandedSpell(false); + //DisableBotSpellTimers(); // Do this once and only in this constructor GenerateAppearance(); @@ -131,8 +128,7 @@ 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) { @@ -175,13 +171,12 @@ 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); bool stance_flag = false; if (!database.botdb.LoadStance(this, stance_flag) && bot_owner) { @@ -207,6 +202,10 @@ Bot::Bot( SetTaunting((GetClass() == Class::Warrior || GetClass() == Class::Paladin || GetClass() == Class::ShadowKnight) && (GetBotStance() == Stance::Aggressive)); SetPauseAI(false); + m_combat_jitter_timer.Disable(); + auto_save_timer.Disable(); + m_rogue_evade_timer.Disable(); + m_monk_evade_timer.Disable(); m_auto_defend_timer.Disable(); SetGuardFlag(false); SetHoldFlag(false); @@ -220,11 +219,6 @@ Bot::Bot( rest_timer.Disable(); ping_timer.Disable(); - SetFollowDistance(BOT_FOLLOW_DISTANCE_DEFAULT); - if (IsCasterClass(GetClass())) - SetStopMeleeLevel((uint8)RuleI(Bots, CasterStopMeleeLevel)); - else - SetStopMeleeLevel(255); strcpy(name, GetCleanName()); @@ -235,7 +229,10 @@ 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; @@ -251,6 +248,10 @@ Bot::Bot( LoadAAs(); + LoadDefaultBotSettings(); + + database.botdb.LoadBotSettings(this); + if (database.botdb.LoadBuffs(this)) { //reapply some buffs uint32 buff_count = GetMaxBuffSlots(); @@ -269,6 +270,10 @@ Bot::Bot( switch (spell.effect_id[x1]) { case SE_IllusionCopy: case SE_Illusion: { + if (GetIllusionBlock()) { + break; + } + if (spell.base_value[x1] == -1) { if (gender == Gender::Female) { gender = Gender::Male; @@ -538,39 +543,51 @@ 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) - return 0; + const EQ::ItemData *ammo_item = nullptr; + + if (ammo_inst) { + ammo_item = ammo_inst->GetItem(); + } - // everything is good! - return (range_item->Range + ammo_item->Range); + // Bow requires arrows + if (range_item && range_item->ItemType == EQ::item::ItemTypeBow && (!ammo_item || ammo_item->ItemType != EQ::item::ItemTypeArrow)) { + return 0; + } + + // Throwing items + if (range_item && (range_item->ItemType == EQ::item::ItemTypeSmallThrowing || range_item->ItemType == EQ::item::ItemTypeLargeThrowing)) { + return range_item->Range; + } + + // Bows and arrows + if (range_item && ammo_item && range_item->ItemType == EQ::item::ItemTypeBow && ammo_item->ItemType == EQ::item::ItemTypeArrow) { + return (range_item->Range + ammo_item->Range); + } + + 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"); - } +void Bot::ChangeBotRangedWeapons(bool isRanged) { + if (!isRanged) { + 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"); //TODO bot rewrite - make this say throwing or bow } - else - BotGroupSay(this, "I don't know how to use a bow"); } void Bot::Sit() { @@ -1304,10 +1321,13 @@ bool Bot::IsValidName() bool Bot::IsValidName(std::string& name) { - if (name.length() < 4) + if (name.length() < 4 || name.length() > 15) { return false; - if (!isupper(name[0])) + } + + if (!isupper(name[0])) { return false; + } for (char c : name.substr(1)) { if (!RuleB(Bots, AllowCamelCaseNames) && !islower(c)) { @@ -1344,6 +1364,7 @@ bool Bot::Save() database.botdb.SaveBuffs(this); database.botdb.SaveTimers(this); database.botdb.SaveStance(this); + database.botdb.SaveBotSettings(this); if (!SavePet()) bot_owner->Message(Chat::White, "Failed to save pet for '%s'", GetCleanName()); @@ -1389,24 +1410,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.DeleteBot(GetBotID())) { - return false; + if (!database.botdb.DeleteBotSettings(GetBotID())) { + return false; + } + + if (!database.botdb.DeleteBot(GetBotID())) { + return false; + } } return true; @@ -1708,94 +1735,137 @@ 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(); - } + if (GetManaRatio() < GetManaWhenToMed() || (GetHPRatio() < GetHPWhenToMed() && GetLevel() < GetStopMeleeLevel())) { + if ((!IsEngaged() || (IsEngaged() && GetMedInCombat() && !HasTargetReflection())) && !isSitting) { + Sit(); } - } else { - if (IsSitting()) { + } + else { + if (isSitting) { 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()); +void Bot::BotRangedAttack(Mob* other, bool CanDoubleAttack) { + if (!TargetValidation(other)) { return; } + + if (!CanDoubleAttack && ((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()); Message(0, "Error: Timer not up. Attack %d, ranged %d", attack_timer.GetRemainingTime(), ranged_timer.GetRemainingTime()); return; } const auto rangedItem = GetBotItem(EQ::invslot::slotRange); const EQ::ItemData* RangeWeapon = nullptr; - if (rangedItem) + if (rangedItem) { RangeWeapon = rangedItem->GetItem(); + } + + if (!RangeWeapon) { + return; + } const auto ammoItem = GetBotItem(EQ::invslot::slotAmmo); const EQ::ItemData* Ammo = nullptr; if (ammoItem) Ammo = ammoItem->GetItem(); - if (!RangeWeapon || !Ammo) + // Bow requires arrows + if ( + !Ammo || + (RangeWeapon && + ( + (RangeWeapon->ItemType != EQ::item::ItemTypeBow && RangeWeapon->ItemType != EQ::item::ItemTypeSmallThrowing && RangeWeapon->ItemType != EQ::item::ItemTypeLargeThrowing) || + (RangeWeapon->ItemType == EQ::item::ItemTypeBow && (Ammo->ItemType != EQ::item::ItemTypeArrow)) || + ( + (RangeWeapon->ItemType == EQ::item::ItemTypeSmallThrowing || RangeWeapon->ItemType == EQ::item::ItemTypeLargeThrowing) && + ammoItem->GetCharges() < 1 || + ( + (RuleI(Bots, StackSizeMin) != -1 && rangedItem->GetCharges() != RangeWeapon->StackSize) || + rangedItem->GetCharges() < RuleI(Bots, StackSizeMin) + ) + ) + ) + ) + ) { + if (!Ammo || ammoItem->GetCharges() < 1) { + GetOwner()->Message(Chat::Yellow, "I do not have enough any 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)) + LogCombatDetail("Ranged attacking [{}] with {} [{}] ([{}]){}{}{}", + other->GetCleanName(), + (RangeWeapon->ItemType == EQ::item::ItemTypeBow ? "bow" : "throwing"), + RangeWeapon->Name, + RangeWeapon->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) : "") + ); + + if (!IsAttackAllowed(other) || IsCasting() || DivineAura() || IsStunned() || IsMezzed() || (GetAppearance() == eaDead)) { return; - - SendItemAnimation(other, Ammo, EQ::skills::SkillArchery); - DoArcheryAttackDmg(other, rangedItem, ammoItem); // watch - - //break invis when you attack - if (invisible) { - LogCombatDetail("Removing invisibility due to melee attack"); - BuffFadeByEffect(SE_Invisibility); - BuffFadeByEffect(SE_Invisibility2); - invisible = false; } - if (invisible_undead) { - LogCombatDetail("Removing invisibility vs. undead due to melee attack"); - BuffFadeByEffect(SE_InvisVsUndead); - BuffFadeByEffect(SE_InvisVsUndead2); - invisible_undead = false; + SendItemAnimation(other, Ammo, (RangeWeapon->ItemType == EQ::item::ItemTypeBow ? EQ::skills::SkillArchery : EQ::skills::SkillThrowing)); + if (RangeWeapon->ItemType == EQ::item::ItemTypeBow) { + DoArcheryAttackDmg(other, rangedItem, ammoItem); // watch + //EndlessQuiver AA base1 = 100% Chance to avoid consumption arrow. + int ChanceAvoidConsume = aabonuses.ConsumeProjectile + itembonuses.ConsumeProjectile + spellbonuses.ConsumeProjectile; + + // Consume Ammo, unless Ammo Consumption is disabled or player has Endless Quiver + bool consumes_ammo = RuleB(Bots, BotArcheryConsumesAmmo); + if ( + consumes_ammo && + ( + RangeWeapon->ExpendableArrow || + !ChanceAvoidConsume || + (ChanceAvoidConsume < 100 && zone->random.Int(0, 99) > ChanceAvoidConsume) + ) + ) { + ammoItem->SetCharges((ammoItem->GetCharges() - 1)); + LogCombat("Consumed Archery Ammo from slot {}.", EQ::invslot::slotAmmo); + + if (ammoItem->GetCharges() < 1) { + RemoveBotItemBySlot(EQ::invslot::slotAmmo); + BotRemoveEquipItem(EQ::invslot::slotAmmo); + } + } + else if (!consumes_ammo) { + LogCombat("Archery Ammo Consumption is disabled."); + } + else { + LogCombat("Endless Quiver prevented Ammo Consumption."); + } + } + else { + DoThrowingAttackDmg(other, rangedItem); // watch + // Consume Ammo, unless Ammo Consumption is disabled + if (RuleB(Bots, BotThrowingConsumesAmmo)) { + ammoItem->SetCharges((ammoItem->GetCharges() - 1)); + LogCombat("Consumed Throwing Ammo from slot {}.", EQ::invslot::slotAmmo); + + if (ammoItem->GetCharges() < 1) { + RemoveBotItemBySlot(EQ::invslot::slotAmmo); + BotRemoveEquipItem(EQ::invslot::slotAmmo); + } + } + else { + LogCombat("Throwing Ammo Consumption is disabled."); + } } - if (invisible_animals) { - LogCombatDetail("Removing invisibility vs. animals due to melee attack"); - BuffFadeByEffect(SE_InvisVsAnimals); - invisible_animals = false; - } - - if (spellbonuses.NegateIfCombat) - BuffFadeByEffect(SE_NegateIfCombat); - - 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); - } + CommonBreakInvisibleFromCombat(); } bool Bot::CheckBotDoubleAttack(bool tripleAttack) { @@ -1832,6 +1902,53 @@ bool Bot::CheckBotDoubleAttack(bool tripleAttack) { return false; } +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) { @@ -1839,13 +1956,6 @@ void Bot::SetTarget(Mob *mob) } } -void Bot::SetStopMeleeLevel(uint8 level) { - if (IsCasterClass(GetClass()) || IsHybridClass(GetClass())) - _stopMeleeLevel = level; - else - _stopMeleeLevel = 255; -} - void Bot::SetGuardMode() { StopMoving(); @@ -1864,25 +1974,6 @@ void Bot::SetHoldMode() { // AI Processing for the Bot object -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 -}; - void Bot::AI_Process() { @@ -1940,6 +2031,13 @@ void Bot::AI_Process() return; } + if (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); @@ -1951,7 +2049,6 @@ void Bot::AI_Process() } // HEAL ROTATION CASTING CHECKS - HealRotationChecks(); if (GetAttackFlag()) { // Push owner's target onto our hate list @@ -1962,12 +2059,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(); } @@ -1991,7 +2086,6 @@ void Bot::AI_Process() // DEFAULT (ACQUIRE TARGET) // VERIFY TARGET AND STANCE - auto tar = GetBotTarget(bot_owner); if (!tar) { return; @@ -2006,7 +2100,6 @@ 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)) { return; } @@ -2026,76 +2119,136 @@ void Bot::AI_Process() SendAddPlayerState(PlayerState::Aggressive); } +// COMBAT RANGE CALCS + + bool atCombatRange = false; + bool behindMob = false; + uint8 stopMeleeLevel = GetStopMeleeLevel(); + const EQ::ItemInstance* p_item; + const EQ::ItemInstance* s_item; + float melee_distance_min = 0.0f; + float melee_distance_max = 0.0f; + float melee_distance = 0.0f; + + CheckCombatRange(tar, sqrt(tar_distance), atCombatRange, behindMob, p_item, s_item, melee_distance_min, melee_distance_max, melee_distance, stopMeleeLevel); + // PULLING FLAG (ACTIONABLE RANGE) if (GetPullingFlag()) { - - constexpr size_t PULL_AGGRO = 5225; // spells[5225]: 'Throw Stone' - 0 cast time - - if (tar_distance <= (spells[PULL_AGGRO].range * spells[PULL_AGGRO].range)) { - - StopMoving(); - CastSpell(PULL_AGGRO, tar->GetID()); + //TODO bot rewrite - add ways to here to determine if throw stone is allowed, then check for ranged/spell/melee + if (!IsBotNonSpellFighter() && !HOLDING && AI_HasSpells() && AI_EngagedCastCheck()) { return; } + + if (RuleB(Bots, AllowSpellPulling)) { + uint16 pullSpell = RuleI(Bots, PullSpellID); + + if (tar_distance <= (spells[pullSpell].range * spells[pullSpell].range)) { + StopMoving(); + + if (!TargetValidation(tar)) { return; } + + CastSpell(pullSpell, tar->GetID()); + + return; + } + } + else { + if (atCombatRange && IsBotRanged()){ + StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); + + TryRangedAttack(tar); + + if (!TargetValidation(tar)) { return; } + + if (CheckDoubleRangedAttack()) { + BotRangedAttack(tar, true); + } + + return; + } + } } -// COMBAT RANGE CALCS - - bool atCombatRange; - const EQ::ItemInstance* p_item; - const EQ::ItemInstance* s_item; - CheckCombatRange(tar, tar_distance, atCombatRange, p_item, s_item); - // ENGAGED AT COMBAT RANGE // We can fight if (atCombatRange) { + bool jitterCooldown = false; + + if (m_combat_jitter_timer.GetRemainingTime() > 1 && m_combat_jitter_timer.Enabled()) { + jitterCooldown = true; + } + + if (IsMoving() || GetCombatJitterFlag() || GetCombatOutOfRangeJitterFlag()) { + if (!GetCombatJitterFlag() || !IsMoving() || GetCombatOutOfRangeJitterFlag()) { + StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); + } + + return; + } + + if (!jitterCooldown && AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == Class::Bard)) { + DoCombatPositioning(tar, Goal, stopMeleeLevel, tar_distance, melee_distance_min, melee_distance, melee_distance_max, behindMob); + return; + } + else { + if (!IsSitting() && !IsFacingMob(tar)) { + FaceTarget(tar); + return; + } + } + + if (!IsBotNonSpellFighter() && !HOLDING && AI_HasSpells() && AI_EngagedCastCheck()) { + return; + } if (IsMoving()) { StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); return; } - if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == Class::Bard)) { + if (IsBotRanged() && ranged_timer.Check(false)) { // Can shoot mezzed, stunned and dead!? + TryRangedAttack(tar); - if (TryEvade(tar)) { - return; - } + if (!TargetValidation(tar)) { return; } - if (TryFacingTarget(tar)) { - return; + if (CheckDoubleRangedAttack()) { + BotRangedAttack(tar, true); } } - - if (!IsBotNonSpellFighter() && 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)) { - return; - } - - if (!IsBotArcher() && GetLevel() < GetStopMeleeLevel()) { - if (!TryClassAttacks(tar)) { + else if (!IsBotRanged() && GetLevel() < stopMeleeLevel) { + if (tar->IsEnraged() && !BehindMob(tar, GetX(), GetY())) { return; } - if (!TryPrimaryWeaponAttacks(tar, p_item)) { - return; + 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 (!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; } - if (GetAppearance() == eaDead) { - return; } } @@ -2107,12 +2260,9 @@ void Bot::AI_Process() // End not in combat range - if (TryMeditate()) { - return; - } + TryMeditate(); } else { // Out-of-combat behavior - SetAttackFlag(false); SetAttackingFlag(false); if (!bot_owner->GetBotPulling()) { @@ -2145,16 +2295,19 @@ void Bot::AI_Process() if (TryNonCombatMovementChecks(bot_owner, follow_mob, Goal)) { return; } - if (TryIdleChecks(fm_distance)) { + if (!HOLDING && AI_HasSpells() && TryIdleChecks(fm_distance)) { return; } - if (TryBardMovementCasts()) { + if (!HOLDING && 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 +2318,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(); @@ -2196,6 +2348,7 @@ bool Bot::TryNonCombatMovementChecks(Client* bot_owner, const Mob* follow_mob, g } } } + return false; } @@ -2210,18 +2363,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; @@ -2283,30 +2428,35 @@ bool Bot::TryAutoDefend(Client* bot_owner, float leash_distance) { 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); @@ -2316,8 +2466,8 @@ bool Bot::TryPursueTarget(float leash_distance, glm::vec3& Goal) { GetPet()->SetTarget(nullptr); } } - return true; + return true; } else { if (IsMoving()) { StopMoving(); @@ -2333,156 +2483,108 @@ 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 && 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 (CastToClient()->CheckDoubleAttack()) { + Attack(target, hand, false, false); + + if (hand == EQ::invslot::slotPrimary) { + + if (HasTwoHanderEquipped()) { + auto extraattackchance = aabonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE] + spellbonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE] + + itembonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_CHANCE]; + if (extraattackchance && zone->random.Roll(extraattackchance)) { + auto extraattackamt = std::max({ aabonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_NUM_ATKS], spellbonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_NUM_ATKS], itembonuses.ExtraAttackChance[SBIndex::EXTRA_ATTACK_NUM_ATKS] }); + for (int i = 0; i < extraattackamt; i++) { + Attack(target, hand, false, false); + } + } + } + else { + auto extraattackchance_primary = aabonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_CHANCE] + spellbonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_CHANCE] + + itembonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_CHANCE]; + if (extraattackchance_primary && zone->random.Roll(extraattackchance_primary)) { + auto extraattackamt_primary = std::max({ aabonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_NUM_ATKS], spellbonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_NUM_ATKS], itembonuses.ExtraAttackChancePrimary[SBIndex::EXTRA_ATTACK_NUM_ATKS] }); + for (int i = 0; i < extraattackamt_primary; i++) { + Attack(target, hand, false, false); + } + } + } } - if (!s_itemdata) { - return false; + if (hand == EQ::invslot::slotSecondary) { + auto extraattackchance_secondary = aabonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_CHANCE] + spellbonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_CHANCE] + + itembonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_CHANCE]; + if (extraattackchance_secondary && zone->random.Roll(extraattackchance_secondary)) { + auto extraattackamt_secondary = std::max({ aabonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_NUM_ATKS], spellbonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_NUM_ATKS], itembonuses.ExtraAttackChanceSecondary[SBIndex::EXTRA_ATTACK_NUM_ATKS] }); + for (int i = 0; i < extraattackamt_secondary; i++) { + Attack(target, hand, false, false); + } + } } - bool use_fist = true; - if (s_itemdata) { - use_fist = false; - } + // you can only triple from the main hand + if (hand == EQ::invslot::slotPrimary && CanThisClassTripleAttack()) { + if (CheckTripleAttack()) { + Attack(target, hand, false, false); + int flurry_chance = aabonuses.FlurryChance + spellbonuses.FlurryChance + + itembonuses.FlurryChance; - if (use_fist || !s_itemdata->IsType2HWeapon()) { + if (flurry_chance && zone->random.Roll(flurry_chance)) { + Attack(target, hand, false, false); - float DualWieldProbability = 0.0f; - - int32 Ambidexterity = (aabonuses.Ambidexterity + spellbonuses.Ambidexterity + itembonuses.Ambidexterity); - DualWieldProbability = ((GetSkill(EQ::skills::SkillDualWield) + GetLevel() + Ambidexterity) / 400.0f); // 78.0 max chance - - int32 DWBonus = (spellbonuses.DualWieldChance + itembonuses.DualWieldChance); - DualWieldProbability += (DualWieldProbability * float(DWBonus) / 100.0f); - - float random = zone->random.Real(0, 1); - if (random < DualWieldProbability) { // Max 78% for DW chance - Attack(tar, EQ::invslot::slotSecondary); // Single attack with offhand - - 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 (zone->random.Roll(flurry_chance)) { + Attack(target, hand, false, false); + } + //MessageString(Chat::NPCFlurry, YOU_FLURRY); //TODO bot rewrite - add output to others hits with flurry message } } } } } - 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)) { - - Attack(tar, EQ::invslot::slotPrimary, true); - } - - if (GetAppearance() == eaDead) { return false; } - // quad attack, does this belong here?? - if (GetSpecialAbility(SpecialAbility::QuadrupleAttack) && CheckBotDoubleAttack(true)) { - Attack(tar, EQ::invslot::slotPrimary, true); - } - } - - 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)) { - - if (zone->random.Int(0, 100) < flurrychance) { - - MessageString(Chat::NPCFlurry, YOU_FLURRY); - Attack(tar, EQ::invslot::slotPrimary, false); - - if (GetAppearance() == eaDead) { return false; } - Attack(tar, EQ::invslot::slotPrimary, false); - } - } - - 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 (IsBotRanged() && ranged_timer.Check(false)) { - if (!GetTarget() || GetAppearance() == eaDead) { return false; } - if (GetTarget()->GetHPRatio() <= 99.0f) { + if (!TargetValidation(tar)) { return false; } + + if (GetPullingFlag() || GetTarget()->GetHPRatio() <= 99.0f) { BotRangedAttack(tar); } + return true; } + return false; } bool Bot::TryFacingTarget(Mob* tar) { - if (!IsSitting() && !IsFacingMob(tar)) { FaceTarget(tar); return true; @@ -2492,93 +2594,95 @@ bool Bot::TryFacingTarget(Mob* tar) { bool Bot::TryEvade(Mob* tar) { + if (HasTargetReflection() && !tar->IsFeared() && !tar->IsStunned()) { + if (GetClass() == Class::Rogue && !GetSpellHold(BotSpellTypes::Escape)) { + if (m_rogue_evade_timer.Check(false)) { + int timer_duration = (HideReuseTime - GetSkillReuseTime(EQ::skills::SkillHide)) * 1000; - if ( - !IsRooted() && - HasTargetReflection() && - !tar->IsFeared() && - !tar->IsStunned() && - GetClass() == Class::Rogue && - m_evade_timer.Check(false) - ) { - int timer_duration = (HideReuseTime - GetSkillReuseTime(EQ::skills::SkillHide)) * 1000; + if (timer_duration < 0) { + timer_duration = 0; + } - if (timer_duration < 0) { - timer_duration = 0; + m_rogue_evade_timer.Start(timer_duration); + BotGroupSay(this, "Attempting to evade %s", tar->GetCleanName()); + + if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillHide)) { + //SendAppearancePacket(AT_Invis, Invisibility::Invisible); + RogueEvade(tar); + } + + //SendAppearancePacket(AT_Invis, Invisibility::Visible); + return true; + } } + else if (GetClass() == Class::Monk && GetLevel() >= 17 && !GetSpellHold(BotSpellTypes::Escape)) { + if (m_monk_evade_timer.Check(false)) { + int timer_duration = (FeignDeathReuseTime - GetSkillReuseTime(EQ::skills::SkillFeignDeath)) * 1000; - m_evade_timer.Start(timer_duration); - if (zone->random.Int(0, 260) < (int) GetSkill(EQ::skills::SkillHide)) { - RogueEvade(tar); + if (timer_duration < 0) { + timer_duration = 0; + } + + m_monk_evade_timer.Start(timer_duration); + BotGroupSay(this, "Attempting to evade %s", tar->GetCleanName()); + + if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillFeignDeath)) { + //SendAppearancePacket(AT_Anim, ANIM_DEATH); + entity_list.MessageCloseString(this, false, 200, 10, STRING_FEIGNFAILED, GetName()); + } + else { + SetFeigned(true); + //SendAppearancePacket(AT_Anim, ANIM_DEATH); + } + + //SendAppearancePacket(AT_Anim, ANIM_STAND); + SetFeigned(false); + return true; + } } - return true; } return false; } -void Bot::CheckCombatRange(Mob* tar, float tar_distance, bool& atCombatRange, const EQ::ItemInstance*& p_item, const EQ::ItemInstance*& s_item) { - +void Bot::CheckCombatRange(Mob* tar, float tar_distance, bool& atCombatRange, bool& behindMob, const EQ::ItemInstance*& p_item, const EQ::ItemInstance*& s_item, float& melee_distance_min, float& melee_distance_max, float& melee_distance, uint8 stopMeleeLevel) { 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) { - behind_mob = BehindMob(tar, GetX(), GetY()); // Can be separated for other future use - backstab_weapon = p_item && p_item->GetItemBackstabDamage(); + p_item = GetBotItem(EQ::invslot::slotPrimary); + s_item = GetBotItem(EQ::invslot::slotSecondary); + + bool backstab_weapon = false; + + if (GetBehindMob()) { + behindMob = BehindMob(tar, GetX(), GetY()); // Can be separated for other future use + if (GetClass() == Class::Rogue) { + backstab_weapon = p_item && p_item->GetItemBackstabDamage(); + } } // Calculate melee distances - float melee_distance_max = 0.0f; - float melee_distance = 0.0f; + CalcMeleeDistances(tar, p_item, s_item, behindMob, backstab_weapon, melee_distance_max, melee_distance, melee_distance_min, stopMeleeLevel); - CalcMeleeDistances(tar, p_item, s_item, behind_mob, backstab_weapon, melee_distance_max, melee_distance); + //LogTestDebugDetail("{} is {} {}. They are currently {} away {} to be {} [{} - {}] away." + // , GetCleanName() + // , (tar_distance <= melee_distance ? "within range of" : "too far away from") + // , tar->GetCleanName() + // , tar_distance + // , (tar_distance <= melee_distance ? "but only needed" : "but need to be") + // , melee_distance + // , melee_distance_min + // , melee_distance_max + //); //deleteme - float caster_distance_max = GetBotCasterMaxRange(melee_distance_max); - - bool atArcheryRange = IsArcheryRange(tar); - - SetRangerCombatWeapon(atArcheryRange); - - 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) { + if (tar_distance <= melee_distance) { atCombatRange = true; } } -void Bot::SetRangerCombatWeapon(bool atArcheryRange) { - - if (GetRangerAutoWeaponSelect()) { - bool changeWeapons = false; - - if (atArcheryRange && !IsBotArcher()) { - SetBotArcherySetting(true); - changeWeapons = true; - } - - else if (!atArcheryRange && IsBotArcher()) { - SetBotArcherySetting(false); - changeWeapons = true; - } - - if (changeWeapons) { - ChangeBotArcherWeapons(IsBotArcher()); - } - } -} - -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 { - +void Bot::CalcMeleeDistances(const Mob* tar, const EQ::ItemInstance* const& p_item, const EQ::ItemInstance* const& s_item, bool behindMob, bool backstab_weapon, float& melee_distance_max, float& melee_distance, float& melee_distance_min, uint8 stopMeleeLevel) { float size_mod = GetSize(); float other_size_mod = tar->GetSize(); + bool resquareDistance = false; // For races with a fixed size if (GetRace() == Race::LavaDragon || GetRace() == Race::Wurm || GetRace() == Race::GhostDragon) { @@ -2614,58 +2718,115 @@ void Bot::CalcMeleeDistances(const Mob* tar, const EQ::ItemInstance* const& p_it size_mod *= (size_mod * 4.0f); } + if (tar->GetRace() == Race::VeliousDragon) // Lord Vyemm and other velious dragons + { + size_mod *= 1.75; + } + if (tar->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; + if (!RuleB(Bots, UseFlatNormalMeleeRange)) { + 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 (behindMob && 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; + } - 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; + break; } - break; + melee_distance = sqrt(melee_distance); + melee_distance_max = sqrt(melee_distance_max); + } + else { + melee_distance_max = sqrt(melee_distance_max); + melee_distance = melee_distance_max * RuleR(Bots, NormalMeleeRangeDistance); + } + + if (melee_distance > RuleR(Bots, MaxDistanceForMelee)) { + melee_distance = RuleR(Bots, MaxDistanceForMelee); + } + + melee_distance_min = melee_distance_max * RuleR(Bots, PercentMinMeleeDistance); + + if (taunting) { + melee_distance_min = melee_distance_max * RuleR(Bots, PercentTauntMinMeleeDistance); + melee_distance = melee_distance_max * RuleR(Bots, TauntNormalMeleeRangeDistance); + } + + if (!taunting && !IsBotRanged() && GetMaxMeleeRange()) { + melee_distance = melee_distance_max * RuleR(Bots, PercentMaxMeleeRangeDistance); + melee_distance_min = melee_distance_max * RuleR(Bots, PercentMinMaxMeleeRangeDistance); + } + + + /* Caster Range Checks */ + bool isStopMeleeLevel = GetLevel() >= stopMeleeLevel; + if (isStopMeleeLevel) { + melee_distance = GetBotCasterMaxRange(melee_distance_max); + if (RuleB(Bots, CastersStayJustOutOfMeleeRange)) { + melee_distance_min = melee_distance_max + 1; + } + else { + melee_distance_min = melee_distance_max * RuleR(Bots, PercentMinCasterRangeDistance); + } + } + + /* Archer Checks*/ + if (IsBotRanged()) { + float archeryRange = GetBotRangedValue(); + float casterRange = GetBotCasterRange(); + float minArcheryRange = RuleI(Combat, MinRangedAttackDist); + melee_distance = std::min(archeryRange, (casterRange * 2)); + melee_distance_min = std::max(std::max(minArcheryRange, (melee_distance_max + 1)), std::min(casterRange, archeryRange)); } } @@ -2685,10 +2846,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(this, tar) && !CheckLosCheat(leash_owner, tar)) || !IsAttackAllowed(tar) ) { invalid_target_state = true; @@ -2753,9 +2915,7 @@ Mob* Bot::GetBotTarget(Client* bot_owner) } } - if (GetArchetype() == Archetype::Caster) { - BotMeditate(true); - } + BotMeditate(IsSitting()); } return t; @@ -2915,13 +3075,11 @@ bool Bot::CheckIfIncapacitated() { 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; } @@ -2969,10 +3127,9 @@ 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); @@ -2999,7 +3156,7 @@ 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( @@ -3020,8 +3177,9 @@ void Bot::BotPullerProcess(Client* bot_owner, Raid* raid) { 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); } } @@ -3108,6 +3266,22 @@ bool Bot::Spawn(Client* botCharacterOwner) { SentPositionPacket(0.0f, 0.0f, 0.0f, 0.0f, 0); ping_timer.Start(8000); auto_save_timer.Start(RuleI(Bots, AutosaveIntervalSeconds) * 1000); + + pDontHealMeBefore = 0; + pDontGroupHealMeBefore = 0; + pDontGroupHoTHealMeBefore = 0; + pDontBuffMeBefore = Timer::GetCurrentTime() + 400; + pDontDotMeBefore = 0; + pDontRootMeBefore = 0; + pDontSnareMeBefore = 0; + pDontCureMeBefore = 0; + pDontRegularHealMeBefore = 0; + pDontVeryFastHealMeBefore = 0; + pDontFastHealMeBefore = 0; + pDontCompleteHealMeBefore = 0; + pDontGroupCompleteHealMeBefore = 0; + pDontHotHealMeBefore = 0; + // there is something askew with spawn struct appearance fields... // I re-enabled this until I can sort it out const auto& m = GetBotItemSlots(); @@ -3144,6 +3318,11 @@ bool Bot::Spawn(Client* botCharacterOwner) { } } + if (RuleB(Bots, RunSpellTypeChecksOnSpawn)) { + OwnerMessage("Running SpellType checks. There may be some spells that are flagged as incorrect but actually are correct. Use this as a 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 +3606,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) { @@ -3824,23 +4002,37 @@ 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 { - client->Message( - Chat::Yellow, - fmt::format( - "{} is only a partially stacked item, the trade has been cancelled!", - item_link - ).c_str() - ); - client->ResetTrade(); - return; + 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; + } + else { + client->Message( + Chat::Yellow, + fmt::format( + "{} 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); @@ -4765,8 +4957,9 @@ void Bot::RogueAssassinate(Mob* other) { } 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); @@ -4834,58 +5027,58 @@ void Bot::DoClassAttacks(Mob *target, bool IsRiposte) { if (ma_time) { switch (GetClass()) { - case Class::Monk: { - int reuse = (MonkSpecialAttack(target, EQ::skills::SkillTigerClaw) - 1); + 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; + // 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; + 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; } - else { - break; + + 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 + ); + } + + auto classic = RuleB(Combat, ClassicMasterWu); + while (extra) { + MonkSpecialAttack(GetTarget(), (classic ? MonkSPA[zone->random.Int(0, 4)] : EQ::skills::SkillTigerClaw)); + --extra; } - wuchance /= 4; } - Mob* bo = GetBotOwner(); - if (bo && bo->IsClient() && bo->CastToClient()->GetBotOption(Client::booMonkWuMessage)) { + float HasteModifier = (GetHaste() * 0.01f); + monkattack_timer.Start((reuse * 1000) / HasteModifier); - 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; - } + break; } - - float HasteModifier = (GetHaste() * 0.01f); - monkattack_timer.Start((reuse * 1000) / HasteModifier); - - break; - } - default: - break; + default: + break; } } @@ -5337,7 +5530,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(); @@ -5622,12 +5815,10 @@ 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() && GetRaid()->GetGroup(GetCleanName()) != RAID_GROUPLESS)) && + (spellTarget->IsBot() || spellTarget->IsClient()) && + (RuleB(Bots, GroupBuffing) || RuleB(Bots, CrossRaidBuffingAndHealing)) ) { bool noGroupSpell = false; uint16 thespell = spell_id; @@ -5635,9 +5826,9 @@ bool Bot::DoFinishedSpellSingleTarget(uint16 spell_id, Mob* spellTarget, EQ::spe 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) { @@ -5655,24 +5846,42 @@ 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))) { - } - else - SpellOnTarget(thespell, g->members[i]); + std::vector v; + if (RuleB(Bots, CrossRaidBuffingAndHealing)) { + v = GatherSpellTargets(true); + } + else { + v = GatherGroupSpellTargets(); + } - if (g->members[i] && g->members[i]->GetPetID()) - SpellOnTarget(thespell, g->members[i]->GetPet()); + for (Mob* m : v) { + 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; + } + } + } } } - 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) * (v.size() - 1))); } } + stopLogic = true; } + return true; } @@ -5691,30 +5900,26 @@ 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()) { + const std::vector v = GatherGroupSpellTargets(spellTarget); + for (Mob* m : v) { + 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 +5988,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; } @@ -6162,24 +6367,52 @@ 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 + + if (GetClass() == Class::Bard) { + if (IsSitting()) { + BuffFadeBySitModifier(); + regen = 2; + regen += (itembonuses.ManaRegen + aabonuses.ManaRegen); + } + else { + regen = 1; + regen += (itembonuses.ManaRegen + aabonuses.ManaRegen); + } + } + else { + if (IsSitting()) { + BuffFadeBySitModifier(); + if (GetArchetype() != Archetype::Melee) { + 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); - } else - regen = (2 + spellbonuses.ManaRegen + itembonuses.ManaRegen); - regen += aabonuses.ManaRegen + itembonuses.heroic_mana_regen; + if (IsHeroicINTCasterClass(GetClass())) { + regen += itembonuses.HeroicINT * RuleR(Character, HeroicIntelligenceMultiplier) / 25; + } + else if (IsHeroicWISCasterClass(GetClass())) { + regen += itembonuses.HeroicWIS * RuleR(Character, HeroicWisdomMultiplier) / 25; + } + else { + regen = 0; + } - regen = ((regen * RuleI(Character, ManaRegenMultiplier)) / 100); - float mana_regen_rate = RuleR(Bots, ManaRegen); - if (mana_regen_rate < 0.0f) - mana_regen_rate = 0.0f; + regen += aabonuses.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; + } + + regen = (regen * mana_regen_rate); + } - regen = (regen * mana_regen_rate); return regen; } @@ -6363,16 +6596,39 @@ 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() - ); + if (RuleB(Bots, PreventBotCampOnFD) && GetBotOwner()->GetFeigned()) { + GetBotOwner()->Message(Chat::White, "You cannot camp bots while feigned."); return; } + if (RuleB(Bots, PreventBotCampOnEngaged)) { + Raid* raid = entity_list.GetRaidByClient(GetBotOwner()->CastToClient()); + if (raid && raid->IsEngaged()) { + GetBotOwner()->Message(Chat::White, "You cannot spawn bots while your raid is engaged."); + return; + } + + auto* owner_group = GetBotOwner()->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) { + GetBotOwner()->Message(Chat::White, "You cannot spawn bots while your group is engaged,"); + return; + } + } + } + else { + if (GetBotOwner()->CastToClient()->GetAggroCount() > 0) { + GetBotOwner()->Message(Chat::White, "You cannot spawn bots while you are engaged,"); + return; + } + } + } + Sit(); LeaveHealRotationMemberPool(); @@ -6400,10 +6656,10 @@ 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)); @@ -6799,349 +7055,64 @@ 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) { +bool EntityList::Bot_AICheckCloseBeneficialSpells(Bot* caster, uint8 iChance, float iRange, uint16 spellType) { - if ((iSpellTypes & SPELL_TYPES_DETRIMENTAL) != 0) { + if (BOT_SPELL_TYPES_DETRIMENTAL(spellType, caster->GetClass())) { LogError("[EntityList::Bot_AICheckCloseBeneficialSpells] detrimental spells requested"); return false; } - if (!caster || !caster->AI_HasSpells()) { - return false; + std::vector v; + + if (IsGroupTargetOnlyBotSpellType(spellType)) { + v = caster->GatherGroupSpellTargets(); + } + else { + if (RuleB(Bots, CrossRaidBuffingAndHealing)) { + v = caster->GatherSpellTargets(true); + } + else { + v = caster->GatherGroupSpellTargets(); + } + v = caster->GatherSpellTargets(); } - if (iChance < 100) { - uint8 tmp = zone->random.Int(1, 100); - if (tmp > iChance) - return false; - } + Mob* tar = nullptr; - uint8 botCasterClass = caster->GetClass(); + for (Mob* m : v) { + tar = m; - 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 (!tar) { + continue; } - 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; - } - } + uint8 chanceToCast = caster->IsEngaged() ? caster->GetChanceToCastBySpellType(spellType) : 100; - 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; - } - } - - 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; - - if (caster->AICastSpell(g->members[i]->GetPet(), 100, SpellType_Heal)) - return true; - } - } - } - } + if (!caster->PrecastChecks(tar, spellType)) { + 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)) { + if (caster->AICastSpell(tar, chanceToCast, spellType)) { + return true; + } + + if (tar->HasPet() && (!m->GetPet()->IsFamiliar() || RuleB(Bots, AllowBuffingHealingFamiliars))) { + tar = m->GetPet(); + + if (!tar) { + continue; + } + + if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { + continue; + } + + if (!caster->PrecastChecks(tar, spellType)) { + continue; + } + + if (caster->AICastSpell(tar, chanceToCast, spellType)) { return true; - } else - return false; - } - - 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 (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; - } - - 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 (iSpellTypes == SpellType_HateRedux) { - if (!caster->IsEngaged()) - return false; - - 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 (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)) - ) { - return true; - } - } - } - } 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; - } - } - } } } } @@ -7438,7 +7409,7 @@ uint8 Bot::GetNumberNeedingHealedInGroup(uint8 hpr, bool includePets, Raid* raid need_healed++; } - if (includePets && member->GetPet() && member->GetPet()->GetHPRatio() <= hpr) { + if (includePets && member->GetPet() && !member->GetPet()->IsFamiliar() && member->GetPet()->GetHPRatio() <= hpr) { need_healed++; } } @@ -7568,69 +7539,74 @@ int Bot::GroupLeadershipAAOffenseEnhancement() { bool Bot::GetNeedsCured(Mob *tar) { bool needCured = 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; } + + needCured = true; } } } } } + return needCured; } 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() > GetUltimateSpellMinThreshold(BotSpellTypes::HateRedux, tar) && tar->GetHPRatio() < GetUltimateSpellMaxThreshold(BotSpellTypes::HateRedux, tar)) { return true; - default: - return false; } } return false; } -bool Bot::HasOrMayGetAggro() { +bool Bot::HasOrMayGetAggro(bool SitAggro, uint32 spell_id) { bool mayGetAggro = false; + if (GetTarget() && GetTarget()->GetHateTop()) { - Mob *topHate = GetTarget()->GetHateTop(); - if (topHate == this) + Mob* topHate = GetTarget()->GetHateTop(); + if (topHate == this) { mayGetAggro = true; + } else { uint32 myHateAmt = GetTarget()->GetHateAmount(this); uint32 topHateAmt = GetTarget()->GetHateAmount(topHate); - if (myHateAmt > 0 && topHateAmt > 0 && (uint8)((myHateAmt / topHateAmt) * 100) > 90) + if (SitAggro && !spell_id) { + myHateAmt *= (1 + (RuleI(Aggro, SittingAggroMod) / 100)); + } + + if (spell_id && IsValidSpell(spell_id) && GetTarget()) { + myHateAmt += CheckAggroAmount(spell_id, GetTarget()); + } + + if ( + topHateAmt < 1 || + ( + myHateAmt > 0 && + (uint8)((myHateAmt / topHateAmt) * 100) > RuleI(Bots, HasOrMayGetAggroThreshold) + ) + ) { mayGetAggro = true; + } } } + return mayGetAggro; } @@ -7990,51 +7966,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()); } @@ -8284,24 +8233,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, @@ -8479,16 +8410,20 @@ float Bot::GetBotCasterMaxRange(float melee_distance_max) {// Calculate caster d float caster_distance_min = 0.0f; float caster_distance = 0.0f; - caster_distance_max = GetBotCasterRange() * GetBotCasterRange(); + caster_distance_max = GetBotCasterRange(); + if (!GetBotCasterRange() && GetLevel() >= GetStopMeleeLevel() && GetClass() >= Class::Warrior && GetClass() <= Class::Berserker) { - caster_distance_max = MAX_CASTER_DISTANCE[GetClass() - 1]; + caster_distance_max = GetDefaultBotBaseSetting(BotBaseSettings::CasterRange); } + 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; } @@ -8500,33 +8435,36 @@ int32 Bot::CalcItemATKCap() bool Bot::CheckSpawnConditions(Client* c) { - if (c->GetFeigned()) { - c->Message(Chat::White, "You cannot spawn a bot-group while feigned."); + if (RuleB(Bots, PreventBotSpawnOnFD) && c->GetFeigned()) { + c->Message(Chat::White, "You cannot spawn bots while feigned."); return false; } - 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; - } + if (RuleB(Bots, PreventBotSpawnOnEngaged)) { + 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); + 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 spawn bots while your group is engaged,"); - return false; + 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; + } } } - } 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 spawn bots while you are engaged,"); + return false; + } } } @@ -8599,12 +8537,12 @@ void Bot::SetSpellRecastTimer(uint16 spell_id, int32 recast_delay) { if (CheckSpellRecastTimer(spell_id)) { BotTimer_Struct 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; @@ -8617,7 +8555,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 ) @@ -8705,12 +8643,12 @@ void Bot::SetDisciplineReuseTimer(uint16 spell_id, int32 reuse_timer) if (CheckDisciplineReuseTimer(spell_id)) { BotTimer_Struct 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; @@ -9210,3 +9148,2275 @@ 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 }; + +std::vector Bot::GatherGroupSpellTargets(Mob* target, bool noClients, bool noBots) { + std::vector valid_spell_targets; + + if (IsRaidGrouped()) { + if (auto raid = entity_list.GetRaidByBotName(GetName())) { + std::vector raidGroupMembers; + if (target) { + auto raidGroup = raid->GetGroup(target->GetName()); + + if (raidGroup != RAID_GROUPLESS) { + raidGroupMembers = raid->GetRaidGroupMembers(raidGroup); + } + else { + return valid_spell_targets; + } + } + else { + auto raidGroup = raid->GetGroup(GetName()); + + if (raidGroup != RAID_GROUPLESS) { + raidGroupMembers = raid->GetRaidGroupMembers(raidGroup); + } + else { + return valid_spell_targets; + } + } + + for (const auto& m : raidGroupMembers) { + if ( + m.member && m.group_number != RAID_GROUPLESS && + ( + (m.member->IsClient() && !noClients) || + (m.member->IsBot() && !noBots) + ) + ) { + valid_spell_targets.emplace_back(m.member); + } + } + } + } + else if (IsGrouped()) { + Group* group = GetGroup(); + if (group) { + for (const auto& m : group->members) { + if ( + m && + ( + (m->IsClient() && !noClients) || + (m->IsBot() && !noBots) + ) + ) { + valid_spell_targets.emplace_back(m); + } + } + } + } + else { + valid_spell_targets.emplace_back(this); + } + + return valid_spell_targets; +} + +std::vector Bot::GatherSpellTargets(bool entireRaid, bool noClients, bool noBots, bool noPets) { + std::vector valid_spell_targets; + + if (IsRaidGrouped()) { + if (auto raid = entity_list.GetRaidByBotName(GetName())) { + if (entireRaid) { + for (const auto& m : raid->members) { + if (m.member && m.group_number != RAID_GROUPLESS && ((m.member->IsClient() && !noClients) || (m.member->IsBot() && !noBots))) { + valid_spell_targets.emplace_back(m.member); + } + } + } + else { + std::vector raidGroup = raid->GetRaidGroupMembers(raid->GetGroup(GetName())); + + for (const auto& m : raidGroup) { + if (m.member && m.group_number != RAID_GROUPLESS && ((m.member->IsClient() && !noClients) || (m.member->IsBot() && !noBots))) { + valid_spell_targets.emplace_back(m.member); + } + } + } + } + } + else if (IsGrouped()) { + Group* group = GetGroup(); + if (group) { + for (const auto& m : group->members) { + if (m && ((m->IsClient() && !noClients) || (m->IsBot() && !noBots))) { + valid_spell_targets.emplace_back(m); + } + } + } + } + else { + valid_spell_targets.emplace_back(this); + } + + return valid_spell_targets; +} + +bool Bot::PrecastChecks(Mob* tar, uint16 spellType) { + if (!tar) { + LogBotPreChecksDetail("{} says, 'Cancelling cast due to PrecastChecks !tar.'", GetCleanName()); //deleteme + return false; + } + + LogBotPreChecksDetail("{} says, 'Running [{}] PreChecks on [{}].'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + + if (GetUltimateSpellHold(spellType, tar)) { + LogBotHoldChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetUltimateSpellHold.'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + return false; + } + + if (GetManaRatio() < GetSpellTypeMinManaLimit(spellType) || GetManaRatio() > GetSpellTypeMaxManaLimit(spellType)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetSpellTypeMinManaLimit or GetSpellTypeMaxManaLimit.'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + return false; + } + + if (GetHPRatio() < GetSpellTypeMinHPLimit(spellType) || GetHPRatio() > GetSpellTypeMaxHPLimit(spellType)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetSpellTypeMinHPLimit or GetSpellTypeMaxHPLimit.'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + return false; + } + + if (!GetUltimateSpellDelayCheck(spellType, tar)) { + LogBotDelayChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetUltimateSpellDelayCheck.'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + return false; + } + + switch (spellType) { //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(spellType, tar) < GetUltimateSpellMinThreshold(spellType, tar) || GetHPRatioForSpellType(spellType, tar) > GetUltimateSpellMaxThreshold(spellType, tar)) { + LogBotThresholdChecksDetail("{} says, 'Cancelling cast of [{}] on [{}] due to GetUltimateSpellMinThreshold or GetUltimateSpellMaxThreshold.'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + return false; + } + } + + return true; +} + +bool Bot::CastChecks(uint16 spellid, Mob* tar, uint16 spellType, bool doPrechecks, bool AECheck) { + if (!tar) { + LogBotPreChecksDetail("{} says, 'Cancelling cast due to CastChecks !tar.'", GetCleanName()); //deleteme + return false; + } + + if (doPrechecks) { + if (spells[spellid].target_type == ST_Self && tar != this) { + tar = this; + } + + if (!PrecastChecks(tar, spellType)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast due to !PrecastChecks.'", GetCleanName()); //deleteme + return false; + } + } + + LogBotPreChecksDetail("{} says, 'Running [{}] CastChecks on [{}].'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + + if (!IsValidSpell(spellid)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast due to !IsValidSpell.'", GetCleanName()); //deleteme + return false; + } + + if (spells[spellid].target_type == ST_Self && tar != this) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to ST_Self.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (!CheckSpellRecastTimer(spellid)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} due to !CheckSpellRecastTimer.'", GetCleanName(), GetSpellName(spellid)); //deleteme + return false; + } + + if (!BotHasEnoughMana(spellid)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} due to !BotHasEnoughMana.'", GetCleanName(), GetSpellName(spellid)); //deleteme + return false; + } + + if (zone->IsSpellBlocked(spellid, glm::vec3(GetPosition()))) { + LogBotPreChecks("{} says, 'Cancelling cast of {} due to IsSpellBlocked.'", GetCleanName(), GetSpellName(spellid)); //deleteme + return false; + } + + if (!zone->CanLevitate() && IsEffectInSpell(spellid, SE_Levitate)) { + LogBotPreChecks("{} says, 'Cancelling cast of {} due to !CanLevitate.'", GetCleanName(), GetSpellName(spellid)); //deleteme + return false; + } + + if (spells[spellid].time_of_day == SpellTimeRestrictions::Day && !zone->zone_time.IsDayTime()) { + LogBotPreChecks("{} says, 'Cancelling cast of {} due to !IsDayTime.'", GetCleanName(), GetSpellName(spellid)); //deleteme + return false; + } + + if (spells[spellid].time_of_day == SpellTimeRestrictions::Night && !zone->zone_time.IsNightTime()) { + LogBotPreChecks("{} says, 'Cancelling cast of {} due to !IsNightTime.'", GetCleanName(), GetSpellName(spellid)); //deleteme + return false; + } + + if (spells[spellid].zone_type == 1 && !zone->CanCastOutdoor()) { + LogBotPreChecks("{} says, 'Cancelling cast of {} due to !CanCastOutdoor.'", GetCleanName(), GetSpellName(spellid)); //deleteme + return false; + } + + if (!AECheck && !IsValidSpellRange(spellid, tar)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsValidSpellRange.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (!IsValidTargetType(spellid, GetSpellTargetType(spellid), tar->GetBodyType())) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsValidTargetType.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (tar->GetSpecialAbility(SpecialAbility::CastingFromRangeImmunity) && !CombatRange(tar)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to IMMUNE_CASTING_FROM_RANGE.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (tar->IsImmuneToBotSpell(spellid, this)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsImmuneToBotSpell.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (!DoResistCheckBySpellType(tar, spellid, spellType)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to DoResistCheckBySpellType.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (!IsCommandedSpell() && !taunting && GetSpellTypeAggroCheck(spellType) && HasOrMayGetAggro(IsSitting(), spellid) && !tar->IsFleeing()) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to HasOrMayGetAggro.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if ( + (RequiresStackCheck(spellType) || (!RequiresStackCheck(spellType) && CalcBuffDuration(this, tar, spellid) != 0)) + && + tar->CanBuffStack(spellid, GetLevel(), true) < 0 + ) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to !CanBuffStack.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + LogBotPreChecksDetail("{} says, 'Doing CanCastSpellType checks of {} on {}.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + if (!CanCastSpellType(spellType, spellid, tar)) { + return false; + } + + return true; +} + +bool Bot::CanCastSpellType(uint16 spellType, uint16 spellid, Mob* tar) { + if (!spellid || !tar) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to failsafe checks.'", GetCleanName(), (spellid ? GetSpellName(spellid) : (spellType ? GetSpellTypeNameByID(spellType) : "Unknown")), (tar ? tar->GetCleanName() : "Unknown")); //deleteme + return false; + } + + uint8 botClass = GetClass(); + //uint8 botLevel = GetLevel(); + + switch (spellType) { + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::DamageShields: + case BotSpellTypes::ResistBuffs: + if ( + !( + spells[spellid].target_type == ST_Target || + spells[spellid].target_type == ST_Pet || + (tar == this && spells[spellid].target_type != ST_TargetsTarget) || + spells[spellid].target_type == ST_Group || + spells[spellid].target_type == ST_GroupTeleport //|| + //(botClass == Class::Bard && spells[spellid].target_type == ST_AEBard) //TODO bot rewrite - is this needed? + ) + ) { + LogBotPreChecks("{} says, 'Cancelling cast of {} on {} due to target_type checks.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (tar->IsBlockedBuff(spellid)) { + LogBotPreChecks("{} says, 'Cancelling cast of {} on {} due to IsBlockedBuff.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (IsEffectInSpell(spellid, SE_Teleport) || IsEffectInSpell(spellid, SE_Succor)) { + LogBotPreChecks("{} says, 'Cancelling cast of {} on {} due to Teleport.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (tar->IsPet() && !RuleB(Bots, CanCastIllusionsOnPets) && IsEffectInSpell(spellid, SE_Illusion)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to PetSE_Illusion.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (spells[spellid].target_type == ST_Pet && (!tar->IsPet() || (tar->GetOwner() != this && !RuleB(Bots, CanCastPetOnlyOnOthersPets)))) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to PetOnly.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if ((IsGroupSpell(spellid) && tar->IsPet()) && (!tar->GetOwner() || (RuleB(Bots, RequirePetAffinity) && !tar->GetOwner()->HasPetAffinity()))) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to PetGroupSpellTarget.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (!IsCommandedSpell() && IsTargetAlreadyReceivingSpell(tar, spellid)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsTargetAlreadyReceivingSpell.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (!IsCommandedSpell()) { + switch (tar->GetArchetype()) { + case Archetype::Caster: + if ( + tar->IsBot() && tar->GetLevel() > tar->CastToBot()->GetStopMeleeLevel() && + ( + IsEffectInSpell(spellid, SE_AttackSpeed) || IsEffectInSpell(spellid, SE_ReverseDS)) || + (SpellEffectsCount(spellid) == 1 && IsEffectInSpell(spellid, SE_ATK) || IsEffectInSpell(spellid, SE_STR) + ) + ) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Caster.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + break; + case Archetype::Melee: + if ( + IsEffectInSpell(spellid, SE_IncreaseSpellHaste) || IsEffectInSpell(spellid, SE_ManaPool) || + IsEffectInSpell(spellid, SE_CastingLevel) || IsEffectInSpell(spellid, SE_ManaRegen_v2) || + IsEffectInSpell(spellid, SE_CurrentMana) + ) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Melee.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + break; + case Archetype::Hybrid: //Hybrids get all buffs + default: + break; + } + } + + // Differences for each type + if (spellType != BotSpellTypes::InCombatBuff) { + if (IsEffectInSpell(spellid, SE_AbsorbMagicAtt) || IsEffectInSpell(spellid, 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)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsLichSpell.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + } + } + } + } + } + + break; + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + //switch (spells[spellid].target_type) { //TODO bot rewrite - is this needed? + // case ST_AEBard: + // case ST_AECaster: + // case ST_GroupTeleport: + // case ST_Group: + // case ST_Self: + // break; + // default: + // LogBotPreChecks("{} says, 'Cancelling cast of {} on {} due to target_type checks.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + // return false; + //} + + if (!IsCommandedSpell() && IsTargetAlreadyReceivingSpell(tar, spellid)) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to IsTargetAlreadyReceivingSpell.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + + if (!IsCommandedSpell()) { + switch (tar->GetArchetype()) { + case Archetype::Caster: + if ( + tar->IsBot() && tar->GetLevel() > tar->CastToBot()->GetStopMeleeLevel() && + ( + IsEffectInSpell(spellid, SE_AttackSpeed) || IsEffectInSpell(spellid, SE_ReverseDS)) || + (SpellEffectsCount(spellid) == 1 && IsEffectInSpell(spellid, SE_ATK) || IsEffectInSpell(spellid, SE_STR) + ) + ) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Caster.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + break; + case Archetype::Melee: + if ( + IsEffectInSpell(spellid, SE_IncreaseSpellHaste) || IsEffectInSpell(spellid, SE_ManaPool) || + IsEffectInSpell(spellid, SE_CastingLevel) || IsEffectInSpell(spellid, SE_ManaRegen_v2) || + IsEffectInSpell(spellid, SE_CurrentMana) + ) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of {} on {} due to Archetype::Melee.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return false; + } + break; + case Archetype::Hybrid: //Hybrids get all buffs + default: + break; + } + } + + break; + default: + break; + } + + LogBotPreChecksDetail("{} says, {} on {} passed CanCastSpellType.'", GetCleanName(), GetSpellName(spellid), tar->GetCleanName()); //deleteme + return true; +} + +bool Bot::BotHasEnoughMana(uint16 spell_id) { + if (!IsValidSpell(spell_id)) { + return false; + } + + //int32 manaCost = GetActSpellCost(spell_id, spells[spell_id].mana); + int32 manaCost = spells[spell_id].mana; + + if (GetMana() < manaCost) { + return false; + } + + return true; +} + +bool Bot::IsTargetAlreadyReceivingSpell(Mob* tar, uint16 spellid) { + + if (!tar || !spellid) { + return true; + } + + if (IsNPC() && CastToNPC()->GetSwarmOwner()) { + return true; + } + + const std::vector v = GatherSpellTargets(); + for (Mob* m : v) { + if ( + m->IsBot() && + m->IsCasting() && + m->CastToBot()->casting_spell_targetid && + entity_list.GetMobID(m->CastToBot()->casting_spell_targetid) == entity_list.GetMobID(tar->GetID()) && + m->CastingSpellID() == spellid + ) { + + return true; + } + else { + if (IsGroupSpell(spellid)) { + if ( + m->IsBot() && + m->IsCasting() && + m->CastToBot()->casting_spell_targetid && + m->CastingSpellID() == spellid + ) { + + const std::vector v = GatherGroupSpellTargets(); + for (Mob* m : v) { + if (entity_list.GetMobID(m->CastToBot()->casting_spell_targetid) == entity_list.GetMobID(m->GetID())) { + return true; + } + } + } + } + } + } + + return false; +} + +bool Bot::DoResistCheck(Mob* tar, uint16 spellid, int32 resist_limit) { + + if (!tar || spellid == 0) { + return false; + } + + int32 resist_difficulty = -spells[spellid].resist_difficulty; + int32 level_mod = (tar->GetLevel() - GetLevel()) * (tar->GetLevel() - GetLevel()) / 2; + + if (tar->GetLevel() - GetLevel() < 0) { + level_mod = -level_mod; + } + + int32 targetResist = 0; + + switch (GetSpellResistType(spellid)) { + case RESIST_NONE: + return true; + case RESIST_MAGIC: + targetResist = tar->GetMR(); + break; + case RESIST_COLD: + targetResist = tar->GetCR(); + break; + case RESIST_FIRE: + targetResist = tar->GetFR(); + break; + case RESIST_POISON: + targetResist = tar->GetPR(); + break; + case RESIST_DISEASE: + targetResist = tar->GetDR(); + break; + case RESIST_CORRUPTION: + targetResist = tar->GetCorrup(); + break; + default: + return true; + } + //LogBotPreChecksDetail("DoResistCheck on {} for {} - TarResist [{}] LMod [{}] ResistDiff [{}] - Adjust [{}] > ResistLim [{}]", tar->GetCleanName(), GetSpellName(spellid), targetResist, level_mod, resist_difficulty, (targetResist + level_mod - resist_difficulty), resist_limit); //deleteme) + if ((targetResist + level_mod - resist_difficulty) > resist_limit) { + return false; + } + + return true; +} + +bool Bot::DoResistCheckBySpellType(Mob* tar, uint16 spellid, uint16 spellType) { + if (!tar || !IsValidSpell(spellid)) { + return false; + } + + if (GetSpellTypeResistLimit(spellType) == 0) { + return true; + } + + return DoResistCheck(tar, spellid, GetSpellTypeResistLimit(spellType)); +} + +bool Bot::IsValidTargetType(uint16 spellid, int targetType, uint8 bodyType) { + if (!spellid) { + return false; + } + + switch (targetType) { + case ST_Undead: + if (bodyType == BodyType::Undead || bodyType == BodyType::SummonedUndead || bodyType == BodyType::Vampire) { + return true; + } + + break; + case ST_Summoned: + if (bodyType == BodyType::Summoned || bodyType == BodyType::Summoned2 || bodyType == BodyType::Summoned3) { + return true; + } + + break; + case ST_Animal: + if (bodyType == BodyType::Animal) { + return true; + } + + break; + case ST_Plant: + if (bodyType == BodyType::Plant) { + return true; + } + + break; + case ST_Giant: + if (bodyType == BodyType::Giant || bodyType == BodyType::RaidGiant) { + return true; + } + + break; + case ST_Dragon: + if (bodyType == BodyType::Dragon || bodyType == BodyType::VeliousDragon) { + return true; + } + + break; + default: + return true; + } + + return false; +} + +bool Bot::IsMobEngagedByAnyone(Mob* tar) { + if (!tar) { + return false; + } + + const std::vector v = GatherSpellTargets(true); + + for (Mob* m : v) { + if (m->GetTarget() == tar) { + if ( + m->IsBot() && + !m->CastToBot()->GetHoldFlag() && + m->IsEngaged() && + ( + !m->CastToBot()->IsBotNonSpellFighter() || + ( + m->GetLevel() >= m->CastToBot()->GetStopMeleeLevel() && + !m->IsCasting() + ) + ) + ) { + 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 spellid) { + 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(spellid, GetSpellTargetType(spellid), npc->GetBodyType())) { + return false; + } + + if (!IsAttackAllowed(GetTarget())) { + return false; + } + + if (!DoLosChecks(this, 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 settingType, uint16 botSetting, int settingValue) { + switch (settingType) { + case BotSettingCategories::BaseSetting: + SetBotBaseSetting(botSetting, settingValue); + break; + case BotSettingCategories::SpellHold: + SetSpellHold(botSetting, settingValue); + break; + case BotSettingCategories::SpellDelay: + SetSpellDelay(botSetting, settingValue); + break; + case BotSettingCategories::SpellMinThreshold: + SetSpellMinThreshold(botSetting, settingValue); + break; + case BotSettingCategories::SpellMaxThreshold: + SetSpellMaxThreshold(botSetting, settingValue); + break; + case BotSettingCategories::SpellTypeAggroCheck: + SetSpellTypeAggroCheck(botSetting, settingValue); + break; + case BotSettingCategories::SpellTypeMinManaPct: + SetSpellTypeMinManaLimit(botSetting, settingValue); + break; + case BotSettingCategories::SpellTypeMaxManaPct: + SetSpellTypeMaxManaLimit(botSetting, settingValue); + break; + case BotSettingCategories::SpellTypeMinHPPct: + SetSpellTypeMinHPLimit(botSetting, settingValue); + break; + case BotSettingCategories::SpellTypeMaxHPPct: + SetSpellTypeMaxHPLimit(botSetting, settingValue); + break; + case BotSettingCategories::SpellTypeIdlePriority: + SetSpellTypePriority(botSetting, BotPriorityCategories::Idle, settingValue); + break; + case BotSettingCategories::SpellTypeEngagedPriority: + SetSpellTypePriority(botSetting, BotPriorityCategories::Engaged, settingValue); + break; + case BotSettingCategories::SpellTypePursuePriority: + SetSpellTypePriority(botSetting, BotPriorityCategories::Pursue, settingValue); + break; + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + SetSpellTypeAEOrGroupTargetCount(botSetting, settingValue); + break; + } +} + +void Bot::SetBotBaseSetting(uint16 botSetting, int settingValue) { + switch (botSetting) { + case BotBaseSettings::ExpansionBitmask: + SetExpansionBitmask(settingValue); + break; + case BotBaseSettings::ShowHelm: + SetShowHelm(settingValue); + break; + case BotBaseSettings::FollowDistance: + SetFollowDistance(EQ::Clamp(static_cast(settingValue), static_cast(1), BOT_FOLLOW_DISTANCE_DEFAULT_MAX)); + break; + case BotBaseSettings::StopMeleeLevel: + SetStopMeleeLevel(settingValue); + break; + case BotBaseSettings::EnforceSpellSettings: + SetBotEnforceSpellSetting(settingValue); + break; + case BotBaseSettings::RangedSetting: + SetBotRangedSetting(settingValue); + break; + case BotBaseSettings::PetSetTypeSetting: + SetPetChooserID(settingValue); + break; + case BotBaseSettings::BehindMob: + SetBehindMob(settingValue); + break; + case BotBaseSettings::CasterRange: + SetBotCasterRange(settingValue); + break; + case BotBaseSettings::IllusionBlock: + SetIllusionBlock(settingValue); + break; + case BotBaseSettings::MaxMeleeRange: + SetMaxMeleeRange(settingValue); + break; + case BotBaseSettings::MedInCombat: + SetMedInCombat(settingValue); + break; + case BotBaseSettings::HPWhenToMed: + SetHPWhenToMed(settingValue); + break; + case BotBaseSettings::ManaWhenToMed: + SetManaWhenToMed(settingValue); + break; + default: + break; + } +} + +int Bot::GetBotBaseSetting(uint16 botSetting) { + switch (botSetting) { + case BotBaseSettings::ExpansionBitmask: + //LogBotSettingsDetail("Returning current GetExpansionBitmask of [{}] for [{}]", GetExpansionBitmask(), GetCleanName()); //deleteme + return GetExpansionBitmask(); + case BotBaseSettings::ShowHelm: + //LogBotSettingsDetail("Returning current GetShowHelm of [{}] for [{}]", GetShowHelm(), GetCleanName()); //deleteme + return GetShowHelm(); + case BotBaseSettings::FollowDistance: + //LogBotSettingsDetail("Returning current GetFollowDistance of [{}] for [{}]", GetFollowDistance(), GetCleanName()); //deleteme + return GetFollowDistance(); + case BotBaseSettings::StopMeleeLevel: + //LogBotSettingsDetail("Returning current GetStopMeleeLevel of [{}] for [{}]", GetStopMeleeLevel(), GetCleanName()); //deleteme + return GetStopMeleeLevel(); + case BotBaseSettings::EnforceSpellSettings: + //LogBotSettingsDetail("Returning current GetBotEnforceSpellSetting of [{}] for [{}]", GetBotEnforceSpellSetting(), GetCleanName()); //deleteme + return GetBotEnforceSpellSetting(); + case BotBaseSettings::RangedSetting: + //LogBotSettingsDetail("Returning current IsBotRanged of [{}] for [{}]", IsBotRanged(), GetCleanName()); //deleteme + return IsBotRanged(); + case BotBaseSettings::PetSetTypeSetting: + //LogBotSettingsDetail("Returning current GetPetChooserID of [{}] for [{}]", GetPetChooserID(), GetCleanName()); //deleteme + return GetPetChooserID(); + case BotBaseSettings::BehindMob: + //LogBotSettingsDetail("Returning current GetBehindMob of [{}] for [{}]", GetBehindMob(), GetCleanName()); //deleteme + return GetBehindMob(); + case BotBaseSettings::CasterRange: + //LogBotSettingsDetail("Returning current GetBotCasterRange of [{}] for [{}]", GetBotCasterRange(), GetCleanName()); //deleteme + return GetBotCasterRange(); + case BotBaseSettings::IllusionBlock: + //LogBotSettingsDetail("Returning current GetIllusionBlock of [{}] for [{}]", GetIllusionBlock(), GetCleanName()); //deleteme + return GetIllusionBlock(); + case BotBaseSettings::MaxMeleeRange: + //LogBotSettingsDetail("Returning current MaxMeleeRange of [{}] for [{}]", GetMaxMeleeRange(), GetCleanName()); //deleteme + return GetMaxMeleeRange(); + case BotBaseSettings::MedInCombat: + //LogBotSettingsDetail("Returning current GetMedInCombate of [{}] for [{}]", GetMaxMeleeRange(), GetCleanName()); //deleteme + return GetMedInCombat(); + case BotBaseSettings::HPWhenToMed: + //LogBotSettingsDetail("Returning current GetHPWhenToMed of [{}] for [{}]", GetMaxMeleeRange(), GetCleanName()); //deleteme + return GetHPWhenToMed(); + case BotBaseSettings::ManaWhenToMed: + //LogBotSettingsDetail("Returning current GetManaWhenToMed of [{}] for [{}]", GetMaxMeleeRange(), GetCleanName()); //deleteme + return GetManaWhenToMed(); + default: + return true; + } + + return true; +} + +int Bot::GetDefaultBotBaseSetting(uint16 botSetting) { + switch (botSetting) { + case BotBaseSettings::ExpansionBitmask: + return RuleI(Bots, BotExpansionSettings); + case BotBaseSettings::ShowHelm: + return true; + case BotBaseSettings::FollowDistance: + return BOT_FOLLOW_DISTANCE_DEFAULT; + 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) { + return true; + } + else { + return false; + } + case BotBaseSettings::CasterRange: + 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::HPWhenToMed: + case BotBaseSettings::ManaWhenToMed: + return 80; + case BotBaseSettings::EnforceSpellSettings: + case BotBaseSettings::RangedSetting: + case BotBaseSettings::IllusionBlock: + case BotBaseSettings::MaxMeleeRange: + default: + return false; + } + + return true; +} + + +void Bot::LoadDefaultBotSettings() { + for (uint16 i = BotBaseSettings::START; i <= BotBaseSettings::END; ++i) { + SetBotBaseSetting(i, GetDefaultSetting(BotSettingCategories::BaseSetting, i)); + LogBotSettingsDetail("{} says, 'Setting default {} [{}] to [{}]'", GetCleanName(), GetBotSettingCategoryName(i), i, GetDefaultBotBaseSetting(i)); //deleteme + } + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + BotSpellSettings_Struct t; + + t.spellType = i; + t.shortName = GetSpellTypeShortNameByID(i); + t.name = GetSpellTypeNameByID(i); + t.hold = GetDefaultSpellHold(i); + t.delay = GetDefaultSpellDelay(i); + t.minThreshold = GetDefaultSpellMinThreshold(i); + t.maxThreshold = GetDefaultSpellMaxThreshold(i); + t.resistLimit = GetDefaultSpellTypeResistLimit(i); + t.aggroCheck = GetDefaultSpellTypeAggroCheck(i); + t.minManaPct = GetDefaultSpellTypeMinManaLimit(i); + t.maxManaPct = GetDefaultSpellTypeMaxManaLimit(i); + t.minHPPct = GetDefaultSpellTypeMinHPLimit(i); + t.maxHPPct = GetDefaultSpellTypeMaxHPLimit(i); + t.idlePriority = GetDefaultSpellTypePriority(i, BotPriorityCategories::Idle, GetClass()); + t.engagedPriority = GetDefaultSpellTypePriority(i, BotPriorityCategories::Engaged, GetClass()); + t.pursuePriority = GetDefaultSpellTypePriority(i, BotPriorityCategories::Pursue, GetClass()); + t.AEOrGroupTargetCount = GetDefaultSpellTypeAEOrGroupTargetCount(i); + t.recastTimer.Start(); + + _spellSettings.push_back(t); + + LogBotSettingsDetail("{} says, 'Setting defaults for {} ({}) [#{}]'", GetCleanName(), t.name, t.shortName, t.spellType); //deleteme + LogBotSettingsDetail("{} says, 'Hold = [{}] | Delay = [{}ms] | MinThreshold = [{}\%] | MaxThreshold = [{}\%]'", GetCleanName(), GetDefaultSpellHold(i), GetDefaultSpellDelay(i), GetDefaultSpellMinThreshold(i), GetDefaultSpellMaxThreshold(i)); //deleteme + LogBotSettingsDetail("{} says, 'AggroCheck = [{}] | MinManaPCT = [{}\%] | MaxManaPCT = [{}\%] | MinHPPCT = [{}\% | MaxHPPCT = [{}\%]'", GetCleanName(), GetDefaultSpellTypeAggroCheck(i), GetDefaultSpellTypeMinManaLimit(i), GetDefaultSpellTypeMaxManaLimit(i), GetDefaultSpellTypeMinHPLimit(i), GetDefaultSpellTypeMaxHPLimit(i)); //deleteme + LogBotSettingsDetail("{} says, 'IdlePriority = [{}] | EngagedPriority = [{}] | PursuePriority = [{}] | AEOrGroupTargetCount = [{}] | recastTimer = [{}]'", GetCleanName(), GetDefaultSpellTypeIdlePriority(i, GetClass()), GetDefaultSpellTypeEngagedPriority(i, GetClass()), GetDefaultSpellTypePursuePriority(i, GetClass()), GetDefaultSpellTypeAEOrGroupTargetCount(i), t.recastTimer.GetRemainingTime()); //deleteme + } +} + +void Bot::SetBotSpellRecastTimer(uint16 spellType, Mob* tar, bool preCast) { + if (!tar) { + return; + } + + if (!preCast && BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType)) { + return; + } + + uint32 addedDelay = 0; + + switch (spellType) { //Additional delays + case BotSpellTypes::Mez: + addedDelay = RuleI(Bots, MezSuccessDelay); + break; + case BotSpellTypes::AEMez: + addedDelay = RuleI(Bots, AEMezSuccessDelay); + break; + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + return tar->GetOwner()->SetSpellTypeRecastTimer(spellType, (GetUltimateSpellDelay(spellType, tar) + addedDelay)); + } + else if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType)) { + tar->SetSpellTypeRecastTimer(spellType, (GetUltimateSpellDelay(spellType, tar) + addedDelay)); + } + else { + SetSpellTypeRecastTimer(spellType, (GetUltimateSpellDelay(spellType, tar) + addedDelay)); + } +} + +BotSpell Bot::GetSpellByHealType(uint16 spellType, Mob* tar) { + switch (spellType) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + return GetBestBotSpellForVeryFastHeal(this, tar, spellType); + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + return GetBestBotSpellForFastHeal(this, tar, spellType); + case BotSpellTypes::RegularHeal: + case BotSpellTypes::PetRegularHeals: + return GetBestBotSpellForRegularSingleTargetHeal(this, tar, spellType); + case BotSpellTypes::GroupHeals: + return GetBestBotSpellForGroupHeal(this, tar, spellType); + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::PetCompleteHeals: + return GetBestBotSpellForPercentageHeal(this, tar, spellType); + case BotSpellTypes::GroupCompleteHeals: + return GetBestBotSpellForGroupCompleteHeal(this, tar, spellType); + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + return GetBestBotSpellForHealOverTime(this, tar, spellType); + case BotSpellTypes::GroupHoTHeals: + return GetBestBotSpellForGroupHealOverTime(this, tar, spellType); + } +} + +uint16 Bot::GetSpellTypePriority(uint16 spellType, uint8 priorityType) { + switch (priorityType) { + case BotPriorityCategories::Idle: + return _spellSettings[spellType].idlePriority; + case BotPriorityCategories::Engaged: + return _spellSettings[spellType].engagedPriority; + case BotPriorityCategories::Pursue: + return _spellSettings[spellType].pursuePriority; + default: + return 0; + } +} + +int Bot::GetDefaultSetting(uint16 settingCategory, uint16 settingType) { + switch (settingCategory) { + case BotSettingCategories::BaseSetting: + return GetDefaultBotBaseSetting(settingType); + case BotSettingCategories::SpellHold: + return GetDefaultSpellHold(settingType); + case BotSettingCategories::SpellDelay: + return GetDefaultSpellDelay(settingType); + case BotSettingCategories::SpellMinThreshold: + return GetDefaultSpellMinThreshold(settingType); + case BotSettingCategories::SpellMaxThreshold: + return GetDefaultSpellMinThreshold(settingType); + case BotSettingCategories::SpellTypeAggroCheck: + return GetDefaultSpellTypeAggroCheck(settingType); + case BotSettingCategories::SpellTypeMinManaPct: + return GetDefaultSpellTypeMinManaLimit(settingType); + case BotSettingCategories::SpellTypeMaxManaPct: + return GetDefaultSpellTypeMaxManaLimit(settingType); + case BotSettingCategories::SpellTypeMinHPPct: + return GetDefaultSpellTypeMinHPLimit(settingType); + case BotSettingCategories::SpellTypeMaxHPPct: + return GetDefaultSpellTypeMaxHPLimit(settingType); + case BotSettingCategories::SpellTypeIdlePriority: + return GetDefaultSpellTypePriority(settingType, BotPriorityCategories::Idle, GetClass()); + case BotSettingCategories::SpellTypeEngagedPriority: + return GetDefaultSpellTypePriority(settingType, BotPriorityCategories::Engaged, GetClass()); + case BotSettingCategories::SpellTypePursuePriority: + return GetDefaultSpellTypePriority(settingType, BotPriorityCategories::Pursue, GetClass()); + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + return GetDefaultSpellTypeAEOrGroupTargetCount(settingType); + default: + break; + } +} + +int Bot::GetSetting(uint16 settingCategory, uint16 settingType) { + switch (settingCategory) { + case BotSettingCategories::BaseSetting: + return GetBotBaseSetting(settingType); + case BotSettingCategories::SpellHold: + return GetSpellHold(settingType); + case BotSettingCategories::SpellDelay: + return GetSpellDelay(settingType); + case BotSettingCategories::SpellMinThreshold: + return GetSpellMinThreshold(settingType); + case BotSettingCategories::SpellMaxThreshold: + return GetSpellMinThreshold(settingType); + case BotSettingCategories::SpellTypeAggroCheck: + return GetSpellTypeAggroCheck(settingType); + case BotSettingCategories::SpellTypeMinManaPct: + return GetSpellTypeMinManaLimit(settingType); + case BotSettingCategories::SpellTypeMaxManaPct: + return GetSpellTypeMaxManaLimit(settingType); + case BotSettingCategories::SpellTypeMinHPPct: + return GetSpellTypeMinHPLimit(settingType); + case BotSettingCategories::SpellTypeMaxHPPct: + return GetSpellTypeMaxHPLimit(settingType); + case BotSettingCategories::SpellTypeIdlePriority: + return GetSpellTypePriority(settingType, BotPriorityCategories::Idle); + case BotSettingCategories::SpellTypeEngagedPriority: + return GetSpellTypePriority(settingType, BotPriorityCategories::Engaged); + case BotSettingCategories::SpellTypePursuePriority: + return GetSpellTypePriority(settingType, BotPriorityCategories::Pursue); + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + return GetSpellTypeAEOrGroupTargetCount(settingType); + default: + break; + } +} + +uint16 Bot::GetDefaultSpellTypePriority(uint16 spellType, uint8 priorityType, uint8 botClass) { + switch (priorityType) { + case BotPriorityCategories::Idle: + return GetDefaultSpellTypeIdlePriority(spellType, botClass); + case BotPriorityCategories::Engaged: + return GetDefaultSpellTypeEngagedPriority(spellType, botClass); + case BotPriorityCategories::Pursue: + return GetDefaultSpellTypePursuePriority(spellType, botClass); + default: + return 0; + } +} + +uint16 Bot::GetDefaultSpellTypeIdlePriority(uint16 spellType, uint8 botClass) { + if (!BOT_SPELL_TYPES_BENEFICIAL(spellType, botClass)) { + return 0; + } + + uint16 priority = 0; + + switch (spellType) { + case BotSpellTypes::VeryFastHeals: + priority = 1; + + break; + case BotSpellTypes::FastHeals: + priority = 2; + + break; + case BotSpellTypes::GroupHeals: + priority = 3; + + break; + case BotSpellTypes::RegularHeal: + priority = 4; + + break; + case BotSpellTypes::GroupCompleteHeals: + priority = 5; + + break; + case BotSpellTypes::CompleteHeal: + priority = 6; + + break; + case BotSpellTypes::GroupHoTHeals: + priority = 7; + + break; + case BotSpellTypes::HoTHeals: + priority = 8; + + break; + case BotSpellTypes::GroupCures: + priority = 9; + + break; + case BotSpellTypes::Cure: + priority = 10; + + break; + case BotSpellTypes::InCombatBuff: // this has a check at the end to decrement everything below if it's not a SK (SK use InCombatBuffs as it is their hate line so it's not use when Idle) + priority = 11; + + break; + case BotSpellTypes::PetVeryFastHeals: + priority = 12; + + break; + case BotSpellTypes::PetFastHeals: + priority = 13; + + break; + case BotSpellTypes::PetRegularHeals: + priority = 14; + + break; + case BotSpellTypes::PetCompleteHeals: + priority = 15; + + break; + case BotSpellTypes::PetHoTHeals: + priority = 16; + + break; + case BotSpellTypes::Pet: + priority = 17; + + break; + case BotSpellTypes::Buff: + priority = 18; + + break; + case BotSpellTypes::OutOfCombatBuffSong: + priority = 19; + + break; + case BotSpellTypes::ResistBuffs: + priority = 20; + + break; + case BotSpellTypes::DamageShields: + priority = 21; + + break; + case BotSpellTypes::PetBuffs: + priority = 22; + + break; + case BotSpellTypes::PreCombatBuff: + priority = 23; + + break; + case BotSpellTypes::PreCombatBuffSong: + priority = 24; + + break; + default: + priority = 0; //unused + + break; + } + + if ( + priority >= 11 && + botClass && botClass == Class::ShadowKnight && + spellType != BotSpellTypes::InCombatBuff + ) { + --priority; + } + + return priority; +} + +uint16 Bot::GetDefaultSpellTypeEngagedPriority(uint16 spellType, uint8 botClass) { + switch (spellType) { + 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::AELifetap: + return 17; + case BotSpellTypes::Lifetap: + return 18; + case BotSpellTypes::HateRedux: + return 19; + case BotSpellTypes::AEMez: + return 20; + case BotSpellTypes::Mez: + return 21; + case BotSpellTypes::AEDispel: + return 22; + case BotSpellTypes::Dispel: + return 23; + case BotSpellTypes::AEDebuff: + return 24; + case BotSpellTypes::Debuff: + return 25; + case BotSpellTypes::AESnare: + return 26; + case BotSpellTypes::Snare: + return 27; + case BotSpellTypes::AEFear: + return 28; + case BotSpellTypes::Fear: + return 29; + case BotSpellTypes::AESlow: + return 30; + case BotSpellTypes::Slow: + return 31; + case BotSpellTypes::AERoot: + return 32; + case BotSpellTypes::Root: + return 33; + case BotSpellTypes::AEDoT: + return 34; + case BotSpellTypes::DOT: + return 35; + case BotSpellTypes::AEStun: + return 36; + case BotSpellTypes::PBAENuke: + return 37; + case BotSpellTypes::AENukes: + return 38; + case BotSpellTypes::AERains: + return 39; + case BotSpellTypes::Stun: + return 40; + case BotSpellTypes::Nuke: + return 41; + case BotSpellTypes::InCombatBuff: + return 42; + case BotSpellTypes::InCombatBuffSong: + return 43; + case BotSpellTypes::Pet: + return 44; + default: + return 0; + } +} + +uint16 Bot::GetDefaultSpellTypePursuePriority(uint16 spellType, uint8 botClass) { + switch (spellType) { + 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; + default: + return 0; + } +} + +uint16 Bot::GetDefaultSpellTypeResistLimit(uint16 spellType) { + + if (!BOT_SPELL_TYPES_BENEFICIAL(spellType, GetClass())) { + return RuleI(Bots, SpellResistLimit); + } + else { + return 0; + } +} + +bool Bot::GetDefaultSpellTypeAggroCheck(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + case BotSpellTypes::AESnare: + case BotSpellTypes::Snare: + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + case BotSpellTypes::AEStun: + case BotSpellTypes::Stun: + return true; + default: + return false; + } +} + +uint8 Bot::GetDefaultSpellTypeMinManaLimit(uint16 spellType) { + return 0; +} + +uint8 Bot::GetDefaultSpellTypeMaxManaLimit(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::InCombatBuff: + if (GetClass() == Class::Shaman) { + return 75; + } + + break; + default: + break; + } + + return 100; +} + +uint8 Bot::GetDefaultSpellTypeMinHPLimit(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::InCombatBuff: + if (GetClass() == Class::Shaman) { + return 40; + } + + break; + default: + break; + } + + return 0; +} + +uint8 Bot::GetDefaultSpellTypeMaxHPLimit(uint16 spellType) { + return 100; +} + +uint16 Bot::GetDefaultSpellTypeAEOrGroupTargetCount(uint16 spellType) { + if (IsAEBotSpellType(spellType)) { + return RuleI(Bots, MinTargetsForAESpell); + } + + if (IsGroupBotSpellType(spellType)) { + return RuleI(Bots, MinTargetsForGroupSpell); + } + + return 0; +} + +void Bot::SetSpellTypePriority(uint16 spellType, uint8 priorityType, uint16 priority) { + switch (priorityType) { + case BotPriorityCategories::Idle: + _spellSettings[spellType].idlePriority = priority; + break; + case BotPriorityCategories::Engaged: + _spellSettings[spellType].engagedPriority = priority; + break; + case BotPriorityCategories::Pursue: + _spellSettings[spellType].pursuePriority = priority; + break; + default: + return; + } +} + +void Bot::SetSpellTypeResistLimit(uint16 spellType, uint16 resistLimit) { + _spellSettings[spellType].resistLimit = resistLimit; +} + +void Bot::SetSpellTypeAggroCheck(uint16 spellType, bool aggroCheck) { + _spellSettings[spellType].aggroCheck = aggroCheck; +} + +void Bot::SetSpellTypeMinManaLimit(uint16 spellType, uint8 manaLimit) { + _spellSettings[spellType].minManaPct = manaLimit; +} + +void Bot::SetSpellTypeMaxManaLimit(uint16 spellType, uint8 manaLimit) { + _spellSettings[spellType].maxManaPct = manaLimit; +} + +void Bot::SetSpellTypeMinHPLimit(uint16 spellType, uint8 hpLimit) { + _spellSettings[spellType].minHPPct = hpLimit; +} + +void Bot::SetSpellTypeMaxHPLimit(uint16 spellType, uint8 hpLimit) { + _spellSettings[spellType].maxHPPct = hpLimit; +} + +void Bot::SetSpellTypeAEOrGroupTargetCount(uint16 spellType, uint16 targetCount) { + _spellSettings[spellType].AEOrGroupTargetCount = targetCount; +} + +std::string Bot::GetBotSettingCategoryName(uint8 setting_type) { + switch (setting_type) { + case BotBaseSettings::ExpansionBitmask: + return "ExpansionBitmask"; + case BotBaseSettings::ShowHelm: + return "ShowHelm"; + case BotBaseSettings::FollowDistance: + return "FollowDistance"; + case BotBaseSettings::StopMeleeLevel: + return "StopMeleeLevel"; + case BotBaseSettings::EnforceSpellSettings: + return "EnforceSpellSettings"; + case BotBaseSettings::RangedSetting: + return "RangedSetting"; + case BotBaseSettings::PetSetTypeSetting: + return "PetSetTypeSetting"; + case BotBaseSettings::BehindMob: + return "BehindMob"; + case BotBaseSettings::CasterRange: + return "CasterRange"; + case BotBaseSettings::IllusionBlock: + return "IllusionBlock"; + case BotBaseSettings::MaxMeleeRange: + return "MaxMeleeRange"; + case BotBaseSettings::MedInCombat: + return "MedInCombat"; + case BotBaseSettings::HPWhenToMed: + return "HPWhenToMed"; + case BotBaseSettings::ManaWhenToMed: + return "ManaWhenToMed"; + default: + return "Null"; + } + + return "Null"; +} + +std::string Bot::GetBotSpellCategoryName(uint8 setting_type) { + switch (setting_type) { + case BotSettingCategories::BaseSetting: + return "BaseSetting"; + case BotSettingCategories::SpellHold: + return "SpellHold"; + case BotSettingCategories::SpellDelay: + return "SpellDelay"; + case BotSettingCategories::SpellMinThreshold: + return "SpellMinThreshold"; + case BotSettingCategories::SpellMaxThreshold: + return "SpellMaxThreshold"; + case BotSettingCategories::SpellTypeAggroCheck: + return "SpellTypeAggroCheck"; + case BotSettingCategories::SpellTypeMinManaPct: + return "SpellTypeMinManaPct"; + case BotSettingCategories::SpellTypeMaxManaPct: + return "SpellTypeMaxManaPct"; + case BotSettingCategories::SpellTypeMinHPPct: + return "SpellTypeMinHPPct"; + case BotSettingCategories::SpellTypeMaxHPPct: + return "SpellTypeMaxHPPct"; + case BotSettingCategories::SpellTypeIdlePriority: + return "SpellTypeIdlePriority"; + case BotSettingCategories::SpellTypeEngagedPriority: + return "SpellTypeEngagedPriority"; + case BotSettingCategories::SpellTypePursuePriority: + return "SpellTypePursuePriority"; + case BotSettingCategories::SpellTypeAEOrGroupTargetCount: + return "SpellTypeAEOrGroupTargetCount"; + case BotSettingCategories::SpellTypeRecastDelay: + return "SpellTypeRecastDelay"; + default: + return "Null"; + } + + return "Null"; +} + +std::list Bot::GetSpellTypesPrioritized(uint8 priorityType) { + std::list castOrder; + std::list tempCastOrder; + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; i++) { + BotSpellTypeOrder typeSettings = { + .spellType = i, + .priority = GetSpellTypePriority(i, priorityType) + }; + + castOrder.emplace_back(typeSettings); + } + + for (auto& currentType : castOrder) { + if (currentType.priority != 0) { + tempCastOrder.emplace_back(currentType); + } + } + + castOrder = tempCastOrder; + + if (castOrder.size() > 1) { + castOrder.sort( + [](BotSpellTypeOrder const& l, BotSpellTypeOrder const& r) { + return l.priority < r.priority; + } + ); + } + + return castOrder; +} + +bool Bot::AttemptAICastSpell(uint16 spellType) { + bool result = false; + + Mob* tar = GetTarget(); + + if (!taunting && GetSpellTypeAggroCheck(spellType) && HasOrMayGetAggro(IsSitting())) { + LogBotPreChecksDetail("{} says, 'Cancelling cast of [{}] due to GetSpellTypeAggroCheck and HasOrMayGetAggro.'", GetCleanName(), GetSpellTypeNameByID(spellType)); //deleteme + return result; + } + + if (BOT_SPELL_TYPES_BENEFICIAL(spellType, GetClass())) { + if (!PrecastChecks(this, spellType) || !AICastSpell(this, GetChanceToCastBySpellType(spellType), spellType)) { + if (GetClass() == Class::Bard) { + return result; + } + + if (!tar || !PrecastChecks(tar, spellType) || !AICastSpell(tar, GetChanceToCastBySpellType(spellType), spellType)) { + if (!entity_list.Bot_AICheckCloseBeneficialSpells(this, GetChanceToCastBySpellType(spellType), BotAISpellRange, spellType)) { + return result; + } + } + } + } + else { + if (!tar || !PrecastChecks(tar, spellType) || !AICastSpell(tar, GetChanceToCastBySpellType(spellType), spellType)) { + return result; + } + } + + result = true; + + return result; +} + +uint16 Bot::GetSpellListSpellType(uint16 spellType) { + switch (spellType) { + 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::ResistBuffs: + 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: + 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::Charm: + case BotSpellTypes::Escape: + case BotSpellTypes::HateRedux: + case BotSpellTypes::InCombatBuff: + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::Pet: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::Resurrect: + default: + return spellType; + } + + return spellType; +} + +bool Bot::IsValidSpellTypeBySpellID(uint16 spellType, uint16 spellid) { + if (IsAEBotSpellType(spellType) && !IsAnyAESpell(spellid)) { + return false; + } + + if (IsGroupBotSpellType(spellType) && !IsGroupSpell(spellid)) { + return false; + } + + switch (spellType) { //TODO bot rewrite - fix Buff/ResistBuff + case BotSpellTypes::Buff: + if (IsEffectInSpell(spellid, SE_DamageShield)) { + return false; + } + + if ( + IsEffectInSpell(spellid, SE_ResistMagic) || + IsEffectInSpell(spellid, SE_ResistFire) || + IsEffectInSpell(spellid, SE_ResistCold) || + IsEffectInSpell(spellid, SE_ResistPoison) || + IsEffectInSpell(spellid, SE_ResistDisease) || + IsEffectInSpell(spellid, SE_ResistCorruption) || + IsEffectInSpell(spellid, SE_ResistAll) + ) { + return false; + } + + return true; + case BotSpellTypes::ResistBuffs: + if ( + IsEffectInSpell(spellid, SE_ResistMagic) || + IsEffectInSpell(spellid, SE_ResistFire) || + IsEffectInSpell(spellid, SE_ResistCold) || + IsEffectInSpell(spellid, SE_ResistPoison) || + IsEffectInSpell(spellid, SE_ResistDisease) || + IsEffectInSpell(spellid, SE_ResistCorruption) || + IsEffectInSpell(spellid, SE_ResistAll) + ) { + return true; + } + + return false; + case BotSpellTypes::DamageShields: + if (IsEffectInSpell(spellid, SE_DamageShield)) { + return true; + } + + return false; + case BotSpellTypes::PBAENuke: + if (IsPBAENukeSpell(spellid) && !IsStunSpell(spellid)) { + return true; + } + + return false; + case BotSpellTypes::AERains: + if (IsAERainNukeSpell(spellid) && !IsStunSpell(spellid)) { + return true; + } + return false; + case BotSpellTypes::AEStun: + case BotSpellTypes::Stun: + if (IsStunSpell(spellid)) { + return true; + } + + return false; + case BotSpellTypes::AENukes: + case BotSpellTypes::Nuke: + if (!IsStunSpell(spellid)) { + return true; + } + + return false; + default: + return true; + } + + return true; +} + +void Bot::SetCastedSpellType(uint16 spellType) { + _castedSpellType = spellType; +} + +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 stopMeleeLevel, float tar_distance, float melee_distance_min, float melee_distance, float melee_distance_max, bool behindMob) { + if (HasTargetReflection()) { + if (!tar->IsFeared() && !tar->IsStunned()) { + if (TryEvade(tar)) { + return; + } + } + + if (tar->IsRooted() && !taunting) { // 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, false, true)) { + RunToGoalWithJitter(Goal); + return; + } + } + } + + if (taunting && tar_distance < melee_distance_min) { // Back up any taunting bots that are too close + if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, false, taunting)) { + RunToGoalWithJitter(Goal); + return; + } + } + } + else { + if (!tar->IsFeared()) { + if (taunting) { // Taunting adjustments + Mob* mobTar = tar->GetTarget(); + if (!mobTar || mobTar == nullptr) { + DoFaceCheckNoJitter(tar); + return; + } + + if (RuleB(Bots, TauntingBotsFollowTopHate)) { // If enabled, taunting bots will stick to top hate + if ((DistanceSquared(m_Position, mobTar->GetPosition()) > pow(RuleR(Bots, DistanceTauntingBotsStickMainHate), 2))) { + Goal = mobTar->GetPosition(); + RunToGoalWithJitter(Goal); + return; + } + } + else { // Otherwise, stick to any other bots that are taunting + if (mobTar->IsBot() && mobTar->CastToBot()->taunting && (DistanceSquared(m_Position, mobTar->GetPosition()) > pow(RuleR(Bots, DistanceTauntingBotsStickMainHate), 2))) { + Goal = mobTar->GetPosition(); + RunToGoalWithJitter(Goal); + return; + } + } + } + else if (tar_distance < melee_distance_min || (GetBehindMob() && !behindMob) || !HasRequiredLoSForPositioning(tar)) { // Regular adjustment + if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_min, melee_distance, GetBehindMob(), taunting)) { + RunToGoalWithJitter(Goal); + return; + } + //else { + // if (stopMeleeLevel || IsBotArcher()) { + // if (IsBotArcher()) { + // float minArcheryRange = RuleI(Combat, MinRangedAttackDist) * RuleI(Combat, MinRangedAttackDist); + // if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, minArcheryRange, melee_distance, false, taunting)) { + // RunToGoalWithJitter(Goal); + // return; + // } + // } + // else { + // if (PlotBotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance_max + 1, melee_distance, false, taunting)) { + // RunToGoalWithJitter(Goal); + // return; + // } + // } + // } + // DoFaceCheckWithJitter(tar); + // return; + //} + } + } + } + + DoFaceCheckNoJitter(tar); + return; +} + +bool Bot::CheckDoubleRangedAttack() { + int32 chance = spellbonuses.DoubleRangedAttack + itembonuses.DoubleRangedAttack + aabonuses.DoubleRangedAttack; + if (chance && zone->random.Roll(chance)) + return true; + + return false; +} + +bool Bot::RequiresLoSForPositioning() { + if (GetLevel() < GetStopMeleeLevel()) { + return true; + } + else if (GetClass() == Class::Bard) { + return false; + } + else if (GetClass() == Class::Cleric) { //TODO bot rewrite - add check to see if spell requires los + return false; + } + + return true; +} + +bool Bot::HasRequiredLoSForPositioning(Mob* tar) { + if (!tar) { + return true; + } + + if (GetClass() == Class::Cleric) { //add check to see if spell requires los + return true; + } + else if (GetClass() == Class::Bard && GetLevel() >= GetStopMeleeLevel()) { + return true; + } + if (!DoLosChecks(this, tar)) { + return false; + } + + return true; +} + +bool Bot::IsInGroupOrRaid(bool announce) { + if (!GetOwner()) { + return false; + } + + Mob* c = GetOwner(); + + if ( + (!GetRaid() && !GetGroup()) || + (!c->GetRaid() && !c->GetGroup()) + ) { + return false; + } + + if ( + c->GetRaid() && + ( + !GetRaid() || + c->GetRaid() != GetRaid() || + GetRaid()->GetGroup(GetCleanName()) == RAID_GROUPLESS + ) + ) { + return false; + } + + if ( + c->GetGroup() && + ( + !GetGroup() || + c->GetGroup() != GetGroup() + ) + ) { + return false; + } + + + if (announce) { + c->Message( + Chat::Yellow, + fmt::format( + "{} says, 'I am not currently in your group or raid.", + GetCleanName() + ).c_str() + ); + } + + return true; +} + +bool Bot::HasValidAETarget(Bot* botCaster, uint16 spellid, uint16 spellType, Mob* tar) { + int spellRange = botCaster->GetActSpellRange(spellid, spells[spellid].range); + int spellAERange = botCaster->GetActSpellRange(spellid, spells[spellid].aoe_range); + int targetCount = 0; + + for (auto& close_mob : botCaster->m_close_mobs) { + Mob* m = close_mob.second; + + if (tar == m) { + continue; + } + + switch (spellType) { + 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; + default: + break; + } + + if (!m->IsNPC() || !m->CastToNPC()->IsOnHatelist(botCaster->GetOwner())) { + continue; + } + + if (SpellBreaksMez(spellid) && m->IsMezzed()) { + continue; + } + + if (IsPBAESpell(spellid)) { + if ( + spellAERange >= Distance(botCaster->GetPosition(), m->GetPosition()) && + botCaster->CastChecks(spellid, m, spellType, true, true) + ) { + ++targetCount; + } + } + else { + if (!tar || spellRange < Distance(botCaster->GetPosition(), tar->GetPosition()) || !DoLosChecks(this, m)) { + continue; + } + + if ( + spellAERange >= Distance(tar->GetPosition(), m->GetPosition()) && + botCaster->CastChecks(spellid, m, spellType, true, true) + ) { + ++targetCount; + } + } + } + + if (targetCount < botCaster->GetSpellTypeAEOrGroupTargetCount(spellType)) { + return false; + } + + SetHasLoS(true); + + return true; +} + +void Bot::CopySettings(Bot* to, uint8 settingType, uint16 spellType) { + switch (settingType) { + case BotSettingCategories::BaseSetting: + for (uint16 i = BotBaseSettings::START; i <= BotBaseSettings::END; ++i) { + to->SetBotBaseSetting(i, GetBotBaseSetting(i)); + } + + break; + case BotSettingCategories::SpellHold: + if (spellType != UINT16_MAX) { + to->SetSpellHold(spellType, GetSpellHold(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellHold(i, GetSpellHold(i)); + } + } + + break; + case BotSettingCategories::SpellDelay: + if (spellType != UINT16_MAX) { + to->SetSpellDelay(spellType, GetSpellDelay(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellDelay(i, GetSpellDelay(i)); + } + } + + break; + case BotSettingCategories::SpellMinThreshold: + if (spellType != UINT16_MAX) { + to->SetSpellMinThreshold(spellType, GetSpellMinThreshold(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellMinThreshold(i, GetSpellMinThreshold(i)); + } + } + + break; + case BotSettingCategories::SpellMaxThreshold: + if (spellType != UINT16_MAX) { + to->SetSpellMaxThreshold(spellType, GetSpellMaxThreshold(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellMaxThreshold(i, GetSpellMaxThreshold(i)); + } + } + + break; + case BotSettingCategories::SpellTypeAggroCheck: + if (spellType != UINT16_MAX) { + to->SetSpellTypeAggroCheck(spellType, GetSpellTypeAggroCheck(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeAggroCheck(i, GetSpellTypeAggroCheck(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMinManaPct: + if (spellType != UINT16_MAX) { + to->SetSpellTypeMinManaLimit(spellType, GetSpellTypeMinManaLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMinManaLimit(i, GetSpellTypeMinManaLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMaxManaPct: + if (spellType != UINT16_MAX) { + to->SetSpellTypeMaxManaLimit(spellType, GetSpellTypeMaxManaLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMaxManaLimit(i, GetSpellTypeMaxManaLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMinHPPct: + if (spellType != UINT16_MAX) { + to->SetSpellTypeMinHPLimit(spellType, GetSpellTypeMinHPLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMinHPLimit(i, GetSpellTypeMinHPLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeMaxHPPct: + if (spellType != UINT16_MAX) { + to->SetSpellTypeMaxHPLimit(spellType, GetSpellTypeMaxHPLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeMaxHPLimit(i, GetSpellTypeMaxHPLimit(i)); + } + } + + break; + case BotSettingCategories::SpellTypeIdlePriority: + if (spellType != UINT16_MAX) { + to->SetSpellTypePriority(spellType, BotPriorityCategories::Idle, GetSpellTypePriority(spellType, 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 (spellType != UINT16_MAX) { + to->SetSpellTypePriority(spellType, BotPriorityCategories::Engaged, GetSpellTypePriority(spellType, 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 (spellType != UINT16_MAX) { + to->SetSpellTypePriority(spellType, BotPriorityCategories::Pursue, GetSpellTypePriority(spellType, 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 (spellType != UINT16_MAX) { + to->SetSpellTypeAEOrGroupTargetCount(spellType, GetSpellTypeAEOrGroupTargetCount(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + to->SetSpellTypeAEOrGroupTargetCount(i, GetSpellTypeAEOrGroupTargetCount(i)); + } + } + + break; + } +} + +void Bot::CopyBotSpellSettings(Bot* to) +{ + 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); +} diff --git a/zone/bot.h b/zone/bot.h index 68b027f05..97d987e56 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -42,6 +42,9 @@ constexpr uint32 BOT_FOLLOW_DISTANCE_DEFAULT_MAX = 2500; // as DSq value (50 uni 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; @@ -49,6 +52,13 @@ 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 +97,58 @@ 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 SpellTypeAggroCheck = 5; + constexpr uint8 SpellTypeMinManaPct = 6; + constexpr uint8 SpellTypeMaxManaPct = 7; + constexpr uint8 SpellTypeMinHPPct = 8; + constexpr uint8 SpellTypeMaxHPPct = 9; + constexpr uint8 SpellTypeIdlePriority = 10; + constexpr uint8 SpellTypeEngagedPriority = 11; + constexpr uint8 SpellTypePursuePriority = 12; + constexpr uint8 SpellTypeAEOrGroupTargetCount = 13; + constexpr uint8 SpellTypeRecastDelay = 14; + + constexpr uint16 START = BotSettingCategories::BaseSetting; + constexpr uint16 START_NO_BASE = BotSettingCategories::SpellHold; + constexpr uint16 START_CLIENT = BotSettingCategories::SpellHold; + constexpr uint16 END_CLIENT = BotSettingCategories::SpellMaxThreshold; + constexpr uint16 END = BotSettingCategories::SpellTypeAEOrGroupTargetCount; // Increment as needed +}; + +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 CasterRange = 8; + constexpr uint16 IllusionBlock = 9; + constexpr uint16 MaxMeleeRange = 10; + constexpr uint16 MedInCombat = 11; + constexpr uint16 HPWhenToMed = 12; + constexpr uint16 ManaWhenToMed = 13; + + constexpr uint16 START = BotBaseSettings::ShowHelm; // Everything above this cannot be copied, changed or viewed by players + constexpr uint16 END = BotBaseSettings::ManaWhenToMed; // Increment as needed +}; class Bot : public NPC { friend class Mob; @@ -128,7 +190,7 @@ public: // 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; @@ -199,8 +261,8 @@ public: 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 isRanged); void Sit(); void Stand(); bool IsSitting() const override; @@ -228,7 +290,7 @@ public: uint8 GetNumberNeedingHealedInRaidGroup(uint8& need_healed, uint8 hpr, bool includePets, Raid* raid); 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,18 +389,20 @@ public: void AI_Bot_Event_SpellCastFinished(bool iCastSucceeded, uint16 slot); // AI Methods - bool AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes); + bool AICastSpell(Mob* tar, uint8 iChance, uint16 spellType); + bool AttemptAICastSpell(uint16 spellType); 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; } + void SetGuardMode(); void SetHoldMode(); - uint32 GetBotCasterRange() const { return m_bot_caster_range; } + bool IsValidSpellRange(uint16 spell_id, Mob const* tar); // Bot AI Methods @@ -377,6 +441,97 @@ public: 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); + + std::vector GatherGroupSpellTargets(Mob* target = nullptr, bool noClients = false, bool noBots = false); + std::vector GatherSpellTargets(bool entireRaid = false, bool noClients = false, bool noBots = false, bool noPets = false); + + bool PrecastChecks(Mob* tar, uint16 spellType); + bool CastChecks(uint16 spellid, Mob* tar, uint16 spellType, bool doPrechecks = false, bool AECheck = false); + bool CanCastSpellType(uint16 spellType, uint16 spellid, Mob* tar); + bool BotHasEnoughMana(uint16 spell_id); + bool IsTargetAlreadyReceivingSpell(Mob* tar, uint16 spellid); + bool DoResistCheck(Mob* target, uint16 spellid, int32 resist_limit); + bool DoResistCheckBySpellType(Mob* tar, uint16 spellid, uint16 spellType); + bool IsValidTargetType(uint16 spellid, int targetType, uint8 bodyType); + bool IsMobEngagedByAnyone(Mob* tar); + void SetBotSetting(uint8 settingType, uint16 botSetting, int settingValue); + void CopySettings(Bot* to, uint8 settingType, uint16 spellType = UINT16_MAX); + void CopyBotSpellSettings(Bot* to); + void ResetBotSpellSettings(); + int GetBotBaseSetting(uint16 botSetting); + int GetDefaultBotBaseSetting(uint16 botSetting); + void SetBotBaseSetting(uint16 botSetting, int settingValue); + void LoadDefaultBotSettings(); + void SetBotSpellRecastTimer(uint16 spellType, Mob* spelltar, bool preCast = false); + BotSpell GetSpellByHealType(uint16 spellType, Mob* tar); + + std::string GetBotSpellCategoryName(uint8 setting_type); + std::string GetBotSettingCategoryName(uint8 setting_type); + + int GetDefaultSetting(uint16 settingCategory, uint16 settingType); + uint16 GetDefaultSpellTypePriority(uint16 spellType, uint8 priorityType, uint8 botClass); + uint16 GetDefaultSpellTypeIdlePriority(uint16 spellType, uint8 botClass); + uint16 GetDefaultSpellTypeEngagedPriority(uint16 spellType, uint8 botClass); + uint16 GetDefaultSpellTypePursuePriority(uint16 spellType, uint8 botClass); + uint16 GetDefaultSpellTypeResistLimit(uint16 spellType); + bool GetDefaultSpellTypeAggroCheck(uint16 spellType); + uint8 GetDefaultSpellTypeMinManaLimit(uint16 spellType); + uint8 GetDefaultSpellTypeMaxManaLimit(uint16 spellType); + uint8 GetDefaultSpellTypeMinHPLimit(uint16 spellType); + uint8 GetDefaultSpellTypeMaxHPLimit(uint16 spellType); + uint16 GetDefaultSpellTypeAEOrGroupTargetCount(uint16 spellType); + + int GetSetting(uint16 settingCategory, uint16 settingType); + uint16 GetSpellTypePriority(uint16 spellType, uint8 priorityType); + void SetSpellTypePriority(uint16 spellType, uint8 priorityType, uint16 priority); + inline uint16 GetSpellTypeResistLimit(uint16 spellType) const { return _spellSettings[spellType].resistLimit; } + void SetSpellTypeResistLimit(uint16 spellType, uint16 resistLimit); + inline bool GetSpellTypeAggroCheck(uint16 spellType) const { return _spellSettings[spellType].aggroCheck; } + void SetSpellTypeAggroCheck(uint16 spellType, bool AggroCheck); + inline uint8 GetSpellTypeMinManaLimit(uint16 spellType) const { return _spellSettings[spellType].minManaPct; } + inline uint8 GetSpellTypeMaxManaLimit(uint16 spellType) const { return _spellSettings[spellType].maxManaPct; } + void SetSpellTypeMinManaLimit(uint16 spellType, uint8 manaLimit); + void SetSpellTypeMaxManaLimit(uint16 spellType, uint8 manaLimit); + inline uint8 GetSpellTypeMinHPLimit(uint16 spellType) const { return _spellSettings[spellType].minHPPct; } + inline uint8 GetSpellTypeMaxHPLimit(uint16 spellType) const { return _spellSettings[spellType].maxHPPct; } + void SetSpellTypeMinHPLimit(uint16 spellType, uint8 hpLimit); + void SetSpellTypeMaxHPLimit(uint16 spellType, uint8 hpLimit); + inline uint16 GetSpellTypeAEOrGroupTargetCount(uint16 spellType) const { return _spellSettings[spellType].AEOrGroupTargetCount; } + void SetSpellTypeAEOrGroupTargetCount(uint16 spellType, uint16 targetCount); + + bool GetShowHelm() const { return _showHelm; } + void SetShowHelm(bool showHelm) { _showHelm = showHelm; } + 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 GetBotCasterRange() const { return _casterRange; } + void SetBotCasterRange(uint32 casterRange) { _casterRange = casterRange; } + bool GetMedInCombat() const { return _medInCombat; } + void SetMedInCombat(bool value) { _medInCombat = value; } + uint8 GetHPWhenToMed() const { return _HPWhenToMed; } + void SetHPWhenToMed(uint8 value) { _HPWhenToMed = value; } + uint8 GetManaWhenToMed() const { return _ManaWhenToMed; } + void SetManaWhenToMed(uint8 value) { _ManaWhenToMed = value; } + void SetHasLoS(bool hasLoS) { _hasLoS = hasLoS; } + bool HasLoS() const { return _hasLoS; } + + bool IsInGroupOrRaid(bool announce = false); + void SendSpellTypesWindow(Client* c, std::string arg0, std::string arg1, std::string arg2, bool helpPrompt = false); + + std::list GetSpellTypesPrioritized(uint8 priorityType); + uint16 GetSpellListSpellType(uint16 spellType); + bool IsValidSpellTypeBySpellID(uint16 spellType, uint16 spellid); + inline uint16 GetCastedSpellType() const { return _castedSpellType; } + void SetCastedSpellType(uint16 spellType); + + bool HasValidAETarget(Bot* botCaster, uint16 spellid, uint16 spellType, Mob* tar); + + void CheckBotSpells(); + [[nodiscard]] int GetMaxBuffSlots() const final { return EQ::spells::LONG_BUFFS; } [[nodiscard]] int GetMaxSongSlots() const final { return EQ::spells::SHORT_BUFFS; } @@ -423,33 +578,36 @@ public: 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 std::list GetBotSpellsForSpellEffect(Bot* botCaster, uint16 spellType, int spellEffect); + static std::list GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, uint16 spellType, int spellEffect, SpellTargetType targetType); + static std::list GetBotSpellsBySpellType(Bot* botCaster, uint16 spellType); + static std::list GetPrioritizedBotSpellsBySpellType(Bot* botCaster, uint16 spellType, Mob* tar, bool AE = false); - 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 BotSpell GetFirstBotSpellBySpellType(Bot* botCaster, uint16 spellType); + static BotSpell GetBestBotSpellForVeryFastHeal(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForFastHeal(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForHealOverTime(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForPercentageHeal(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetFirstBotSpellForSingleTargetHeal(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForGroupHealOverTime(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForGroupCompleteHeal(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); + static BotSpell GetBestBotSpellForGroupHeal(Bot* botCaster, Mob* tar, uint16 spellType = BotSpellTypes::RegularHeal); - static Mob* GetFirstIncomingMobToMez(Bot* botCaster, BotSpell botSpell); - static BotSpell GetBestBotSpellForMez(Bot* botCaster); - static BotSpell GetBestBotMagicianPetSpell(Bot* botCaster); + static Mob* GetFirstIncomingMobToMez(Bot* botCaster, int16 spellid, uint16 spellType, bool AE = false); + bool IsValidMezTarget(Mob* owner, Mob* npc, uint16 spellid); + static BotSpell GetBestBotSpellForMez(Bot* botCaster, uint16 spellType = BotSpellTypes::Mez); + static BotSpell GetBestBotMagicianPetSpell(Bot* botCaster, uint16 spellType = BotSpellTypes::Mez); 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 GetBestBotSpellForNukeByTargetType(Bot* botCaster, SpellTargetType targetType, uint16 spellType, bool AE = false, Mob* tar = nullptr); + static BotSpell GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType targetType, uint16 spellType, bool AE = false, Mob* tar = nullptr); + static BotSpell GetBestBotWizardNukeSpellByTargetResists(Bot* botCaster, Mob* target, uint16 spellType); static BotSpell GetDebuffBotSpell(Bot* botCaster, Mob* target); - static BotSpell GetBestBotSpellForCure(Bot* botCaster, Mob* target); + static BotSpell GetBestBotSpellForCure(Bot* botCaster, Mob* target, uint16 spellType); static BotSpell GetBestBotSpellForResistDebuff(Bot* botCaster, Mob* target); + static BotSpell GetBestBotSpellForNukeByBodyType(Bot* botCaster, uint8 bodyType, uint16 spellType, bool AE = false, Mob* tar = nullptr); + static BotSpell GetBestBotSpellForRez(Bot* botCaster, Mob* target, uint16 spellType); + static BotSpell GetBestBotSpellForCharm(Bot* botCaster, Mob* target, uint16 spellType); static NPCType *CreateDefaultNPCTypeStructForBot( const std::string& botName, @@ -472,12 +630,11 @@ public: 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,8 +642,7 @@ public: bool GetRangerAutoWeaponSelect() { return _rangerAutoWeaponSelect; } uint8 GetBotStance() { return _botStance; } - uint8 GetChanceToCastBySpellType(uint32 spellType); - bool GetBotEnforceSpellSetting() { return m_enforce_spell_settings; } + uint8 GetChanceToCastBySpellType(uint16 spellType); float GetBotCasterMaxRange(float melee_distance_max); bool IsGroupHealer() const { return m_CastingRoles.GroupHealer; } bool IsGroupSlower() const { return m_CastingRoles.GroupSlower; } @@ -527,8 +683,6 @@ public: 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; } @@ -600,13 +754,11 @@ public: 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 botRangedSetting) { _botRangedSetting = botRangedSetting; } 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 +776,6 @@ public: 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 +789,7 @@ public: 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 +801,12 @@ public: 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, @@ -750,24 +897,11 @@ public: 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 botClass, BotSpell& botSpell, uint16 spellType); + bool BotCastHeal(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType); + bool BotCastNuke(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType); + bool BotCastPet(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType); + bool BotCastCure(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType); bool CheckIfIncapacitated(); bool IsAIProcessValid(const Client* bot_owner, const Group* bot_group, const Raid* raid); @@ -796,39 +930,65 @@ public: const Mob* tar, const EQ::ItemInstance* const& p_item, const EQ::ItemInstance* const& s_item, - bool behind_mob, + bool behindMob, bool backstab_weapon, float& melee_distance_max, - float& melee_distance - ) const; + float& melee_distance, + float& melee_distance_min, + uint8 stopMeleeLevel + ); // Combat Checks 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); + void CheckCombatRange( + Mob* tar, + float tar_distance, + bool& atCombatRange, + bool& behindMob, + const EQ::ItemInstance*& p_item, + const EQ::ItemInstance*& s_item, + float& melee_distance_min, + float& melee_distance_max, + float& melee_distance, + uint8 stopMeleeLevel + ); + 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 stopMeleeLevel, float tar_distance, float melee_distance_min, float melee_distance, float melee_distance_max, bool behindMob); + 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); + void BotRangedAttack(Mob* other, bool CanDoubleAttack = false); + bool CheckDoubleRangedAttack(); // Public "Refactor" Methods static bool CheckSpawnConditions(Client* c); + inline bool CommandedDoSpellCast(int32 i, Mob* tar, int32 mana_cost) { return AIDoSpellCast(i, tar, mana_cost); } + protected: void BotMeditate(bool isSitting); bool CheckBotDoubleAttack(bool Triple = false); + bool CheckTripleAttack(); void PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* client); bool AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore = nullptr) override; @@ -849,9 +1009,7 @@ private: uint32 _botOwnerCharacterID; bool _spawnStatus; Mob* _botOwner; - bool m_bot_archery_setting; bool _botCharmer; - bool _petChooser; uint8 _petChooserID; bool berserk; EQ::InventoryProfile m_inv; @@ -876,9 +1034,15 @@ private: 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_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; @@ -888,7 +1052,7 @@ private: 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 +1060,22 @@ private: 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 _casterRange; + bool _behindMobStatus; + bool _maxMeleeRangeStatus; + bool _medInCombat; + uint8 _HPWhenToMed; + uint8 _ManaWhenToMed; + uint16 _castedSpellType; + bool _hasLoS; + bool _commandedSpell; // Private "base stats" Members int32 _baseMR; diff --git a/zone/bot_command.cpp b/zone/bot_command.cpp index b15531dd7..3f6dfef5b 100644 --- a/zone/bot_command.cpp +++ b/zone/bot_command.cpp @@ -1249,8 +1249,8 @@ int bot_command_init(void) 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("behindmob", "Toggles whether or not your bot tries to stay behind a mob", AccountStatus::Player, bot_command_behind_mob) || bot_command_add("bindaffinity", "Orders a bot to attempt an affinity binding", AccountStatus::Player, bot_command_bind_affinity) || 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) || @@ -1270,8 +1270,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,16 +1279,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("cast", "Tells the first found specified bot to cast the given spell type", AccountStatus::Player, bot_command_cast) || 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("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("copysettings", "Copies settings from one bot to another", AccountStatus::Player, bot_command_copy_settings) || bot_command_add("cure", "Orders a bot to remove any ailments", AccountStatus::Player, bot_command_cure) || + bot_command_add("defaultsettings", "Restores a bot back to default settings", AccountStatus::Player, bot_command_default_settings) || bot_command_add("defensive", "Orders a bot to use a defensive discipline", AccountStatus::Player, bot_command_defensive) || 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) || @@ -1320,6 +1324,7 @@ int bot_command_init(void) 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) || @@ -1329,6 +1334,7 @@ int bot_command_init(void) 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("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("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("owneroption", "Sets options available to bot owners", AccountStatus::Player, bot_command_owner_option) || @@ -1346,7 +1352,23 @@ int bot_command_init(void) 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("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("size", "Orders a bot to change a player's size", AccountStatus::Player, bot_command_size) || + 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("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("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) || @@ -1360,7 +1382,7 @@ int bot_command_init(void) 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("waterbreathing", "Orders a bot to cast a water breathing spell", AccountStatus::Player, bot_command_water_breathing) ) { bot_command_deinit(); return -1; @@ -1613,7 +1635,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas bot_owner->Message( Chat::White, 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. Mixed case {} allowed.", bot_name, RuleB(Bots, AllowCamelCaseNames) ? "is" : "is not" ).c_str() ); @@ -1625,7 +1647,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas bot_owner->Message( Chat::White, fmt::format( - "Failed to query name availability for '{}'.", + "'{}' is already in use or an invalid name.", bot_name ).c_str() ); @@ -1806,40 +1828,6 @@ 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) { @@ -2085,8 +2073,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() ); @@ -2126,18 +2117,152 @@ bool helper_spell_list_fail(Client *bot_owner, bcst_list* spell_list, BCEnum::Sp return false; } +void Bot::SendSpellTypesWindow(Client* c, std::string arg0, std::string arg1, std::string arg2, bool helpPrompt) { + if (helpPrompt) { + c->Message( + Chat::Yellow, + fmt::format( + "Use {}, {}, {} for a list of spell types by ID", + Saylink::Silent( + fmt::format("{} listid 0-19", arg0) + ), + Saylink::Silent( + fmt::format("{} listid 20-39", arg0) + ), + Saylink::Silent( + fmt::format("{} listid 40+", arg0) + ) + ).c_str() + ); + + c->Message( + Chat::Yellow, + fmt::format( + "Use {}, {}, {} for a list of spell types by short name", + Saylink::Silent( + fmt::format("{} listname 0-19", arg0) + ), + Saylink::Silent( + fmt::format("{} listname 20-39", arg0) + ), + Saylink::Silent( + fmt::format("{} listname 40+", arg0) + ) + ).c_str() + ); + + return; + } + + uint8 minCount = 0; + uint8 maxCount = 0; + + if (BotSpellTypes::END <= 19) { + minCount = BotSpellTypes::START; + maxCount = BotSpellTypes::END; + } + else if (!arg2.compare("0-19")) { + minCount = BotSpellTypes::START; + maxCount = 19; + } + else if (!arg2.compare("20-39")) { + minCount = std::min(static_cast(20), static_cast(BotSpellTypes::END)); + maxCount = std::min(static_cast(39), static_cast(BotSpellTypes::END)); + } + else if (!arg2.compare("40+")) { + minCount = std::min(static_cast(40), static_cast(BotSpellTypes::END)); + maxCount = BotSpellTypes::END; + } + else { + c->Message(Chat::Yellow, "You must choose a valid range option"); + + 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 fillerLine = "-----------"; + std::string spellTypeField = "Spell Type"; + std::string pluralS = "s"; + std::string idField = "ID"; + std::string shortnameField = "Short Name"; + + std::string popup_text = DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(goldenrod, spellTypeField) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(goldenrod, idField) : DialogueWindow::ColorMessage(goldenrod, shortnameField)) + ) + ) + ); + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(gold, fillerLine) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(gold, fillerLine) + ) + ) + ); + + for (int i = minCount; i <= maxCount; ++i) { + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}{}", + DialogueWindow::ColorMessage(forest_green, c->GetSpellTypeNameByID(i)), + DialogueWindow::ColorMessage(forest_green, pluralS) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(slate_blue, std::to_string(i)) : DialogueWindow::ColorMessage(slate_blue, c->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/behind_mob.cpp" #include "bot_commands/bind_affinity.cpp" #include "bot_commands/bot.cpp" +#include "bot_commands/bot_settings.cpp" +#include "bot_commands/cast.cpp" #include "bot_commands/caster_range.cpp" #include "bot_commands/charm.cpp" +#include "bot_commands/class_race_list.cpp" #include "bot_commands/click_item.cpp" +#include "bot_commands/copy_settings.cpp" #include "bot_commands/cure.cpp" +#include "bot_commands/default_settings.cpp" #include "bot_commands/defensive.cpp" #include "bot_commands/depart.cpp" #include "bot_commands/escape.cpp" @@ -2148,11 +2273,13 @@ bool helper_spell_list_fail(Client *bot_owner, bcst_list* spell_list, BCEnum::Sp #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/max_melee_range.cpp" #include "bot_commands/mesmerize.cpp" #include "bot_commands/movement_speed.cpp" #include "bot_commands/name.cpp" @@ -2167,8 +2294,24 @@ bool helper_spell_list_fail(Client *bot_owner, bcst_list* spell_list, BCEnum::Sp #include "bot_commands/resurrect.cpp" #include "bot_commands/rune.cpp" #include "bot_commands/send_home.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/size.cpp" #include "bot_commands/spell.cpp" +#include "bot_commands/spell_aggro_checks.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_target_count.cpp" #include "bot_commands/summon.cpp" #include "bot_commands/summon_corpse.cpp" #include "bot_commands/suspend.cpp" diff --git a/zone/bot_command.h b/zone/bot_command.h index 74dfa1401..53cb2776b 100644 --- a/zone/bot_command.h +++ b/zone/bot_command.h @@ -666,6 +666,27 @@ namespace MyBots UniquifySBL(sbl); } } + + static void PopulateSBL_ByAtMMR(Client* bot_owner, std::list& 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 @@ -875,6 +896,7 @@ namespace ActionableBots ABT_HealRotation, ABT_HealRotationMembers, ABT_HealRotationTargets, + ABT_MMR, ABT_Class, ABT_Race, ABT_Spawned, @@ -892,6 +914,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,8 +922,8 @@ 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' @@ -941,6 +964,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 +1030,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); @@ -1256,9 +1287,9 @@ namespace ActionableBots sbl.remove_if([min_level](const Bot* l) { return (l->GetLevel() < min_level); }); } - static void Filter_ByArcher(Client* bot_owner, std::list& sbl) { + static void Filter_ByRanged(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()); }); + sbl.remove_if([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) { @@ -1637,12 +1668,18 @@ 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_behind_mob(Client* c, const Seperator* sep); void bot_command_bind_affinity(Client *c, const Seperator *sep); void bot_command_bot(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_caster_range(Client* c, const Seperator* sep); void bot_command_charm(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_copy_settings(Client* c, const Seperator* sep); void bot_command_cure(Client *c, const Seperator *sep); +void bot_command_default_settings(Client* c, const Seperator* sep); void bot_command_defensive(Client *c, const Seperator *sep); void bot_command_depart(Client *c, const Seperator *sep); void bot_command_escape(Client *c, const Seperator *sep); @@ -1653,11 +1690,13 @@ 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_max_melee_range(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_owner_option(Client *c, const Seperator *sep); @@ -1671,7 +1710,23 @@ 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_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_size(Client *c, const Seperator *sep); +void bot_command_spell_aggro_checks(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_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); @@ -1706,7 +1761,6 @@ 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_spawn(Client *c, const Seperator *sep); void bot_command_stance(Client *c, const Seperator *sep); @@ -1716,8 +1770,8 @@ 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); @@ -1757,7 +1811,6 @@ bool helper_bot_appearance_fail(Client *bot_owner, Bot *my_bot, BCEnum::AFType f 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); diff --git a/zone/bot_commands/actionable.cpp b/zone/bot_commands/actionable.cpp index 84965b57d..73ec9ef6c 100644 --- a/zone/bot_commands/actionable.cpp +++ b/zone/bot_commands/actionable.cpp @@ -3,22 +3,61 @@ 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"); + std::vector description = + { + "Lists actionable command arguments and use descriptions." + }; + + std::vector 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 bots as actionable" + }; + + 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 = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + 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 index 6a3881d98..b40bf7123 100644 --- a/zone/bot_commands/aggressive.cpp +++ b/zone/bot_commands/aggressive.cpp @@ -7,32 +7,26 @@ void bot_command_aggressive(Client* c, const Seperator* sep) 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] - ); + 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, "note: Orders a bot to use a aggressive discipline"); 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; + const int ab_mask = ActionableBots::ABM_Type1; + 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) { + + 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; } diff --git a/zone/bot_commands/appearance.cpp b/zone/bot_commands/appearance.cpp index fcaec025c..c6aed9e08 100644 --- a/zone/bot_commands/appearance.cpp +++ b/zone/bot_commands/appearance.cpp @@ -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() ); diff --git a/zone/bot_commands/apply_poison.cpp b/zone/bot_commands/apply_poison.cpp index cfffff2cb..444e370ed 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 60cad8cd8..8cce85db1 100644 --- a/zone/bot_commands/attack.cpp +++ b/zone/bot_commands/attack.cpp @@ -5,14 +5,17 @@ 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"); @@ -26,11 +29,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; + 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; } diff --git a/zone/bot_commands/behind_mob.cpp b/zone/bot_commands/behind_mob.cpp new file mode 100644 index 000000000..5761c4c31 --- /dev/null +++ b/zone/bot_commands/behind_mob.cpp @@ -0,0 +1,173 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Toggles whether or not bots will stay behind the mob during combat." + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [value] [actionable]" + , sep->arg[0] + ) + }; + + std::vector examples_one = + { + "To set Monks to stay behind the mob:", + fmt::format( + "{} 1 byclass 7", + sep->arg[0] + ) + }; + std::vector examples_two = { }; + std::vector examples_three = + { + "To check the behind mob status for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 1; + bool current_check = false; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + typeValue = atoi(sep->arg[1]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!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(typeValue); + ++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, + typeValue ? "will now" : "will no longer" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/bind_affinity.cpp b/zone/bot_commands/bind_affinity.cpp index e6d01fe4c..a59c5947a 100644 --- a/zone/bot_commands/bind_affinity.cpp +++ b/zone/bot_commands/bind_affinity.cpp @@ -7,6 +7,7 @@ void bot_command_bind_affinity(Client *c, const Seperator *sep) return; if (helper_is_help_or_usage(sep->arg[1])) { c->Message(Chat::White, "usage: () %s", sep->arg[0]); + c->Message(Chat::White, "note: Orders a bot to attempt an affinity binding", sep->arg[0]); helper_send_usage_required_bots(c, BCEnum::SpT_BindAffinity); return; } diff --git a/zone/bot_commands/bot.cpp b/zone/bot_commands/bot.cpp index 5d2bfe18e..7da91504d 100644 --- a/zone/bot_commands/bot.cpp +++ b/zone/bot_commands/bot.cpp @@ -18,7 +18,7 @@ void bot_command_bot(Client *c, const Seperator *sep) subcommand_list.push_back("botstance"); subcommand_list.push_back("botstopmeleelevel"); subcommand_list.push_back("botsummon"); - subcommand_list.push_back("bottogglearcher"); + subcommand_list.push_back("bottoggleranged"); subcommand_list.push_back("bottogglehelm"); subcommand_list.push_back("botupdate"); @@ -33,7 +33,7 @@ void bot_command_camp(Client *c, const Seperator *sep) 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; } const int ab_mask = ActionableBots::ABM_Type1; @@ -452,8 +452,8 @@ void bot_command_follow_distance(Client *c, const Seperator *sep) if (helper_command_alias_fail(c, "bot_command_follow_distance", sep->arg[0], "botfollowdistance")) 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]); + c->Message(Chat::White, "usage: %s [set [1 to %i]] [distance] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationmembers | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0], BOT_FOLLOW_DISTANCE_DEFAULT_MAX); + c->Message(Chat::White, "usage: %s [clear] ([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; @@ -464,7 +464,7 @@ void bot_command_follow_distance(Client *c, const Seperator *sep) if (!strcasecmp(sep->arg[1], "set")) { if (!sep->IsNumber(2)) { - c->Message(Chat::White, "A numeric [distance] is required to use this command"); + c->Message(Chat::White, "A numeric [distance] is required to use this command. [1 to %i]", BOT_FOLLOW_DISTANCE_DEFAULT_MAX); return; } @@ -492,22 +492,15 @@ void bot_command_follow_distance(Client *c, const Seperator *sep) continue; bot_iter->SetFollowDistance(bfd); - if (ab_type != ActionableBots::ABT_All && !database.botdb.SaveFollowDistance(bot_iter->GetBotID(), bfd)) { - return; - } ++bot_count; } if (ab_type == ActionableBots::ABT_All) { - if (!database.botdb.SaveAllFollowDistances(c->CharacterID(), bfd)) { - return; - } - - c->Message(Chat::White, "%s all of your bot follow distances", set_flag ? "Set" : "Cleared"); + c->Message(Chat::White, "%s all of your bot follow distances to %i", set_flag ? "Set" : "Cleared", bfd); } else { - c->Message(Chat::White, "%s %i of your spawned bot follow distances", (set_flag ? "Set" : "Cleared"), bot_count); + c->Message(Chat::White, "%s %i of your spawned bot follow distances to %i", (set_flag ? "Set" : "Cleared"), bot_count, bfd); } } @@ -516,7 +509,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"); @@ -764,75 +757,31 @@ void bot_command_list_bots(Client *c, const Seperator *sep) } } -void bot_command_out_of_combat(Client *c, const Seperator *sep) -{ - if (helper_command_alias_fail(c, "bot_command_out_of_combat", sep->arg[0], "botoutofcombat")) - 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]); - return; - } - const int ab_mask = (ActionableBots::ABM_Target | ActionableBots::ABM_ByName); - - 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); - - helper_bot_out_of_combat(c, bot_iter); - } -} - 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]); + 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; + + const int ab_mask = ActionableBots::ABM_Type1; - 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::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, ab_type_arg, sbl, ab_mask, sep->arg[2]) == ActionableBots::ABT_None) + 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) @@ -1062,65 +1011,74 @@ void bot_command_spawn(Client *c, const Seperator *sep) 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")) { 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, "usage: %s [current | value: 1-9] ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); c->Message( Chat::White, fmt::format( - "Value: {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({}), {} ({})", + "Value: {} ({}), {} ({}), {} ({})", Stance::Passive, Stance::GetName(Stance::Passive), Stance::Balanced, Stance::GetName(Stance::Balanced), - Stance::Efficient, - Stance::GetName(Stance::Efficient), - Stance::Reactive, - Stance::GetName(Stance::Reactive), Stance::Aggressive, - Stance::GetName(Stance::Aggressive), - Stance::Assist, - Stance::GetName(Stance::Assist), - Stance::Burn, - Stance::GetName(Stance::Burn), - Stance::Efficient2, - Stance::GetName(Stance::Efficient2), - Stance::AEBurn, - Stance::GetName(Stance::AEBurn) + Stance::GetName(Stance::Aggressive) ).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; + 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 > 300) { + c->Message(Chat::White, "You must enter a value within the range of 0 - 300."); + return; } } - - if (!current_flag && bst == Stance::Unknown) { - c->Message(Chat::White, "A [current] argument or valid numeric [value] is required to use this command"); + else if (!arg1.compare("current")) { + ++ab_arg; + 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[2], sbl, ab_mask, sep->arg[3]) == ActionableBots::ABT_None) + 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; + } + + if (!current_check && (value == Stance::Unknown || (value != Stance::Passive && value != Stance::Balanced && value != Stance::Aggressive))) { + c->Message(Chat::White, "A [current] argument or valid numeric [value] is required to use this command"); + return; + } for (auto bot_iter : sbl) { if (!bot_iter) continue; - if (!current_flag) { - bot_iter->SetBotStance(bst); + if (!current_check) { + bot_iter->SetBotStance(value); bot_iter->Save(); } @@ -1135,50 +1093,138 @@ void bot_command_stance(Client *c, const Seperator *sep) } } -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, "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: Only caster or hybrid class bots may be modified"); 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::White, "Incorrect argument, use %s help for a list of options.", sep->arg[0]); 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::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; } - // [reset] falls through with initialization value - my_bot->SetStopMeleeLevel(sml); - database.botdb.SaveStopMeleeLevel(my_bot->GetBotID(), sml); + sbl.remove(nullptr); - c->Message(Chat::White, "Successfully set stop melee level for %s to %u", my_bot->GetCleanName(), sml); + Bot* first_found = nullptr; + int success_count = 0; + + for (auto my_bot : sbl) { + if (!IsCasterClass(my_bot->GetClass()) && !IsHybridClass(my_bot->GetClass())) { + c->Message( + Chat::White, + fmt::format( + "{} says, 'This command only works on caster or hybrid classes.'", + my_bot->GetCleanName() + ).c_str() + ); + continue; + } + + if (!first_found) { + first_found = my_bot; + } + + if (sync_sml) { + sml = my_bot->GetLevel(); + } + + if (reset_sml) { + sml = my_bot->GetDefaultBotBaseSetting(BotBaseSettings::StopMeleeLevel); + } + + 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,19 +1234,24 @@ 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) { + 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; } @@ -1245,34 +1296,44 @@ 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")) { 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 ([option: on | off]) ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "note: Toggles a ranged bot between melee and ranged weapon use"); 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 archer_state = false; - bool toggle_archer = true; + bool ranged_state = false; + bool toggle_ranged = true; int ab_arg = 1; if (!arg1.compare("on")) { - archer_state = true; - toggle_archer = false; - ab_arg = 2; + ranged_state = true; + toggle_ranged = false; + ++ab_arg; } else if (!arg1.compare("off")) { - toggle_archer = false; - ab_arg = 2; + toggle_ranged = false; + ++ab_arg; + } + + 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, sep->arg[(ab_arg + 1)]) == ActionableBots::ABT_None) { + + 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; } @@ -1281,17 +1342,18 @@ void bot_command_toggle_archer(Client *c, const Seperator *sep) continue; } - if (toggle_archer) { - bot_iter->SetBotArcherySetting(!bot_iter->IsBotArcher(), true); + if (bot_iter->GetBotRangedValue() < RuleI(Combat, MinRangedAttackDist)) { + c->Message(Chat::Yellow, "%s does not have proper weapons or ammo to be at range.", bot_iter->GetCleanName()); + continue; + } + + if (toggle_ranged) { + bot_iter->SetBotRangedSetting(!bot_iter->IsBotRanged()); } else { - bot_iter->SetBotArcherySetting(archer_state, true); - } - bot_iter->ChangeBotArcherWeapons(bot_iter->IsBotArcher()); - - if (bot_iter->GetClass() == Class::Ranger && bot_iter->GetLevel() >= 61) { - bot_iter->SetRangerAutoWeaponSelect(bot_iter->IsBotArcher()); + bot_iter->SetBotRangedSetting(ranged_state); } + bot_iter->ChangeBotRangedWeapons(bot_iter->IsBotRanged()); } } @@ -1300,7 +1362,7 @@ void bot_command_toggle_helm(Client *c, const Seperator *sep) if (helper_command_alias_fail(c, "bot_command_toggle_helm", sep->arg[0], "bottogglehelm")) 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]); + c->Message(Chat::White, "usage: %s ([option: on | off]) ([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; @@ -1313,11 +1375,11 @@ void bot_command_toggle_helm(Client *c, const Seperator *sep) if (!arg1.compare("on")) { helm_state = true; toggle_helm = false; - ab_arg = 2; + ++ab_arg; } else if (!arg1.compare("off")) { toggle_helm = false; - ab_arg = 2; + ++ab_arg; } std::list sbl; @@ -1336,9 +1398,6 @@ void bot_command_toggle_helm(Client *c, const Seperator *sep) bot_iter->SetShowHelm(helm_state); if (ab_type != ActionableBots::ABT_All) { - if (!database.botdb.SaveHelmAppearance(bot_iter->GetBotID(), bot_iter->GetShowHelm())) { - return; - } EQApplicationPacket* outapp = new EQApplicationPacket(OP_SpawnAppearance, sizeof(SpawnAppearance_Struct)); SpawnAppearance_Struct* saptr = (SpawnAppearance_Struct*)outapp->pBuffer; @@ -1355,13 +1414,6 @@ void bot_command_toggle_helm(Client *c, const Seperator *sep) } if (ab_type == ActionableBots::ABT_All) { - if (toggle_helm) { - database.botdb.ToggleAllHelmAppearances(c->CharacterID()); - } - else { - database.botdb.SaveAllHelmAppearances(c->CharacterID(), helm_state); - } - c->Message(Chat::White, "%s all of your bot show helm flags", toggle_helm ? "Toggled" : (helm_state ? "Set" : "Cleared")); } else { @@ -1439,7 +1491,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 000000000..e94bea5f1 --- /dev/null +++ b/zone/bot_commands/bot_settings.cpp @@ -0,0 +1,43 @@ +#include "../bot_command.h" + +void bot_command_bot_settings(Client* c, const Seperator* sep) +{ + std::list subcommand_list; + subcommand_list.push_back("behindmob"); + subcommand_list.push_back("casterrange"); + subcommand_list.push_back("copysettings"); + subcommand_list.push_back("defaultsettings"); + subcommand_list.push_back("enforcespelllist"); + subcommand_list.push_back("follow"); + subcommand_list.push_back("followdistance"); + subcommand_list.push_back("illusionblock"); + subcommand_list.push_back("maxmeleerange"); + subcommand_list.push_back("owneroption"); + subcommand_list.push_back("petsettype"); + subcommand_list.push_back("sithppercent"); + subcommand_list.push_back("sitincombat"); + subcommand_list.push_back("sitmanapercent"); + subcommand_list.push_back("sithppercent"); + subcommand_list.push_back("spellaggrocheck"); + subcommand_list.push_back("spelldelays"); + subcommand_list.push_back("spellengagedpriority"); + subcommand_list.push_back("spellholds"); + subcommand_list.push_back("spellidlepriority"); + subcommand_list.push_back("spellmaxhppct"); + subcommand_list.push_back("spellmaxmanapct"); + subcommand_list.push_back("spellmaxthresholds"); + subcommand_list.push_back("spellminhppct"); + subcommand_list.push_back("spellminmanapct"); + subcommand_list.push_back("spellminthresholds"); + subcommand_list.push_back("spellpursuepriority"); + subcommand_list.push_back("spelltargetcount"); + subcommand_list.push_back("spelllist"); + subcommand_list.push_back("stance"); + subcommand_list.push_back("togglehelm"); + subcommand_list.push_back("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 000000000..77d3ab23b --- /dev/null +++ b/zone/bot_commands/cast.cpp @@ -0,0 +1,301 @@ +#include "../bot_command.h" + +void bot_command_cast(Client* c, const Seperator* sep) +{ + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Commands bots to force cast a specific spell type, ignoring all settings (holds, delays, thresholds, etc)" + }; + + std::vector 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" + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To tell everyone to Nuke the target:", + fmt::format( + "{} {} spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} spawned", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + std::vector examples_two = + { + "To tell all Enchanters to slow the target:", + fmt::format( + "{} {} byclass {}", + sep->arg[0], + Class::Enchanter, + c->GetSpellTypeShortNameByID(BotSpellTypes::Slow) + ), + fmt::format( + "{} {} byclass {}", + sep->arg[0], + Class::Enchanter, + BotSpellTypes::Slow + ) + }; + std::vector examples_three = + { + "To tell Clrbot to resurrect the targeted corpse:", + fmt::format( + "{} {} byname Clrbot", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Resurrect) + ), + fmt::format( + "{} {} byname Clrbot", + sep->arg[0], + BotSpellTypes::Resurrect + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 2; + uint16 spellType = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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 ( + spellType == BotSpellTypes::PetBuffs || + spellType == BotSpellTypes::PetCompleteHeals || + spellType == BotSpellTypes::PetFastHeals || + spellType == BotSpellTypes::PetHoTHeals || + spellType == BotSpellTypes::PetRegularHeals || + spellType == BotSpellTypes::PetVeryFastHeals + ) { + c->Message(Chat::Yellow, "Pet type heals and buffs are not supported, use the regular spell type."); + return; + } + + Mob* tar = c->GetTarget(); + LogTestDebug("{}: Attempting {} on {}", __LINE__, c->GetSpellTypeNameByID(spellType), (tar ? tar->GetCleanName() : "NOBODY")); //deleteme + if (spellType != BotSpellTypes::Escape && spellType != BotSpellTypes::Pet) { + if (!tar) { + c->Message(Chat::Yellow, "You need a target for that."); + return; + } + + if (BOT_SPELL_TYPES_DETRIMENTAL(spellType) && !c->IsAttackAllowed(tar)) { + c->Message(Chat::Yellow, "You cannot attack [%s].", tar->GetCleanName()); + return; + } + + if (BOT_SPELL_TYPES_BENEFICIAL(spellType)) { + if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { + c->Message(Chat::Yellow, "[%s] is an invalid target.", tar->GetCleanName()); + return; + } + } + } + LogTestDebug("{}: Attempting {} on {}", __LINE__, c->GetSpellTypeNameByID(spellType), (tar ? tar->GetCleanName() : "NOBODY")); //deleteme + switch (spellType) { + case BotSpellTypes::Stun: + case BotSpellTypes::AEStun: + if (tar->GetSpecialAbility(SpecialAbility::StunImmunity)) { + c->Message(Chat::Yellow, "[%s] is immune to stuns.", tar->GetCleanName()); + return; + } + + break; + case BotSpellTypes::Resurrect: + if (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse()) { + c->Message(Chat::Yellow, "[%s] is an invalid target. I can only resurrect player corpses.", tar->GetCleanName()); + return; + } + + break; + default: + break; + } + + 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::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); + + BotSpell botSpell; + botSpell.SpellId = 0; + botSpell.SpellIndex = 0; + botSpell.ManaCost = 0; + bool isSuccess = false; + uint16 successCount = 0; + Bot* firstFound = nullptr; + + for (auto bot_iter : sbl) { + if (!bot_iter->IsInGroupOrRaid()) { + continue; + } + + /* + TODO bot rewrite - + FIX: Snares, Group Cures, OOC Song, Precombat, HateRedux, Fear/AE Fear + ICB (SK) casting hate on friendly but not hostile? + NEED TO CHECK: precombat, AE Dispel, AE Lifetap + DO I NEED A PBAE CHECK??? + */ + if (bot_iter->GetHoldFlag() || bot_iter->GetAppearance() == eaDead || bot_iter->IsFeared() || bot_iter->IsStunned() || bot_iter->IsMezzed() || bot_iter->DivineAura() || bot_iter->GetHP() < 0) { + continue; + } + + Mob* newTar = tar; + LogTestDebug("{}: Attempting {} on {}", __LINE__, c->GetSpellTypeNameByID(spellType), (newTar ? newTar->GetCleanName() : "NOBODY")); //deleteme + if (!SpellTypeRequiresTarget(spellType, bot_iter->GetClass())) { + newTar = bot_iter; + } + + if (!newTar) { + continue; + } + + if (BOT_SPELL_TYPES_DETRIMENTAL(spellType, bot_iter->GetClass()) && !bot_iter->IsAttackAllowed(newTar)) { + bot_iter->BotGroupSay( + bot_iter, + fmt::format( + "I cannot attack [{}].", + newTar->GetCleanName() + ).c_str() + ); + + continue; + } + + LogTestDebug("{}: Attempting {} on {}", __LINE__, c->GetSpellTypeNameByID(spellType), (newTar ? newTar->GetCleanName() : "NOBODY")); //deleteme + + bot_iter->SetCommandedSpell(true); + + if (bot_iter->AICastSpell(newTar, 100, spellType)) { + if (!firstFound) { + firstFound = bot_iter; + } + + isSuccess = true; + ++successCount; + } + + bot_iter->SetCommandedSpell(false); + continue; + } + + if (!isSuccess) { + c->Message( + Chat::Yellow, + fmt::format( + "No bots are capable of casting [{}] on {}.", + c->GetSpellTypeNameByID(spellType), + tar ? tar->GetCleanName() : "your target" + ).c_str() + ); + } + else { + c->Message( Chat::Yellow, + fmt::format( + "{} {} [{}]{}", + ((successCount == 1 && firstFound) ? firstFound->GetCleanName() : (fmt::format("{}", successCount).c_str())), + ((successCount == 1 && firstFound) ? "casted" : "of your bots casted"), + c->GetSpellTypeNameByID(spellType), + 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 index 2c1faf639..447c739db 100644 --- a/zone/bot_commands/caster_range.cpp +++ b/zone/bot_commands/caster_range.cpp @@ -7,7 +7,7 @@ void bot_command_caster_range(Client* c, const Seperator* sep) } 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, "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: 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."); @@ -15,6 +15,7 @@ void bot_command_caster_range(Client* c, const Seperator* sep) 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]; @@ -23,7 +24,7 @@ void bot_command_caster_range(Client* c, const Seperator* sep) uint32 crange = 0; if (sep->IsNumber(1)) { - ab_arg = 2; + ++ab_arg; 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."); @@ -31,7 +32,7 @@ void bot_command_caster_range(Client* c, const Seperator* sep) } } else if (!arg1.compare("current")) { - ab_arg = 2; + ++ab_arg; current_check = true; } else { @@ -78,8 +79,6 @@ void bot_command_caster_range(Client* c, const Seperator* sep) else { my_bot->SetBotCasterRange(crange); ++success_count; - - database.botdb.SaveBotCasterRange(my_bot->GetBotID(), crange); } } if (!current_check) { diff --git a/zone/bot_commands/class_race_list.cpp b/zone/bot_commands/class_race_list.cpp new file mode 100644 index 000000000..84c611ab3 --- /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 b236815d8..80375b52c 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]); diff --git a/zone/bot_commands/copy_settings.cpp b/zone/bot_commands/copy_settings.cpp new file mode 100644 index 000000000..fafead14c --- /dev/null +++ b/zone/bot_commands/copy_settings.cpp @@ -0,0 +1,366 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Copies settings from one bot to another bot" + }; + + std::vector 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" + }; + + std::vector example_format = + { + fmt::format( + "{} [from] [to] [option]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To copy all settings from BotA to BotB:", + fmt::format( + "{} BotA BotB all", + sep->arg[0] + ) + }; + std::vector examples_two = + { + "To copy only Nuke spelltypesettings from BotA to BotB:", + fmt::format( + "{} BotA BotB spelltypesettings {}", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} BotA BotB spelltypesettings {}", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + }; + std::vector examples_three = { }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid", + "targetgroup, namesgroup, healrotationtargets", + "mmr, byclass, byrace, spawned" + }; + + std::vector options = + { + "all, misc, spellsettings, spelltypesettings", + "holds, delays, minthresholds, maxthresholds", + "minmanapct, maxmanapct, minhppct, maxhppct", + "idlepriority, engagedpriority, pursuepriority", + "aggrochecks, targetcounts" + }; + std::vector options_one = + { + "[spellsettings] will copy ^spellsettings options", + "[spelltypesettings] copies all spell type settings", + "[all] copies all settings" + }; + std::vector options_two = + { + "[misc] copies all miscellaneous options such as:", + "- ^showhelm, ^followd, ^stopmeleelevel", + "- ^enforcespellsettings, ^bottoggleranged, ^petsettype", + "- ^behindmob, ^casterrange, ^illusionblock", + "- ^sitincombat, ^sithppercent and ^sitmanapercent", + + }; + std::vector options_three = + { + "The remaining options copy that specific type" + }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 2; + bool validOption = false; + uint16 spellType = UINT16_MAX; + std::vector options = + { + "all", + "misc" + "spellsettings", + "spelltypesettings", + "holds", + "delays", + "minthresholds", + "maxthresholds", + "aggrochecks", + "minmanapct", + "maxmanapct", + "minhppct", + "maxhppct", + "idlepriority", + "engagedpriority", + "pursuepriority", + "targetcounts" + }; + + for (int i = 0; i < options.size(); i++) { + if (sep->arg[3] == options[i]) { + if (options[i] != "all" && options[i] != "misc" && options[i] != "spellsettings") { + + if (sep->IsNumber(4) || c->GetSpellTypeIDByShortName(sep->arg[4]) != UINT16_MAX) { + if (sep->IsNumber(4)) { + spellType = atoi(sep->arg[4]); + } + + if (c->GetSpellTypeIDByShortName(sep->arg[4]) != UINT16_MAX) { + spellType = c->GetSpellTypeIDByShortName(sep->arg[4]); + } + + if (spellType < BotSpellTypes::START || spellType > 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 ( + (options[i] == "all" || options[i] == "misc" || options[i] == "spellsettings") && + ((sep->IsNumber(2) || c->GetSpellTypeIDByShortName(sep->arg[4]) != UINT16_MAX)) + ) { + break; + } + + validOption = true; + break; + } + } + + if (!validOption) { + 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 (!strcasecmp(sep->arg[3], "misc")) { + from->CopySettings(to, BotSettingCategories::BaseSetting); + output = "Miscellaneous"; + } + else if (!strcasecmp(sep->arg[3], "holds")) { + from->CopySettings(to, BotSettingCategories::SpellHold, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellHold); + } + else if (!strcasecmp(sep->arg[3], "delays")) { + from->CopySettings(to, BotSettingCategories::SpellDelay, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellDelay); + } + else if (!strcasecmp(sep->arg[3], "minthresholds")) { + from->CopySettings(to, BotSettingCategories::SpellMinThreshold, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellMinThreshold); + } + else if (!strcasecmp(sep->arg[3], "maxthresholds")) { + from->CopySettings(to, BotSettingCategories::SpellMaxThreshold, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellMaxThreshold); + } + else if (!strcasecmp(sep->arg[3], "aggrochecks")) { + from->CopySettings(to, BotSettingCategories::SpellTypeAggroCheck, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeAggroCheck); + } + else if (!strcasecmp(sep->arg[3], "minmanapct")) { + from->CopySettings(to, BotSettingCategories::SpellTypeMinManaPct, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeMinManaPct); + } + else if (!strcasecmp(sep->arg[3], "maxmanapct")) { + from->CopySettings(to, BotSettingCategories::SpellTypeMaxManaPct, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeMaxManaPct); + } + else if (!strcasecmp(sep->arg[3], "minhppct")) { + from->CopySettings(to, BotSettingCategories::SpellTypeMinHPPct, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeMinHPPct); + } + else if (!strcasecmp(sep->arg[3], "maxhppct")) { + from->CopySettings(to, BotSettingCategories::SpellTypeMaxHPPct, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeMaxHPPct); + } + else if (!strcasecmp(sep->arg[3], "idlepriority")) { + from->CopySettings(to, BotSettingCategories::SpellTypeIdlePriority, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeIdlePriority); + } + else if (!strcasecmp(sep->arg[3], "engagedpriority")) { + from->CopySettings(to, BotSettingCategories::SpellTypeEngagedPriority, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeEngagedPriority); + } + else if (!strcasecmp(sep->arg[3], "pursuepriority")) { + from->CopySettings(to, BotSettingCategories::SpellTypePursuePriority, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypePursuePriority); + } + else if (!strcasecmp(sep->arg[3], "targetcounts")) { + from->CopySettings(to, BotSettingCategories::SpellTypeAEOrGroupTargetCount, spellType); + output = from->GetBotSpellCategoryName(BotSettingCategories::SpellTypeAEOrGroupTargetCount); + } + else if (!strcasecmp(sep->arg[3], "spellsettings")) { + from->CopyBotSpellSettings(to); + output = "^spellsettings"; + } + else if (!strcasecmp(sep->arg[3], "spelltypesettings")) { + from->CopySettings(to, BotSettingCategories::SpellHold, spellType); + from->CopySettings(to, BotSettingCategories::SpellDelay, spellType); + from->CopySettings(to, BotSettingCategories::SpellMinThreshold, spellType); + from->CopySettings(to, BotSettingCategories::SpellMaxThreshold, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeAggroCheck, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMinManaPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxManaPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMinHPPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxHPPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeIdlePriority, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeEngagedPriority, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypePursuePriority, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeAEOrGroupTargetCount, spellType); + output = "spell type"; + } + else if (!strcasecmp(sep->arg[3], "all")) { + from->CopySettings(to, BotSettingCategories::BaseSetting); + from->CopySettings(to, BotSettingCategories::SpellHold, spellType); + from->CopySettings(to, BotSettingCategories::SpellDelay, spellType); + from->CopySettings(to, BotSettingCategories::SpellMinThreshold, spellType); + from->CopySettings(to, BotSettingCategories::SpellMaxThreshold, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeAggroCheck, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMinManaPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxManaPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMinHPPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeMaxHPPct, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeIdlePriority, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeEngagedPriority, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypePursuePriority, spellType); + from->CopySettings(to, BotSettingCategories::SpellTypeAEOrGroupTargetCount, spellType); + from->CopyBotSpellSettings(to); + } + 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; + } + + c->Message( + Chat::Green, + fmt::format( + "{}'s{}{} settings were copied to {}.", + from->GetCleanName(), + ( + spellType != UINT16_MAX ? + fmt::format(" [{}] ", + c->GetSpellTypeNameByID(spellType) + ) + : " " + ), + output, + to->GetCleanName() + ).c_str() + ); +} diff --git a/zone/bot_commands/default_settings.cpp b/zone/bot_commands/default_settings.cpp new file mode 100644 index 000000000..1f7c854ca --- /dev/null +++ b/zone/bot_commands/default_settings.cpp @@ -0,0 +1,473 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Restores a bot's setting(s) to defaults" + }; + + std::vector 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" + }; + + std::vector example_format = + { + fmt::format( + "{} [option] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To restore delays for Clerics:", + fmt::format( + "{} delays byclass 2", + sep->arg[0] + ) + }; + std::vector examples_two = + { + "To restore only Snare delays for BotA:", + fmt::format( + "{} delays {} byname BotA", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} delays {} byname BotA", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_three = { }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets mmr, byclass, byrace, spawned" + }; + + std::vector options = + { + "all, misc, spellsettings, spelltypesettings, holds, delays, minthresholds, maxthresholds minmanapct, maxmanapct, minhppct, maxhppct, idlepriority, engagedpriority, pursuepriority, aggrocheck, targetcounts" + }; + std::vector options_one = + { + "[spellsettings] will restore ^spellsettings options", + "[spelltypesettings] restores all spell type settings", + "[all] restores all settings" + }; + std::vector options_two = + { + "[misc] restores all miscellaneous options such as:", + "- ^showhelm, ^followd, ^stopmeleelevel", + "- ^enforcespellsettings, ^bottoggleranged, ^petsettype", + "- ^behindmob, ^casterrange, ^illusionblock", + "- ^sitincombat, ^sithppercent and ^sitmanapercent", + + }; + std::vector options_three = + { + "The remaining options restore that specific type" + }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 2; + bool validOption = false; + uint16 spellType = UINT16_MAX; + std::vector options = + { + "all", + "misc" + "spellsettings", + "spelltypesettings", + "holds", + "delays", + "minthresholds", + "maxthresholds", + "aggrocheck", + "minmanapct", + "maxmanapct", + "minhppct", + "maxhppct", + "idlepriority", + "engagedpriority", + "pursuepriority", + "targetcounts" + }; + + for (int i = 0; i < options.size(); i++) { + if (sep->arg[1] == options[i]) { + if (options[i] != "all" && options[i] != "misc" && options[i] != "spellsettings") { + + if (sep->IsNumber(2) || c->GetSpellTypeIDByShortName(sep->arg[2]) != UINT16_MAX) { + if (sep->IsNumber(2)) { + spellType = atoi(sep->arg[2]); + } + + if (c->GetSpellTypeIDByShortName(sep->arg[2]) != UINT16_MAX) { + spellType = c->GetSpellTypeIDByShortName(sep->arg[2]); + } + + if (spellType < BotSpellTypes::START || spellType > 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; + } + + ++ab_arg; + } + } + else if ( + (options[i] == "all" || options[i] == "misc" || options[i] == "spellsettings") && + ((sep->IsNumber(2) || c->GetSpellTypeIDByShortName(sep->arg[2]) != UINT16_MAX)) + ) { + break; + } + + validOption = true; + break; + } + } + + if (!validOption) { + 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::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; + std::string output = ""; + for (auto my_bot : sbl) { + if (!first_found) { + first_found = my_bot; + } + + if (!strcasecmp(sep->arg[1], "misc")) { + for (uint16 i = BotBaseSettings::START; i <= BotBaseSettings::END; ++i) { + my_bot->SetBotBaseSetting(i, my_bot->GetDefaultBotBaseSetting(i)); + output = "miscellanous settings"; + } + } + else if (!strcasecmp(sep->arg[1], "holds")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellHold(spellType, my_bot->GetDefaultSpellHold(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellHold(i, my_bot->GetDefaultSpellHold(i)); + } + } + + output = "hold settings"; + } + else if (!strcasecmp(sep->arg[1], "delays")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellDelay(spellType, my_bot->GetDefaultSpellDelay(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellDelay(i, my_bot->GetDefaultSpellDelay(i)); + } + } + + output = "delay settings"; + } + else if (!strcasecmp(sep->arg[1], "minthresholds")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellMinThreshold(spellType, my_bot->GetDefaultSpellMinThreshold(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellMinThreshold(i, my_bot->GetDefaultSpellMinThreshold(i)); + } + } + + output = "minimum threshold settings"; + } + else if (!strcasecmp(sep->arg[1], "maxthresholds")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellMaxThreshold(spellType, my_bot->GetDefaultSpellMaxThreshold(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellMaxThreshold(i, my_bot->GetDefaultSpellMaxThreshold(i)); + } + } + + output = "maximum threshold settings"; + } + else if (!strcasecmp(sep->arg[1], "aggrochecks")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypeAggroCheck(spellType, my_bot->GetDefaultSpellTypeAggroCheck(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeAggroCheck(i, my_bot->GetDefaultSpellTypeAggroCheck(i)); + } + } + + output = "aggro check settings"; + } + else if (!strcasecmp(sep->arg[1], "minmanapct")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypeMinManaLimit(spellType, my_bot->GetDefaultSpellTypeMinManaLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMinManaLimit(i, my_bot->GetDefaultSpellTypeMinManaLimit(i)); + } + } + + output = "min mana settings"; + } + else if (!strcasecmp(sep->arg[1], "maxmanapct")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypeMaxManaLimit(spellType, my_bot->GetDefaultSpellTypeMaxManaLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMaxManaLimit(i, my_bot->GetDefaultSpellTypeMaxManaLimit(i)); + } + } + + output = "max mana settings"; + } + else if (!strcasecmp(sep->arg[1], "minhppct")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypeMinHPLimit(spellType, my_bot->GetDefaultSpellTypeMinHPLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMinHPLimit(i, my_bot->GetDefaultSpellTypeMinHPLimit(i)); + } + } + + output = "min hp settings"; + } + else if (!strcasecmp(sep->arg[1], "maxhppct")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypeMaxHPLimit(spellType, my_bot->GetDefaultSpellTypeMaxHPLimit(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeMaxHPLimit(i, my_bot->GetDefaultSpellTypeMaxHPLimit(i)); + } + } + + output = "max hp settings"; + } + else if (!strcasecmp(sep->arg[1], "idlepriority")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(spellType, BotPriorityCategories::Idle, my_bot->GetClass())); + } + 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())); + } + } + + output = "idle priority settings"; + } + else if (!strcasecmp(sep->arg[1], "engagedpriority")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(spellType, BotPriorityCategories::Engaged, my_bot->GetClass())); + } + 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())); + } + } + + output = "engaged priority settings"; + } + else if (!strcasecmp(sep->arg[1], "pursuepriority")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(spellType, BotPriorityCategories::Pursue, my_bot->GetClass())); + } + 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())); + } + } + + output = "pursue priority settings"; + } + else if (!strcasecmp(sep->arg[1], "targetcounts")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellDelay(spellType, my_bot->GetDefaultSpellDelay(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i)); + } + } + + output = "ae/group count settings"; + } + else if (!strcasecmp(sep->arg[1], "spellsettings")) { + my_bot->ResetBotSpellSettings(); + output = "^spellsettings"; + } + else if (!strcasecmp(sep->arg[1], "spelltypesettings")) { + if (spellType != UINT16_MAX) { + my_bot->SetSpellHold(spellType, my_bot->GetDefaultSpellHold(spellType)); + my_bot->SetSpellDelay(spellType, my_bot->GetDefaultSpellDelay(spellType)); + my_bot->SetSpellMinThreshold(spellType, my_bot->GetDefaultSpellMinThreshold(spellType)); + my_bot->SetSpellMaxThreshold(spellType, my_bot->GetDefaultSpellMaxThreshold(spellType)); + my_bot->SetSpellTypeAggroCheck(spellType, my_bot->GetDefaultSpellTypeAggroCheck(spellType)); + my_bot->SetSpellTypeMinManaLimit(spellType, my_bot->GetDefaultSpellTypeMinManaLimit(spellType)); + my_bot->SetSpellTypeMaxManaLimit(spellType, my_bot->GetDefaultSpellTypeMaxManaLimit(spellType)); + my_bot->SetSpellTypeMinHPLimit(spellType, my_bot->GetDefaultSpellTypeMinHPLimit(spellType)); + my_bot->SetSpellTypeMaxHPLimit(spellType, my_bot->GetDefaultSpellTypeMaxHPLimit(spellType)); + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(spellType, BotPriorityCategories::Idle, my_bot->GetClass())); + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(spellType, BotPriorityCategories::Engaged, my_bot->GetClass())); + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(spellType, BotPriorityCategories::Pursue, my_bot->GetClass())); + my_bot->SetSpellTypeAEOrGroupTargetCount(spellType, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(spellType)); + } + else { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellHold(i, my_bot->GetDefaultSpellHold(i)); + my_bot->SetSpellDelay(i, my_bot->GetDefaultSpellDelay(i)); + my_bot->SetSpellMinThreshold(i, my_bot->GetDefaultSpellMinThreshold(i)); + my_bot->SetSpellMaxThreshold(i, my_bot->GetDefaultSpellMaxThreshold(i)); + my_bot->SetSpellTypeAggroCheck(i, my_bot->GetDefaultSpellTypeAggroCheck(i)); + my_bot->SetSpellTypeMinManaLimit(i, my_bot->GetDefaultSpellTypeMinManaLimit(i)); + my_bot->SetSpellTypeMaxManaLimit(i, my_bot->GetDefaultSpellTypeMaxManaLimit(i)); + my_bot->SetSpellTypeMinHPLimit(i, my_bot->GetDefaultSpellTypeMinHPLimit(i)); + my_bot->SetSpellTypeMaxHPLimit(i, my_bot->GetDefaultSpellTypeMaxHPLimit(i)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetClass())); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetClass())); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetClass())); + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i)); + } + } + + 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)); + } + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + my_bot->SetSpellHold(i, my_bot->GetDefaultSpellHold(i)); + my_bot->SetSpellDelay(i, my_bot->GetDefaultSpellDelay(i)); + my_bot->SetSpellMinThreshold(i, my_bot->GetDefaultSpellMinThreshold(i)); + my_bot->SetSpellMaxThreshold(i, my_bot->GetDefaultSpellMaxThreshold(i)); + my_bot->SetSpellTypeAggroCheck(i, my_bot->GetDefaultSpellTypeAggroCheck(i)); + my_bot->SetSpellTypeMinManaLimit(i, my_bot->GetDefaultSpellTypeMinManaLimit(i)); + my_bot->SetSpellTypeMaxManaLimit(i, my_bot->GetDefaultSpellTypeMaxManaLimit(i)); + my_bot->SetSpellTypeMinHPLimit(i, my_bot->GetDefaultSpellTypeMinHPLimit(i)); + my_bot->SetSpellTypeMaxHPLimit(i, my_bot->GetDefaultSpellTypeMaxHPLimit(i)); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Idle, my_bot->GetClass())); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Engaged, my_bot->GetClass())); + my_bot->SetSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetDefaultSpellTypePriority(i, BotPriorityCategories::Pursue, my_bot->GetClass())); + my_bot->SetSpellTypeAEOrGroupTargetCount(i, my_bot->GetDefaultSpellTypeAEOrGroupTargetCount(i)); + }; + + my_bot->ResetBotSpellSettings(); + + output = "settings"; + + } + + ++success_count; + } + + if (success_count == 1) { + c->Message( + Chat::Green, + fmt::format( + "{} says, '{}{} were restored.'", + first_found->GetCleanName(), + ( + spellType != UINT16_MAX ? + fmt::format("My [{}] ", + c->GetSpellTypeNameByID(spellType) + ) + : "My " + ), + output + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bot's{}{} were restored.", + success_count, + ( + spellType != UINT16_MAX ? + fmt::format(" [{}] ", + c->GetSpellTypeNameByID(spellType) + ) + : " " + ), + output + ).c_str() + ); + } +} diff --git a/zone/bot_commands/defensive.cpp b/zone/bot_commands/defensive.cpp index a2edc3e6e..081e07f34 100644 --- a/zone/bot_commands/defensive.cpp +++ b/zone/bot_commands/defensive.cpp @@ -6,22 +6,27 @@ void bot_command_defensive(Client *c, const Seperator *sep) 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]); + c->Message(Chat::White, "usage: %s ([actionable: target | byname | ownergroup | ownerraid | targetgroup | namesgroup | healrotationtargets | mmr | 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]; + 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) { + 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); int success_count = 0; diff --git a/zone/bot_commands/follow.cpp b/zone/bot_commands/follow.cpp index 65ec73f8b..3ff0cb44b 100644 --- a/zone/bot_commands/follow.cpp +++ b/zone/bot_commands/follow.cpp @@ -1,11 +1,11 @@ #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")) 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 ([option: reset]) [actionable: byname | ownergroup | ownerraid | namesgroup | mmr | byclass | byrace | spawned]] ([actionable_name])", sep->arg[0]); c->Message(Chat::White, "usage: %s chain", sep->arg[0]); return; } @@ -26,13 +26,22 @@ void bot_command_follow(Client *c, const Seperator *sep) } else if (!optional_arg.compare("reset")) { reset = true; - ab_arg = 2; - name_arg = 3; + ++ab_arg; + ++name_arg ; } else { - target_mob = ActionableTarget::VerifyFriendly(c, BCEnum::TT_Single); + //target_mob = ActionableTarget::VerifyFriendly(c, BCEnum::TT_Single); + target_mob = c->GetTarget(); if (!target_mob) { - c->Message(Chat::White, "You must a friendly mob to use this command"); + c->Message(Chat::White, "You must a friendly player or bot within your group or raid to use this command"); + return; + } + else if (!target_mob->IsBot() && !target_mob->IsClient()) { + c->Message(Chat::White, "You must a friendly player or bot within your group or raid to use this command"); + return; + } + else if ((target_mob->GetGroup() && target_mob->GetGroup() != c->GetGroup()) || (target_mob->GetRaid() && target_mob->GetRaid() != c->GetRaid())) { + c->Message(Chat::White, "You must a friendly player or bot within your group or raid to use this command"); return; } } @@ -51,29 +60,54 @@ void bot_command_follow(Client *c, const Seperator *sep) sbl.remove(nullptr); for (auto bot_iter : sbl) { 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()); - bot_iter->SetManualFollow(false); - } - else { - if (bot_iter == target_mob) - bot_iter->SetFollowID(c->GetID()); - else - bot_iter->SetFollowID(target_mob->GetID()); - - bot_iter->SetManualFollow(true); - } - } - else { + 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 (target_mob->IsGrouped() || target_mob->IsRaidGrouped()) { + bot_iter->SetFollowID(target_mob->GetID()); + bot_iter->SetManualFollow(true); + } + else if (bot_iter == target_mob) { + bot_iter->SetFollowID(c->GetID()); + bot_iter->SetManualFollow(true); + } + else { + bot_iter->SetFollowID(0); + bot_iter->SetManualFollow(false); + } + } + } + //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()); + // + // bot_iter->SetManualFollow(false); + // } + // else { + // if (bot_iter == target_mob) + // bot_iter->SetFollowID(c->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()) continue; @@ -81,31 +115,37 @@ void bot_command_follow(Client *c, const Seperator *sep) bot_iter->GetPet()->SetFollowID(bot_iter->GetID()); } + Mob* follow_mob = nullptr; if (sbl.size() == 1) { - Mob* follow_mob = entity_list.GetMob(sbl.front()->GetFollowID()); - Bot::BotGroupSay( - sbl.front(), + follow_mob = entity_list.GetMob(sbl.front()->GetFollowID()); + c->Message( + Chat::White, fmt::format( "Following {}.", - follow_mob ? follow_mob->GetCleanName() : "no one" + follow_mob ? follow_mob->GetCleanName() : "you" ).c_str() ); - } else { + } + else { if (reset) { c->Message( Chat::White, fmt::format( - "{} of your bots are following their default assignments.", + "{} of your bots are following you.", sbl.size() ).c_str() ); - } else { + } + else { + if (!sbl.empty()) { + follow_mob = entity_list.GetMob(sbl.front()->GetFollowID()); + } c->Message( Chat::White, fmt::format( "{} of your bots are following {}.", sbl.size(), - target_mob->GetCleanName() + follow_mob ? follow_mob->GetCleanName() : "you" ).c_str() ); } diff --git a/zone/bot_commands/guard.cpp b/zone/bot_commands/guard.cpp index 27a9ed343..1cacd2742 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]; diff --git a/zone/bot_commands/hold.cpp b/zone/bot_commands/hold.cpp index 49f0f516f..0fad872dc 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]; diff --git a/zone/bot_commands/illusion_block.cpp b/zone/bot_commands/illusion_block.cpp new file mode 100644 index 000000000..0a3625046 --- /dev/null +++ b/zone/bot_commands/illusion_block.cpp @@ -0,0 +1,172 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Toggles whether or not bots will block the illusion effects of spells cast by players or bots." + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set BotA to block illusions:", + fmt::format( + "{} 1 byname BotA", + sep->arg[0] + ) + }; + std::vector examples_two = { }; + std::vector examples_three = + { + "To check the illusion block status for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 1; + bool current_check = false; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + typeValue = atoi(sep->arg[1]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!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(typeValue); + ++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, + typeValue ? "will now" : "will no longer" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/inventory.cpp b/zone/bot_commands/inventory.cpp index b934f96a2..7371475ed 100644 --- a/zone/bot_commands/inventory.cpp +++ b/zone/bot_commands/inventory.cpp @@ -217,9 +217,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 +242,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 +262,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); diff --git a/zone/bot_commands/max_melee_range.cpp b/zone/bot_commands/max_melee_range.cpp new file mode 100644 index 000000000..e876a9759 --- /dev/null +++ b/zone/bot_commands/max_melee_range.cpp @@ -0,0 +1,172 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Toggles whether or not bots will stay at max melee range during combat." + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set BotA to stay at max melee range:", + fmt::format( + "{} 1 byname BotA", + sep->arg[0] + ) + }; + std::vector examples_two = { }; + std::vector examples_three = + { + "To check the max melee range status for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 1; + bool current_check = false; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + typeValue = atoi(sep->arg[1]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!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(typeValue); + ++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, + typeValue ? "will now" : "will no longer" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/mesmerize.cpp b/zone/bot_commands/mesmerize.cpp index 532a3c404..1f51a70d3 100644 --- a/zone/bot_commands/mesmerize.cpp +++ b/zone/bot_commands/mesmerize.cpp @@ -2,42 +2,40 @@ 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; + bool isSuccess = false; 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; + for (auto bot_iter : sbl) { + std::list botSpellList = bot_iter->GetPrioritizedBotSpellsBySpellType(bot_iter, BotSpellTypes::Mez, c->GetTarget(), IsAEBotSpellType(BotSpellTypes::Mez)); - auto target_mob = actionable_targets.Select(c, local_entry->target_type, ENEMY); - if (!target_mob) - continue; + for (const auto& s : botSpellList) { + if (!IsValidSpell(s.SpellId)) { + continue; + } - if (spells[local_entry->spell_id].max_value[EFFECTIDTOINDEX(1)] < target_mob->GetLevel()) - continue; + if (!bot_iter->IsInGroupOrRaid()) { + 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 (!bot_iter->CastChecks(s.SpellId, c->GetTarget(), BotSpellTypes::Mez, false, false)) { + 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; + if (bot_iter->CommandedDoSpellCast(s.SpellIndex, c->GetTarget(), s.ManaCost)) { + bot_iter->BotGroupSay(bot_iter, "Casting %s [%s] on %s.", GetSpellName(s.SpellId), bot_iter->GetSpellTypeNameByID(BotSpellTypes::Mez), c->GetTarget()->GetCleanName()); + isSuccess = true; + } + } } - helper_no_available_bots(c, my_bot); + if (!isSuccess) { + helper_no_available_bots(c); + } } diff --git a/zone/bot_commands/pet.cpp b/zone/bot_commands/pet.cpp index f6c6ae21d..c8b48368c 100644 --- a/zone/bot_commands/pet.cpp +++ b/zone/bot_commands/pet.cpp @@ -19,7 +19,7 @@ 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; @@ -98,7 +98,8 @@ 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")) 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, "usage: %s [type: auto | water | fire | air | earth | monster | epic] ([actionable: target | byname] ([actionable_name]))", sep->arg[0]); + c->Message(Chat::White, "if set to 'auto', bots will choose their own pet type"); c->Message(Chat::White, "requires one of the following bot classes:"); c->Message(Chat::White, "Magician(1)"); return; @@ -109,29 +110,41 @@ void bot_command_pet_set_type(Client *c, const Seperator *sep) uint8 pet_type = 255; uint8 level_req = 255; - if (!pet_arg.compare("water")) { + if (!pet_arg.compare("auto")) { pet_type = 0; level_req = 1; } - else if (!pet_arg.compare("fire")) { + else if (!pet_arg.compare("water")) { pet_type = 1; + level_req = 1; + } + else if (!pet_arg.compare("fire")) { + pet_type = 2; level_req = 3; } else if (!pet_arg.compare("air")) { - pet_type = 2; + pet_type = 3; level_req = 4; } else if (!pet_arg.compare("earth")) { - pet_type = 3; + pet_type = 4; level_req = 5; } else if (!pet_arg.compare("monster")) { - pet_type = 4; + pet_type = 5; level_req = 30; } + else if (!pet_arg.compare("epic")) { + pet_type = 6; + if (!RuleB(Bots, AllowMagicianEpicPet)) { + c->Message(Chat::Yellow, "Epic pets are currently disabled for bots."); + return; + } + level_req = RuleI(Bots,AllowMagicianEpicPetLevel); + } if (pet_type == 255) { - c->Message(Chat::White, "You must specify a pet [type: water | fire | air | earth | monster]"); + c->Message(Chat::White, "You must specify a pet [type: auto | water | fire | air | earth | monster | epic]"); return; } @@ -157,7 +170,23 @@ void bot_command_pet_set_type(Client *c, const Seperator *sep) if (!bot_iter) continue; - bot_iter->SetPetChooser(true); + if (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; + } + } + bot_iter->SetPetChooserID(pet_type); if (bot_iter->GetPet()) { auto pet_id = bot_iter->GetPetID(); diff --git a/zone/bot_commands/pull.cpp b/zone/bot_commands/pull.cpp index 6265d141e..fa222fcfa 100644 --- a/zone/bot_commands/pull.cpp +++ b/zone/bot_commands/pull.cpp @@ -7,15 +7,27 @@ 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 + + const int ab_mask = ActionableBots::ABM_Type1; + + 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, "ownergroup", sbl, ab_mask) == ActionableBots::ABT_None) { + + 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); auto target_mob = ActionableTarget::VerifyEnemy(c, BCEnum::TT_Single); diff --git a/zone/bot_commands/release.cpp b/zone/bot_commands/release.cpp index 1872d43d6..bf8741d33 100644 --- a/zone/bot_commands/release.cpp +++ b/zone/bot_commands/release.cpp @@ -5,7 +5,7 @@ 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; diff --git a/zone/bot_commands/sit_hp_percent.cpp b/zone/bot_commands/sit_hp_percent.cpp new file mode 100644 index 000000000..ac94a13fd --- /dev/null +++ b/zone/bot_commands/sit_hp_percent.cpp @@ -0,0 +1,172 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "HP % threshold when bots will sit in combat if allowed." + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set Clerics to sit at 45% HP:", + fmt::format( + "{} 45 byclass 2", + sep->arg[0] + ) + }; + std::vector examples_two = { }; + std::vector examples_three = + { + "To check the HP threshold for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 1; + bool current_check = false; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + typeValue = atoi(sep->arg[1]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!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->GetHPWhenToMed() + ).c_str() + ); + } + else { + my_bot->SetHPWhenToMed(typeValue); + ++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->GetHPWhenToMed() + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots will now sit in combat whem at or below [{}%%] HP.'", + success_count, + typeValue + ).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 000000000..ffcf4d887 --- /dev/null +++ b/zone/bot_commands/sit_in_combat.cpp @@ -0,0 +1,172 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Toggles whether or not bots will sit in combat to heal or med." + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set Clerics to sit in combat:", + fmt::format( + "{} 1 byclass 2", + sep->arg[0] + ) + }; + std::vector examples_two = { }; + std::vector examples_three = + { + "To check the sit in combat state for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 1; + bool current_check = false; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + typeValue = atoi(sep->arg[1]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!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(typeValue); + ++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, + typeValue ? "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 000000000..85afc8047 --- /dev/null +++ b/zone/bot_commands/sit_mana_percent.cpp @@ -0,0 +1,172 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Mana % threshold when bots will sit in combat if allowed." + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set Clerics to sit at 45% mana:", + fmt::format( + "{} 45 byclass 2", + sep->arg[0] + ) + }; + std::vector examples_two = { }; + std::vector examples_three = + { + "To check the mana threshold for all bots:", + fmt::format( + "{} current spawned", + sep->arg[0] + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + int ab_arg = 1; + bool current_check = false; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + typeValue = atoi(sep->arg[1]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!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->GetManaWhenToMed() + ).c_str() + ); + } + else { + my_bot->SetManaWhenToMed(typeValue); + ++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->GetManaWhenToMed() + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots will now sit in combat whem at or below [{}%%] mana.'", + success_count, + typeValue + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell.cpp b/zone/bot_commands/spell.cpp index 45d9ec6eb..833c8ef7a 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 000000000..e9ae709ee --- /dev/null +++ b/zone/bot_commands/spell_aggro_checks.cpp @@ -0,0 +1,236 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Toggles whether or not bots will cast a spell type if they think it will get them aggro" + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to check aggro on nukes:", + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + std::vector examples_two = + { + "To set Shadowknights to ignore aggro checks on snares:", + fmt::format( + "{} {} 0 byclass 5", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 0 byclass 5", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_three = + { + "To check the current DoT aggro check on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] aggro check is currently [{}].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypeAggroCheck(spellType) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + my_bot->SetSpellTypeAggroCheck(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypeAggroCheck(spellType) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots [{}] their [{}] aggro check.", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue ? "enabled" : "disabled" + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_delays.cpp b/zone/bot_commands/spell_delays.cpp new file mode 100644 index 000000000..3c3b153ca --- /dev/null +++ b/zone/bot_commands/spell_delays.cpp @@ -0,0 +1,242 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Controls how long a bot will wait between casts of different spell types" + }; + + std::vector notes = + { + "- 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", + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all Necromancers to an 8s DoT delay:", + fmt::format( + "{} {} 8000 byclass 11", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} 8000 byclass 11", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + std::vector examples_two = + { + "To set all Warriors to receive Fast Heals every 2.5s:", + fmt::format( + "{} {} 2500 byclass 1", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 2500 byclass 1", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + std::vector examples_three = + { + "To check the current Nuke delay on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid targetgroup, namesgroup, healrotationtargets mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 1 || typeValue > 60000) { + c->Message(Chat::Yellow, "You must enter a value between 1-60000 (1ms 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 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 (!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(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellDelay(spellType) / 1000.00 + ).c_str() + ); + } + else { + my_bot->SetSpellDelay(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellDelay(spellType) / 1000.00 + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] spell delay to [{}] seconds.", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue / 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 000000000..03c151c3a --- /dev/null +++ b/zone/bot_commands/spell_engaged_priority.cpp @@ -0,0 +1,240 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Sets the order of spell casts when engaged in combat by spell type" + }; + + std::vector 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." + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all Shaman to cast slows first:", + fmt::format( + "{} {} 1 byclass 10", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Slow) + ), + fmt::format( + "{} {} 1 byclass 10", + sep->arg[0], + BotSpellTypes::Slow + ) + }; + std::vector examples_two = + { + "To set all bots to not cast snares:", + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_three = + { + "To check the current engaged priority of dispels on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Dispel) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Dispel + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-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::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 (!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(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypePriority(spellType, BotPriorityCategories::Engaged) + ).c_str() + ); + } + else { + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Engaged, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypePriority(spellType, BotPriorityCategories::Engaged) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] engaged cast priority to [{}].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/spell_holds.cpp b/zone/bot_commands/spell_holds.cpp new file mode 100644 index 000000000..effd97a7a --- /dev/null +++ b/zone/bot_commands/spell_holds.cpp @@ -0,0 +1,229 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Toggles whether or not bots can cast or receive certain spell types" + }; + + std::vector notes = + { + "- 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", + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to hold DoTs:", + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + std::vector examples_two = + { + "To check the current DoT settings on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + std::vector examples_three = { }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] spell hold is currently [{}].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellHold(spellType) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + my_bot->SetSpellHold(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellHold(spellType) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots [{}] their [{}] spell hold.", + success_count, + typeValue ? "enabled" : "disabled", + c->GetSpellTypeNameByID(spellType) + ).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 000000000..9bb95cf46 --- /dev/null +++ b/zone/bot_commands/spell_idle_priority.cpp @@ -0,0 +1,240 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Sets the order of spell casts when not in combat by spell type" + }; + + std::vector 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." + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all Clerics to cast fast heals third:", + fmt::format( + "{} {} 3 byclass 2", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 3 byclass 2", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + std::vector examples_two = + { + "To set all bots to not cast cures:", + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Cure) + ), + fmt::format( + "{} {} 0 spawned", + sep->arg[0], + BotSpellTypes::Cure + ) + }; + std::vector examples_three = + { + "To check the current idle priority of buffs on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Buff) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Buff + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-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::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 (!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(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypePriority(spellType, BotPriorityCategories::Idle) + ).c_str() + ); + } + else { + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Idle, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypePriority(spellType, BotPriorityCategories::Idle) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] idle cast priority to [{}].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..9d94fd722 --- /dev/null +++ b/zone/bot_commands/spell_max_hp_pct.cpp @@ -0,0 +1,236 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Controls at what health percentage a bot will start casting different spell types" + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to start snaring when their health is at or below 100% HP:", + fmt::format( + "{} {} 100 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_two = + { + "To set BotA to start casting fast heals at 30% HP:", + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + std::vector examples_three = + { + "To check the current Stun max HP percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum HP is currently [{}%%].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypeMaxHPLimit(spellType) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMaxHPLimit(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypeMaxHPLimit(spellType) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] maximum HP to [{}%%].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..9e22617b0 --- /dev/null +++ b/zone/bot_commands/spell_max_mana_pct.cpp @@ -0,0 +1,236 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Controls at what mana percentage a bot will stop casting different spell types" + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to start snaring when their mana is at or below 100% HP:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_two = + { + "To set BotA to start casting fast heals at 30% mana:", + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + std::vector examples_three = + { + "To check the current Stun max mana percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum mana is currently [{}%%].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypeMaxManaLimit(spellType) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMaxManaLimit(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypeMaxManaLimit(spellType) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] maximum mana to [{}%%].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..8ded61c1f --- /dev/null +++ b/zone/bot_commands/spell_max_thresholds.cpp @@ -0,0 +1,243 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Controls at what target HP % the bot will start casting different spell types" + }; + + std::vector notes = + { + "- 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", + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to start snaring at 99%:", + fmt::format( + "{} {} 99 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 99 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_two = + { + "To set bot Enchbot to start casting Debuffs at 99%:", + fmt::format( + "{} {} 99 byname Enchbot", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Debuff) + ), + fmt::format( + "{} {} 99 byname Enchbot", + sep->arg[0], + BotSpellTypes::Debuff + ) + }; + std::vector examples_three = + { + "To check the current Nuke max threshold on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Nuke + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] maximum threshold is currently [{}%%].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellMaxThreshold(spellType) + ).c_str() + ); + } + else { + my_bot->SetSpellMaxThreshold(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellMaxThreshold(spellType) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] maximum threshold to [{}%%].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..739ab0d1c --- /dev/null +++ b/zone/bot_commands/spell_min_hp_pct.cpp @@ -0,0 +1,236 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Controls at what health percentage a bot will stop casting different spell types" + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to stop snaring when their health is below 10% HP:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_two = + { + "To set BotA to stop casting fast heals at 30% HP:", + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + std::vector examples_three = + { + "To check the current Stun min HP percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum HP is currently [{}%%].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypeMinHPLimit(spellType) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMinHPLimit(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypeMinHPLimit(spellType) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] minimum HP to [{}%%].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..e83362f76 --- /dev/null +++ b/zone/bot_commands/spell_min_mana_pct.cpp @@ -0,0 +1,236 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Controls at what mana percentage a bot will stop casting different spell types" + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to stop snaring when their mana is below 10% HP:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Snare) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Snare + ) + }; + std::vector examples_two = + { + "To set BotA to stop casting fast heals at 30% mana:", + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} 30 byname BotA", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + std::vector examples_three = + { + "To check the current Stun min mana percent on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Stun) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Stun + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum mana is currently [{}%%].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypeMinManaLimit(spellType) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeMinManaLimit(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypeMinManaLimit(spellType) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] minimum mana to [{}%%].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..6df8fc999 --- /dev/null +++ b/zone/bot_commands/spell_min_thresholds.cpp @@ -0,0 +1,244 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Controls at what target HP % the bot will stop casting different spell types" + }; + + std::vector notes = + { + "- 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", + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to stop debuffing at 10%:", + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Debuff) + ), + fmt::format( + "{} {} 10 spawned", + sep->arg[0], + BotSpellTypes::Debuff + ) + }; + std::vector examples_two = + { + "To set all Druids to stop casting DoTs at 15%:", + fmt::format( + "{} {} 15 byclass 6", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::DOT) + ), + fmt::format( + "{} {} 15 byclass 6", + sep->arg[0], + BotSpellTypes::DOT + ) + }; + std::vector examples_three = + { + "To check the current Fast Heal min threshold on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::FastHeals) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid", + "targetgroup, namesgroup, healrotationtargets", + "mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] minimum threshold is currently [{}%%].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellMinThreshold(spellType) + ).c_str() + ); + } + else { + my_bot->SetSpellMinThreshold(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellMinThreshold(spellType) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] minimum threshold to [{}%%].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..593606437 --- /dev/null +++ b/zone/bot_commands/spell_pursue_priority.cpp @@ -0,0 +1,240 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Sets the order of spell casts when the mob is fleeing in combat by spell type" + }; + + std::vector 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." + }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to cast nukes first:", + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Nuke) + ), + fmt::format( + "{} {} 1 spawned", + sep->arg[0], + BotSpellTypes::FastHeals + ) + }; + std::vector examples_two = + { + "To set all Shaman to not cast cures:", + fmt::format( + "{} {} 0 byclass 10", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Cure) + ), + fmt::format( + "{} {} 0 byclass 10", + sep->arg[0], + BotSpellTypes::Cure + ) + }; + std::vector examples_three = + { + "To check the current pursue priority of buffs on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::Buff) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::Buff + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 100) { + c->Message(Chat::Yellow, "You must enter a value between 0-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::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 (!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(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypePriority(spellType, BotPriorityCategories::Pursue) + ).c_str() + ); + } + else { + my_bot->SetSpellTypePriority(spellType, BotPriorityCategories::Pursue, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypePriority(spellType, BotPriorityCategories::Pursue) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] pursue cast priority to [{}].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).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 000000000..f87e232ee --- /dev/null +++ b/zone/bot_commands/spell_target_count.cpp @@ -0,0 +1,236 @@ +#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")) { + return; + } + + if (helper_is_help_or_usage(sep->arg[1])) { + std::vector description = + { + "Decides how many eligible targets are required for an AE or group spell to cast by spell type" + }; + + std::vector notes = { }; + + std::vector example_format = + { + fmt::format( + "{} [Type Shortname] [value] [actionable]" + , sep->arg[0] + ), + fmt::format( + "{} [Type ID] [value] [actionable]" + , sep->arg[0] + ) + }; + std::vector examples_one = + { + "To set all bots to AEMez with 5 or more targets:", + fmt::format( + "{} {} 5 spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::AEMez) + ), + fmt::format( + "{} {} 5 spawned", + sep->arg[0], + BotSpellTypes::AEMez + ) + }; + std::vector examples_two = + { + "To set Wizards to require 5 targets for AENukes:", + fmt::format( + "{} {} 3 byname BotA", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::AENukes) + ), + fmt::format( + "{} {} 3 byname BotA", + sep->arg[0], + BotSpellTypes::AENukes + ) + }; + std::vector examples_three = + { + "To check the current AESlow count on all bots:", + fmt::format( + "{} {} current spawned", + sep->arg[0], + c->GetSpellTypeShortNameByID(BotSpellTypes::AESlow) + ), + fmt::format( + "{} {} current spawned", + sep->arg[0], + BotSpellTypes::AESlow + ) + }; + + std::vector actionables = + { + "target, byname, ownergroup, ownerraid, targetgroup, namesgroup, healrotationtargets, mmr, byclass, byrace, spawned" + }; + + std::vector options = { }; + std::vector options_one = { }; + std::vector options_two = { }; + std::vector options_three = { }; + + std::string popup_text = c->SendCommandHelpWindow( + c, + description, + notes, + example_format, + examples_one, examples_two, examples_three, + actionables, + options, + options_one, options_two, options_three + ); + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient(sep->arg[0], popup_text.c_str()); + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], "", "", true); + 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]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + c->CastToBot()->SendSpellTypesWindow(c, sep->arg[0], sep->arg[1], sep->arg[2]); + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + // String/Int type checks + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (spellType < BotSpellTypes::START || spellType > 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 (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 1 || typeValue > 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::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 (!first_found) { + first_found = my_bot; + } + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "{} says, 'My [{}] target count is currently [{}].'", + my_bot->GetCleanName(), + c->GetSpellTypeNameByID(spellType), + my_bot->GetSpellTypeAEOrGroupTargetCount(spellType) + ).c_str() + ); + } + else { + my_bot->SetSpellTypeAEOrGroupTargetCount(spellType, typeValue); + ++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(), + c->GetSpellTypeNameByID(spellType), + first_found->GetSpellTypeAEOrGroupTargetCount(spellType) + ).c_str() + ); + } + else { + c->Message( + Chat::Green, + fmt::format( + "{} of your bots set their [{}] target count to [{}].", + success_count, + c->GetSpellTypeNameByID(spellType), + typeValue + ).c_str() + ); + } + } +} diff --git a/zone/bot_commands/suspend.cpp b/zone/bot_commands/suspend.cpp index e3198e79e..287db739c 100644 --- a/zone/bot_commands/suspend.cpp +++ b/zone/bot_commands/suspend.cpp @@ -6,7 +6,7 @@ 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; diff --git a/zone/bot_commands/taunt.cpp b/zone/bot_commands/taunt.cpp index a15d29a5b..866e9899c 100644 --- a/zone/bot_commands/taunt.cpp +++ b/zone/bot_commands/taunt.cpp @@ -2,12 +2,15 @@ void bot_command_taunt(Client *c, const Seperator *sep) { - if (helper_command_alias_fail(c, "bot_command_taunt", sep->arg[0], "taunt")) - 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]); + if (helper_command_alias_fail(c, "bot_command_taunt", sep->arg[0], "taunt")) { 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 | mmr | byclass | byrace | spawned] ([actionable_name]))", sep->arg[0]); + return; + } + const int ab_mask = ActionableBots::ABM_Type1; std::string arg1 = sep->arg[1]; @@ -15,26 +18,30 @@ void bot_command_taunt(Client *c, const Seperator *sep) bool taunt_state = false; bool toggle_taunt = true; int ab_arg = 1; + if (!arg1.compare("on")) { taunt_state = true; toggle_taunt = false; - ab_arg = 2; + ++ab_arg; } else if (!arg1.compare("off")) { toggle_taunt = false; - ab_arg = 2; + ++ab_arg; } 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); int taunting_count = 0; diff --git a/zone/bot_commands/timer.cpp b/zone/bot_commands/timer.cpp index 93eab687d..30b495d3a 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,14 +90,17 @@ 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; + 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); for (auto my_bot : sbl) { diff --git a/zone/bot_database.cpp b/zone/bot_database.cpp index a24cd0c30..20715d0d7 100644 --- a/zone/bot_database.cpp +++ b/zone/bot_database.cpp @@ -35,6 +35,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" @@ -400,28 +401,13 @@ bool BotDatabase::LoadBot(const uint32 bot_id, Bot*& loaded_bot) e.spells_id, e.time_spawned, e.zone_id, - t, - e.expansion_bitmask + t ); if (loaded_bot) { loaded_bot->SetSurname(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 +462,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 +526,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); } @@ -868,7 +841,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 +1643,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 +1691,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 +2131,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 +2161,7 @@ std::vector BotDatabase::GetBotIDsByCharacterID(const uint32 character_i class_id ) : "" - ) + ) ) ); @@ -2360,3 +2199,177 @@ 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 mobID = (m->IsClient() ? m->CastToClient()->CharacterID() : m->CastToBot()->GetBotID()); + + std::string query = ""; + + if (m->IsClient()) { + query = fmt::format("`char_id` = {}", mobID); + } + else { + query = fmt::format("`bot_id` = {}", mobID); + } + + const auto& l = BotSettingsRepository::GetWhere(database, query); + + if (l.empty()) { + return true; + } + + for (const auto& e : l) { + LogBotSettings("[{}] says, 'Loading {} [{}] - setting to [{}]." + , m->GetCleanName() + , (e.setting_type == BotSettingCategories::BaseSetting ? m->CastToBot()->GetBotSettingCategoryName(e.setting_id) : m->CastToBot()->GetBotSpellCategoryName(e.setting_id)) + , e.setting_id + , e.value + ); //deleteme + + m->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 botID = (m->IsClient() ? 0 : m->CastToBot()->GetBotID()); + uint32 charID = (m->IsClient() ? m->CastToClient()->CharacterID() : 0); + + std::string query = ""; + + if (m->IsClient()) { + query = fmt::format("`char_id` = {}", charID); + } + else { + query = fmt::format("`bot_id` = {}", botID); + } + + BotSettingsRepository::DeleteWhere(database, query); + + std::vector v; + + if (m->IsBot()) { + for (uint16 i = BotBaseSettings::START; i <= BotBaseSettings::END; ++i) { + if (m->CastToBot()->GetBotBaseSetting(i) != m->CastToBot()->GetDefaultBotBaseSetting(i)) { + auto e = BotSettingsRepository::BotSettings{ + .char_id = charID, + .bot_id = botID, + .setting_id = static_cast(i), + .setting_type = static_cast(BotSettingCategories::BaseSetting), + .value = static_cast(m->CastToBot()->GetBotBaseSetting(i)), + .category_name = m->CastToBot()->GetBotSpellCategoryName(BotSettingCategories::BaseSetting), + .setting_name = m->CastToBot()->GetBotSettingCategoryName(i) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), m->CastToBot()->GetBotSettingCategoryName(i), i, e.value, m->CastToBot()->GetDefaultBotBaseSetting(i)); //deleteme + } + } + + 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)) { + auto e = BotSettingsRepository::BotSettings{ + .char_id = charID, + .bot_id = botID, + .setting_id = static_cast(x), + .setting_type = static_cast(i), + .value = m->CastToBot()->GetSetting(i, x), + .category_name = m->CastToBot()->GetBotSpellCategoryName(i), + .setting_name = m->CastToBot()->GetSpellTypeNameByID(x) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), m->CastToBot()->GetBotSpellCategoryName(i), m->GetSpellTypeNameByID(x), x, e.value, m->CastToBot()->GetDefaultSetting(i, x)); //deleteme + } + } + } + } + + if (m->IsClient()) { + if (m->CastToClient()->GetDefaultBotSettings(BotSettingCategories::BaseSetting, BotBaseSettings::IllusionBlock) != m->GetIllusionBlock()) { // Only illusion block supported + auto e = BotSettingsRepository::BotSettings{ + .char_id = charID, + .bot_id = botID, + .setting_id = static_cast(BotBaseSettings::IllusionBlock), + .setting_type = static_cast(BotSettingCategories::BaseSetting), + .value = m->GetIllusionBlock(), + .category_name = m->CastToBot()->GetBotSpellCategoryName(BotSettingCategories::BaseSetting), + .setting_name = m->CastToBot()->GetBotSettingCategoryName(BotBaseSettings::IllusionBlock) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), m->CastToBot()->GetBotSettingCategoryName(BotBaseSettings::IllusionBlock), BotBaseSettings::IllusionBlock, e.value, m->GetIllusionBlock()); //deleteme + } + + 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(), m->CastToBot()->GetBotSpellCategoryName(i), m->CastToBot()->GetSpellTypeNameByID(x), x, m->CastToClient()->GetBotSetting(i, x), m->CastToClient()->GetDefaultBotSettings(i, x)); //deleteme + if (IsClientBotSpellType(x) && m->CastToClient()->GetBotSetting(i, x) != m->CastToClient()->GetDefaultBotSettings(i, x)) { + auto e = BotSettingsRepository::BotSettings{ + .char_id = charID, + .bot_id = botID, + .setting_id = static_cast(x), + .setting_type = static_cast(i), + .value = m->CastToClient()->GetBotSetting(i, x), + .category_name = m->CastToBot()->GetBotSpellCategoryName(i), + .setting_name = m->CastToBot()->GetSpellTypeNameByID(x) + }; + + v.emplace_back(e); + + LogBotSettings("{} says, 'Saving {} {} [{}] - set to [{}] default [{}].'", m->GetCleanName(), m->CastToBot()->GetBotSpellCategoryName(i), m->CastToBot()->GetSpellTypeNameByID(x), x, e.value, m->CastToClient()->GetDefaultBotSettings(i, x)); //deleteme + } + } + } + } + + 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; +} diff --git a/zone/bot_database.h b/zone/bot_database.h index 139ebc643..3539db5c6 100644 --- a/zone/bot_database.h +++ b/zone/bot_database.h @@ -88,10 +88,6 @@ public: 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 +116,16 @@ public: 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 +196,6 @@ public: 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_structs.h b/zone/bot_structs.h index 50e319e9e..0cfc5913e 100644 --- a/zone/bot_structs.h +++ b/zone/bot_structs.h @@ -92,4 +92,9 @@ struct BotTimer_Struct { uint32 item_id; }; +struct BotSpellTypeOrder { + uint16 spellType; + uint16 priority; +}; + #endif // BOT_STRUCTS diff --git a/zone/botspellsai.cpp b/zone/botspellsai.cpp index 0050a58b2..ee3aaf3a3 100644 --- a/zone/botspellsai.cpp +++ b/zone/botspellsai.cpp @@ -21,1226 +21,528 @@ #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 iChance, uint16 spellType) { if (!tar) { return false; } - if (!AI_HasSpells()) { + LogBotPreChecksDetail("{} says, 'Attempting {} AICastSpell on {}.'", GetCleanName(), GetSpellTypeNameByID(spellType), tar->GetCleanName()); //deleteme + + if ( + !AI_HasSpells() || + (spellType == BotSpellTypes::Pet && tar != this) || + (IsPetBotSpellType(spellType) && !tar->IsPet()) || + (spellType == BotSpellTypes::Buff && tar->IsPet()) || + (spellType == BotSpellTypes::InCombatBuffSong && tar->IsPet()) || + (spellType == BotSpellTypes::OutOfCombatBuffSong && tar->IsPet()) || + (spellType == BotSpellTypes::PreCombatBuffSong && tar->IsPet()) || + (!RuleB(Bots, AllowBuffingHealingFamiliars) && tar->IsFamiliar()) || + (tar->IsPet() && tar->IsCharmed() && spellType == BotSpellTypes::PetBuffs && !RuleB(Bots, AllowCharmedPetBuffs)) || + (tar->IsPet() && tar->IsCharmed() && (spellType == BotSpellTypes::Cure || spellType == BotSpellTypes::GroupCures) && !RuleB(Bots, AllowCharmedPetCures)) || + (tar->IsPet() && tar->IsCharmed() && IsHealBotSpellType(spellType) && !RuleB(Bots, AllowCharmedPetHeals)) + ) { return false; } - + if (iChance < 100 && zone->random.Int(0, 100) > iChance) { return false; } - if (tar->GetAppearance() == eaDead && ((tar->IsClient() && !tar->CastToClient()->GetFeigned()) || tar->IsBot())) { - return false; + if (spellType != BotSpellTypes::Resurrect && tar->GetAppearance() == eaDead) { + if (!((tar->IsClient() && tar->CastToClient()->GetFeigned()) || tar->IsBot())) { + 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. + SetCastedSpellType(UINT16_MAX); 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: + if (SpellTypeRequiresLoS(spellType, botClass) && tar != this) { + SetHasLoS(DoLosChecks(this, tar)); + } + else { + SetHasLoS(true); + } + + switch (spellType) { + case BotSpellTypes::Slow: + if (tar->GetSpecialAbility(SpecialAbility::SlowImmunity)) { + return false; + } + + break; + case BotSpellTypes::Snare: + if (tar->GetSpecialAbility(SpecialAbility::SnareImmunity)) { + return false; + } + + break; + //SpecialAbility::PacifyImmunity -- TODO bot rewrite + case BotSpellTypes::Fear: + if (tar->GetSpecialAbility(SpecialAbility::FearImmunity)) { + return false; + } + + if (!IsCommandedSpell() && (tar->IsRooted() || tar->GetSnaredAmount() == -1)) { + return false; + } + + break; + case BotSpellTypes::Dispel: + if (tar->GetSpecialAbility(SpecialAbility::DispellImmunity)) { + return false; + } + + if (!IsCommandedSpell() && tar->CountDispellableBuffs() <= 0) { + return false; + } + + break; + case BotSpellTypes::HateRedux: + if (!IsCommandedSpell() && !GetNeedsHateRedux(tar)) { + return false; + } + + break; + case BotSpellTypes::InCombatBuff: + if (GetClass() == Class::ShadowKnight && (tar->IsOfClientBot() || (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()))) { + return false; + } + + if (!IsCommandedSpell() && GetClass() != Class::Shaman && spellType == BotSpellTypes::InCombatBuff && IsCasterClass(GetClass()) && GetLevel() >= GetStopMeleeLevel()) { + return false; + } + + break; + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::Buff: + case BotSpellTypes::PetBuffs: + case BotSpellTypes::PreCombatBuff: + case BotSpellTypes::DamageShields: + case BotSpellTypes::ResistBuffs: + if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { + return false; + } + + break; + case BotSpellTypes::PreCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + if (!IsCommandedSpell() && (IsEngaged() || tar->IsEngaged())) { // Out-of-Combat songs can not be cast in combat + return false; + } + + break; + case BotSpellTypes::AEMez: + case BotSpellTypes::Mez: + return BotCastMez(tar, botClass, botSpell, spellType); + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + case BotSpellTypes::AEStun: + case BotSpellTypes::Stun: + return BotCastNuke(tar, botClass, botSpell, spellType); + 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; + } + + return BotCastHeal(tar, botClass, botSpell, spellType); + case BotSpellTypes::GroupCures: + case BotSpellTypes::Cure: + if (!tar->IsOfClientBot() && !(tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot())) { + return false; + } + + return BotCastCure(tar, botClass, botSpell, spellType); + case BotSpellTypes::Pet: + if (HasPet() || IsBotCharmer()) { + return false; + } + + return BotCastPet(tar, botClass, botSpell, spellType); + case BotSpellTypes::Resurrect: + if (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse()) { + return false; + } + + break; + case BotSpellTypes::Charm: + if (tar->IsCharmed() || !tar->IsNPC() || tar->GetSpecialAbility(SpecialAbility::CharmImmunity)) { + return false; + } + + break; default: - return false; + break; + } + + std::list botSpellList = GetPrioritizedBotSpellsBySpellType(this, spellType, tar, IsAEBotSpellType(spellType)); + + for (const auto& s : botSpellList) { + + if (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { + continue; + } + + if (IsInvulnerabilitySpell(s.SpellId)) { + tar = this; //target self for invul type spells + } + + if (IsCommandedSpell() && IsCasting()) { + BotGroupSay( + this, + fmt::format( + "Interrupting {}. I have been commanded to try to cast a [{}] spell, {} on {}.", + CastingSpellID() ? spells[CastingSpellID()].name : "my spell", + GetSpellTypeNameByID(spellType), + spells[s.SpellId].name, + tar->GetCleanName() + ).c_str() + ); + + InterruptSpell(); + } + + if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { + if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType)) { + SetCastedSpellType(UINT16_MAX); + + if (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spellType, tar, true); + } + } + else { + SetCastedSpellType(spellType); + } + + if (botClass != Class::Bard || RuleB(Bots, BardsAnnounceCasts)) { + BotGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(s.SpellId), + GetSpellTypeNameByID(spellType), + (tar == this ? "myself" : tar->GetCleanName()) + ).c_str() + ); + } + + return true; + } } return false; } -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; +bool Bot::BotCastMez(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType) { + std::list botSpellList = GetPrioritizedBotSpellsBySpellType(this, spellType, tar, IsAEBotSpellType(spellType)); - 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: + for (const auto& s : botSpellList) { + if (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { continue; } - if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) - continue; - casted_spell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); - if (casted_spell) - break; - } + if (!IsCommandedSpell()) { + Mob* addMob = GetFirstIncomingMobToMez(this, s.SpellId, spellType, IsAEBotSpellType(spellType)); - return casted_spell; -} - -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; - - 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 (!addMob) { + return false; } - if (casted_spell) { - BotGroupSay( - this, - fmt::format( - "Attempting to reduce hate on {} with {}.", - tar->GetCleanName(), - spells[iter.SpellId].name - ).c_str() - ); + + tar = addMob; + } + + if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { + if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType)) { + SetCastedSpellType(UINT16_MAX); + + if (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spellType, tar, true); + } } + else { + SetCastedSpellType(spellType); + } + + BotGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(s.SpellId), + GetSpellTypeNameByID(spellType), + (tar == this ? "myself" : tar->GetCleanName()) + ).c_str() + ); + + return true; } } - return casted_spell; + return false; } -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); +bool Bot::BotCastCure(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType) { + uint32 currentTime = Timer::GetCurrentTime(); + uint32 nextCureTime = tar->DontCureMeBefore(); - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; + if (!IsCommandedSpell()) { + if ((nextCureTime > currentTime) || !GetNeedsCured(tar)) { + return false; } + } - uint32 TempDontCureMeBeforeTime = tar->DontCureMeBefore(); + botSpell = GetBestBotSpellForCure(this, tar, spellType); - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontCureMeBeforeTime); + if (!IsValidSpellAndLoS(botSpell.SpellId, HasLoS())) { + return false; + } - if (casted_spell && botClass != Class::Bard) { + if (AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost)) { + if (botClass != Class::Bard || RuleB(Bots, BardsAnnounceCasts)) { 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); - } - } - } - 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; - } - 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; - } - } - - 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 + "Curing the group with {}.", + GetSpellName(botSpell.SpellId) ).c_str() ); - } - } - } - return casted_spell; -} -bool Bot::BotCastDOT(Mob* tar, uint8 botLevel, const BotSpell& botSpell, const bool& checked_los) { - bool casted_spell = false; + const std::vector v = GatherGroupSpellTargets(tar); - 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; - } - } - } - 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; - } - } - } - } - 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; - } - - //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 - } - - // 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; - } - - // 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; - } - - 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; - } - } - // 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; - } - } - - if (CheckSpellRecastTimer(s.SpellId)) { - uint32 TempDontBuffMeBefore = tar->DontBuffMeBefore(); - casted_spell = AIDoSpellCast(s.SpellIndex, tar, s.ManaCost, &TempDontBuffMeBefore); - if (TempDontBuffMeBefore != tar->DontBuffMeBefore()) { - tar->SetDontBuffMeBefore(TempDontBuffMeBefore); - } - } - - if (casted_spell) { - return casted_spell; - } - } - } - 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; - } - - botSpell = GetFirstBotSpellBySpellType(this, SpellType_Pet); - } - else if (botClass == Class::Magician) { - botSpell = GetBestBotMagicianPetSpell(this); - } - else { - botSpell = GetFirstBotSpellBySpellType(this, SpellType_Pet); - } - - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - return casted_spell; -} - -bool Bot::BotCastDispel(Mob* tar, BotSpell& botSpell, uint32 iSpellTypes, const bool& checked_los) { - - bool casted_spell = false; - if (tar->GetHPRatio() > 95.0f) { - if (!checked_los && (!CheckLosFN(tar) || !CheckWaterLoS(tar))) { - return casted_spell; - } - - botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); - - if (!IsValidSpell(botSpell.SpellId)) { - return casted_spell; - } - // TODO: Check target to see if there is anything to dispel - - if (tar->CountDispellableBuffs() > 0 && IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - } - return casted_spell; -} - -bool Bot::BotCastNuke(Mob* tar, uint8 botLevel, uint8 botClass, BotSpell& botSpell, const bool& checked_los) { - - 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; - } - - if (botClass == Class::Cleric || botClass == Class::Enchanter) - { - float manaRatioToCast = 75.0f; - - 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 we're at specified mana % or below, don't nuke as cleric or enchanter - if (GetManaRatio() <= manaRatioToCast) - return casted_spell; - } - - 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 (!tar->GetSpecialAbility(SpecialAbility::StunImmunity) && !tar->IsStunned() && (zone->random.Int(1, 100) <= stunChance)) { - botSpell = GetBestBotSpellForStunByTargetType(this, ST_Target); - } - } - - 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; - } - - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - } - 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; - } - - if (IsInvulnerabilitySpell(botSpell.SpellId)) { - tar = this; //target self for invul type spells - } - - if (IsValidSpellRange(botSpell.SpellId, tar) || botClass == Class::Bard) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); - } - } - 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); - - 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; - } - - // 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; - } - - // 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; - } - - 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; - } - 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; - } - uint32 TempDontRootMeBefore = tar->DontRootMeBefore(); - - if (IsValidSpellRange(botSpell.SpellId, tar)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontRootMeBefore); - } - - if (TempDontRootMeBefore != tar->DontRootMeBefore()) { - tar->SetDontRootMeBefore(TempDontRootMeBefore); - } - } - 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); + if (!IsCommandedSpell()) { + for (Mob* m : v) { + SetBotSpellRecastTimer(spellType, m, true); } } } else { - float hpRatioToCast = 0.0f; + BotGroupSay( + this, + fmt::format( + "Curing {} with {}.", + (tar == this ? "myself" : tar->GetCleanName()), + GetSpellName(botSpell.SpellId) + ).c_str() + ); - 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 (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spellType, tar, true); } - //If we're at specified mana % or below, don't heal as hybrid - if (tar->GetHPRatio() <= hpRatioToCast) { - botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); - } - } - - 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 (!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; - } - - uint32 TempDontHealMeBeforeTime = tar->DontHealMeBefore(); - - if (IsValidSpellRange(botSpell.SpellId, tar) || botClass == Class::Bard) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontHealMeBeforeTime); - } - - 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() - ); - - 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); + return true; } } } - return casted_spell; + + return false; } -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))) { +bool Bot::BotCastPet(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType) { + 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 false; + } + if (familiar_buff_slot >= 0) { + BuffFadeBySlot(familiar_buff_slot); + return false; + } + + botSpell = GetFirstBotSpellBySpellType(this, spellType); + } + else if (botClass == Class::Magician) { + botSpell = GetBestBotMagicianPetSpell(this, spellType); + } + else { + botSpell = GetFirstBotSpellBySpellType(this, spellType); + } + + if (!IsValidSpellAndLoS(botSpell.SpellId, HasLoS())) { return false; } - //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 (AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost)) { + SetCastedSpellType(spellType); + BotGroupSay( + this, + fmt::format( + "Summoning a pet [{}].", + GetSpellName(botSpell.SpellId) + ).c_str() + ); - botSpell = GetBestBotSpellForMez(this); + return true; + } + + return false; +} + +bool Bot::BotCastNuke(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType) { + if (spellType == BotSpellTypes::Stun || spellType == BotSpellTypes::AEStun) { + uint8 stunChance = (tar->IsCasting() ? RuleI(Bots, StunCastChanceIfCasting) : RuleI(Bots, StunCastChanceNormal)); + + if (botClass == Class::Paladin) { + stunChance = RuleI(Bots, StunCastChancePaladins); + } + + if ( + !tar->GetSpecialAbility(SpecialAbility::StunImmunity) && + ( + IsCommandedSpell() || + (!tar->IsStunned() && (zone->random.Int(1, 100) <= stunChance)) + ) + ) { + botSpell = GetBestBotSpellForStunByTargetType(this, ST_TargetOptional, spellType, IsAEBotSpellType(spellType), tar); + } + + if (!IsValidSpellAndLoS(botSpell.SpellId, HasLoS())) { + return false; + } + } + + if (!IsValidSpellAndLoS(botSpell.SpellId, HasLoS())) { + botSpell = GetBestBotSpellForNukeByBodyType(this, tar->GetBodyType(), spellType, IsAEBotSpellType(spellType), tar); + } + + if (!IsValidSpellAndLoS(botSpell.SpellId, HasLoS()) && spellType == BotSpellTypes::Nuke && botClass == Class::Wizard) { + botSpell = GetBestBotWizardNukeSpellByTargetResists(this, tar, spellType); + } + + if (!IsValidSpellAndLoS(botSpell.SpellId, HasLoS())) { + std::list botSpellList = GetPrioritizedBotSpellsBySpellType(this, spellType, tar, IsAEBotSpellType(spellType)); + + for (const auto& s : botSpellList) { + if (!IsValidSpellAndLoS(s.SpellId, HasLoS())) { + continue; + } + + if (AIDoSpellCast(s.SpellIndex, tar, s.ManaCost)) { + SetCastedSpellType(spellType); + + if (botClass != Class::Bard || RuleB(Bots, BardsAnnounceCasts)) { + BotGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(s.SpellId), + GetSpellTypeNameByID(spellType), + tar->GetCleanName() + ).c_str() + ); + } + + return true; + } + } + } + else { + if (AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost)) { + SetCastedSpellType(spellType); + + if (botClass != Class::Bard || RuleB(Bots, BardsAnnounceCasts)) { + BotGroupSay( + this, + fmt::format( + "Casting {} [{}] on {}.", + GetSpellName(botSpell.SpellId), + GetSpellTypeNameByID(spellType), + tar->GetCleanName() + ).c_str() + ); + } + + return true; + } + } + + return false; +} + +bool Bot::BotCastHeal(Mob* tar, uint8 botClass, BotSpell& botSpell, uint16 spellType) { + botSpell = GetSpellByHealType(spellType, tar); if (!IsValidSpell(botSpell.SpellId)) { return false; } - Mob* addMob = GetFirstIncomingMobToMez(this, botSpell); + if (AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost)) { + if (botClass != Class::Bard || RuleB(Bots, BardsAnnounceCasts)) { + if (IsGroupSpell(botSpell.SpellId)) { + BotGroupSay( + this, + fmt::format( + "Healing the group with {} [{}].", + GetSpellName(botSpell.SpellId), + GetSpellTypeNameByID(spellType) + ).c_str() + ); - if (!addMob) { - return false; - } + if (botClass != Class::Bard) { + const std::vector v = GatherGroupSpellTargets(tar); - if (!(!addMob->IsImmuneToSpell(botSpell.SpellId, this) && addMob->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) { - return false; - } + if (!IsCommandedSpell()) { + for (Mob* m : v) { + SetBotSpellRecastTimer(spellType, m, true); + } + } + } - if (IsValidSpellRange(botSpell.SpellId, addMob)) { - casted_spell = AIDoSpellCast(botSpell.SpellIndex, addMob, botSpell.ManaCost); - } - if (casted_spell) { - if (raid) { - raid->RaidSay( - GetCleanName(), - fmt::format( - "Attempting to mesmerize {} with {}.", - addMob->GetCleanName(), - spells[botSpell.SpellId].name - ).c_str(), - 0, - 100 - ); - } else { - BotGroupSay( - this, - fmt::format( - "Attempting to mesmerize {} with {}.", - addMob->GetCleanName(), - spells[botSpell.SpellId].name - ).c_str() - ); + } + else { + BotGroupSay( + this, + fmt::format( + "Healing {} with {} [{}].", + (tar == this ? "myself" : tar->GetCleanName()), + GetSpellName(botSpell.SpellId), + GetSpellTypeNameByID(spellType) + ).c_str() + ); + + if (botClass != Class::Bard) { + if (!IsCommandedSpell()) { + SetBotSpellRecastTimer(spellType, tar, true); + } + } + } } + + return true; } - return casted_spell; + + return false; } bool Bot::AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgainBefore) { @@ -1260,14 +562,14 @@ 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()); @@ -1276,7 +578,7 @@ bool Bot::AIDoSpellCast(int32 i, Mob* tar, int32 mana_cost, uint32* oDontDoAgain ( ( ( - (spells[AIBot_spells[i].spellid].target_type==ST_GroupTeleport && AIBot_spells[i].type == SpellType_Heal) || + (spells[AIBot_spells[i].spellid].target_type==ST_GroupTeleport && AIBot_spells[i].type == BotSpellTypes::RegularHeal) || 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 || @@ -1305,32 +607,49 @@ 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 || IsFeared() || IsStunned() || IsMezzed() || DivineAura() || 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()); + LogBotPreChecksDetail("{} says, 'AI_PursueCastCheck started.'", GetCleanName()); //deleteme 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; + } - if (!AICastSpell(GetTarget(), 100, SpellType_Snare)) { - if (!AICastSpell(GetTarget(), 100, SpellType_Lifetap) && !AICastSpell(GetTarget(), 100, SpellType_Nuke)) { + auto castOrder = GetSpellTypesPrioritized(BotPriorityCategories::Pursue); + + for (auto& currentCast : castOrder) { + if (currentCast.priority == 0) { + LogBotPreChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(currentCast.spellType)); //deleteme + continue; + } + + if (currentCast.spellType == BotSpellTypes::Resurrect || currentCast.spellType == BotSpellTypes::Charm) { // Unsupported by AI currently. + continue; + } + + result = AttemptAICastSpell(currentCast.spellType); + + 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,453 +657,104 @@ bool Bot::AI_PursueCastCheck() { } bool Bot::AI_IdleCastCheck() { + if (GetAppearance() == eaDead || IsFeared() || IsStunned() || IsMezzed() || DivineAura() || GetHP() < 0) { + return false; + } + bool result = false; if (AIautocastspell_timer->Check(false)) { + LogAIDetail("Bot Non-Engaged autocast check triggered: [{}]", GetCleanName()); + LogBotPreChecksDetail("{} says, 'AI_IdleCastCheck started.'", GetCleanName()); //deleteme + AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. - bool pre_combat = false; + bool preCombat = false; Client* test_against = nullptr; 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(); } if (test_against) { - pre_combat = test_against->GetBotPrecombat(); + preCombat = 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 castOrder = GetSpellTypesPrioritized(BotPriorityCategories::Idle); + + for (auto& currentCast : castOrder) { + if (currentCast.priority == 0) { + LogBotPreChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(currentCast.spellType)); //deleteme + 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 (!preCombat && (currentCast.spellType == BotSpellTypes::PreCombatBuff || currentCast.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 (currentCast.spellType == BotSpellTypes::Resurrect || currentCast.spellType == BotSpellTypes::Charm) { // Unsupported by AI currently. + 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)) { - } - } - } - } - } - } - } + result = AttemptAICastSpell(currentCast.spellType); - 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 (result) { + break; } - 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)) { - } - } - } - } - } - } - - 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)) { - } - } - } - } - } - 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 = true; - break; - } - default: - 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 || IsFeared() || IsStunned() || IsMezzed() || DivineAura() || GetHP() < 0) { + return false; + } + bool result = false; bool failedToCast = false; if (GetTarget() && AIautocastspell_timer->Check(false)) { + LogAIDetail("Bot Engaged autocast check triggered: [{}]", GetCleanName()); + LogBotPreChecksDetail("{} says, 'AI_EngagedCastCheck started.'", GetCleanName()); //deleteme + AIautocastspell_timer->Disable(); //prevent the timer from going off AGAIN while we are casting. - uint8 botClass = GetClass(); - bool mayGetAggro = HasOrMayGetAggro(); + if (!IsAttackAllowed(GetTarget())) { + return false; + } - LogAIDetail("Engaged autocast check triggered (BOTS). Trying to cast healing spells then maybe offensive spells"); + auto castOrder = GetSpellTypesPrioritized(BotPriorityCategories::Engaged); - 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; - } - } - } - } - } + for (auto& currentCast : castOrder) { + if (currentCast.priority == 0) { + LogBotPreChecksDetail("{} says, '[{}] is priority 0, skipping.'", GetCleanName(), GetSpellTypeNameByID(currentCast.spellType)); //deleteme + continue; } - } - 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; - } - } - } - } - } - } + + if (currentCast.spellType == BotSpellTypes::Resurrect || currentCast.spellType == BotSpellTypes::Charm) { // Unsupported by AI currently. + continue; } - } - 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; - } - } - } - } - } - } - } - } - } - 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; - } - } - } - } - } - } - } - } - 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; - } - } - } - } - } - } - } - } - } - 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; - } - } - } - } - } - } - } - } - 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; - } - } - } - } - } - } - } - } - 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; - } - } - } - } - } - } - } - } - 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(currentCast.spellType); + + if (result) { + break; } } if (!AIautocastspell_timer->Enabled()) { - AIautocastspell_timer->Start(RandomTimer(150, 300), false); - } - - if (!failedToCast) { - result = true; + AIautocastspell_timer->Start(RandomTimer(RuleI(Bots, MinDelayBetweenInCombatCastAttempts), RuleI(Bots, MaxDelayBetweenInCombatCastAttempts)), false); } } @@ -1819,22 +789,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); } } @@ -1879,7 +849,7 @@ bool Bot::AIHealRotation(Mob* tar, bool useFastHeals) { return castedSpell; } -std::list Bot::GetBotSpellsForSpellEffect(Bot* botCaster, int spellEffect) { +std::list Bot::GetBotSpellsForSpellEffect(Bot* botCaster, uint16 spellType, int spellEffect) { std::list result; if (!botCaster) { @@ -1894,13 +864,16 @@ std::list Bot::GetBotSpellsForSpellEffect(Bot* botCaster, int spellEff std::vector botSpellList = botCaster->AIBot_spells; 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 + if (!IsValidSpellAndLoS(botSpellList[i].spellid, botCaster->HasLoS())) { continue; } - if (IsEffectInSpell(botSpellList[i].spellid, spellEffect) || GetSpellTriggerSpellID(botSpellList[i].spellid, spellEffect)) { + if ( + botCaster->CheckSpellRecastTimer(botSpellList[i].spellid) && + (botSpellList[i].type == spellType || botSpellList[i].type == botCaster->GetSpellListSpellType(spellType)) && + botCaster->IsValidSpellTypeBySpellID(spellType, botSpellList[i].spellid) && + (IsEffectInSpell(botSpellList[i].spellid, spellEffect) || GetSpellTriggerSpellID(botSpellList[i].spellid, spellEffect)) + ) { BotSpell botSpell; botSpell.SpellId = botSpellList[i].spellid; botSpell.SpellIndex = i; @@ -1914,7 +887,7 @@ 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* botCaster, uint16 spellType, int spellEffect, SpellTargetType targetType) { std::list result; if (!botCaster) { @@ -1929,18 +902,19 @@ std::list Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, std::vector botSpellList = botCaster->AIBot_spells; 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 + if (!IsValidSpellAndLoS(botSpellList[i].spellid, botCaster->HasLoS())) { continue; } if ( + botCaster->CheckSpellRecastTimer(botSpellList[i].spellid) && + (botSpellList[i].type == spellType || botSpellList[i].type == botCaster->GetSpellListSpellType(spellType)) && + botCaster->IsValidSpellTypeBySpellID(spellType, botSpellList[i].spellid) && ( IsEffectInSpell(botSpellList[i].spellid, spellEffect) || GetSpellTriggerSpellID(botSpellList[i].spellid, spellEffect) ) && - spells[botSpellList[i].spellid].target_type == targetType + (targetType == ST_TargetOptional || spells[botSpellList[i].spellid].target_type == targetType) ) { BotSpell botSpell; botSpell.SpellId = botSpellList[i].spellid; @@ -1954,7 +928,7 @@ std::list Bot::GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, return result; } -std::list Bot::GetBotSpellsBySpellType(Bot* botCaster, uint32 spellType) { +std::list Bot::GetBotSpellsBySpellType(Bot* botCaster, uint16 spellType) { std::list result; if (!botCaster) { @@ -1969,13 +943,15 @@ std::list Bot::GetBotSpellsBySpellType(Bot* botCaster, uint32 spellTyp std::vector botSpellList = botCaster->AIBot_spells; 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 + if (!IsValidSpellAndLoS(botSpellList[i].spellid, botCaster->HasLoS())) { continue; } - if (botSpellList[i].type & spellType) { + if ( + botCaster->CheckSpellRecastTimer(botSpellList[i].spellid) && + (botSpellList[i].type == spellType || botSpellList[i].type == botCaster->GetSpellListSpellType(spellType)) && + botCaster->IsValidSpellTypeBySpellID(spellType, botSpellList[i].spellid) + ) { BotSpell botSpell; botSpell.SpellId = botSpellList[i].spellid; botSpell.SpellIndex = i; @@ -1989,27 +965,59 @@ std::list Bot::GetBotSpellsBySpellType(Bot* botCaster, uint32 spellTyp return result; } -std::list Bot::GetPrioritizedBotSpellsBySpellType(Bot* botCaster, uint32 spellType) { +std::list Bot::GetPrioritizedBotSpellsBySpellType(Bot* botCaster, uint16 spellType, Mob* tar, bool AE) { std::list result; if (botCaster && botCaster->AI_HasSpells()) { std::vector botSpellList = botCaster->AIBot_spells; 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 + if (!IsValidSpellAndLoS(botSpellList[i].spellid, botCaster->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 (spellType == BotSpellTypes::HateRedux && botCaster->GetClass() == Class::Bard) { + if (spells[botSpellList[i].spellid].target_type != ST_Target) { + continue; + } + } - result.push_back(botSpell); + if ( + botCaster->CheckSpellRecastTimer(botSpellList[i].spellid) && + (botSpellList[i].type == spellType || botSpellList[i].type == botCaster->GetSpellListSpellType(spellType)) && + botCaster->IsValidSpellTypeBySpellID(spellType, botSpellList[i].spellid) + ) { + if (!AE && IsAnyAESpell(botSpellList[i].spellid) && !IsGroupSpell(botSpellList[i].spellid)) { + continue; + } + else if (AE && !IsAnyAESpell(botSpellList[i].spellid)) { + continue; + } + + if ( + ( + !botCaster->IsCommandedSpell() || (botCaster->IsCommandedSpell() && (spellType != BotSpellTypes::Mez && spellType != BotSpellTypes::AEMez)) + ) + && (!IsPBAESpell(botSpellList[i].spellid) && !botCaster->CastChecks(botSpellList[i].spellid, tar, spellType, false, IsGroupBotSpellType(spellType))) // TODO bot rewrite - needed for ae spells? + ) { + continue; + } + + if ( + botCaster->IsCommandedSpell() || + !AE || + (spellType == BotSpellTypes::GroupCures) || + (spellType == BotSpellTypes::AEMez) || + (AE && botCaster->HasValidAETarget(botCaster, botSpellList[i].spellid, spellType, tar)) + ) { + BotSpell_wPriority botSpell; + botSpell.SpellId = botSpellList[i].spellid; + botSpell.SpellIndex = i; + botSpell.ManaCost = botSpellList[i].manacost; + botSpell.Priority = botSpellList[i].priority; + + result.push_back(botSpell); + } } } @@ -2025,7 +1033,7 @@ std::list Bot::GetPrioritizedBotSpellsBySpellType(Bot* botCa return result; } -BotSpell Bot::GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType) { +BotSpell Bot::GetFirstBotSpellBySpellType(Bot* botCaster, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2036,13 +1044,15 @@ BotSpell Bot::GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType) { std::vector botSpellList = botCaster->AIBot_spells; 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 + if (!IsValidSpellAndLoS(botSpellList[i].spellid, botCaster->HasLoS())) { continue; } - if ((botSpellList[i].type & spellType) && botCaster->CheckSpellRecastTimer(botSpellList[i].spellid)) { + if ( + botCaster->CheckSpellRecastTimer(botSpellList[i].spellid) && + (botSpellList[i].type == spellType || botSpellList[i].type == botCaster->GetSpellListSpellType(spellType)) && + botCaster->IsValidSpellTypeBySpellID(spellType, botSpellList[i].spellid) + ) { result.SpellId = botSpellList[i].spellid; result.SpellIndex = i; result.ManaCost = botSpellList[i].manacost; @@ -2055,7 +1065,7 @@ BotSpell Bot::GetFirstBotSpellBySpellType(Bot* botCaster, uint32 spellType) { return result; } -BotSpell Bot::GetBestBotSpellForFastHeal(Bot *botCaster) { +BotSpell Bot::GetBestBotSpellForVeryFastHeal(Bot* botCaster, Mob* tar, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2063,11 +1073,12 @@ BotSpell Bot::GetBestBotSpellForFastHeal(Bot *botCaster) { result.ManaCost = 0; if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_CurrentHP); for (auto botSpellListItr : botSpellList) { // Assuming all the spells have been loaded into this list by level and in descending order - if (IsFastHealSpell(botSpellListItr.SpellId) && botCaster->CheckSpellRecastTimer(botSpellListItr.SpellId)) { + if ( + IsVeryFastHealSpell(botSpellListItr.SpellId) && botCaster->CastChecks(botSpellListItr.SpellId, tar, spellType)) { result.SpellId = botSpellListItr.SpellId; result.SpellIndex = botSpellListItr.SpellIndex; result.ManaCost = botSpellListItr.ManaCost; @@ -2080,7 +1091,7 @@ BotSpell Bot::GetBestBotSpellForFastHeal(Bot *botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForFastHeal(Bot *botCaster, Mob* tar, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2088,29 +1099,14 @@ BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* botCaster) { result.ManaCost = 0; if (botCaster) { - std::list botHoTSpellList = GetBotSpellsForSpellEffect(botCaster, SE_HealOverTime); - std::vector botSpellList = botCaster->AIBot_spells; + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_CurrentHP); - for (auto botSpellListItr : botHoTSpellList) { + for (auto botSpellListItr : botSpellList) { // 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(botSpellListItr.SpellId) && botCaster->CastChecks(botSpellListItr.SpellId, tar, spellType)) { + result.SpellId = botSpellListItr.SpellId; + result.SpellIndex = botSpellListItr.SpellIndex; + result.ManaCost = botSpellListItr.ManaCost; break; } @@ -2120,7 +1116,32 @@ BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot *botCaster) { +BotSpell Bot::GetBestBotSpellForHealOverTime(Bot* botCaster, Mob* tar, uint16 spellType) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (botCaster) { + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_HealOverTime); + + for (auto botSpellListItr : botSpellList) { + // Assuming all the spells have been loaded into this list by level and in descending order + if (IsHealOverTimeSpell(botSpellListItr.SpellId) && botCaster->CastChecks(botSpellListItr.SpellId, tar, spellType)) { + result.SpellId = botSpellListItr.SpellId; + result.SpellIndex = botSpellListItr.SpellIndex; + result.ManaCost = botSpellListItr.ManaCost; + + break; + } + } + } + + return result; +} + +BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot *botCaster, Mob* tar, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2129,18 +1150,21 @@ BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot *botCaster) { if (botCaster && botCaster->AI_HasSpells()) { std::vector botSpellList = botCaster->AIBot_spells; - 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 (IsCompleteHealSpell(botSpellList[i].spellid) && botCaster->CheckSpellRecastTimer(botSpellList[i].spellid)) { + if ( + (botSpellList[i].type == spellType || botSpellList[i].type == botCaster->GetSpellListSpellType(spellType)) && + botCaster->IsValidSpellTypeBySpellID(spellType, botSpellList[i].spellid) && + IsCompleteHealSpell(botSpellList[i].spellid) && + botCaster->CastChecks(botSpellList[i].spellid, tar, spellType) + ) { result.SpellId = botSpellList[i].spellid; result.SpellIndex = i; result.ManaCost = botSpellList[i].manacost; + break; } } @@ -2149,7 +1173,7 @@ BotSpell Bot::GetBestBotSpellForPercentageHeal(Bot *botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster, Mob* tar, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2157,14 +1181,15 @@ BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster) { result.ManaCost = 0; if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_CurrentHP); 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)) { + if (IsRegularSingleTargetHealSpell(botSpellListItr->SpellId) && botCaster->CastChecks(botSpellListItr->SpellId, tar, spellType)) { result.SpellId = botSpellListItr->SpellId; result.SpellIndex = botSpellListItr->SpellIndex; result.ManaCost = botSpellListItr->ManaCost; + break; } } @@ -2173,7 +1198,7 @@ BotSpell Bot::GetBestBotSpellForRegularSingleTargetHeal(Bot* botCaster) { return result; } -BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* botCaster) { +BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* botCaster, Mob* tar, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2181,20 +1206,15 @@ BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* botCaster) { result.ManaCost = 0; if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_CurrentHP); 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) || - IsFastHealSpell(botSpellListItr->SpellId) - ) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { + if (IsRegularSingleTargetHealSpell(botSpellListItr->SpellId) && botCaster->CastChecks(botSpellListItr->SpellId, tar, spellType)) { result.SpellId = botSpellListItr->SpellId; result.SpellIndex = botSpellListItr->SpellIndex; result.ManaCost = botSpellListItr->ManaCost; + break; } } @@ -2203,7 +1223,7 @@ BotSpell Bot::GetFirstBotSpellForSingleTargetHeal(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForGroupHeal(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForGroupHeal(Bot* botCaster, Mob* tar, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2211,85 +1231,31 @@ BotSpell Bot::GetBestBotSpellForGroupHeal(Bot* botCaster) { result.ManaCost = 0; if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CurrentHP); + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_CurrentHP); + const std::vector v = botCaster->GatherSpellTargets(); + int targetCount = 0; 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 ( - IsRegularGroupHealSpell(botSpellListItr->SpellId) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; - break; - } - } - } + if (IsRegularGroupHealSpell(botSpellListItr->SpellId)) { + if (!botCaster->IsCommandedSpell()) { + targetCount = 0; - return result; -} - -BotSpell Bot::GetBestBotSpellForGroupHealOverTime(Bot* botCaster) { - 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; - - for (std::list::iterator botSpellListItr = botHoTSpellList.begin(); botSpellListItr != botHoTSpellList.end(); ++botSpellListItr) { - // Assuming all the spells have been loaded into this list by level and in descending order - if (IsGroupHealOverTimeSpell(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; + for (Mob* m : v) { + if (botCaster->IsValidSpellRange(botSpellListItr->SpellId, m) && botCaster->CastChecks(botSpellListItr->SpellId, m, spellType, true, IsGroupBotSpellType(spellType))) { + ++targetCount; + } } - 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 (targetCount < botCaster->GetSpellTypeAEOrGroupTargetCount(spellType)) { + continue; } } - break; - } - } - } - - return result; -} - -BotSpell Bot::GetBestBotSpellForGroupCompleteHeal(Bot* botCaster) { - BotSpell result; - - result.SpellId = 0; - result.SpellIndex = 0; - result.ManaCost = 0; - - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_CompleteHeal); - - 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 ( - IsGroupCompleteHealSpell(botSpellListItr->SpellId) && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { result.SpellId = botSpellListItr->SpellId; result.SpellIndex = botSpellListItr->SpellIndex; result.ManaCost = botSpellListItr->ManaCost; + break; } } @@ -2298,7 +1264,7 @@ BotSpell Bot::GetBestBotSpellForGroupCompleteHeal(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForMez(Bot* botCaster) { +BotSpell Bot::GetBestBotSpellForGroupHealOverTime(Bot* botCaster, Mob* tar, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2306,7 +1272,89 @@ BotSpell Bot::GetBestBotSpellForMez(Bot* botCaster) { result.ManaCost = 0; if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_Mez); + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_HealOverTime); + const std::vector v = botCaster->GatherSpellTargets(); + int targetCount = 0; + + 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 (IsGroupHealOverTimeSpell(botSpellListItr->SpellId)) { + if (!botCaster->IsCommandedSpell()) { + targetCount = 0; + + for (Mob* m : v) { + if (botCaster->IsValidSpellRange(botSpellListItr->SpellId, m) && botCaster->CastChecks(botSpellListItr->SpellId, m, spellType, true, IsGroupBotSpellType(spellType))) { + ++targetCount; + } + } + + if (targetCount < botCaster->GetSpellTypeAEOrGroupTargetCount(spellType)) { + continue; + } + } + + result.SpellId = botSpellListItr->SpellId; + result.SpellIndex = botSpellListItr->SpellIndex; + result.ManaCost = botSpellListItr->ManaCost; + + break; + } + } + } + + return result; +} + +BotSpell Bot::GetBestBotSpellForGroupCompleteHeal(Bot* botCaster, Mob* tar, uint16 spellType) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (botCaster) { + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_CompleteHeal); + const std::vector v = botCaster->GatherSpellTargets(); + int targetCount = 0; + + 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 (IsGroupCompleteHealSpell(botSpellListItr->SpellId)) { + if (!botCaster->IsCommandedSpell()) { + targetCount = 0; + + for (Mob* m : v) { + if (botCaster->IsValidSpellRange(botSpellListItr->SpellId, m) && botCaster->CastChecks(botSpellListItr->SpellId, m, spellType, true, IsGroupBotSpellType(spellType))) { + ++targetCount; + } + } + + if (targetCount < botCaster->GetSpellTypeAEOrGroupTargetCount(spellType)) { + continue; + } + } + + result.SpellId = botSpellListItr->SpellId; + result.SpellIndex = botSpellListItr->SpellIndex; + result.ManaCost = botSpellListItr->ManaCost; + + break; + } + } + } + + return result; +} + +BotSpell Bot::GetBestBotSpellForMez(Bot* botCaster, uint16 spellType) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (botCaster) { + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_Mez); 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 @@ -2326,102 +1374,101 @@ BotSpell Bot::GetBestBotSpellForMez(Bot* botCaster) { return result; } -BotSpell Bot::GetBestBotSpellForMagicBasedSlow(Bot* botCaster) { - BotSpell result; +Mob* Bot::GetFirstIncomingMobToMez(Bot* botCaster, int16 spellid, uint16 spellType, bool AE) { + Mob* result = nullptr; - result.SpellId = 0; - result.SpellIndex = 0; - result.ManaCost = 0; + if (botCaster && botCaster->GetOwner()) { + int spellRange = (!AE ? botCaster->GetActSpellRange(spellid, spells[spellid].range) : botCaster->GetActSpellRange(spellid, spells[spellid].aoe_range)); + int buff_count = 0; + NPC* npc = nullptr; - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_AttackSpeed); + for (auto& close_mob : botCaster->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_MAGIC && - botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) - ) { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; - break; + if (!npc) { + continue; } - } - } - return result; -} - -BotSpell Bot::GetBestBotSpellForDiseaseBasedSlow(Bot* botCaster) { - BotSpell result; - - result.SpellId = 0; - result.SpellIndex = 0; - result.ManaCost = 0; - - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_AttackSpeed); - - 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; - - break; + if (!botCaster->IsValidMezTarget(botCaster->GetOwner(), npc, spellid)) { + continue; } - } - } - return result; -} + if (AE) { + int targetCount = 0; -Mob* Bot::GetFirstIncomingMobToMez(Bot* botCaster, BotSpell botSpell) { - Mob* result = 0; + for (auto& close_mob : botCaster->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 (!botCaster->IsValidMezTarget(botCaster->GetOwner(), m, spellid)) { + continue; + } - for(std::list::iterator itr = npc_list.begin(); itr != npc_list.end(); ++itr) { - NPC* npc = *itr; - - 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 (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 (IsPBAESpell(spellid)) { + if (spellRange < Distance(botCaster->GetPosition(), m->GetPosition())) { + continue; } } + else { + if (spellRange < Distance(m->GetPosition(), npc->GetPosition())) { + continue; + } + } + + if (botCaster->CastChecks(spellid, m, spellType, true, true)) { + ++targetCount; + } + + if (targetCount >= botCaster->GetSpellTypeAEOrGroupTargetCount(spellType)) { + break; + } } + + if (targetCount < botCaster->GetSpellTypeAEOrGroupTargetCount(spellType)) { + continue; + } + + if (zone->random.Int(1, 100) < RuleI(Bots, AEMezChance)) { + botCaster->SetSpellTypeRecastTimer(spellType, RuleI(Bots, MezFailDelay)); + return result; + } + + result = npc; + } + else { + if (spellRange < Distance(botCaster->GetPosition(), npc->GetPosition())) { + continue; + } + + if (!botCaster->CastChecks(spellid, npc, spellType, true)) { + continue; + } + + if (zone->random.Int(1, 100) < RuleI(Bots, MezChance)) { + botCaster->SetSpellTypeRecastTimer(spellType, RuleI(Bots, MezAEFailDelay)); + + return result; + } + + result = npc; } - if (result) - break; + if (result) { + botCaster->SetHasLoS(true); + + return result; + } } } return result; } -BotSpell Bot::GetBestBotMagicianPetSpell(Bot *botCaster) { +BotSpell Bot::GetBestBotMagicianPetSpell(Bot *botCaster, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2429,14 +1476,169 @@ BotSpell Bot::GetBestBotMagicianPetSpell(Bot *botCaster) { result.ManaCost = 0; if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, SE_SummonPet); - + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_SummonPet); std::string petType = GetBotMagicianPetType(botCaster); 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 (IsSummonPetSpell(botSpellListItr->SpellId) && botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) { - if (!strncmp(spells[botSpellListItr->SpellId].teleport_zone, petType.c_str(), petType.length())) { + if ( + IsSummonPetSpell(botSpellListItr->SpellId) && + botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) && + !strncmp(spells[botSpellListItr->SpellId].teleport_zone, petType.c_str(), petType.length()) + ) { + result.SpellId = botSpellListItr->SpellId; + result.SpellIndex = botSpellListItr->SpellIndex; + result.ManaCost = botSpellListItr->ManaCost; + + break; + } + } + } + + return result; +} + +std::string Bot::GetBotMagicianPetType(Bot* botCaster) { + std::string result; + + if (botCaster) { + bool epicAllowed = false; + if (RuleB(Bots, AllowMagicianEpicPet)) { + if (botCaster->GetLevel() >= RuleI(Bots, AllowMagicianEpicPetLevel)) { + if (!RuleI(Bots, RequiredMagicianEpicPetItemID)) { + epicAllowed = true; + } + else { + bool has_item = botCaster->HasBotItem(RuleI(Bots, RequiredMagicianEpicPetItemID)) != INVALID_INDEX; + + if (has_item) { + epicAllowed = true; + } + } + } + } + + if (botCaster->GetPetChooserID() > 0) { + switch (botCaster->GetPetChooserID()) { + case SumWater: + result = std::string("SumWater"); + break; + case SumFire: + result = std::string("SumFire"); + break; + case SumAir: + result = std::string("SumAir"); + break; + case SumEarth: + result = std::string("SumEarth"); + break; + case MonsterSum: + result = std::string("MonsterSum"); + break; + case SumMageMultiElement: + if (epicAllowed) { + result = std::string(RuleS(Bots, EpicPetSpellName)); + + if (result.empty()) { + result = "SumMageMultiElement"; + } + } + else { + result = std::string("MonsterSum"); + } + break; + } + } + else { + if (epicAllowed) { + result = std::string(RuleS(Bots, EpicPetSpellName)); + + if (result.empty()) { + result = "SumMageMultiElement"; + } + } + 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 { + int counter; + + if (botCaster->GetLevel() < 30) { + counter = zone->random.Int(1, 4); + } + else { + counter = zone->random.Int(1, 5); + } + + switch (counter) { + case 1: + result = std::string("SumWater"); + break; + case 2: + result = std::string("SumFire"); + break; + case 3: + result = std::string("SumAir"); + break; + case 4: + result = std::string("SumEarth"); + break; + default: + result = std::string("MonsterSum"); + break; + } + } + } + } + } + + return result; +} + +BotSpell Bot::GetBestBotSpellForNukeByTargetType(Bot* botCaster, SpellTargetType targetType, uint16 spellType, bool AE, Mob* tar) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (tar == nullptr) { + tar = botCaster->GetTarget(); + } + + if (botCaster) { + std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, spellType, SE_CurrentHP, targetType); + + 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)) { + if (!AE && IsAnyAESpell(botSpellListItr->SpellId) && !IsGroupSpell(botSpellListItr->SpellId)) { + continue; + } + else if (AE && !IsAnyAESpell(botSpellListItr->SpellId)) { + continue; + } + + if (!IsPBAESpell(botSpellListItr->SpellId) && !botCaster->CastChecks(botSpellListItr->SpellId, tar, spellType, false, IsGroupBotSpellType(spellType))) { + continue; + } + + + if ( + botCaster->IsCommandedSpell() || + !AE || + (AE && botCaster->HasValidAETarget(botCaster, botSpellListItr->SpellId, spellType, tar)) + ) { result.SpellId = botSpellListItr->SpellId; result.SpellIndex = botSpellListItr->SpellIndex; result.ManaCost = botSpellListItr->ManaCost; @@ -2450,114 +1652,7 @@ BotSpell Bot::GetBestBotMagicianPetSpell(Bot *botCaster) { return result; } -std::string Bot::GetBotMagicianPetType(Bot* botCaster) { - std::string result; - - if (botCaster) { - if (botCaster->IsPetChooser()) { - switch(botCaster->GetPetChooserID()) { - 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; - } - } - 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; - } - } - else { - // Over level 30 - int counter = zone->random.Int(0, 4); - - 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; - } - } - } - } - - return result; -} - -BotSpell Bot::GetBestBotSpellForNukeByTargetType(Bot* botCaster, SpellTargetType targetType) { - BotSpell result; - - result.SpellId = 0; - result.SpellIndex = 0; - result.ManaCost = 0; - - if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, SE_CurrentHP, targetType); - - 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; - - break; - } - } - } - - return result; -} - -BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType targetType) +BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType targetType, uint16 spellType, bool AE, Mob* tar) { BotSpell result; @@ -2565,19 +1660,40 @@ BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType result.SpellIndex = 0; result.ManaCost = 0; + if (tar == nullptr) { + tar = botCaster->GetTarget(); + } + if (botCaster) { - std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, SE_Stun, targetType); + std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, spellType, SE_Stun, targetType); 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 (IsStunSpell(botSpellListItr->SpellId) && botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) - { - result.SpellId = botSpellListItr->SpellId; - result.SpellIndex = botSpellListItr->SpellIndex; - result.ManaCost = botSpellListItr->ManaCost; - break; + if (IsStunSpell(botSpellListItr->SpellId)) { + if (!AE && IsAnyAESpell(botSpellListItr->SpellId) && !IsGroupSpell(botSpellListItr->SpellId)) { + continue; + } + else if (AE && !IsAnyAESpell(botSpellListItr->SpellId)) { + continue; + } + + if (!IsPBAESpell(botSpellListItr->SpellId) && !botCaster->CastChecks(botSpellListItr->SpellId, tar, spellType, false, IsGroupBotSpellType(spellType))) { + continue; + } + + if ( + botCaster->IsCommandedSpell() || + !AE || + (AE && botCaster->HasValidAETarget(botCaster, botSpellListItr->SpellId, spellType, tar)) + ) { + result.SpellId = botSpellListItr->SpellId; + result.SpellIndex = botSpellListItr->SpellIndex; + result.ManaCost = botSpellListItr->ManaCost; + + break; + } } } } @@ -2585,7 +1701,7 @@ BotSpell Bot::GetBestBotSpellForStunByTargetType(Bot* botCaster, SpellTargetType return result; } -BotSpell Bot::GetBestBotWizardNukeSpellByTargetResists(Bot* botCaster, Mob* target) { +BotSpell Bot::GetBestBotWizardNukeSpellByTargetResists(Bot* botCaster, Mob* target, uint16 spellType) { BotSpell result; result.SpellId = 0; @@ -2593,50 +1709,74 @@ BotSpell Bot::GetBestBotWizardNukeSpellByTargetResists(Bot* botCaster, Mob* targ result.ManaCost = 0; if (botCaster && target) { + const int lureResisValue = -100; - const int maxTargetResistValue = 300; + + int32 level_mod = (target->GetLevel() - botCaster->GetLevel()) * (target->GetLevel() - botCaster->GetLevel()) / 2; + + if (target->GetLevel() - botCaster->GetLevel() < 0) { + level_mod = -level_mod; + } + const int maxTargetResistValue = botCaster->GetSpellTypeResistLimit(spellType); bool selectLureNuke = false; - if ((target->GetMR() > maxTargetResistValue) && (target->GetCR() > maxTargetResistValue) && (target->GetFR() > maxTargetResistValue)) + if (((target->GetMR() + level_mod) > maxTargetResistValue) && ((target->GetCR() + level_mod) > maxTargetResistValue) && ((target->GetFR() + level_mod) > maxTargetResistValue)) { selectLureNuke = true; + } - std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, SE_CurrentHP, ST_Target); + std::list botSpellList = GetBotSpellsForSpellEffectAndTargetType(botCaster, spellType, SE_CurrentHP, ST_Target); BotSpell firstWizardMagicNukeSpellFound; firstWizardMagicNukeSpellFound.SpellId = 0; firstWizardMagicNukeSpellFound.SpellIndex = 0; firstWizardMagicNukeSpellFound.ManaCost = 0; + bool spellSelected = false; - for(std::list::iterator botSpellListItr = botSpellList.begin(); botSpellListItr != botSpellList.end(); ++botSpellListItr) { + 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 - bool spellSelected = false; + if (!botCaster->IsValidSpellRange(botSpellListItr->SpellId, target)) { + continue; + } - if (botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) { - if (selectLureNuke && (spells[botSpellListItr->SpellId].resist_difficulty < lureResisValue)) { + if (selectLureNuke && (spells[botSpellListItr->SpellId].resist_difficulty < lureResisValue)) { + if (botCaster->CastChecks(botSpellListItr->SpellId, target, spellType)) { spellSelected = 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 (!selectLureNuke && IsPureNukeSpell(botSpellListItr->SpellId)) { + if ( + ((target->GetMR() < target->GetCR()) || (target->GetMR() < target->GetFR())) && + (GetSpellResistType(botSpellListItr->SpellId) == RESIST_MAGIC) && + (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue) && + botCaster->CastChecks(botSpellListItr->SpellId, target, spellType) + ) { + spellSelected = true; + } + else if ( + ((target->GetCR() < target->GetMR()) || (target->GetCR() < target->GetFR())) && + (GetSpellResistType(botSpellListItr->SpellId) == RESIST_COLD) && + (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue) && + botCaster->CastChecks(botSpellListItr->SpellId, target, spellType) + ) { + spellSelected = true; + } + else if ( + ((target->GetFR() < target->GetCR()) || (target->GetFR() < target->GetMR())) && + (GetSpellResistType(botSpellListItr->SpellId) == RESIST_FIRE) && + (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue) && + botCaster->CastChecks(botSpellListItr->SpellId, target, spellType) + ) { + spellSelected = true; + } + else if ( + (GetSpellResistType(botSpellListItr->SpellId) == RESIST_MAGIC) && + (spells[botSpellListItr->SpellId].resist_difficulty > lureResisValue) && + botCaster->CastChecks(botSpellListItr->SpellId, target, spellType) + ) { + firstWizardMagicNukeSpellFound.SpellId = botSpellListItr->SpellId; + firstWizardMagicNukeSpellFound.SpellIndex = botSpellListItr->SpellIndex; + firstWizardMagicNukeSpellFound.ManaCost = botSpellListItr->ManaCost; } } @@ -2649,6 +1789,25 @@ BotSpell Bot::GetBestBotWizardNukeSpellByTargetResists(Bot* botCaster, Mob* targ } } + if (!spellSelected) { + 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 (botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId)) { + if (botCaster->CastChecks(botSpellListItr->SpellId, target, spellType)) { + spellSelected = true; + } + } + if (spellSelected) { + result.SpellId = botSpellListItr->SpellId; + result.SpellIndex = botSpellListItr->SpellIndex; + result.ManaCost = botSpellListItr->ManaCost; + + break; + } + } + } + if (result.SpellId == 0) { result = firstWizardMagicNukeSpellFound; } @@ -2671,13 +1830,11 @@ BotSpell Bot::GetDebuffBotSpell(Bot* botCaster, Mob *tar) { std::vector botSpellList = botCaster->AIBot_spells; 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 + if (!IsValidSpellAndLoS(botSpellList[i].spellid, botCaster->HasLoS())) { continue; } - if (((botSpellList[i].type & SpellType_Debuff) || IsDebuffSpell(botSpellList[i].spellid)) + if (((botSpellList[i].type == BotSpellTypes::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)) { @@ -2719,13 +1876,11 @@ BotSpell Bot::GetBestBotSpellForResistDebuff(Bot* botCaster, Mob *tar) { std::vector botSpellList = botCaster->AIBot_spells; 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 + if (!IsValidSpellAndLoS(botSpellList[i].spellid, botCaster->HasLoS())) { continue; } - if (((botSpellList[i].type & SpellType_Debuff) || IsResistDebuffSpell(botSpellList[i].spellid)) + if (((botSpellList[i].type == BotSpellTypes::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)) @@ -2746,109 +1901,82 @@ BotSpell Bot::GetBestBotSpellForResistDebuff(Bot* botCaster, Mob *tar) { return result; } -BotSpell Bot::GetBestBotSpellForCure(Bot* botCaster, Mob *tar) { +BotSpell Bot::GetBestBotSpellForCure(Bot* botCaster, Mob* tar, uint16 spellType) { 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) { + std::list botSpellListItr = GetPrioritizedBotSpellsBySpellType(botCaster, spellType, tar); + + if (IsGroupBotSpellType(spellType)) { + const std::vector v = botCaster->GatherGroupSpellTargets(tar); + int countNeedsCured = 0; + uint16 countPoisoned = 0; + uint16 countDiseased = 0; + uint16 countCursed = 0; + uint16 countCorrupted = 0; - if (botCaster && botCaster->AI_HasSpells()) { - std::list cureList = GetPrioritizedBotSpellsBySpellType(botCaster, SpellType_Cure); + for (std::list::iterator itr = botSpellListItr.begin(); itr != botSpellListItr.end(); ++itr) { + if (!IsValidSpell(itr->SpellId) || !IsGroupSpell(itr->SpellId)) { + continue; + } - if (tar->HasGroup()) { - Group *g = tar->GetGroup(); - - if (g) { - for( int i = 0; imembers[i] && !g->members[i]->qglobal) { - if (botCaster->GetNeedsCured(g->members[i])) - countNeedsCured++; + for (Mob* m : v) { + if (botCaster->IsCommandedSpell() || botCaster->GetNeedsCured(m)) { + if (botCaster->CastChecks(itr->SpellId, m, spellType, true, IsGroupBotSpellType(spellType))) { + if (m->FindType(SE_PoisonCounter)) { + ++countPoisoned; + } + if (m->FindType(SE_DiseaseCounter)) { + ++countDiseased; + } + if (m->FindType(SE_CurseCounter)) { + ++countCursed; + } + if (m->FindType(SE_CorruptionCounter)) { + ++countCorrupted; + } + } } } + + if ( + (countPoisoned >= botCaster->GetSpellTypeAEOrGroupTargetCount(spellType) && IsEffectInSpell(itr->SpellId, SE_PoisonCounter)) || + (countDiseased >= botCaster->GetSpellTypeAEOrGroupTargetCount(spellType) && IsEffectInSpell(itr->SpellId, SE_DiseaseCounter)) || + (countCursed >= botCaster->GetSpellTypeAEOrGroupTargetCount(spellType) && IsEffectInSpell(itr->SpellId, SE_CurseCounter)) || + (countCorrupted >= botCaster->GetSpellTypeAEOrGroupTargetCount(spellType) && IsEffectInSpell(itr->SpellId, SE_CorruptionCounter)) + ) { + result.SpellId = itr->SpellId; + result.SpellIndex = itr->SpellIndex; + result.ManaCost = itr->ManaCost; + + break; + } } } - - //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; - } - - if (spellSelected) - { - result.SpellId = selectedBotSpell.SpellId; - result.SpellIndex = selectedBotSpell.SpellIndex; - result.ManaCost = selectedBotSpell.ManaCost; - - break; - } + else { + for (std::list::iterator itr = botSpellListItr.begin(); itr != botSpellListItr.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 +1985,69 @@ BotSpell Bot::GetBestBotSpellForCure(Bot* botCaster, Mob *tar) { return result; } -uint8 Bot::GetChanceToCastBySpellType(uint32 spellType) +uint8 Bot::GetChanceToCastBySpellType(uint16 spellType) //TODO bot rewrite - adjust, move AEs to own rule? { - 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; + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AEStun: + 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::DamageShields: + return RuleI(Bots, PercentChanceToCastBuff); + case BotSpellTypes::Escape: + return RuleI(Bots, PercentChanceToCastEscape); + case BotSpellTypes::Lifetap: + return RuleI(Bots, PercentChanceToCastLifetap); + case BotSpellTypes::AESnare: + 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::AEMez: + case BotSpellTypes::Mez: + return RuleI(Bots, PercentChanceToCastMez); + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + return RuleI(Bots, PercentChanceToCastSlow); + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + return RuleI(Bots, PercentChanceToCastDebuff); + case BotSpellTypes::Cure: + return RuleI(Bots, PercentChanceToCastCure); + case BotSpellTypes::HateRedux: + return RuleI(Bots, PercentChanceToCastHateRedux); + case BotSpellTypes::AEFear: + case BotSpellTypes::Fear: + return RuleI(Bots, PercentChanceToCastFear); + case BotSpellTypes::RegularHeal: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::FastHeals: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::GroupHoTHeals: + 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); } - 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); + return RuleI(Bots, PercentChanceToCastOtherType); } bool Bot::AI_AddBotSpells(uint32 bot_spell_id) { @@ -3340,7 +2429,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 (!BOT_SPELL_TYPES_INNATE(entry.type) && entry.priority == 0) { entry.priority = 1; } @@ -3504,6 +2593,460 @@ bool Bot::IsValidSpellRange(uint16 spell_id, Mob const* tar) { if (spellrange >= DistanceSquared(m_Position, tar->GetPosition())) { return true; } + + spellrange = (GetActSpellRange(spell_id, spells[spell_id].aoe_range) * GetActSpellRange(spell_id, spells[spell_id].aoe_range)); + if (spellrange >= DistanceSquared(m_Position, tar->GetPosition())) { + return true; + } } + return false; } + +BotSpell Bot::GetBestBotSpellForNukeByBodyType(Bot* botCaster, uint8 bodyType, uint16 spellType, bool AE, Mob* tar) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (!botCaster || !bodyType) { + return result; + } + + if (tar == nullptr) { + tar = botCaster->GetTarget(); + } + + switch (bodyType) { + case BodyType::Undead: + case BodyType::SummonedUndead: + case BodyType::Vampire: + result = GetBestBotSpellForNukeByTargetType(botCaster, (!AE ? ST_Undead : ST_UndeadAE), spellType, AE, tar); + break; + case BodyType::Summoned: + case BodyType::Summoned2: + case BodyType::Summoned3: + result = GetBestBotSpellForNukeByTargetType(botCaster, (!AE ? ST_Summoned : ST_SummonedAE), spellType, AE, tar); + break; + case BodyType::Animal: + result = GetBestBotSpellForNukeByTargetType(botCaster, ST_Animal, spellType, AE, tar); + break; + case BodyType::Plant: + result = GetBestBotSpellForNukeByTargetType(botCaster, ST_Plant, spellType, AE, tar); + break; + case BodyType::Giant: + result = GetBestBotSpellForNukeByTargetType(botCaster, ST_Giant, spellType, AE, tar); + break; + case BodyType::Dragon: + result = GetBestBotSpellForNukeByTargetType(botCaster, ST_Dragon, spellType, AE, tar); + break; + default: + break; + } + + return result; +} + +void Bot::CheckBotSpells() { + bool valid = false; + uint16 correctType; + auto spellList = BotSpellsEntriesRepository::All(content_db); + uint16 spell_id; + + for (const auto& s : spellList) { + if (!IsValidSpell(s.spell_id)) { + LogBotSpellTypeChecks("{} is an invalid spell", s.spell_id); //deleteme + continue; + } + + spell_id = s.spell_id; + + if (spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] >= 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); //deleteme + } + else { + if (spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] > s.minlevel) { + LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the min level is currently set to {}." + , GetSpellName(spell_id) + , spell_id + , spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + , s.minlevel + ); //deleteme + + LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]" + , spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] + , spell_id + , s.npc_spells_id + , GetSpellName(spell_id) + , spell_id + , s.minlevel + , spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + ); //deleteme + } + + if (spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] < s.minlevel) { + LogBotSpellTypeChecks("{} [#{}] could be used starting at level {} for a {} [#{}] instead of the current min level of {}." + , GetSpellName(spell_id) + , spell_id + , spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + , s.minlevel + ); //deleteme + + LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `minlevel` = {} WHERE `spellid` = {} AND `npc_spells_id` = {}; -- {} [#{}] from minlevel {} to {} for {} [#{}]" + , spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] + , spell_id + , s.npc_spells_id + , GetSpellName(spell_id) + , spell_id + , s.minlevel + , spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + ); //deleteme + } + + + if (spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] > s.maxlevel) { + LogBotSpellTypeChecks("{} [#{}] is not usable until level {} for a {} [#{}] and the max level is currently set to {}." + , GetSpellName(spell_id) + , spell_id + , spells[spell_id].classes[s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX] + , GetClassIDName(s.npc_spells_id - BOT_CLASS_BASE_ID_PREFIX) + , s.npc_spells_id + , s.maxlevel + ); //deleteme + } + } + + correctType = UINT16_MAX; + valid = false; + + + switch (s.type) { + case BotSpellTypes::Nuke: //DONE + if (IsAnyNukeOrStunSpell(spell_id) && !IsEffectInSpell(spell_id, SE_Root) && !IsDebuffSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::RegularHeal: //DONE + //if (IsAnyHealSpell(spell_id) && !IsEscapeSpell(spell_id) && (IsRegularPetHealSpell(spell_id) || !IsCureSpell(spell_id))) { + if (IsAnyHealSpell(spell_id) && !IsEscapeSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Root: //DONE + if (IsEffectInSpell(spell_id, SE_Root)) { + valid = true; + break; + } + break; + case BotSpellTypes::Buff: //DONE + if (IsAnyBuffSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Pet: //DONE + if (IsSummonPetSpell(spell_id) || IsEffectInSpell(spell_id, SE_TemporaryPets)) { + valid = true; + break; + } + break; + case BotSpellTypes::Lifetap: //DONE + if (IsLifetapSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Snare: //DONE + if (IsEffectInSpell(spell_id, SE_MovementSpeed) && IsDetrimentalSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::DOT: //DONE + if (IsStackableDOT(spell_id) || IsDamageOverTimeSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Dispel: //DONE + if (IsDispelSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::InCombatBuff: //DONE + if ( + IsSelfConversionSpell(spell_id) || + IsAnyBuffSpell(spell_id) || + (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) + ) { + valid = true; + break; + } + break; + case BotSpellTypes::Mez: //DONE + if (IsMesmerizeSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Charm: //DONE + if (IsCharmSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Slow: //DONE + if (IsSlowSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Debuff: //DONE + if (IsDebuffSpell(spell_id) && !IsEscapeSpell(spell_id) && !IsHateReduxSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Cure: //DONE + if (IsCureSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::PreCombatBuff: //DONE + if ( + IsBuffSpell(spell_id) && + IsBeneficialSpell(spell_id) && + !IsBardSong(spell_id) && + !IsEscapeSpell(spell_id) && + (!IsSummonPetSpell(spell_id) && !IsEffectInSpell(spell_id, SE_TemporaryPets)) + ) { + valid = true; + break; + } + break; + case BotSpellTypes::InCombatBuffSong: //DONE + case BotSpellTypes::OutOfCombatBuffSong: //DONE + case BotSpellTypes::PreCombatBuffSong: //DONE + if ( + IsBuffSpell(spell_id) && + IsBeneficialSpell(spell_id) && + IsBardSong(spell_id) && + !IsEscapeSpell(spell_id) && + (!IsSummonPetSpell(spell_id) && !IsEffectInSpell(spell_id, SE_TemporaryPets)) + ) { + valid = true; + break; + } + break; + case BotSpellTypes::Fear: //DONE + if (IsFearSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Escape: + if (IsEscapeSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::HateRedux: + if (IsHateReduxSpell(spell_id)) { + valid = true; + break; + } + break; + case BotSpellTypes::Resurrect: + if (IsEffectInSpell(spell_id, SE_Revive)) { + valid = true; + break; + } + break; + default: + break; + + } + + if (IsAnyNukeOrStunSpell(spell_id) && !IsEffectInSpell(spell_id, SE_Root) && !IsDebuffSpell(spell_id)) { + correctType = BotSpellTypes::Nuke; + } + //else if (IsAnyHealSpell(spell_id) && !IsEscapeSpell(spell_id) && (IsRegularPetHealSpell(spell_id) || !IsCureSpell(spell_id))) { + else if (IsAnyHealSpell(spell_id) && !IsEscapeSpell(spell_id)) { + correctType = BotSpellTypes::RegularHeal; + } + else if (IsEffectInSpell(spell_id, SE_Root)) { + correctType = BotSpellTypes::Root; + } + else if (IsAnyBuffSpell(spell_id)) { + correctType = BotSpellTypes::Buff; + } + else if (IsSummonPetSpell(spell_id) || IsEffectInSpell(spell_id, SE_TemporaryPets)) { + correctType = BotSpellTypes::Pet; + } + else if (IsLifetapSpell(spell_id)) { + correctType = BotSpellTypes::Lifetap; + } + else if (IsEffectInSpell(spell_id, SE_MovementSpeed) && IsDetrimentalSpell(spell_id)) { + correctType = BotSpellTypes::Snare; + } + else if (IsStackableDOT(spell_id) || IsDamageOverTimeSpell(spell_id)) { + correctType = BotSpellTypes::DOT; + } + else if (IsDispelSpell(spell_id)) { + correctType = BotSpellTypes::Dispel; + } + else if ( + IsSelfConversionSpell(spell_id) || + IsAnyBuffSpell(spell_id) || + (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) + ) { + correctType = BotSpellTypes::InCombatBuff; + } + else if (IsMesmerizeSpell(spell_id)) { + correctType = BotSpellTypes::Mez; + } + else if (IsCharmSpell(spell_id)) { + correctType = BotSpellTypes::Charm; + } + else if (IsSlowSpell(spell_id)) { + correctType = BotSpellTypes::Slow; + } + else if (IsDebuffSpell(spell_id) && !IsEscapeSpell(spell_id) && !IsHateReduxSpell(spell_id)) { + correctType = BotSpellTypes::Debuff; + } + else if (IsCureSpell(spell_id)) { + correctType = BotSpellTypes::Cure; + } + else if ( + IsBuffSpell(spell_id) && + IsBeneficialSpell(spell_id) && + IsBardSong(spell_id) && + !IsEscapeSpell(spell_id) && + (!IsSummonPetSpell(spell_id) && !IsEffectInSpell(spell_id, SE_TemporaryPets)) + ) { + if ( + s.type == BotSpellTypes::InCombatBuffSong || + s.type == BotSpellTypes::OutOfCombatBuffSong || + s.type == BotSpellTypes::PreCombatBuffSong + ) { + correctType = s.type; + } + else { + correctType = BotSpellTypes::OutOfCombatBuffSong; + } + } + else if ( + IsBuffSpell(spell_id) && + IsBeneficialSpell(spell_id) && + !IsBardSong(spell_id) && + !IsEscapeSpell(spell_id) && + (!IsSummonPetSpell(spell_id) && !IsEffectInSpell(spell_id, SE_TemporaryPets)) + ) { + correctType = BotSpellTypes::PreCombatBuff; + } + else if (IsFearSpell(spell_id)) { + correctType = BotSpellTypes::Fear; + } + else if (IsEscapeSpell(spell_id)) { + correctType = BotSpellTypes::Escape; + } + else if (IsHateReduxSpell(spell_id)) { + correctType = BotSpellTypes::HateRedux; + } + else if (IsEffectInSpell(spell_id, SE_Revive)) { + correctType = BotSpellTypes::Resurrect; + } + + if (!valid || (correctType == UINT16_MAX) || (s.type != correctType)) { + LogBotSpellTypeChecks("{} [#{}] is incorrect. It is currently set as {} [#{}] and should be {} [#{}]" + , GetSpellName(spell_id) + , spell_id + , GetSpellTypeNameByID(s.type) + , s.type + , GetSpellTypeNameByID(correctType) + , correctType + ); //deleteme + LogBotSpellTypeChecksDetail("UPDATE bot_spells_entries SET `type` = {} WHERE `spellid` = {}; -- {} [#{}] from {} [#{}] to {} [#{}]" + , correctType + , spell_id + , GetSpellName(spell_id) + , spell_id + , GetSpellTypeNameByID(s.type) + , s.type + , GetSpellTypeNameByID(correctType) + , correctType + ); //deleteme + } + } +} + +BotSpell Bot::GetBestBotSpellForRez(Bot* botCaster, Mob* target, uint16 spellType) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (botCaster) { + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_Revive); + + 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 ( + IsResurrectSpell(botSpellListItr->SpellId) && + botCaster->CheckSpellRecastTimer(botSpellListItr->SpellId) + ) { + result.SpellId = botSpellListItr->SpellId; + result.SpellIndex = botSpellListItr->SpellIndex; + result.ManaCost = botSpellListItr->ManaCost; + + break; + } + } + } + + return result; +} + +BotSpell Bot::GetBestBotSpellForCharm(Bot* botCaster, Mob* target, uint16 spellType) { + BotSpell result; + + result.SpellId = 0; + result.SpellIndex = 0; + result.ManaCost = 0; + + if (botCaster) { + std::list botSpellList = GetBotSpellsForSpellEffect(botCaster, spellType, SE_Charm); + + 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 ( + IsCharmSpell(botSpellListItr->SpellId) && + botCaster->CastChecks(botSpellListItr->SpellId, target, spellType) + ) { + result.SpellId = botSpellListItr->SpellId; + result.SpellIndex = botSpellListItr->SpellIndex; + result.ManaCost = botSpellListItr->ManaCost; + + break; + } + } + } + + return result; +} diff --git a/zone/client.cpp b/zone/client.cpp index 920553f5c..fe36a6056 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" @@ -775,6 +776,8 @@ bool Client::Save(uint8 iCommitNow) { database.SaveCharacterEXPModifier(this); + database.botdb.SaveBotSettings(this); + return true; } @@ -5871,7 +5874,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(); @@ -12896,11 +12899,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 ); } @@ -13056,8 +13059,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; } @@ -13075,3 +13078,259 @@ void Client::ClientToNpcAggroProcess() LogAggro("Checking Reverse Aggro (client->npc) scanned_npcs ([{}])", npc_scan_count); } } + +void Client::LoadDefaultBotSettings() { + // 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)); //deleteme + + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + BotSpellSettings_Struct t; + + t.spellType = i; + t.shortName = GetSpellTypeShortNameByID(i); + t.name = GetSpellTypeNameByID(i); + t.hold = GetDefaultSpellHold(i); + t.delay = GetDefaultSpellDelay(i); + t.minThreshold = GetDefaultSpellMinThreshold(i); + t.maxThreshold = GetDefaultSpellMaxThreshold(i); + + _spellSettings.push_back(t); + + LogBotSettingsDetail("{} says, 'Setting defaults for {} ({}) [#{}]'", GetCleanName(), t.name, t.shortName, t.spellType); //deleteme + LogBotSettingsDetail("{} says, 'Hold = [{}] | Delay = [{}ms] | MinThreshold = [{}\%] | MaxThreshold = [{}\%]'", GetCleanName(), GetDefaultSpellHold(i), GetDefaultSpellDelay(i), GetDefaultSpellMinThreshold(i), GetDefaultSpellMaxThreshold(i)); //deleteme + } +} + +int Client::GetDefaultBotSettings(uint8 settingType, uint16 botSetting) { + switch (settingType) { + case BotSettingCategories::BaseSetting: + return false; + case BotSettingCategories::SpellHold: + return GetDefaultSpellHold(botSetting); + case BotSettingCategories::SpellDelay: + return GetDefaultSpellDelay(botSetting); + case BotSettingCategories::SpellMinThreshold: + return GetDefaultSpellMinThreshold(botSetting); + case BotSettingCategories::SpellMaxThreshold: + return GetDefaultSpellMaxThreshold(botSetting); + } +} + +int Client::GetBotSetting(uint8 settingType, uint16 botSetting) { + switch (settingType) { + case BotSettingCategories::SpellHold: + return GetSpellHold(botSetting); + case BotSettingCategories::SpellDelay: + return GetSpellDelay(botSetting); + case BotSettingCategories::SpellMinThreshold: + return GetSpellMinThreshold(botSetting); + case BotSettingCategories::SpellMaxThreshold: + return GetSpellMaxThreshold(botSetting); + } +} + +void Client::SetBotSetting(uint8 settingType, uint16 botSetting, uint32 settingValue) { + switch (settingType) { + case BotSettingCategories::BaseSetting: + SetBaseSetting(botSetting, settingValue); + break; + case BotSettingCategories::SpellHold: + SetSpellHold(botSetting, settingValue); + break; + case BotSettingCategories::SpellDelay: + SetSpellDelay(botSetting, settingValue); + break; + case BotSettingCategories::SpellMinThreshold: + SetSpellMinThreshold(botSetting, settingValue); + break; + case BotSettingCategories::SpellMaxThreshold: + SetSpellMaxThreshold(botSetting, settingValue); + break; + } +} + +std::string Client::SendCommandHelpWindow( + Client* c, + 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 +) { + + unsigned stringLength = 0; + unsigned currentPlace = 0; + uint16 maxLength = RuleI(Command, MaxHelpLineLength); //character length of a line before splitting in to multiple lines + const std::string& description_color = RuleS(Command, DescriptionColor); + const std::string& description_header_color = RuleS(Command, DescriptionHeaderColor); + const std::string& alt_description_color = RuleS(Command, AltDescriptionColor); + const std::string& note_color = RuleS(Command, NoteColor); + const std::string& note_header_color = RuleS(Command, NoteHeaderColor); + const std::string& alt_note_color = RuleS(Command, AltNoteColor); + const std::string& example_color = RuleS(Command, ExampleColor); + const std::string& example_header_color = RuleS(Command, ExampleHeaderColor); + const std::string& sub_example_color = RuleS(Command, SubExampleColor); + const std::string& alt_example_color = RuleS(Command, AltExampleColor); + const std::string& sub_alt_example_color = RuleS(Command, SubAltExampleColor); + const std::string& option_color = RuleS(Command, OptionColor); + const std::string& option_header_color = RuleS(Command, OptionHeaderColor); + const std::string& sub_option_color = RuleS(Command, SubOptionColor); + const std::string& alt_option_color = RuleS(Command, AltOptionColor); + const std::string& sub_alt_option_color = RuleS(Command, SubAltOptionColor); + const std::string& actionable_color = RuleS(Command, ActionableColor); + const std::string& actionable_header_color = RuleS(Command, ActionableHeaderColor); + const std::string& alt_actionable_color = RuleS(Command, AltActionableColor); + const std::string& header_color = RuleS(Command, HeaderColor); + const std::string& secondary_header_color = RuleS(Command, SecondaryHeaderColor); + const std::string& alt_header_color = RuleS(Command, AltHeaderColor); + const std::string& filler_line_color = RuleS(Command, FillerLineColor); + + + + std::string fillerLine = "--------------------------------------------------------------------"; + std::string fillerDia = DialogueWindow::TableRow(DialogueWindow::TableCell(fmt::format("{}", DialogueWindow::ColorMessage(filler_line_color, fillerLine)))); + std::string breakLine = DialogueWindow::Break(); + std::string indent = "        "; + std::string bullet = "- "; + std::string popup_text = ""; + + /* + maxLength is how long you want lines to be before splitting them. This will look for the last space before the count and split there so words are not split mid sentence + Any SplitCommandHelpText can be be have the first string from a vector differ in color from the next strings by setting secondColor to true and assigning a color. + ex: SplitCommandHelpText(examples_one, example_color, maxLength, true, alt_example_color) + - This will make the first string from examples_one vector be the color of example_color and all following strings the color of alt_example_color + ex: SplitCommandHelpText(examples_one, example_color, maxLength) + - This will apply the color example_color to everything in examples_one vector + */ + + if (!description.empty()) { + popup_text += GetCommandHelpHeader(description_header_color, "[Description]"); + popup_text += SplitCommandHelpText(description, description_color, maxLength, true, alt_description_color); + } + + if (!notes.empty()) { + popup_text += breakLine; + popup_text += breakLine; + popup_text += GetCommandHelpHeader(note_header_color, "[Notes]"); + popup_text += SplitCommandHelpText(notes, note_color, maxLength, true, alt_note_color); + } + + if (!example_format.empty()) { + popup_text += fillerDia; + popup_text += GetCommandHelpHeader(example_header_color, "[Examples]"); + popup_text += SplitCommandHelpText(example_format, example_color, maxLength, true, alt_example_color); + } + + if (!examples_one.empty()) { + popup_text += breakLine; + popup_text += breakLine; + popup_text += SplitCommandHelpText(examples_one, sub_example_color, maxLength, true, sub_alt_example_color); + } + + if (!examples_two.empty()) { + popup_text += SplitCommandHelpText(examples_two, sub_example_color, maxLength, true, sub_alt_example_color); + } + + if (!examples_three.empty()) { + popup_text += SplitCommandHelpText(examples_three, sub_example_color, maxLength, true, sub_alt_example_color); + } + + if (!options.empty()) { + popup_text += fillerDia; + popup_text += GetCommandHelpHeader(option_header_color, "[Options]"); + popup_text += SplitCommandHelpText(options, option_color, maxLength, true, alt_option_color); + } + + if (!options_one.empty()) { + popup_text += breakLine; + popup_text += breakLine; + popup_text += SplitCommandHelpText(options_one, sub_option_color, maxLength, true, sub_alt_option_color); + } + + if (!options_two.empty()) { + popup_text += SplitCommandHelpText(options_two, sub_option_color, maxLength, true, sub_alt_option_color); + } + + if (!options_three.empty()) { + popup_text += SplitCommandHelpText(options_three, secondary_header_color, maxLength, true, sub_alt_option_color); + } + + if (!actionables.empty()) { + popup_text += fillerDia; + popup_text += GetCommandHelpHeader(actionable_header_color, "[Actionables]"); + popup_text += SplitCommandHelpText(actionables, actionable_color, maxLength, true, alt_actionable_color); + } + + popup_text = DialogueWindow::Table(popup_text); + + return popup_text; +} + +std::string Client::GetCommandHelpHeader(std::string color, std::string header) { + std::string returnText = DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(color, header) + ) + ) + ); + + return returnText; +} + +std::string Client::SplitCommandHelpText(std::vector msg, std::string color, uint16 maxLength, bool secondColor, std::string secondaryColor) { + std::string returnText; + + for (int i = 0; i < msg.size(); i++) { + std::vector msg_split; + int stringLength = msg[i].length() + 1; + int endCount = 0; + int newCount = 0; + int splitCount = 0; + + for (int x = 0; x < stringLength; x = endCount) { + endCount = std::min(int(stringLength), (int(x) + std::min(int(stringLength), int(maxLength)))); + + if ((stringLength - (x + 1)) > maxLength) { + for (int y = endCount; y >= x; --y) { + if (msg[i][y] == ' ') { + splitCount = y - x; + msg_split.emplace_back(msg[i].substr(x, splitCount)); + endCount = y + 1; + + break; + } + + if (y == x) { + msg_split.emplace_back(msg[i].substr(x, maxLength)); + + break; + } + } + } + else { + msg_split.emplace_back(msg[i].substr(x, (stringLength - 1) - x)); + + break; + } + } + + for (const auto& s : msg_split) { + returnText += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(((secondColor && i == 0) ? color : secondaryColor), s) + ) + ) + ); + + } + } + + return returnText; +} diff --git a/zone/client.h b/zone/client.h index 5acc388b3..71310dd84 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1250,6 +1250,20 @@ public: PendingTranslocate_Struct PendingTranslocateData; void SendOPTranslocateConfirm(Mob *Caster, uint16 SpellID); + // Help Window + std::string SendCommandHelpWindow( + Client* c, + 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 + ); + std::string GetCommandHelpHeader(std::string color, std::string header); + std::string SplitCommandHelpText(std::vector msg, std::string color, uint16 maxLength, bool secondColor = false, std::string secondaryColor = ""); + // Task System Methods void LoadClientTaskState(); void RemoveClientTaskState(); @@ -2219,11 +2233,29 @@ public: void CampAllBots(uint8 class_id = Class::None); void SpawnRaidBotsOnConnect(Raid* raid); + void LoadDefaultBotSettings(); + int GetDefaultBotSettings(uint8 settingType, uint16 botSetting); + int GetBotSetting(uint8 settingType, uint16 botSetting); + void SetBotSetting(uint8 settingType, uint16 botSetting, uint32 settingValue); + private: bool bot_owner_options[_booCount]; bool m_bot_pulling; bool m_bot_precombat; + uint8 fast_heal_threshold; + uint8 heal_threshold; + uint8 complete_heal_threshold; + uint8 hot_heal_threshold; + uint32 fast_heal_delay; + uint32 heal_delay; + uint32 complete_heal_delay; + uint32 hot_heal_delay; + uint32 cure_delay; + uint8 cure_min_threshold; + uint8 cure_threshold; + bool illusion_block; + bool CanTradeFVNoDropItem(); void SendMobPositions(); void PlayerTradeEventLog(Trade *t, Trade *t2); diff --git a/zone/client_bot.cpp b/zone/client_bot.cpp index 1f3f809dd..bfe4cdb58 100644 --- a/zone/client_bot.cpp +++ b/zone/client_bot.cpp @@ -19,6 +19,10 @@ uint32 Client::GetBotCreationLimit(uint8 class_id) { uint32 bot_creation_limit = RuleI(Bots, CreationLimit); + if (Admin() >= RuleI(Bots, MinStatusToBypassCreateLimit)) { + return RuleI(Bots, MinStatusToBypassCreateLimit); + } + const auto bucket_name = fmt::format( "bot_creation_limit{}", ( @@ -67,6 +71,10 @@ int Client::GetBotSpawnLimit(uint8 class_id) { int bot_spawn_limit = RuleI(Bots, SpawnLimit); + if (Admin() >= RuleI(Bots, MinStatusToBypassSpawnLimit)) { + return RuleI(Bots, MinStatusToBypassSpawnLimit); + } + const auto bucket_name = fmt::format( "bot_spawn_limit{}", ( diff --git a/zone/client_mods.cpp b/zone/client_mods.cpp index 0387b795b..23ef13454 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 4afe67935..06c7e7c60 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -412,7 +412,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; @@ -666,6 +666,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]); @@ -1483,6 +1487,12 @@ void Client::Handle_Connect_OP_ZoneEntry(const EQApplicationPacket *app) LogError("Error loading AA points for [{}]", GetName()); } + /* Load Bots */ + + 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) @@ -1579,7 +1589,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 diff --git a/zone/command.cpp b/zone/command.cpp index 513cd0218..21f57a709 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -147,6 +147,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::Player, 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) || @@ -217,6 +218,10 @@ int command_init(void) command_add("spawn", "[name] [race] [level] [material] [hp] [gender] [class] [priweapon] [secweapon] [merchantid] - Spawn an NPC", AccountStatus::Steward, command_spawn) || command_add("spawneditmass", "[Search Criteria] [Edit Option] [Edit Value] [Apply] Mass editing spawn command (Apply is optional, 0 = False, 1 = True, default is False)", AccountStatus::GMLeadAdmin, command_spawneditmass) || command_add("spawnfix", "Find targeted NPC in database based on its X/Y/heading and update the database to make it spawn at your current location/heading.", AccountStatus::GMAreas, command_spawnfix) || + command_add("spelldelays", "Controls the delay between casts for a specific spell type", AccountStatus::Player, command_spell_delays) || + command_add("spellholds", "Controls whether a bot holds the specified spell type or not", AccountStatus::Player, command_spell_holds) || + command_add("spellmaxthresholds", "Controls the minimum target HP threshold for a spell to be cast for a specific type", AccountStatus::Player, command_spell_max_thresholds) || + command_add("spellminthresholds", "Controls the maximum target HP threshold for a spell to be cast for a specific type", AccountStatus::Player, command_spell_min_thresholds) || command_add("stun", "[duration] - Stuns you or your target for duration", AccountStatus::GMAdmin, command_stun) || command_add("summon", "[Character Name] - Summons your corpse, NPC, or player target, or by character name if specified", AccountStatus::QuestTroupe, command_summon) || command_add("summonburiedplayercorpse", "Summons the target's oldest buried corpse, if any exist.", AccountStatus::GMAdmin, command_summonburiedplayercorpse) || @@ -841,6 +846,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" @@ -909,6 +915,10 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/spawn.cpp" #include "gm_commands/spawneditmass.cpp" #include "gm_commands/spawnfix.cpp" +#include "gm_commands/spell_delays.cpp" +#include "gm_commands/spell_holds.cpp" +#include "gm_commands/spell_max_thresholds.cpp" +#include "gm_commands/spell_min_thresholds.cpp" #include "gm_commands/faction_association.cpp" #include "gm_commands/stun.cpp" #include "gm_commands/summon.cpp" diff --git a/zone/command.h b/zone/command.h index d59fdf9a5..aa5771da9 100644 --- a/zone/command.h +++ b/zone/command.h @@ -100,6 +100,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); @@ -170,6 +171,10 @@ void command_shutdown(Client *c, const Seperator *sep); void command_spawn(Client *c, const Seperator *sep); void command_spawneditmass(Client *c, const Seperator *sep); void command_spawnfix(Client *c, const Seperator *sep); +void command_spell_delays(Client* c, const Seperator* sep); +void command_spell_holds(Client* c, const Seperator* sep); +void command_spell_max_thresholds(Client* c, const Seperator* sep); +void command_spell_min_thresholds(Client* c, const Seperator* sep); void command_stun(Client *c, const Seperator *sep); void command_summon(Client *c, const Seperator *sep); void command_summonburiedplayercorpse(Client *c, const Seperator *sep); diff --git a/zone/entity.cpp b/zone/entity.cpp index fab1be99f..b34f62d3d 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2953,6 +2953,7 @@ void EntityList::ScanCloseMobs(Mob *scanning_mob) for (auto &e : mob_list) { auto mob = e.second; + if (mob->GetID() <= 0) { continue; } diff --git a/zone/entity.h b/zone/entity.h index 33313f14e..814a5feb5 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -628,7 +628,7 @@ private: Client* GetBotOwnerByBotID(const uint32 bot_id); std::list GetBotsByBotOwnerCharacterID(uint32 botOwnerCharacterID); - bool Bot_AICheckCloseBeneficialSpells(Bot* caster, uint8 iChance, float iRange, uint32 iSpellTypes); // TODO: Evaluate this closesly in hopes to eliminate + bool Bot_AICheckCloseBeneficialSpells(Bot* caster, uint8 iChance, float iRange, uint16 spellType); // 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 ScanCloseClientMobs(std::unordered_map& close_mobs, Mob* scanning_mob); diff --git a/zone/exp.cpp b/zone/exp.cpp index be6b8af2b..70f72cacb 100644 --- a/zone/exp.cpp +++ b/zone/exp.cpp @@ -1004,6 +1004,7 @@ void Client::SetLevel(uint8 set_level, bool command) } } else { SetHP(CalcMaxHP()); // Why not, lets give them a free heal + SetMana(CalcMaxMana()); } if (RuleI(World, PVPMinLevel) > 0 && level >= RuleI(World, PVPMinLevel) && m_pp.pvp == 0) { diff --git a/zone/gm_commands/illusion_block.cpp b/zone/gm_commands/illusion_block.cpp new file mode 100644 index 000000000..cd05b965a --- /dev/null +++ b/zone/gm_commands/illusion_block.cpp @@ -0,0 +1,31 @@ +#include "../client.h" + +void command_illusion_block(Client* c, const Seperator* sep) +{ + int arguments = sep->argnum; + if (!arguments || !strcasecmp(sep->arg[1], "help")) { + c->Message(Chat::White, "usage: #illusionblock [help | current | value]."); + c->Message(Chat::White, "note: Used to control whether or not illusion effects will land on you."); + c->Message(Chat::White, "note: A value of 0 is disabled (Allow Illusions), 1 is enabled (Block Illusions)."); + c->Message(Chat::White, "note: Use [current] to check the current setting."); + return; + } + + if (sep->IsNumber(1)) { + int setStatus = atoi(sep->arg[1]); + if (setStatus == 0 || setStatus == 1) { + c->SetIllusionBlock(setStatus); + c->Message(Chat::White, "Your Illusion Block has been %s.", (setStatus ? "enabled" : "disabled")); + } + else { + c->Message(Chat::White, "You must enter 0 for disabled or 1 for enabled."); + return; + } + } + else if (!strcasecmp(sep->arg[1], "current")) { + c->Message(Chat::White, "You're currently %s illusions.", (c->GetIllusionBlock() ? "blocking" : "allowing")); + } + else { + c->Message(Chat::White, "Incorrect argument, use %s help for a list of options.", sep->arg[0]); + } +} diff --git a/zone/gm_commands/spell_delays.cpp b/zone/gm_commands/spell_delays.cpp new file mode 100644 index 000000000..09b2b0c5d --- /dev/null +++ b/zone/gm_commands/spell_delays.cpp @@ -0,0 +1,215 @@ +#include "../command.h" + +void command_spell_delays(Client* c, const Seperator* sep) +{ + const int arguments = sep->argnum; + if (arguments) { + const bool is_help = !strcasecmp(sep->arg[1], "help"); + + if (is_help) { + c->Message(Chat::White, "usage: %s [spelltype ID | spelltype Shortname] [current | value: 0-1].", sep->arg[0]); + c->Message(Chat::White, "example: [%s 15 4000] or [%s cures 4000] would allow bots to cast cures on you every 4 seconds.", sep->arg[0], sep->arg[0]); + c->Message(Chat::White, "note: Use [current] to check your current setting."); + c->Message( + Chat::White, + fmt::format( + "note: Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + std::string arg1 = sep->arg[1]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + const std::string& color_red = "red_1"; + const std::string& color_blue = "royal_blue"; + const std::string& color_green = "forest_green"; + const std::string& bright_green = "green"; + const std::string& bright_red = "red"; + const std::string& heroic_color = "gold"; + + std::string fillerLine = "-----------"; + std::string spellTypeField = "Spell Type"; + std::string pluralS = "s"; + std::string idField = "ID"; + std::string shortnameField = "Short Name"; + + std::string popup_text = DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(bright_green, spellTypeField) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(bright_green, idField) : DialogueWindow::ColorMessage(bright_green, shortnameField)) + ) + ) + ); + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + ); + + for (int i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + if (!IsClientBotSpellType(i)) { + continue; + } + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}{}", + DialogueWindow::ColorMessage(color_green, c->GetSpellTypeNameByID(i)), + DialogueWindow::ColorMessage(color_green, pluralS) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(color_blue, std::to_string(i)) : DialogueWindow::ColorMessage(color_blue, c->GetSpellTypeShortNameByID(i))) + ) + ) + ); + } + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient("Spell Types", popup_text.c_str()); + + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + else { + if (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->GetSpellTypeIDByShortName(arg1); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + 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)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 1 || typeValue > 60000) { + c->Message(Chat::Yellow, "You must enter a value between 1-60000 (1ms 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; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "Your current {} delay is {} seconds.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellDelay(spellType) / 1000.00 + ).c_str() + ); + } + else { + c->SetSpellDelay(spellType, typeValue); + c->Message( + Chat::Green, + fmt::format( + "Your {} delay was set to {} seconds.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellDelay(spellType) / 1000.00 + ).c_str() + ); + } +} diff --git a/zone/gm_commands/spell_holds.cpp b/zone/gm_commands/spell_holds.cpp new file mode 100644 index 000000000..90a725dfb --- /dev/null +++ b/zone/gm_commands/spell_holds.cpp @@ -0,0 +1,218 @@ +#include "../command.h" + +void command_spell_holds(Client *c, const Seperator *sep) +{ + const int arguments = sep->argnum; + if (arguments) { + const bool is_help = !strcasecmp(sep->arg[1], "help"); + + if (is_help) { + c->Message(Chat::White, "usage: %s [spelltype ID | spelltype Shortname] [current | value: 0-1].", sep->arg[0]); + c->Message(Chat::White, "example: [%s 15 1] or [%s cures 1] would prevent bots from casting cures on you.", sep->arg[0], sep->arg[0]); + c->Message(Chat::White, "note: Use [current] to check your current setting."); + c->Message(Chat::White, "note: Set to 0 to unhold the given spell type."); + c->Message(Chat::White, "note: Set to 1 to hold the given spell type."); + c->Message( + Chat::White, + fmt::format( + "note: Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + std::string arg1 = sep->arg[1]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + const std::string& color_red = "red_1"; + const std::string& color_blue = "royal_blue"; + const std::string& color_green = "forest_green"; + const std::string& bright_green = "green"; + const std::string& bright_red = "red"; + const std::string& heroic_color = "gold"; + + std::string fillerLine = "-----------"; + std::string spellTypeField = "Spell Type"; + std::string pluralS = "s"; + std::string idField = "ID"; + std::string shortnameField = "Short Name"; + + std::string popup_text = DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(bright_green, spellTypeField) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(bright_green, idField) : DialogueWindow::ColorMessage(bright_green, shortnameField)) + ) + ) + ); + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + ); + + for (int i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + if (!IsClientBotSpellType(i)) { + continue; + } + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}{}", + DialogueWindow::ColorMessage(color_green, c->GetSpellTypeNameByID(i)), + DialogueWindow::ColorMessage(color_green, pluralS) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(color_blue, std::to_string(i)) : DialogueWindow::ColorMessage(color_blue, c->GetSpellTypeShortNameByID(i))) + ) + ) + ); + } + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient("Spell Types", popup_text.c_str()); + + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + else { + if (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->GetSpellTypeIDByShortName(arg1); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + 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; + } + } + + // Enable/Disable/Current checks + if (sep->IsNumber(2)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 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; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "Your current Hold {}s status is {}.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellHold(spellType) ? "enabled" : "disabled" + ).c_str() + ); + } + else { + c->SetSpellHold(spellType, typeValue); + c->Message( + Chat::Green, + fmt::format( + "Your Hold {}s status was {}.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellHold(spellType) ? "enabled" : "disabled" + ).c_str() + ); + } +} diff --git a/zone/gm_commands/spell_max_thresholds.cpp b/zone/gm_commands/spell_max_thresholds.cpp new file mode 100644 index 000000000..a67660d2e --- /dev/null +++ b/zone/gm_commands/spell_max_thresholds.cpp @@ -0,0 +1,216 @@ +#include "../command.h" + +void command_spell_max_thresholds(Client* c, const Seperator* sep) +{ + const int arguments = sep->argnum; + if (arguments) { + const bool is_help = !strcasecmp(sep->arg[1], "help"); + + if (is_help) { + c->Message(Chat::White, "usage: %s [spelltype ID | spelltype Shortname] [current | value: 0-1].", sep->arg[0]); + c->Message(Chat::White, "example: [%s 15 95] or [%s cures 95] would allow bots to cast cures on you when you are under 95%% health.", sep->arg[0], sep->arg[0]); + c->Message(Chat::White, "note: Use [current] to check your current setting."); + c->Message( + Chat::White, + fmt::format( + "note: Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + std::string arg1 = sep->arg[1]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + const std::string& color_red = "red_1"; + const std::string& color_blue = "royal_blue"; + const std::string& color_green = "forest_green"; + const std::string& bright_green = "green"; + const std::string& bright_red = "red"; + const std::string& heroic_color = "gold"; + + std::string fillerLine = "-----------"; + std::string spellTypeField = "Spell Type"; + std::string pluralS = "s"; + std::string idField = "ID"; + std::string shortnameField = "Short Name"; + + std::string popup_text = DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(bright_green, spellTypeField) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(bright_green, idField) : DialogueWindow::ColorMessage(bright_green, shortnameField)) + ) + ) + ); + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + ); + + for (int i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + if (!IsClientBotSpellType(i)) { + continue; + } + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}{}", + DialogueWindow::ColorMessage(color_green, c->GetSpellTypeNameByID(i)), + DialogueWindow::ColorMessage(color_green, pluralS) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(color_blue, std::to_string(i)) : DialogueWindow::ColorMessage(color_blue, c->GetSpellTypeShortNameByID(i))) + ) + ) + ); + } + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient("Spell Types", popup_text.c_str()); + + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + else { + if (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->GetSpellTypeIDByShortName(arg1); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + 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; + } + } + + // Enable/Disable/Current checks + if (sep->IsNumber(2)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 150) { + c->Message(Chat::Yellow, "You must enter a value between 0-150 (0%% to 150%% 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; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "Your current max threshold for {}s is {}%%.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellMaxThreshold(spellType) + ).c_str() + ); + } + else { + c->SetSpellMaxThreshold(spellType, typeValue); + c->Message( + Chat::Green, + fmt::format( + "Your max threshold for {}s was set to {}%%.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellMaxThreshold(spellType) + ).c_str() + ); + } +} diff --git a/zone/gm_commands/spell_min_thresholds.cpp b/zone/gm_commands/spell_min_thresholds.cpp new file mode 100644 index 000000000..1b0fcfdf2 --- /dev/null +++ b/zone/gm_commands/spell_min_thresholds.cpp @@ -0,0 +1,216 @@ +#include "../command.h" + +void command_spell_min_thresholds(Client* c, const Seperator* sep) +{ + const int arguments = sep->argnum; + if (arguments) { + const bool is_help = !strcasecmp(sep->arg[1], "help"); + + if (is_help) { + c->Message(Chat::White, "usage: %s [spelltype ID | spelltype Shortname] [current | value: 0-1].", sep->arg[0]); + c->Message(Chat::White, "example: [%s 15 15] or [%s cures 15] would prevent bots from casting cures on you when you are under 15%% health.", sep->arg[0], sep->arg[0]); + c->Message(Chat::White, "note: Use [current] to check your current setting."); + c->Message( + Chat::White, + fmt::format( + "note: Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + + std::string arg1 = sep->arg[1]; + + if (!arg1.compare("listid") || !arg1.compare("listname")) { + const std::string& color_red = "red_1"; + const std::string& color_blue = "royal_blue"; + const std::string& color_green = "forest_green"; + const std::string& bright_green = "green"; + const std::string& bright_red = "red"; + const std::string& heroic_color = "gold"; + + std::string fillerLine = "-----------"; + std::string spellTypeField = "Spell Type"; + std::string pluralS = "s"; + std::string idField = "ID"; + std::string shortnameField = "Short Name"; + + std::string popup_text = DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(bright_green, spellTypeField) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(bright_green, idField) : DialogueWindow::ColorMessage(bright_green, shortnameField)) + ) + ) + ); + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + DialogueWindow::ColorMessage(heroic_color, fillerLine) + ) + ) + ); + + for (int i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + if (!IsClientBotSpellType(i)) { + continue; + } + + popup_text += DialogueWindow::TableRow( + DialogueWindow::TableCell( + fmt::format( + "{}{}", + DialogueWindow::ColorMessage(color_green, c->GetSpellTypeNameByID(i)), + DialogueWindow::ColorMessage(color_green, pluralS) + ) + ) + + DialogueWindow::TableCell( + fmt::format( + "{}", + (!arg1.compare("listid") ? DialogueWindow::ColorMessage(color_blue, std::to_string(i)) : DialogueWindow::ColorMessage(color_blue, c->GetSpellTypeShortNameByID(i))) + ) + ) + ); + } + + popup_text = DialogueWindow::Table(popup_text); + + c->SendPopupToClient("Spell Types", popup_text.c_str()); + + return; + } + + std::string arg2 = sep->arg[2]; + int ab_arg = 2; + bool current_check = false; + uint16 spellType = 0; + uint32 typeValue = 0; + + if (sep->IsNumber(1)) { + spellType = atoi(sep->arg[1]); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + else { + if (c->GetSpellTypeIDByShortName(arg1) != UINT16_MAX) { + spellType = c->GetSpellTypeIDByShortName(arg1); + + if (!IsClientBotSpellType(spellType)) { + c->Message( + Chat::White, + fmt::format( + "You must choose a valid spell type. Use {} for a list of spell types by ID or {} for a list of spell types by short name.", + Saylink::Silent( + fmt::format("{} listid", sep->arg[0]) + ), + Saylink::Silent( + fmt::format("{} listname", sep->arg[0]) + ) + ).c_str() + ); + + return; + } + } + 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; + } + } + + // Enable/Disable/Current checks + if (sep->IsNumber(2)) { + typeValue = atoi(sep->arg[2]); + ++ab_arg; + if (typeValue < 0 || typeValue > 150) { + c->Message(Chat::Yellow, "You must enter a value between 0-150 (0%% to 150%% 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; + } + + if (current_check) { + c->Message( + Chat::Green, + fmt::format( + "Your current min threshold for {}s is {}%%.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellMinThreshold(spellType) + ).c_str() + ); + } + else { + c->SetSpellMinThreshold(spellType, typeValue); + c->Message( + Chat::Green, + fmt::format( + "Your min threshold for {}s was set to {}%%.", + c->GetSpellTypeNameByID(spellType), + c->GetSpellMinThreshold(spellType) + ).c_str() + ); + } +} diff --git a/zone/lua_bot.cpp b/zone/lua_bot.cpp index 2cf0e7db3..8003e1388 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); @@ -762,7 +757,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 b07d14abc..7a038b7dc 100644 --- a/zone/lua_bot.h +++ b/zone/lua_bot.h @@ -52,7 +52,6 @@ public: 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/merc.cpp b/zone/merc.cpp index 6200e5b89..d66501efa 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 (BOT_SPELL_TYPES_DETRIMENTAL(iSpellTypes)) { //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.. @@ -1569,7 +1569,7 @@ bool Merc::AIDoSpellCast(uint16 spellid, Mob* tar, int32 mana_cost, uint32* oDon float dist2 = 0; - if (mercSpell.type & SpellType_Escape) { + if (mercSpell.type == SpellType_Escape) { dist2 = 0; } else dist2 = DistanceSquared(m_Position, tar->GetPosition()); diff --git a/zone/mob.cpp b/zone/mob.cpp index dd62d9329..65374ce65 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -1692,6 +1692,11 @@ void Mob::StopMoving() void Mob::StopMoving(float new_heading) { + if (IsBot()) { + CastToBot()->SetCombatJitterFlag(false); + CastToBot()->SetCombatOutOfRangeJitterFlag(false); + } + StopNavigation(); RotateTo(new_heading); @@ -4664,7 +4669,7 @@ bool Mob::CanThisClassDoubleAttack(void) const bool Mob::CanThisClassTripleAttack() const { - if (!IsClient()) { + if (!IsOfClientBot()) { return false; // When they added the real triple attack skill, mobs lost the ability to triple } else { if (RuleB(Combat, ClassicTripleAttack)) { @@ -4678,7 +4683,12 @@ bool Mob::CanThisClassTripleAttack() const ) ); } else { - return CastToClient()->HasSkill(EQ::skills::SkillTripleAttack); + if (IsClient()) { + return CastToClient()->HasSkill(EQ::skills::SkillTripleAttack); + } + else { + return GetSkill(EQ::skills::SkillTripleAttack) > 0; + } } } } @@ -4812,6 +4822,88 @@ bool Mob::PlotPositionAroundTarget(Mob* target, float &x_dest, float &y_dest, fl return Result; } +bool Mob::PlotBotPositionAroundTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float min_distance, float max_distance, bool behindOnly, bool frontOnly, bool bypassLoS) { + bool Result = false; + + if (target) { + float look_heading = 0; + + min_distance = min_distance; + max_distance = max_distance; + float tempX = 0; + float tempY = 0; + float tempZ = target->GetZ(); + float bestZ = 0; + auto offset = GetZOffset(); + const float tarX = target->GetX(); + const float tarY = target->GetY(); + float tar_distance = 0; + + glm::vec3 temp_z_Position; + glm::vec4 temp_m_Position; + + const uint16 maxIterationsAllowed = 50; + uint16 counter = 0; + //LogTestDebug("Plotting for {} - Min: [{}] - Max: [{}] - BehindMob: [{}] - Taunt [{}] -- LosReq [{}]", GetCleanName(), min_distance, max_distance, behindOnly, frontOnly, bypassLoS ? "bypassed" : CastToBot()->RequiresLoSForPositioning() ? "true" : "false"); //deleteme + while (counter < maxIterationsAllowed) { + tempX = tarX + zone->random.Real(-max_distance, max_distance); + tempY = tarY + zone->random.Real(-max_distance, max_distance); + + temp_z_Position.x = tempX; + temp_z_Position.y = tempY; + temp_z_Position.z = tempZ; + bestZ = GetFixedZ(temp_z_Position); + + if (bestZ != BEST_Z_INVALID) { + tempZ = bestZ; + } + else { + //LogTestDebug("{} - Plot Failed GetFixedZ - Try #[{}].", GetCleanName(), (counter + 1)); //deleteme + counter++; + continue; + } + + temp_m_Position.x = tempX; + temp_m_Position.y = tempY; + temp_m_Position.z = tempZ; + //tar_distance = DistanceNoZ(target->GetPosition(), temp_m_Position); + tar_distance = Distance(target->GetPosition(), temp_m_Position); + + if (tar_distance > max_distance || tar_distance < min_distance) { + //LogTestDebug("{} - Plot Failed Distance - Try #[{}]. Target LOCs XYZ - [{}], [{}], [{}] - Temp LOCs [{}], [{}], [{}] - Distance between = [{}] - Melee Distance = [{}] - Difference = [{}]", GetCleanName(), (counter + 1), target->GetX(), target->GetY(), target->GetZ(), tempX, tempY, tempZ, tar_distance, max_distance, (tar_distance / max_distance)); + counter++; + continue; + } + if (frontOnly && !InFrontMob(target, tempX, tempY)) { + //LogTestDebug("{} - Plot Failed frontOnly - Try #[{}]. Target LOCs XYZ - [{}], [{}], [{}] - Temp LOCs [{}], [{}], [{}] - Distance between = [{}] - Melee Distance = [{}] - Difference = [{}]", GetCleanName(), (counter + 1), target->GetX(), target->GetY(), target->GetZ(), tempX, tempY, tempZ, tar_distance, max_distance, (tar_distance / max_distance)); + counter++; + continue; + } + else if (behindOnly && !BehindMob(target, tempX, tempY)) { + //LogTestDebug("{} - Plot Failed BehindMob - Try #[{}]. Target LOCs XYZ - [{}], [{}], [{}] - Temp LOCs [{}], [{}], [{}] - Distance between = [{}] - Melee Distance = [{}] - Difference = [{}]", GetCleanName(), (counter + 1), target->GetX(), target->GetY(), target->GetZ(), tempX, tempY, tempZ, tar_distance, max_distance, (tar_distance / max_distance)); + counter++; + continue; + } + if (!bypassLoS && CastToBot()->RequiresLoSForPositioning() && !CheckPositioningLosFN(target, tempX, tempY, tempZ)) { + //LogTestDebug("{} - Plot Failed LoS - Try #[{}]. Target LOCs XYZ - [{}], [{}], [{}] - Temp LOCs [{}], [{}], [{}] - Distance between = [{}] - Melee Distance = [{}] - Difference = [{}]", GetCleanName(), (counter + 1), target->GetX(), target->GetY(), target->GetZ(), tempX, tempY, tempZ, tar_distance, max_distance, (tar_distance / max_distance)); + counter++; + continue; + } + //LogTestDebug("{} - Plot PASSED! - Try #[{}]. Target LOCs XYZ - [{}], [{}], [{}] - Temp LOCs [{}], [{}], [{}] - Distance between = [{}] - Melee Distance = [{}] - Difference = [{}]", GetCleanName(), (counter + 1), target->GetX(), target->GetY(), target->GetZ(), tempX, tempY, tempZ, tar_distance, max_distance, (tar_distance / max_distance)); + Result = true; + break; + } + + if (Result) { + x_dest = tempX; + y_dest = tempY; + z_dest = tempZ; + } + } + + return Result; +} + bool Mob::HateSummon() { // check if mob has ability to summon // 97% is the offical % that summoning starts on live, not 94 @@ -8574,8 +8666,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() @@ -8604,6 +8697,732 @@ void Mob::CheckScanCloseMobsMovingTimer() } } +uint16 Mob::GetSpellTypeIDByShortName(std::string spellTypeString) { + + for (int i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + if (!Strings::ToLower(spellTypeString).compare(GetSpellTypeShortNameByID(i))) { + return i; + } + } + + return UINT16_MAX; +} + +std::string Mob::GetSpellTypeNameByID(uint16 spellType) { + std::string spellTypeName = "null"; + + switch (spellType) { + case BotSpellTypes::Nuke: + spellTypeName = "Nuke"; + break; + case BotSpellTypes::RegularHeal: + spellTypeName = "Regular Heal"; + break; + case BotSpellTypes::Root: + spellTypeName = "Root"; + break; + case BotSpellTypes::Buff: + spellTypeName = "Buff"; + break; + case BotSpellTypes::Escape: + spellTypeName = "Escape"; + break; + case BotSpellTypes::Pet: + spellTypeName = "Pet"; + break; + case BotSpellTypes::Lifetap: + spellTypeName = "Lifetap"; + break; + case BotSpellTypes::Snare: + spellTypeName = "Snare"; + break; + case BotSpellTypes::DOT: + spellTypeName = "DoT"; + break; + case BotSpellTypes::Dispel: + spellTypeName = "Dispel"; + break; + case BotSpellTypes::InCombatBuff: + spellTypeName = "In-Combat Buff"; + break; + case BotSpellTypes::Mez: + spellTypeName = "Mez"; + break; + case BotSpellTypes::Charm: + spellTypeName = "Charm"; + break; + case BotSpellTypes::Slow: + spellTypeName = "Slow"; + break; + case BotSpellTypes::Debuff: + spellTypeName = "Debuff"; + break; + case BotSpellTypes::Cure: + spellTypeName = "Cure"; + break; + case BotSpellTypes::GroupCures: + spellTypeName = "Group Cure"; + break; + case BotSpellTypes::Resurrect: + spellTypeName = "Resurrect"; + break; + case BotSpellTypes::HateRedux: + spellTypeName = "Hate Reduction"; + break; + case BotSpellTypes::InCombatBuffSong: + spellTypeName = "In-Combat Buff Song"; + break; + case BotSpellTypes::OutOfCombatBuffSong: + spellTypeName = "Out-of-Combat Buff Song"; + break; + case BotSpellTypes::PreCombatBuff: + spellTypeName = "Pre-Combat Buff"; + break; + case BotSpellTypes::PreCombatBuffSong: + spellTypeName = "Pre-Combat Buff Song"; + break; + case BotSpellTypes::Fear: + spellTypeName = "Fear"; + break; + case BotSpellTypes::Stun: + spellTypeName = "Stun"; + break; + case BotSpellTypes::CompleteHeal: + spellTypeName = "Complete Heal"; + break; + case BotSpellTypes::FastHeals: + spellTypeName = "Fast Heal"; + break; + case BotSpellTypes::VeryFastHeals: + spellTypeName = "Very Fast Heal"; + break; + case BotSpellTypes::GroupHeals: + spellTypeName = "Group Heal"; + break; + case BotSpellTypes::GroupCompleteHeals: + spellTypeName = "Group Complete Heal"; + break; + case BotSpellTypes::GroupHoTHeals: + spellTypeName = "Group HoT Heal"; + break; + case BotSpellTypes::HoTHeals: + spellTypeName = "HoT Heal"; + break; + case BotSpellTypes::AENukes: + spellTypeName = "AE Nuke"; + break; + case BotSpellTypes::AERains: + spellTypeName = "AE Rain"; + break; + case BotSpellTypes::AEMez: + spellTypeName = "AE Mez"; + break; + case BotSpellTypes::AEStun: + spellTypeName = "AE Stun"; + break; + case BotSpellTypes::AEDebuff: + spellTypeName = "AE Debuff"; + break; + case BotSpellTypes::AESlow: + spellTypeName = "AE Slow"; + break; + case BotSpellTypes::AESnare: + spellTypeName = "AE Snare"; + break; + case BotSpellTypes::AEFear: + spellTypeName = "AE Fear"; + break; + case BotSpellTypes::AEDispel: + spellTypeName = "AE Dispel"; + break; + case BotSpellTypes::AERoot: + spellTypeName = "AE Root"; + break; + case BotSpellTypes::AEDoT: + spellTypeName = "AE DoT"; + break; + case BotSpellTypes::AELifetap: + spellTypeName = "AE Lifetap"; + break; + case BotSpellTypes::PBAENuke: + spellTypeName = "PBAE Nuke"; + break; + case BotSpellTypes::PetBuffs: + spellTypeName = "Pet Buff"; + break; + case BotSpellTypes::PetRegularHeals: + spellTypeName = "Pet Regular Heal"; + break; + case BotSpellTypes::PetCompleteHeals: + spellTypeName = "Pet Complete Heal"; + break; + case BotSpellTypes::PetFastHeals: + spellTypeName = "Pet Fast Heal"; + break; + case BotSpellTypes::PetVeryFastHeals: + spellTypeName = "Pet Very Fast Heal"; + break; + case BotSpellTypes::PetHoTHeals: + spellTypeName = "Pet HoT Heal"; + break; + case BotSpellTypes::DamageShields: + spellTypeName = "Damage Shield"; + break; + case BotSpellTypes::ResistBuffs: + spellTypeName = "Resist Buff"; + break; + default: + break; + } + + return spellTypeName; +} + +std::string Mob::GetSpellTypeShortNameByID(uint16 spellType) { + std::string spellTypeName = "null"; + + switch (spellType) { + case BotSpellTypes::Nuke: + spellTypeName = "nukes"; + break; + case BotSpellTypes::RegularHeal: + spellTypeName = "regularheals"; + break; + case BotSpellTypes::Root: + spellTypeName = "roots"; + break; + case BotSpellTypes::Buff: + spellTypeName = "buffs"; + break; + case BotSpellTypes::Escape: + spellTypeName = "escapes"; + break; + case BotSpellTypes::Pet: + spellTypeName = "pets"; + break; + case BotSpellTypes::Lifetap: + spellTypeName = "lifetaps"; + break; + case BotSpellTypes::Snare: + spellTypeName = "snares"; + break; + case BotSpellTypes::DOT: + spellTypeName = "dots"; + break; + case BotSpellTypes::Dispel: + spellTypeName = "dispels"; + break; + case BotSpellTypes::InCombatBuff: + spellTypeName = "incombatbuffs"; + break; + case BotSpellTypes::Mez: + spellTypeName = "mez"; + break; + case BotSpellTypes::Charm: + spellTypeName = "charms"; + break; + case BotSpellTypes::Slow: + spellTypeName = "slows"; + break; + case BotSpellTypes::Debuff: + spellTypeName = "debuffs"; + break; + case BotSpellTypes::Cure: + spellTypeName = "cures"; + break; + case BotSpellTypes::GroupCures: + spellTypeName = "groupcures"; + break; + case BotSpellTypes::Resurrect: + spellTypeName = "resurrect"; + break; + case BotSpellTypes::HateRedux: + spellTypeName = "hateredux"; + break; + case BotSpellTypes::InCombatBuffSong: + spellTypeName = "incombatbuffsongs"; + break; + case BotSpellTypes::OutOfCombatBuffSong: + spellTypeName = "outofcombatbuffsongs"; + break; + case BotSpellTypes::PreCombatBuff: + spellTypeName = "precombatbuffs"; + break; + case BotSpellTypes::PreCombatBuffSong: + spellTypeName = "precombatbuffsongs"; + break; + case BotSpellTypes::Fear: + spellTypeName = "fears"; + break; + case BotSpellTypes::Stun: + spellTypeName = "stuns"; + break; + case BotSpellTypes::CompleteHeal: + spellTypeName = "completeheals"; + break; + case BotSpellTypes::FastHeals: + spellTypeName = "fastheals"; + break; + case BotSpellTypes::VeryFastHeals: + spellTypeName = "veryfastheals"; + break; + case BotSpellTypes::GroupHeals: + spellTypeName = "groupheals"; + break; + case BotSpellTypes::GroupCompleteHeals: + spellTypeName = "groupcompleteheals"; + break; + case BotSpellTypes::GroupHoTHeals: + spellTypeName = "grouphotheals"; + break; + case BotSpellTypes::HoTHeals: + spellTypeName = "hotheals"; + break; + case BotSpellTypes::AENukes: + spellTypeName = "aenukes"; + break; + case BotSpellTypes::AERains: + spellTypeName = "aerains"; + break; + case BotSpellTypes::AEMez: + spellTypeName = "aemez"; + break; + case BotSpellTypes::AEStun: + spellTypeName = "aestuns"; + break; + case BotSpellTypes::AEDebuff: + spellTypeName = "aedebuffs"; + break; + case BotSpellTypes::AESlow: + spellTypeName = "aeslows"; + break; + case BotSpellTypes::AESnare: + spellTypeName = "aesnares"; + break; + case BotSpellTypes::AEFear: + spellTypeName = "aefears"; + break; + case BotSpellTypes::AEDispel: + spellTypeName = "aedispels"; + break; + case BotSpellTypes::AERoot: + spellTypeName = "aeroots"; + break; + case BotSpellTypes::AEDoT: + spellTypeName = "aedots"; + break; + case BotSpellTypes::AELifetap: + spellTypeName = "aelifetaps"; + break; + case BotSpellTypes::PBAENuke: + spellTypeName = "pbaenukes"; + break; + case BotSpellTypes::PetBuffs: + spellTypeName = "petbuffs"; + break; + case BotSpellTypes::PetRegularHeals: + spellTypeName = "petregularheals"; + break; + case BotSpellTypes::PetCompleteHeals: + spellTypeName = "petcompleteheals"; + break; + case BotSpellTypes::PetFastHeals: + spellTypeName = "petfastheals"; + break; + case BotSpellTypes::PetVeryFastHeals: + spellTypeName = "petveryfastheals"; + break; + case BotSpellTypes::PetHoTHeals: + spellTypeName = "pethotheals"; + break; + case BotSpellTypes::DamageShields: + spellTypeName = "damageshields"; + break; + case BotSpellTypes::ResistBuffs: + spellTypeName = "resistbuffs"; + break; + default: + break; + } + + return spellTypeName; +} + +bool Mob::GetDefaultSpellHold(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::AEMez: + case BotSpellTypes::AEStun: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::AESlow: + case BotSpellTypes::AESnare: + case BotSpellTypes::AEDoT: + case BotSpellTypes::AELifetap: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::AERoot: + case BotSpellTypes::Root: + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + case BotSpellTypes::AEFear: + case BotSpellTypes::Fear: + return true; + case BotSpellTypes::Snare: + if (GetClass() == Class::Wizard) { + return true; + } + else { + return false; + } + case BotSpellTypes::InCombatBuffSong: + case BotSpellTypes::OutOfCombatBuffSong: + case BotSpellTypes::PreCombatBuffSong: + if (GetClass() == Class::Bard) { + return true; + } + else { + return false; + } + default: + return false; + } +} + +uint16 Mob::GetDefaultSpellDelay(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + return 1500; + case BotSpellTypes::FastHeals: + case BotSpellTypes::PetFastHeals: + return 2500; + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + case BotSpellTypes::GroupHeals: + case BotSpellTypes::RegularHeal: + case BotSpellTypes::PetRegularHeals: + 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: + return 6000; + case BotSpellTypes::AERoot: + case BotSpellTypes::Root: + case BotSpellTypes::CompleteHeal: + case BotSpellTypes::GroupCompleteHeals: + case BotSpellTypes::PetCompleteHeals: + return 8000; + case BotSpellTypes::Fear: + case BotSpellTypes::AEFear: + return 15000; + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + return 22000; + default: + return 1; + } +} + +uint8 Mob::GetDefaultSpellMinThreshold(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::AENukes: + case BotSpellTypes::AERains: + case BotSpellTypes::PBAENuke: + case BotSpellTypes::Nuke: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + case BotSpellTypes::AEDispel: + case BotSpellTypes::Dispel: + case BotSpellTypes::AESlow: + case BotSpellTypes::Slow: + return 20; + case BotSpellTypes::AEDoT: + case BotSpellTypes::DOT: + return 35; + case BotSpellTypes::Charm: + return 50; + case BotSpellTypes::Mez: + case BotSpellTypes::AEMez: + return 85; + default: + return 0; + } +} + +uint8 Mob::GetDefaultSpellMaxThreshold(uint16 spellType) { + switch (spellType) { + case BotSpellTypes::Escape: + case BotSpellTypes::VeryFastHeals: + case BotSpellTypes::PetVeryFastHeals: + return 25; + case BotSpellTypes::AELifetap: + case BotSpellTypes::Lifetap: + 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: + 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::Dispel: + case BotSpellTypes::AEDebuff: + case BotSpellTypes::Debuff: + case BotSpellTypes::Stun: + return 99; + case BotSpellTypes::Buff: + case BotSpellTypes::Charm: + case BotSpellTypes::Cure: + 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::ResistBuffs: + case BotSpellTypes::Resurrect: + return 100; + case BotSpellTypes::GroupHoTHeals: + case BotSpellTypes::HoTHeals: + case BotSpellTypes::PetHoTHeals: + if (GetClass() == Class::Necromancer || GetClass() == Class::Shaman) { + return 60; + } + else { + return 90; + } + default: + return 100; + } +} + +void Mob::SetSpellHold(uint16 spellType, bool holdStatus) { + _spellSettings[spellType].hold = holdStatus; +} + +void Mob::SetSpellDelay(uint16 spellType, uint16 delayValue) { + _spellSettings[spellType].delay = delayValue; +} + +void Mob::SetSpellMinThreshold(uint16 spellType, uint8 thresholdValue) { + _spellSettings[spellType].minThreshold = thresholdValue; +} + +void Mob::SetSpellMaxThreshold(uint16 spellType, uint8 thresholdValue) { + _spellSettings[spellType].maxThreshold = thresholdValue; +} + +void Mob::SetSpellTypeRecastTimer(uint16 spellType, uint32 recastTime) { + _spellSettings[spellType].recastTimer.Start(recastTime); + LogBotDelayChecksDetail("{} says, 'My {} Delay was to {} seconds.'" + , GetCleanName() + , GetSpellTypeNameByID(spellType) + , (recastTime / 1000.00) + ); //deleteme +} + +void Mob::StartBotSpellTimers() { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + _spellSettings[i].recastTimer.Start(); + } +} + +void Mob::DisableBotSpellTimers() { + for (uint16 i = BotSpellTypes::START; i <= BotSpellTypes::END; ++i) { + _spellSettings[i].recastTimer.Disable(); + } +} + +bool Mob::GetUltimateSpellHold(uint16 spellType, Mob* tar) { + if (!tar) { + return GetSpellHold(spellType); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + return tar->GetOwner()->GetSpellHold(GetPetSpellType(spellType)); + } + + if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType) && tar->IsOfClientBot()) { + return tar->GetSpellHold(spellType); + } + + return GetSpellHold(spellType); +} + +uint16 Mob::GetUltimateSpellDelay(uint16 spellType, Mob* tar) { + if (!tar) { + return GetSpellDelay(spellType); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + return tar->GetOwner()->GetSpellDelay(GetPetSpellType(spellType)); + } + + if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType) && tar->IsOfClientBot()) { + return tar->GetSpellDelay(spellType); + } + + return GetSpellDelay(spellType); +} + +bool Mob::GetUltimateSpellDelayCheck(uint16 spellType, Mob* tar) { + if (!tar) { + return SpellTypeRecastCheck(spellType); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + return tar->GetOwner()->SpellTypeRecastCheck(GetPetSpellType(spellType)); + } + + if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType) && tar->IsOfClientBot()) { + return tar->SpellTypeRecastCheck(spellType); + } + + return SpellTypeRecastCheck(spellType); +} + +uint8 Mob::GetUltimateSpellMinThreshold(uint16 spellType, Mob* tar) { + if (!tar) { + return GetSpellMinThreshold(spellType); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + return tar->GetOwner()->GetSpellMinThreshold(GetPetSpellType(spellType)); + } + + if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType) && tar->IsOfClientBot()) { + return tar->GetSpellMinThreshold(spellType); + } + + return GetSpellMinThreshold(spellType); +} + +uint8 Mob::GetUltimateSpellMaxThreshold(uint16 spellType, Mob* tar) { + if (!tar) { + return GetSpellMaxThreshold(spellType); + } + + if (tar->IsPet() && tar->GetOwner() && tar->GetOwner()->IsOfClientBot()) { + return tar->GetOwner()->GetSpellMaxThreshold(GetPetSpellType(spellType)); + } + + if (BOT_SPELL_TYPES_OTHER_BENEFICIAL(spellType) && tar->IsOfClientBot()) { + return tar->GetSpellMaxThreshold(spellType); + } + + return GetSpellMaxThreshold(spellType); +} + +uint16 Mob::GetPetSpellType(uint16 spellType) { + switch (spellType) { + 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; + default: + break; + } + + return spellType; +} + +uint8 Mob::GetHPRatioForSpellType(uint16 spellType, Mob* tar) { + switch (spellType) { + 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(); +} + +void Mob::SetBotSetting(uint8 settingType, uint16 botSetting, int settingValue) { + if (!IsOfClientBot()) { + return; + } + + if (IsClient()) { + CastToClient()->SetBotSetting(settingType, botSetting, settingValue); + return; + } + + if (IsBot()) { + CastToBot()->SetBotSetting(settingType, botSetting, settingValue); + return; + } + + return; +} + +void Mob::SetBaseSetting(uint16 baseSetting, int settingValue) { + switch (baseSetting) { + case BotBaseSettings::IllusionBlock: + SetIllusionBlock(settingValue); + break; + default: + break; + } +} + +bool Mob::TargetValidation(Mob* other) { + if (!other || GetAppearance() == eaDead) { + return false; + } + + return true; +} + std::unordered_map &Mob::GetCloseMobList(float distance) { return entity_list.GetCloseMobList(this, distance); diff --git a/zone/mob.h b/zone/mob.h index 297aa80cb..a9d4e5857 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -93,6 +93,28 @@ struct AppearanceStruct { uint8 texture = UINT8_MAX; }; +struct BotSpellSettings_Struct +{ + uint16 spellType; // type ID of bot category + std::string shortName; // 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 minThreshold; // minimum target health threshold to allow casting of spell type + uint8 maxThreshold; // maximum target health threshold to allow casting of spell type + uint16 resistLimit; // resist limit to skip spell type + bool aggroCheck; // whether or not to check for possible aggro before casting + uint8 minManaPct; // lower mana percentage limit to allow spell cast + uint8 maxManaPct; // upper mana percentage limit to allow spell cast + uint8 minHPPct; // lower HP percentage limit to allow spell cast + uint8 maxHPPct; // upper HP percentage limit to allow spell cast + uint16 idlePriority; // idle priority of the spell type + uint16 engagedPriority; // engaged priority of the spell type + uint16 pursuePriority; // pursue priority of the spell type + uint16 AEOrGroupTargetCount; // require target count to cast an AE or Group spell type + Timer recastTimer; // recast timer based off delay +}; + class DataBucketKey; class Mob : public Entity { public: @@ -208,6 +230,8 @@ public: // Bot attack flag Timer bot_attack_flag_timer; + std::vector _spellSettings; + //Somewhat sorted: needs documenting! //Attack @@ -403,6 +427,51 @@ public: virtual bool CheckFizzle(uint16 spell_id); virtual bool CheckSpellLevelRestriction(Mob *caster, uint16 spell_id); virtual bool IsImmuneToSpell(uint16 spell_id, Mob *caster); + + virtual bool IsImmuneToBotSpell(uint16 spell_id, Mob* caster); + + inline bool SpellTypeRecastCheck(uint16 spellType) { return (IsClient() ? true : _spellSettings[spellType].recastTimer.GetRemainingTime() > 0 ? false : true); } + + uint16 GetSpellTypeIDByShortName(std::string spellTypeString); + + std::string GetSpellTypeNameByID(uint16 spellType); + std::string GetSpellTypeShortNameByID(uint16 spellType); + + bool GetDefaultSpellHold(uint16 spellType); + uint16 GetDefaultSpellDelay(uint16 spellType); + uint8 GetDefaultSpellMinThreshold(uint16 spellType); + uint8 GetDefaultSpellMaxThreshold(uint16 spellType); + + inline bool GetSpellHold(uint16 spellType) const { return _spellSettings[spellType].hold; } + void SetSpellHold(uint16 spellType, bool holdStatus); + inline uint16 GetSpellDelay(uint16 spellType) const { return _spellSettings[spellType].delay; } + void SetSpellDelay(uint16 spellType, uint16 delayValue); + inline uint8 GetSpellMinThreshold(uint16 spellType) const { return _spellSettings[spellType].minThreshold; } + void SetSpellMinThreshold(uint16 spellType, uint8 thresholdValue); + inline uint8 GetSpellMaxThreshold(uint16 spellType) const { return _spellSettings[spellType].maxThreshold; } + void SetSpellMaxThreshold(uint16 spellType, uint8 thresholdValue); + + inline uint16 GetSpellTypeRecastTimer(uint16 spellType) { return _spellSettings[spellType].recastTimer.GetRemainingTime(); } + void SetSpellTypeRecastTimer(uint16 spellType, uint32 recastTime); + + uint8 GetHPRatioForSpellType(uint16 spellType, Mob* tar); + bool GetUltimateSpellHold(uint16 spellType, Mob* tar); + uint16 GetUltimateSpellDelay(uint16 spellType, Mob* tar); + bool GetUltimateSpellDelayCheck(uint16 spellType, Mob* tar); + uint8 GetUltimateSpellMinThreshold(uint16 spellType, Mob* tar); + uint8 GetUltimateSpellMaxThreshold(uint16 spellType, Mob* tar); + + uint16 GetPetSpellType(uint16 spellType); + + void DisableBotSpellTimers(); + void StartBotSpellTimers(); + + void SetBotSetting(uint8 settingType, uint16 botSetting, int settingValue); + void SetBaseSetting(uint16 baseSetting, int settingValue); + + void SetIllusionBlock(bool value) { _illusionBlock = value; } + bool GetIllusionBlock() const { return _illusionBlock; } + virtual float GetAOERange(uint16 spell_id); void InterruptSpell(uint16 spellid = SPELL_UNKNOWN); void InterruptSpell(uint16, uint16, uint16 spellid = SPELL_UNKNOWN); @@ -791,6 +860,11 @@ public: 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* who, Mob* other); + bool CheckLosCheatExempt(Mob* who, Mob* other); + bool DoLosChecks(Mob* who, Mob* other); + bool TargetValidation(Mob* other); inline void SetLastLosState(bool value) { last_los_check = value; } inline bool CheckLastLosState() const { return last_los_check; } std::string GetMobDescription(); @@ -854,6 +928,7 @@ public: void ShowStats(Client* client); void ShowBuffs(Client* c); bool PlotPositionAroundTarget(Mob* target, float &x_dest, float &y_dest, float &z_dest, bool lookForAftArc = true); + bool PlotBotPositionAroundTarget(Mob* target, float& x_dest, float& y_dest, float& z_dest, float min_distance, float max_distance, bool behindOnly = false, bool frontOnly = false, bool bypassLoS = false); virtual int GetKillExpMod() const { return 100; } // aura functions @@ -1252,13 +1327,30 @@ public: void NPCSpecialAttacks(const char* parse, int permtag, bool reset = true, bool remove = false); inline uint32 DontHealMeBefore() const { return pDontHealMeBefore; } + inline uint32 DontGroupHealMeBefore() const { return pDontGroupHealMeBefore; } + inline uint32 DontGroupHoTHealMeBefore() const { return pDontGroupHoTHealMeBefore; } + inline uint32 DontRegularHealMeBefore() const { return pDontRegularHealMeBefore; } + inline uint32 DontVeryFastHealMeBefore() const { return pDontVeryFastHealMeBefore; } + inline uint32 DontFastHealMeBefore() const { return pDontFastHealMeBefore; } + inline uint32 DontCompleteHealMeBefore() const { return pDontCompleteHealMeBefore; } + inline uint32 DontGroupCompleteHealMeBefore() const { return pDontGroupCompleteHealMeBefore; } + inline uint32 DontHotHealMeBefore() const { return pDontHotHealMeBefore; } 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 SetDontGroupHealMeBefore(uint32 time) { pDontGroupHealMeBefore = time; } + void SetDontGroupHoTHealMeBefore(uint32 time) { pDontGroupHoTHealMeBefore = time; } + void SetDontRegularHealMeBefore(uint32 time) { pDontRegularHealMeBefore = time; } + void SetDontVeryFastHealMeBefore(uint32 time) { pDontVeryFastHealMeBefore = time; } + void SetDontFastHealMeBefore(uint32 time) { pDontFastHealMeBefore = time; } + void SetDontCompleteHealMeBefore(uint32 time) { pDontCompleteHealMeBefore = time; } + void SetDontGroupCompleteHealMeBefore(uint32 time) { pDontGroupCompleteHealMeBefore = time; } + void SetDontHotHealMeBefore(uint32 time) { pDontHotHealMeBefore = time; } void SetDontBuffMeBefore(uint32 time) { pDontBuffMeBefore = time; } void SetDontDotMeBefore(uint32 time) { pDontDotMeBefore = time; } void SetDontSnareMeBefore(uint32 time) { pDontSnareMeBefore = time; } @@ -1858,6 +1950,14 @@ protected: bool pause_timer_complete; bool DistractedFromGrid; uint32 pDontHealMeBefore; + uint32 pDontGroupHealMeBefore; + uint32 pDontGroupHoTHealMeBefore; + uint32 pDontRegularHealMeBefore; + uint32 pDontVeryFastHealMeBefore; + uint32 pDontFastHealMeBefore; + uint32 pDontCompleteHealMeBefore; + uint32 pDontGroupCompleteHealMeBefore; + uint32 pDontHotHealMeBefore; uint32 pDontBuffMeBefore; uint32 pDontDotMeBefore; uint32 pDontRootMeBefore; @@ -1880,6 +1980,9 @@ protected: //bot attack flags std::vector bot_attack_flags; + //bot related settings + bool _illusionBlock; + glm::vec3 m_TargetRing; GravityBehavior flymode; diff --git a/zone/perl_bot.cpp b/zone/perl_bot.cpp index 86f104565..f24134c6a 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); @@ -716,7 +711,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 8bb39afda..528445d41 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_npc.cpp b/zone/perl_npc.cpp index b47b8c0e8..4f0999376 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 ad29a76a9..6be160943 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/spell_effects.cpp b/zone/spell_effects.cpp index 7940a7c8e..df0f74542 100644 --- a/zone/spell_effects.cpp +++ b/zone/spell_effects.cpp @@ -1483,6 +1483,10 @@ 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() && 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; } @@ -1492,6 +1496,10 @@ 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() && GetIllusionBlock()) { + break; + } + if(caster && caster->GetTarget()){ SendIllusionPacket ( @@ -10660,7 +10668,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 15d1e5574..292106f77 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -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); @@ -1273,6 +1272,17 @@ 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()) { + CastToBot()->SetCastedSpellType(UINT16_MAX); + } + + if (IsBot() && IsValidSpell(spellid)) { + if (CastToBot()->CheckSpellRecastTimer(spellid)) { + CastToBot()->ClearSpellRecastTimer(spellid); + } + } + if (!IsValidSpell(spellid)) { if (bardsong) { spellid = bardsong; @@ -2074,7 +2084,17 @@ bool Mob::DetermineSpellTargets(uint16 spell_id, Mob *&spell_target, Mob *&ae_ce } case ST_Pet: { - spell_target = GetPet(); + if ( + !( + IsBot() && + spell_target && + spell_target->GetOwner() != this && + RuleB(Bots, CanCastPetOnlyOnOthersPets) + ) + ) { + + spell_target = GetPet(); + } if(!spell_target) { LogSpells("Spell [{}] canceled: invalid target (no pet)", spell_id); @@ -2821,6 +2841,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. */ @@ -3412,12 +3439,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)) { @@ -3564,6 +3596,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::Red, + 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( "{} {}", @@ -3758,9 +3803,15 @@ int Mob::CanBuffStack(uint16 spellid, uint8 caster_level, bool iFailIfOverwrite) 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; + return firstfree; + } + 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) { @@ -4406,6 +4457,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()) { @@ -4624,6 +4687,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; } @@ -7426,3 +7497,123 @@ bool Mob::CheckWaterLoS(Mob* m) zone->watermap->InLiquid(m->GetPosition()) ); } + +bool Mob::IsImmuneToBotSpell(uint16 spell_id, Mob* caster) +{ + int effect_index; + + if (caster == nullptr) + return(false); + + //TODO: this function loops through the effect list for + //this spell like 10 times, this could easily be consolidated + //into one loop through with a switch statement. + + LogSpells("Checking to see if we are immune to spell [{}] cast by [{}]", spell_id, caster->GetName()); + + if (!IsValidSpell(spell_id)) + return true; + + if (IsBeneficialSpell(spell_id) && (caster->GetNPCTypeID())) //then skip the rest, stop NPCs aggroing each other with buff spells. 2013-03-05 + return false; + + if (IsMesmerizeSpell(spell_id)) + { + if (GetSpecialAbility(SpecialAbility::MesmerizeImmunity)) { + return true; + } + + // 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 (IsEffectInSpell(spell_id, SE_Fear)) + { + effect_index = GetSpellEffectIndex(spell_id, SE_Fear); + if (GetSpecialAbility(SpecialAbility::FearImmunity)) { + return true; + } + else 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 (IsCharmSpell(spell_id)) + { + if (GetSpecialAbility(SpecialAbility::CharmImmunity)) + { + return true; + } + + 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 + ( + 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; +}