mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp
Keleborn 441f9f7552
Warnings PR 1: Event warnings and headers (#2106)
# Pull Request

This is the first in a series of PRs intended to eliminate warnings in
the module. The design intent is to eliminate the calling event when not
needed in the body of the function. Based off of SmashingQuasars work.

---

## How to Test the Changes

- Step-by-step instructions to test the change
- Any required setup (e.g. multiple players, bots, specific
configuration)
- Expected behavior and how to verify it

## Complexity & Impact

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

- Does this change increase per-bot or per-tick processing?
    - [x] No
    - [ ] Yes (**describe and justify impact**)

- Could this logic scale poorly under load?
    - [x] No
    - [ ] Yes (**explain why**)

---

## Defaults & Configuration

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

If this introduces more advanced or AI-heavy logic:

- [ ] Lightweight mode remains the default
- [ ] More complex behavior is optional and thereby configurable

---

## AI Assistance

- Was AI assistance (e.g. ChatGPT or similar tools) used while working
on this change?
    - [x] No
    - [ ] Yes (**explain below**)

---

## 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

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or
performance should be carefully discussed
before merging.

---------

Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com>
2026-02-14 20:55:10 +01:00

267 lines
7.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 "ReleaseSpiritAction.h"
#include "ServerFacade.h"
#include "Event.h"
#include "GameGraveyard.h"
#include "NearestNpcsValue.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "Playerbots.h"
#include "ServerFacade.h"
#include "Corpse.h"
#include "Log.h"
// ReleaseSpiritAction implementation
bool ReleaseSpiritAction::Execute(Event event)
{
if (bot->IsAlive())
{
if (!bot->InBattleground())
{
botAI->TellMasterNoFacing("I am not dead, will wait here");
// -follow in bg is overwriten each tick with +follow
// +stay in bg causes stuttering effect as bot is cycled between +stay and +follow each tick
botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT);
}
return false;
}
if (bot->GetCorpse() && bot->HasPlayerFlag(PLAYER_FLAGS_GHOST))
{
botAI->TellMasterNoFacing("I am already a spirit");
return false;
}
const WorldPacket& packet = event.getPacket();
const std::string message = !packet.empty() && packet.GetOpcode() == CMSG_REPOP_REQUEST
? "Releasing..."
: "Meet me at the graveyard";
botAI->TellMasterNoFacing(message);
IncrementDeathCount();
bot->DurabilityRepairAll(false, 1.0f, false);
LogRelease("released");
WorldPacket releasePacket(CMSG_REPOP_REQUEST);
releasePacket << uint8(0);
bot->GetSession()->HandleRepopRequestOpcode(releasePacket);
return true;
}
void ReleaseSpiritAction::IncrementDeathCount() const
{
// Death Count to prevent skeleton piles
Player* master = botAI->GetMaster();
if (!master || GET_PLAYERBOT_AI(master))
{
uint32 deathCount = AI_VALUE(uint32, "death count");
context->GetValue<uint32>("death count")->Set(deathCount + 1);
}
}
void ReleaseSpiritAction::LogRelease(const std::string& releaseMsg, bool isAutoRelease) const
{
const std::string teamPrefix = bot->GetTeamId() == TEAM_ALLIANCE ? "A" : "H";
LOG_DEBUG("playerbots", "Bot {} {}:{} <{}> {}",
bot->GetGUID().ToString().c_str(),
teamPrefix,
bot->GetLevel(),
bot->GetName().c_str(),
releaseMsg.c_str());
}
// AutoReleaseSpiritAction implementation
bool AutoReleaseSpiritAction::Execute(Event /*event*/)
{
IncrementDeathCount();
bot->DurabilityRepairAll(false, 1.0f, false);
LogRelease("auto released", true);
WorldPacket packet(CMSG_REPOP_REQUEST);
packet << uint8(0);
bot->GetSession()->HandleRepopRequestOpcode(packet);
LogRelease("releases spirit", true);
if (bot->InBattleground())
{
return HandleBattlegroundSpiritHealer();
}
botAI->SetNextCheckDelay(1000);
return true;
}
bool AutoReleaseSpiritAction::isUseful()
{
if (!bot->isDead() || bot->InArena())
return false;
if (bot->InBattleground())
return ShouldDelayBattlegroundRelease();
if (bot->HasPlayerFlag(PLAYER_FLAGS_GHOST))
return false;
return ShouldAutoRelease();
}
bool AutoReleaseSpiritAction::HandleBattlegroundSpiritHealer()
{
constexpr uint32_t RESURRECT_DELAY = 15;
const time_t now = time(nullptr);
if ((now - m_bgGossipTime < RESURRECT_DELAY) &&
bot->HasAura(SPELL_WAITING_FOR_RESURRECT))
{
return false;
}
float bgRange = 2000.0f;
GuidVector npcs = NearestNpcsValue(botAI, bgRange);
Unit* spiritHealer = nullptr;
for (auto const& guid : npcs)
{
Unit* unit = botAI->GetUnit(guid);
if (unit && unit->IsFriendlyTo(bot) && unit->IsSpiritService())
{
spiritHealer = unit;
break;
}
}
if (!spiritHealer)
return false;
if (bot->GetDistance(spiritHealer) >= INTERACTION_DISTANCE)
{
// Bot needs to actually click spirit-healer in BG to get res timer going
// and in IOC it's not within clicking range when they res in own base
// Teleport to nearest friendly Spirit Healer when not currently in range of one.
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(bot->GetMapId(), spiritHealer->GetPositionX(), spiritHealer->GetPositionY(), spiritHealer->GetPositionZ(), 0.f);
RESET_AI_VALUE(bool, "combat::self target");
RESET_AI_VALUE(WorldPosition, "current position");
}
else if (!botAI->IsRealPlayer())
{
m_bgGossipTime = now;
WorldPacket packet(CMSG_GOSSIP_HELLO);
packet << spiritHealer->GetGUID();
bot->GetSession()->HandleGossipHelloOpcode(packet);
}
return true;
}
bool AutoReleaseSpiritAction::ShouldAutoRelease() const
{
if (!bot->GetGroup())
return true;
Player* groupLeader = botAI->GetGroupLeader();
if (!groupLeader || groupLeader == bot)
return true;
if (!botAI->HasActivePlayerMaster())
return true;
if (botAI->HasActivePlayerMaster() &&
groupLeader->GetMapId() == bot->GetMapId() &&
bot->GetMap() &&
(bot->GetMap()->IsRaid() || bot->GetMap()->IsDungeon()))
{
return false;
}
return ServerFacade::instance().IsDistanceGreaterThan(
AI_VALUE2(float, "distance", "group leader"),
sPlayerbotAIConfig.sightDistance);
}
bool AutoReleaseSpiritAction::ShouldDelayBattlegroundRelease() const
{
// The below delays release to spirit with 6 seconds.
// This prevents currently casted (ranged) spells to be re-directed to the died bot's ghost.
// If the bot already is a spirit, reset release time and return true
if (bot->HasPlayerFlag(PLAYER_FLAGS_GHOST))
{
botAI->bgReleaseAttemptTime = 0;
return true;
}
// Delay release to spirit.
const time_t now = time(nullptr);
constexpr time_t RELEASE_DELAY = 6;
if (botAI->bgReleaseAttemptTime == 0)
botAI->bgReleaseAttemptTime = now;
if (now - botAI->bgReleaseAttemptTime < RELEASE_DELAY)
return false;
botAI->bgReleaseAttemptTime = 0;
return true;
}
bool RepopAction::Execute(Event /*event*/)
{
const GraveyardStruct* graveyard = GetGrave(
AI_VALUE(uint32, "death count") > 10 ||
CalculateDeadTime() > 30 * MINUTE
);
if (!graveyard)
return false;
PerformGraveyardTeleport(graveyard);
return true;
}
bool RepopAction::isUseful()
{
return !bot->InBattleground();
}
int64 RepopAction::CalculateDeadTime() const
{
if (Corpse* corpse = bot->GetCorpse())
return time(nullptr) - corpse->GetGhostTime();
return bot->isDead() ? 0 : 60 * MINUTE;
}
void RepopAction::PerformGraveyardTeleport(const GraveyardStruct* graveyard) const
{
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
bot->TeleportTo(graveyard->Map, graveyard->x, graveyard->y, graveyard->z, 0.f);
RESET_AI_VALUE(bool, "combat::self target");
RESET_AI_VALUE(WorldPosition, "current position");
}
// SelfResurrectAction implementation for Warlock's Soulstone Resurrection/Shaman's Reincarnation
bool SelfResurrectAction::Execute(Event /*event*/)
{
if (!bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL))
{
WorldPacket packet(CMSG_SELF_RES);
bot->GetSession()->HandleSelfResOpcode(packet);
return true;
}
return false;
}
bool SelfResurrectAction::isUseful()
{
return !bot->IsAlive() && bot->GetUInt32Value(PLAYER_SELF_RES_SPELL);
}