mod-playerbots/src/Ai/Base/Actions/WaitForAttackAction.cpp
kadeshar bbd9d3e37a
Wait for attack strategy migration (#2211)
## Pull Request Description
Migration of "wait for attack" strategy from cmangos playerbots.

Resolves: https://github.com/mod-playerbots/mod-playerbots/issues/990

## Feature Evaluation
Optional strategy for bots which are in party with real player. 

## How to Test the Changes
- add strategy to bot "nc +wait for attack" and "co +wait for attack"
- set time via command "wait for attack time x" where x is time which
they wait in seconds (you should get response from bot)
- attack any target (for example dummy in main city)(bot should wait
with attack)

## Impact Assessment
    - [ ] No, not at all
    - [x] Minimal impact (**explain below**)
    - [ ] Moderate impact (**explain below**)

Performance wise only bots having this optinal strategy have additional
cost in multiplier which check every attack action that should be
execute.

- Does this change modify default bot behavior?
    - [x] No
    - [ ] Yes (**explain why**)



- Does this change add new decision branches or increase maintenance
complexity?
    - [x] No
    - [ ] Yes (**explain below**)



## Messages to Translate
Does this change add bot messages to translate?
- [ ] No
- [x] Yes (**list messages in the table**)

| Message key  | Default message |
| --------------- | ------------------ |
1740 | Please provide a time to set (in seconds)
1741 | Please provide valid time to set (in seconds) between 0 and 99
1742 | Wait for attack time set to %new_time seconds

## AI Assistance
Was AI assistance used while working on this change?
- [ ] No
- [x] Yes (**explain below**)
<!--
If yes, please specify:
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code
generation).
- Which parts of the change were influenced or generated, and whether it
was thoroughly reviewed.
-->

Copilot CLI - help with migration



## Final Checklist

- [x] Stability is not compromised.
- [x] Performance impact is understood, tested, and acceptable.
- [x] Added logic complexity is justified and explained.
- [x] Documentation updated if needed (Conf comments, WiKi commands).

## Notes for Reviewers
<!-- Anything else that's helpful to review or test your pull request.
-->
2026-03-27 10:38:46 -07:00

167 lines
5.6 KiB
C++

/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "WaitForAttackAction.h"
#include <algorithm>
#include <cctype>
#include "ObjectAccessor.h"
#include "PlayerbotAI.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "ServerFacade.h"
#include "TravelMgr.h"
#include "WaitForAttackStrategy.h"
namespace
{
WorldPosition GetBestPoint(AiObjectContext* context, Player* bot, Unit* target,
float minDistance, float maxDistance)
{
WorldPosition botPosition(bot);
WorldPosition targetPosition(target);
int8 startDir = urand(0, 1) * 2 - 1;
float const radiansIncrement = (5.0f / 180.0f) * static_cast<float>(M_PI);
float startAngle = targetPosition.getAngleTo(botPosition) +
frand(0.0f, radiansIncrement) * startDir;
float distance = frand(minDistance, maxDistance);
GuidVector enemies = AI_VALUE(GuidVector, "possible targets no los");
for (float tryAngle = 0.0f; tryAngle < static_cast<float>(M_PI); tryAngle += radiansIncrement)
{
for (int8 tryDir = -1; tryAngle && tryDir < 1; tryDir += 2)
{
float pointAngle = startAngle + tryAngle * startDir * tryDir;
float x = targetPosition.GetPositionX() + distance * cos(pointAngle);
float y = targetPosition.GetPositionY() + distance * sin(pointAngle);
float z = targetPosition.GetPositionZ() + 1.0f;
WorldPosition point(targetPosition.GetMapId(), x, y, z);
point.setZ(point.getHeight());
// Check line of sight to target
if (!target->IsWithinLOS(point.GetPositionX(), point.GetPositionY(),
point.GetPositionZ() + bot->GetCollisionHeight()))
continue;
// Check if enemies are close to this point
bool enemyClose = false;
for (ObjectGuid const& enemyGUID : enemies)
{
Unit* enemy = ObjectAccessor::GetUnit(*bot, enemyGUID);
if (enemy && enemy->IsWithinLOSInMap(bot) && enemy->IsHostileTo(bot))
{
float enemyAttackRange = enemy->GetCombatReach() + ATTACK_DISTANCE;
WorldPosition enemyPos(enemy);
if (enemyPos.sqDistance(point) <= (enemyAttackRange * enemyAttackRange))
{
enemyClose = true;
break;
}
}
}
if (enemyClose)
continue;
// Check if bot can path to this point
if (!botPosition.canPathTo(point, bot))
continue;
return point;
}
}
return botPosition;
}
} // namespace
bool WaitForAttackKeepSafeDistanceAction::Execute(Event /*event*/)
{
Unit* target = AI_VALUE(Unit*, "current target");
// If our target is moving towards a stationary unit, use that unit as anchor
if (target && !target->IsStopped())
{
ObjectGuid targetGuid = target->GetTarget();
if (targetGuid)
{
Unit* targetsTarget = ObjectAccessor::GetUnit(*target, targetGuid);
if (targetsTarget && targetsTarget->IsStopped())
target = targetsTarget;
}
}
if (target && target->IsAlive())
{
float safeDistance = std::max(
target->GetCombatReach() + ATTACK_DISTANCE,
WaitForAttackStrategy::GetSafeDistance());
float safeDistanceThreshold = WaitForAttackStrategy::GetSafeDistanceThreshold();
WorldPosition bestPoint = GetBestPoint(context, bot, target,
safeDistance - safeDistanceThreshold, safeDistance);
if (bestPoint)
return MoveTo(bestPoint.GetMapId(), bestPoint.GetPositionX(),
bestPoint.GetPositionY(), bestPoint.GetPositionZ());
}
return false;
}
bool SetWaitForAttackTimeAction::Execute(Event event)
{
std::string newTimeStr = event.getParam();
if (newTimeStr.empty())
{
std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"wait_for_attack_provide_time",
"Please provide a time to set (in seconds)",
std::map<std::string, std::string>());
botAI->TellMaster(text);
return false;
}
if (!std::all_of(newTimeStr.begin(), newTimeStr.end(), ::isdigit))
{
std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"wait_for_attack_invalid_time",
"Please provide valid time to set (in seconds) between 0 and 99",
std::map<std::string, std::string>());
botAI->TellMaster(text);
return false;
}
int newTime = std::stoi(newTimeStr);
if (newTime < 0 || newTime > 99)
{
std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"wait_for_attack_invalid_time",
"Please provide valid time to set (in seconds) between 0 and 99",
std::map<std::string, std::string>());
botAI->TellMaster(text);
return false;
}
context->GetValue<uint8>("wait for attack time")->Set(static_cast<uint8>(newTime));
std::map<std::string, std::string> placeholders;
placeholders["%new_time"] = std::to_string(newTime);
std::string const text = PlayerbotTextMgr::instance().GetBotTextOrDefault(
"wait_for_attack_time_set",
"Wait for attack time set to %new_time seconds",
placeholders);
botAI->TellMaster(text);
return true;
}