From ba835250c8dc8b99993ccfd107ce1c0deff7f40d Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:40:12 +0100 Subject: [PATCH] New whisper command "pvp stats" that allows players to ask a bot to report its current Arena Points, Honor Points, and Arena Teams (#2071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request This PR adds a new whisper command "pvp stats" that allows players to ask a bot to report its current Arena Points, Honor Points, and Arena Teams (name and team rating). Reason: Due to a client limitation in WoW 3.3.5a, the inspection window does not display another player's Arena or Honor points , only team data. This command provides an easy in-game way to check a bot’s PvP currencies without modifying the client or core packets. --- ## Design Philosophy Uses existing core getters (GetArenaPoints, GetHonorPoints, GetArenaTeamId, etc.). Fully integrated into the chat command system (ChatTriggerContext, ChatActionContext). Safe, no gameplay changes, purely informational. No harcoded texts, use database local instead --- ## How to Test the Changes /w BotName pvp stats Bot reply: [PVP] Arena Points: 302 | Honor Points: 11855 [PVP] 2v2: (rating 2000) [PVP] 3v3: (rating 573) ## 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: - [x] 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 --- Multibot already ready Here is a sample of multibot when merged: image --- ## 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> --- ..._ai_playerbot_multi_pvp_text_variables.sql | 101 ++++++++++++++++++ src/Ai/Base/Actions/TellPvpStatsAction.cpp | 100 +++++++++++++++++ src/Ai/Base/Actions/TellPvpStatsAction.h | 20 ++++ src/Ai/Base/ChatActionContext.h | 3 + src/Ai/Base/ChatTriggerContext.h | 2 + .../Strategy/ChatCommandHandlerStrategy.cpp | 2 + src/Bot/PlayerbotAI.cpp | 1 + 7 files changed, 229 insertions(+) create mode 100644 data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql create mode 100644 src/Ai/Base/Actions/TellPvpStatsAction.cpp create mode 100644 src/Ai/Base/Actions/TellPvpStatsAction.h diff --git a/data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql b/data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql new file mode 100644 index 00000000..9889147f --- /dev/null +++ b/data/sql/playerbots/updates/2026_01_31_00_ai_playerbot_multi_pvp_text_variables.sql @@ -0,0 +1,101 @@ +-- ######################################################### +-- Playerbots - Add PVP / Arena texts for TellPvpAction +-- Localized for all WotLK locales (koKR, frFR, deDE, zhCN, +-- zhTW, esES, esMX, ruRU) +-- ######################################################### + +-- --------------------------------------------------------- +-- pvp_currency +-- [PVP] Arena points: %arena_points | Honor Points: %honor_points +-- --------------------------------------------------------- +INSERT INTO `ai_playerbot_texts` + (`name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +SELECT + 'pvp_currency', + '[PVP] Arena points: %arena_points | Honor Points: %honor_points', + 0, 0, + -- koKR + '[PVP] 투기장 점수: %arena_points | 명예 점수: %honor_points', + -- frFR + '[PVP] Points d''arène : %arena_points | Points d''honneur : %honor_points', + -- deDE + '[PVP] Arenapunkte: %arena_points | Ehrenpunkte: %honor_points', + -- zhCN + '[PVP] 竞技场点数:%arena_points | 荣誉点数:%honor_points', + -- zhTW + '[PVP] 競技場點數:%arena_points | 榮譽點數:%honor_points', + -- esES + '[PVP] Puntos de arena: %arena_points | Puntos de honor: %honor_points', + -- esMX + '[PVP] Puntos de arena: %arena_points | Puntos de honor: %honor_points', + -- ruRU + '[PVP] Очки арены: %arena_points | Очки чести: %honor_points' +WHERE NOT EXISTS ( + SELECT 1 FROM `ai_playerbot_texts` WHERE `name` = 'pvp_currency' +); + +-- --------------------------------------------------------- +-- pvp_arena_team +-- [PVP] %bracket: <%team_name> (rating %team_rating) +-- --------------------------------------------------------- +INSERT INTO `ai_playerbot_texts` + (`name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +SELECT + 'pvp_arena_team', + '[PVP] %bracket: <%team_name> (rating %team_rating)', + 0, 0, + -- koKR + '[PVP] %bracket: <%team_name> (평점 %team_rating)', + -- frFR + '[PVP] %bracket : <%team_name> (cote %team_rating)', + -- deDE + '[PVP] %bracket: <%team_name> (Wertung %team_rating)', + -- zhCN + '[PVP] %bracket: <%team_name> (评分 %team_rating)', + -- zhTW + '[PVP] %bracket: <%team_name> (評分 %team_rating)', + -- esES + '[PVP] %bracket: <%team_name> (índice %team_rating)', + -- esMX + '[PVP] %bracket: <%team_name> (índice %team_rating)', + -- ruRU + '[PVP] %bracket: <%team_name> (рейтинг %team_rating)' +WHERE NOT EXISTS ( + SELECT 1 FROM `ai_playerbot_texts` WHERE `name` = 'pvp_arena_team' +); + +-- --------------------------------------------------------- +-- pvp_no_arena_team +-- [PVP] I have no Arena Team. +-- --------------------------------------------------------- +INSERT INTO `ai_playerbot_texts` + (`name`, `text`, `say_type`, `reply_type`, + `text_loc1`, `text_loc2`, `text_loc3`, `text_loc4`, + `text_loc5`, `text_loc6`, `text_loc7`, `text_loc8`) +SELECT + 'pvp_no_arena_team', + '[PVP] I have no Arena Team.', + 0, 0, + -- koKR + '[PVP] 투기장 팀이 없습니다.', + -- frFR + '[PVP] Je n''ai aucune équipe d''arène.', + -- deDE + '[PVP] Ich habe kein Arenateam.', + -- zhCN + '[PVP] 我没有竞技场战队。', + -- zhTW + '[PVP] 我沒有競技場隊伍。', + -- esES + '[PVP] No tengo equipo de arena.', + -- esMX + '[PVP] No tengo equipo de arena.', + -- ruRU + '[PVP] У меня нет команды арены.' +WHERE NOT EXISTS ( + SELECT 1 FROM `ai_playerbot_texts` WHERE `name` = 'pvp_no_arena_team' +); diff --git a/src/Ai/Base/Actions/TellPvpStatsAction.cpp b/src/Ai/Base/Actions/TellPvpStatsAction.cpp new file mode 100644 index 00000000..943cf415 --- /dev/null +++ b/src/Ai/Base/Actions/TellPvpStatsAction.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016+ AzerothCore , 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 "TellPvpStatsAction.h" + +#include + +#include "ArenaTeam.h" +#include "ArenaTeamMgr.h" +#include "Event.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "PlayerbotTextMgr.h" +#include "Playerbots.h" +#include "SharedDefines.h" +#include "Language.h" + +namespace +{ + inline char const* BracketName(uint8 slot) + { + switch (slot) + { + case ARENA_SLOT_2v2: return "2v2"; + case ARENA_SLOT_3v3: return "3v3"; + default: return "5v5"; // ARENA_SLOT_5v5 + } + } +} + +bool TellPvpStatsAction::Execute(Event event) +{ + if (!bot) + return false; + + // Prefer the actual chat sender (whisper / say / etc.) if available. + Player* requester = nullptr; + + if (Unit* owner = event.getOwner()) + requester = owner->ToPlayer(); + + // Fallback to master if event owner is not available. + if (!requester) + requester = GetMaster(); + + // If we still do not have a valid player to answer to, bail out. + if (!requester) + return false; + + // PVP currencies + std::map currencyPlaceholders; + currencyPlaceholders["%arena_points"] = std::to_string(bot->GetArenaPoints()); + currencyPlaceholders["%honor_points"] = std::to_string(bot->GetHonorPoints()); + + std::string const currencyText = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pvp_currency", + "[PVP] Arena points: %arena_points | Honor Points: %honor_points", + currencyPlaceholders); + + bot->Whisper(currencyText, LANG_UNIVERSAL, requester); + + // Arena Teams by slot + bool anyTeam = false; + for (uint8 slot = 0; slot < MAX_ARENA_SLOT; ++slot) + { + uint32 const teamId = bot->GetArenaTeamId(slot); + if (!teamId) + continue; + + if (ArenaTeam* team = sArenaTeamMgr->GetArenaTeamById(teamId)) + { + anyTeam = true; + std::map placeholders; + placeholders["%bracket"] = BracketName(slot); + placeholders["%team_name"] = team->GetName(); + placeholders["%team_rating"] = std::to_string(team->GetRating()); + + std::string const teamText = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pvp_arena_team", + "[PVP] %bracket: <%team_name> (rating %team_rating)", + placeholders); + + bot->Whisper(teamText, LANG_UNIVERSAL, requester); + } + } + + if (!anyTeam) + { + std::string const noTeamText = PlayerbotTextMgr::instance().GetBotTextOrDefault( + "pvp_no_arena_team", + "[PVP] I have no Arena Team.", + std::map()); + + bot->Whisper(noTeamText, LANG_UNIVERSAL, requester); + } + + return true; +} diff --git a/src/Ai/Base/Actions/TellPvpStatsAction.h b/src/Ai/Base/Actions/TellPvpStatsAction.h new file mode 100644 index 00000000..025cbd0d --- /dev/null +++ b/src/Ai/Base/Actions/TellPvpStatsAction.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2016+ AzerothCore , 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. + */ + +#ifndef _PLAYERBOT_TELLPVPSTATSACTION_H +#define _PLAYERBOT_TELLPVPSTATSACTION_H + +#include "Action.h" + +class PlayerbotAI; + +class TellPvpStatsAction : public Action +{ +public: + TellPvpStatsAction(PlayerbotAI* botAI) : Action(botAI, "tell pvp stats") {} + + bool Execute(Event event) override; +}; +#endif \ No newline at end of file diff --git a/src/Ai/Base/ChatActionContext.h b/src/Ai/Base/ChatActionContext.h index 4873f520..5e215e12 100644 --- a/src/Ai/Base/ChatActionContext.h +++ b/src/Ai/Base/ChatActionContext.h @@ -67,6 +67,7 @@ #include "TellItemCountAction.h" #include "TellLosAction.h" #include "TellReputationAction.h" +#include "TellPvpStatsAction.h" #include "TellTargetAction.h" #include "TradeAction.h" #include "TrainerAction.h" @@ -97,6 +98,7 @@ public: creators["quests"] = &ChatActionContext::quests; creators["leave"] = &ChatActionContext::leave; creators["reputation"] = &ChatActionContext::reputation; + creators["tell pvp stats"] = &ChatActionContext::tell_pvp_stats; creators["log"] = &ChatActionContext::log; creators["los"] = &ChatActionContext::los; creators["rpg status"] = &ChatActionContext::rpg_status; @@ -279,6 +281,7 @@ private: static Action* quests(PlayerbotAI* botAI) { return new ListQuestsAction(botAI); } static Action* leave(PlayerbotAI* botAI) { return new LeaveGroupAction(botAI); } static Action* reputation(PlayerbotAI* botAI) { return new TellReputationAction(botAI); } + static Action* tell_pvp_stats(PlayerbotAI* botAI) { return new TellPvpStatsAction(botAI); } static Action* log(PlayerbotAI* botAI) { return new LogLevelAction(botAI); } static Action* los(PlayerbotAI* botAI) { return new TellLosAction(botAI); } static Action* rpg_status(PlayerbotAI* botAI) { return new TellRpgStatusAction(botAI); } diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index b3498ce3..3e71839b 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -24,6 +24,7 @@ public: creators["leave"] = &ChatTriggerContext::leave; creators["rep"] = &ChatTriggerContext::reputation; creators["reputation"] = &ChatTriggerContext::reputation; + creators["pvp stats"] = &ChatTriggerContext::pvp_stats; creators["log"] = &ChatTriggerContext::log; creators["los"] = &ChatTriggerContext::los; creators["rpg status"] = &ChatTriggerContext::rpg_status; @@ -224,6 +225,7 @@ private: static Trigger* stats(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stats"); } static Trigger* leave(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "leave"); } static Trigger* reputation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "reputation"); } + static Trigger* pvp_stats(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "pvp stats"); } static Trigger* log(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "log"); } static Trigger* los(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "los"); } static Trigger* rpg_status(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "rpg status"); } diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index 0a81686a..04f79790 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -27,6 +27,7 @@ void ChatCommandHandlerStrategy::InitTriggers(std::vector& trigger PassTroughStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("rep", { NextAction("reputation", relevance) })); + triggers.push_back(new TriggerNode("pvp stats", { NextAction("tell pvp stats", relevance) })); triggers.push_back(new TriggerNode("q", { NextAction("query quest", relevance), NextAction("query item usage", relevance) })); triggers.push_back(new TriggerNode("add all loot", { NextAction("add all loot", relevance), @@ -116,6 +117,7 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("stats"); supported.push_back("leave"); supported.push_back("reputation"); + supported.push_back("tell pvp stats"); supported.push_back("log"); supported.push_back("los"); supported.push_back("rpg status"); diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 34fc1336..437e62fc 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -892,6 +892,7 @@ bool PlayerbotAI::IsAllowedCommand(std::string const text) unsecuredCommands.insert("invite"); unsecuredCommands.insert("leave"); unsecuredCommands.insert("lfg"); + unsecuredCommands.insert("pvp stats"); unsecuredCommands.insert("rpg status"); }