From b5c0a8a27a951557ecfcbb991cdfa98b158bddec Mon Sep 17 00:00:00 2001 From: sindoring Date: Fri, 31 Jul 2026 19:45:21 +0400 Subject: [PATCH] =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B,=20=D1=80=D1=83=D1=81?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D1=8F,=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=98=D0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 36 + docker-compose.override.yml | 42 + docker-compose.prod.yml | 12 + modules/mod-playerbots-ai/README.md | 114 +++ .../conf/mod_playerbots_ai.conf.dist | 29 + modules/mod-playerbots-ai/include.sh | 3 + .../src/mod_playerbots_ai.cpp | 808 +++++++++++++++++ .../src/mod_playerbots_ai_loader.cpp | 6 + .../Base/Actions/AcceptInvitationAction.cpp | 2 +- .../src/Ai/Base/Actions/AcceptQuestAction.cpp | 8 +- .../src/Ai/Base/Actions/AreaTriggerAction.cpp | 6 +- .../src/Ai/Base/Actions/ArenaTeamActions.cpp | 4 +- .../src/Ai/Base/Actions/AttackAction.cpp | 12 +- .../AutoMaintenanceOnLevelupAction.cpp | 2 +- .../src/Ai/Base/Actions/BankAction.cpp | 8 +- .../Ai/Base/Actions/BattleGroundTactics.cpp | 6 +- .../src/Ai/Base/Actions/BuffAction.cpp | 10 +- .../src/Ai/Base/Actions/BuyAction.cpp | 6 +- .../Ai/Base/Actions/CastCustomSpellAction.cpp | 10 +- .../src/Ai/Base/Actions/ChangeChatAction.cpp | 4 +- .../Ai/Base/Actions/ChangeStrategyAction.cpp | 2 +- .../Ai/Base/Actions/ChangeTalentsAction.cpp | 22 +- .../Ai/Base/Actions/ChatShortcutActions.cpp | 26 +- .../Ai/Base/Actions/ChooseRpgTargetAction.cpp | 2 +- .../Ai/Base/Actions/ChooseTargetActions.cpp | 2 +- .../Base/Actions/ChooseTravelTargetAction.cpp | 40 +- .../Base/Actions/CustomStrategyEditAction.cpp | 4 +- .../src/Ai/Base/Actions/DebugAction.cpp | 14 +- .../src/Ai/Base/Actions/DropQuestAction.cpp | 20 +- .../src/Ai/Base/Actions/EmoteAction.cpp | 20 +- .../src/Ai/Base/Actions/EquipAction.cpp | 10 +- .../src/Ai/Base/Actions/EquipGlyphsAction.cpp | 6 +- .../src/Ai/Base/Actions/FlagAction.cpp | 2 +- .../src/Ai/Base/Actions/FollowActions.cpp | 6 +- .../src/Ai/Base/Actions/GenericActions.cpp | 6 +- .../src/Ai/Base/Actions/GiveItemAction.cpp | 4 +- .../src/Ai/Base/Actions/GoAction.cpp | 28 +- .../src/Ai/Base/Actions/GossipHelloAction.cpp | 4 +- .../src/Ai/Base/Actions/GuildAcceptAction.cpp | 6 +- .../src/Ai/Base/Actions/GuildBankAction.cpp | 8 +- .../Base/Actions/GuildManagementActions.cpp | 2 +- .../src/Ai/Base/Actions/HelpAction.cpp | 4 +- .../src/Ai/Base/Actions/HireAction.cpp | 8 +- .../src/Ai/Base/Actions/InventoryAction.cpp | 24 +- .../Ai/Base/Actions/InviteToGroupAction.cpp | 4 +- .../src/Ai/Base/Actions/LeaveGroupAction.cpp | 2 +- .../src/Ai/Base/Actions/ListQuestsActions.cpp | 8 +- .../src/Ai/Base/Actions/ListSpellsAction.cpp | 4 +- .../src/Ai/Base/Actions/LogLevelAction.cpp | 4 +- .../Ai/Base/Actions/LootStrategyAction.cpp | 10 +- .../src/Ai/Base/Actions/MailAction.cpp | 6 +- .../Ai/Base/Actions/MoveToRpgTargetAction.cpp | 2 +- .../Base/Actions/MoveToTravelTargetAction.cpp | 4 +- .../src/Ai/Base/Actions/MovementActions.cpp | 34 +- .../src/Ai/Base/Actions/OpenItemAction.cpp | 2 +- .../src/Ai/Base/Actions/OutfitAction.cpp | 16 +- .../Ai/Base/Actions/PetitionSignAction.cpp | 10 +- .../src/Ai/Base/Actions/PositionAction.cpp | 16 +- .../Ai/Base/Actions/QueryItemUsageAction.cpp | 4 +- .../src/Ai/Base/Actions/QueryQuestAction.cpp | 2 +- .../src/Ai/Base/Actions/QuestAction.cpp | 24 +- .../Base/Actions/QuestConfirmAcceptAction.cpp | 2 +- .../src/Ai/Base/Actions/ReadyCheckAction.cpp | 6 +- .../Ai/Base/Actions/ReleaseSpiritAction.cpp | 4 +- .../src/Ai/Base/Actions/RepairAllAction.cpp | 4 +- .../src/Ai/Base/Actions/ResetAiAction.cpp | 2 +- .../Actions/RevealGatheringItemAction.cpp | 8 +- .../Base/Actions/ReviveFromCorpseAction.cpp | 6 +- .../src/Ai/Base/Actions/RewardAction.cpp | 2 +- .../src/Ai/Base/Actions/RpgSubActions.cpp | 4 +- .../src/Ai/Base/Actions/RtscAction.cpp | 8 +- .../src/Ai/Base/Actions/SaveManaAction.cpp | 4 +- .../Ai/Base/Actions/SecurityCheckAction.cpp | 2 +- .../src/Ai/Base/Actions/SellAction.cpp | 4 +- .../src/Ai/Base/Actions/SendMailAction.cpp | 14 +- .../src/Ai/Base/Actions/SetCraftAction.cpp | 10 +- .../src/Ai/Base/Actions/SetHomeAction.cpp | 8 +- .../src/Ai/Base/Actions/ShareQuestAction.cpp | 4 +- .../Ai/Base/Actions/SkipSpellsListAction.cpp | 8 +- .../Base/Actions/TalkToQuestGiverAction.cpp | 16 +- .../src/Ai/Base/Actions/TameAction.cpp | 42 +- .../src/Ai/Base/Actions/TaxiAction.cpp | 8 +- .../src/Ai/Base/Actions/TeleportAction.cpp | 6 +- .../Ai/Base/Actions/TellCastFailedAction.cpp | 14 +- .../src/Ai/Base/Actions/TellGlyphsAction.cpp | 4 +- .../Ai/Base/Actions/TellItemCountAction.cpp | 2 +- .../src/Ai/Base/Actions/TellLosAction.cpp | 10 +- .../src/Ai/Base/Actions/TellMasterAction.cpp | 2 +- .../Ai/Base/Actions/TellReputationAction.cpp | 8 +- .../src/Ai/Base/Actions/TellTargetAction.cpp | 6 +- .../src/Ai/Base/Actions/TradeAction.cpp | 10 +- .../src/Ai/Base/Actions/TradeStatusAction.cpp | 69 +- .../Actions/TradeStatusExtendedAction.cpp | 2 +- .../src/Ai/Base/Actions/TrainerAction.cpp | 14 +- .../src/Ai/Base/Actions/UnlockItemAction.cpp | 4 +- .../Base/Actions/UnlockTradedItemAction.cpp | 12 +- .../src/Ai/Base/Actions/UseItemAction.cpp | 12 +- .../Ai/Base/Actions/UseMeetingStoneAction.cpp | 18 +- .../src/Ai/Base/Actions/WhoAction.cpp | 4 +- .../src/Ai/Base/Actions/WtsAction.cpp | 2 +- .../src/Ai/Base/Value/Stances.cpp | 2 +- .../src/Ai/World/Rpg/Action/NewRpgAction.cpp | 4 +- .../Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 10 +- .../src/Ai/World/Rpg/NewRpgInfo.cpp | 2 +- .../mod-playerbots/src/Bot/Cmd/ChatHelper.cpp | 2 +- .../mod-playerbots/src/Bot/PlayerbotAI.cpp | 62 +- .../mod-playerbots/src/Bot/PlayerbotMgr.cpp | 44 +- .../src/Bot/RandomPlayerbotMgr.cpp | 4 +- .../src/Script/PlayerbotCommandScript.cpp | 14 +- .../mod-playerbots/src/Script/Playerbots.cpp | 7 + playerbots-hardcoded-phrases-ru.tsv | 393 ++++++++ scripts/prod-deploy.sh | 23 + services/playerbots-ai/.dockerignore | 2 + services/playerbots-ai/Dockerfile | 10 + services/playerbots-ai/app.py | 843 ++++++++++++++++++ setup-modules.sh | 44 +- src/server/game/Chat/Channels/Channel.h | 1 + start-server.sh | 28 + 118 files changed, 2928 insertions(+), 482 deletions(-) create mode 100644 modules/mod-playerbots-ai/README.md create mode 100644 modules/mod-playerbots-ai/conf/mod_playerbots_ai.conf.dist create mode 100644 modules/mod-playerbots-ai/include.sh create mode 100644 modules/mod-playerbots-ai/src/mod_playerbots_ai.cpp create mode 100644 modules/mod-playerbots-ai/src/mod_playerbots_ai_loader.cpp create mode 100644 playerbots-hardcoded-phrases-ru.tsv create mode 100644 services/playerbots-ai/.dockerignore create mode 100644 services/playerbots-ai/Dockerfile create mode 100644 services/playerbots-ai/app.py diff --git a/.env.example b/.env.example index 2771796cb..655c59e52 100644 --- a/.env.example +++ b/.env.example @@ -65,6 +65,42 @@ AC_RATE_XP_KILL=1 # Legacy alias для docker override. Держим синхронно с ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN. AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN=0 +# ------------------------------------------------------------------------------ +# Playerbots AI / Yandex AI Studio +# ------------------------------------------------------------------------------ + +# Включайте после заполнения YANDEX_AI_API_KEY и YANDEX_AI_FOLDER_ID. +ACORE_PLAYERBOTS_AI_ENABLED=0 + +# Секретный API-ключ сервисного аккаунта Yandex AI Studio. +# Не коммитьте заполненное значение. +YANDEX_AI_API_KEY= + +# Идентификатор каталога Yandex Cloud. +YANDEX_AI_FOLDER_ID= + +# Пусто = gpt:///aliceai-llm-flash +YANDEX_AI_MODEL_URI= +YANDEX_AI_BASE_URL=https://ai.api.cloud.yandex.net/v1 + +# Параметры генерации и игрового rate limit. +PLAYERBOTS_AI_REQUEST_TIMEOUT=12 +PLAYERBOTS_AI_TEMPERATURE=0.4 +PLAYERBOTS_AI_MAX_TOKENS=120 +PLAYERBOTS_AI_HISTORY_MESSAGES=6 +PLAYERBOTS_AI_LOG_API_RESPONSES=1 +# Постоянный кэш классификации команд. 604800 секунд = 7 дней. +PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED=1 +PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS=604800 +PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES=10000 +ACORE_PLAYERBOTS_AI_COMMANDS_ENABLED=1 +# 1 = доступно всем; 0 = недоступно никому до реализации подписки. +ACORE_PLAYERBOTS_AI_AVAILABLE_FOR_ALL=1 +ACORE_PLAYERBOTS_AI_PLAYER_COOLDOWN_SECONDS=8 +ACORE_PLAYERBOTS_AI_MAX_QUEUED_REQUESTS=32 +ACORE_PLAYERBOTS_AI_MAX_REPLY_CHARACTERS=240 +ACORE_PLAYERBOTS_AI_DEBUG=0 + # ------------------------------------------------------------------------------ # Значения по умолчанию для ./create-account.sh # ------------------------------------------------------------------------------ diff --git a/docker-compose.override.yml b/docker-compose.override.yml index d59944bad..e85749058 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,4 +1,43 @@ services: + playerbots-ai: + container_name: playerbots-ai + image: moonwell/playerbots-ai:local + build: + context: ./services/playerbots-ai + dockerfile: Dockerfile + networks: + - ac-network + restart: unless-stopped + environment: + PLAYERBOTS_AI_PORT: "8080" + PLAYERBOTS_AI_REQUEST_TIMEOUT: "${PLAYERBOTS_AI_REQUEST_TIMEOUT:-12}" + PLAYERBOTS_AI_TEMPERATURE: "${PLAYERBOTS_AI_TEMPERATURE:-0.4}" + PLAYERBOTS_AI_MAX_TOKENS: "${PLAYERBOTS_AI_MAX_TOKENS:-120}" + PLAYERBOTS_AI_MAX_REPLY_CHARACTERS: "${ACORE_PLAYERBOTS_AI_MAX_REPLY_CHARACTERS:-240}" + PLAYERBOTS_AI_HISTORY_MESSAGES: "${PLAYERBOTS_AI_HISTORY_MESSAGES:-6}" + PLAYERBOTS_AI_COMMANDS_ENABLED: "${ACORE_PLAYERBOTS_AI_COMMANDS_ENABLED:-1}" + PLAYERBOTS_AI_LOG_API_RESPONSES: "${PLAYERBOTS_AI_LOG_API_RESPONSES:-1}" + PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED: "${PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED:-1}" + PLAYERBOTS_AI_CLASSIFIER_CACHE_DB: "/data/classifier-cache.sqlite3" + PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS: "${PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS:-604800}" + PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES: "${PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES:-10000}" + PLAYERBOTS_AI_SYSTEM_PROMPT: "${PLAYERBOTS_AI_SYSTEM_PROMPT:-}" + YANDEX_AI_API_KEY: "${YANDEX_AI_API_KEY:-}" + YANDEX_AI_FOLDER_ID: "${YANDEX_AI_FOLDER_ID:-}" + YANDEX_AI_MODEL_URI: "${YANDEX_AI_MODEL_URI:-}" + YANDEX_AI_BASE_URL: "${YANDEX_AI_BASE_URL:-https://ai.api.cloud.yandex.net/v1}" + volumes: + - playerbots-ai-cache:/data + healthcheck: + test: + - CMD + - python + - -c + - "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=2)" + interval: 5s + timeout: 3s + retries: 12 + ac-worldserver: stdin_open: true tty: true @@ -9,3 +48,6 @@ services: - ./modules:/azerothcore/modules:ro - ./lua_scripts:/azerothcore/lua_scripts - ./client_patch/enUS/DBFilesClient/LFGDungeons.dbc:/azerothcore/env/dist/data/dbc/LFGDungeons.dbc:ro + +volumes: + playerbots-ai-cache: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2518cbb9c..7d5c151ff 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,4 +1,16 @@ services: + playerbots-ai: + deploy: + resources: + limits: + cpus: '0.5' + memory: 128M + logging: + driver: "json-file" + options: + max-size: "20m" + max-file: "3" + ac-database: command: > --innodb_buffer_pool_size=4G diff --git a/modules/mod-playerbots-ai/README.md b/modules/mod-playerbots-ai/README.md new file mode 100644 index 000000000..435332467 --- /dev/null +++ b/modules/mod-playerbots-ai/README.md @@ -0,0 +1,114 @@ +# MoonWell Playerbots AI + +This module sends a local `/say` or public-channel message to the LLM only when +a real player mentions the complete name of an online bot. A channel bot must +be a channel member; a `/say` bot must be within the server's normal listening +range. HTTP work runs outside the AzerothCore world thread. + +Alice AI LLM Flash can classify direct natural-language requests into a small +allowlisted set of native playerbots commands. The server validates the command +again and executes it only against the addressed bot; existing playerbots +ownership and group security checks still apply. + +The routing prompt covers party movement and combat, LFG, combat/non-combat +strategies, spells, RTI/RTSC, focus healing, loot lists, inventory operations, +quests, pets, talents, glyphs, maintenance, and information commands documented +in the upstream Playerbot Commands wiki. Administrative, console, account, +debug/cheat, and destructive `destroy`/`drop`/mail commands are intentionally +not executable through the LLM. + +Common Russian movement/combat phrases use a deterministic fast path. Other +requests first pass through a command-only classifier; ordinary conversation is +then sent separately to the roleplay prompt. While AI routing is enabled, the +legacy public-channel parser is bypassed and `setup-modules.sh` also forces +`AiPlayerbot.RandomBotTalk = 0`. RandomBotTalk consumes incoming chat packets +inside each bot, so bypassing only the parser does not prevent its delayed +stock replies. Whisper, party, raid, and guild command hooks remain unchanged. + +Recognized commands produce a gender-aware local-chat acknowledgement +(`Понял команду.` or `Поняла команду.`) before they are handed to the native +playerbots command handler. Bot and player gender are also included in the LLM +context for pronoun, past-tense verb, and adjective agreement. Russian requests +to show bags or inventory use the native `inv` command; `open items` is reserved +for actually opening container items. + +Trade and quest requests have deterministic link-preserving routes before the +LLM classifier: + +- `дай/передай [item]` -> `t [item]`; `начни обмен` -> `t` +- `покажи задания` -> `quests all`; completed/incomplete filters are supported +- `покажи прогресс [quest]` -> `q [quest]` +- `прими [quest]` / `прими все задания` -> `accept [quest]` / `accept *` +- `поделись [quest]` -> `share [quest]` +- `выбери [item] в награду` -> `r [item]` +- `сдай готовое задание` -> `talk` +- `откажись от [quest]` -> `drop [quest]` only with an explicit quest link + +These routes preserve the original WoW hyperlink byte-for-byte. The local +playerbots trade action continues processing after it creates the trade, so one +`t [item]` command both initiates the exchange and places the requested item in +the trade window. + +The bundled `playerbots-ai` bridge uses the Yandex AI Studio OpenAI-compatible +Chat Completions API and defaults to Alice AI LLM Flash. + +## Configuration + +Set these values in the repository `.env`: + +```dotenv +ACORE_PLAYERBOTS_AI_ENABLED=1 +ACORE_PLAYERBOTS_AI_COMMANDS_ENABLED=1 +YANDEX_AI_API_KEY= +YANDEX_AI_FOLDER_ID= +``` + +Until the subscription system exists, public access is controlled by one +explicit switch: + +```dotenv +ACORE_PLAYERBOTS_AI_AVAILABLE_FOR_ALL=1 +``` + +`1` allows every player to reach the bridge. `0` denies LLM access to everyone +without spending tokens. When subscriptions are implemented, their entitlement +check will be added to the disabled branch instead of using a temporary manual +account list. + +The command classifier has a persistent SQLite cache in the +`playerbots-ai-cache` Docker volume. The key excludes the addressed bot name, +so repeated phrases such as “покажи инвентарь” are shared across players and +bots. Both commands and “ordinary chat, not a command” decisions are cached. +The cache does not store roleplay responses or conversation history. +Obvious social phrases such as greetings, “как дела?” and “расскажи о себе” +skip the command classifier entirely: they still receive an Alice roleplay +reply, but do not pay the roughly 800-token command-catalog prompt first. + +```dotenv +PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED=1 +PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS=604800 +PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES=10000 +``` + +Changing the classifier prompt automatically creates a new cache namespace. +Old rows expire normally, and the oldest rows are removed at the size limit. + +The default model URI is: + +```text +gpt:///aliceai-llm-flash +``` + +Run `./start-server.sh` or `scripts/prod-deploy.sh`. The deployment wrapper +builds and starts the bridge before `ac-worldserver`. + +Never commit a populated API key. + +Full Yandex response JSON is logged by default by the bridge: + +```dotenv +PLAYERBOTS_AI_LOG_API_RESPONSES=1 +``` + +Use `docker compose logs -f playerbots-ai` to follow classifier and chat +responses. Authentication headers and the API key are never logged. diff --git a/modules/mod-playerbots-ai/conf/mod_playerbots_ai.conf.dist b/modules/mod-playerbots-ai/conf/mod_playerbots_ai.conf.dist new file mode 100644 index 000000000..ed9cb8581 --- /dev/null +++ b/modules/mod-playerbots-ai/conf/mod_playerbots_ai.conf.dist @@ -0,0 +1,29 @@ +# +# MoonWell Playerbots AI +# + +# Enables LLM replies in public channels. +PlayerbotsAI.Enabled = 0 + +# Enables natural-language commands through an allowlisted playerbots command set. +PlayerbotsAI.CommandsEnabled = 1 + +# Temporary public-access switch. A future subscription check will be inserted +# when this is disabled; until that system exists, 0 denies access to everyone. +PlayerbotsAI.AvailableForAll = 1 + +# Internal HTTP bridge. The Yandex API key is kept in the bridge container. +PlayerbotsAI.BridgeHost = playerbots-ai +PlayerbotsAI.BridgePort = 8080 +PlayerbotsAI.BridgePath = /v1/chat + +# Per-player request cooldown and bounded worker queue. +PlayerbotsAI.PlayerCooldownSeconds = 8 +PlayerbotsAI.MaxQueuedRequests = 32 + +# Maximum UTF-8 characters sent back to an in-game channel. +PlayerbotsAI.MaxReplyCharacters = 240 + +# Extra request/response logging. Prompts and API keys are never logged. +PlayerbotsAI.Debug = 0 +п diff --git a/modules/mod-playerbots-ai/include.sh b/modules/mod-playerbots-ai/include.sh new file mode 100644 index 000000000..e9f37ac7b --- /dev/null +++ b/modules/mod-playerbots-ai/include.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +# This module uses AzerothCore's standard source and loader discovery. diff --git a/modules/mod-playerbots-ai/src/mod_playerbots_ai.cpp b/modules/mod-playerbots-ai/src/mod_playerbots_ai.cpp new file mode 100644 index 000000000..7082be2cb --- /dev/null +++ b/modules/mod-playerbots-ai/src/mod_playerbots_ai.cpp @@ -0,0 +1,808 @@ +#include "Channel.h" +#include "ChannelMgr.h" +#include "Config.h" +#include "Language.h" +#include "Log.h" +#include "ObjectAccessor.h" +#include "Player.h" +#include "PlayerbotAI.h" +#include "PlayerbotMgr.h" +#include "RandomPlayerbotMgr.h" +#include "ScriptMgr.h" +#include "World.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +struct AiRequest +{ + ObjectGuid botGuid; + ObjectGuid playerGuid; + TeamId teamId; + std::string channelName; + std::string host; + std::string port; + std::string path; + std::string body; +}; + +struct AiResponse +{ + ObjectGuid botGuid; + ObjectGuid playerGuid; + TeamId teamId; + std::string channelName; + std::string command; + std::string text; +}; + +struct BridgeResponse +{ + std::string command; + std::string text; +}; + +std::string JsonEscape(std::string const& value) +{ + std::ostringstream output; + for (unsigned char character : value) + { + switch (character) + { + case '"': output << "\\\""; break; + case '\\': output << "\\\\"; break; + case '\b': output << "\\b"; break; + case '\f': output << "\\f"; break; + case '\n': output << "\\n"; break; + case '\r': output << "\\r"; break; + case '\t': output << "\\t"; break; + default: + if (character < 0x20) + { + output << "\\u" << std::hex << std::setw(4) << std::setfill('0') + << static_cast(character) << std::dec; + } + else + { + output << static_cast(character); + } + break; + } + } + return output.str(); +} + +std::vector DecodeAndCaseFold(std::string const& text) +{ + std::vector result; + result.reserve(text.size()); + + for (std::size_t index = 0; index < text.size();) + { + unsigned char first = static_cast(text[index]); + uint32 codepoint = 0xFFFD; + std::size_t length = 1; + + if (first < 0x80) + { + codepoint = first; + } + else if ((first & 0xE0) == 0xC0 && index + 1 < text.size()) + { + codepoint = ((first & 0x1F) << 6) + | (static_cast(text[index + 1]) & 0x3F); + length = 2; + } + else if ((first & 0xF0) == 0xE0 && index + 2 < text.size()) + { + codepoint = ((first & 0x0F) << 12) + | ((static_cast(text[index + 1]) & 0x3F) << 6) + | (static_cast(text[index + 2]) & 0x3F); + length = 3; + } + else if ((first & 0xF8) == 0xF0 && index + 3 < text.size()) + { + codepoint = ((first & 0x07) << 18) + | ((static_cast(text[index + 1]) & 0x3F) << 12) + | ((static_cast(text[index + 2]) & 0x3F) << 6) + | (static_cast(text[index + 3]) & 0x3F); + length = 4; + } + + if (codepoint >= 'A' && codepoint <= 'Z') + codepoint += 'a' - 'A'; + else if (codepoint >= 0x0410 && codepoint <= 0x042F) + codepoint += 0x20; + else if (codepoint == 0x0401) + codepoint = 0x0451; + + result.push_back(codepoint); + index += length; + } + + return result; +} + +bool IsWordCodepoint(uint32 codepoint) +{ + return (codepoint >= '0' && codepoint <= '9') + || (codepoint >= 'a' && codepoint <= 'z') + || (codepoint >= 0x0430 && codepoint <= 0x044F) + || codepoint == 0x0451; +} + +std::optional FindCompleteName( + std::vector const& message, + std::vector const& name) +{ + if (name.empty() || name.size() > message.size()) + return std::nullopt; + + for (std::size_t position = 0; position + name.size() <= message.size(); ++position) + { + if (!std::equal(name.begin(), name.end(), message.begin() + position)) + continue; + + bool leftBoundary = position == 0 || !IsWordCodepoint(message[position - 1]); + std::size_t end = position + name.size(); + bool rightBoundary = end == message.size() || !IsWordCodepoint(message[end]); + if (leftBoundary && rightBoundary) + return position; + } + + return std::nullopt; +} + +std::string NormalizeReply(std::string text, uint32 maxCharacters) +{ + std::replace(text.begin(), text.end(), '\r', ' '); + std::replace(text.begin(), text.end(), '\n', ' '); + + std::string compact; + compact.reserve(text.size()); + bool previousSpace = false; + for (unsigned char character : text) + { + bool isSpace = character == ' ' || character == '\t'; + if (isSpace && previousSpace) + continue; + compact.push_back(isSpace ? ' ' : static_cast(character)); + previousSpace = isSpace; + } + + std::size_t first = compact.find_first_not_of(' '); + if (first == std::string::npos) + return {}; + compact.erase(0, first); + std::size_t last = compact.find_last_not_of(' '); + compact.erase(last + 1); + + if (!maxCharacters) + return compact; + + std::size_t byteIndex = 0; + uint32 characters = 0; + while (byteIndex < compact.size() && characters < maxCharacters) + { + unsigned char firstByte = static_cast(compact[byteIndex]); + std::size_t length = firstByte < 0x80 ? 1 + : (firstByte & 0xE0) == 0xC0 ? 2 + : (firstByte & 0xF0) == 0xE0 ? 3 + : (firstByte & 0xF8) == 0xF0 ? 4 + : 1; + byteIndex = std::min(compact.size(), byteIndex + length); + ++characters; + } + + if (byteIndex < compact.size()) + { + compact.resize(byteIndex); + compact += "…"; + } + return compact; +} + +BridgeResponse ParseBridgeResponse(std::string response) +{ + static std::string const commandPrefix = "PB-COMMAND:"; + if (response.rfind(commandPrefix, 0) != 0) + return {"", std::move(response)}; + + std::size_t newline = response.find('\n'); + if (newline == std::string::npos) + return {"", ""}; + + return { + response.substr(commandPrefix.size(), newline - commandPrefix.size()), + response.substr(newline + 1) + }; +} + +bool IsAllowedPlayerbotCommand(std::string const& command) +{ + if (command.empty() || command.size() > 300 || command.find('\\') != std::string::npos) + return false; + + if (std::any_of(command.begin(), command.end(), [](unsigned char character) + { + return character < 0x20; + })) + { + return false; + } + + static std::set const allowedCommands = { + "accept", + "accept *", + "add all loot", + "attack", + "attack rti target", + "autogear", + "buff", + "disperse disable", + "flee", + "focus heal ?", + "focus heal clear", + "focus heal none", + "focus heal unset", + "follow", + "give leader", + "glyphs", + "grind", + "help", + "home", + "inv", + "invite", + "items", + "leave", + "lfg", + "ll all", + "ll gray", + "ll normal", + "ll quest", + "ll skill", + "los", + "maintenance", + "open items", + "outfit ?", + "pet aggressive", + "pet attack", + "pet defensive", + "pet follow", + "pet passive", + "pet stance", + "pet stay", + "quests", + "quests all", + "quests completed", + "quests incompleted", + "ready", + "release", + "reset", + "reset botAI", + "revive", + "roll", + "rti", + "rtsc", + "rtsc cancel", + "rtsc go save", + "rtsc toggle", + "runaway", + "spells", + "ss reset", + "stay", + "stats", + "summon", + "t", + "talents", + "talents spec list", + "talk", + "tame", + "tame family", + "tank attack", + "trainer", + "trainer learn", + "who" + }; + if (allowedCommands.find(command) != allowedCommands.end()) + return true; + + static std::vector const allowedPrefixes = { + "accept ", + "b ", + "bank ", + "cast ", + "co ", + "disperse set ", + "e ", + "focus heal ", + "gb ", + "glyph equip ", + "lfg ", + "ll ", + "nc ", + "outfit ", + "pet ", + "q ", + "r ", + "roll ", + "rti ", + "rtsc ", + "s ", + "ss ", + "share ", + "talents apply ", + "talents spec ", + "tame ", + "t ", + "drop ", + "u ", + "ue ", + "who " + }; + return std::any_of( + allowedPrefixes.begin(), + allowedPrefixes.end(), + [&command](std::string const& prefix) + { + return command.size() > prefix.size() && command.rfind(prefix, 0) == 0; + }); +} + +std::string ReadHttpBody( + std::string const& host, + std::string const& port, + std::string const& path, + std::string const& body) +{ + using boost::asio::ip::tcp; + + boost::asio::io_context ioContext; + tcp::resolver resolver(ioContext); + tcp::socket socket(ioContext); + boost::asio::connect(socket, resolver.resolve(host, port)); + + std::ostringstream request; + request << "POST " << path << " HTTP/1.1\r\n" + << "Host: " << host << ':' << port << "\r\n" + << "User-Agent: MoonWell-playerbots-ai/1.0\r\n" + << "Content-Type: application/json; charset=utf-8\r\n" + << "Accept: text/plain\r\n" + << "Connection: close\r\n" + << "Content-Length: " << body.size() << "\r\n\r\n" + << body; + + boost::asio::write(socket, boost::asio::buffer(request.str())); + + boost::asio::streambuf response; + boost::asio::read_until(socket, response, "\r\n\r\n"); + std::istream responseStream(&response); + + std::string httpVersion; + unsigned int statusCode = 0; + std::string statusMessage; + responseStream >> httpVersion >> statusCode; + std::getline(responseStream, statusMessage); + if (!responseStream || httpVersion.rfind("HTTP/", 0) != 0) + throw std::runtime_error("invalid HTTP response"); + + std::string header; + while (std::getline(responseStream, header) && header != "\r") + { + } + + std::ostringstream responseBody; + if (response.size()) + responseBody << &response; + + boost::system::error_code error; + while (boost::asio::read(socket, response, boost::asio::transfer_at_least(1), error)) + responseBody << &response; + if (error != boost::asio::error::eof) + throw boost::system::system_error(error); + + if (response.size()) + responseBody << &response; + + if (statusCode < 200 || statusCode >= 300) + { + throw std::runtime_error( + "bridge returned HTTP " + std::to_string(statusCode) + ": " + responseBody.str()); + } + + return responseBody.str(); +} + +class PlayerbotsAiBridge +{ +public: + static PlayerbotsAiBridge& Instance() + { + static PlayerbotsAiBridge instance; + return instance; + } + + void Start() + { + bool expected = false; + if (!_running.compare_exchange_strong(expected, true)) + return; + + _stopping = false; + _worker = std::thread([this] { Run(); }); + LOG_INFO("server.loading", "[PlayerbotsAI] Async bridge worker started"); + } + + void Stop() + { + if (!_running.exchange(false)) + return; + + { + std::lock_guard lock(_requestMutex); + _stopping = true; + } + _requestReady.notify_all(); + if (_worker.joinable()) + _worker.join(); + + std::lock_guard requestLock(_requestMutex); + _requests.clear(); + LOG_INFO("server.loading", "[PlayerbotsAI] Async bridge worker stopped"); + } + + bool Submit(AiRequest request, uint32 maxQueuedRequests) + { + if (!_running) + return false; + + std::lock_guard lock(_requestMutex); + if (_stopping || _requests.size() >= maxQueuedRequests) + return false; + _requests.push_back(std::move(request)); + _requestReady.notify_one(); + return true; + } + + std::deque TakeResponses() + { + std::lock_guard lock(_responseMutex); + std::deque responses; + responses.swap(_responses); + return responses; + } + +private: + PlayerbotsAiBridge() = default; + ~PlayerbotsAiBridge() + { + Stop(); + } + + void Run() + { + while (true) + { + AiRequest request; + { + std::unique_lock lock(_requestMutex); + _requestReady.wait(lock, [this] { return _stopping || !_requests.empty(); }); + if (_stopping && _requests.empty()) + break; + request = std::move(_requests.front()); + _requests.pop_front(); + } + + try + { + BridgeResponse response = ParseBridgeResponse(ReadHttpBody( + request.host, request.port, request.path, request.body)); + std::lock_guard lock(_responseMutex); + _responses.push_back({ + request.botGuid, + request.playerGuid, + request.teamId, + std::move(request.channelName), + std::move(response.command), + std::move(response.text) + }); + } + catch (std::exception const& error) + { + LOG_ERROR("server.loading", "[PlayerbotsAI] Bridge request failed: {}", error.what()); + } + } + } + + std::atomic _running{false}; + bool _stopping{false}; + std::thread _worker; + std::mutex _requestMutex; + std::condition_variable _requestReady; + std::deque _requests; + std::mutex _responseMutex; + std::deque _responses; +}; + +bool IsEnabled() +{ + return sConfigMgr->GetOption("PlayerbotsAI.Enabled", false); +} + +bool IsAvailableForAll() +{ + return sConfigMgr->GetOption("PlayerbotsAI.AvailableForAll", true); +} + +AiRequest BuildRequest( + Player* bot, + Player* player, + std::string channelName, + std::string const& message) +{ + std::ostringstream body; + body << '{' + << "\"bot_guid\":\"" << bot->GetGUID().GetCounter() << "\"," + << "\"player_guid\":\"" << player->GetGUID().GetCounter() << "\"," + << "\"bot_name\":\"" << JsonEscape(bot->GetName()) << "\"," + << "\"player_name\":\"" << JsonEscape(player->GetName()) << "\"," + << "\"message\":\"" << JsonEscape(message) << "\"," + << "\"bot_gender\":" << static_cast(bot->getGender()) << ',' + << "\"player_gender\":" << static_cast(player->getGender()) << ',' + << "\"bot_class\":" << static_cast(bot->getClass()) << ',' + << "\"bot_race\":" << static_cast(bot->getRace()) << ',' + << "\"bot_level\":" << static_cast(bot->GetLevel()) << ',' + << "\"zone_id\":" << bot->GetZoneId() + << '}'; + + return { + bot->GetGUID(), + player->GetGUID(), + bot->GetTeamId(), + std::move(channelName), + sConfigMgr->GetOption("PlayerbotsAI.BridgeHost", "playerbots-ai"), + std::to_string(sConfigMgr->GetOption("PlayerbotsAI.BridgePort", 8080)), + sConfigMgr->GetOption("PlayerbotsAI.BridgePath", "/v1/chat"), + body.str() + }; +} + +class PlayerbotsAiPlayerScript : public PlayerScript +{ +public: + PlayerbotsAiPlayerScript() + : PlayerScript( + "PlayerbotsAiPlayerScript", + { + PLAYERHOOK_ON_BEFORE_SEND_CHAT_MESSAGE, + PLAYERHOOK_CAN_PLAYER_USE_CHANNEL_CHAT + }) + { + } + + void OnPlayerBeforeSendChatMessage( + Player* player, + uint32& type, + uint32& /*language*/, + std::string& message) override + { + if (type == CHAT_MSG_SAY) + HandleAddressedMessage(player, message, nullptr); + } + + bool OnPlayerCanUseChat( + Player* player, + uint32 type, + uint32 /*language*/, + std::string& message, + Channel* channel) override + { + if (type == CHAT_MSG_CHANNEL) + HandleAddressedMessage(player, message, channel); + return true; + } + +private: + void HandleAddressedMessage(Player* player, std::string const& message, Channel* channel) + { + if (!IsEnabled() || !player || message.empty()) + return; + + if (PlayerbotsMgr::instance().GetPlayerbotAI(player)) + return; + + std::vector foldedMessage = DecodeAndCaseFold(message); + Player* selectedBot = nullptr; + std::size_t selectedPosition = foldedMessage.size(); + + PlayerBotMap bots = sRandomPlayerbotMgr.GetAllBots(); + if (PlayerbotMgr* playerbotMgr = PlayerbotsMgr::instance().GetPlayerbotMgr(player)) + { + for (auto iterator = playerbotMgr->GetPlayerBotsBegin(); + iterator != playerbotMgr->GetPlayerBotsEnd(); + ++iterator) + { + bots.emplace(iterator->first, iterator->second); + } + } + for (auto const& [guid, bot] : bots) + { + if (!bot || !bot->IsInWorld()) + continue; + + if (channel) + { + if (!channel->HasMember(guid)) + continue; + } + else if (!bot->IsWithinDistInMap( + player, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY))) + { + continue; + } + + std::optional position = + FindCompleteName(foldedMessage, DecodeAndCaseFold(bot->GetName())); + if (position && *position < selectedPosition) + { + selectedBot = bot; + selectedPosition = *position; + } + } + + if (!selectedBot) + return; + + if (!IsAvailableForAll()) + { + std::string const deniedReply = "Разговоры со мной через ИИ сейчас недоступны."; + if (channel) + channel->Say(selectedBot->GetGUID(), deniedReply, LANG_UNIVERSAL); + else + selectedBot->Say(deniedReply, LANG_UNIVERSAL); + return; + } + + auto now = std::chrono::steady_clock::now(); + uint32 cooldownSeconds = + sConfigMgr->GetOption("PlayerbotsAI.PlayerCooldownSeconds", 8); + auto cooldown = std::chrono::seconds(cooldownSeconds); + auto found = _lastRequest.find(player->GetGUID()); + if (found != _lastRequest.end() && now - found->second < cooldown) + return; + + uint32 maxQueuedRequests = + std::max(1, sConfigMgr->GetOption("PlayerbotsAI.MaxQueuedRequests", 32)); + if (!PlayerbotsAiBridge::Instance().Submit( + BuildRequest( + selectedBot, + player, + channel ? channel->GetName() : std::string(), + message), + maxQueuedRequests)) + { + LOG_WARN("server.loading", "[PlayerbotsAI] Request queue is unavailable or full"); + return; + } + + _lastRequest[player->GetGUID()] = now; + if (sConfigMgr->GetOption("PlayerbotsAI.Debug", false)) + { + LOG_INFO( + "server.loading", + "[PlayerbotsAI] {} addressed bot {} in {}", + player->GetName(), + selectedBot->GetName(), + channel ? channel->GetName() : "/say"); + } + } + + std::map _lastRequest; +}; + +class PlayerbotsAiWorldScript : public WorldScript +{ +public: + PlayerbotsAiWorldScript() + : WorldScript( + "PlayerbotsAiWorldScript", + {WORLDHOOK_ON_STARTUP, WORLDHOOK_ON_UPDATE, WORLDHOOK_ON_SHUTDOWN}) + { + } + + void OnStartup() override + { + if (IsEnabled()) + PlayerbotsAiBridge::Instance().Start(); + } + + void OnUpdate(uint32 /*diff*/) override + { + if (!IsEnabled()) + return; + + uint32 maxCharacters = + sConfigMgr->GetOption("PlayerbotsAI.MaxReplyCharacters", 240); + for (AiResponse& response : PlayerbotsAiBridge::Instance().TakeResponses()) + { + Player* bot = ObjectAccessor::FindPlayer(response.botGuid); + if (!bot || !bot->IsInWorld() || !PlayerbotsMgr::instance().GetPlayerbotAI(bot)) + continue; + + Player* player = ObjectAccessor::FindPlayer(response.playerGuid); + if (!player || !player->IsInWorld()) + continue; + + Channel* channel = nullptr; + if (response.channelName.empty()) + { + if (!bot->IsWithinDistInMap( + player, sWorld->getFloatConfig(CONFIG_LISTEN_RANGE_SAY))) + { + continue; + } + } + else + { + ChannelMgr* channelManager = ChannelMgr::forTeam(response.teamId); + channel = channelManager + ? channelManager->GetChannel(response.channelName, bot, false) + : nullptr; + if (!channel || !channel->HasMember(bot->GetGUID())) + continue; + } + + if (sConfigMgr->GetOption("PlayerbotsAI.CommandsEnabled", true) + && IsAllowedPlayerbotCommand(response.command)) + { + PlayerbotAI* botAi = PlayerbotsMgr::instance().GetPlayerbotAI(bot); + botAi->HandleCommand(CHAT_MSG_WHISPER, response.command, player); + } + + std::string reply = NormalizeReply(std::move(response.text), maxCharacters); + if (!reply.empty()) + { + if (channel) + channel->Say(bot->GetGUID(), reply, LANG_UNIVERSAL); + else + bot->Say(reply, LANG_UNIVERSAL); + } + } + } + + void OnShutdown() override + { + PlayerbotsAiBridge::Instance().Stop(); + } +}; +} + +void AddSC_mod_playerbots_ai() +{ + new PlayerbotsAiPlayerScript(); + new PlayerbotsAiWorldScript(); +} diff --git a/modules/mod-playerbots-ai/src/mod_playerbots_ai_loader.cpp b/modules/mod-playerbots-ai/src/mod_playerbots_ai_loader.cpp new file mode 100644 index 000000000..b1421f101 --- /dev/null +++ b/modules/mod-playerbots-ai/src/mod_playerbots_ai_loader.cpp @@ -0,0 +1,6 @@ +void AddSC_mod_playerbots_ai(); + +void Addmod_playerbots_aiScripts() +{ + AddSC_mod_playerbots_ai(); +} diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp index 1be3c0921..2df2b2053 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptInvitationAction.cpp @@ -55,7 +55,7 @@ bool AcceptInvitationAction::Execute(Event event) botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT); botAI->Reset(); - botAI->TellMaster("Hello"); + botAI->TellMaster("Здравствуйте."); if (sPlayerbotAIConfig.summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig.sightDistance) { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp index 005cfb08a..1d6c8f564 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AcceptQuestAction.cpp @@ -18,7 +18,7 @@ bool AcceptAllQuestsAction::ProcessQuest(Quest const* quest, Object* questGiver) if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { LOG_INFO("playerbots", "{} => Quest [{}] accepted", bot->GetName(), quest->GetTitle()); - bot->Say("Quest [" + text_quest + "] accepted", LANG_UNIVERSAL); + bot->Say("Задание [" + text_quest + "] accepted", LANG_UNIVERSAL); } return true; @@ -113,7 +113,7 @@ bool AcceptQuestShareAction::Execute(Event event) if (bot->HasQuest(quest)) { bot->SetDivider(ObjectGuid::Empty); - botAI->TellError("I have this quest"); + botAI->TellError("У меня уже есть это задание"); return false; } @@ -121,7 +121,7 @@ bool AcceptQuestShareAction::Execute(Event event) { // can't take quest bot->SetDivider(ObjectGuid::Empty); - botAI->TellError("I can't take this quest"); + botAI->TellError("Я не могу взять это задание"); return false; } @@ -149,7 +149,7 @@ bool AcceptQuestShareAction::Execute(Event event) bot->CastSpell(bot, qInfo->GetSrcSpell(), true); } - botAI->TellMaster("Quest accepted"); + botAI->TellMaster("Задание принято."); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp index a3339ea5c..297583e20 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AreaTriggerAction.cpp @@ -36,7 +36,7 @@ bool ReachAreaTriggerAction::Execute(Event event) if (bot->GetMapId() != at->map) { - botAI->TellError("I won't follow: too far away"); + botAI->TellError("Я не последую за вами: вы слишком далеко"); return true; } @@ -51,7 +51,7 @@ bool ReachAreaTriggerAction::Execute(Event event) float distance = bot->GetDistance(at->x, at->y, at->z); float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig.reactDelay; - botAI->TellError("Wait for me"); + botAI->TellError("Подождите меня"); botAI->SetNextCheckDelay(delay); context->GetValue("last area trigger")->Get().lastAreaTrigger = triggerId; @@ -76,6 +76,6 @@ bool AreaTriggerAction::Execute(Event /*event*/) p.rpos(0); bot->GetSession()->HandleAreaTriggerOpcode(p); - botAI->TellMaster("Hello"); + botAI->TellMaster("Здравствуйте."); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp index 82672cd6d..858c76a2d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ArenaTeamActions.cpp @@ -31,7 +31,7 @@ bool ArenaTeamAcceptAction::Execute(Event event) if (bot->GetArenaTeamId(at->GetSlot())) { // bot is already in an arena team - bot->Say("Sorry, I am already in such team", LANG_UNIVERSAL); + bot->Say("У меня уже есть такая команда", LANG_UNIVERSAL); accept = false; } @@ -39,7 +39,7 @@ bool ArenaTeamAcceptAction::Execute(Event event) { WorldPacket data(CMSG_ARENA_TEAM_ACCEPT); bot->GetSession()->HandleArenaTeamAcceptOpcode(data); - bot->Say("Thanks for the invite!", LANG_UNIVERSAL); + bot->Say("Спасибо за приглашение!", LANG_UNIVERSAL); LOG_INFO("playerbots", "Bot {} <{}> accepts Arena Team invite", bot->GetGUID().ToString().c_str(), bot->GetName().c_str()); return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp index 87bbd31c6..1a4322faf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AttackAction.cpp @@ -37,7 +37,7 @@ bool AttackMyTargetAction::Execute(Event /*event*/) if (!guid) { if (verbose) - botAI->TellError("You have no target"); + botAI->TellError("У вас нет цели"); return false; } @@ -63,7 +63,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) bot->HasUnitState(UNIT_STATE_IN_FLIGHT)) { if (verbose) - botAI->TellError("I cannot attack in flight"); + botAI->TellError("Я не могу атаковать в полёте"); return false; } @@ -71,7 +71,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (!target) { if (verbose) - botAI->TellError("I have no target"); + botAI->TellError("У меня нет цели"); return false; } @@ -91,7 +91,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) sPlayerbotAIConfig.IsPvpProhibited(target->GetZoneId(), target->GetAreaId()))) { if (verbose) - botAI->TellError("I cannot attack other players in PvP prohibited areas."); + botAI->TellError("Я не могу атаковать игроков в зонах, где запрещено PvP."); return false; } @@ -123,7 +123,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (sameTarget && inCombat && sameAttackMode) { if (verbose) - botAI->TellError("I am already attacking " + std::string(target->GetName()) + "."); + botAI->TellError("Я уже атакую: " + std::string(target->GetName()) + "."); return false; } @@ -131,7 +131,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/) if (!bot->IsValidAttackTarget(target)) { if (verbose) - botAI->TellError("I cannot attack an invalid target."); + botAI->TellError("Я не могу атаковать недопустимую цель."); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp index 72433c15f..313fd7fbe 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/AutoMaintenanceOnLevelupAction.cpp @@ -54,7 +54,7 @@ void AutoMaintenanceOnLevelupAction::AutoLearnSpell() { std::string const temp = out.str(); out.seekp(0); - out << "Learned spells: "; + out << "Изученные заклинания: "; out << temp; out.seekp(-2, out.cur); out << "."; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp index 4d8d6c4d8..eda06e02d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BankAction.cpp @@ -23,7 +23,7 @@ bool BankAction::Execute(Event event) return ExecuteBank(text, npc); } - botAI->TellError("Cannot find banker nearby"); + botAI->TellError("Поблизости нет банкира"); return false; } @@ -82,7 +82,7 @@ bool BankAction::Withdraw(uint32 itemid) bot->StoreItem(dest, pItem, true); std::ostringstream out; - out << "got " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " from bank"; + out << "получено: " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " из банка"; botAI->TellMaster(out.str()); return true; } @@ -102,14 +102,14 @@ bool BankAction::Deposit(Item* pItem) bot->RemoveItem(pItem->GetBagSlot(), pItem->GetSlot(), true); bot->BankItem(dest, pItem, true); - out << "put " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " to bank"; + out << "помещено: " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " в банк"; botAI->TellMaster(out.str()); return true; } void BankAction::ListItems() { - botAI->TellMaster("=== Bank ==="); + botAI->TellMaster("=== Банк ==="); std::map items; std::map soulbound; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp index 78c156280..3f05a2f76 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BattleGroundTactics.cpp @@ -1278,13 +1278,13 @@ bool BGTactics::HandleConsoleCommand(ChatHandler* handler, char const* args) { if (!sPlayerbotAIConfig.enabled) { - handler->PSendSysMessage("|cffff0000Playerbot system is currently disabled!"); + handler->PSendSysMessage("|cffff0000Система Playerbot сейчас отключена!"); return true; } WorldSession* session = handler->GetSession(); if (!session) { - handler->PSendSysMessage("Command can only be used from an active session"); + handler->PSendSysMessage("Команду можно использовать только из активной игровой сессии"); return true; } std::string const commandOutput = HandleConsoleCommandPrivate(session, args); @@ -4304,7 +4304,7 @@ bool ArenaTactics::Execute(Event /*event*/) float x, y, z; target->GetPosition(x, y, z); - botAI->TellMasterNoFacing("Repositioning to exit the LoS target!"); + botAI->TellMasterNoFacing("Меняю позицию, чтобы выйти из поля зрения цели!"); return MoveTo(target->GetMapId(), x + frand(-1, +1), y + frand(-1, +1), z, false, true); } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp index d15dcf65e..732b33cec 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BuffAction.cpp @@ -64,19 +64,19 @@ void BuffAction::TellHeader(uint32 subClass) switch (subClass) { case ITEM_SUBCLASS_ELIXIR: - botAI->TellMaster("--- Elixir ---"); + botAI->TellMaster("--- Эликсиры ---"); return; case ITEM_SUBCLASS_FLASK: - botAI->TellMaster("--- Flask ---"); + botAI->TellMaster("--- Настои ---"); return; case ITEM_SUBCLASS_SCROLL: - botAI->TellMaster("--- Scroll ---"); + botAI->TellMaster("--- Свитки ---"); return; case ITEM_SUBCLASS_FOOD: - botAI->TellMaster("--- Food ---"); + botAI->TellMaster("--- Еда ---"); return; case ITEM_SUBCLASS_ITEM_ENHANCEMENT: - botAI->TellMaster("--- Enchant ---"); + botAI->TellMaster("--- Чары ---"); return; } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp index f0729f251..214dd1989 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/BuyAction.cpp @@ -197,7 +197,7 @@ bool BuyAction::Execute(Event event) if (!result) { std::ostringstream out; - out << "Nobody sells " << ChatHelper::FormatItem(proto) << " nearby"; + out << "Никто не продаёт: " << ChatHelper::FormatItem(proto) << " поблизости"; botAI->TellMaster(out.str()); continue; } @@ -215,7 +215,7 @@ bool BuyAction::Execute(Event event) if (!vendored) { - botAI->TellError("There are no vendors nearby"); + botAI->TellError("Поблизости нет торговцев"); return false; } @@ -248,7 +248,7 @@ bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, Ite if (newCount > oldCount) { std::ostringstream out; - out << "Buying " << ChatHelper::FormatItem(proto); + out << "Покупаю: " << ChatHelper::FormatItem(proto); botAI->TellMaster(out.str()); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp index 15c35ee43..2e809c793 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CastCustomSpellAction.cpp @@ -117,7 +117,7 @@ bool CastCustomSpellAction::Execute(Event event) std::ostringstream msg; if (!spell) { - msg << "Unknown spell " << text; + msg << "Неизвестное заклинание: " << text; botAI->TellError(msg.str()); return false; } @@ -125,7 +125,7 @@ bool CastCustomSpellAction::Execute(Event event) SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell); if (!spellInfo) { - msg << "Unknown spell " << text; + msg << "Неизвестное заклинание: " << text; botAI->TellError(msg.str()); return false; } @@ -154,7 +154,7 @@ bool CastCustomSpellAction::Execute(Event event) if (!bot->GetTrader() && !botAI->CanCastSpell(spell, target, true, itemTarget)) { - msg << "Cannot cast " << spellName.str(); + msg << "Не удалось применить: " << spellName.str(); botAI->TellError(msg.str()); return false; } @@ -162,7 +162,7 @@ bool CastCustomSpellAction::Execute(Event event) bool result = spell ? botAI->CastSpell(spell, target, itemTarget) : botAI->CastSpell(text, target, itemTarget); if (result) { - msg << "Casting " << spellName.str(); + msg << "Применяю: " << spellName.str(); if (castCount > 1) { @@ -176,7 +176,7 @@ bool CastCustomSpellAction::Execute(Event event) } else { - msg << "Cast " << spellName.str() << " is failed"; + msg << "Применение " << spellName.str() << " завершилось ошибкой"; botAI->TellError(msg.str()); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp index 04d692c21..cbef636e6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeChatAction.cpp @@ -15,7 +15,7 @@ bool ChangeChatAction::Execute(Event event) if (parsed == CHAT_MSG_SYSTEM) { std::ostringstream out; - out << "Current chat is " << chat->FormatChat(*context->GetValue("chat")); + out << "Текущий канал чата: " << chat->FormatChat(*context->GetValue("chat")); botAI->TellMaster(out); } else @@ -23,7 +23,7 @@ bool ChangeChatAction::Execute(Event event) context->GetValue("chat")->Set(parsed); std::ostringstream out; - out << "Chat set to " << chat->FormatChat(parsed); + out << "Канал чата установлен: " << chat->FormatChat(parsed); botAI->TellMaster(out); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp index b4fe5faaf..48ea3653f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeStrategyAction.cpp @@ -45,7 +45,7 @@ bool ChangeNonCombatStrategyAction::Execute(Event event) { if (text.find("loot") != std::string::npos || text.find("gather") != std::string::npos) { - botAI->TellError("You can change any strategy except loot and gather"); + botAI->TellError("Можно изменять любую стратегию, кроме loot и gather"); return false; } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp index f04db2207..6ffb6edca 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChangeTalentsAction.cpp @@ -38,7 +38,7 @@ bool ChangeTalentsAction::Execute(Event event) if (param.find("switch 1") != std::string::npos) { bot->ActivateSpec(0); - out << "Active first talent"; + out << "Активирован первый набор талантов"; botAI->ResetStrategies(); } else if (param.find("switch 2") != std::string::npos) @@ -49,7 +49,7 @@ bool ChangeTalentsAction::Execute(Event event) bot->CastSpell(bot, 63624, true, nullptr, nullptr, bot->GetGUID()); } bot->ActivateSpec(1); - out << "Active second talent"; + out << "Активирован второй набор талантов"; botAI->ResetStrategies(); } } @@ -57,7 +57,7 @@ bool ChangeTalentsAction::Execute(Event event) { PlayerbotFactory factory(bot, bot->GetLevel()); factory.InitTalentsTree(true); - out << "Auto pick talents"; + out << "Автоматический выбор талантов включён"; botAI->ResetStrategies(); } else if (param.find("spec list") != std::string::npos) @@ -78,13 +78,13 @@ bool ChangeTalentsAction::Execute(Event event) } else { - out << "Unknown command."; + out << "Неизвестная команда."; } } else { uint32 tab = AiFactory::GetPlayerSpecTab(bot); - out << "My current talent spec is: " + out << "Моя текущая специализация: " << "|h|cffffffff"; out << chat->FormatClass(bot, tab) << "\n"; out << TalentsHelp(); @@ -98,7 +98,7 @@ bool ChangeTalentsAction::Execute(Event event) std::string ChangeTalentsAction::TalentsHelp() { std::ostringstream out; - out << "Talents usage: talents switch <1/2>, talents autopick, talents spec list, " + out << "Команды талантов: talents switch <1/2>, talents autopick, talents spec list, " "talents spec , talents apply ."; return out.str(); } @@ -127,7 +127,7 @@ std::string ChangeTalentsAction::SpecList() out << tabCount[0] << "-" << tabCount[1] << "-" << tabCount[2] << ")"; botAI->TellMasterNoFacing(out.str()); } - out << "Total " << specFound << " specs found"; + out << "Найдено: " << specFound << " специализаций"; return out.str(); } @@ -149,12 +149,12 @@ std::string ChangeTalentsAction::SpecPick(std::string param) factory.InitGlyphs(false); std::ostringstream out; - out << "Picking " << sPlayerbotAIConfig.premadeSpecName[cls][specNo]; + out << "Выбираю: " << sPlayerbotAIConfig.premadeSpecName[cls][specNo]; return out.str(); } } std::ostringstream out; - out << "Spec " << param << " not found"; + out << "Специализация " << param << " не найдена"; return out.str(); } @@ -165,11 +165,11 @@ std::string ChangeTalentsAction::SpecApply(std::string param) std::vector> parsedSpecLink = PlayerbotAIConfig::ParseTempTalentsOrder(cls, param); if (parsedSpecLink.size() == 0) { - out << "Invalid link " << param; + out << "Неверная ссылка: " << param; return out.str(); } PlayerbotFactory::InitTalentsByParsedSpecLink(bot, parsedSpecLink, true); - out << "Applying " << param; + out << "Применяю: " << param; return out.str(); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp index 30563e8e6..598489734 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChatShortcutActions.cpp @@ -89,7 +89,7 @@ bool FollowChatShortcutAction::Execute(Event /*event*/) if (moved) { - botAI->TellMaster("Following"); + botAI->TellMaster("Следую за целью."); return true; } } @@ -101,10 +101,10 @@ bool FollowChatShortcutAction::Execute(Event /*event*/) if (bot->isDead()) { bot->ResurrectPlayer(1.0f, false); - botAI->TellMasterNoFacing("Back from the grave!"); + botAI->TellMasterNoFacing("Возвращение из могилы!"); } else - botAI->TellMaster("You are too far away from me! I will there soon."); + botAI->TellMaster("Вы слишком далеко! Я скоро подойду."); bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); bot->TeleportTo(master->GetMapId(), master->GetPositionX(), master->GetPositionY(), master->GetPositionZ(), @@ -112,7 +112,7 @@ bool FollowChatShortcutAction::Execute(Event /*event*/) } */ - botAI->TellMaster("Following"); + botAI->TellMaster("Следую за целью."); return true; } @@ -129,7 +129,7 @@ bool StayChatShortcutAction::Execute(Event /*event*/) SetReturnPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); SetStayPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()); - botAI->TellMaster("Staying"); + botAI->TellMaster("Остаюсь на месте."); return true; } @@ -144,7 +144,7 @@ bool MoveFromGroupChatShortcutAction::Execute(Event /*event*/) botAI->ChangeStrategy("+move from group", BOT_STATE_NON_COMBAT); botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT); - botAI->TellMaster("Moving away from group"); + botAI->TellMaster("Отхожу от группы"); return true; } @@ -163,11 +163,11 @@ bool FleeChatShortcutAction::Execute(Event /*event*/) if (bot->GetMapId() != master->GetMapId() || bot->GetDistance(master) > sPlayerbotAIConfig.sightDistance) { - botAI->TellError("I will not flee with you - too far away"); + botAI->TellError("Я не могу отступать вместе с вами — вы слишком далеко"); return true; } - botAI->TellMaster("Fleeing"); + botAI->TellMaster("Отступаю."); return true; } @@ -184,7 +184,7 @@ bool GoawayChatShortcutAction::Execute(Event /*event*/) ResetReturnPosition(); ResetStayPosition(); - botAI->TellMaster("Running away"); + botAI->TellMaster("Отступаю"); return true; } @@ -200,7 +200,7 @@ bool GrindChatShortcutAction::Execute(Event /*event*/) ResetReturnPosition(); ResetStayPosition(); - botAI->TellMaster("Grinding"); + botAI->TellMaster("Фармлю."); return true; } @@ -220,7 +220,7 @@ bool TankAttackChatShortcutAction::Execute(Event /*event*/) ResetReturnPosition(); ResetStayPosition(); - botAI->TellMaster("Attacking"); + botAI->TellMaster("Атакую."); return true; } @@ -236,7 +236,7 @@ bool MaxDpsChatShortcutAction::Execute(Event /*event*/) botAI->Reset(); botAI->ChangeStrategy("-threat,-conserve mana,-cast time,+dps debuff,+boost", BOT_STATE_COMBAT); - botAI->TellMaster("Max DPS!"); + botAI->TellMaster("Максимальный урон!"); return true; } @@ -250,6 +250,6 @@ bool BwlChatShortcutAction::Execute(Event /*event*/) botAI->Reset(); botAI->ChangeStrategy("+bwl", BOT_STATE_NON_COMBAT); botAI->ChangeStrategy("+bwl", BOT_STATE_COMBAT); - botAI->TellMasterNoFacing("Add Bwl Strategies!"); + botAI->TellMasterNoFacing("Стратегии Логова Крыла Тьмы включены!"); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp index e14d0cf3a..efbcbb2db 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseRpgTargetAction.cpp @@ -258,7 +258,7 @@ bool ChooseRpgTargetAction::Execute(Event /*event*/) if (botAI->HasStrategy("debug", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject()) { std::ostringstream out; - out << "found: "; + out << "найдено: "; out << chat->FormatWorldobject(guidP.GetWorldObject()); out << " " << relevances.front(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp index f7538c03c..7ad7a4033 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTargetActions.cpp @@ -169,7 +169,7 @@ bool AttackRtiTargetAction::Execute(Event /*event*/) } } else - botAI->TellError("I dont see my rti attack target"); + botAI->TellError("Я не вижу назначенную групповую цель для атаки"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp index 695f0d311..52f96b0f8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ChooseTravelTargetAction.cpp @@ -244,15 +244,15 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave std::string Sub; if (newTarget->isGroupCopy()) - out << "Following group "; + out << "Следую за группой: "; else if (oldDestination && oldDestination == destination) - out << "Continuing "; + out << "Продолжаю: "; else - out << "Traveling "; + out << "Путешествую: "; out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y"; - out << " for " << chat->FormatQuest(quest); + out << " за " << chat->FormatQuest(quest); out << " to " << QuestDestination->getTitle(); @@ -265,20 +265,20 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave WorldPosition botLocation(bot); if (newTarget->isGroupCopy()) - out << "Following group "; + out << "Следую за группой: "; else if (oldDestination && oldDestination == destination) - out << "Continuing "; + out << "Продолжаю: "; else - out << "Traveling "; + out << "Путешествую: "; out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y"; - out << " for "; + out << " за "; if (AI_VALUE2(bool, "group or", "should sell,can sell")) - out << "selling items"; + out << "продаю предметы"; else if (AI_VALUE2(bool, "group or", "should repair,can repair")) - out << "repairing"; + out << "ремонтируюсь"; else out << "rpg"; @@ -293,11 +293,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave WorldPosition botLocation(bot); if (newTarget->isGroupCopy()) - out << "Following group "; + out << "Следую за группой: "; else if (oldDestination && oldDestination == destination) - out << "Continuing "; + out << "Продолжаю: "; else - out << "Traveling "; + out << "Путешествую: "; out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y"; @@ -314,11 +314,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave WorldPosition botLocation(bot); if (newTarget->isGroupCopy()) - out << "Following group "; + out << "Следую за группой: "; else if (oldDestination && oldDestination == destination) - out << "Continuing "; + out << "Продолжаю: "; else - out << "Traveling "; + out << "Путешествую: "; out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y"; @@ -335,11 +335,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave WorldPosition botLocation(bot); if (newTarget->isGroupCopy()) - out << "Following group "; + out << "Следую за группой: "; else if (oldDestination && oldDestination == destination) - out << "Continuing "; + out << "Продолжаю: "; else - out << "Traveling "; + out << "Путешествую: "; out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y"; @@ -353,7 +353,7 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave { if (!oldTarget->getDestination() || oldTarget->getDestination()->getName() != "NullTravelDestination") { - botAI->TellMaster("No where to travel. Idling a bit."); + botAI->TellMaster("Некуда идти. Немного подожду."); } } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp index a8e83e87c..12db7c791 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/CustomStrategyEditAction.cpp @@ -31,7 +31,7 @@ bool CustomStrategyEditAction::Execute(Event event) bool CustomStrategyEditAction::PrintHelp() { - botAI->TellMaster("=== Custom strategies ==="); + botAI->TellMaster("=== Пользовательские стратегии ==="); uint32 owner = botAI->GetBot()->GetGUID().GetCounter(); @@ -48,7 +48,7 @@ bool CustomStrategyEditAction::PrintHelp() } while (result->NextRow()); } - botAI->TellMaster("Usage: cs "); + botAI->TellMaster("Использование: cs <имя> <индекс> <команда>"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp index 378cdab79..eeafe2cf0 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DebugAction.cpp @@ -79,7 +79,7 @@ bool DebugAction::Execute(Event event) TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot); std::ostringstream out; - out << "Traveling to " << dest->getTitle() << ": "; + out << "Направляюсь к: " << dest->getTitle() << ": "; for (auto node : route.getNodes()) { @@ -92,7 +92,7 @@ bool DebugAction::Execute(Event event) } else { - botAI->TellMasterNoFacing("Destination " + destination + " not found."); + botAI->TellMasterNoFacing("Точка назначения " + destination + " not found."); return true; } } @@ -104,7 +104,7 @@ bool DebugAction::Execute(Event event) if (!quest) { - botAI->TellMasterNoFacing("Quest " + text.substr(6) + " not found."); + botAI->TellMasterNoFacing("Задание " + text.substr(6) + " not found."); return false; } @@ -164,7 +164,7 @@ bool DebugAction::Execute(Event event) else if (text.find("bquest") != std::string::npos) { std::ostringstream out; - out << "bad quests:"; + out << "ошибочные задания:"; // uint32 noT = 0, noG = 0, noO = 0; //not used, line marked for removal. @@ -205,7 +205,7 @@ bool DebugAction::Execute(Event event) endNode->setLinked(false); } - botAI->TellMasterNoFacing("Node " + name + " created."); + botAI->TellMasterNoFacing("Узел " + name + " created."); TravelNodeMap::instance().setHasToGen(); @@ -222,12 +222,12 @@ bool DebugAction::Execute(Event event) if (startNode->isImportant()) { - botAI->TellMasterNoFacing("Node can not be removed."); + botAI->TellMasterNoFacing("Не удалось удалить узел."); } TravelNodeMap::instance().m_nMapMtx.lock(); TravelNodeMap::instance().removeNode(startNode); - botAI->TellMasterNoFacing("Node removed."); + botAI->TellMasterNoFacing("Узел удалён."); TravelNodeMap::instance().m_nMapMtx.unlock(); TravelNodeMap::instance().setHasToGen(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp index b3cba9c56..5dece3336 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/DropQuestAction.cpp @@ -51,10 +51,10 @@ bool DropQuestAction::Execute(Event event) const Quest* pQuest = sObjectMgr->GetQuestTemplate(entry); const std::string text_quest = ChatHelper::FormatQuest(pQuest); LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), pQuest->GetTitle()); - bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL); + bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL); } - botAI->TellMaster("Quest removed"); + botAI->TellMaster("Задание удалено."); return true; } @@ -63,7 +63,7 @@ bool CleanQuestLogAction::Execute(Event event) Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); if (!requester) { - botAI->TellMaster("No event owner detected"); + botAI->TellMaster("Не удалось определить владельца события"); return false; } @@ -75,7 +75,7 @@ bool CleanQuestLogAction::Execute(Event event) // Only output this message if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { - botAI->TellMaster("Clean Quest Log command received, removing grey/trivial quests..."); + botAI->TellMaster("Получена команда очистки журнала: удаляю серые и незначительные задания..."); } uint8 botLevel = bot->GetLevel(); // Get bot's level @@ -127,7 +127,7 @@ bool CleanQuestLogAction::Execute(Event event) // Output only if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { - botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] will be removed because it is trivial (grey)."); + botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] will be removed because it is trivial (grey)."); } // Remove quest @@ -143,12 +143,12 @@ bool CleanQuestLogAction::Execute(Event event) { const std::string text_quest = ChatHelper::FormatQuest(quest); LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle()); - bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL); + bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL); } if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { - botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] has been removed."); + botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] has been removed."); } } else @@ -156,7 +156,7 @@ bool CleanQuestLogAction::Execute(Event event) // Only output if "debug rpg" strategy is enabled if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { - botAI->TellMaster("Quest [ " + quest->GetTitle() + " ] is not trivial and will be kept."); + botAI->TellMaster("Задание [ " + quest->GetTitle() + " ] is not trivial and will be kept."); } } } @@ -234,9 +234,9 @@ void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isG { const std::string text_quest = ChatHelper::FormatQuest(quest); LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle()); - bot->Say("Quest [ " + text_quest + " ] removed", LANG_UNIVERSAL); + bot->Say("Задание [ " + text_quest + " ] removed", LANG_UNIVERSAL); } - botAI->TellMaster("Quest removed" + chat->FormatQuest(quest)); + botAI->TellMaster("Задание удалено." + chat->FormatQuest(quest)); } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp index dd6957cc8..e7e573545 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EmoteAction.cpp @@ -163,7 +163,7 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal) if (botAI->GetMaster() == source) { botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT); - botAI->TellMasterNoFacing("Fine.. I'll stay right here.."); + botAI->TellMasterNoFacing("Хорошо... Останусь здесь..."); } break; case TEXT_EMOTE_BECKON: @@ -171,7 +171,7 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal) if (botAI->GetMaster() == source) { botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT); - botAI->TellMasterNoFacing("Wherever you go, I'll follow.."); + botAI->TellMasterNoFacing("Куда бы вы ни пошли, я последую за вами..."); } break; case TEXT_EMOTE_WAVE: @@ -579,36 +579,36 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal) break; /*case TEXT_EMOTE_BADFEELING: bot->HandleEmoteCommand(EMOTE_ONESHOT_QUESTION); - bot->Say("I'm just waiting for the ominous music now...", LANG_UNIVERSAL); + bot->Say("Теперь осталось дождаться зловещей музыки...", LANG_UNIVERSAL); break; case TEXT_EMOTE_MAP: bot->HandleEmoteCommand(EMOTE_ONESHOT_NO); - bot->Say("Noooooooo.. you just couldn't ask for directions, huh?", LANG_UNIVERSAL); + bot->Say("Неееет... Нельзя было просто спросить дорогу?", LANG_UNIVERSAL); break; case TEXT_EMOTE_IDEA: case TEXT_EMOTE_THINK: bot->HandleEmoteCommand(EMOTE_ONESHOT_NO); - bot->Say("Oh boy.. another genius idea...", LANG_UNIVERSAL); + bot->Say("Ох... Ещё одна гениальная идея...", LANG_UNIVERSAL); break; case TEXT_EMOTE_OFFER: bot->HandleEmoteCommand(EMOTE_ONESHOT_NO); - bot->Say("No thanks.. I had some back at the last village", LANG_UNIVERSAL); + bot->Say("Нет, спасибо... В прошлой деревне уже хватило", LANG_UNIVERSAL); break; case TEXT_EMOTE_PET: bot->HandleEmoteCommand(EMOTE_ONESHOT_ROAR); - bot->Say("Do I look like a dog to you?!", LANG_UNIVERSAL); + bot->Say("Я похож на собаку?!", LANG_UNIVERSAL); break; case TEXT_EMOTE_ROLLEYES: bot->HandleEmoteCommand(EMOTE_ONESHOT_POINT); - bot->Say("Keep doing that and I'll roll those eyes right out of your head..", LANG_UNIVERSAL); + bot->Say("Продолжайте — и глаза скоро закатятся навсегда...", LANG_UNIVERSAL); break; case TEXT_EMOTE_SING: bot->HandleEmoteCommand(EMOTE_ONESHOT_APPLAUD); - bot->Say("Lovely... just lovely..", LANG_UNIVERSAL); + bot->Say("Прекрасно... Просто прекрасно...", LANG_UNIVERSAL); break; case TEXT_EMOTE_COVEREARS: bot->HandleEmoteCommand(EMOTE_ONESHOT_EXCLAMATION); - bot->Yell("You think that's going to help you?!", LANG_UNIVERSAL); + bot->Yell("Думаете, это вам поможет?!", LANG_UNIVERSAL); break;*/ default: // return false; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp index d6900391f..343377c4d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EquipAction.cpp @@ -75,7 +75,7 @@ void EquipAction::EquipItem(Item* item) { bot->SetAmmo(itemId); std::ostringstream out; - out << "equipping " << chat->FormatItem(itemProto); + out << "Надеваю: " << chat->FormatItem(itemProto); botAI->TellMaster(out); return; } @@ -112,7 +112,7 @@ void EquipAction::EquipItem(Item* item) bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket); std::ostringstream out; - out << "Equipping " << chat->FormatItem(itemProto) << " in ranged slot"; + out << "Надеваю: " << chat->FormatItem(itemProto) << " в ячейку оружия дальнего боя"; botAI->TellMaster(out); return; } @@ -227,7 +227,7 @@ void EquipAction::EquipItem(Item* item) } std::ostringstream out; - out << "Equipping " << chat->FormatItem(itemProto) << " in main hand"; + out << "Надеваю: " << chat->FormatItem(itemProto) << " в правую руку"; botAI->TellMaster(out); return; } @@ -244,7 +244,7 @@ void EquipAction::EquipItem(Item* item) bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket); std::ostringstream out; - out << "Equipping " << chat->FormatItem(itemProto) << " in offhand"; + out << "Надеваю: " << chat->FormatItem(itemProto) << " в левую руку"; botAI->TellMaster(out); return; } @@ -324,7 +324,7 @@ void EquipAction::EquipItem(Item* item) } std::ostringstream out; - out << "Equipping " << chat->FormatItem(itemProto); + out << "Надеваю: " << chat->FormatItem(itemProto); botAI->TellMaster(out); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp index 05c064b96..cd2d7ca73 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/EquipGlyphsAction.cpp @@ -106,7 +106,7 @@ bool EquipGlyphsAction::Execute(Event event) std::vector glyphs; if (!CollectGlyphs(itemIds, glyphs)) { - botAI->TellMaster("Usage: glyph equip <6 glyph item IDs> (3 major, 3 minor)."); + botAI->TellMaster("Использование: glyph equip <6 ID символов> (3 больших и 3 малых)."); return false; } @@ -144,12 +144,12 @@ bool EquipGlyphsAction::Execute(Event event) if (!placed) { - botAI->TellMaster("Not enought empty sockets for all glyphs."); + botAI->TellMaster("Недостаточно свободных ячеек для всех символов."); return false; } } - botAI->TellMaster("Glyphs updated."); + botAI->TellMaster("Символы обновлены."); // Flag for custom glyphs botAI->GetAiObjectContext()->GetValue("custom_glyphs")->Set(true); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp index 023b376d6..78e5529b4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FlagAction.cpp @@ -10,7 +10,7 @@ bool FlagAction::TellUsage() { - botAI->TellError("Usage: flag cloak/helm/pvp on/set/off/clear/toggle/?"); + botAI->TellError("Использование: flag cloak/helm/pvp on/set/off/clear/toggle/?"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp index c27de01ff..6a74d3ca3 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/FollowActions.cpp @@ -130,15 +130,15 @@ bool FleeToGroupLeaderAction::Execute(Event /*event*/) if (distance < sPlayerbotAIConfig.reactDistance * 3) { if (!urand(0, 3)) - botAI->TellMaster("I am close, wait for me!"); + botAI->TellMaster("Я рядом, подождите меня!"); } else if (distance < 1000) { if (!urand(0, 10)) - botAI->TellMaster("I heading to your position."); + botAI->TellMaster("Я направляюсь к вам."); } else if (!urand(0, 20)) - botAI->TellMaster("I am traveling to your position."); + botAI->TellMaster("Я иду к вам."); botAI->SetNextCheckDelay(3000); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp index 35fa51b31..40a753ff5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GenericActions.cpp @@ -112,7 +112,7 @@ bool TogglePetSpellAutoCastAction::Execute(Event /*event*/) // Debug message if pet spells have been toggled and debug is enabled if (toggled && sPlayerbotAIConfig.petChatCommandDebug == 1) - botAI->TellMaster("Pet autocast spells have been toggled."); + botAI->TellMaster("Автоматическое применение способностей питомца переключено."); return toggled; } @@ -178,7 +178,7 @@ bool SetPetStanceAction::Execute(Event /*event*/) // If there are no controlled pets or guardians, notify the player and exit if (targets.empty()) { - botAI->TellError("You have no pet or guardian pet."); + botAI->TellError("У вас нет питомца или стража."); return false; } @@ -220,7 +220,7 @@ bool SetPetStanceAction::Execute(Event /*event*/) // If debug is enabled in config, inform the master of the new stance if (sPlayerbotAIConfig.petChatCommandDebug == 1) - botAI->TellMaster("Pet stance set to " + stanceText + " (applied to all pets/guardians)."); + botAI->TellMaster("Режим питомца: " + stanceText + " (applied to all pets/guardians)."); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp index 6019d1e38..363aca7a8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GiveItemAction.cpp @@ -45,13 +45,13 @@ bool GiveItemAction::Execute(Event /*event*/) moved = true; std::ostringstream out; - out << "Got " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " from " << bot->GetName(); + out << "Получено: " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " от " << bot->GetName(); receiverAi->TellMasterNoFacing(out.str()); } else { std::ostringstream out; - out << "Cannot get " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " from " + out << "Не удалось получить: " << chat->FormatItem(item->GetTemplate(), item->GetCount()) << " от " << bot->GetName() << "- my bags are full"; receiverAi->TellError(out.str()); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp index 61a7550c2..df2043b9f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GoAction.cpp @@ -30,7 +30,7 @@ bool GoAction::Execute(Event event) Map2ZoneCoordinates(x, y, bot->GetZoneId()); std::ostringstream out; - out << "I am at " << x << "," << y; + out << "Я нахожусь в точке " << x << "," << y; botAI->TellMaster(out.str()); return true; } @@ -53,14 +53,14 @@ bool GoAction::Execute(Event event) target->setForced(true); std::ostringstream out; - out << "Traveling to " << dest->getTitle(); + out << "Направляюсь к: " << dest->getTitle(); botAI->TellMasterNoFacing(out.str()); return true; } else { - botAI->TellMasterNoFacing("Clearing travel target"); + botAI->TellMasterNoFacing("Цель путешествия сброшена"); target->setTarget(TravelMgr::instance().nullTravelDestination, TravelMgr::instance().nullWorldPosition); target->setForced(false); return true; @@ -78,12 +78,12 @@ bool GoAction::Execute(Event event) if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, go), sPlayerbotAIConfig.reactDistance)) { - botAI->TellError("It is too far away"); + botAI->TellError("Слишком далеко."); return false; } std::ostringstream out; - out << "Moving to " << ChatHelper::FormatGameobject(go); + out << "Двигаюсь к: " << ChatHelper::FormatGameobject(go); botAI->TellMasterNoFacing(out.str()); return MoveNear(bot->GetMapId(), go->GetPositionX(), go->GetPositionY(), go->GetPositionZ() + 0.5f, sPlayerbotAIConfig.followDistance); @@ -103,7 +103,7 @@ bool GoAction::Execute(Event event) if (strstri(unit->GetName().c_str(), param.c_str())) { std::ostringstream out; - out << "Moving to " << unit->GetName(); + out << "Двигаюсь к: " << unit->GetName(); botAI->TellMasterNoFacing(out.str()); return MoveNear(bot->GetMapId(), unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ() + 0.5f, sPlayerbotAIConfig.followDistance); @@ -137,7 +137,7 @@ bool GoAction::Execute(Event event) out << x << ";" << y << ";" << z << " ="; - out << "path is: "; + out << "путь: "; out << type; @@ -179,20 +179,20 @@ bool GoAction::Execute(Event event) if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, x, y), sPlayerbotAIConfig.reactDistance)) { - botAI->TellMaster("It is too far away"); + botAI->TellMaster("Слишком далеко."); return false; } if (map->IsInWater(bot->GetPhaseMask(), x, y, z, bot->GetCollisionHeight())) { - botAI->TellError("It is in water"); + botAI->TellError("Цель находится в воде"); return false; } float ground = map->GetHeight(x, y, z + 0.5f); if (ground <= INVALID_HEIGHT) { - botAI->TellError("I can't go there"); + botAI->TellError("Я не могу туда пройти"); return false; } @@ -200,7 +200,7 @@ bool GoAction::Execute(Event event) Map2ZoneCoordinates(x1, y1, bot->GetZoneId()); std::ostringstream out; - out << "Moving to " << x1 << "," << y1; + out << "Двигаюсь к: " << x1 << "," << y1; botAI->TellMasterNoFacing(out.str()); return MoveNear(bot->GetMapId(), x, y, z + 0.5f, sPlayerbotAIConfig.followDistance); @@ -212,16 +212,16 @@ bool GoAction::Execute(Event event) if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, pos.x, pos.y), sPlayerbotAIConfig.reactDistance)) { - botAI->TellError("It is too far away"); + botAI->TellError("Слишком далеко."); return false; } std::ostringstream out; - out << "Moving to position " << param; + out << "Двигаюсь к позиции: " << param; botAI->TellMasterNoFacing(out.str()); return MoveNear(bot->GetMapId(), pos.x, pos.y, pos.z + 0.5f, sPlayerbotAIConfig.followDistance); } - botAI->TellMaster("Whisper 'go x,y', 'go [game object]', 'go unit' or 'go position' and I will go there"); + botAI->TellMaster("Напишите «go x,y», «go [объект]», «go unit» или «go position», и я отправлюсь туда"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp index 9c5083d80..8e19dd01b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GossipHelloAction.cpp @@ -83,7 +83,7 @@ bool GossipHelloAction::ProcessGossip(int32 menuToSelect, bool silent) if (menuToSelect != -1 && !menu.GetItem(menuToSelect)) { if (!silent) - botAI->TellError("Unknown gossip option"); + botAI->TellError("Неизвестный вариант диалога"); return false; } @@ -138,7 +138,7 @@ bool GossipHelloAction::Execute(ObjectGuid guid, int32 menuToSelect, bool silent else if (!bot->PlayerTalkClass) { if (!silent) - botAI->TellError("I need to talk first"); + botAI->TellError("Сначала нужно поговорить"); return false; } else diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp index cd7635a97..18cc0139a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildAcceptAction.cpp @@ -28,17 +28,17 @@ bool GuildAcceptAction::Execute(Event event) uint32 guildId = inviter->GetGuildId(); if (!guildId) { - botAI->TellError("You are not in a guild!"); + botAI->TellError("Вы не состоите в гильдии!"); accept = false; } else if (bot->GetGuildId()) { - botAI->TellError("Sorry, I am in a guild already"); + botAI->TellError("Я уже состою в гильдии"); accept = false; } else if (!botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, inviter, true)) { - botAI->TellError("Sorry, I don't want to join your guild :("); + botAI->TellError("Я не хочу вступать в вашу гильдию :("); accept = false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp index 9693556d7..eeea973ec 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildBankAction.cpp @@ -17,7 +17,7 @@ bool GuildBankAction::Execute(Event event) if (!bot->GetGuildId() || (GetMaster() && GetMaster()->GetGuildId() != bot->GetGuildId())) { - botAI->TellMaster("I'm not in your guild!"); + botAI->TellMaster("Я не состою в вашей гильдии!"); return false; } @@ -31,7 +31,7 @@ bool GuildBankAction::Execute(Event event) return Execute(text, go); } - botAI->TellMaster("Cannot find the guild bank nearby"); + botAI->TellMaster("Поблизости нет банка гильдии"); return false; } @@ -65,11 +65,11 @@ bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* bank) // check source pos rights (item moved to bank) if (!guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM)) - out << "I can't put " << chat->FormatItem(item->GetTemplate()) + out << "Я не могу положить: " << chat->FormatItem(item->GetTemplate()) << " to guild bank. I have no rights to put items in the first guild bank tab"; else { - out << chat->FormatItem(item->GetTemplate()) << " put to guild bank"; + out << chat->FormatItem(item->GetTemplate()) << " положено в банк гильдии"; guild->SwapItemsWithInventory(bot, false, 0, 255, playerBag, playerSlot, 0); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp index 0b5a7cf48..2f021e5b7 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/GuildManagementActions.cpp @@ -298,7 +298,7 @@ bool GuildLeaveAction::Execute(Event event) Player* owner = event.getOwner(); if (owner && !botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, owner, true)) { - botAI->TellError("Sorry, I am happy in my guild :)"); + botAI->TellError("Меня устраивает моя гильдия :)"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp index c3d14a0ff..0fbfa3411 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/HelpAction.cpp @@ -23,7 +23,7 @@ bool HelpAction::Execute(Event /*event*/) void HelpAction::TellChatCommands() { std::ostringstream out; - out << "Whisper any of: "; + out << "Напишите одну из команд: "; out << CombineSupported(chatContext->supports()); out << ", [item], [quest] or [object] link"; botAI->TellError(out.str()); @@ -32,7 +32,7 @@ void HelpAction::TellChatCommands() void HelpAction::TellStrategies() { std::ostringstream out; - out << "Possible strategies (co/nc/dead commands): "; + out << "Доступные стратегии (команды co/nc/dead): "; out << CombineSupported(botAI->GetAiObjectContext()->GetSupportedStrategies()); botAI->TellError(out.str()); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp index 374438f74..2cd47d8e5 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/HireAction.cpp @@ -30,13 +30,13 @@ bool HireAction::Execute(Event /*event*/) if (charCount >= 10) { - botAI->TellMaster("You already have the maximum number of characters"); + botAI->TellMaster("У вас уже максимальное количество персонажей"); return false; } if (bot->GetLevel() > master->GetLevel()) { - botAI->TellMaster("You cannot hire higher level characters than you"); + botAI->TellMaster("Нельзя нанять персонажа выше вашего уровня"); return false; } @@ -46,13 +46,13 @@ bool HireAction::Execute(Event /*event*/) if (discount < moneyReq) { std::ostringstream out; - out << "You cannot hire me - I barely know you. Make sure you have at least " << chat->formatMoney(moneyReq) + out << "Вы не можете меня нанять: мы почти не знакомы. Требуется не менее " << chat->formatMoney(moneyReq) << " as a trade discount"; botAI->TellMaster(out.str()); return false; } - botAI->TellMaster("I will join you at your next relogin"); + botAI->TellMaster("Я присоединюсь после вашего следующего входа в игру"); bot->SetMoney(moneyReq); RandomPlayerbotMgr::instance().Remove(bot); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp index 83fc00f12..247dc5117 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InventoryAction.cpp @@ -130,40 +130,40 @@ void InventoryAction::TellItems(std::map itemMap, std::mapClass) { case ITEM_CLASS_CONSUMABLE: - botAI->TellMaster("--- consumable ---"); + botAI->TellMaster("--- Расходуемые предметы ---"); break; case ITEM_CLASS_CONTAINER: - botAI->TellMaster("--- container ---"); + botAI->TellMaster("--- Сумки ---"); break; case ITEM_CLASS_WEAPON: - botAI->TellMaster("--- weapon ---"); + botAI->TellMaster("--- Оружие ---"); break; case ITEM_CLASS_ARMOR: - botAI->TellMaster("--- armor ---"); + botAI->TellMaster("--- Доспехи ---"); break; case ITEM_CLASS_REAGENT: - botAI->TellMaster("--- reagent ---"); + botAI->TellMaster("--- Реагенты ---"); break; case ITEM_CLASS_PROJECTILE: - botAI->TellMaster("--- projectile ---"); + botAI->TellMaster("--- Боеприпасы ---"); break; case ITEM_CLASS_TRADE_GOODS: - botAI->TellMaster("--- trade goods ---"); + botAI->TellMaster("--- Хозяйственные товары ---"); break; case ITEM_CLASS_RECIPE: - botAI->TellMaster("--- recipe ---"); + botAI->TellMaster("--- Рецепты ---"); break; case ITEM_CLASS_QUIVER: - botAI->TellMaster("--- quiver ---"); + botAI->TellMaster("--- Колчаны ---"); break; case ITEM_CLASS_QUEST: - botAI->TellMaster("--- quest items ---"); + botAI->TellMaster("--- Предметы заданий ---"); break; case ITEM_CLASS_KEY: - botAI->TellMaster("--- keys ---"); + botAI->TellMaster("--- Ключи ---"); break; case ITEM_CLASS_MISC: - botAI->TellMaster("--- other ---"); + botAI->TellMaster("--- Прочее ---"); break; } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp index 136f13958..027243752 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/InviteToGroupAction.cpp @@ -438,7 +438,7 @@ bool LfgAction::Execute(Event event) std::ostringstream out; if (allowedRoles[role] > 1) { - out << "Joining as " << placeholders["%role"] << ", " << placeholders["%spotsleft"] << " " + out << "Присоединяюсь в роли: " << placeholders["%role"] << ", " << placeholders["%spotsleft"] << " " << placeholders["%role"] << " spots left."; botAI->TellMasterNoFacing(out.str()); @@ -447,7 +447,7 @@ bool LfgAction::Execute(Event event) } else { - out << "Joining as " << placeholders["%role"] << "."; + out << "Присоединяюсь в роли: " << placeholders["%role"] << "."; botAI->TellMasterNoFacing(out.str()); //botAI->DoSpecificAction("autogear"); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp index 337d11544..38193b310 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LeaveGroupAction.cpp @@ -86,7 +86,7 @@ bool LeaveGroupAction::Leave() Player* master = botAI -> GetMaster(); if (master) - botAI->TellMaster("Goodbye!", PLAYERBOT_SECURITY_TALK); + botAI->TellMaster("До свидания!", PLAYERBOT_SECURITY_TALK); botAI->LeaveOrDisbandGroup(); return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp index 0071b113f..341603c19 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ListQuestsActions.cpp @@ -44,19 +44,19 @@ void ListQuestsAction::ListQuests(QuestListFilter filter, QuestTravelDetail trav bool showCompleted = filter & QUEST_LIST_FILTER_COMPLETED; if (showIncompleted) - botAI->TellMaster("--- Incompleted quests ---"); + botAI->TellMaster("--- Невыполненные задания ---"); uint32 incompleteCount = ListQuests(false, !showIncompleted, travelDetail); if (showCompleted) - botAI->TellMaster("--- Completed quests ---"); + botAI->TellMaster("--- Выполненные задания ---"); uint32 completeCount = ListQuests(true, !showCompleted, travelDetail); - botAI->TellMaster("--- Summary ---"); + botAI->TellMaster("--- Итого ---"); std::ostringstream out; - out << "Total: " << (completeCount + incompleteCount) << " / 25 (incompleted: " << incompleteCount + out << "Всего: " << (completeCount + incompleteCount) << " / 25 (невыполнено: " << incompleteCount << ", completed: " << completeCount << ")"; botAI->TellMaster(out); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp index 720e2f983..afa9ed0cb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ListSpellsAction.cpp @@ -284,11 +284,11 @@ bool ListSpellsAction::Execute(Event event) if (spells.empty()) { // CHANGE: Give early feedback when no spells match the filter. - botAI->TellMaster("No spells found."); + botAI->TellMaster("Заклинания не найдены."); return true; } - botAI->TellMaster("=== Spells ==="); + botAI->TellMaster("=== Заклинания ==="); std::sort(spells.begin(), spells.end(), CompareSpells); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp index cd8b5c28d..bd529379b 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LogLevelAction.cpp @@ -17,11 +17,11 @@ bool LogLevelAction::Execute(Event event) if (param != "?") { value->Set(string2logLevel(param)); - out << "My log level set to " << logLevel2string(value->Get()); + out << "Уровень журнала установлен: " << logLevel2string(value->Get()); } else { - out << "My log level is " << logLevel2string(value->Get()); + out << "Текущий уровень журнала: " << logLevel2string(value->Get()); } botAI->TellMaster(out); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp index 68f8089c2..6239f15a2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/LootStrategyAction.cpp @@ -24,14 +24,14 @@ bool LootStrategyAction::Execute(Event event) { { std::ostringstream out; - out << "Loot strategy: "; + out << "Стратегия добычи: "; out << lootStrategy->Get()->GetName(); botAI->TellMaster(out); } { std::ostringstream out; - out << "Always loot items: "; + out << "Всегда собирать: "; for (uint32 itemId : alwaysLootItems) { @@ -54,7 +54,7 @@ bool LootStrategyAction::Execute(Event event) lootStrategy->Set(LootStrategyValue::instance(strategy)); std::ostringstream out; - out << "Loot strategy set to " << lootStrategy->Get()->GetName(); + out << "Установлена стратегия добычи: " << lootStrategy->Get()->GetName(); botAI->TellMaster(out); return true; } @@ -80,12 +80,12 @@ bool LootStrategyAction::Execute(Event event) if (j != alwaysLootItems.end()) alwaysLootItems.erase(j); - botAI->TellMaster("Item(s) removed from always loot list"); + botAI->TellMaster("Предметы удалены из списка обязательного сбора"); } else { alwaysLootItems.insert(itemid); - botAI->TellMaster("Item(s) added to always loot list"); + botAI->TellMaster("Предметы добавлены в список обязательного сбора"); } } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp index 4ee2fee6d..09cec55af 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MailAction.cpp @@ -17,7 +17,7 @@ class TellMailProcessor : public MailProcessor public: bool Before(PlayerbotAI* botAI) override { - botAI->TellMaster("=== Mailbox ==="); + botAI->TellMaster("=== Почта ==="); tells.clear(); return true; } @@ -83,7 +83,7 @@ public: Player* bot = botAI->GetBot(); if (!CheckBagSpace(bot)) { - botAI->TellError("Not enough bag space"); + botAI->TellError("В сумках недостаточно места"); return false; } @@ -256,7 +256,7 @@ bool MailAction::Execute(Event event) if (!MailProcessor::FindMailbox(botAI)) { - botAI->TellError("There is no mailbox nearby"); + botAI->TellError("Поблизости нет почтового ящика"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp index ad197e4fe..11175d059 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToRpgTargetAction.cpp @@ -33,7 +33,7 @@ bool MoveToRpgTargetAction::Execute(Event /*event*/) if (botAI->HasStrategy("debug rpg", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject()) { std::ostringstream out; - out << "Heading to: "; + out << "Направляюсь к цели: "; out << chat->FormatWorldobject(guidP.GetWorldObject()); botAI->TellMasterNoFacing(out); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp index f238135d9..6bca4931d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MoveToTravelTargetAction.cpp @@ -55,9 +55,9 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/) { std::ostringstream out; if (botAI->GetMaster() && !bot->GetGroup()->IsMember(botAI->GetMaster()->GetGUID())) - out << "Waiting a bit for "; + out << "Немного подожду: "; else - out << "Please hurry up "; + out << "Пожалуйста, поторопитесь: "; out << member->GetName(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp index 2b0359254..73707228c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/MovementActions.cpp @@ -164,7 +164,7 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged) if (dest.isSet()) return MoveTo(dest.mapId, dest.x, dest.y, dest.z); else - botAI->TellError("All paths not in LOS"); + botAI->TellError("Ни один путь не находится в поле зрения"); return false; } @@ -1160,13 +1160,13 @@ bool MovementAction::Follow(Unit* target, float distance, float angle) if (bot->isDead() && botAI->GetMaster()->IsAlive()) { bot->ResurrectPlayer(1.0f, false); - botAI->TellMasterNoFacing("I live, again!"); + botAI->TellMasterNoFacing("Я снова жив!"); } else - botAI->TellError("I am stuck while following"); + botAI->TellError("Я застрял во время следования"); bot->CombatStop(true); - botAI->TellMasterNoFacing("I will there soon."); + botAI->TellMasterNoFacing("Я скоро буду там."); bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP); bot->TeleportTo(target->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(), target->GetOrientation()); return false; @@ -1379,7 +1379,7 @@ bool MovementAction::Flee(Unit* target) if (!IsMovingAllowed()) { - botAI->TellError("I am stuck while fleeing"); + botAI->TellError("Я застрял при отступлении"); return false; } @@ -1535,7 +1535,7 @@ bool MovementAction::Flee(Unit* target) float rx, ry, rz; if (!manager.CalculateDestination(&rx, &ry, &rz)) { - botAI->TellError("Nowhere to flee"); + botAI->TellError("Некуда отступать"); return false; } @@ -1940,7 +1940,7 @@ bool AvoidAoeAction::AvoidAuraWithDynamicObj() lastTellTimer = time(NULL); lastMoveTimer = getMSTime(); std::ostringstream out; - out << "I'm avoiding " << name.str() << " (" << spellInfo->Id << ")" << " Radius " << radius << " - [Aura]"; + out << "Избегаю: " << name.str() << " (" << spellInfo->Id << ")" << "; радиус " << radius << " — [аура]"; bot->Say(out.str(), LANG_UNIVERSAL); } return true; @@ -2008,7 +2008,7 @@ bool AvoidAoeAction::AvoidGameObjectWithDamage() lastTellTimer = time(NULL); lastMoveTimer = getMSTime(); std::ostringstream out; - out << "I'm avoiding " << name.str() << " (" << spellInfo->Id << ")" << " Radius " << radius + out << "Избегаю: " << name.str() << " (" << spellInfo->Id << ")" << "; радиус " << radius << " - [Trap]"; bot->Say(out.str(), LANG_UNIVERSAL); } @@ -2075,7 +2075,7 @@ bool AvoidAoeAction::AvoidUnitWithDamageAura() lastTellTimer = time(NULL); lastMoveTimer = getMSTime(); std::ostringstream out; - out << "I'm avoiding " << name.str() << " (" << triggerSpellInfo->Id << ")" + out << "Избегаю: " << name.str() << " (" << triggerSpellInfo->Id << ")" << " Radius " << radius << " - [Unit Trigger]"; bot->Say(out.str(), LANG_UNIVERSAL); } @@ -2554,7 +2554,7 @@ bool DisperseSetAction::Execute(Event event) if (text == "disable") { RESET_AI_VALUE(float, "disperse distance"); - botAI->TellMasterNoFacing("Disable disperse"); + botAI->TellMasterNoFacing("Рассредоточение отключено"); return true; } if (text == "enable" || text == "reset") @@ -2569,7 +2569,7 @@ bool DisperseSetAction::Execute(Event event) } float dis = AI_VALUE(float, "disperse distance"); std::ostringstream out; - out << "Enable disperse distance " << std::setprecision(2) << dis; + out << "Включено рассредоточение с дистанцией " << std::setprecision(2) << dis; botAI->TellMasterNoFacing(out.str()); return true; } @@ -2579,13 +2579,13 @@ bool DisperseSetAction::Execute(Event event) std::ostringstream out; if (dis <= 0.0f) { - out << "Enable disperse first"; + out << "Сначала включите рассредоточение"; botAI->TellMasterNoFacing(out.str()); return true; } dis += 1.0f; SET_AI_VALUE(float, "disperse distance", dis); - out << "Increase disperse distance to " << std::setprecision(2) << dis; + out << "Дистанция рассредоточения увеличена до " << std::setprecision(2) << dis; botAI->TellMasterNoFacing(out.str()); return true; } @@ -2599,7 +2599,7 @@ bool DisperseSetAction::Execute(Event event) } SET_AI_VALUE(float, "disperse distance", dis); std::ostringstream out; - out << "Increase disperse distance to " << std::setprecision(2) << dis; + out << "Дистанция рассредоточения увеличена до " << std::setprecision(2) << dis; botAI->TellMasterNoFacing(out.str()); return true; } @@ -2611,18 +2611,18 @@ bool DisperseSetAction::Execute(Event event) std::ostringstream out; if (dis < 0 || dis > 100.0f) { - out << "Invalid disperse distance " << std::setprecision(2) << dis; + out << "Недопустимая дистанция рассредоточения: " << std::setprecision(2) << dis; } else { SET_AI_VALUE(float, "disperse distance", dis); - out << "Set disperse distance to " << std::setprecision(2) << dis; + out << "Дистанция рассредоточения установлена: " << std::setprecision(2) << dis; } botAI->TellMasterNoFacing(out.str()); return true; } std::ostringstream out; - out << "Usage: disperse [enable | disable | increase | decrease | set {distance}]"; + out << "Использование: disperse [enable | disable | increase | decrease | set {дистанция}]"; float dis = AI_VALUE(float, "disperse distance"); if (dis > 0.0f) { diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp index 0ea7560ed..3d1f02986 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/OpenItemAction.cpp @@ -36,6 +36,6 @@ void OpenItemAction::OpenItem(Item* item, uint8 bag, uint8 slot) botAI->GetAiObjectContext()->GetValue("loot target")->Set(lootObject); std::ostringstream out; - out << "Opened item: " << item->GetTemplate()->Name1; + out << "Открыт предмет: " << item->GetTemplate()->Name1; botAI->TellMaster(out.str()); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp index 78f7f9bc4..dcb05e94d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/OutfitAction.cpp @@ -17,9 +17,9 @@ bool OutfitAction::Execute(Event event) if (param == "?") { List(); - botAI->TellMaster("outfit +[item] to add items"); - botAI->TellMaster("outfit -[item] to remove items"); - botAI->TellMaster("outfit equip/replace to equip items"); + botAI->TellMaster("outfit <имя> +[предмет] — добавить предметы"); + botAI->TellMaster("outfit <имя> -[предмет] — удалить предметы"); + botAI->TellMaster("outfit <имя> equip/replace — надеть предметы"); } else { @@ -30,7 +30,7 @@ bool OutfitAction::Execute(Event event) Save(name, items); std::ostringstream out; - out << "Setting outfit " << name << " as " << param; + out << "Настраиваю комплект: " << name << " как " << param; botAI->TellMaster(out); return true; } @@ -48,7 +48,7 @@ bool OutfitAction::Execute(Event event) if (command == "equip") { std::ostringstream out; - out << "Equipping outfit " << name; + out << "Надеваю комплект: " << name; botAI->TellMaster(out); EquipItems(outfit); @@ -57,7 +57,7 @@ bool OutfitAction::Execute(Event event) else if (command == "replace") { std::ostringstream out; - out << "Replacing current equip with outfit " << name; + out << "Заменяю текущую экипировку комплектом: " << name; botAI->TellMaster(out); for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++) @@ -82,7 +82,7 @@ bool OutfitAction::Execute(Event event) else if (command == "reset") { std::ostringstream out; - out << "Resetting outfit " << name; + out << "Сбрасываю комплект: " << name; botAI->TellMaster(out); Save(name, ItemIds()); @@ -91,7 +91,7 @@ bool OutfitAction::Execute(Event event) else if (command == "update") { std::ostringstream out; - out << "Updating with current items outfit " << name; + out << "Обновляю комплект текущими предметами: " << name; botAI->TellMaster(out); Update(name); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp index 9b6cebfcb..5b456d9bd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PetitionSignAction.cpp @@ -38,7 +38,7 @@ bool PetitionSignAction::Execute(Event event) if (bot->GetArenaTeamId(slot)) { // player is already in an arena team - botAI->TellError("Sorry, I am already in such team"); + botAI->TellError("У меня уже есть такая команда"); accept = false; } } @@ -46,13 +46,13 @@ bool PetitionSignAction::Execute(Event event) { if (bot->GetGuildId()) { - botAI->TellError("Sorry, I am in a guild already"); + botAI->TellError("Я уже состою в гильдии"); accept = false; } if (bot->GetGuildIdInvited()) { - botAI->TellError("Sorry, I am invited to a guild already"); + botAI->TellError("Меня уже пригласили в гильдию"); accept = false; } @@ -60,7 +60,7 @@ bool PetitionSignAction::Execute(Event event) /*if (QueryResult* result = CharacterDatabase.Query("SELECT playerguid FROM petition_sign WHERE player_account = {} AND petitionguid = {}'", bot->GetSession()->GetAccountId(), petitionGuid.GetCounter())) { - botAI->TellError("Sorry, I already signed this pettition"); + botAI->TellError("Я уже подписал эту хартию"); accept = false; } */ @@ -88,7 +88,7 @@ bool PetitionSignAction::Execute(Event event) WorldPacket data(CMSG_PETITION_SIGN, 20); data << petitionGuid << unk; bot->GetSession()->HandlePetitionSignOpcode(data); - bot->Say("Thanks for the invite!", LANG_UNIVERSAL); + bot->Say("Спасибо за приглашение!", LANG_UNIVERSAL); LOG_INFO("playerbots", "Bot {} <{}> accepts {} invite", bot->GetGUID().ToString().c_str(), bot->GetName().c_str(), isArena ? "Arena" : "Guild"); return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp index 34d339f2c..23dcd4415 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/PositionAction.cpp @@ -12,7 +12,7 @@ void TellPosition(PlayerbotAI* botAI, std::string const name, PositionInfo pos) { std::ostringstream out; - out << "Position " << name; + out << "Позиция " << name; if (pos.isSet()) { @@ -22,7 +22,7 @@ void TellPosition(PlayerbotAI* botAI, std::string const name, PositionInfo pos) out << " is set to " << x << "," << y; } else - out << " is not set"; + out << " не установлена"; botAI->TellMaster(out); } @@ -52,7 +52,7 @@ bool PositionAction::Execute(Event event) std::vector params = split(param, ' '); if (params.size() != 2) { - botAI->TellMaster("Whisper position ?/set/reset"); + botAI->TellMaster("Напишите: position <имя> ?/set/reset"); return false; } @@ -72,7 +72,7 @@ bool PositionAction::Execute(Event event) posMap[name] = pos; std::ostringstream out; - out << "Position " << name << " is set"; + out << "Позиция " << name << " установлена"; botAI->TellMaster(out); return true; } @@ -83,7 +83,7 @@ bool PositionAction::Execute(Event event) posMap[name] = pos; std::ostringstream out; - out << "Position " << name << " is set"; + out << "Позиция " << name << " установлена"; botAI->TellMaster(out); return true; } @@ -94,7 +94,7 @@ bool PositionAction::Execute(Event event) posMap[name] = pos; std::ostringstream out; - out << "Position " << name << " is reset"; + out << "Позиция " << name << " сброшена"; botAI->TellMaster(out); return true; } @@ -108,7 +108,7 @@ bool MoveToPositionAction::Execute(Event /*event*/) if (!pos.isSet()) { std::ostringstream out; - out << "Position " << qualifier << " is not set"; + out << "Позиция " << qualifier << " не установлена"; botAI->TellMaster(out); return false; } @@ -169,7 +169,7 @@ bool ReturnToStayPositionAction::isPossible() const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z); if (distance > sPlayerbotAIConfig.reactDistance) { - botAI->TellMaster("The stay position is too far to return. I am going to stay where I am now"); + botAI->TellMaster("Заданная точка слишком далеко. Останусь на текущем месте"); // Set the stay position to current position stayPosition.Set(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId()); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp index 34e2cc831..28344d877 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QueryItemUsageAction.cpp @@ -139,7 +139,7 @@ std::string const QueryItemUsageAction::QueryItemPrice(ItemTemplate const* item) } } if (sellPrice) - msg << "Sell: " << chat->formatMoney(sellPrice); + msg << "Продажа: " << chat->formatMoney(sellPrice); std::ostringstream out; out << item->ItemId; @@ -153,7 +153,7 @@ std::string const QueryItemUsageAction::QueryItemPrice(ItemTemplate const* item) if (sellPrice) msg << " "; - msg << "Buy: " << chat->formatMoney(buyPrice); + msg << "Покупка: " << chat->formatMoney(buyPrice); } return msg.str(); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp index 9540a9bf8..a89ec2a93 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QueryQuestAction.cpp @@ -117,7 +117,7 @@ void QueryQuestAction::TellObjectives(uint32 questId) // Checks if the questTemplate is valid if (!questTemplate) { - botAI->TellMaster("Quest template not found."); + botAI->TellMaster("Шаблон задания не найден."); return; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp index f442f91a7..ec4021c17 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QuestAction.cpp @@ -158,9 +158,9 @@ bool QuestAction::CompleteQuest(Player* player, uint32 entry) if (botAI->HasStrategy("debug quest", BotState::BOT_STATE_NON_COMBAT) || botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT)) { LOG_INFO("playerbots", "{} => Quest [ {} ] completed", bot->GetName(), pQuest->GetTitle()); - bot->Say("Quest [ " + text_quest + " ] completed", LANG_UNIVERSAL); + bot->Say("Задание [ " + text_quest + " ] completed", LANG_UNIVERSAL); } - botAI->TellMasterNoFacing("Quest completed " + text_quest); + botAI->TellMasterNoFacing("Задание выполнено: " + text_quest); player->CompleteQuest(entry); @@ -188,7 +188,7 @@ bool QuestAction::ProcessQuests(WorldObject* questGiver) { //if (botAI->HasStrategy("debug", BotState::BOT_STATE_COMBAT) || botAI->HasStrategy("debug", BotState::BOT_STATE_NON_COMBAT)) - botAI->TellError("Cannot talk to quest giver"); + botAI->TellError("Не удалось поговорить с персонажем, выдающим задание"); return false; } @@ -220,18 +220,18 @@ bool QuestAction::AcceptQuest(Quest const* quest, ObjectGuid questGiver) uint32 questId = quest->GetQuestId(); if (bot->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE) - out << "Already completed"; + out << "Уже выполнено"; else if (!bot->CanTakeQuest(quest, false)) { if (!bot->SatisfyQuestStatus(quest, false)) - out << "Already on"; + out << "Уже выполняется"; else - out << "Can't take"; + out << "Невозможно взять"; } else if (!bot->SatisfyQuestLog(false)) - out << "Quest log is full"; + out << "Журнал заданий заполнен"; else if (!bot->CanAddQuest(quest, false)) - out << "Bags are full"; + out << "Сумки заполнены"; else { WorldPacket p(CMSG_QUESTGIVER_ACCEPT_QUEST); @@ -250,11 +250,11 @@ bool QuestAction::AcceptQuest(Quest const* quest, ObjectGuid questGiver) if (bot->GetQuestStatus(questId) != QUEST_STATUS_NONE && bot->GetQuestStatus(questId) != QUEST_STATUS_REWARDED) { BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest); - out << "Accepted " << chat->FormatQuest(quest); + out << "Принято: " << chat->FormatQuest(quest); botAI->TellMaster(out); return true; } - out << "Cannot accept"; + out << "Невозможно принять"; } out << " " << chat->FormatQuest(quest); @@ -288,7 +288,7 @@ bool QuestUpdateCompleteAction::Execute(Event event) // } const auto format = ChatHelper::FormatQuest(qInfo); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Quest completed " + format); + botAI->TellMasterNoFacing("Задание выполнено: " + format); BroadcastHelper::BroadcastQuestUpdateComplete(botAI, bot, qInfo); botAI->rpgStatistic.questCompleted++; // LOG_DEBUG("playerbots", "[New rpg] {} complete quest {}", bot->GetName(), qInfo->GetQuestId()); @@ -461,7 +461,7 @@ bool QuestUpdateFailedTimerAction::Execute(Event event) } else { - botAI->TellMaster("Failed timer for " + std::to_string(questId)); + botAI->TellMaster("Не удалось запустить таймер задания: " + std::to_string(questId)); } //drop quest diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp index 9c47af37b..dbea74686 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/QuestConfirmAcceptAction.cpp @@ -16,7 +16,7 @@ bool QuestConfirmAcceptAction::Execute(Event event) return false; } std::ostringstream out; - out << "Quest: " << chat->FormatQuest(quest) << " confirm accept"; + out << "Задание: " << chat->FormatQuest(quest) << " — подтвердите принятие"; botAI->TellMaster(out); bot->GetSession()->HandleQuestConfirmAccept(sendPacket); return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp index 1c510d69d..835a60ffe 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReadyCheckAction.cpp @@ -97,19 +97,19 @@ public: { if (!bot->GetUInt32Value(PLAYER_AMMO_ID)) { - botAI->TellError("Out of ammo!"); + botAI->TellError("Закончились боеприпасы!"); return false; } if (!bot->GetPet()) { - botAI->TellError("No pet!"); + botAI->TellError("Нет питомца!"); return false; } if (bot->GetPet()->GetHappinessState() == UNHAPPY) { - botAI->TellError("Pet is unhappy!"); + botAI->TellError("Питомец недоволен!"); return false; } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp index 9a923d4fd..bff6eaa72 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReleaseSpiritAction.cpp @@ -22,7 +22,7 @@ bool ReleaseSpiritAction::Execute(Event event) { if (!bot->InBattleground()) { - botAI->TellMasterNoFacing("I am not dead, will wait here"); + botAI->TellMasterNoFacing("Мне не требуется воскрешение, подожду здесь"); // -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); @@ -33,7 +33,7 @@ bool ReleaseSpiritAction::Execute(Event event) if (bot->GetCorpse() && bot->HasPlayerFlag(PLAYER_FLAGS_GHOST)) { - botAI->TellMasterNoFacing("I am already a spirit"); + botAI->TellMasterNoFacing("Я уже дух"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp index efb20d88d..c9f1a5ec6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RepairAllAction.cpp @@ -45,7 +45,7 @@ bool RepairAllAction::Execute(Event /*event*/) if (totalCost > 0) { std::ostringstream out; - out << "Repair: " << chat->formatMoney(totalCost) << " (" << unit->GetName() << ")"; + out << "Ремонт: " << chat->formatMoney(totalCost) << " (" << unit->GetName() << ")"; botAI->TellMasterNoFacing(out.str()); bot->PlayDistanceSound(1116); @@ -56,6 +56,6 @@ bool RepairAllAction::Execute(Event /*event*/) return true; } - botAI->TellError("Cannot find any npc to repair at"); + botAI->TellError("Не удалось найти персонажа для ремонта"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp index 19017c916..d8015ab65 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ResetAiAction.cpp @@ -46,6 +46,6 @@ bool ResetAiAction::Execute(Event event) } PlayerbotRepository::instance().Reset(botAI); botAI->ResetStrategies(false); - botAI->TellMaster("AI was reset to defaults"); + botAI->TellMaster("Настройки ИИ сброшены"); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp index 72b0ef2ec..6396f709a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RevealGatheringItemAction.cpp @@ -62,18 +62,18 @@ bool RevealGatheringItemAction::Execute(Event /*event*/) return false; std::ostringstream msg; - msg << "I see a " << ChatHelper::FormatGameobject(go) << ". "; + msg << "Вижу: " << ChatHelper::FormatGameobject(go) << ". "; switch (go->GetGoType()) { case GAMEOBJECT_TYPE_CHEST: - msg << "Let's look at it."; + msg << "Давайте посмотрим."; break; case GAMEOBJECT_TYPE_FISHINGNODE: - msg << "Let's fish a bit."; + msg << "Давайте немного порыбачим."; break; default: - msg << "Should we go nearer?"; + msg << "Подойти ближе?"; } // everything is fine, do it diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp index 0051a5a49..9b26e19c9 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ReviveFromCorpseAction.cpp @@ -28,7 +28,7 @@ bool ReviveFromCorpseAction::Execute(Event event) { if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) { - botAI->TellMasterNoFacing("Welcome back!"); + botAI->TellMasterNoFacing("С возвращением!"); botAI->ChangeStrategy("+follow,-stay", BOT_STATE_NON_COMBAT); return true; } @@ -297,7 +297,7 @@ bool SpiritHealerAction::Execute(Event /*event*/) Corpse* corpse = bot->GetCorpse(); if (!corpse) { - botAI->TellError("I am not a spirit"); + botAI->TellError("Я не дух"); return false; } @@ -322,7 +322,7 @@ bool SpiritHealerAction::Execute(Event /*event*/) bot->SpawnCorpseBones(); context->GetValue("current target")->Set(nullptr); bot->SetTarget(); - botAI->TellMaster("Hello"); + botAI->TellMaster("Здравствуйте."); if (dCount > 20) context->GetValue("death count")->Set(0); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp index 8fe1c6ff7..01255c5bf 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RewardAction.cpp @@ -39,7 +39,7 @@ bool RewardAction::Execute(Event event) if (groupLeaderUnit && Reward(itemId, groupLeaderUnit)) return true; - botAI->TellError("Cannot talk to quest giver"); + botAI->TellError("Не удалось поговорить с персонажем, выдающим задание"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.cpp index de16ed17d..e067702d6 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RpgSubActions.cpp @@ -374,7 +374,7 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/) "You can use this " + chat->FormatItem(item->GetTemplate()) + " better than me, " + guidP.GetPlayer()->GetName() /*chat->FormatWorldobject(guidP.GetPlayer())*/ + "."); else - bot->Say("You can use this " + chat->FormatItem(item->GetTemplate()) + " better than me, " + + bot->Say("Можно использовать: " + chat->FormatItem(item->GetTemplate()) + " better than me, " + player->GetName() /*chat->FormatWorldobject(player)*/ + ".", (bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH)); @@ -389,7 +389,7 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/) } } else - bot->Say("Start trade with" + chat->FormatWorldobject(player), + bot->Say("Начинаю обмен с" + chat->FormatWorldobject(player), (bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH)); botAI->SetNextCheckDelay(sPlayerbotAIConfig.rpgDelay); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp index 8309a4f09..ee8657968 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/RtscAction.cpp @@ -20,15 +20,15 @@ bool RTSCAction::Execute(Event event) if (command != "reset" && !master->HasSpell(RTSC_MOVE_SPELL)) { master->learnSpell(RTSC_MOVE_SPELL, false); - botAI->TellMasterNoFacing("RTS control enabled."); - botAI->TellMasterNoFacing("Aedm (Awesome energetic do move) spell trained."); + botAI->TellMasterNoFacing("Управление RTS включено."); + botAI->TellMasterNoFacing("Заклинание Aedm для управления перемещением изучено."); } else if (command == "reset") { if (master->HasSpell(RTSC_MOVE_SPELL)) { master->removeSpell(RTSC_MOVE_SPELL, SPEC_MASK_ALL, false); - botAI->TellMasterNoFacing("RTS control spell removed."); + botAI->TellMasterNoFacing("Заклинание управления RTS удалено."); } RESET_AI_VALUE(bool, "RTSC selected"); @@ -123,7 +123,7 @@ bool RTSCAction::Execute(Event event) if (command.find("show") != std::string::npos) { std::ostringstream out; - out << "saved: "; + out << "сохранено: "; for (auto value : botAI->GetAiObjectContext()->GetValues()) if (value.find("RTSC saved location::") != std::string::npos) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp index bfabe36a6..ff0dbd341 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SaveManaAction.cpp @@ -16,7 +16,7 @@ bool SaveManaAction::Execute(Event event) if (text == "?") { std::ostringstream out; - out << "Mana save level: " << Format(value); + out << "Уровень экономии маны: " << Format(value); botAI->TellMaster(out); return true; } @@ -55,7 +55,7 @@ bool SaveManaAction::Execute(Event event) botAI->GetAiObjectContext()->GetValue("mana save level")->Set(value); std::ostringstream out; - out << "Mana save level set: " << Format(value); + out << "Уровень экономии маны установлен: " << Format(value); botAI->TellMaster(out); return true; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp index 512d7357f..6260ee734 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SecurityCheckAction.cpp @@ -25,7 +25,7 @@ bool SecurityCheckAction::Execute(Event /*event*/) if ((botAI->GetGroupLeader()->GetSession()->GetSecurity() == SEC_PLAYER) && (!bot->GetGuildId() || bot->GetGuildId() != botAI->GetGroupLeader()->GetGuildId())) { - botAI->TellError("I will play with this loot type only if I'm in your guild :/"); + botAI->TellError("Я буду использовать этот тип добычи, только если состою в вашей гильдии :/"); botAI->ChangeStrategy("+passive,+stay", BOT_STATE_NON_COMBAT); botAI->ChangeStrategy("+passive,+stay", BOT_STATE_COMBAT); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp index 8ff78f3af..a89941dd8 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SellAction.cpp @@ -84,7 +84,7 @@ bool SellAction::Execute(Event event) return true; } - botAI->TellError("Usage: s gray/*/vendor/[item link]"); + botAI->TellError("Использование: s gray/*/vendor/[ссылка на предмет]"); return false; } @@ -127,7 +127,7 @@ void SellAction::Sell(Item* item) bot->SetMoney(botMoney); } - out << "Selling " << chat->FormatItem(item->GetTemplate()); + out << "Продаю: " << chat->FormatItem(item->GetTemplate()); botAI->TellMaster(out); bot->PlayDistanceSound(120); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp index 9be9d553a..39e30bad2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SendMailAction.cpp @@ -53,14 +53,14 @@ bool SendMailAction::Execute(Event event) if (!mailboxFound && !randomBot) { - bot->Whisper("There is no mailbox nearby", LANG_UNIVERSAL, tellTo); + bot->Whisper("Поблизости нет почтового ящика", LANG_UNIVERSAL, tellTo); return false; } ItemIds ids = chat->parseItems(text); if (ids.size() > 1) { - bot->Whisper("You can not request more than one item", LANG_UNIVERSAL, tellTo); + bot->Whisper("Можно запросить только один предмет", LANG_UNIVERSAL, tellTo); return false; } @@ -72,13 +72,13 @@ bool SendMailAction::Execute(Event event) if (randomBot) { - bot->Whisper("I cannot send money", LANG_UNIVERSAL, tellTo); + bot->Whisper("Я не могу отправить деньги", LANG_UNIVERSAL, tellTo); return false; } if (bot->GetMoney() < money) { - botAI->TellError("I don't have enough money"); + botAI->TellError("У меня недостаточно денег"); return false; } @@ -100,7 +100,7 @@ bool SendMailAction::Execute(Event event) CharacterDatabase.CommitTransaction(trans); std::ostringstream out; - out << "Sending mail to " << receiver->GetName(); + out << "Отправляю письмо: " << receiver->GetName(); botAI->TellMaster(out.str()); return true; } @@ -125,7 +125,7 @@ bool SendMailAction::Execute(Event event) if (item->IsSoulBound() || item->IsConjuredConsumable()) { std::ostringstream out; - out << "Cannot send " << ChatHelper::FormatItem(item->GetTemplate()); + out << "Не удалось отправить: " << ChatHelper::FormatItem(item->GetTemplate()); bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo); continue; } @@ -160,7 +160,7 @@ bool SendMailAction::Execute(Event event) CharacterDatabase.CommitTransaction(trans); std::ostringstream out; - out << "Sent mail to " << receiver->GetName(); + out << "Письмо отправлено: " << receiver->GetName(); bot->Whisper(out.str(), LANG_UNIVERSAL, tellTo); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp index f8682c45e..c789ef740 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetCraftAction.cpp @@ -24,7 +24,7 @@ bool SetCraftAction::Execute(Event event) if (link == "reset") { data.Reset(); - botAI->TellMaster("I will not craft anything"); + botAI->TellMaster("Я ничего не буду создавать"); return true; } @@ -37,7 +37,7 @@ bool SetCraftAction::Execute(Event event) ItemIds itemIds = chat->parseItems(link); if (itemIds.empty()) { - botAI->TellMaster("Usage: 'craft [itemId]' or 'craft reset'"); + botAI->TellMaster("Использование: «craft [ID предмета]» или «craft reset»"); return false; } @@ -93,7 +93,7 @@ bool SetCraftAction::Execute(Event event) if (data.required.empty()) { - botAI->TellMaster("I cannot craft this"); + botAI->TellMaster("Я не могу это создать"); return false; } @@ -108,7 +108,7 @@ void SetCraftAction::TellCraft() CraftData& data = AI_VALUE(CraftData&, "craft"); if (data.IsEmpty()) { - botAI->TellMaster("I will not craft anything"); + botAI->TellMaster("Я ничего не буду создавать"); return; } @@ -117,7 +117,7 @@ void SetCraftAction::TellCraft() return; std::ostringstream out; - out << "I will craft " << chat->FormatItem(proto) << " using reagents: "; + out << "Я создам: " << chat->FormatItem(proto) << "; реагенты: "; bool first = true; for (std::map::iterator i = data.required.begin(); i != data.required.end(); ++i) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp index 0fa2ef50b..aaccf9cfe 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SetHomeAction.cpp @@ -30,14 +30,14 @@ bool SetHomeAction::Execute(Event /*event*/) { Creature* creature = botAI->GetCreature(selection); bot->GetSession()->SendBindPoint(creature); - botAI->TellMaster("This inn is my new home"); + botAI->TellMaster("Эта таверна теперь мой дом"); return true; } else { Creature* creature = botAI->GetCreature(selection); bot->GetSession()->SendBindPoint(creature); - botAI->TellMaster("This inn is my new home"); + botAI->TellMaster("Эта таверна теперь мой дом"); return true; } } @@ -50,10 +50,10 @@ bool SetHomeAction::Execute(Event /*event*/) continue; bot->GetSession()->SendBindPoint(unit); - botAI->TellMaster("This inn is my new home"); + botAI->TellMaster("Эта таверна теперь мой дом"); return true; } - botAI->TellError("Can't find any innkeeper around"); + botAI->TellError("Поблизости нет хозяина таверны"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp index a9fe32c97..21f0a32b1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/ShareQuestAction.cpp @@ -32,7 +32,7 @@ bool ShareQuestAction::Execute(Event event) WorldPacket p; p << entry; bot->GetSession()->HandlePushQuestToParty(p); - botAI->TellMaster("Quest shared"); + botAI->TellMaster("Задание передано группе"); return true; } } @@ -99,7 +99,7 @@ bool AutoShareQuestAction::Execute(Event event) WorldPacket p; p << logQuest; bot->GetSession()->HandlePushQuestToParty(p); - botAI->TellMaster("Quest shared"); + botAI->TellMaster("Задание передано группе"); shared = true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp index ad7e86a5d..2096ff1fb 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/SkipSpellsListAction.cpp @@ -30,7 +30,7 @@ bool SkipSpellsListAction::Execute(Event event) if (cmd == "reset") { skipSpells.clear(); - botAI->TellMaster("Ignored spell list is empty"); + botAI->TellMaster("Список игнорируемых заклинаний пуст"); return true; } @@ -39,11 +39,11 @@ bool SkipSpellsListAction::Execute(Event event) std::ostringstream out; if (skipSpells.empty()) { - botAI->TellMaster("Ignored spell list is empty"); + botAI->TellMaster("Список игнорируемых заклинаний пуст"); return true; } - out << "Ignored spell list: "; + out << "Список игнорируемых заклинаний: "; bool first = true; for (uint32 spellId : skipSpells) @@ -71,7 +71,7 @@ bool SkipSpellsListAction::Execute(Event event) uint32 spellId = chat->parseSpell(cmd); if (!spellId) { - botAI->TellError("Unknown spell"); + botAI->TellError("Неизвестное заклинание"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp index 8fd120d60..42ed28388 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TalkToQuestGiverAction.cpp @@ -19,7 +19,7 @@ bool TalkToQuestGiverAction::ProcessQuest(Quest const* quest, Object* questGiver { bool isCompleted = false; std::ostringstream out; - out << "Quest "; + out << "Задание "; QuestStatus status = bot->GetQuestStatus(quest->GetQuestId()); Player* master = GetMaster(); @@ -92,7 +92,7 @@ bool TalkToQuestGiverAction::TurnInQuest(Quest const* quest, Object* questGiver, const Quest* pQuest = sObjectMgr->GetQuestTemplate(questID); const std::string text_quest = ChatHelper::FormatQuest(pQuest); LOG_INFO("playerbots", "{} => Quest [ {} ] completed", bot->GetName(), pQuest->GetTitle()); - bot->Say("Quest [ " + text_quest + " ] completed", LANG_UNIVERSAL); + bot->Say("Задание [ " + text_quest + " ] completed", LANG_UNIVERSAL); } return true; @@ -190,11 +190,11 @@ void TalkToQuestGiverAction::RewardMultipleItem(Quest const* quest, Object* ques } ItemTemplate const* item = sObjectMgr->GetItemTemplate(quest->RewardChoiceItemId[best]); bot->RewardQuest(quest, best, questGiver, true); - out << "Rewarded " << ChatHelper::FormatItem(item); + out << "Получена награда: " << ChatHelper::FormatItem(item); } else { - out << "Unable to find suitable reward. Asking for help...."; + out << "Не удалось подобрать подходящую награду. Нужна помощь..."; AskToSelectReward(quest, out, true); } } @@ -218,7 +218,7 @@ void TalkToQuestGiverAction::RewardMultipleItem(Quest const* quest, Object* ques ItemTemplate const* item = sObjectMgr->GetItemTemplate(quest->RewardChoiceItemId[firstId]); bot->RewardQuest(quest, firstId, questGiver, true); - out << "Rewarded " << ChatHelper::FormatItem(item); + out << "Получена награда: " << ChatHelper::FormatItem(item); } } } @@ -226,7 +226,7 @@ void TalkToQuestGiverAction::RewardMultipleItem(Quest const* quest, Object* ques void TalkToQuestGiverAction::AskToSelectReward(Quest const* quest, std::ostringstream& out, bool forEquip) { std::ostringstream msg; - msg << "Choose reward: "; + msg << "Выберите награду: "; for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i) { @@ -240,7 +240,7 @@ void TalkToQuestGiverAction::AskToSelectReward(Quest const* quest, std::ostrings } botAI->TellMaster(msg); - out << "Reward pending"; + out << "Ожидается выбор награды"; } bool TurnInQueryQuestAction::Execute(Event event) @@ -280,7 +280,7 @@ bool TurnInQueryQuestAction::Execute(Event event) } } std::ostringstream out; - out << "Quest "; + out << "Задание "; switch (status) { case QUEST_STATUS_COMPLETE: diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp index e968a0b01..15ef800fd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TameAction.cpp @@ -124,7 +124,7 @@ bool TameAction::Execute(Event event) } catch (...) { - botAI->TellError("Invalid tame id."); + botAI->TellError("Неверный ID приручения."); } } else if (mode == "family" && !value.empty()) @@ -163,7 +163,7 @@ bool TameAction::Execute(Event event) } else { - botAI->TellMaster("Pet changed and initialized!"); + botAI->TellMaster("Питомец заменён и подготовлен!"); } } @@ -198,7 +198,7 @@ bool TameAction::SetPetByName(const std::string& name) // If the creature is exotic and the bot doesn't have Beast Mastery, show error and fail if (IsExoticPet(&creature) && !HasBeastMastery(bot)) { - botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent."); + botAI->TellError("Я не могу использовать экзотических питомцев без таланта «Повелитель зверей»."); return false; } @@ -215,7 +215,7 @@ bool TameAction::SetPetByName(const std::string& name) } // If no suitable pet found, show an error and return failure - botAI->TellError("No tameable pet found with name: " + name); + botAI->TellError("Не найден приручаемый питомец с именем: " + name); return false; } @@ -232,21 +232,21 @@ bool TameAction::SetPetById(uint32 id) if (!creature->IsTameable(true)) { // If not tameable at all, show an error and fail - botAI->TellError("No tameable pet found with id: " + std::to_string(id)); + botAI->TellError("Не найден приручаемый питомец с ID: " + std::to_string(id)); return false; } // If it's an exotic pet, make sure the bot has the Beast Mastery talent if (IsExoticPet(creature) && !HasBeastMastery(bot)) { - botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent."); + botAI->TellError("Я не могу использовать экзотических питомцев без таланта «Повелитель зверей»."); return false; } // Check if the bot is actually allowed to tame this pet (honoring exotic pet rules) if (!creature->IsTameable(bot->CanTameExoticPets())) { - botAI->TellError("No tameable pet found with id: " + std::to_string(id)); + botAI->TellError("Не найден приручаемый питомец с ID: " + std::to_string(id)); return false; } @@ -258,7 +258,7 @@ bool TameAction::SetPetById(uint32 id) } // If no valid creature was found by id, show an error - botAI->TellError("No tameable pet found with id: " + std::to_string(id)); + botAI->TellError("Не найден приручаемый питомец с ID: " + std::to_string(id)); return false; } @@ -316,9 +316,9 @@ bool TameAction::SetPetByFamily(const std::string& family) if (candidates.empty()) { if (foundExotic && !HasBeastMastery(bot)) - botAI->TellError("I cannot use exotic pets unless I have the Beast Mastery talent."); + botAI->TellError("Я не могу использовать экзотических питомцев без таланта «Повелитель зверей»."); else - botAI->TellError("No tameable pet found with family: " + family); + botAI->TellError("Не найден приручаемый питомец семейства: " + family); return false; } @@ -343,14 +343,14 @@ bool TameAction::RenamePet(const std::string& newName) // Check if the bot currently has a pet if (!pet) { - botAI->TellError("You have no pet to rename."); + botAI->TellError("У вас нет питомца для переименования."); return false; } // Validate the new name: must not be empty and max 12 characters if (newName.empty() || newName.length() > 12) { - botAI->TellError("Pet name must be between 1 and 12 alphabetic characters."); + botAI->TellError("Имя питомца должно содержать от 1 до 12 букв."); return false; } @@ -359,7 +359,7 @@ bool TameAction::RenamePet(const std::string& newName) { if (!std::isalpha(static_cast(c))) { - botAI->TellError("Pet name must only contain alphabetic characters (A-Z, a-z)."); + botAI->TellError("Имя питомца должно состоять только из букв."); return false; } } @@ -373,7 +373,7 @@ bool TameAction::RenamePet(const std::string& newName) // Check if the new name is reserved or forbidden if (sObjectMgr->IsReservedName(normalized)) { - botAI->TellError("That pet name is forbidden. Please choose another name."); + botAI->TellError("Это имя питомца запрещено. Выберите другое."); return false; } @@ -383,8 +383,8 @@ bool TameAction::RenamePet(const std::string& newName) bot->GetSession()->SendPetNameQuery(pet->GetGUID(), pet->GetEntry()); // Notify the master about the rename and give a tip to update the client name display - botAI->TellMaster("Your pet has been renamed to " + normalized + "!"); - botAI->TellMaster("If you do not see the new name, please dismiss and recall your pet."); + botAI->TellMaster("Ваш питомец переименован в " + normalized + "!"); + botAI->TellMaster("Если новое имя не появилось, отпустите и снова призовите питомца."); // Remove the current pet and (re-)cast Call Pet spell if the bot is a hunter bot->RemovePet(nullptr, PET_SAVE_AS_CURRENT, true); @@ -402,7 +402,7 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry) // Ensure the player is a hunter and at least level 10 (required for pets) if (bot->getClass() != CLASS_HUNTER || bot->GetLevel() < 10) { - botAI->TellError("Only level 10+ hunters can have pets."); + botAI->TellError("Питомцы доступны охотникам не ниже 10-го уровня."); return false; } @@ -410,7 +410,7 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry) CreatureTemplate const* creature = sObjectMgr->GetCreatureTemplate(creatureEntry); if (!creature) { - botAI->TellError("Creature template not found."); + botAI->TellError("Шаблон существа не найден."); return false; } @@ -431,7 +431,7 @@ bool TameAction::CreateAndSetPet(uint32 creatureEntry) Pet* pet = bot->CreateTamedPetFrom(creatureEntry, 0); if (!pet) { - botAI->TellError("Failed to create pet."); + botAI->TellError("Не удалось создать питомца."); return false; } @@ -486,13 +486,13 @@ bool TameAction::AbandonPet() // Remove the pet from the bot and mark it as deleted in the database bot->RemovePet(pet, PET_SAVE_AS_DELETED); // Inform the bot's master/player that the pet was abandoned - botAI->TellMaster("Your pet has been abandoned."); + botAI->TellMaster("Питомец отпущен."); return true; } else { // If there is no hunter pet, show an error message - botAI->TellError("You have no hunter pet to abandon."); + botAI->TellError("У вас нет питомца охотника, которого можно отпустить."); return false; } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp index 1f9e89cc1..f6c628f8c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TaxiAction.cpp @@ -24,7 +24,7 @@ bool TaxiAction::Execute(Event event) { movement.taxiNodes.clear(); movement.Set(nullptr); - botAI->TellMaster("I am ready for the next flight"); + botAI->TellMaster("Можно отправляться в следующий полёт"); return true; } @@ -82,7 +82,7 @@ bool TaxiAction::Execute(Event event) if (param == "?") { - botAI->TellMasterNoFacing("=== Taxi ==="); + botAI->TellMasterNoFacing("=== Маршруты полётов ==="); uint32 index = 1; for (uint32 node : nodes) @@ -118,13 +118,13 @@ bool TaxiAction::Execute(Event event) { movement.taxiNodes.clear(); movement.Set(nullptr); - botAI->TellError("I can't fly with you"); + botAI->TellError("Я не могу лететь вместе с вами"); return false; } return true; } - botAI->TellError("Cannot find any flightmaster to talk"); + botAI->TellError("Не удалось найти распорядителя полётов"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp index b981decbf..17a6b7ca2 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TeleportAction.cpp @@ -54,7 +54,7 @@ bool TeleportAction::Execute(Event /*event*/) if (closestPortal && bot->IsWithinDistInMap(closestPortal, INTERACTION_DISTANCE)) { std::ostringstream out; - out << "Using portal: " << closestPortal->GetName(); + out << "Использую портал: " << closestPortal->GetName(); botAI->TellMasterNoFacing(out.str()); WorldPacket data(CMSG_GAMEOBJ_USE); @@ -82,7 +82,7 @@ bool TeleportAction::Execute(Event /*event*/) continue; std::ostringstream out; - out << "Teleporting using " << goInfo->name; + out << "Телепортируюсь через: " << goInfo->name; botAI->TellMasterNoFacing(out.str()); botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT); @@ -109,6 +109,6 @@ bool TeleportAction::Execute(Event /*event*/) } // If no teleport option is found - botAI->TellError("Cannot find any portal to teleport"); + botAI->TellError("Не удалось найти портал для телепортации"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp index 792a32445..1f58d2069 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellCastFailedAction.cpp @@ -28,26 +28,26 @@ bool TellCastFailedAction::Execute(Event event) switch (result) { case SPELL_FAILED_NOT_READY: - out << "not ready"; + out << "ещё не готово"; break; case SPELL_FAILED_REQUIRES_SPELL_FOCUS: - out << "requires spell focus"; + out << "требуется объект силы"; break; case SPELL_FAILED_REQUIRES_AREA: - out << "cannot cast here"; + out << "здесь применить нельзя"; break; case SPELL_FAILED_EQUIPPED_ITEM_CLASS: - out << "requires item"; + out << "требуется предмет"; break; case SPELL_FAILED_EQUIPPED_ITEM_CLASS_MAINHAND: case SPELL_FAILED_EQUIPPED_ITEM_CLASS_OFFHAND: - out << "requires weapon"; + out << "требуется оружие"; break; case SPELL_FAILED_PREVENTED_BY_MECHANIC: - out << "interrupted"; + out << "прервано"; break; default: - out << "cannot cast"; + out << "невозможно применить"; } if (spellInfo->CalcCastTime() >= 2000) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp index 3eac10473..fd961b5d4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellGlyphsAction.cpp @@ -105,9 +105,9 @@ bool TellGlyphsAction::Execute(Event event) // 4. Send chat messages //----------------------------------------------------------------- if (first) // no glyphs - botAI->TellMaster("No glyphs equipped"); + botAI->TellMaster("Символы не установлены"); else - botAI->TellMaster(std::string("Glyphs: ") + list.str()); + botAI->TellMaster(std::string("Символы: ") + list.str()); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp index ade7571f3..89a5baf4a 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellItemCountAction.cpp @@ -23,7 +23,7 @@ bool TellItemCountAction::Execute(Event event) soulbound[proto->ItemId] = item->IsSoulBound(); } - botAI->TellMaster("=== Inventory ==="); + botAI->TellMaster("=== Инвентарь ==="); for (std::map::iterator i = itemMap.begin(); i != itemMap.end(); ++i) { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(i->first); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp index 3d18c5826..dc8ae4c91 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellLosAction.cpp @@ -77,7 +77,7 @@ void TellLosAction::ListGameObjects(std::string const title, GuidVector gos) bool TellAuraAction::Execute(Event /*event*/) { - botAI->TellMaster("--- Auras ---"); + botAI->TellMaster("--- Ауры ---"); sLog->outMessage("playerbot", LOG_LEVEL_DEBUG, "--- Auras ---"); Unit::AuraApplicationMap& map = bot->GetAppliedAuras(); for (Unit::AuraApplicationMap::iterator i = map.begin(); i != map.end(); ++i) @@ -104,7 +104,7 @@ bool TellAuraAction::Execute(Event /*event*/) " isArea: " + std::to_string(is_area) + " duration: " + std::to_string(duration) + " spellId: " + std::to_string(spellId) + " isPositive: " + std::to_string(isPositive)); - botAI->TellMaster("Info of Aura - name: " + auraName + " caster: " + caster_name + " type: " + + botAI->TellMaster("Сведения об ауре — название: " + auraName + " caster: " + caster_name + " type: " + std::to_string(type) + " owner: " + owner_name + " distance: " + std::to_string(distance) + " isArea: " + std::to_string(is_area) + " duration: " + std::to_string(duration) + " spellId: " + std::to_string(spellId) + " isPositive: " + std::to_string(isPositive)); @@ -120,7 +120,7 @@ bool TellAuraAction::Execute(Event /*event*/) " radius: " + std::to_string(radius) + " spell id: " + std::to_string(spellId) + " duration: " + std::to_string(duration)); - botAI->TellMaster(std::string("Info of DynamicObject -") + " name: " + dyn_owner->GetName() + + botAI->TellMaster(std::string("Сведения о динамическом объекте —") + " name: " + dyn_owner->GetName() + " radius: " + std::to_string(radius) + " spell id: " + std::to_string(spellId) + " duration: " + std::to_string(duration)); } @@ -131,7 +131,7 @@ bool TellAuraAction::Execute(Event /*event*/) bool TellEstimatedDpsAction::Execute(Event /*event*/) { float dps = AI_VALUE(float, "estimated group dps"); - botAI->TellMaster("Estimated Group DPS: " + std::to_string(dps)); + botAI->TellMaster("Расчётный урон группы в секунду: " + std::to_string(dps)); return true; } @@ -147,7 +147,7 @@ bool TellCalculateItemAction::Execute(Event event) float score = calculator.CalculateItem(item.itemId, item.randomPropertyId); std::ostringstream out; - out << "Calculated score of " << chat->FormatItem(proto) << " : " << score; + out << "Расчётная ценность предмета " << chat->FormatItem(proto) << " : " << score; botAI->TellMasterNoFacing(out.str()); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp index 6156ce081..714c5ede1 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellMasterAction.cpp @@ -16,7 +16,7 @@ bool TellMasterAction::Execute(Event /*event*/) bool OutOfReactRangeAction::Execute(Event /*event*/) { - botAI->TellMaster("Wait for me!"); + botAI->TellMaster("Подождите меня!"); return true; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp index 0ccff606a..4750d2b84 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellReputationAction.cpp @@ -36,16 +36,16 @@ bool TellReputationAction::Execute(Event /*event*/) switch (rank) { case REP_HATED: - out << "cc2222hated"; + out << "cc2222Ненависть"; break; case REP_HOSTILE: - out << "ff0000hostile"; + out << "ff0000Враждебность"; break; case REP_UNFRIENDLY: - out << "ee6622unfriendly"; + out << "ee6622Неприязнь"; break; case REP_NEUTRAL: - out << "ffff00neutral"; + out << "ffff00Равнодушие"; break; case REP_FRIENDLY: out << "00ff00friendly"; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp index 422d5e542..60e3f379c 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TellTargetAction.cpp @@ -16,7 +16,7 @@ bool TellTargetAction::Execute(Event /*event*/) if (target) { std::ostringstream out; - out << "Attacking " << target->GetName(); + out << "Атакую: " << target->GetName(); botAI->TellMaster(out); context->GetValue("old target")->Set(target); @@ -27,7 +27,7 @@ bool TellTargetAction::Execute(Event /*event*/) bool TellAttackersAction::Execute(Event /*event*/) { - botAI->TellMaster("--- Attackers ---"); + botAI->TellMaster("--- Атакующие ---"); GuidVector attackers = context->GetValue("attackers")->Get(); int32 count = 0; @@ -40,7 +40,7 @@ bool TellAttackersAction::Execute(Event /*event*/) botAI->TellMaster(std::to_string(++count) + std::string(".") + unit->GetName()); } - botAI->TellMaster("--- Threat ---"); + botAI->TellMaster("--- Угроза ---"); HostileReference* ref = bot->getHostileRefMgr().getFirst(); if (!ref) diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp index 810710abb..ab4f62463 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeAction.cpp @@ -41,7 +41,15 @@ bool TradeAction::Execute(Event event) WorldPacket packet(CMSG_INITIATE_TRADE); packet << player->GetGUID(); bot->GetSession()->HandleInitiateTradeOpcode(packet); - return true; + + // HandleInitiateTradeOpcode creates both TradeData objects + // synchronously. Continue processing the same `t ` command + // so a natural-language request can both open the trade and place + // the requested item without requiring the player to repeat it. + if (!bot->GetTrader() || bot->GetTrader() != player) + return true; + if (text.empty()) + return true; } else if (player->GetTrader() != bot) return false; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp index 1c6ffc8ee..ed2af344f 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusAction.cpp @@ -16,6 +16,45 @@ #include "RandomPlayerbotMgr.h" #include "SetCraftAction.h" +namespace +{ +std::string FormatMoneyRussian(uint32 copper) +{ + std::ostringstream out; + if (!copper) + return "0 мед."; + + uint32 gold = copper / 10000; + copper -= gold * 10000; + uint32 silver = copper / 100; + copper -= silver * 100; + + bool space = false; + if (gold) + { + out << gold << " зол."; + space = true; + } + + if (silver && gold < 50) + { + if (space) + out << ' '; + out << silver << " сер."; + space = true; + } + + if (copper && gold < 10) + { + if (space) + out << ' '; + out << copper << " мед."; + } + + return out.str(); +} +} + bool TradeStatusAction::Execute(Event event) { Player* trader = bot->GetTrader(); @@ -28,13 +67,13 @@ bool TradeStatusAction::Execute(Event event) // Allow the master and group members to trade if (trader != master && !traderBotAI && (!bot->GetGroup() || !bot->GetGroup()->IsMember(trader->GetGUID()))) { - bot->Whisper("I'm kind of busy now", LANG_UNIVERSAL, trader); + bot->Whisper("Сейчас мне не до обмена", LANG_UNIVERSAL, trader); return false; } if (sPlayerbotAIConfig.enableRandomBotTrading == 0 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot))) { - bot->Whisper("Trading is disabled", LANG_UNIVERSAL, trader); + bot->Whisper("Обмен отключён", LANG_UNIVERSAL, trader); return false; } @@ -138,7 +177,7 @@ void TradeStatusAction::BeginTrade() ListItemsVisitor visitor; IterateItems(&visitor); - botAI->TellMaster("=== Inventory ==="); + botAI->TellMaster("=== Инвентарь ==="); TellItems(visitor.items, visitor.soulbound); if (sRandomPlayerbotMgr.IsRandomBot(bot)) @@ -147,7 +186,7 @@ void TradeStatusAction::BeginTrade() if (discount) { std::ostringstream out; - out << "Discount up to: " << chat->formatMoney(discount); + out << "Доступная скидка: " << FormatMoneyRussian(discount); botAI->TellMaster(out); } } @@ -186,9 +225,9 @@ bool TradeStatusAction::CheckTrade() { if (bot->GetGroup() && bot->GetGroup()->IsMember(bot->GetTrader()->GetGUID()) && botAI->HasRealPlayerMaster()) - botAI->TellMasterNoFacing("Thank you " + chat->FormatWorldobject(bot->GetTrader())); + botAI->TellMasterNoFacing("Спасибо, " + chat->FormatWorldobject(bot->GetTrader())); else - bot->Say("Thank you " + chat->FormatWorldobject(bot->GetTrader()), + bot->Say("Спасибо, " + chat->FormatWorldobject(bot->GetTrader()), (bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH)); } return isGettingItem; @@ -216,12 +255,12 @@ bool TradeStatusAction::CheckTrade() int32 playerMoney = trader->GetTradeData()->GetMoney() + playerItemsMoney; if (botItemsMoney > 0 && sPlayerbotAIConfig.enableRandomBotTrading == 2 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot))) { - bot->Whisper("Selling is disabled.", LANG_UNIVERSAL, trader); + bot->Whisper("Продажа отключена.", LANG_UNIVERSAL, trader); return false; } if (playerItemsMoney && sPlayerbotAIConfig.enableRandomBotTrading == 3 && (sRandomPlayerbotMgr.IsRandomBot(bot)|| sRandomPlayerbotMgr.IsAddclassBot(bot))) { - bot->Whisper("Buying is disabled.", LANG_UNIVERSAL, trader); + bot->Whisper("Покупка отключена.", LANG_UNIVERSAL, trader); return false; } for (uint32 slot = 0; slot < TRADE_SLOT_TRADED_COUNT; ++slot) @@ -258,7 +297,7 @@ bool TradeStatusAction::CheckTrade() if (!botItemsMoney && !playerItemsMoney) { - botAI->TellError("There are no items to trade"); + botAI->TellError("В обмене нет предметов"); return false; } @@ -272,7 +311,7 @@ bool TradeStatusAction::CheckTrade() { if (moneyDelta < 0) { - botAI->TellError("You can use discount to buy items only"); + botAI->TellError("Скидку можно использовать только для покупки предметов"); botAI->PlaySound(TEXT_EMOTE_NO); return false; } @@ -288,16 +327,16 @@ bool TradeStatusAction::CheckTrade() switch (urand(0, 4)) { case 0: - botAI->TellMaster("A pleasure doing business with you"); + botAI->TellMaster("Сделка состоялась"); break; case 1: - botAI->TellMaster("Fair trade"); + botAI->TellMaster("Честный обмен"); break; case 2: - botAI->TellMaster("Thanks"); + botAI->TellMaster("Спасибо"); break; case 3: - botAI->TellMaster("Off with you"); + botAI->TellMaster("До встречи"); break; } @@ -306,7 +345,7 @@ bool TradeStatusAction::CheckTrade() } std::ostringstream out; - out << "I want " << chat->formatMoney(-(delta + discount)) << " for this"; + out << "За это нужно " << FormatMoneyRussian(-(delta + discount)); botAI->TellMaster(out); botAI->PlaySound(TEXT_EMOTE_NO); return false; diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp index ada779e36..d74cbf159 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TradeStatusExtendedAction.cpp @@ -75,7 +75,7 @@ bool TradeStatusExtendedAction::Execute(Event event) } else { - botAI->TellMaster("I can't unlock this item."); + botAI->TellMaster("Я не могу открыть этот предмет."); } } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp index 0f3256099..7849c0f5d 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/TrainerAction.cpp @@ -117,7 +117,7 @@ bool TrainerAction::Execute(Event event) if (trainer_spells.empty()) { - botAI->TellError("No spells can be learned from this trainer"); + botAI->TellError("У этого тренера нечему учиться"); return false; } @@ -150,7 +150,7 @@ void TrainerAction::TellFooter(uint32 totalCost) if (totalCost) { std::ostringstream out; - out << "Total cost: " << chat->formatMoney(totalCost); + out << "Общая стоимость: " << chat->formatMoney(totalCost); botAI->TellMaster(out); } } @@ -159,11 +159,11 @@ bool MaintenanceAction::Execute(Event /*event*/) { if (!sPlayerbotAIConfig.maintenanceCommand) { - botAI->TellError("maintenance command is not allowed, please check the configuration."); + botAI->TellError("Команда maintenance отключена. Проверьте настройки."); return false; } - botAI->TellMaster("I'm maintaining"); + botAI->TellMaster("Выполняю обслуживание"); PlayerbotFactory factory(bot, bot->GetLevel()); if (!botAI->IsAlt()) @@ -269,18 +269,18 @@ bool AutoGearAction::Execute(Event /*event*/) { if (!sPlayerbotAIConfig.autoGearCommand) { - botAI->TellError("autogear command is not allowed, please check the configuration."); + botAI->TellError("Команда autogear отключена. Проверьте настройки."); return false; } if (!sPlayerbotAIConfig.autoGearCommandAltBots && !sPlayerbotAIConfig.IsInRandomAccountList(bot->GetSession()->GetAccountId())) { - botAI->TellError("You cannot use autogear on alt bots."); + botAI->TellError("Нельзя использовать autogear для ботов альтернативных персонажей."); return false; } - botAI->TellMaster("I'm auto gearing"); + botAI->TellMaster("Автоматически подбираю экипировку"); uint32 gs = sPlayerbotAIConfig.autoGearScoreLimit == 0 ? 0 : PlayerbotFactory::CalcMixedGearScore(sPlayerbotAIConfig.autoGearScoreLimit, diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp index 7a4b3bd17..baf09d542 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockItemAction.cpp @@ -27,9 +27,9 @@ void UnlockItemAction::UnlockItem(Item* item) if (botAI->CastSpell(PICK_LOCK_SPELL_ID, bot, item)) { std::ostringstream out; - out << "Used Pick Lock on: " << item->GetTemplate()->Name1; + out << "Замок открыт у предмета: " << item->GetTemplate()->Name1; botAI->TellMaster(out.str()); } else - botAI->TellError("Failed to cast Pick Lock."); + botAI->TellError("Не удалось применить «Взлом замка»."); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp index 90cdbfb02..7173a7f71 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UnlockTradedItemAction.cpp @@ -18,13 +18,13 @@ bool UnlockTradedItemAction::Execute(Event /*event*/) Item* lockbox = tradeData->GetItem(TRADE_SLOT_NONTRADED); if (!lockbox) { - botAI->TellError("No item in the Do Not Trade slot."); + botAI->TellError("В ячейке «Не для обмена» нет предмета."); return false; } if (!CanUnlockItem(lockbox)) { - botAI->TellError("Cannot unlock this item."); + botAI->TellError("Этот предмет невозможно открыть."); return false; } @@ -66,7 +66,7 @@ bool UnlockTradedItemAction::CanUnlockItem(Item* item) else { std::ostringstream out; - out << "Lockpicking skill too low (" << botSkill << "/" << requiredSkill << ") to unlock: " + out << "Навык взлома слишком низкий (" << botSkill << "/" << requiredSkill << ") для открытия: " << item->GetTemplate()->Name1; botAI->TellMaster(out.str()); } @@ -80,7 +80,7 @@ void UnlockTradedItemAction::UnlockItem(Item* item) { if (!bot->HasSpell(PICK_LOCK_SPELL_ID)) { - botAI->TellError("Cannot unlock, Pick Lock spell is missing."); + botAI->TellError("Не удалось открыть: отсутствует навык «Взлом замка»."); return; } @@ -88,11 +88,11 @@ void UnlockTradedItemAction::UnlockItem(Item* item) if (botAI->CastSpell(PICK_LOCK_SPELL_ID, bot->GetTrader(), item)) // Unit target is trader { std::ostringstream out; - out << "Picking Lock on traded item: " << item->GetTemplate()->Name1; + out << "Открываю замок на переданном предмете: " << item->GetTemplate()->Name1; botAI->TellMaster(out.str()); } else { - botAI->TellError("Failed to cast Pick Lock."); + botAI->TellError("Не удалось применить «Взлом замка»."); } } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp index 3483986f9..17eade154 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UseItemAction.cpp @@ -35,7 +35,7 @@ bool UseItemAction::Execute(Event event) return UseItemOnGameObject(*items.begin(), *gos.begin()); } - botAI->TellError("No items (or game objects) available"); + botAI->TellError("Нет доступных предметов или игровых объектов"); return false; } @@ -48,7 +48,7 @@ bool UseItemAction::UseGameObject(ObjectGuid guid) go->Use(bot); std::ostringstream out; - out << "Using " << chat->FormatGameobject(go); + out << "Использую: " << chat->FormatGameobject(go); botAI->TellMasterNoFacing(out.str()); return true; } @@ -94,7 +94,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni bool targetSelected = false; std::ostringstream out; - out << "Using " << chat->FormatItem(item->GetTemplate()); + out << "Использую: " << chat->FormatItem(item->GetTemplate()); if (item->GetTemplate()->Stackable > 1) { @@ -125,7 +125,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni { bool fit = SocketItem(itemTarget, item) || SocketItem(itemTarget, item, true); if (!fit) - botAI->TellMaster("Socket does not fit"); + botAI->TellMaster("Предмет не подходит для этой ячейки"); return fit; } @@ -175,7 +175,7 @@ bool UseItemAction::UseItem(Item* item, ObjectGuid goGuid, Item* itemTarget, Uni bot->GetSession()->HandleQuestgiverAcceptQuestOpcode(packet); std::ostringstream out; - out << "Got quest " << chat->FormatQuest(qInfo); + out << "Получено задание: " << chat->FormatQuest(qInfo); botAI->TellMasterNoFacing(out.str()); return true; } @@ -374,7 +374,7 @@ bool UseItemAction::SocketItem(Item* item, Item* gem, bool replace) if (fits) { std::ostringstream out; - out << "Socketing " << chat->FormatItem(item->GetTemplate()); + out << "Вставляю самоцвет в: " << chat->FormatItem(item->GetTemplate()); out << " with " << chat->FormatItem(gem->GetTemplate()); botAI->TellMaster(out); diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp index 0862be68f..24292b9dd 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/UseMeetingStoneAction.cpp @@ -36,7 +36,7 @@ bool UseMeetingStoneAction::Execute(Event event) if (bot->IsInCombat()) { - botAI->TellError("I am in combat"); + botAI->TellError("Я в бою"); return false; } @@ -73,13 +73,13 @@ bool SummonAction::Execute(Event /*event*/) if (SummonUsingGos(master, bot, true) || SummonUsingNpcs(master, bot, true)) { - botAI->TellMasterNoFacing("Hello!"); + botAI->TellMasterNoFacing("Здравствуйте!"); return true; } if (SummonUsingGos(bot, master, true) || SummonUsingNpcs(bot, master, true)) { - botAI->TellMasterNoFacing("Welcome!"); + botAI->TellMasterNoFacing("Добро пожаловать!"); return true; } @@ -153,7 +153,7 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras if (player->GetVehicle()) { - botAI->TellError("You cannot summon me while I'm on a vehicle"); + botAI->TellError("Нельзя призвать меня, пока я нахожусь в транспорте"); return false; } @@ -174,20 +174,20 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras if (summoner->IsInCombat() && !sPlayerbotAIConfig.allowSummonInCombat) { - botAI->TellError("You cannot summon me while you're in combat"); + botAI->TellError("Нельзя призвать меня, пока вы в бою"); return false; } if (!summoner->IsAlive() && !sPlayerbotAIConfig.allowSummonWhenMasterIsDead) { - botAI->TellError("You cannot summon me while you're dead"); + botAI->TellError("Нельзя призвать меня, пока вы мертвы"); return false; } if (bot->isDead() && !bot->HasPlayerFlag(PLAYER_FLAGS_GHOST) && !sPlayerbotAIConfig.allowSummonWhenBotIsDead) { - botAI->TellError("You cannot summon me while I'm dead, you need to release my spirit first"); + botAI->TellError("Нельзя призвать меня до воскрешения. Сначала освободите мой дух"); return false; } @@ -199,7 +199,7 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras { bot->ResurrectPlayer(1.0f, false); bot->SpawnCorpseBones(); - botAI->TellMasterNoFacing("I live, again!"); + botAI->TellMasterNoFacing("Я снова жив!"); botAI->GetAiObjectContext()->GetValue("prioritized targets")->Reset(); } @@ -229,6 +229,6 @@ bool SummonAction::Teleport(Player* summoner, Player* player, bool preserveAuras } if (summoner != player) - botAI->TellError("Not enough place to summon"); + botAI->TellError("Недостаточно места для призыва"); return false; } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp index 8f83b6e48..fdb364752 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WhoAction.cpp @@ -54,7 +54,7 @@ bool WhoAction::Execute(Event event) if (!out.str().empty()) out << ", "; - out << "playing with " << botAI->GetMaster()->GetName(); + out << "играет вместе с: " << botAI->GetMaster()->GetName(); } std::string const tell = out.str(); @@ -78,7 +78,7 @@ std::string const WhoAction::QueryTrade(std::string const text) if (!sellPrice) continue; - out << "Selling " << chat->FormatItem(sell->GetTemplate(), sell->GetCount()) << " for " + out << "Продаю: " << chat->FormatItem(sell->GetTemplate(), sell->GetCount()) << " за " << chat->formatMoney(sellPrice); return out.str(); } diff --git a/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp b/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp index 950ab7d05..bc215f5f4 100644 --- a/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Actions/WtsAction.cpp @@ -50,7 +50,7 @@ bool WtsAction::Execute(Event event) continue; std::ostringstream tell; - tell << "I'll buy " << chat->FormatItem(proto) << " for " << chat->formatMoney(buyPrice); + tell << "Я куплю: " << chat->FormatItem(proto) << " за " << chat->formatMoney(buyPrice); // ignore random bot chat filter bot->Whisper(tell.str(), LANG_UNIVERSAL, owner); diff --git a/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp b/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp index 81d9b285c..280bb5706 100644 --- a/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp +++ b/modules/mod-playerbots/src/Ai/Base/Value/Stances.cpp @@ -255,7 +255,7 @@ bool SetStanceAction::Execute(Event event) std::ostringstream str; str << "Invalid stance: |cffff0000" << stance; botAI->TellMaster(str); - botAI->TellMaster("Please set to any of:|cffffffff near (default), tank, turnback, behind"); + botAI->TellMaster("Выберите положение:|cffffffff near (по умолчанию), tank, turnback или behind"); return false; } diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 6ccb84548..6102797a9 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -47,10 +47,10 @@ bool StartRpgDoQuestAction::Execute(Event event) if (quest) { botAI->rpgInfo.ChangeToDoQuest(questId, quest); - bot->Whisper("Start to do quest " + std::to_string(questId), LANG_UNIVERSAL, owner); + bot->Whisper("Начинаю выполнять задание: " + std::to_string(questId), LANG_UNIVERSAL, owner); return true; } - bot->Whisper("Invalid quest " + text, LANG_UNIVERSAL, owner); + bot->Whisper("Недопустимое задание: " + text, LANG_UNIVERSAL, owner); return false; } diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index bd27bece5..3e1cea014 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -238,7 +238,7 @@ bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid) { AcceptQuest(quest, guid); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Quest accepted " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing("Задание принято: " + ChatHelper::FormatQuest(quest)); BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest); botAI->rpgStatistic.questAccepted++; LOG_DEBUG("playerbots", "[New RPG] {} accept quest {}", bot->GetName(), quest->GetQuestId()); @@ -247,7 +247,7 @@ bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid) { TurnInQuest(quest, guid); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Quest rewarded " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing("Награда за задание получена: " + ChatHelper::FormatQuest(quest)); BroadcastHelper::BroadcastQuestTurnedIn(botAI, bot, quest); botAI->rpgStatistic.questRewarded++; LOG_DEBUG("playerbots", "[New RPG] {} turned in quest {}", bot->GetName(), quest->GetQuestId()); @@ -539,7 +539,7 @@ bool NewRpgBaseAction::OrganizeQuestLog() packet << (uint8)i; bot->GetSession()->HandleQuestLogRemoveQuest(packet); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing("Задание отменено: " + ChatHelper::FormatQuest(quest)); botAI->rpgStatistic.questDropped++; dropped++; } @@ -564,7 +564,7 @@ bool NewRpgBaseAction::OrganizeQuestLog() packet << (uint8)i; bot->GetSession()->HandleQuestLogRemoveQuest(packet); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing("Задание отменено: " + ChatHelper::FormatQuest(quest)); botAI->rpgStatistic.questDropped++; dropped++; } @@ -586,7 +586,7 @@ bool NewRpgBaseAction::OrganizeQuestLog() packet << (uint8)i; bot->GetSession()->HandleQuestLogRemoveQuest(packet); if (botAI->GetMaster()) - botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest)); + botAI->TellMasterNoFacing("Задание отменено: " + ChatHelper::FormatQuest(quest)); botAI->rpgStatistic.questDropped++; } diff --git a/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp b/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp index def0d472d..323ff4b80 100644 --- a/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/modules/mod-playerbots/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -95,7 +95,7 @@ NewRpgStatus NewRpgInfo::GetStatus() std::string NewRpgInfo::ToString() { std::stringstream out; - out << "Status: "; + out << "Состояние: "; std::visit([&out, this](auto&& arg) { using T = std::decay_t; diff --git a/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp b/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp index bf2887470..68ac1923a 100644 --- a/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp +++ b/modules/mod-playerbots/src/Bot/Cmd/ChatHelper.cpp @@ -410,7 +410,7 @@ std::string const ChatHelper::FormatWorldEntry(int32 entry) else if (entry > 0 && cInfo) out << cInfo->Name; else - out << "unknown"; + out << "неизвестная"; out << "]|h|r"; return out.str(); diff --git a/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp b/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp index 6c5675e05..af4c1f2c4 100644 --- a/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp +++ b/modules/mod-playerbots/src/Bot/PlayerbotAI.cpp @@ -420,7 +420,7 @@ void PlayerbotAI::UpdateAIGroupMaster() botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT); if (botAI->GetMaster() == botAI->GetGroupLeader()) - botAI->TellMaster("Hello, I follow you!"); + botAI->TellMaster("Привет! Я иду за тобой."); else botAI->TellMaster(!urand(0, 2) ? "Hello!" : "Hi!"); } @@ -824,7 +824,7 @@ void PlayerbotAI::Reset(bool full) { WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); bot->GetSession()->HandleLogoutCancelOpcode(data); - TellMaster("Logout cancelled!"); + TellMaster("Выход из игры отменён!"); } currentEngine = engines[BOT_STATE_NON_COMBAT]; @@ -1018,7 +1018,7 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro if (!bot->GetSession()->isLogingOut()) { if (type == CHAT_MSG_WHISPER) - TellMaster("I'm logging out!"); + TellMaster("Я выхожу из игры!"); PlayerbotMgr* masterBotMgr = nullptr; if (master) @@ -1032,7 +1032,7 @@ void PlayerbotAI::HandleCommand(uint32 type, std::string const text, Player* fro if (bot->GetSession()->isLogingOut()) { if (type == CHAT_MSG_WHISPER) - TellMaster("Logout cancelled!"); + TellMaster("Выход из игры отменён!"); WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); bot->GetSession()->HandleLogoutCancelOpcode(data); @@ -1632,7 +1632,7 @@ void PlayerbotAI::ApplyInstanceStrategies(uint32 mapId, bool tellMaster) if (tellMaster && !strategyName.empty()) { std::ostringstream out; - out << "Added " << strategyName << " instance strategy"; + out << "Добавлена: " << strategyName << " — стратегия подземелья"; TellMasterNoFacing(out.str()); } } @@ -3610,51 +3610,51 @@ bool PlayerbotAI::CastSpell(uint32 spellId, Unit* target, Item* itemTarget) if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; - out << "Spell cast failed - "; - out << "Spell ID: " << spellId << " (" << ChatHelper::FormatSpell(spellInfo) << "), "; - out << "Error Code: " << static_cast(result) << " (0x" << std::hex << static_cast(result) + out << "Не удалось применить заклинание — "; + out << "ID заклинания: " << spellId << " (" << ChatHelper::FormatSpell(spellInfo) << "), "; + out << "Код ошибки: " << static_cast(result) << " (0x" << std::hex << static_cast(result) << std::dec << "), "; - out << "Bot: " << bot->GetName() << ", "; + out << "Бот: " << bot->GetName() << ", "; // Check spell target type if (targets.GetUnitTarget()) { - out << "Target: Unit (" << targets.GetUnitTarget()->GetName() + out << "Цель: существо (" << targets.GetUnitTarget()->GetName() << ", Low GUID: " << targets.GetUnitTarget()->GetGUID().GetCounter() << ", High GUID: " << static_cast(targets.GetUnitTarget()->GetGUID().GetHigh()) << "), "; } if (targets.GetGOTarget()) { - out << "Target: GameObject (Low GUID: " << targets.GetGOTarget()->GetGUID().GetCounter() + out << "Цель: игровой объект (младший GUID: " << targets.GetGOTarget()->GetGUID().GetCounter() << ", High GUID: " << static_cast(targets.GetGOTarget()->GetGUID().GetHigh()) << "), "; } if (targets.GetItemTarget()) { - out << "Target: Item (Low GUID: " << targets.GetItemTarget()->GetGUID().GetCounter() + out << "Цель: предмет (младший GUID: " << targets.GetItemTarget()->GetGUID().GetCounter() << ", High GUID: " << static_cast(targets.GetItemTarget()->GetGUID().GetHigh()) << "), "; } // Check if bot is in trade mode if (bot->GetTradeData()) { - out << "Trade Mode: Active, "; + out << "Режим обмена: активен, "; Item* tradeItem = bot->GetTradeData()->GetTraderData()->GetItem(TRADE_SLOT_NONTRADED); if (tradeItem) { - out << "Trade Item: " << tradeItem->GetEntry() + out << "Предмет обмена: " << tradeItem->GetEntry() << " (Low GUID: " << tradeItem->GetGUID().GetCounter() << ", High GUID: " << static_cast(tradeItem->GetGUID().GetHigh()) << "), "; } else { - out << "Trade Item: None, "; + out << "Предмет обмена: отсутствует, "; } } else { - out << "Trade Mode: Inactive, "; + out << "Режим обмена: неактивен, "; } TellMasterNoFacing(out); @@ -3691,7 +3691,7 @@ bool PlayerbotAI::CastSpell(uint32 spellId, Unit* target, Item* itemTarget) if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; - out << "Casting " << ChatHelper::FormatSpell(spellInfo); + out << "Применяю: " << ChatHelper::FormatSpell(spellInfo); TellMasterNoFacing(out); } @@ -3820,7 +3820,7 @@ bool PlayerbotAI::CastSpell(uint32 spellId, float x, float y, float z, Item* ite if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; - out << "Casting " << ChatHelper::FormatSpell(spellInfo); + out << "Применяю: " << ChatHelper::FormatSpell(spellInfo); TellMasterNoFacing(out); } @@ -4039,7 +4039,7 @@ bool PlayerbotAI::CastVehicleSpell(uint32 spellId, Unit* target) if (HasStrategy("debug spell", BOT_STATE_NON_COMBAT)) { std::ostringstream out; - out << "Casting Vehicle Spell" << ChatHelper::FormatSpell(spellInfo); + out << "Применяю способность транспорта: " << ChatHelper::FormatSpell(spellInfo); TellMasterNoFacing(out); } @@ -5243,7 +5243,7 @@ std::string const PlayerbotAI::HandleRemoteCommand(std::string const command) TravelTarget* target = GetAiObjectContext()->GetValue("travel target")->Get(); if (target->getDestination()) { - out << "Destination = " << target->getDestination()->getName(); + out << "Место назначения: " << target->getDestination()->getName(); out << ": " << target->getDestination()->getTitle(); @@ -5285,9 +5285,9 @@ std::string const PlayerbotAI::HandleRemoteCommand(std::string const command) AiObjectContext* context = GetAiObjectContext(); - out << "Current money: " << ChatHelper::formatMoney(bot->GetMoney()) << " free to use:" + out << "Текущие деньги: " << ChatHelper::formatMoney(bot->GetMoney()) << "; доступно:" << ChatHelper::formatMoney(AI_VALUE2(uint32, "free money for", (uint32)NeedMoneyFor::anything)) << "\n"; - out << "Purpose | Available / Needed \n"; + out << "Назначение | Доступно / Требуется \\n"; for (uint32 i = 1; i < (uint32)NeedMoneyFor::anything; i++) { @@ -5296,28 +5296,28 @@ std::string const PlayerbotAI::HandleRemoteCommand(std::string const command) switch (needMoneyFor) { case NeedMoneyFor::none: - out << "nothing"; + out << "без назначения"; break; case NeedMoneyFor::repair: - out << "repair"; + out << "ремонт"; break; case NeedMoneyFor::ammo: - out << "ammo"; + out << "боеприпасы"; break; case NeedMoneyFor::spells: - out << "spells"; + out << "заклинания"; break; case NeedMoneyFor::travel: - out << "travel"; + out << "путешествия"; break; case NeedMoneyFor::consumables: - out << "consumables"; + out << "расходуемые предметы"; break; case NeedMoneyFor::gear: - out << "gear"; + out << "экипировка"; break; case NeedMoneyFor::guild: - out << "guild"; + out << "гильдия"; break; default: break; @@ -5331,7 +5331,7 @@ std::string const PlayerbotAI::HandleRemoteCommand(std::string const command) } std::ostringstream out; - out << "invalid command: " << command; + out << "неверная команда: " << command; return out.str(); } diff --git a/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp b/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp index 1db6affd2..75272eb9b 100644 --- a/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp +++ b/modules/mod-playerbots/src/Bot/PlayerbotMgr.cpp @@ -112,7 +112,7 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId if (!isRndbot && !sameAccount && !sameGuild && !addClassBot && !linkedAccount) { allowed = false; - out << "Failure: You are not allowed to control bot " << botName.c_str(); + out << "Ошибка: вам запрещено управлять ботом " << botName.c_str(); } if (masterAccountId && masterPlayer) { @@ -126,7 +126,7 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId if (count >= PlayerbotAIConfig::instance().maxAddedBots) { allowed = false; - out << "Failure: You have added too many bots (more than " << sPlayerbotAIConfig.maxAddedBots << ")"; + out << "Ошибка: добавлено слишком много ботов (больше " << sPlayerbotAIConfig.maxAddedBots << ")"; } } if (!allowed) @@ -315,7 +315,7 @@ void PlayerbotMgr::CancelLogout() { WorldPackets::Character::LogoutCancel data = WorldPacket(CMSG_LOGOUT_CANCEL); bot->GetSession()->HandleLogoutCancelOpcode(data); - botAI->TellMaster("Logout cancelled!"); + botAI->TellMaster("Выход из игры отменён!"); } } @@ -402,7 +402,7 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) return; else if (bot) { - botAI->TellMaster("I'm logging out!"); + botAI->TellMaster("Я выхожу из игры!"); WorldPackets::Character::LogoutRequest data = WorldPacket(CMSG_LOGOUT_REQUEST); botWorldSessionPtr->HandleLogoutRequestOpcode(data); if (!bot) @@ -425,7 +425,7 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) } // if instant logout possible, do it else if (bot && (logout || !botWorldSessionPtr->isLogingOut())) { - botAI->TellMaster("Goodbye!"); + botAI->TellMaster("До свидания!"); RemoveFromPlayerbotsMap(guid); // deletes bot player ptr inside this WorldSession PlayerBotMap botWorldSessionPtr->LogoutPlayer(true); // this will delete the bot Player object and PlayerbotAI object delete botWorldSessionPtr; // finally delete the bot's WorldSession @@ -442,7 +442,7 @@ void PlayerbotHolder::DisablePlayerBot(ObjectGuid guid) { return; } - botAI->TellMaster("Goodbye!"); + botAI->TellMaster("До свидания!"); bot->StopMoving(); bot->GetMotionMaster()->Clear(); @@ -575,7 +575,7 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) // set delay on login botAI->SetNextCheckDelay(urand(2000, 4000)); - botAI->TellMaster("Hello!", PLAYERBOT_SECURITY_TALK); + botAI->TellMaster("Здравствуйте!", PLAYERBOT_SECURITY_TALK); // Queue group operations for world thread if (master && master->GetGroup() && !group) @@ -896,14 +896,14 @@ bool PlayerbotMgr::HandlePlayerbotMgrCommand(ChatHandler* handler, char const* a { if (!sPlayerbotAIConfig.enabled) { - handler->PSendSysMessage("|cffff0000Playerbot system is currently disabled!"); + handler->PSendSysMessage("|cffff0000Система Playerbot сейчас отключена!"); return false; } WorldSession* m_session = handler->GetSession(); if (!m_session) { - handler->PSendSysMessage("You may only add bots from an active session"); + handler->PSendSysMessage("Добавлять ботов можно только из активной игровой сессии"); return false; } @@ -911,7 +911,7 @@ bool PlayerbotMgr::HandlePlayerbotMgrCommand(ChatHandler* handler, char const* a PlayerbotMgr* mgr = GET_PLAYERBOT_MGR(player); if (!mgr) { - handler->PSendSysMessage("You cannot control bots yet"); + handler->PSendSysMessage("Вы пока не можете управлять ботами"); return false; } @@ -1330,7 +1330,7 @@ std::vector PlayerbotHolder::HandlePlayerbotCommand(char const* arg ObjectGuid member = sCharacterCache->GetCharacterGuidByName(bot); if (!member) { - out << "character not found"; + out << "персонаж не найден"; } else if (master && member != master->GetGUID()) { @@ -1445,7 +1445,7 @@ std::string const PlayerbotHolder::ListBots(Player* master) std::ostringstream out; bool first = true; - out << "Bot roster: "; + out << "Список ботов: "; for (std::vector::iterator i = names.begin(); i != names.end(); ++i) { if (first) @@ -1837,7 +1837,7 @@ void PlayerbotMgr::HandleSetSecurityKeyCommand(Player* player, const std::string "REPLACE INTO playerbots_account_keys (account_id, security_key) VALUES ({}, '{}')", accountId, hashedKey.str()); - ChatHandler(player->GetSession()).PSendSysMessage("Security key set successfully."); + ChatHandler(player->GetSession()).PSendSysMessage("Ключ безопасности успешно установлен."); } void PlayerbotMgr::HandleLinkAccountCommand(Player* player, const std::string& accountName, const std::string& key) @@ -1845,7 +1845,7 @@ void PlayerbotMgr::HandleLinkAccountCommand(Player* player, const std::string& a QueryResult result = LoginDatabase.Query("SELECT id FROM account WHERE username = '{}'", accountName); if (!result) { - ChatHandler(player->GetSession()).PSendSysMessage("Account not found."); + ChatHandler(player->GetSession()).PSendSysMessage("Учётная запись не найдена."); return; } @@ -1855,7 +1855,7 @@ void PlayerbotMgr::HandleLinkAccountCommand(Player* player, const std::string& a result = PlayerbotsDatabase.Query("SELECT security_key FROM playerbots_account_keys WHERE account_id = {}", linkedAccountId); if (!result) { - ChatHandler(player->GetSession()).PSendSysMessage("Invalid security key."); + ChatHandler(player->GetSession()).PSendSysMessage("Неверный ключ безопасности."); return; } @@ -1872,7 +1872,7 @@ void PlayerbotMgr::HandleLinkAccountCommand(Player* player, const std::string& a std::string storedKey = result->Fetch()->Get(); if (hashedKey.str() != storedKey) { - ChatHandler(player->GetSession()).PSendSysMessage("Invalid security key."); + ChatHandler(player->GetSession()).PSendSysMessage("Неверный ключ безопасности."); return; } @@ -1884,7 +1884,7 @@ void PlayerbotMgr::HandleLinkAccountCommand(Player* player, const std::string& a "INSERT IGNORE INTO playerbots_account_links (account_id, linked_account_id) VALUES ({}, {})", linkedAccountId, accountId); - ChatHandler(player->GetSession()).PSendSysMessage("Account linked successfully."); + ChatHandler(player->GetSession()).PSendSysMessage("Учётная запись успешно привязана."); } void PlayerbotMgr::HandleViewLinkedAccountsCommand(Player* player) @@ -1894,11 +1894,11 @@ void PlayerbotMgr::HandleViewLinkedAccountsCommand(Player* player) if (!result) { - ChatHandler(player->GetSession()).PSendSysMessage("No linked accounts."); + ChatHandler(player->GetSession()).PSendSysMessage("Нет привязанных учётных записей."); return; } - ChatHandler(player->GetSession()).PSendSysMessage("Linked accounts:"); + ChatHandler(player->GetSession()).PSendSysMessage("Привязанные учётные записи:"); do { Field* fields = result->Fetch(); @@ -1913,7 +1913,7 @@ void PlayerbotMgr::HandleViewLinkedAccountsCommand(Player* player) } else { - ChatHandler(player->GetSession()).PSendSysMessage("- Unknown account"); + ChatHandler(player->GetSession()).PSendSysMessage("- Неизвестная учётная запись"); } } while (result->NextRow()); } @@ -1923,7 +1923,7 @@ void PlayerbotMgr::HandleUnlinkAccountCommand(Player* player, const std::string& QueryResult result = LoginDatabase.Query("SELECT id FROM account WHERE username = '{}'", accountName); if (!result) { - ChatHandler(player->GetSession()).PSendSysMessage("Account not found."); + ChatHandler(player->GetSession()).PSendSysMessage("Учётная запись не найдена."); return; } @@ -1934,5 +1934,5 @@ void PlayerbotMgr::HandleUnlinkAccountCommand(Player* player, const std::string& PlayerbotsDatabase.Execute("DELETE FROM playerbots_account_links WHERE (account_id = {} AND linked_account_id = {}) OR (account_id = {} AND linked_account_id = {})", accountId, linkedAccountId, linkedAccountId, accountId); - ChatHandler(player->GetSession()).PSendSysMessage("Account unlinked successfully."); + ChatHandler(player->GetSession()).PSendSysMessage("Учётная запись успешно отвязана."); } diff --git a/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp b/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp index 412c27a96..e40d53d0c 100644 --- a/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp +++ b/modules/mod-playerbots/src/Bot/RandomPlayerbotMgr.cpp @@ -3037,7 +3037,7 @@ void RandomPlayerbotMgr::OnPlayerLogin(Player* player) { botAI->SetMaster(player); botAI->ResetStrategies(); - botAI->TellMaster("Hello"); + botAI->TellMaster("Здравствуйте."); } break; @@ -3414,7 +3414,7 @@ std::string const RandomPlayerbotMgr::HandleRemoteCommand(std::string const requ if (pos == request.end()) { std::ostringstream out; - out << "invalid request: " << request; + out << "неверный запрос: " << request; return out.str(); } diff --git a/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp b/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp index a7a073952..ac8164ae5 100644 --- a/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp +++ b/modules/mod-playerbots/src/Script/PlayerbotCommandScript.cpp @@ -115,7 +115,7 @@ public: { if (!args || !*args) { - handler->PSendSysMessage("Usage: .playerbots account setKey "); + handler->PSendSysMessage("Использование: .playerbots account setKey <ключ безопасности>"); return false; } @@ -130,7 +130,7 @@ public: } else { - handler->PSendSysMessage("PlayerbotMgr instance not found."); + handler->PSendSysMessage("Экземпляр PlayerbotMgr не найден."); return false; } } @@ -145,7 +145,7 @@ public: if (!accountName || !key) { - handler->PSendSysMessage("Usage: .playerbots account link "); + handler->PSendSysMessage("Использование: .playerbots account link <имя учётной записи> <ключ безопасности>"); return false; } @@ -159,7 +159,7 @@ public: } else { - handler->PSendSysMessage("PlayerbotMgr instance not found."); + handler->PSendSysMessage("Экземпляр PlayerbotMgr не найден."); return false; } } @@ -176,7 +176,7 @@ public: } else { - handler->PSendSysMessage("PlayerbotMgr instance not found."); + handler->PSendSysMessage("Экземпляр PlayerbotMgr не найден."); return false; } } @@ -189,7 +189,7 @@ public: char* accountName = strtok((char*)args, " "); if (!accountName) { - handler->PSendSysMessage("Usage: .playerbots account unlink "); + handler->PSendSysMessage("Использование: .playerbots account unlink <имя учётной записи>"); return false; } @@ -203,7 +203,7 @@ public: } else { - handler->PSendSysMessage("PlayerbotMgr instance not found."); + handler->PSendSysMessage("Экземпляр PlayerbotMgr не найден."); return false; } } diff --git a/modules/mod-playerbots/src/Script/Playerbots.cpp b/modules/mod-playerbots/src/Script/Playerbots.cpp index 3a17b13b7..c385ee320 100644 --- a/modules/mod-playerbots/src/Script/Playerbots.cpp +++ b/modules/mod-playerbots/src/Script/Playerbots.cpp @@ -235,6 +235,13 @@ public: bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Channel* channel) override { + // mod-playerbots-ai owns public-channel messages while enabled. Running + // the legacy parser too creates an unrelated RandomBotTalk response for + // the same natural-language message. Whisper, party, raid, and guild + // command handling use separate hooks and remain unchanged. + if (sConfigMgr->GetOption("PlayerbotsAI.Enabled", false)) + return true; + PlayerbotMgr* const playerbotMgr = PlayerbotsMgr::instance().GetPlayerbotMgr(player); if (playerbotMgr != nullptr && channel->GetFlags() & 0x18) diff --git a/playerbots-hardcoded-phrases-ru.tsv b/playerbots-hardcoded-phrases-ru.tsv new file mode 100644 index 000000000..a91b35360 --- /dev/null +++ b/playerbots-hardcoded-phrases-ru.tsv @@ -0,0 +1,393 @@ +original_en translation_ru +I'm logging out! Я выхожу из игры! +|cffff0000Playerbot system is currently disabled! |cffff0000Система Playerbot сейчас отключена! +You may only add bots from an active session Добавлять ботов можно только из активной игровой сессии +You cannot control bots yet Вы пока не можете управлять ботами +Security key set successfully. Ключ безопасности успешно установлен. +Account not found. Учётная запись не найдена. +Invalid security key. Неверный ключ безопасности. +Account linked successfully. Учётная запись успешно привязана. +No linked accounts. Нет привязанных учётных записей. +Linked accounts: Привязанные учётные записи: +- Unknown account - Неизвестная учётная запись +Account unlinked successfully. Учётная запись успешно отвязана. +Usage: .playerbots account setKey Использование: .playerbots account setKey <ключ безопасности> +PlayerbotMgr instance not found. Экземпляр PlayerbotMgr не найден. +Usage: .playerbots account link Использование: .playerbots account link <имя учётной записи> <ключ безопасности> +Usage: .playerbots account unlink Использование: .playerbots account unlink <имя учётной записи> +Please set to any of:|cffffffff near (default), tank, turnback, behind Выберите положение:|cffffffff near (по умолчанию), tank, turnback или behind +Sorry, I am already in such team У меня уже есть такая команда +Sorry, I am in a guild already Я уже состою в гильдии +Sorry, I am invited to a guild already Меня уже пригласили в гильдию +Sorry, I already signed this pettition Я уже подписал эту хартию +Thanks for the invite! Спасибо за приглашение! +Cannot find any portal to teleport Не удалось найти портал для телепортации +I will not craft anything Я ничего не буду создавать +Usage: 'craft [itemId]' or 'craft reset' Использование: «craft [ID предмета]» или «craft reset» +I cannot craft this Я не могу это создать +Welcome back! С возвращением! +I am not a spirit Я не дух +AI was reset to defaults Настройки ИИ сброшены +Quest shared Задание передано группе +--- Auras --- --- Ауры --- +Info of Aura - name: Сведения об ауре — название: +Info of DynamicObject - Сведения о динамическом объекте — +Estimated Group DPS: Расчётный урон группы в секунду: +I am in combat Я в бою +Welcome! Добро пожаловать! +You cannot summon me while I'm on a vehicle Нельзя призвать меня, пока я нахожусь в транспорте +You cannot summon me while you're in combat Нельзя призвать меня, пока вы в бою +You cannot summon me while you're dead Нельзя призвать меня, пока вы мертвы +You cannot summon me while I'm dead, you need to release my spirit first Нельзя призвать меня до воскрешения. Сначала освободите мой дух +I live, again! Я снова жив! +Not enough place to summon Недостаточно места для призыва +Usage: flag cloak/helm/pvp on/set/off/clear/toggle/? Использование: flag cloak/helm/pvp on/set/off/clear/toggle/? +No glyphs equipped Символы не установлены +Glyphs: Символы: +I heading to your position. Я направляюсь к вам. +I am traveling to your position. Я иду к вам. +Command can only be used from an active session Команду можно использовать только из активной игровой сессии +Repositioning to exit the LoS target! Меняю позицию, чтобы выйти из поля зрения цели! +I can't unlock this item. Я не могу открыть этот предмет. +Whisper position ?/set/reset Напишите: position <имя> ?/set/reset +The stay position is too far to return. I am going to stay where I am now Заданная точка слишком далеко. Останусь на текущем месте +You are not in a guild! Вы не состоите в гильдии! +Sorry, I don't want to join your guild :( Я не хочу вступать в вашу гильдию :( +Ignored spell list is empty Список игнорируемых заклинаний пуст +Unknown spell Неизвестное заклинание +=== Inventory === === Инвентарь === +Cannot find any npc to repair at Не удалось найти персонажа для ремонта +Destination Точка назначения +Quest Задание +Node Узел +Node can not be removed. Не удалось удалить узел. +Node removed. Узел удалён. +Cannot talk to quest giver Не удалось поговорить с персонажем, выдающим задание +No spells can be learned from this trainer У этого тренера нечему учиться +maintenance command is not allowed, please check the configuration. Команда maintenance отключена. Проверьте настройки. +I'm maintaining Выполняю обслуживание +autogear command is not allowed, please check the configuration. Команда autogear отключена. Проверьте настройки. +You cannot use autogear on alt bots. Нельзя использовать autogear для ботов альтернативных персонажей. +I'm auto gearing Автоматически подбираю экипировку +Sorry, I am happy in my guild :) Меня устраивает моя гильдия :) +There is no mailbox nearby Поблизости нет почтового ящика +You can not request more than one item Можно запросить только один предмет +I cannot send money Я не могу отправить деньги +I don't have enough money У меня недостаточно денег +Wait for me! Подождите меня! +This inn is my new home Эта таверна теперь мой дом +Can't find any innkeeper around Поблизости нет хозяина таверны +Quest [ Задание [ +Quest completed Задание выполнено: +Failed timer for Не удалось запустить таймер задания: +Fine.. I'll stay right here.. Хорошо... Останусь здесь... +Wherever you go, I'll follow.. Куда бы вы ни пошли, я последую за вами... +I'm just waiting for the ominous music now... Теперь осталось дождаться зловещей музыки... +Noooooooo.. you just couldn't ask for directions, huh? Неееет... Нельзя было просто спросить дорогу? +Oh boy.. another genius idea... Ох... Ещё одна гениальная идея... +No thanks.. I had some back at the last village Нет, спасибо... В прошлой деревне уже хватило +Do I look like a dog to you?! Я похож на собаку?! +Keep doing that and I'll roll those eyes right out of your head.. Продолжайте — и глаза скоро закатятся навсегда... +Lovely... just lovely.. Прекрасно... Просто прекрасно... +You think that's going to help you?! Думаете, это вам поможет?! +--- Attackers --- --- Атакующие --- +--- Threat --- --- Угроза --- +RTS control enabled. Управление RTS включено. +Aedm (Awesome energetic do move) spell trained. Заклинание Aedm для управления перемещением изучено. +RTS control spell removed. Заклинание управления RTS удалено. +Invalid tame id. Неверный ID приручения. +Pet changed and initialized! Питомец заменён и подготовлен! +I cannot use exotic pets unless I have the Beast Mastery talent. Я не могу использовать экзотических питомцев без таланта «Повелитель зверей». +No tameable pet found with name: Не найден приручаемый питомец с именем: +No tameable pet found with id: Не найден приручаемый питомец с ID: +No tameable pet found with family: Не найден приручаемый питомец семейства: +You have no pet to rename. У вас нет питомца для переименования. +Pet name must be between 1 and 12 alphabetic characters. Имя питомца должно содержать от 1 до 12 букв. +Pet name must only contain alphabetic characters (A-Z, a-z). Имя питомца должно состоять только из букв. +That pet name is forbidden. Please choose another name. Это имя питомца запрещено. Выберите другое. +Your pet has been renamed to Ваш питомец переименован в +If you do not see the new name, please dismiss and recall your pet. Если новое имя не появилось, отпустите и снова призовите питомца. +Only level 10+ hunters can have pets. Питомцы доступны охотникам не ниже 10-го уровня. +Creature template not found. Шаблон существа не найден. +Failed to create pet. Не удалось создать питомца. +Your pet has been abandoned. Питомец отпущен. +You have no hunter pet to abandon. У вас нет питомца охотника, которого можно отпустить. +I am ready for the next flight Можно отправляться в следующий полёт +=== Taxi === === Маршруты полётов === +I can't fly with you Я не могу лететь вместе с вами +Cannot find any flightmaster to talk Не удалось найти распорядителя полётов +All paths not in LOS Ни один путь не находится в поле зрения +I am stuck while following Я застрял во время следования +I will there soon. Я скоро буду там. +I am stuck while fleeing Я застрял при отступлении +Nowhere to flee Некуда отступать +Disable disperse Рассредоточение отключено +You can use this Можно использовать: +Start trade with Начинаю обмен с +You have no target У вас нет цели +I cannot attack in flight Я не могу атаковать в полёте +I have no target У меня нет цели +I cannot attack other players in PvP prohibited areas. Я не могу атаковать игроков в зонах, где запрещено PvP. +I am already attacking Я уже атакую: +I cannot attack an invalid target. Я не могу атаковать недопустимую цель. +Back from the grave! Возвращение из могилы! +You are too far away from me! I will there soon. Вы слишком далеко! Я скоро подойду. +Moving away from group Отхожу от группы +I will not flee with you - too far away Я не могу отступать вместе с вами — вы слишком далеко +Running away Отступаю +Max DPS! Максимальный урон! +Add Bwl Strategies! Стратегии Логова Крыла Тьмы включены! +Usage: glyph equip <6 glyph item IDs> (3 major, 3 minor). Использование: glyph equip <6 ID символов> (3 больших и 3 малых). +Not enought empty sockets for all glyphs. Недостаточно свободных ячеек для всех символов. +Glyphs updated. Символы обновлены. +Clearing travel target Цель путешествия сброшена +It is in water Цель находится в воде +I can't go there Я не могу туда пройти +Whisper 'go x,y', 'go [game object]', 'go unit' or 'go position' and I will go there Напишите «go x,y», «go [объект]», «go unit» или «go position», и я отправлюсь туда +There are no vendors nearby Поблизости нет торговцев +I dont see my rti attack target Я не вижу назначенную групповую цель для атаки +I will play with this loot type only if I'm in your guild :/ Я буду использовать этот тип добычи, только если состою в вашей гильдии :/ +No items (or game objects) available Нет доступных предметов или игровых объектов +Socket does not fit Предмет не подходит для этой ячейки +I am not dead, will wait here Мне не требуется воскрешение, подожду здесь +I am already a spirit Я уже дух +No event owner detected Не удалось определить владельца события +Clean Quest Log command received, removing grey/trivial quests... Получена команда очистки журнала: удаляю серые и незначительные задания... +Unknown gossip option Неизвестный вариант диалога +I need to talk first Сначала нужно поговорить +Cannot find banker nearby Поблизости нет банкира +=== Bank === === Банк === +No spells found. Заклинания не найдены. +=== Spells === === Заклинания === +I'm not in your guild! Я не состою в вашей гильдии! +Cannot find the guild bank nearby Поблизости нет банка гильдии +--- Incompleted quests --- --- Невыполненные задания --- +--- Completed quests --- --- Выполненные задания --- +--- Summary --- --- Итого --- +Quest template not found. Шаблон задания не найден. +You can change any strategy except loot and gather Можно изменять любую стратегию, кроме loot и gather +Usage: s gray/*/vendor/[item link] Использование: s gray/*/vendor/[ссылка на предмет] +You already have the maximum number of characters У вас уже максимальное количество персонажей +You cannot hire higher level characters than you Нельзя нанять персонажа выше вашего уровня +I will join you at your next relogin Я присоединюсь после вашего следующего входа в игру +I won't follow: too far away Я не последую за вами: вы слишком далеко +Wait for me Подождите меня +--- consumable --- --- Расходуемые предметы --- +--- container --- --- Сумки --- +--- weapon --- --- Оружие --- +--- armor --- --- Доспехи --- +--- reagent --- --- Реагенты --- +--- projectile --- --- Боеприпасы --- +--- trade goods --- --- Хозяйственные товары --- +--- recipe --- --- Рецепты --- +--- quiver --- --- Колчаны --- +--- quest items --- --- Предметы заданий --- +--- keys --- --- Ключи --- +--- other --- --- Прочее --- +Quest [ Задание [ +I have this quest У меня уже есть это задание +I can't take this quest Я не могу взять это задание +Out of ammo! Закончились боеприпасы! +No pet! Нет питомца! +Pet is unhappy! Питомец недоволен! +Item(s) removed from always loot list Предметы удалены из списка обязательного сбора +Item(s) added to always loot list Предметы добавлены в список обязательного сбора +outfit +[item] to add items outfit <имя> +[предмет] — добавить предметы +outfit -[item] to remove items outfit <имя> -[предмет] — удалить предметы +outfit equip/replace to equip items outfit <имя> equip/replace — надеть предметы +--- Elixir --- --- Эликсиры --- +--- Flask --- --- Настои --- +--- Scroll --- --- Свитки --- +--- Food --- --- Еда --- +--- Enchant --- --- Чары --- +Failed to cast Pick Lock. Не удалось применить «Взлом замка». +=== Custom strategies === === Пользовательские стратегии === +Usage: cs Использование: cs <имя> <индекс> <команда> +No where to travel. Idling a bit. Некуда идти. Немного подожду. +=== Mailbox === === Почта === +Not enough bag space В сумках недостаточно места +Pet autocast spells have been toggled. Автоматическое применение способностей питомца переключено. +Pet stance set to Режим питомца: +No item in the Do Not Trade slot. В ячейке «Не для обмена» нет предмета. +Cannot unlock this item. Этот предмет невозможно открыть. +Cannot unlock, Pick Lock spell is missing. Не удалось открыть: отсутствует навык «Взлом замка». +Quest accepted Задание принято: +Quest rewarded Награда за задание получена: +Quest dropped Задание отменено: +Start to do quest Начинаю выполнять задание: +Invalid quest Недопустимое задание: +Added Добавлена: + instance strategy — стратегия подземелья +Spell cast failed - Не удалось применить заклинание — +Spell ID: ID заклинания: +Error Code: Код ошибки: +Bot: Бот: +Target: Unit ( Цель: существо ( +Target: GameObject (Low GUID: Цель: игровой объект (младший GUID: +Target: Item (Low GUID: Цель: предмет (младший GUID: +Trade Mode: Active, Режим обмена: активен, +Trade Item: Предмет обмена: +Trade Item: None, Предмет обмена: отсутствует, +Trade Mode: Inactive, Режим обмена: неактивен, +Casting Применяю: +Casting Vehicle Spell Применяю способность транспорта: +Destination = Место назначения: +Current money: Текущие деньги: + free to use: ; доступно: +Purpose | Available / Needed \n Назначение | Доступно / Требуется \n +nothing без назначения +repair ремонт +ammo боеприпасы +spells заклинания +travel путешествия +consumables расходуемые предметы +gear экипировка +guild гильдия +invalid command: неверная команда: +Failure: You are not allowed to control bot Ошибка: вам запрещено управлять ботом +Failure: You have added too many bots (more than Ошибка: добавлено слишком много ботов (больше +character not found персонаж не найден +Bot roster: Список ботов: +invalid request: неверный запрос: +Status: Состояние: +Using portal: Использую портал: +Teleporting using Телепортируюсь через: +Waiting a bit for Немного подожду: +Please hurry up Пожалуйста, поторопитесь: +Calculated score of Расчётная ценность предмета +Lockpicking skill too low ( Навык взлома слишком низкий ( +) to unlock: ) для открытия: +Picking Lock on traded item: Открываю замок на переданном предмете: +Heading to: Направляюсь к цели: +equipping Надеваю: +Equipping Надеваю: + in ranged slot в ячейку оружия дальнего боя + in main hand в правую руку + in offhand в левую руку +I will craft Я создам: + using reagents: ; реагенты: +found: найдено: +Total cost: Общая стоимость: +Whisper any of: Напишите одну из команд: +Possible strategies (co/nc/dead commands): Доступные стратегии (команды co/nc/dead): +Following group Следую за группой: +Continuing Продолжаю: +Traveling Путешествую: +selling items продаю предметы +repairing ремонтируюсь +saved: сохранено: +Traveling to Направляюсь к: +bad quests: ошибочные задания: +Joining as Присоединяюсь в роли: +Attacking Атакую: +Quest: Задание: + confirm accept — подтвердите принятие +Used Pick Lock on: Замок открыт у предмета: +I'll buy Я куплю: + for за +Setting outfit Настраиваю комплект: + as как +Equipping outfit Надеваю комплект: +Replacing current equip with outfit Заменяю текущую экипировку комплектом: +Resetting outfit Сбрасываю комплект: +Updating with current items outfit Обновляю комплект текущими предметами: +Already completed Уже выполнено +Already on Уже выполняется +Can't take Невозможно взять +Quest log is full Журнал заданий заполнен +Bags are full Сумки заполнены +Accepted Принято: +Cannot accept Невозможно принять +Ignored spell list: Список игнорируемых заклинаний: +Learned spells: Изученные заклинания: +not ready ещё не готово +requires spell focus требуется объект силы +cannot cast here здесь применить нельзя +requires item требуется предмет +requires weapon требуется оружие +interrupted прервано +cannot cast невозможно применить +Position Позиция + is set установлена + is reset сброшена + is not set не установлена +I see a Вижу: +Let's look at it. Давайте посмотрим. +Let's fish a bit. Давайте немного порыбачим. +Should we go nearer? Подойти ближе? +Repair: Ремонт: +Sell: Продажа: +Buy: Покупка: +My log level set to Уровень журнала установлен: +My log level is Текущий уровень журнала: +Got Получено: + from от +Cannot get Не удалось получить: +Sending mail to Отправляю письмо: +Cannot send Не удалось отправить: +Sent mail to Письмо отправлено: +Opened item: Открыт предмет: +Total: Всего: + / 25 (incompleted: / 25 (невыполнено: +Nobody sells Никто не продаёт: + nearby поблизости +Buying Покупаю: +Rewarded Получена награда: +Unable to find suitable reward. Asking for help.... Не удалось подобрать подходящую награду. Нужна помощь... +Choose reward: Выберите награду: +Reward pending Ожидается выбор награды +Mana save level: Уровень экономии маны: +Mana save level set: Уровень экономии маны установлен: +Current chat is Текущий канал чата: +Chat set to Канал чата установлен: +Unknown spell Неизвестное заклинание: +Cannot cast Не удалось применить: +Cast Применение + is failed завершилось ошибкой +You cannot hire me - I barely know you. Make sure you have at least Вы не можете меня нанять: мы почти не знакомы. Требуется не менее + reputation репутации +Loot strategy: Стратегия добычи: +Always loot items: Всегда собирать: +Loot strategy set to Установлена стратегия добычи: +I am at Я нахожусь в точке +Moving to Двигаюсь к: +path is: путь: +Moving to position Двигаюсь к позиции: +cc2222hated cc2222Ненависть +ff0000hostile ff0000Враждебность +ee6622unfriendly ee6622Неприязнь +ffff00neutral ffff00Равнодушие +I'm avoiding Избегаю: + Radius ; радиус + - [Aura] — [аура] + - [Spell] — [заклинание] +Enable disperse distance Включено рассредоточение с дистанцией +Enable disperse first Сначала включите рассредоточение +Increase disperse distance to Дистанция рассредоточения увеличена до +Decrease disperse distance to Дистанция рассредоточения уменьшена до +Invalid disperse distance Недопустимая дистанция рассредоточения: +Set disperse distance to Дистанция рассредоточения установлена: +Usage: disperse [enable | disable | increase | decrease | set {distance}] Использование: disperse [enable | disable | increase | decrease | set {дистанция}] +got получено: + from bank из банка +put помещено: + to bank в банк +Selling Продаю: +Active first talent Активирован первый набор талантов +Active second talent Активирован второй набор талантов +Auto pick talents Автоматический выбор талантов включён +Unknown command. Неизвестная команда. +My current talent spec is: Моя текущая специализация: +Talents usage: talents switch <1/2>, talents autopick, talents spec list, Команды талантов: talents switch <1/2>, talents autopick, talents spec list, +Total Найдено: + specs found специализаций +Picking Выбираю: +Spec Специализация + not found не найдена +Invalid link Неверная ссылка: +Applying Применяю: +playing with играет вместе с: +Using Использую: +Got quest Получено задание: +Socketing Вставляю самоцвет в: +I can't put Я не могу положить: diff --git a/scripts/prod-deploy.sh b/scripts/prod-deploy.sh index 4bf5f7aaa..08717cf11 100644 --- a/scripts/prod-deploy.sh +++ b/scripts/prod-deploy.sh @@ -163,6 +163,7 @@ source "$ENV_FILE" set +a PLAYERBOTS_RUSSIAN_NAMES="${ACORE_PLAYERBOTS_RUSSIAN_NAMES:-1}" +PLAYERBOTS_AI_ENABLED="${ACORE_PLAYERBOTS_AI_ENABLED:-0}" cd "$ROOT_DIR" @@ -172,6 +173,15 @@ if ! [[ "$PLAYERBOTS_RUSSIAN_NAMES" =~ ^[01]$ ]]; then die "ACORE_PLAYERBOTS_RUSSIAN_NAMES must be 0 or 1, got: $PLAYERBOTS_RUSSIAN_NAMES" fi +if ! [[ "$PLAYERBOTS_AI_ENABLED" =~ ^[01]$ ]]; then + die "ACORE_PLAYERBOTS_AI_ENABLED must be 0 or 1, got: $PLAYERBOTS_AI_ENABLED" +fi + +if [[ "$PLAYERBOTS_AI_ENABLED" == "1" ]] && \ + { [[ -z "${YANDEX_AI_API_KEY:-}" ]] || [[ -z "${YANDEX_AI_FOLDER_ID:-}" ]]; }; then + die "Playerbots AI requires YANDEX_AI_API_KEY and YANDEX_AI_FOLDER_ID in $ENV_FILE" +fi + if [[ $EUID -eq 0 ]]; then apply_sysctl else @@ -222,6 +232,19 @@ bash "$ROOT_DIR/setup-modules.sh" log "initializing client data" run_one_shot_service "ac-client-data-init" "ac-client-data-init" +if [[ "$PLAYERBOTS_AI_ENABLED" == "1" ]]; then + log "starting playerbots AI bridge" + if [[ -n "$BUILD_FLAG" ]]; then + $COMPOSE up -d --build playerbots-ai + else + $COMPOSE up -d playerbots-ai + fi + if ! wait_for_healthy "playerbots-ai" 60; then + print_service_logs "playerbots-ai" + die "playerbots-ai did not become healthy" + fi +fi + log "starting authserver and worldserver" if [[ -n "$BUILD_FLAG" ]]; then $COMPOSE up -d --build ac-authserver ac-worldserver diff --git a/services/playerbots-ai/.dockerignore b/services/playerbots-ai/.dockerignore new file mode 100644 index 000000000..43ae0e2a6 --- /dev/null +++ b/services/playerbots-ai/.dockerignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/services/playerbots-ai/Dockerfile b/services/playerbots-ai/Dockerfile new file mode 100644 index 000000000..7c9e4bdb7 --- /dev/null +++ b/services/playerbots-ai/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-alpine + +WORKDIR /app +COPY app.py /app/app.py +RUN mkdir -p /data && chown nobody:nobody /data + +USER nobody +EXPOSE 8080 + +CMD ["python", "-u", "/app/app.py"] diff --git a/services/playerbots-ai/app.py b/services/playerbots-ai/app.py new file mode 100644 index 000000000..6a39904e0 --- /dev/null +++ b/services/playerbots-ai/app.py @@ -0,0 +1,843 @@ +#!/usr/bin/env python3 + +"""Internal MoonWell bridge between mod-playerbots-ai and Yandex AI Studio.""" + +from __future__ import annotations + +import json +import hashlib +import logging +import os +import re +import sqlite3 +import threading +import time +import urllib.error +import urllib.request +from collections import defaultdict, deque +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from typing import Any + + +LOG = logging.getLogger("playerbots-ai") +logging.basicConfig( + level=os.getenv("PLAYERBOTS_AI_LOG_LEVEL", "INFO").upper(), + format="%(asctime)s %(levelname)s %(message)s", +) + +HOST = os.getenv("PLAYERBOTS_AI_HOST", "0.0.0.0") +PORT = int(os.getenv("PLAYERBOTS_AI_PORT", "8080")) +YANDEX_API_KEY = os.getenv("YANDEX_AI_API_KEY", "").strip() +YANDEX_FOLDER_ID = os.getenv("YANDEX_AI_FOLDER_ID", "").strip() +YANDEX_BASE_URL = os.getenv( + "YANDEX_AI_BASE_URL", "https://ai.api.cloud.yandex.net/v1" +).rstrip("/") +YANDEX_MODEL_URI = os.getenv("YANDEX_AI_MODEL_URI", "").strip() +MODEL_URI = YANDEX_MODEL_URI or ( + f"gpt://{YANDEX_FOLDER_ID}/aliceai-llm-flash" if YANDEX_FOLDER_ID else "" +) +REQUEST_TIMEOUT = float(os.getenv("PLAYERBOTS_AI_REQUEST_TIMEOUT", "12")) +TEMPERATURE = float(os.getenv("PLAYERBOTS_AI_TEMPERATURE", "0.4")) +MAX_TOKENS = int(os.getenv("PLAYERBOTS_AI_MAX_TOKENS", "120")) +MAX_REPLY_CHARACTERS = int(os.getenv("PLAYERBOTS_AI_MAX_REPLY_CHARACTERS", "240")) +HISTORY_MESSAGES = max(0, int(os.getenv("PLAYERBOTS_AI_HISTORY_MESSAGES", "6"))) +LOG_API_RESPONSES = os.getenv("PLAYERBOTS_AI_LOG_API_RESPONSES", "1").strip().lower() in { + "1", + "true", + "yes", + "on", +} +COMMANDS_ENABLED = os.getenv("PLAYERBOTS_AI_COMMANDS_ENABLED", "1").strip().lower() in { + "1", + "true", + "yes", + "on", +} +CLASSIFIER_CACHE_ENABLED = os.getenv( + "PLAYERBOTS_AI_CLASSIFIER_CACHE_ENABLED", "1" +).strip().lower() in {"1", "true", "yes", "on"} +CLASSIFIER_CACHE_DB = os.getenv( + "PLAYERBOTS_AI_CLASSIFIER_CACHE_DB", "/data/classifier-cache.sqlite3" +) +CLASSIFIER_CACHE_TTL_SECONDS = max( + 60, int(os.getenv("PLAYERBOTS_AI_CLASSIFIER_CACHE_TTL_SECONDS", "604800")) +) +CLASSIFIER_CACHE_MAX_ENTRIES = max( + 100, int(os.getenv("PLAYERBOTS_AI_CLASSIFIER_CACHE_MAX_ENTRIES", "10000")) +) + +SAFE_EXACT_COMMANDS = { + "accept", + "accept *", + "add all loot", + "attack", + "attack rti target", + "autogear", + "buff", + "disperse disable", + "flee", + "focus heal ?", + "focus heal clear", + "focus heal none", + "focus heal unset", + "follow", + "give leader", + "glyphs", + "grind", + "help", + "home", + "inv", + "invite", + "items", + "leave", + "lfg", + "ll all", + "ll gray", + "ll normal", + "ll quest", + "ll skill", + "los", + "maintenance", + "open items", + "outfit ?", + "pet aggressive", + "pet attack", + "pet defensive", + "pet follow", + "pet passive", + "pet stance", + "pet stay", + "quests", + "quests all", + "quests completed", + "quests incompleted", + "ready", + "release", + "reset", + "reset botAI", + "revive", + "roll", + "rti", + "rtsc", + "rtsc cancel", + "rtsc go save", + "rtsc toggle", + "runaway", + "spells", + "ss reset", + "stats", + "stay", + "summon", + "t", + "talents", + "talents spec list", + "talk", + "tame", + "tame family", + "tank attack", + "trainer", + "trainer learn", + "who", +} + +SAFE_COMMAND_PREFIXES = ( + "accept ", + "b ", + "bank ", + "cast ", + "co ", + "disperse set ", + "e ", + "focus heal ", + "gb ", + "glyph equip ", + "lfg ", + "ll ", + "nc ", + "outfit ", + "pet ", + "q ", + "r ", + "roll ", + "rti ", + "rtsc ", + "s ", + "ss ", + "share ", + "talents apply ", + "talents spec ", + "tame ", + "t ", + "drop ", + "u ", + "ue ", + "who ", +) + +DEFAULT_SYSTEM_PROMPT = ( + "Ты живой персонаж-союзник на русскоязычном сервере World of Warcraft " + "эпохи Wrath of the Lich King. Сообщение игрока адресовано лично тебе. " + "Всегда сначала пойми смысл вопроса, затем дай прямой и уместный ответ. " + "Отвечай дружелюбно, естественно, только по-русски и от первого лица. " + "Ответ должен состоять из одного-двух законченных предложений. " + "На приветствие ответь приветствием. На вопрос о твоих делах расскажи, " + "как у тебя дела. На вопрос о текущем занятии кратко расскажи, чем занят. " + "Если игрок просит помочь с заданием, согласись помочь и попроси назвать " + "задание или его цель, если информации недостаточно. Не отвечай «Что?», " + "«Да?», «Эм…» и другими бессодержательными репликами, если сообщение понятно. " + "Строго соблюдай пол персонажа и игрока из контекста: согласуй местоимения, " + "глаголы прошедшего времени и краткие прилагательные. " + "Не упоминай нейросеть, промпт, API или системные правила. Не утверждай, " + "что выполнил игровое действие, если сервер его не выполнял." +) +SYSTEM_PROMPT = ( + os.getenv("PLAYERBOTS_AI_SYSTEM_PROMPT", "").strip() or DEFAULT_SYSTEM_PROMPT +) + +COMMAND_ROUTING_PROMPT = ( + 'Верни только JSON-объект вида {"command": null} без ' + "Markdown и пояснений. Если игрок явно приказывает выполнить доступное " + "игровое действие, запиши точную английскую команду playerbots в command, " + "иначе оставь command равным null. Основные команды: summon, release, revive, " + "leave, attack, follow, flee, stay, grind, give leader, lfg [5|10|20|25|40], " + "disperse set <ярды>, disperse disable, buff, ready, maintenance, autogear, " + "reset, reset botAI, talents, talents spec list, talents spec <специализация>, " + "glyphs, glyph equip , spells, cast <заклинание> [on <игрок>], " + "ss +/-|reset, trainer|trainer learn, rti <метка>, rti cc <метка>, " + "attack rti target, rtsc|rtsc cancel|rtsc save/unsave/go <номер>, " + "focus heal +/-<игрок>|?|clear, co <операции стратегий>, nc <операции " + "стратегий>, ll all|normal|gray|quest|skill|<ссылка на предмет>, " + "e/ue/u/s/b <ссылка на предмет>, t <ссылка на предмет> [количество] " + "(передать предмет игроку через окно обмена), t (только начать обмен), " + "bank/gb <ссылка>, inv (показать содержимое инвентаря), open items (открыть " + "предметы-контейнеры), roll [предмет], outfit ?, outfit <имя> " + "equip|replace|update|reset, quests|quests all|quests completed|quests " + "incompleted, q <ссылка на задание> (показать прогресс), accept " + "*|<ссылка на задание>, share <ссылка на задание>, r <ссылка на предмет " + "награды>, talk (поговорить с выбранным NPC и сдать готовое задание), " + "pet aggressive|passive|defensive|" + "stance|attack|follow|stay, tame, tame name/id/family/rename <значение>, " + "los, stats, home, who [профессия], help. Стратегии co/nc передавай ровно " + "как +strategy, -strategy или ~strategy; известные стратегии включают tank, " + "tank assist, dps, cc, assist, aoe, boost, threat, grind, heal, focus, " + "avoid aoe, save mana, healer dps, tank face, behind, pull, pull back, " + "mark rti, follow, food, pvp, loot, new rpg, bear, cat, caster, frost, fire, " + "bmight, bwisdom, bkings, bsanc, bdps, bspeed, bmana, imp, voidwalker, " + "succubus, felhunter, felguard, ss master, ss self, ss tank, ss healer, " + "moltencore, bwl, aq20, karazhan, gruulslair, magtheridon, ssc, voa, naxx, " + "wotlk-os, wotlk-eoe, ulduar, onyxia, icc. Сохраняй ссылки WoW, имена, " + "названия заклинаний и предметов из сообщения без изменений. Никогда не " + "создавай команды с точкой, playerbot/playerbots, account, debug, cdebug, " + "cheat, destroy, mail, sendmail, init или console. Команду drop <задание> " + "создавай только по явной просьбе отказаться, бросить или удалить конкретное " + "задание. Если это обычный " + "разговор, вопрос, приветствие или неоднозначное пожелание, command должен " + "быть null. Вопрос «можешь ли ты» сам по себе не является приказом. " + "Примеры точного преобразования: «иди за мной» → " + "follow; «стой здесь» → stay; «покажи инвентарь» → inv; «покажи " + "характеристики» → stats; «покажи заклинания» → spells; «включи режим " + "танка» → co +tank; «выключи АоЕ» → " + "co -aoe; «начни собирать добычу» → nc +loot; «перестань следовать» → " + "nc -follow; «сделай череп приоритетной целью» → rti skull; «назначь луну " + "целью контроля» → rti cc moon; «начни обмен» → t; «дай мне [предмет]» → " + "t [предмет]; «покажи все задания» → quests all; «покажи прогресс " + "[задания]» → q [задание]; «прими все задания» → accept *; «поделись " + "[заданием]» → share [задание]; «выбери [предмет] в награду» → r [предмет]; " + "«сдай готовое задание» → talk; «обслужи " + "экипировку и выучи доступные навыки» → maintenance." +) + +CLASS_NAMES = { + 1: "воин", + 2: "паладин", + 3: "охотник", + 4: "разбойник", + 5: "жрец", + 6: "рыцарь смерти", + 7: "шаман", + 8: "маг", + 9: "чернокнижник", + 11: "друид", +} + +RACE_NAMES = { + 1: "человек", + 2: "орк", + 3: "дворф", + 4: "ночной эльф", + 5: "нежить", + 6: "таурен", + 7: "гном", + 8: "тролль", + 10: "эльф крови", + 11: "дреней", +} + +history: dict[str, deque[dict[str, str]]] = defaultdict( + lambda: deque(maxlen=HISTORY_MESSAGES) +) +history_lock = threading.Lock() +cache_stats_lock = threading.Lock() +classifier_cache_hits = 0 +classifier_cache_misses = 0 +classifier_cache_available = CLASSIFIER_CACHE_ENABLED + + +def configured() -> bool: + return bool(YANDEX_API_KEY and YANDEX_FOLDER_ID and MODEL_URI) + + +def initialize_classifier_cache() -> None: + global classifier_cache_available + if not CLASSIFIER_CACHE_ENABLED: + return + try: + directory = os.path.dirname(CLASSIFIER_CACHE_DB) + if directory: + os.makedirs(directory, exist_ok=True) + with sqlite3.connect(CLASSIFIER_CACHE_DB, timeout=5) as database: + database.execute("PRAGMA journal_mode=WAL") + database.execute( + """ + CREATE TABLE IF NOT EXISTS classifier_cache ( + cache_key TEXT PRIMARY KEY, + command TEXT NOT NULL, + updated_at INTEGER NOT NULL + ) + """ + ) + except (OSError, sqlite3.Error) as error: + classifier_cache_available = False + LOG.error("classifier cache unavailable, continuing without it: %s", error) + + +def classifier_cache_key(payload: dict[str, Any]) -> str: + normalized = normalized_routing_text(payload) + prompt_version = hashlib.sha256(COMMAND_ROUTING_PROMPT.encode("utf-8")).hexdigest()[:16] + return f"{prompt_version}:{normalized}" + + +def get_cached_classifier_command(payload: dict[str, Any]) -> tuple[bool, str]: + global classifier_cache_hits, classifier_cache_misses + if not classifier_cache_available: + return False, "" + + cache_key = classifier_cache_key(payload) + cutoff = int(time.time()) - CLASSIFIER_CACHE_TTL_SECONDS + try: + with sqlite3.connect(CLASSIFIER_CACHE_DB, timeout=5) as database: + row = database.execute( + "SELECT command, updated_at FROM classifier_cache WHERE cache_key = ?", + (cache_key,), + ).fetchone() + if row and row[1] < cutoff: + database.execute( + "DELETE FROM classifier_cache WHERE cache_key = ?", (cache_key,) + ) + row = None + except sqlite3.Error as error: + LOG.error("classifier cache read failed, using API: %s", error) + return False, "" + + with cache_stats_lock: + if row: + classifier_cache_hits += 1 + else: + classifier_cache_misses += 1 + if not row: + return False, "" + + LOG.info( + "classifier cache hit: key=%s result=%s", + hashlib.sha256(cache_key.encode("utf-8")).hexdigest()[:12], + row[0] or "chat", + ) + return True, row[0] + + +def cache_classifier_command(payload: dict[str, Any], command: str) -> None: + if not classifier_cache_available: + return + + now = int(time.time()) + cutoff = now - CLASSIFIER_CACHE_TTL_SECONDS + try: + with sqlite3.connect(CLASSIFIER_CACHE_DB, timeout=5) as database: + database.execute( + """ + INSERT INTO classifier_cache(cache_key, command, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(cache_key) DO UPDATE SET + command = excluded.command, + updated_at = excluded.updated_at + """, + (classifier_cache_key(payload), command, now), + ) + database.execute( + "DELETE FROM classifier_cache WHERE updated_at < ?", (cutoff,) + ) + count = database.execute( + "SELECT COUNT(*) FROM classifier_cache" + ).fetchone()[0] + excess = count - CLASSIFIER_CACHE_MAX_ENTRIES + if excess > 0: + database.execute( + """ + DELETE FROM classifier_cache WHERE cache_key IN ( + SELECT cache_key FROM classifier_cache + ORDER BY updated_at ASC LIMIT ? + ) + """, + (excess,), + ) + except sqlite3.Error as error: + LOG.error("classifier cache write failed, continuing without cache: %s", error) + + +def classifier_cache_entry_count() -> int: + if not classifier_cache_available or not os.path.exists(CLASSIFIER_CACHE_DB): + return 0 + try: + with sqlite3.connect(CLASSIFIER_CACHE_DB, timeout=5) as database: + return database.execute( + "SELECT COUNT(*) FROM classifier_cache" + ).fetchone()[0] + except sqlite3.Error: + return 0 + + +def required_string(payload: dict[str, Any], key: str, max_length: int) -> str: + value = payload.get(key) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{key} must be a non-empty string") + return value.strip()[:max_length] + + +def integer(payload: dict[str, Any], key: str, default: int = 0) -> int: + value = payload.get(key, default) + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{key} must be an integer") + return value + + +def clean_reply(text: str) -> str: + compact = " ".join(text.replace("\r", " ").replace("\n", " ").split()) + if len(compact) > MAX_REPLY_CHARACTERS: + compact = compact[:MAX_REPLY_CHARACTERS].rstrip() + "…" + return compact + + +def validated_native_command(arguments: Any) -> str: + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError: + return "" + if not isinstance(arguments, dict): + return "" + command = arguments.get("command") + if not isinstance(command, str): + return "" + command = command.strip() + if ( + not command + or len(command) > 300 + or "\\" in command + or any(ord(character) < 0x20 for character in command) + ): + return "" + if command in SAFE_EXACT_COMMANDS: + return command + if any( + command.startswith(prefix) and len(command) > len(prefix) + for prefix in SAFE_COMMAND_PREFIXES + ): + return command + return "" + + +def normalized_command_text(text: str) -> str: + folded = text.lower().replace("ё", "е") + return " ".join( + "".join(character if character.isalnum() else " " for character in folded).split() + ) + + +def normalized_routing_text(payload: dict[str, Any]) -> str: + message = required_string(payload, "message", 1000) + bot_name = required_string(payload, "bot_name", 64) + message_without_name = re.sub( + re.escape(bot_name), " ", message, flags=re.IGNORECASE + ) + return normalized_command_text(message_without_name) + + +def is_obvious_social_chat(payload: dict[str, Any]) -> bool: + text = normalized_routing_text(payload) + patterns = ( + r"^(?:привет|здравствуй|здравствуйте|добрый день|добрый вечер)\b", + r"\bкак (?:у тебя )?дела\b", + r"\bкак (?:твое|твоё) настроение\b", + r"\bчто ты (?:делаешь|делаешь тут|сейчас делаешь)\b", + r"\b(?:расскажи|поведай) (?:о|про) себя\b", + r"\bкто ты\b", + ) + return any(re.search(pattern, text) for pattern in patterns) + + +def deterministic_command(message: str) -> str: + text = normalized_command_text(message) + + item_match = re.search( + r"(?:\|c[0-9a-fA-F]{8})?\|Hitem:[^|]+\|h\[[^]]+\]\|h(?:\|r)?", + message, + ) + quest_match = re.search( + r"(?:\|c[0-9a-fA-F]{8})?\|Hquest:[^|]+\|h\[[^]]+\]\|h(?:\|r)?", + message, + ) + item_link = item_match.group(0) if item_match else "" + quest_link = quest_match.group(0) if quest_match else "" + + if item_link and re.search( + r"\b(?:выбери|возьми)\b.*\b(?:награду|награде|награды)\b" + r"|\b(?:награду|награде|награды)\b.*\b(?:выбери|возьми)\b", + text, + ): + return f"r {item_link}" + if item_link and re.search( + r"\b(?:дай|передай|отдай|вручи|обменяй)\b", text + ): + return f"t {item_link}" + + if quest_link: + if re.search(r"\b(?:прими|возьми)\b", text): + return f"accept {quest_link}" + if re.search(r"\b(?:поделись|расшарь|передай)\b", text): + return f"share {quest_link}" + if re.search(r"\b(?:откажись|брось|удали)\b", text): + return f"drop {quest_link}" + if re.search( + r"\b(?:покажи|расскажи|проверь)\b.*\b(?:задание|задания|квест|квеста|прогресс)\b", + text, + ): + return f"q {quest_link}" + + patterns = ( + (r"\b(?:следуй|иди|пойдем|пошли)\s+за\s+мной\b", "follow"), + ( + r"\b(?:стой|останься|оставайся|жди|подожди)\s+" + r"(?:тут|здесь|на\s+месте)\b", + "stay", + ), + (r"\bне\s+двигайся\b", "stay"), + (r"\b(?:атакуй|бей|ударь)\s+(?:мою\s+)?цель\b", "attack"), + (r"\b(?:отступай|убегай|беги\s+ко\s+мне)\b", "flee"), + (r"\bпригласи\s+меня\s+(?:в\s+)?(?:группу|пати|рейд)\b", "invite"), + (r"\b(?:выйди|покинь)\s+(?:из\s+)?(?:группу|группы|пати|рейд|рейда)\b", "leave"), + (r"\b(?:собери|подбери)\s+(?:всю\s+)?(?:добычу|лут)\b", "add all loot"), + (r"\b(?:воскресни|оживи)\b", "revive"), + (r"\b(?:освободи|выпусти)\s+дух\b", "release"), + (r"\b(?:телепортируйся|призовись)\s+ко\s+мне\b", "summon"), + (r"\b(?:бафни|усиль)\s+(?:меня|группу|пати|нас)\b", "buff"), + ( + r"\b(?:покажи|открой)\s+(?:мне\s+)?" + r"(?:свой\s+|свои\s+)?(?:инвентарь|сумки|вещи)\b", + "inv", + ), + ( + r"\b(?:начни|открой|предложи)\s+(?:мне\s+)?обмен\b" + r"|\bдавай\s+обменяемся\b" + r"|\bдай\s+мне\s+(?:свои\s+)?предметы\b", + "t", + ), + ( + r"\b(?:покажи|перечисли)\s+(?:мне\s+)?(?:все\s+)?" + r"(?:свои\s+)?(?:задания|квесты)\b" + r"|\bкакие\s+у\s+тебя\s+(?:задания|квесты)\b", + "quests all", + ), + ( + r"\b(?:покажи|перечисли)\s+(?:мне\s+)?(?:завершенные|выполненные)\s+" + r"(?:задания|квесты)\b", + "quests completed", + ), + ( + r"\b(?:покажи|перечисли)\s+(?:мне\s+)?" + r"(?:незавершенные|невыполненные)\s+(?:задания|квесты)\b", + "quests incompleted", + ), + (r"\b(?:прими|возьми)\s+все\s+(?:задания|квесты)\b", "accept *"), + ( + r"\b(?:сдай|заверши)\s+(?:готовое\s+|выполненное\s+)?" + r"(?:задание|квест)\b", + "talk", + ), + ) + for pattern, command in patterns: + if re.search(pattern, text): + return command + return "" + + +def gender_context(value: int, subject: str) -> str: + if value == 0: + return ( + f"{subject}: мужской пол; используй соответствующие мужские " + "местоимения и формы слов." + ) + if value == 1: + return ( + f"{subject}: женский пол; используй соответствующие женские " + "местоимения и формы слов." + ) + return ( + f"{subject}: пол неизвестен; избегай местоимений и форм слов, " + "указывающих на пол." + ) + + +def command_acknowledgement(payload: dict[str, Any]) -> str: + bot_gender = integer(payload, "bot_gender", 2) + if bot_gender == 0: + return "Понял команду." + if bot_gender == 1: + return "Поняла команду." + return "Команда понята." + + +def build_messages(payload: dict[str, Any]) -> tuple[str, list[dict[str, str]]]: + bot_guid = required_string(payload, "bot_guid", 32) + player_guid = required_string(payload, "player_guid", 32) + bot_name = required_string(payload, "bot_name", 64) + player_name = required_string(payload, "player_name", 64) + message = required_string(payload, "message", 1000) + bot_class = CLASS_NAMES.get(integer(payload, "bot_class"), "приключенец") + bot_race = RACE_NAMES.get(integer(payload, "bot_race"), "неизвестная раса") + bot_gender = integer(payload, "bot_gender", 2) + player_gender = integer(payload, "player_gender", 2) + bot_level = integer(payload, "bot_level") + zone_id = integer(payload, "zone_id") + + conversation_key = f"{bot_guid}:{player_guid}" + context = ( + f"Ты — {bot_name}, {bot_race}, {bot_class}, уровень {bot_level}. " + f"{gender_context(bot_gender, 'Персонаж')} " + f"Игрока зовут {player_name}. {gender_context(player_gender, 'Игрок')} " + f"Текущая игровая зона имеет ID {zone_id}." + ) + + messages: list[dict[str, str]] = [ + {"role": "system", "content": SYSTEM_PROMPT}, + ] + messages.append({"role": "system", "content": context}) + with history_lock: + messages.extend(list(history[conversation_key])) + messages.append({"role": "user", "content": message}) + return conversation_key, messages + + +def request_yandex( + messages: list[dict[str, str]], + *, + purpose: str, + structured: bool, + temperature: float, +) -> str: + request_payload: dict[str, Any] = { + "model": MODEL_URI, + "messages": messages, + "temperature": temperature, + "max_tokens": MAX_TOKENS, + "stream": False, + } + if structured: + request_payload["response_format"] = {"type": "json_object"} + request_body = json.dumps(request_payload, ensure_ascii=False).encode("utf-8") + + request = urllib.request.Request( + f"{YANDEX_BASE_URL}/chat/completions", + data=request_body, + method="POST", + headers={ + "Authorization": f"Api-Key {YANDEX_API_KEY}", + "OpenAI-Project": YANDEX_FOLDER_ID, + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "MoonWell-playerbots-ai/1.0", + }, + ) + + try: + with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as response: + response_payload = json.load(response) + except urllib.error.HTTPError as error: + error_body = error.read(2048).decode("utf-8", errors="replace") + raise RuntimeError(f"Yandex API HTTP {error.code}: {error_body}") from error + except urllib.error.URLError as error: + raise RuntimeError(f"Yandex API connection failed: {error.reason}") from error + + if LOG_API_RESPONSES: + LOG.info( + "Yandex API %s response: %s", + purpose, + json.dumps(response_payload, ensure_ascii=False, separators=(",", ":")), + ) + + try: + assistant_message = response_payload["choices"][0]["message"] + except (KeyError, IndexError, TypeError) as error: + raise RuntimeError("Yandex API response has no assistant message") from error + + content = assistant_message.get("content") + if not isinstance(content, str): + raise RuntimeError("Yandex API returned non-text assistant content") + return content + + +def query_yandex(payload: dict[str, Any]) -> tuple[str, str]: + conversation_key, chat_messages = build_messages(payload) + command = "" + if COMMANDS_ENABLED: + command = deterministic_command(payload["message"]) + if command: + return command_acknowledgement(payload), command + + skip_classifier = is_obvious_social_chat(payload) + cache_hit, command = (False, "") + if not skip_classifier: + cache_hit, command = get_cached_classifier_command(payload) + if not skip_classifier and not cache_hit: + routing_content = request_yandex( + [ + {"role": "system", "content": COMMAND_ROUTING_PROMPT}, + {"role": "user", "content": payload["message"]}, + ], + purpose="classifier", + structured=True, + temperature=0.1, + ) + try: + routed_response = json.loads(routing_content) + except json.JSONDecodeError as error: + raise RuntimeError("Yandex API returned invalid command JSON") from error + if not isinstance(routed_response, dict): + raise RuntimeError("Yandex API returned non-object command JSON") + command = validated_native_command(routed_response) + if routed_response.get("command") is not None and not command: + return "Эту команду я не могу выполнить.", "" + cache_classifier_command(payload, command) + if command: + return command_acknowledgement(payload), command + + reply = clean_reply( + request_yandex( + chat_messages, + purpose="chat", + structured=False, + temperature=TEMPERATURE, + ) + ) + if not reply: + raise RuntimeError("Yandex API returned an empty chat reply") + + with history_lock: + conversation = history[conversation_key] + conversation.append({"role": "user", "content": payload["message"]}) + conversation.append({"role": "assistant", "content": reply}) + return reply, "" + + +class RequestHandler(BaseHTTPRequestHandler): + server_version = "MoonWellPlayerbotsAI/1.0" + + def log_message(self, format_string: str, *args: Any) -> None: + LOG.debug("%s - %s", self.client_address[0], format_string % args) + + def send_text(self, status: HTTPStatus, text: str) -> None: + body = text.encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def send_json(self, status: HTTPStatus, payload: dict[str, Any]) -> None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self) -> None: + if self.path != "/health": + self.send_text(HTTPStatus.NOT_FOUND, "not found") + return + self.send_json( + HTTPStatus.OK, + { + "status": "ok", + "configured": configured(), + "model": MODEL_URI or None, + "commands_enabled": COMMANDS_ENABLED, + "log_api_responses": LOG_API_RESPONSES, + "classifier_cache_enabled": CLASSIFIER_CACHE_ENABLED, + "classifier_cache_available": classifier_cache_available, + "classifier_cache_entries": classifier_cache_entry_count(), + "classifier_cache_hits": classifier_cache_hits, + "classifier_cache_misses": classifier_cache_misses, + }, + ) + + def do_POST(self) -> None: + if self.path != "/v1/chat": + self.send_text(HTTPStatus.NOT_FOUND, "not found") + return + if not configured(): + self.send_text( + HTTPStatus.SERVICE_UNAVAILABLE, + "Yandex AI credentials are not configured", + ) + return + + try: + content_length = int(self.headers.get("Content-Length", "0")) + except ValueError: + self.send_text(HTTPStatus.BAD_REQUEST, "invalid Content-Length") + return + if content_length <= 0 or content_length > 32768: + self.send_text(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, "invalid request size") + return + + try: + payload = json.loads(self.rfile.read(content_length)) + if not isinstance(payload, dict): + raise ValueError("request body must be an object") + reply, command = query_yandex(payload) + except (json.JSONDecodeError, UnicodeDecodeError, ValueError) as error: + self.send_text(HTTPStatus.BAD_REQUEST, str(error)) + return + except Exception as error: + LOG.error("LLM request failed: %s", error) + self.send_text(HTTPStatus.BAD_GATEWAY, "LLM provider request failed") + return + + self.send_text(HTTPStatus.OK, f"PB-COMMAND:{command}\n{reply}") + + +def main() -> None: + initialize_classifier_cache() + LOG.info( + "starting bridge on %s:%d, configured=%s, model=%s", + HOST, + PORT, + configured(), + MODEL_URI or "", + ) + ThreadingHTTPServer((HOST, PORT), RequestHandler).serve_forever() + + +if __name__ == "__main__": + main() diff --git a/setup-modules.sh b/setup-modules.sh index 292cc19e2..05c01b1ec 100644 --- a/setup-modules.sh +++ b/setup-modules.sh @@ -108,6 +108,16 @@ FORCE_PLAYERBOTS="${FORCE_PLAYERBOTS:-${ACORE_PLAYERBOTS_FORCE:-0}}" : "${ACORE_PLAYERBOTS_ENABLED:=1}" : "${ACORE_PLAYERBOTS_RUSSIAN_NAMES:=1}" : "${ACORE_PLAYERBOTS_RANDOM_BOT_TALK:=1}" +: "${ACORE_PLAYERBOTS_AI_ENABLED:=0}" +: "${ACORE_PLAYERBOTS_AI_COMMANDS_ENABLED:=1}" +: "${ACORE_PLAYERBOTS_AI_AVAILABLE_FOR_ALL:=1}" +: "${ACORE_PLAYERBOTS_AI_BRIDGE_HOST:=playerbots-ai}" +: "${ACORE_PLAYERBOTS_AI_BRIDGE_PORT:=8080}" +: "${ACORE_PLAYERBOTS_AI_BRIDGE_PATH:=/v1/chat}" +: "${ACORE_PLAYERBOTS_AI_PLAYER_COOLDOWN_SECONDS:=8}" +: "${ACORE_PLAYERBOTS_AI_MAX_QUEUED_REQUESTS:=32}" +: "${ACORE_PLAYERBOTS_AI_MAX_REPLY_CHARACTERS:=240}" +: "${ACORE_PLAYERBOTS_AI_DEBUG:=0}" : "${ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN:=${AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN:-0}}" : "${ACORE_PLAYERBOTS_MIN_RANDOM_BOTS:=0}" : "${ACORE_PLAYERBOTS_MAX_RANDOM_BOTS:=0}" @@ -363,6 +373,8 @@ sync_module_dists() { "$MODULES_ETC_DIR/mod_learnspells.conf.dist" sync_file "$ROOT_DIR/modules/mod-playerbots/conf/playerbots.conf.dist" \ "$MODULES_ETC_DIR/playerbots.conf.dist" + sync_file "$ROOT_DIR/modules/mod-playerbots-ai/conf/mod_playerbots_ai.conf.dist" \ + "$MODULES_ETC_DIR/mod_playerbots_ai.conf.dist" sync_file "$ROOT_DIR/modules/mod-gamemodes/conf/mod_gamemodes.conf.dist" \ "$MODULES_ETC_DIR/mod_gamemodes.conf.dist" sync_file "$ROOT_DIR/modules/mod-quest-loot-party/conf/mod-quest-loot-party.conf.dist" \ @@ -530,11 +542,19 @@ write_playerbots_conf() { return fi + local random_bot_talk="$ACORE_PLAYERBOTS_RANDOM_BOT_TALK" + if [[ "$ACORE_PLAYERBOTS_AI_ENABLED" == "1" ]]; then + # RandomBotTalk consumes public chat packets inside every bot and can + # send a delayed stock reply even when the legacy channel parser is + # bypassed by mod-playerbots-ai. + random_bot_talk=0 + fi + write_file "$MODULES_ETC_DIR/playerbots.conf.dist" <&2 + exit 1 +fi + +if [[ "$PLAYERBOTS_AI_ENABLED" == "1" ]] && \ + { [[ -z "${YANDEX_AI_API_KEY:-}" ]] || [[ -z "${YANDEX_AI_FOLDER_ID:-}" ]]; }; then + printf 'Playerbots AI requires YANDEX_AI_API_KEY and YANDEX_AI_FOLDER_ID in %s\n' \ + "$ENV_FILE" >&2 + exit 1 +fi + log "applying module configuration" bash "$ROOT_DIR/setup-modules.sh" @@ -218,6 +232,20 @@ bash "$ROOT_DIR/setup-modules.sh" log "initializing client data" run_one_shot_service "ac-client-data-init" "ac-client-data-init" +if [[ "$PLAYERBOTS_AI_ENABLED" == "1" ]]; then + log "starting playerbots AI bridge" + if [[ -n "$BUILD_FLAG" ]]; then + docker compose up -d --build playerbots-ai + else + docker compose up -d playerbots-ai + fi + if ! wait_for_healthy "playerbots-ai" 60; then + print_service_logs "playerbots-ai" + printf 'playerbots-ai did not become healthy\n' >&2 + exit 1 + fi +fi + log "starting authserver and worldserver" if [[ -n "$BUILD_FLAG" ]]; then docker compose up -d --build ac-authserver ac-worldserver