diff --git a/zone/bot.cpp b/zone/bot.cpp index 5ecd824ae..a823b01af 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -9709,7 +9709,7 @@ bool Bot::CastChecks(uint16 spell_id, Mob* tar, uint16 spellType, bool doPrechec return false; } - if (spellType == UINT16_MAX) { //AA cast checks, return here + if (spellType == UINT16_MAX) { //AA/Forced cast checks, return here return true; } @@ -11098,7 +11098,7 @@ bool Bot::AttemptAICastSpell(uint16 spellType) { } bool Bot::AttemptAACastSpell(Mob* tar, uint16 spell_id, AA::Rank* rank) { - if (!tar) { + if (!tar || spells[spell_id].target_type == ST_Self) { tar = this; } @@ -11132,11 +11132,25 @@ bool Bot::AttemptAACastSpell(Mob* tar, uint16 spell_id, AA::Rank* rank) { } + if (IsCasting()) { + BotGroupSay( + this, + fmt::format( + "Interrupting {}. I have been commanded to try to cast an AA - {} on {}.", + CastingSpellID() ? spells[CastingSpellID()].name : "my spell", + spells[spell_id].name, + tar->GetCleanName() + ).c_str() + ); + + InterruptSpell(); + } + if (CastSpell(spell_id, tar->GetID())) { BotGroupSay( this, fmt::format( - "Casting {} on {}.", + "Casting an AA - {} on {}.", GetSpellName(spell_id), (tar == this ? "myself" : tar->GetCleanName()) ).c_str() @@ -11172,6 +11186,121 @@ bool Bot::AttemptAACastSpell(Mob* tar, uint16 spell_id, AA::Rank* rank) { return true; } +bool Bot::AttemptForcedCastSpell(Mob* tar, uint16 spell_id) { + SPDat_Spell_Struct spell = spells[spell_id]; + uint16 forcedSpellID = spell.id; + + if (!tar || (spells[spell_id].target_type == ST_Self && tar != this)) { + LogTestDebug("{} set my target to myself for {} [#{}] due to !tar.", GetCleanName(), spell.name, forcedSpellID); //deleteme + tar = this; + } + + if (IsBeneficialSpell(forcedSpellID)) { + if ( + (tar->IsNPC() && !tar->GetOwner()) || + (tar->GetOwner() && tar->GetOwner()->IsOfClientBot() && !GetBotOwner()->IsInGroupOrRaid(tar->GetOwner())) || + (tar->IsOfClientBot() && !GetBotOwner()->IsInGroupOrRaid(tar)) + ) { + GetBotOwner()->Message( + Chat::Yellow, + fmt::format( + "[{}] is an invalid target. Only players or their pet in your group or raid are eligible targets." + , tar->GetCleanName() + ).c_str() + ); + + return false; + } + } + + if (IsDetrimentalSpell(forcedSpellID) && (!GetBotOwner()->IsAttackAllowed(tar) || !IsAttackAllowed(tar))) { + GetBotOwner()->Message( + Chat::Yellow, + fmt::format( + "{} says, 'I cannot attack [{}]'.", + GetCleanName(), + tar->GetCleanName() + ).c_str() + ); + + return false; + } + + if (!CheckSpellRecastTimer(forcedSpellID)) { + LogTestDebug("{} failed CheckSpellRecastTimer for {} [#{}].", GetCleanName(), spell.name, forcedSpellID); //deleteme + return false; + } + + if ( + !RuleB(Bots, EnableBotTGB) && + IsGroupSpell(forcedSpellID) && + !IsTGBCompatibleSpell(forcedSpellID) && + !IsInGroupOrRaid(tar, true) + ) { + LogTestDebug("{} failed TGB for {} [#{}].", GetCleanName(), spell.name, forcedSpellID); //deleteme + return false; + } + + if (!DoLosChecks(this, tar)) { + LogTestDebug("{} failed LoS for {} [#{}].", GetCleanName(), spell.name, forcedSpellID); //deleteme + return false; + } + + if (!CastChecks(forcedSpellID, tar, UINT16_MAX)) { + LogTestDebug("{} failed CastChecks for {} [#{}].", GetCleanName(), spell.name, forcedSpellID); //deleteme + GetBotOwner()->Message( + Chat::Red, + fmt::format( + "{} says, 'Ability failed to cast. This could be due to this to any number of things: range, mana, immune, etc.'", + GetBotOwner()->GetCleanName() + ).c_str() + ); + + return false; + } + + if (IsCasting()) { + BotGroupSay( + this, + fmt::format( + "Interrupting {}. I have been commanded to try to cast {} on {}.", + CastingSpellID() ? spells[CastingSpellID()].name : "my spell", + spell.name, + tar->GetCleanName() + ).c_str() + ); + + InterruptSpell(); + } + + if (CastSpell(forcedSpellID, tar->GetID())) { + BotGroupSay( + this, + fmt::format( + "Casting {} on {}.", + GetSpellName(forcedSpellID), + (tar == this ? "myself" : tar->GetCleanName()) + ).c_str() + ); + + int timer_duration = CalcBuffDuration(tar, this, forcedSpellID); + + if (timer_duration) { // negatives are perma buffs + timer_duration = GetActSpellDuration(forcedSpellID, timer_duration); + } + + if (timer_duration < 0) { + timer_duration = 0; + } + + SetSpellRecastTimer(forcedSpellID, timer_duration); + + return true; + } + + return false; +} + uint16 Bot::GetSpellListSpellType(uint16 spellType) { switch (spellType) { case BotSpellTypes::AENukes: diff --git a/zone/bot.h b/zone/bot.h index 3371a6b99..98475e909 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -403,6 +403,7 @@ public: bool AICastSpell(Mob* tar, uint8 iChance, uint16 spellType, uint16 subTargetType = UINT16_MAX, uint16 subType = UINT16_MAX); bool AttemptAICastSpell(uint16 spellType); bool AttemptAACastSpell(Mob* tar, uint16 spell_id, AA::Rank* rank); + bool AttemptForcedCastSpell(Mob* tar, uint16 spell_id); bool AI_EngagedCastCheck() override; bool AI_PursueCastCheck() override; bool AI_IdleCastCheck() override; @@ -546,7 +547,6 @@ public: void CheckBotSpells(); - [[nodiscard]] int GetMaxBuffSlots() const final { return EQ::spells::LONG_BUFFS; } [[nodiscard]] int GetMaxSongSlots() const final { return EQ::spells::SHORT_BUFFS; } [[nodiscard]] int GetMaxDiscSlots() const final { return EQ::spells::DISC_BUFFS; } diff --git a/zone/bot_commands/cast.cpp b/zone/bot_commands/cast.cpp index 5866bacf4..65d03ad2b 100644 --- a/zone/bot_commands/cast.cpp +++ b/zone/bot_commands/cast.cpp @@ -46,22 +46,6 @@ void bot_command_cast(Client* c, const Seperator* sep) ) }; 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 Skbot to Harm Touch the target:", fmt::format( @@ -73,6 +57,14 @@ void bot_command_cast(Client* c, const Seperator* sep) sep->arg[0] ) }; + std::vector examples_three = + { + "To tell all bots to try to cast spell #93 (Burst of Flame)", + fmt::format( + "{} spellid 93", + sep->arg[0] + ) + }; std::vector actionables = { @@ -188,8 +180,15 @@ void bot_command_cast(Client* c, const Seperator* sep) uint16 subTargetType = UINT16_MAX; bool aaType = false; int aaID = 0; + bool bySpellID = false; + uint16 chosenSpellID = UINT16_MAX; if (!arg1.compare("aa") || !arg1.compare("harmtouch") || !arg1.compare("layonhands")) { + if (!RuleB(Bots, AllowForcedCastsBySpellID)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + if (!arg1.compare("harmtouch")) { aaID = zone->GetAlternateAdvancementAbilityByRank(aaHarmTouch)->id; } @@ -208,8 +207,25 @@ void bot_command_cast(Client* c, const Seperator* sep) aaType = true; } - if (!aaType) { - // String/Int type checks + if (!arg1.compare("spellid")) { + if (!RuleB(Bots, AllowCastAAs)) { + c->Message(Chat::Yellow, "This commanded type is currently disabled."); + return; + } + + if (sep->IsNumber(2) && IsValidSpell(atoi(sep->arg[2]))) { + ++ab_arg; + chosenSpellID = atoi(sep->arg[2]); + bySpellID = true; + } + else { + c->Message(Chat::Yellow, "You must enter a valid spell ID."); + + return; + } + } + + if (!aaType && !bySpellID) { if (sep->IsNumber(1)) { spellType = atoi(sep->arg[1]); @@ -342,7 +358,7 @@ void bot_command_cast(Client* c, const Seperator* sep) 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; } @@ -352,62 +368,88 @@ void bot_command_cast(Client* c, const Seperator* sep) //LogTestDebug("{}: 'Attempting {} [{}-{}] on {}'", __LINE__, c->GetSpellTypeNameByID(spellType), (subType != UINT16_MAX ? c->GetSubTypeNameByID(subType) : "Standard"), (subTargetType != UINT16_MAX ? c->GetSubTypeNameByID(subTargetType) : "Standard"), (tar ? tar->GetCleanName() : "NOBODY")); //deleteme if (!tar) { - if (!aaType && spellType != BotSpellTypes::Escape && spellType != BotSpellTypes::Pet) { + if ((!aaType && !bySpellID) && spellType != BotSpellTypes::Escape && spellType != BotSpellTypes::Pet) { c->Message(Chat::Yellow, "You need a target for that."); return; } } - switch (spellType) { //Target Checks - case BotSpellTypes::Resurrect: - if (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse()) { - c->Message(Chat::Yellow, "[%s] is not a player's corpse.", tar->GetCleanName()); - - return; - } - - break; - case BotSpellTypes::Identify: - case BotSpellTypes::SendHome: - case BotSpellTypes::BindAffinity: - case BotSpellTypes::SummonCorpse: - if (!tar->IsClient() || !c->IsInGroupOrRaid(tar)) { - c->Message(Chat::Yellow, "[%s] is an invalid target. Only players in your group or raid are eligible targets.", tar->GetCleanName()); - - return; - } - - break; - default: - if ( - (IsBotSpellTypeDetrimental(spellType) && !c->IsAttackAllowed(tar)) || - ( - spellType == BotSpellTypes::Charm && - ( - tar->IsClient() || - tar->IsCorpse() || - tar->GetOwner() - ) - ) - ) { - c->Message(Chat::Yellow, "You cannot attack [%s].", tar->GetCleanName()); - - return; - } - - if (IsBotSpellTypeBeneficial(spellType)) { - if ( - (tar->IsNPC() && !tar->GetOwner()) || - (tar->GetOwner() && tar->GetOwner()->IsOfClientBot() && !c->IsInGroupOrRaid(tar->GetOwner())) || - (tar->IsOfClientBot() && !c->IsInGroupOrRaid(tar)) - ) { - c->Message(Chat::Yellow, "[%s] is an invalid target. Only players or their pet in your group or raid are eligible targets.", tar->GetCleanName()); + if (!aaType && !bySpellID) { + switch (spellType) { //Target Checks + case BotSpellTypes::Resurrect: + if (!tar->IsCorpse() || !tar->CastToCorpse()->IsPlayerCorpse()) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] is not a player's corpse.", + tar->GetCleanName() + ).c_str() + ); return; } - } - break; + break; + case BotSpellTypes::Identify: + case BotSpellTypes::SendHome: + case BotSpellTypes::BindAffinity: + case BotSpellTypes::SummonCorpse: + if (!tar->IsClient() || !c->IsInGroupOrRaid(tar)) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] is an invalid target. Only players in your group or raid are eligible targets.", + tar->GetCleanName() + ).c_str() + ); + + return; + } + + break; + default: + if ( + (IsBotSpellTypeDetrimental(spellType) && !c->IsAttackAllowed(tar)) || + ( + spellType == BotSpellTypes::Charm && + ( + tar->IsClient() || + tar->IsCorpse() || + tar->GetOwner() + ) + ) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "You cannot attack [{}].", + tar->GetCleanName() + ).c_str() + ); + + return; + } + + if (IsBotSpellTypeBeneficial(spellType)) { + if ( + (tar->IsNPC() && !tar->GetOwner()) || + (tar->GetOwner() && tar->GetOwner()->IsOfClientBot() && !c->IsInGroupOrRaid(tar->GetOwner())) || + (tar->IsOfClientBot() && !c->IsInGroupOrRaid(tar)) + ) { + c->Message( + Chat::Yellow, + fmt::format( + "[{}] is an invalid target. Only players or their pet in your group or raid are eligible targets.", + tar->GetCleanName() + ).c_str() + ); + + return; + } + } + + break; + } } const int ab_mask = ActionableBots::ABM_Type1; @@ -451,7 +493,7 @@ void bot_command_cast(Client* c, const Seperator* sep) Mob* newTar = tar; - if (!aaType) { + if (!aaType && !bySpellID) { //LogTestDebug("{}: {} says, 'Attempting {} [{}-{}] on {}'", __LINE__, bot_iter->GetCleanName(), c->GetSpellTypeNameByID(spellType), (subType != UINT16_MAX ? c->GetSubTypeNameByID(subType) : "Standard"), (subTargetType != UINT16_MAX ? c->GetSubTypeNameByID(subTargetType) : "Standard"), (newTar ? newTar->GetCleanName() : "NOBODY")); //deleteme if (!SpellTypeRequiresTarget(spellType, bot_iter->GetClass())) { newTar = bot_iter; @@ -502,9 +544,48 @@ void bot_command_cast(Client* c, const Seperator* sep) isSuccess = true; ++successCount; + + continue; + } + else if (bySpellID) { + SPDat_Spell_Struct spell = spells[chosenSpellID]; + + LogTestDebug("Starting bySpellID checks."); //deleteme + if (!bot_iter->HasBotSpellEntry(chosenSpellID)) { + LogTestDebug("{} does not have {} [#{}].", bot_iter->GetCleanName(), spell.name, chosenSpellID); //deleteme + continue; + } + + if (!tar || (spell.target_type == ST_Self && tar != bot_iter)) { + LogTestDebug("{} set my target to myself for {} [#{}] due to !tar.", bot_iter->GetCleanName(), spell.name, chosenSpellID); //deleteme + tar = bot_iter; + } + + if (bot_iter->AttemptForcedCastSpell(tar, chosenSpellID)) { + if (!firstFound) { + firstFound = bot_iter; + } + + isSuccess = true; + ++successCount; + } + else { + c->Message( + Chat::Red, + fmt::format( + "{} says, '{} [#{}] failed to cast on [{}]. This could be due to this to any number of things: range, mana, immune, etc.'", + bot_iter->GetCleanName(), + spell.name, + chosenSpellID, + tar->GetCleanName() + ).c_str() + ); + } + + continue; } else { - LogTestDebug("{}: {} says, 'Attempting {} [{}-{}] on {}'", __LINE__, bot_iter->GetCleanName(), c->GetSpellTypeNameByID(spellType), (subType != UINT16_MAX ? c->GetSubTypeNameByID(subType) : "Standard"), (subTargetType != UINT16_MAX ? c->GetSubTypeNameByID(subTargetType) : "Standard"), (newTar ? newTar->GetCleanName() : "NOBODY")); //deleteme + LogTestDebug("{}: {} says, 'Attempting {} [{}-{}] on [{}]'", __LINE__, bot_iter->GetCleanName(), c->GetSpellTypeNameByID(spellType), (subType != UINT16_MAX ? c->GetSubTypeNameByID(subType) : "Standard"), (subTargetType != UINT16_MAX ? c->GetSubTypeNameByID(subTargetType) : "Standard"), (newTar ? newTar->GetCleanName() : "NOBODY")); //deleteme bot_iter->SetCommandedSpell(true); if (bot_iter->AICastSpell(newTar, 100, spellType, subTargetType, subType)) { @@ -516,34 +597,54 @@ void bot_command_cast(Client* c, const Seperator* sep) ++successCount; } else { - bot_iter->GetBotOwner()->Message(Chat::Red, "%s says, 'Ability failed to cast. This could be due to this to any number of things: range, mana, immune, etc.'", bot_iter->GetCleanName()); - - continue; + c->Message( + Chat::Red, + fmt::format( + "{} says, 'Ability failed to cast [{}]. This could be due to this to any number of things: range, mana, immune, etc.'", + bot_iter->GetCleanName(), + tar->GetCleanName() + ).c_str() + ); } bot_iter->SetCommandedSpell(false); + + continue; } continue; } + std::string type = ""; + + if (aaType) { + type = zone->GetAAName(zone->GetAlternateAdvancementAbility(aaID)->first_rank_id); + } + else if (bySpellID) { + type = "Forced"; + } + else { + type = c->GetSpellTypeNameByID(spellType); + } + if (!isSuccess) { c->Message( Chat::Yellow, fmt::format( "No bots are capable of casting [{}] on {}.", - (!aaType ? c->GetSpellTypeNameByID(spellType) : zone->GetAAName(zone->GetAlternateAdvancementAbility(aaID)->first_rank_id)), + (bySpellID ? spells[chosenSpellID].name : type), tar ? tar->GetCleanName() : "your target" ).c_str() ); } else { - c->Message( Chat::Yellow, + c->Message( + Chat::Yellow, fmt::format( "{} {} [{}]{}", ((successCount == 1 && firstFound) ? firstFound->GetCleanName() : (fmt::format("{}", successCount).c_str())), ((successCount == 1 && firstFound) ? "casted" : "of your bots casted"), - (!aaType ? c->GetSpellTypeNameByID(spellType) : zone->GetAAName(zone->GetAlternateAdvancementAbility(aaID)->first_rank_id)), + (bySpellID ? spells[chosenSpellID].name : type), tar ? (fmt::format(" on {}.", tar->GetCleanName()).c_str()) : "." ).c_str() );