Compare commits
19 Commits
release
...
b5c0a8a27a
| Author | SHA1 | Date | |
|---|---|---|---|
| b5c0a8a27a | |||
| 85adbe19da | |||
| 6f075133f7 | |||
| d9f0d414fc | |||
| 9b0e732fb8 | |||
| a71eb7bee7 | |||
| 4eb59b7245 | |||
| 737d7c756d | |||
| 58dcb622f3 | |||
| faf8813b11 | |||
| e1c6d2a0b7 | |||
| a5a10166d9 | |||
| 4d4463ca0b | |||
| 38dffee5f6 | |||
| 4d8a2a20e1 | |||
| 7a32675298 | |||
| e7f0ce052b | |||
| b49efcea0d | |||
| 6516b32fc6 |
@@ -65,6 +65,42 @@ AC_RATE_XP_KILL=1
|
|||||||
# Legacy alias для docker override. Держим синхронно с ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN.
|
# Legacy alias для docker override. Держим синхронно с ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN.
|
||||||
AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN=0
|
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://<YANDEX_AI_FOLDER_ID>/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
|
# Значения по умолчанию для ./create-account.sh
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -393,6 +429,14 @@ ACORE_LEARNSPELLS_RIDING_COLD_WEATHER_FLYING=1
|
|||||||
# 1 = включает playerbots.conf, если модуль поддерживается сборкой.
|
# 1 = включает playerbots.conf, если модуль поддерживается сборкой.
|
||||||
ACORE_PLAYERBOTS_ENABLED=1
|
ACORE_PLAYERBOTS_ENABLED=1
|
||||||
|
|
||||||
|
# 1 = при ./start-server.sh и scripts/prod-deploy.sh автоматически
|
||||||
|
# русифицировать имена RNDbot, названия их гильдий и статические сообщения.
|
||||||
|
# Ручной повторный запуск: ./localize-playerbots-ru.sh --apply
|
||||||
|
ACORE_PLAYERBOTS_RUSSIAN_NAMES=1
|
||||||
|
|
||||||
|
# 1 = разрешить ботам отвечать и участвовать в статическом чате.
|
||||||
|
ACORE_PLAYERBOTS_RANDOM_BOT_TALK=1
|
||||||
|
|
||||||
# Автологин random bots.
|
# Автологин random bots.
|
||||||
# Для сервера под соло/кооп лучше оставить 0, если вам нужны только боты в группу по команде.
|
# Для сервера под соло/кооп лучше оставить 0, если вам нужны только боты в группу по команде.
|
||||||
ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN=0
|
ACORE_PLAYERBOTS_RANDOM_BOT_AUTOLOGIN=0
|
||||||
@@ -412,6 +456,9 @@ ACORE_PLAYERBOTS_RANDOM_BOT_ACCOUNT_COUNT=0
|
|||||||
# После этого ОБЯЗАТЕЛЬНО верните 0.
|
# После этого ОБЯЗАТЕЛЬНО верните 0.
|
||||||
ACORE_PLAYERBOTS_DELETE_RANDOM_BOT_ACCOUNTS=0
|
ACORE_PLAYERBOTS_DELETE_RANDOM_BOT_ACCOUNTS=0
|
||||||
|
|
||||||
|
# Разовая очистка гильдий randombot перед удалением их аккаунтов.
|
||||||
|
ACORE_PLAYERBOTS_DELETE_RANDOM_BOT_GUILDS=0
|
||||||
|
|
||||||
# Сколько ботов игрок может одновременно добавить в группу.
|
# Сколько ботов игрок может одновременно добавить в группу.
|
||||||
ACORE_PLAYERBOTS_MAX_ADDED_BOTS=9
|
ACORE_PLAYERBOTS_MAX_ADDED_BOTS=9
|
||||||
|
|
||||||
|
|||||||
@@ -108,3 +108,4 @@ local.properties
|
|||||||
# !modules/yourmodule
|
# !modules/yourmodule
|
||||||
#
|
#
|
||||||
# ==================
|
# ==================
|
||||||
|
.claude
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Solo Random Dungeon client patch
|
||||||
|
|
||||||
|
The server recognizes five custom `LFGDungeons.dbc` entries:
|
||||||
|
|
||||||
|
| Custom ID | Stock category |
|
||||||
|
| --- | --- |
|
||||||
|
| 1258 | Random Classic Dungeon (258) |
|
||||||
|
| 1259 | Random Burning Crusade Dungeon (259) |
|
||||||
|
| 1260 | Random Burning Crusade Heroic (260) |
|
||||||
|
| 1261 | Random Lich King Dungeon (261) |
|
||||||
|
| 1262 | Random Lich King Heroic (262) |
|
||||||
|
|
||||||
|
The generated server DBC is at:
|
||||||
|
|
||||||
|
`client_patch/enUS/DBFilesClient/LFGDungeons.dbc`
|
||||||
|
|
||||||
|
It is mounted into `ac-worldserver` by `docker-compose.override.yml`.
|
||||||
|
|
||||||
|
## Client installation
|
||||||
|
|
||||||
|
Use the DBC extracted from the exact locale of the target 3.3.5a client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/build-solo-lfg-dbc.py \
|
||||||
|
/path/to/client/DBFilesClient/LFGDungeons.dbc \
|
||||||
|
/tmp/solo-rdf/DBFilesClient/LFGDungeons.dbc
|
||||||
|
```
|
||||||
|
|
||||||
|
Put the generated `DBFilesClient/LFGDungeons.dbc` into a client MPQ patch
|
||||||
|
loaded after the stock locale archives. For a Russian client this is normally
|
||||||
|
a new archive under `Data/ruRU/` named like `patch-ruRU-S.MPQ`.
|
||||||
|
|
||||||
|
After installing the patch, delete the client's `Cache` directory. The
|
||||||
|
Dungeon Finder list will contain separate solo random categories appropriate
|
||||||
|
for the character's level.
|
||||||
|
|
||||||
|
Do not replace a localized client DBC with the bundled enUS server DBC. Run
|
||||||
|
the generator against that client's own DBC so all stock localized rows remain
|
||||||
|
intact.
|
||||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
|||||||
|
-- MySQL dump 10.13 Distrib 8.4.3, for Win64 (x86_64)
|
||||||
|
--
|
||||||
|
-- Host: localhost Database: acore_characters
|
||||||
|
-- ------------------------------------------------------
|
||||||
|
-- Server version 8.4.3
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!50503 SET NAMES utf8mb4 */;
|
||||||
|
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `mod_gamemodes_characters`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `mod_gamemodes_characters`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!50503 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `mod_gamemodes_characters` (
|
||||||
|
`guid` int unsigned NOT NULL,
|
||||||
|
`game_mode` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '0 = NORMAL, 1 = TRAITOR',
|
||||||
|
PRIMARY KEY (`guid`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Character Game Modes';
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dumping data for table `mod_gamemodes_characters`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `mod_gamemodes_characters` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `mod_gamemodes_characters` DISABLE KEYS */;
|
||||||
|
/*!40000 ALTER TABLE `mod_gamemodes_characters` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}"
|
||||||
|
GUILD_NAME=""
|
||||||
|
ASSUME_YES=0
|
||||||
|
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") <guild name> [--yes]
|
||||||
|
|
||||||
|
Permanently deletes a guild and its related character-database records.
|
||||||
|
The worldserver must be stopped before running this script.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--yes Skip the interactive confirmation
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
docker compose stop ac-worldserver
|
||||||
|
./delete-guild.sh "Название гильдии"
|
||||||
|
./delete-guild.sh "Название гильдии" --yes
|
||||||
|
docker compose start ac-worldserver
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[delete-guild] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--yes)
|
||||||
|
ASSUME_YES=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
printf 'Unknown option: %s\n\n' "$1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -n "$GUILD_NAME" ]]; then
|
||||||
|
printf 'Only one guild name may be specified\n\n' >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
GUILD_NAME="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$GUILD_NAME" ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$GUILD_NAME" == *$'\n'* || "$GUILD_NAME" == *$'\r'* || "$GUILD_NAME" == *$'\t'* ]]; then
|
||||||
|
printf 'Guild name must not contain control characters\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
RUNNING_SERVICES="$(docker compose ps --status running --services)"
|
||||||
|
|
||||||
|
if grep -qx 'ac-worldserver' <<<"$RUNNING_SERVICES"; then
|
||||||
|
printf 'ac-worldserver is running. Stop it first:\n docker compose stop ac-worldserver\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -qx 'ac-database' <<<"$RUNNING_SERVICES"; then
|
||||||
|
printf 'ac-database is not running. Start it first:\n docker compose up -d ac-database\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ACORE_DELETE_GUILD_NAME="$GUILD_NAME"
|
||||||
|
SQL_GUILD_NAME="$(python3 <<'PY'
|
||||||
|
import os
|
||||||
|
|
||||||
|
value = os.environ["ACORE_DELETE_GUILD_NAME"]
|
||||||
|
print("'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
GUILD_ROW="$(
|
||||||
|
docker compose exec -T ac-database bash -lc \
|
||||||
|
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --batch --skip-column-names acore_characters' \
|
||||||
|
<<<"SELECT guildid, name, leaderguid FROM guild WHERE name = ${SQL_GUILD_NAME} LIMIT 1;"
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ -z "$GUILD_ROW" ]]; then
|
||||||
|
printf 'Guild not found: %s\n' "$GUILD_NAME" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=$'\t' read -r GUILD_ID FOUND_NAME LEADER_GUID <<<"$GUILD_ROW"
|
||||||
|
log "found guild id=${GUILD_ID}, name=${FOUND_NAME}, leader=${LEADER_GUID}"
|
||||||
|
|
||||||
|
if ((ASSUME_YES == 0)); then
|
||||||
|
printf 'This permanently deletes the guild and all items in its bank.\n'
|
||||||
|
printf 'Type the guild name to confirm: '
|
||||||
|
read -r CONFIRMATION
|
||||||
|
if [[ "$CONFIRMATION" != "$FOUND_NAME" ]]; then
|
||||||
|
printf 'Confirmation did not match; nothing was deleted.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
SQL=$(cat <<EOF
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
SET @guild_id := ${GUILD_ID};
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE delete_guild_members (
|
||||||
|
guid INT UNSIGNED NOT NULL PRIMARY KEY
|
||||||
|
) ENGINE=MEMORY
|
||||||
|
SELECT guid FROM guild_member WHERE guildid = @guild_id;
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE delete_guild_bank_items (
|
||||||
|
item_guid INT UNSIGNED NOT NULL PRIMARY KEY
|
||||||
|
) ENGINE=MEMORY
|
||||||
|
SELECT item_guid FROM guild_bank_item WHERE guildid = @guild_id;
|
||||||
|
|
||||||
|
DELETE item_instance
|
||||||
|
FROM item_instance
|
||||||
|
INNER JOIN delete_guild_bank_items
|
||||||
|
ON delete_guild_bank_items.item_guid = item_instance.guid;
|
||||||
|
|
||||||
|
DELETE guild_member_withdraw
|
||||||
|
FROM guild_member_withdraw
|
||||||
|
INNER JOIN delete_guild_members
|
||||||
|
ON delete_guild_members.guid = guild_member_withdraw.guid;
|
||||||
|
|
||||||
|
DELETE FROM guild_bank_eventlog WHERE guildid = @guild_id;
|
||||||
|
DELETE FROM guild_eventlog WHERE guildid = @guild_id;
|
||||||
|
DELETE FROM guild_bank_right WHERE guildid = @guild_id;
|
||||||
|
DELETE FROM guild_bank_item WHERE guildid = @guild_id;
|
||||||
|
DELETE FROM guild_bank_tab WHERE guildid = @guild_id;
|
||||||
|
DELETE FROM guild_rank WHERE guildid = @guild_id;
|
||||||
|
DELETE FROM guild_member WHERE guildid = @guild_id;
|
||||||
|
DELETE FROM guild WHERE guildid = @guild_id;
|
||||||
|
|
||||||
|
SET @deleted := ROW_COUNT();
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
SELECT @guild_id AS guildid, @deleted AS deleted;
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
log "deleting guild ${FOUND_NAME} (${GUILD_ID})"
|
||||||
|
docker compose exec -T ac-database bash -lc \
|
||||||
|
'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --show-warnings acore_characters' <<<"$SQL"
|
||||||
|
|
||||||
|
log "guild deleted"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,43 @@
|
|||||||
services:
|
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:
|
ac-worldserver:
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
@@ -8,3 +47,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./modules:/azerothcore/modules:ro
|
- ./modules:/azerothcore/modules:ro
|
||||||
- ./lua_scripts:/azerothcore/lua_scripts
|
- ./lua_scripts:/azerothcore/lua_scripts
|
||||||
|
- ./client_patch/enUS/DBFilesClient/LFGDungeons.dbc:/azerothcore/env/dist/data/dbc/LFGDungeons.dbc:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
playerbots-ai-cache:
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
services:
|
services:
|
||||||
|
playerbots-ai:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 128M
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "20m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
ac-database:
|
ac-database:
|
||||||
command: >
|
command: >
|
||||||
--innodb_buffer_pool_size=4G
|
--innodb_buffer_pool_size=4G
|
||||||
|
|||||||
+54
-1
@@ -15,6 +15,7 @@ fi
|
|||||||
IMPORT_MYTHICPLUS_SQL="${ACORE_IMPORT_MYTHICPLUS_SQL:-1}"
|
IMPORT_MYTHICPLUS_SQL="${ACORE_IMPORT_MYTHICPLUS_SQL:-1}"
|
||||||
IMPORT_STORE_SQL="${ACORE_IMPORT_STORE_SQL:-1}"
|
IMPORT_STORE_SQL="${ACORE_IMPORT_STORE_SQL:-1}"
|
||||||
IMPORT_ENCOUNTER_JOURNAL_SQL="${ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL:-1}"
|
IMPORT_ENCOUNTER_JOURNAL_SQL="${ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL:-1}"
|
||||||
|
PLAYERBOTS_RUSSIAN_NAMES="${ACORE_PLAYERBOTS_RUSSIAN_NAMES:-1}"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -22,7 +23,8 @@ Usage: $(basename "$0")
|
|||||||
|
|
||||||
Imports required custom SQL that lives outside the normal AzerothCore module paths.
|
Imports required custom SQL that lives outside the normal AzerothCore module paths.
|
||||||
Currently this bootstraps the MythicPlus and Store Lua packages, plus module strings
|
Currently this bootstraps the MythicPlus and Store Lua packages, plus module strings
|
||||||
that are not covered by the normal module SQL import path in this deployment.
|
and playerbot localization data not covered by the normal module SQL import path
|
||||||
|
in this deployment.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +191,28 @@ import_store_sql() {
|
|||||||
mysql_exec "" "CREATE DATABASE IF NOT EXISTS \`store\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
|
mysql_exec "" "CREATE DATABASE IF NOT EXISTS \`store\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Unlike the rest of Store.sql, this is a repeatable migration. It ensures
|
||||||
|
# existing store databases receive the global switch without overwriting a
|
||||||
|
# value later changed by an administrator.
|
||||||
|
mysql_exec "store" "
|
||||||
|
CREATE TABLE IF NOT EXISTS \`store_config\` (
|
||||||
|
\`id\` TINYINT UNSIGNED NOT NULL,
|
||||||
|
\`enabled\` TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (\`id\`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
INSERT IGNORE INTO \`store_config\` (\`id\`, \`enabled\`) VALUES (1, 0);
|
||||||
|
"
|
||||||
|
|
||||||
|
# Store.sql is a one-time bootstrap, not a repeatable migration. It
|
||||||
|
# contains UPDATE and DELETE statements that would overwrite values
|
||||||
|
# maintained in the live store database. The canonical services table is
|
||||||
|
# created by the initial bootstrap, so its presence also protects
|
||||||
|
# installations created before this guard was introduced.
|
||||||
|
if database_exists "store" && table_exists "store" "store_services"; then
|
||||||
|
log "store database already initialized, skipping one-time Store.sql bootstrap"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
temp_sql="$(mktemp)"
|
temp_sql="$(mktemp)"
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -235,6 +259,32 @@ import_aoe_loot_sql() {
|
|||||||
import_sql_file "acore_world" "$aoe_loot_sql_file"
|
import_sql_file "acore_world" "$aoe_loot_sql_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import_playerbots_ru_sql() {
|
||||||
|
local names_sql_file="$ROOT_DIR/modules/mod-playerbots/data/sql/characters/updates/2026_07_30_00_playerbots_russian_names.sql"
|
||||||
|
|
||||||
|
if [[ "$PLAYERBOTS_RUSSIAN_NAMES" == "0" ]]; then
|
||||||
|
log "Russian playerbot localization disabled, skipping name pools"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if table_exists "acore_characters" "playerbots_names_ru" && \
|
||||||
|
table_exists "acore_characters" "playerbots_guild_names_ru"; then
|
||||||
|
log "Russian playerbot name pools already exist, skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
import_sql_file "acore_characters" "$names_sql_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_server_motd() {
|
||||||
|
log "configuring MoonWell login message"
|
||||||
|
mysql_exec "acore_auth" "
|
||||||
|
REPLACE INTO motd (realmid, text)
|
||||||
|
VALUES (-1, 'Добро пожаловать на сервер MoonWell! Приятной игры!');
|
||||||
|
DELETE FROM motd_localized WHERE realmid = -1;
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
import_encounter_journal_sql() {
|
import_encounter_journal_sql() {
|
||||||
local schema_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/01_encounter_journal_schema.sql"
|
local schema_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/01_encounter_journal_schema.sql"
|
||||||
local sample_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/02_encounter_journal_sample.sql"
|
local sample_file="$ROOT_DIR/modules/mod-encounter-journal/data/sql/db-world/base/02_encounter_journal_sample.sql"
|
||||||
@@ -295,6 +345,7 @@ fi
|
|||||||
require_integer_flag "ACORE_IMPORT_MYTHICPLUS_SQL" "$IMPORT_MYTHICPLUS_SQL"
|
require_integer_flag "ACORE_IMPORT_MYTHICPLUS_SQL" "$IMPORT_MYTHICPLUS_SQL"
|
||||||
require_integer_flag "ACORE_IMPORT_STORE_SQL" "$IMPORT_STORE_SQL"
|
require_integer_flag "ACORE_IMPORT_STORE_SQL" "$IMPORT_STORE_SQL"
|
||||||
require_integer_flag "ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL" "$IMPORT_ENCOUNTER_JOURNAL_SQL"
|
require_integer_flag "ACORE_IMPORT_ENCOUNTER_JOURNAL_SQL" "$IMPORT_ENCOUNTER_JOURNAL_SQL"
|
||||||
|
require_integer_flag "ACORE_PLAYERBOTS_RUSSIAN_NAMES" "$PLAYERBOTS_RUSSIAN_NAMES"
|
||||||
|
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
@@ -305,7 +356,9 @@ fi
|
|||||||
|
|
||||||
import_mythicplus_sql
|
import_mythicplus_sql
|
||||||
import_aoe_loot_sql
|
import_aoe_loot_sql
|
||||||
|
import_playerbots_ru_sql
|
||||||
import_encounter_journal_sql
|
import_encounter_journal_sql
|
||||||
import_store_sql
|
import_store_sql
|
||||||
|
configure_server_motd
|
||||||
|
|
||||||
log "custom SQL bootstrap completed"
|
log "custom SQL bootstrap completed"
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
MODE="${1:---dry-run}"
|
||||||
|
CHARACTER_NAMES_SQL="modules/mod-playerbots/data/sql/characters/updates/2026_07_30_00_playerbots_russian_names.sql"
|
||||||
|
BOT_TEXTS_SQL="modules/mod-playerbots/data/sql/playerbots/updates/2026_07_30_00_ai_playerbot_russian_texts.sql"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
--dry-run | --apply) ;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [--dry-run|--apply]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! docker compose ps --status running --services | grep -qx "ac-database"; then
|
||||||
|
echo "ac-database must be running." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mysql_query() {
|
||||||
|
local database="$1"
|
||||||
|
local sql="$2"
|
||||||
|
docker compose exec -T ac-database bash -lc \
|
||||||
|
"mysql --default-character-set=utf8mb4 -N -s -uroot -p\"\$MYSQL_ROOT_PASSWORD\" \"$database\"" <<<"$sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
import_sql() {
|
||||||
|
local database="$1"
|
||||||
|
local file_path="$2"
|
||||||
|
docker compose exec -T ac-database bash -lc \
|
||||||
|
"mysql --default-character-set=utf8mb4 -uroot -p\"\$MYSQL_ROOT_PASSWORD\" \"$database\"" <"$file_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
read -r bot_count guild_count < <(
|
||||||
|
mysql_query acore_characters "
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN REGEXP_LIKE(c.name, '^[[:ascii:]]+$') THEN c.guid
|
||||||
|
END),
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN REGEXP_LIKE(g.name, '^[[:ascii:]]+$') THEN g.guildid
|
||||||
|
END)
|
||||||
|
FROM characters c
|
||||||
|
JOIN acore_playerbots.playerbots_account_type pat
|
||||||
|
ON pat.account_id = c.account AND pat.account_type = 1
|
||||||
|
LEFT JOIN guild g ON g.leaderguid = c.guid;
|
||||||
|
"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "RNDbot characters to rename: $bot_count"
|
||||||
|
echo "RNDbot-led guilds to rename: $guild_count"
|
||||||
|
|
||||||
|
if [[ "$MODE" == "--dry-run" ]]; then
|
||||||
|
echo "Dry run only. Stop ac-worldserver and run '$0 --apply' to apply."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker compose ps --status running --services | grep -qx "ac-worldserver"; then
|
||||||
|
echo "Refusing to rename live characters while ac-worldserver is running." >&2
|
||||||
|
echo "Stop it with: docker compose stop ac-worldserver" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
import_sql acore_characters "$CHARACTER_NAMES_SQL"
|
||||||
|
import_sql acore_playerbots "$BOT_TEXTS_SQL"
|
||||||
|
|
||||||
|
mysql_query acore_characters "
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS moonwell_playerbot_name_backup (
|
||||||
|
guid INT UNSIGNED NOT NULL,
|
||||||
|
original_name VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||||
|
localized_name VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||||
|
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (guid),
|
||||||
|
UNIQUE KEY uq_moonwell_playerbot_localized_name (localized_name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS moonwell_playerbot_guild_name_backup (
|
||||||
|
guildid INT UNSIGNED NOT NULL,
|
||||||
|
original_name VARCHAR(24) NOT NULL,
|
||||||
|
localized_name VARCHAR(24) NOT NULL,
|
||||||
|
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (guildid),
|
||||||
|
UNIQUE KEY uq_moonwell_playerbot_localized_guild_name (localized_name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_available_names;
|
||||||
|
CREATE TEMPORARY TABLE tmp_moonwell_available_names AS
|
||||||
|
SELECT
|
||||||
|
n.gender,
|
||||||
|
n.name,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY n.gender ORDER BY n.name_id) AS row_number_in_category
|
||||||
|
FROM playerbots_names_ru n
|
||||||
|
LEFT JOIN characters used
|
||||||
|
ON used.name = CONVERT(n.name USING utf8mb4) COLLATE utf8mb4_bin
|
||||||
|
LEFT JOIN moonwell_playerbot_name_backup reserved
|
||||||
|
ON reserved.localized_name = CONVERT(n.name USING utf8mb4) COLLATE utf8mb4_bin
|
||||||
|
WHERE used.guid IS NULL
|
||||||
|
AND reserved.guid IS NULL;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_bot_targets;
|
||||||
|
CREATE TEMPORARY TABLE tmp_moonwell_bot_targets AS
|
||||||
|
SELECT
|
||||||
|
c.guid,
|
||||||
|
c.name AS original_name,
|
||||||
|
CASE
|
||||||
|
WHEN c.race IN (1, 5) THEN c.gender
|
||||||
|
WHEN c.race = 7 THEN 2 + c.gender
|
||||||
|
WHEN c.race = 3 THEN 4 + c.gender
|
||||||
|
WHEN c.race = 4 THEN 6 + c.gender
|
||||||
|
WHEN c.race = 11 THEN 8 + c.gender
|
||||||
|
WHEN c.race = 2 THEN 10 + c.gender
|
||||||
|
WHEN c.race = 8 THEN 12 + c.gender
|
||||||
|
WHEN c.race = 6 THEN 14 + c.gender
|
||||||
|
WHEN c.race = 10 THEN 16 + c.gender
|
||||||
|
END AS name_category,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY CASE
|
||||||
|
WHEN c.race IN (1, 5) THEN c.gender
|
||||||
|
WHEN c.race = 7 THEN 2 + c.gender
|
||||||
|
WHEN c.race = 3 THEN 4 + c.gender
|
||||||
|
WHEN c.race = 4 THEN 6 + c.gender
|
||||||
|
WHEN c.race = 11 THEN 8 + c.gender
|
||||||
|
WHEN c.race = 2 THEN 10 + c.gender
|
||||||
|
WHEN c.race = 8 THEN 12 + c.gender
|
||||||
|
WHEN c.race = 6 THEN 14 + c.gender
|
||||||
|
WHEN c.race = 10 THEN 16 + c.gender
|
||||||
|
END
|
||||||
|
ORDER BY c.guid
|
||||||
|
) AS row_number_in_category
|
||||||
|
FROM characters c
|
||||||
|
JOIN acore_playerbots.playerbots_account_type pat
|
||||||
|
ON pat.account_id = c.account AND pat.account_type = 1
|
||||||
|
WHERE REGEXP_LIKE(c.name, '^[[:ascii:]]+$');
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_character_renames;
|
||||||
|
CREATE TEMPORARY TABLE tmp_moonwell_character_renames AS
|
||||||
|
SELECT t.guid, t.original_name, n.name AS localized_name
|
||||||
|
FROM tmp_moonwell_bot_targets t
|
||||||
|
JOIN tmp_moonwell_available_names n
|
||||||
|
ON n.gender = t.name_category
|
||||||
|
AND n.row_number_in_category = t.row_number_in_category;
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
INSERT IGNORE INTO moonwell_playerbot_name_backup (guid, original_name, localized_name)
|
||||||
|
SELECT guid, original_name, localized_name
|
||||||
|
FROM tmp_moonwell_character_renames;
|
||||||
|
|
||||||
|
UPDATE characters c
|
||||||
|
JOIN moonwell_playerbot_name_backup b ON b.guid = c.guid
|
||||||
|
SET c.name = b.localized_name
|
||||||
|
WHERE c.name = b.original_name;
|
||||||
|
|
||||||
|
DELETE declined
|
||||||
|
FROM character_declinedname declined
|
||||||
|
JOIN moonwell_playerbot_name_backup b ON b.guid = declined.guid;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_available_guild_names;
|
||||||
|
CREATE TEMPORARY TABLE tmp_moonwell_available_guild_names AS
|
||||||
|
SELECT
|
||||||
|
n.name,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY n.name_id) AS row_number_in_pool
|
||||||
|
FROM playerbots_guild_names_ru n
|
||||||
|
LEFT JOIN guild used ON used.name = n.name
|
||||||
|
LEFT JOIN moonwell_playerbot_guild_name_backup reserved
|
||||||
|
ON reserved.localized_name = n.name
|
||||||
|
WHERE used.guildid IS NULL
|
||||||
|
AND reserved.guildid IS NULL;
|
||||||
|
|
||||||
|
DROP TEMPORARY TABLE IF EXISTS tmp_moonwell_guild_targets;
|
||||||
|
CREATE TEMPORARY TABLE tmp_moonwell_guild_targets AS
|
||||||
|
SELECT
|
||||||
|
g.guildid,
|
||||||
|
g.name AS original_name,
|
||||||
|
ROW_NUMBER() OVER (ORDER BY g.guildid) AS row_number_in_pool
|
||||||
|
FROM guild g
|
||||||
|
JOIN characters leader ON leader.guid = g.leaderguid
|
||||||
|
JOIN acore_playerbots.playerbots_account_type pat
|
||||||
|
ON pat.account_id = leader.account AND pat.account_type = 1
|
||||||
|
WHERE REGEXP_LIKE(g.name, '^[[:ascii:]]+$');
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
INSERT IGNORE INTO moonwell_playerbot_guild_name_backup (guildid, original_name, localized_name)
|
||||||
|
SELECT t.guildid, t.original_name, n.name
|
||||||
|
FROM tmp_moonwell_guild_targets t
|
||||||
|
JOIN tmp_moonwell_available_guild_names n
|
||||||
|
ON n.row_number_in_pool = t.row_number_in_pool;
|
||||||
|
|
||||||
|
UPDATE guild g
|
||||||
|
JOIN moonwell_playerbot_guild_name_backup b ON b.guildid = g.guildid
|
||||||
|
SET g.name = b.localized_name
|
||||||
|
WHERE g.name = b.original_name;
|
||||||
|
COMMIT;
|
||||||
|
"
|
||||||
|
|
||||||
|
read -r renamed_bot_count renamed_guild_count < <(
|
||||||
|
mysql_query acore_characters "
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM moonwell_playerbot_name_backup),
|
||||||
|
(SELECT COUNT(*) FROM moonwell_playerbot_guild_name_backup);
|
||||||
|
"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Localized RNDbot characters: $renamed_bot_count"
|
||||||
|
echo "Localized RNDbot-led guilds: $renamed_guild_count"
|
||||||
|
echo "Static ruRU texts and Russian name pools are installed."
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
local CONFIG = {
|
local CONFIG = {
|
||||||
maxCategories = 11,
|
maxCategories = 11,
|
||||||
strings = {
|
strings = {
|
||||||
categoryAccessDenied = "You do not have access to this category!",
|
categoryAccessDenied = "У вас нет доступа к этой категории!",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,8 @@ local KEYS = {
|
|||||||
rewardCount_7 = 27,
|
rewardCount_7 = 27,
|
||||||
rewardCount_8 = 28,
|
rewardCount_8 = 28,
|
||||||
new = 29,
|
new = 29,
|
||||||
enabled = 30
|
enabled = 30,
|
||||||
|
previewDisplayId = 31
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,14 @@ end
|
|||||||
|
|
||||||
local StoreHandler = AIO.AddHandlers("STORE_CLIENT", {})
|
local StoreHandler = AIO.AddHandlers("STORE_CLIENT", {})
|
||||||
|
|
||||||
function StoreHandler.FrameData(player, services, links, nav, currencies, rank)
|
function StoreHandler.FrameData(player, storeEnabled, services, links, nav, currencies, rank)
|
||||||
|
SHOP_UI["Vars"].storeEnabled = storeEnabled == 1
|
||||||
|
SHOP_UI.StoreAvailability_Update()
|
||||||
|
|
||||||
|
if not SHOP_UI["Vars"].storeEnabled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
SHOP_UI["Data"].services = services
|
SHOP_UI["Data"].services = services
|
||||||
SHOP_UI["Data"].links = links
|
SHOP_UI["Data"].links = links
|
||||||
SHOP_UI["Data"].nav = nav
|
SHOP_UI["Data"].nav = nav
|
||||||
@@ -112,6 +120,7 @@ SHOP_UI = {
|
|||||||
currentPage = 1,
|
currentPage = 1,
|
||||||
maxPages = 1,
|
maxPages = 1,
|
||||||
accountRank = 0,
|
accountRank = 0,
|
||||||
|
storeEnabled = false,
|
||||||
["playerCurrencies"] = {}
|
["playerCurrencies"] = {}
|
||||||
},
|
},
|
||||||
["Data"] = {
|
["Data"] = {
|
||||||
@@ -144,7 +153,13 @@ function SHOP_UI.MainFrame_Create()
|
|||||||
shopFrame.Title:SetFont("Fonts\\FRIZQT__.TTF", 14)
|
shopFrame.Title:SetFont("Fonts\\FRIZQT__.TTF", 14)
|
||||||
shopFrame.Title:SetShadowOffset(1, -1)
|
shopFrame.Title:SetShadowOffset(1, -1)
|
||||||
shopFrame.Title:SetPoint("TOP", shopFrame, "TOP", 0, -3)
|
shopFrame.Title:SetPoint("TOP", shopFrame, "TOP", 0, -3)
|
||||||
shopFrame.Title:SetText("|cffedd100Shop|r")
|
shopFrame.Title:SetText("|cffedd100Лавка Лунной Жрицы|r")
|
||||||
|
|
||||||
|
shopFrame.UnavailableText = shopFrame:CreateFontString()
|
||||||
|
shopFrame.UnavailableText:SetFont("Fonts\\FRIZQT__.TTF", 24, "OUTLINE")
|
||||||
|
shopFrame.UnavailableText:SetShadowOffset(1, -1)
|
||||||
|
shopFrame.UnavailableText:SetPoint("CENTER", shopFrame, "CENTER", 85, 0)
|
||||||
|
shopFrame.UnavailableText:SetText("|cffedd100Извините, магазин ещё не работает|r")
|
||||||
|
|
||||||
-- create navigation button placeholders, pass parent as arg
|
-- create navigation button placeholders, pass parent as arg
|
||||||
SHOP_UI.NavButtons_Create(shopFrame)
|
SHOP_UI.NavButtons_Create(shopFrame)
|
||||||
@@ -188,6 +203,7 @@ function SHOP_UI.MainFrame_Create()
|
|||||||
shopFrame:SetScript(
|
shopFrame:SetScript(
|
||||||
"OnShow",
|
"OnShow",
|
||||||
function()
|
function()
|
||||||
|
AIO.Handle("STORE_SERVER", "FrameData")
|
||||||
AIO.Handle("STORE_SERVER", "UpdateCurrencies")
|
AIO.Handle("STORE_SERVER", "UpdateCurrencies")
|
||||||
PlaySound("AuctionWindowOpen", "Master")
|
PlaySound("AuctionWindowOpen", "Master")
|
||||||
end
|
end
|
||||||
@@ -206,6 +222,32 @@ function SHOP_UI.MainFrame_Create()
|
|||||||
tinsert(UISpecialFrames, shopFrame:GetName())
|
tinsert(UISpecialFrames, shopFrame:GetName())
|
||||||
|
|
||||||
SHOP_UI["FRAME"] = shopFrame
|
SHOP_UI["FRAME"] = shopFrame
|
||||||
|
SHOP_UI.StoreAvailability_Update()
|
||||||
|
end
|
||||||
|
|
||||||
|
function SHOP_UI.StoreAvailability_Update()
|
||||||
|
local enabled = SHOP_UI["Vars"].storeEnabled
|
||||||
|
|
||||||
|
if SHOP_UI["FRAME"] and SHOP_UI["FRAME"].UnavailableText then
|
||||||
|
if enabled then
|
||||||
|
SHOP_UI["FRAME"].UnavailableText:Hide()
|
||||||
|
else
|
||||||
|
SHOP_UI["FRAME"].UnavailableText:Show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, collectionName in pairs({"NAV_BUTTONS", "SERVICE_BUTTONS", "PAGING_ELEMENTS", "CURRENCY_BUTTONS"}) do
|
||||||
|
local collection = SHOP_UI[collectionName]
|
||||||
|
if collection and not enabled then
|
||||||
|
for _, element in pairs(collection) do
|
||||||
|
element:Hide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if SHOP_UI["MODEL_FRAME"] and not enabled then
|
||||||
|
SHOP_UI["MODEL_FRAME"]:Hide()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- create navigation button placeholders
|
-- create navigation button placeholders
|
||||||
@@ -333,9 +375,9 @@ function SHOP_UI.OnPurchaseConfirm(data)
|
|||||||
end
|
end
|
||||||
|
|
||||||
StaticPopupDialogs["CONFIRM_STORE_PURCHASE"] = {
|
StaticPopupDialogs["CONFIRM_STORE_PURCHASE"] = {
|
||||||
text = "Are you sure you want to purchase %s?",
|
text = "Вы уверены, что хотите купить %s?",
|
||||||
button1 = "Yes",
|
button1 = "Да",
|
||||||
button2 = "No",
|
button2 = "Нет",
|
||||||
OnAccept = function(self, data)
|
OnAccept = function(self, data)
|
||||||
SHOP_UI.OnPurchaseConfirm(data)
|
SHOP_UI.OnPurchaseConfirm(data)
|
||||||
end,
|
end,
|
||||||
@@ -454,7 +496,7 @@ function SHOP_UI.ServiceBoxes_Create(parent)
|
|||||||
service.buyButton.ButtonText = service.buyButton:CreateFontString()
|
service.buyButton.ButtonText = service.buyButton:CreateFontString()
|
||||||
service.buyButton.ButtonText:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
|
service.buyButton.ButtonText:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
|
||||||
service.buyButton.ButtonText:SetPoint("CENTER", service.buyButton, 0, 0)
|
service.buyButton.ButtonText:SetPoint("CENTER", service.buyButton, 0, 0)
|
||||||
service.buyButton.ButtonText:SetText("Buy now!")
|
service.buyButton.ButtonText:SetText("Купить")
|
||||||
|
|
||||||
service.buyButton:SetScript(
|
service.buyButton:SetScript(
|
||||||
"OnClick",
|
"OnClick",
|
||||||
@@ -503,7 +545,7 @@ function SHOP_UI.ServiceBoxes_Create(parent)
|
|||||||
if(self.Type == 1 and service.Flags == 1) then
|
if(self.Type == 1 and service.Flags == 1) then
|
||||||
SHOP_UI.ModelFrame_ShowPlayer(self.Rewards)
|
SHOP_UI.ModelFrame_ShowPlayer(self.Rewards)
|
||||||
elseif((self.Type == 3 or self.Type == 4) and self.DisplayId > 0) then -- Handler for creatures with models
|
elseif((self.Type == 3 or self.Type == 4) and self.DisplayId > 0) then -- Handler for creatures with models
|
||||||
SHOP_UI.ModelFrame_ShowCreature(self.DisplayId)
|
SHOP_UI.ModelFrame_ShowCreature(self.DisplayId, self.PreviewDisplayId)
|
||||||
end
|
end
|
||||||
-- Parchment page sound
|
-- Parchment page sound
|
||||||
PlaySound(836)
|
PlaySound(836)
|
||||||
@@ -614,6 +656,7 @@ function SHOP_UI.ServiceBoxes_Update()
|
|||||||
service.Currency = serviceData[KEYS.service.currency]
|
service.Currency = serviceData[KEYS.service.currency]
|
||||||
service.TooltipHyperlink = serviceData[KEYS.service.hyperlink]
|
service.TooltipHyperlink = serviceData[KEYS.service.hyperlink]
|
||||||
service.DisplayId = serviceData[KEYS.service.displayId]
|
service.DisplayId = serviceData[KEYS.service.displayId]
|
||||||
|
service.PreviewDisplayId = serviceData[KEYS.service.previewDisplayId] or 0
|
||||||
service.Discount = serviceData[KEYS.service.discount]
|
service.Discount = serviceData[KEYS.service.discount]
|
||||||
service.Flags = serviceData[KEYS.service.flags]
|
service.Flags = serviceData[KEYS.service.flags]
|
||||||
service.New = serviceData[KEYS.service.new]
|
service.New = serviceData[KEYS.service.new]
|
||||||
@@ -635,7 +678,7 @@ function SHOP_UI.ServiceBoxes_Update()
|
|||||||
|
|
||||||
-- calculate discount percentage
|
-- calculate discount percentage
|
||||||
local discountPct = math.floor(((service.Price - service.Discount) - service.Price) / service.Price * 100)
|
local discountPct = math.floor(((service.Price - service.Discount) - service.Price) / service.Price * 100)
|
||||||
service.BannerText:SetFormattedText("|cffffffffOn sale: %i%%|r", discountPct)
|
service.BannerText:SetFormattedText("|cffffffffСкидка: %i%%|r", discountPct)
|
||||||
|
|
||||||
-- if service is discounted, then show all the discount frames and override the price text. otherwise hide.
|
-- if service is discounted, then show all the discount frames and override the price text. otherwise hide.
|
||||||
if service.Discount > 1 then
|
if service.Discount > 1 then
|
||||||
@@ -1150,7 +1193,7 @@ function SHOP_UI.ModelFrame_ShowPlayer(displayId)
|
|||||||
PlaySound("INTERFACESOUND_GAMESCROLLBUTTON", "Master")
|
PlaySound("INTERFACESOUND_GAMESCROLLBUTTON", "Master")
|
||||||
end
|
end
|
||||||
|
|
||||||
function SHOP_UI.ModelFrame_ShowCreature(displayId)
|
function SHOP_UI.ModelFrame_ShowCreature(creatureEntry, previewDisplayId)
|
||||||
-- hacky ass model frame handling
|
-- hacky ass model frame handling
|
||||||
-- hide model frames
|
-- hide model frames
|
||||||
if(SHOP_UI["MODEL_FRAME"].playerModel:IsShown()) then
|
if(SHOP_UI["MODEL_FRAME"].playerModel:IsShown()) then
|
||||||
@@ -1161,7 +1204,11 @@ function SHOP_UI.ModelFrame_ShowCreature(displayId)
|
|||||||
SHOP_UI["MODEL_FRAME"]:Hide()
|
SHOP_UI["MODEL_FRAME"]:Hide()
|
||||||
|
|
||||||
-- set the correct unit and show frame
|
-- set the correct unit and show frame
|
||||||
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(displayId)
|
if(previewDisplayId and previewDisplayId > 0) then
|
||||||
|
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(creatureEntry, previewDisplayId)
|
||||||
|
else
|
||||||
|
SHOP_UI["MODEL_FRAME"].creatureModel:SetCreature(creatureEntry)
|
||||||
|
end
|
||||||
SHOP_UI["MODEL_FRAME"].creatureModel:Show()
|
SHOP_UI["MODEL_FRAME"].creatureModel:Show()
|
||||||
SHOP_UI["MODEL_FRAME"]:Show()
|
SHOP_UI["MODEL_FRAME"]:Show()
|
||||||
|
|
||||||
@@ -1193,7 +1240,7 @@ local function ModifyGameMenuFrame()
|
|||||||
storeButton.Text:SetFont("Fonts\\FRIZQT__.TTF", 13, "OUTLINE")
|
storeButton.Text:SetFont("Fonts\\FRIZQT__.TTF", 13, "OUTLINE")
|
||||||
storeButton.Text:SetShadowOffset(1, -1)
|
storeButton.Text:SetShadowOffset(1, -1)
|
||||||
storeButton.Text:SetPoint("CENTER", storeButton, "CENTER", 0, 1)
|
storeButton.Text:SetPoint("CENTER", storeButton, "CENTER", 0, 1)
|
||||||
storeButton.Text:SetText("|cffdbe005Store");
|
storeButton.Text:SetText("|cffdbe005Лавка Лунной Жрицы");
|
||||||
|
|
||||||
-- on click open the shop frame and hide the escape menu
|
-- on click open the shop frame and hide the escape menu
|
||||||
storeButton:SetScript("OnClick", function()
|
storeButton:SetScript("OnClick", function()
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ local KEYS = {
|
|||||||
rewardCount_7 = 27,
|
rewardCount_7 = 27,
|
||||||
rewardCount_8 = 28,
|
rewardCount_8 = 28,
|
||||||
new = 29,
|
new = 29,
|
||||||
enabled = 30
|
enabled = 30,
|
||||||
|
previewDisplayId = 31
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +140,8 @@ function ServiceData.Load()
|
|||||||
Query:GetUInt32(KEYS.service.rewardCount_7),
|
Query:GetUInt32(KEYS.service.rewardCount_7),
|
||||||
Query:GetUInt32(KEYS.service.rewardCount_8),
|
Query:GetUInt32(KEYS.service.rewardCount_8),
|
||||||
Query:GetUInt32(KEYS.service.new),
|
Query:GetUInt32(KEYS.service.new),
|
||||||
|
Query:GetUInt32(KEYS.service.enabled),
|
||||||
|
0,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
until not Query:NextRow()
|
until not Query:NextRow()
|
||||||
@@ -224,6 +227,27 @@ function CreatureDisplays.Load()
|
|||||||
until not ModelQuery:NextRow()
|
until not ModelQuery:NextRow()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Keep the creature entry for the stock preview path, but also send its
|
||||||
|
-- primary CreatureDisplayInfo ID. WarcraftXL can apply that display
|
||||||
|
-- directly, avoiding stale or missing client creature-cache records.
|
||||||
|
for _, service in pairs(ServiceData.Cache) do
|
||||||
|
local serviceType = service[KEYS.service.serviceType]
|
||||||
|
local entry = service[KEYS.service.displayOrEntry]
|
||||||
|
if ((serviceType == 3 or serviceType == 4) and entry > 0) then
|
||||||
|
local creature = CreatureDisplays.Cache[entry]
|
||||||
|
local displayId = 0
|
||||||
|
if (creature) then
|
||||||
|
for modelIndex = 11, 14 do
|
||||||
|
if (creature[modelIndex] and creature[modelIndex] > 0) then
|
||||||
|
displayId = creature[modelIndex]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
service[KEYS.service.previewDisplayId] = displayId
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function LinkData.Load()
|
function LinkData.Load()
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ local CONFIG = {
|
|||||||
mailSenderGUID = 1, -- GUID of the character shown as sender of purchase mails
|
mailSenderGUID = 1, -- GUID of the character shown as sender of purchase mails
|
||||||
strings = {
|
strings = {
|
||||||
-- Currency name is appended to the end of this string
|
-- Currency name is appended to the end of this string
|
||||||
insufficientFunds = "You don't have enough",
|
insufficientFunds = "Недостаточно средств на балансе",
|
||||||
-- Type is appended, ie. title, mount, pet etc.
|
-- Type is appended, ie. title, mount, pet etc.
|
||||||
alreadyKnown = "You already have this",
|
alreadyKnownMount = "Этот транспорт уже изучен",
|
||||||
tooHighLevel = "Your level is too high",
|
alreadyKnownPet = "Этот питомец уже изучен",
|
||||||
mailBody = "Thank you for your purchase!",
|
alreadyKnownTitle = "Этот титул уже получен",
|
||||||
|
tooHighLevel = "Уровень персонажа слишком высок",
|
||||||
|
mailBody = "Благодарим за покупку в Лавке Лунной Жрицы!",
|
||||||
-- The service name is prefixed to this message
|
-- The service name is prefixed to this message
|
||||||
successfulPurchase = "successfully purchased!"
|
successfulPurchase = "Покупка успешно совершена!",
|
||||||
|
storeUnavailable = "Магазин пока не работает"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +25,24 @@ local CONFIG = {
|
|||||||
local AIO = AIO or require("AIO") and require("Store_DataStruct")
|
local AIO = AIO or require("AIO") and require("Store_DataStruct")
|
||||||
|
|
||||||
local CURRENCY_TYPES = {
|
local CURRENCY_TYPES = {
|
||||||
[1] = "GOLD",
|
|
||||||
[2] = "ITEM_TOKEN",
|
|
||||||
[3] = "SERVER_HANDLED"
|
[3] = "SERVER_HANDLED"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local purchaseReferenceSequence = 0
|
||||||
|
|
||||||
|
local function GetAccountBalance(player)
|
||||||
|
local query = AuthDBQuery(
|
||||||
|
"SELECT CAST(`balance` AS DOUBLE) FROM `account_balances` WHERE `account_id` = " ..
|
||||||
|
player:GetAccountId() .. " LIMIT 1;"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not query then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
return query:GetDouble(0)
|
||||||
|
end
|
||||||
|
|
||||||
local SHOP_UI = {
|
local SHOP_UI = {
|
||||||
serviceHandlers = {
|
serviceHandlers = {
|
||||||
[1] = "ItemHandler", -- Okay
|
[1] = "ItemHandler", -- Okay
|
||||||
@@ -45,27 +61,38 @@ local KEYS = GetDataStructKeys();
|
|||||||
|
|
||||||
local StoreHandler = AIO.AddHandlers("STORE_SERVER", {})
|
local StoreHandler = AIO.AddHandlers("STORE_SERVER", {})
|
||||||
|
|
||||||
|
local function IsStoreEnabled()
|
||||||
|
local query = WorldDBQuery("SELECT `enabled` FROM `store`.`store_config` WHERE `id` = 1 LIMIT 1;")
|
||||||
|
return query and query:GetUInt32(0) == 1
|
||||||
|
end
|
||||||
|
|
||||||
function StoreHandler.FrameData(player)
|
function StoreHandler.FrameData(player)
|
||||||
AIO.Handle(player, "STORE_CLIENT", "FrameData", GetServiceData(), GetLinkData(), GetNavData(), GetCurrencyData(), player:GetGMRank())
|
local enabled = IsStoreEnabled()
|
||||||
|
AIO.Handle(
|
||||||
|
player,
|
||||||
|
"STORE_CLIENT",
|
||||||
|
"FrameData",
|
||||||
|
enabled and 1 or 0,
|
||||||
|
enabled and GetServiceData() or {},
|
||||||
|
enabled and GetLinkData() or {},
|
||||||
|
enabled and GetNavData() or {},
|
||||||
|
enabled and GetCurrencyData() or {},
|
||||||
|
player:GetGMRank()
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
function StoreHandler.UpdateCurrencies(player)
|
function StoreHandler.UpdateCurrencies(player)
|
||||||
|
if not IsStoreEnabled() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local tmp = {}
|
local tmp = {}
|
||||||
for currencyId, currency in pairs(GetCurrencyData()) do
|
for currencyId, currency in pairs(GetCurrencyData()) do
|
||||||
local val = 0
|
local val = 0
|
||||||
local currencyTypeText = CURRENCY_TYPES[currency[KEYS.currency.currencyType]]
|
local currencyTypeText = CURRENCY_TYPES[currency[KEYS.currency.currencyType]]
|
||||||
|
|
||||||
-- Handle the different currency types
|
|
||||||
if(currencyTypeText == "GOLD") then
|
|
||||||
val = math.floor(player:GetCoinage() / 10000)
|
|
||||||
end
|
|
||||||
|
|
||||||
if(currencyTypeText == "ITEM_TOKEN") then
|
|
||||||
val = player:GetItemCount(currency[KEYS.currency.data])
|
|
||||||
end
|
|
||||||
|
|
||||||
if(currencyTypeText == "SERVER_HANDLED") then
|
if(currencyTypeText == "SERVER_HANDLED") then
|
||||||
-- Add your custom handling here for retreiving your server handled currencies
|
val = GetAccountBalance(player)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If value is larger than 10k then truncate to make sure it fits within the shop frame
|
-- If value is larger than 10k then truncate to make sure it fits within the shop frame
|
||||||
@@ -73,12 +100,19 @@ function StoreHandler.UpdateCurrencies(player)
|
|||||||
val = "9999+"
|
val = "9999+"
|
||||||
end
|
end
|
||||||
|
|
||||||
table.insert(tmp, val)
|
-- Preserve the database currency id. A positional array silently breaks as
|
||||||
|
-- soon as ids have gaps or are reordered by MySQL.
|
||||||
|
tmp[currencyId] = val
|
||||||
end
|
end
|
||||||
AIO.Handle(player, "STORE_CLIENT", "UpdateCurrencies", tmp)
|
AIO.Handle(player, "STORE_CLIENT", "UpdateCurrencies", tmp)
|
||||||
end
|
end
|
||||||
|
|
||||||
function StoreHandler.Purchase(player, serviceId)
|
function StoreHandler.Purchase(player, serviceId)
|
||||||
|
if not IsStoreEnabled() then
|
||||||
|
player:SendAreaTriggerMessage(CONFIG.strings.storeUnavailable)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local services = GetServiceData()
|
local services = GetServiceData()
|
||||||
|
|
||||||
-- See if the requested service exists
|
-- See if the requested service exists
|
||||||
@@ -99,46 +133,45 @@ function StoreHandler.Purchase(player, serviceId)
|
|||||||
player:PlayDirectSound(120, player)
|
player:PlayDirectSound(120, player)
|
||||||
|
|
||||||
-- Send success toast
|
-- Send success toast
|
||||||
player:SendAreaTriggerMessage(services[serviceId][KEYS.service.name] .. " "..CONFIG.strings.successfulPurchase)
|
player:SendAreaTriggerMessage(CONFIG.strings.successfulPurchase)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Helper functions
|
-- Helper functions
|
||||||
function SHOP_UI.DeductCurrency(player, currencyId, amount)
|
function SHOP_UI.DeductCurrency(player, currencyId, amount, serviceId)
|
||||||
local currency = GetCurrencyData()
|
local currency = GetCurrencyData()
|
||||||
local currencyType = currency[currencyId][KEYS.currency.currencyType]
|
if not currency[currencyId] then
|
||||||
local currencyName = currency[currencyId][KEYS.currency.name]
|
|
||||||
local currencyData = currency[currencyId][KEYS.currency.data]
|
|
||||||
|
|
||||||
-- Gold handling
|
|
||||||
if(CURRENCY_TYPES[currencyType] == "GOLD") then
|
|
||||||
if(player:GetCoinage() < amount * 10000) then
|
|
||||||
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.insufficientFunds.." "..currencyName.."|r")
|
|
||||||
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
player:SetCoinage(player:GetCoinage() - (amount * 10000))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Token handling
|
|
||||||
if(CURRENCY_TYPES[currencyType] == "ITEM_TOKEN") then
|
|
||||||
if not(player:HasItem(currencyData, amount)) then
|
|
||||||
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.insufficientFunds.." "..currencyName.."|r")
|
|
||||||
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
player:RemoveItem(currencyData, amount)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Other special handlingm you have to add your own integration here.
|
|
||||||
if(CURRENCY_TYPES[currencyType] == "SERVER_HANDLED") then
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local currencyType = currency[currencyId][KEYS.currency.currencyType]
|
||||||
|
if CURRENCY_TYPES[currencyType] ~= "SERVER_HANDLED" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
purchaseReferenceSequence = purchaseReferenceSequence + 1
|
||||||
|
local reference = string.format(
|
||||||
|
"game-store:%u:%u:%u:%u:%u",
|
||||||
|
player:GetAccountId(),
|
||||||
|
player:GetGUIDLow(),
|
||||||
|
serviceId or 0,
|
||||||
|
os.time(),
|
||||||
|
purchaseReferenceSequence
|
||||||
|
)
|
||||||
|
local result = AuthDBQuery(
|
||||||
|
"SELECT `store_deduct_balance`(" .. player:GetAccountId() .. ", " .. amount .. ", '" .. reference .. "', " ..
|
||||||
|
player:GetGUIDLow() .. ", " .. (serviceId or 0) .. ") AS `balance_after`;"
|
||||||
|
)
|
||||||
|
local balanceAfter = result and result:GetDouble(0) or -1
|
||||||
|
|
||||||
|
if balanceAfter < 0 then
|
||||||
|
player:SendAreaTriggerMessage("|cFFFF0000" .. CONFIG.strings.insufficientFunds .. "|r")
|
||||||
|
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -152,7 +185,7 @@ function SHOP_UI.ItemHandler(player, data)
|
|||||||
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -169,7 +202,7 @@ function SHOP_UI.ItemHandler(player, data)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Send reward mail
|
-- Send reward mail
|
||||||
SendMail("Purchase of: "..data[KEYS.service.name], CONFIG.strings.mailBody, player:GetGUIDLow(), CONFIG.mailSenderGUID, 62, 0, 0, 0, unpack(items))
|
SendMail("Покупка: "..data[KEYS.service.name], CONFIG.strings.mailBody, player:GetGUIDLow(), CONFIG.mailSenderGUID, 62, 0, 0, 0, unpack(items))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -178,7 +211,7 @@ function SHOP_UI.GoldHandler(player, data)
|
|||||||
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -206,13 +239,13 @@ function SHOP_UI.MountHandler(player, data)
|
|||||||
|
|
||||||
-- check if player already has the spells learned
|
-- check if player already has the spells learned
|
||||||
if(knownCount == rewardCount) then
|
if(knownCount == rewardCount) then
|
||||||
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." mount|r")
|
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnownMount.."|r")
|
||||||
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
|
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -244,13 +277,13 @@ function SHOP_UI.PetHandler(player, data)
|
|||||||
|
|
||||||
-- check if player already has the spells learned
|
-- check if player already has the spells learned
|
||||||
if(knownCount == rewardCount) then
|
if(knownCount == rewardCount) then
|
||||||
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." pet|r")
|
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnownPet.."|r")
|
||||||
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
|
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -271,7 +304,7 @@ function SHOP_UI.BuffHandler(player, data)
|
|||||||
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -292,7 +325,7 @@ function SHOP_UI.ServiceHandler(player, data)
|
|||||||
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -319,7 +352,7 @@ function SHOP_UI.LevelHandler(player, data)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -348,13 +381,13 @@ function SHOP_UI.TitleHandler(player, data)
|
|||||||
|
|
||||||
-- Check whether or not the player already has the specified title
|
-- Check whether or not the player already has the specified title
|
||||||
if(player:HasTitle(data[KEYS.service.reward_1])) then
|
if(player:HasTitle(data[KEYS.service.reward_1])) then
|
||||||
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." title|r")
|
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnownTitle.."|r")
|
||||||
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
|
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Deduct currency
|
-- Deduct currency
|
||||||
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
|
local deducted = SHOP_UI.DeductCurrency(player, currency, amount, data.ID)
|
||||||
|
|
||||||
-- If currency was not deducted from the player, abort and send message
|
-- If currency was not deducted from the player, abort and send message
|
||||||
if not(deducted) then
|
if not(deducted) then
|
||||||
@@ -372,4 +405,4 @@ function SHOP_UI.UnusedHandler(player, data)
|
|||||||
|
|
||||||
-- Since this is unused, always return false until the function is in use.
|
-- Since this is unused, always return false until the function is in use.
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
local PLAYER_EVENT_ON_LOGIN = 3
|
-- Login greeting is managed by the native AzerothCore MOTD in
|
||||||
|
-- import-custom-sql.sh. Keep this file as a harmless Eluna loading example.
|
||||||
local function OnLogin(event, player)
|
|
||||||
player:SendBroadcastMessage("Eluna greetings you")
|
|
||||||
end
|
|
||||||
|
|
||||||
RegisterPlayerEvent(PLAYER_EVENT_ON_LOGIN, OnLogin)
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Game-mode UI synchronization is client-native since MoonWellClient 2.0.
|
||||||
|
--
|
||||||
|
-- The authoritative mode still comes from the server in SMSG_CHAR_ENUM as
|
||||||
|
-- CHARACTER_FLAG_UNK31 (0x40000000). WarcraftXL retains that bit while WoW
|
||||||
|
-- switches from GlueXML to the in-world FrameXML Lua state. Keeping this file
|
||||||
|
-- as a no-op avoids stale deployment scripts failing on a missing path while
|
||||||
|
-- removing the timing-sensitive AIO login event completely.
|
||||||
@@ -1207,6 +1207,9 @@ public:
|
|||||||
|
|
||||||
void OnHeal(Unit* healer, Unit* receiver, uint32& gain) override
|
void OnHeal(Unit* healer, Unit* receiver, uint32& gain) override
|
||||||
{
|
{
|
||||||
|
if (!healer)
|
||||||
|
return;
|
||||||
|
|
||||||
if (healer->IsPlayer())
|
if (healer->IsPlayer())
|
||||||
sEluna->OnPlayerHeal(healer->ToPlayer(), receiver, gain);
|
sEluna->OnPlayerHeal(healer->ToPlayer(), receiver, gain);
|
||||||
|
|
||||||
@@ -1216,6 +1219,9 @@ public:
|
|||||||
|
|
||||||
void OnDamage(Unit* attacker, Unit* receiver, uint32& damage) override
|
void OnDamage(Unit* attacker, Unit* receiver, uint32& damage) override
|
||||||
{
|
{
|
||||||
|
if (!attacker)
|
||||||
|
return;
|
||||||
|
|
||||||
if (attacker->IsPlayer())
|
if (attacker->IsPlayer())
|
||||||
sEluna->OnPlayerDamage(attacker->ToPlayer(), receiver, damage);
|
sEluna->OnPlayerDamage(attacker->ToPlayer(), receiver, damage);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# Game Modes module configuration
|
||||||
|
#
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# GameModes settings
|
||||||
|
########################################
|
||||||
|
|
||||||
|
#
|
||||||
|
# GameModes.Enable
|
||||||
|
# Description: Enable game modes system
|
||||||
|
# Default: 1 - (Enabled)
|
||||||
|
# 0 - (Disabled)
|
||||||
|
#
|
||||||
|
|
||||||
|
GameModes.Enable = 1
|
||||||
|
|
||||||
|
#
|
||||||
|
# GameModes.Traitor.Enable
|
||||||
|
# Description: Enable Traitor game mode
|
||||||
|
# Default: 1 - (Enabled)
|
||||||
|
# 0 - (Disabled)
|
||||||
|
#
|
||||||
|
|
||||||
|
GameModes.Traitor.Enable = 1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
DROP TABLE IF EXISTS `mod_gamemodes_characters`;
|
||||||
|
CREATE TABLE `mod_gamemodes_characters` (
|
||||||
|
`guid` INT UNSIGNED NOT NULL,
|
||||||
|
`game_mode` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 = NORMAL, 1 = TRAITOR',
|
||||||
|
PRIMARY KEY (`guid`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include "GameModes.h"
|
||||||
|
#include "DatabaseEnv.h"
|
||||||
|
#include "Log.h"
|
||||||
|
|
||||||
|
GameModeMgr* GameModeMgr::Instance()
|
||||||
|
{
|
||||||
|
static GameModeMgr instance;
|
||||||
|
return &instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameModeMgr::LoadFromDB()
|
||||||
|
{
|
||||||
|
uint32 oldMSTime = getMSTime();
|
||||||
|
_gameModes.clear();
|
||||||
|
|
||||||
|
QueryResult result = CharacterDatabase.Query("SELECT `guid`, `game_mode` FROM `mod_gamemodes_characters`");
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
LOG_INFO("module", ">> Loaded 0 character game modes. Table `mod_gamemodes_characters` is empty.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 count = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Field* fields = result->Fetch();
|
||||||
|
ObjectGuid::LowType guid = fields[0].Get<uint32>();
|
||||||
|
uint8 mode = fields[1].Get<uint8>();
|
||||||
|
|
||||||
|
if (mode >= GAMEMODE_MAX)
|
||||||
|
{
|
||||||
|
LOG_ERROR("module", "mod-gamemodes: Invalid game mode {} for character {}, skipping.", mode, guid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameModes[guid] = static_cast<GameMode>(mode);
|
||||||
|
++count;
|
||||||
|
} while (result->NextRow());
|
||||||
|
|
||||||
|
LOG_INFO("module", ">> Loaded {} character game modes in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameModeMgr::SaveGameMode(ObjectGuid::LowType guid, GameMode mode)
|
||||||
|
{
|
||||||
|
_gameModes[guid] = mode;
|
||||||
|
|
||||||
|
CharacterDatabase.Execute("REPLACE INTO `mod_gamemodes_characters` (`guid`, `game_mode`) VALUES ({}, {})",
|
||||||
|
guid, static_cast<uint8>(mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameModeMgr::DeleteGameMode(ObjectGuid::LowType guid)
|
||||||
|
{
|
||||||
|
_gameModes.erase(guid);
|
||||||
|
|
||||||
|
CharacterDatabase.Execute("DELETE FROM `mod_gamemodes_characters` WHERE `guid` = {}", guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameMode GameModeMgr::GetGameMode(ObjectGuid::LowType guid) const
|
||||||
|
{
|
||||||
|
auto it = _gameModes.find(guid);
|
||||||
|
if (it != _gameModes.end())
|
||||||
|
return it->second;
|
||||||
|
return GAMEMODE_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameModeMgr::HasGameMode(ObjectGuid::LowType guid, GameMode mode) const
|
||||||
|
{
|
||||||
|
return GetGameMode(guid) == mode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef MOD_GAMEMODES_H
|
||||||
|
#define MOD_GAMEMODES_H
|
||||||
|
|
||||||
|
#include "Player.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
enum GameMode : uint8
|
||||||
|
{
|
||||||
|
GAMEMODE_NORMAL = 0,
|
||||||
|
GAMEMODE_TRAITOR = 1,
|
||||||
|
GAMEMODE_MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom bit in SMSG_CHAR_ENUM character flags.
|
||||||
|
// Chosen as a high unused bit so the packet layout stays unchanged.
|
||||||
|
constexpr uint32 GAMEMODE_CHARACTER_FLAG_TRAITOR = 0x40000000;
|
||||||
|
|
||||||
|
class GameModeMgr
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static GameModeMgr* Instance();
|
||||||
|
|
||||||
|
void LoadFromDB();
|
||||||
|
void SaveGameMode(ObjectGuid::LowType guid, GameMode mode);
|
||||||
|
void DeleteGameMode(ObjectGuid::LowType guid);
|
||||||
|
|
||||||
|
GameMode GetGameMode(ObjectGuid::LowType guid) const;
|
||||||
|
bool HasGameMode(ObjectGuid::LowType guid, GameMode mode) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GameModeMgr() = default;
|
||||||
|
std::unordered_map<ObjectGuid::LowType, GameMode> _gameModes;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define sGameModeMgr GameModeMgr::Instance()
|
||||||
|
|
||||||
|
#endif // MOD_GAMEMODES_H
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
void AddSC_TraitorMode();
|
||||||
|
|
||||||
|
void Addmod_gamemodesScripts()
|
||||||
|
{
|
||||||
|
AddSC_TraitorMode();
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
#include "TraitorMode.h"
|
||||||
|
#include "GameModes.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include "DBCStores.h"
|
||||||
|
#include "DatabaseEnv.h"
|
||||||
|
#include "Log.h"
|
||||||
|
#include "ObjectMgr.h"
|
||||||
|
#include "ReputationMgr.h"
|
||||||
|
#include "ScriptMgr.h"
|
||||||
|
#include "WorldPacket.h"
|
||||||
|
#include "WorldSession.h"
|
||||||
|
|
||||||
|
namespace TraitorMode
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void NormalizeFactionState(Player* player, FactionEntry const* factionEntry, bool visible, bool atWar)
|
||||||
|
{
|
||||||
|
if (!factionEntry || factionEntry->reputationListID < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FactionState* factionState = const_cast<FactionState*>(player->GetReputationMgr().GetState(factionEntry));
|
||||||
|
if (!factionState)
|
||||||
|
return;
|
||||||
|
|
||||||
|
factionState->Flags &= ~(FACTION_FLAG_VISIBLE | FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED |
|
||||||
|
FACTION_FLAG_PEACE_FORCED | FACTION_FLAG_INACTIVE);
|
||||||
|
|
||||||
|
if (visible)
|
||||||
|
factionState->Flags |= FACTION_FLAG_VISIBLE;
|
||||||
|
else
|
||||||
|
factionState->Flags |= FACTION_FLAG_HIDDEN | FACTION_FLAG_INVISIBLE_FORCED;
|
||||||
|
|
||||||
|
if (atWar)
|
||||||
|
factionState->Flags |= FACTION_FLAG_AT_WAR;
|
||||||
|
else
|
||||||
|
factionState->Flags &= ~FACTION_FLAG_AT_WAR;
|
||||||
|
|
||||||
|
factionState->needSend = true;
|
||||||
|
factionState->needSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetFactionStanding(Player* player, uint32 factionId, int32 targetDisplayed, bool visible, bool atWar)
|
||||||
|
{
|
||||||
|
FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId);
|
||||||
|
if (!factionEntry)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// ReputationMgr::GetBaseReputation now always uses the player's actual race
|
||||||
|
// mask, so SetOneFactionReputation is safe to call directly.
|
||||||
|
player->GetReputationMgr().SetOneFactionReputation(factionEntry, static_cast<float>(targetDisplayed), false);
|
||||||
|
NormalizeFactionState(player, factionEntry, visible, atWar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NormalizeTraitorReputationStates(Player* player)
|
||||||
|
{
|
||||||
|
TeamId originalTeam = player->GetTeamId(true);
|
||||||
|
uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE)
|
||||||
|
? HORDE_REPUTATION_FACTIONS
|
||||||
|
: ALLIANCE_REPUTATION_FACTIONS;
|
||||||
|
uint32 const* oldFactions = (originalTeam == TEAM_ALLIANCE)
|
||||||
|
? ALLIANCE_REPUTATION_FACTIONS
|
||||||
|
: HORDE_REPUTATION_FACTIONS;
|
||||||
|
|
||||||
|
auto normalizeFactionAndParent = [player](uint32 factionId, bool visible, bool atWar)
|
||||||
|
{
|
||||||
|
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
|
||||||
|
{
|
||||||
|
NormalizeFactionState(player, factionEntry, visible, atWar);
|
||||||
|
|
||||||
|
if (factionEntry->team)
|
||||||
|
NormalizeFactionState(player, sFactionStore.LookupEntry(factionEntry->team), visible, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint32 i = 0; i < FACTION_COUNT; ++i)
|
||||||
|
{
|
||||||
|
normalizeFactionAndParent(newFactions[i], true, false);
|
||||||
|
normalizeFactionAndParent(oldFactions[i], false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendFactionVisiblePacket(Player* player, FactionEntry const* factionEntry)
|
||||||
|
{
|
||||||
|
if (!factionEntry || factionEntry->reputationListID < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
WorldPacket data(SMSG_SET_FACTION_VISIBLE, 4);
|
||||||
|
data << uint32(factionEntry->reputationListID);
|
||||||
|
player->SendDirectMessage(&data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendTraitorFactionVisibility(Player* player)
|
||||||
|
{
|
||||||
|
TeamId originalTeam = player->GetTeamId(true);
|
||||||
|
uint32 const* newFactions = (originalTeam == TEAM_ALLIANCE)
|
||||||
|
? HORDE_REPUTATION_FACTIONS
|
||||||
|
: ALLIANCE_REPUTATION_FACTIONS;
|
||||||
|
auto sendFactionAndParent = [player](uint32 factionId)
|
||||||
|
{
|
||||||
|
if (FactionEntry const* factionEntry = sFactionStore.LookupEntry(factionId))
|
||||||
|
{
|
||||||
|
SendFactionVisiblePacket(player, factionEntry);
|
||||||
|
|
||||||
|
if (factionEntry->team)
|
||||||
|
SendFactionVisiblePacket(player, sFactionStore.LookupEntry(factionEntry->team));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint32 i = 0; i < FACTION_COUNT; ++i)
|
||||||
|
sendFactionAndParent(newFactions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyFactionSwap(Player* player)
|
||||||
|
{
|
||||||
|
TeamId originalTeam = player->GetTeamId(true);
|
||||||
|
TeamId newTeam = (originalTeam == TEAM_ALLIANCE) ? TEAM_HORDE : TEAM_ALLIANCE;
|
||||||
|
|
||||||
|
player->setTeamId(newTeam);
|
||||||
|
|
||||||
|
// Derive faction template from the canonical race for the new team (same
|
||||||
|
// logic as SetFactionForRace), so we never rely on hardcoded DBC IDs.
|
||||||
|
uint8 representativeRace = (newTeam == TEAM_HORDE) ? HORDE_DEFAULT_RACE : ALLIANCE_DEFAULT_RACE;
|
||||||
|
if (ChrRacesEntry const* rEntry = sChrRacesStore.LookupEntry(representativeRace))
|
||||||
|
player->SetFaction(rEntry->FactionID);
|
||||||
|
|
||||||
|
LOG_INFO("module", "mod-gamemodes: ApplyFactionSwap for {} (GUID: {}), original team = {}, new team = {}, factionTemplate = {}",
|
||||||
|
player->GetName(), player->GetGUID().GetCounter(),
|
||||||
|
(originalTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"),
|
||||||
|
(newTeam == TEAM_ALLIANCE ? "Alliance" : "Horde"),
|
||||||
|
player->GetFaction());
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitTraitorReputations(Player* player)
|
||||||
|
{
|
||||||
|
TeamId originalTeam = player->GetTeamId(true);
|
||||||
|
|
||||||
|
// New faction reputations -> Neutral (standing = 0)
|
||||||
|
const uint32* newFactions = (originalTeam == TEAM_ALLIANCE)
|
||||||
|
? HORDE_REPUTATION_FACTIONS
|
||||||
|
: ALLIANCE_REPUTATION_FACTIONS;
|
||||||
|
|
||||||
|
for (uint32 i = 0; i < FACTION_COUNT; ++i)
|
||||||
|
SetFactionStanding(player, newFactions[i], 0, true, false);
|
||||||
|
|
||||||
|
// Old faction reputations -> Hostile (standing = -6000)
|
||||||
|
const uint32* oldFactions = (originalTeam == TEAM_ALLIANCE)
|
||||||
|
? ALLIANCE_REPUTATION_FACTIONS
|
||||||
|
: HORDE_REPUTATION_FACTIONS;
|
||||||
|
|
||||||
|
for (uint32 i = 0; i < FACTION_COUNT; ++i)
|
||||||
|
SetFactionStanding(player, oldFactions[i], -6000, false, true);
|
||||||
|
|
||||||
|
LOG_INFO("module", "mod-gamemodes: InitTraitorReputations for {} (GUID: {})",
|
||||||
|
player->GetName(), player->GetGUID().GetCounter());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TeleportToNewFactionStart(Player* player)
|
||||||
|
{
|
||||||
|
TeamId originalTeam = player->GetTeamId(true);
|
||||||
|
uint8 spawnRace = (originalTeam == TEAM_ALLIANCE)
|
||||||
|
? HORDE_DEFAULT_RACE
|
||||||
|
: ALLIANCE_DEFAULT_RACE;
|
||||||
|
|
||||||
|
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(spawnRace, player->getClass());
|
||||||
|
if (!info)
|
||||||
|
{
|
||||||
|
// Fallback: try warrior (class 1) for that race
|
||||||
|
info = sObjectMgr->GetPlayerInfo(spawnRace, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info)
|
||||||
|
{
|
||||||
|
LOG_ERROR("module", "mod-gamemodes: No spawn info for race {} class {}", spawnRace, player->getClass());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player->TeleportTo(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation);
|
||||||
|
|
||||||
|
LOG_INFO("module", "mod-gamemodes: Teleported {} to faction start (map {}, {:.1f}, {:.1f}, {:.1f})",
|
||||||
|
player->GetName(), info->mapId, info->positionX, info->positionY, info->positionZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TraitorModePlayer : public PlayerScript
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TraitorModePlayer() : PlayerScript("TraitorModePlayer",
|
||||||
|
{PLAYERHOOK_ON_CREATE, PLAYERHOOK_ON_LOGIN, PLAYERHOOK_ON_REPUTATION_LOAD_FROM_DB, PLAYERHOOK_ON_UPDATE_FACTION}) {}
|
||||||
|
|
||||||
|
// OnPlayerCreate fires on a temporary Player object AFTER SaveToDB.
|
||||||
|
// Changes to faction/reputation here are lost. We only save the game mode to DB.
|
||||||
|
void OnPlayerCreate(Player* player) override
|
||||||
|
{
|
||||||
|
LOG_INFO("module", "mod-gamemodes: OnPlayerCreate for {} (GUID: {}), OutfitId = {}",
|
||||||
|
player->GetName(), player->GetGUID().GetCounter(), player->GetOutfitId());
|
||||||
|
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
|
||||||
|
return;
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (player->GetOutfitId() != GAMEMODE_TRAITOR)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sGameModeMgr->SaveGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR);
|
||||||
|
CharacterDatabase.Execute("UPDATE `characters` SET `cinematic` = 1 WHERE `guid` = {}", player->GetGUID().GetCounter());
|
||||||
|
LOG_INFO("module", "mod-gamemodes: Saved TRAITOR mode for {} (GUID: {})",
|
||||||
|
player->GetName(), player->GetGUID().GetCounter());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reputation load completes before initial packets are sent to the client.
|
||||||
|
// Traitor mode must rewrite faction/reputation state here, otherwise the client
|
||||||
|
// builds the reputation window from the original race defaults.
|
||||||
|
void OnPlayerReputationLoadFromDB(Player* player) override
|
||||||
|
{
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
|
||||||
|
return;
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
|
||||||
|
return;
|
||||||
|
|
||||||
|
TraitorMode::ApplyFactionSwap(player);
|
||||||
|
|
||||||
|
if (player->HasAtLoginFlag(AT_LOGIN_FIRST))
|
||||||
|
TraitorMode::InitTraitorReputations(player);
|
||||||
|
TraitorMode::NormalizeTraitorReputationStates(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnPlayerLogin(Player* player) override
|
||||||
|
{
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
|
||||||
|
return;
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
|
||||||
|
return;
|
||||||
|
|
||||||
|
TraitorMode::ApplyFactionSwap(player);
|
||||||
|
TraitorMode::NormalizeTraitorReputationStates(player);
|
||||||
|
player->GetReputationMgr().SendInitialReputations();
|
||||||
|
TraitorMode::SendTraitorFactionVisibility(player);
|
||||||
|
player->SetPlayerFlag(PLAYER_FLAGS_TRAITOR);
|
||||||
|
|
||||||
|
if (player->HasAtLoginFlag(AT_LOGIN_FIRST))
|
||||||
|
TraitorMode::TeleportToNewFactionStart(player);
|
||||||
|
|
||||||
|
LOG_INFO("module", "mod-gamemodes: OnPlayerLogin END for {} — TeamId = {}, FactionTemplate = {}, OriginalTeam = {}",
|
||||||
|
player->GetName(),
|
||||||
|
static_cast<uint32>(player->GetTeamId()),
|
||||||
|
player->GetFaction(),
|
||||||
|
static_cast<uint32>(player->GetTeamId(true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnPlayerUpdateFaction fires every time SetFactionForRace is called
|
||||||
|
// (login, race change, etc). Re-apply the swap so it's never lost.
|
||||||
|
void OnPlayerUpdateFaction(Player* player) override
|
||||||
|
{
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Enable", true))
|
||||||
|
return;
|
||||||
|
if (!sConfigMgr->GetOption<bool>("GameModes.Traitor.Enable", true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!sGameModeMgr->HasGameMode(player->GetGUID().GetCounter(), GAMEMODE_TRAITOR))
|
||||||
|
return;
|
||||||
|
|
||||||
|
TraitorMode::ApplyFactionSwap(player);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TraitorModeWorld : public WorldScript
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TraitorModeWorld() : WorldScript("TraitorModeWorld",
|
||||||
|
{WORLDHOOK_ON_LOAD_CUSTOM_DATABASE_TABLE}) {}
|
||||||
|
|
||||||
|
void OnLoadCustomDatabaseTable() override
|
||||||
|
{
|
||||||
|
sGameModeMgr->LoadFromDB();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void AddSC_TraitorMode()
|
||||||
|
{
|
||||||
|
new TraitorModePlayer();
|
||||||
|
new TraitorModeWorld();
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#ifndef MOD_GAMEMODES_TRAITOR_H
|
||||||
|
#define MOD_GAMEMODES_TRAITOR_H
|
||||||
|
|
||||||
|
#include "Player.h"
|
||||||
|
|
||||||
|
namespace TraitorMode
|
||||||
|
{
|
||||||
|
// Reputation faction IDs: capital cities + PvP battleground factions
|
||||||
|
constexpr uint32 ALLIANCE_REPUTATION_FACTIONS[] = {
|
||||||
|
72, // Stormwind
|
||||||
|
47, // Ironforge
|
||||||
|
69, // Darnassus
|
||||||
|
54, // Gnomeregan Exiles
|
||||||
|
930, // Exodar
|
||||||
|
890, // Silverwing Sentinels (WSG)
|
||||||
|
730, // Stormpike Guard (AV)
|
||||||
|
509, // League of Arathor (AB)
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr uint32 HORDE_REPUTATION_FACTIONS[] = {
|
||||||
|
76, // Orgrimmar
|
||||||
|
81, // Thunder Bluff
|
||||||
|
68, // Undercity
|
||||||
|
530, // Darkspear Trolls
|
||||||
|
911, // Silvermoon City
|
||||||
|
889, // Warsong Outriders (WSG)
|
||||||
|
729, // Frostwolf Clan (AV)
|
||||||
|
510, // The Defilers (AB)
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr uint32 FACTION_COUNT = 8;
|
||||||
|
|
||||||
|
// Default spawn points for each faction (race 2 = Orc for Horde, race 1 = Human for Alliance)
|
||||||
|
constexpr uint8 HORDE_DEFAULT_RACE = 2; // Orc
|
||||||
|
constexpr uint8 ALLIANCE_DEFAULT_RACE = 1; // Human
|
||||||
|
|
||||||
|
// Apply faction swap on login/faction update
|
||||||
|
void ApplyFactionSwap(Player* player);
|
||||||
|
|
||||||
|
// Set new faction reputations to Neutral (standing 0)
|
||||||
|
void InitTraitorReputations(Player* player);
|
||||||
|
|
||||||
|
// Teleport traitor to the opposite faction's starting zone
|
||||||
|
void TeleportToNewFactionStart(Player* player);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddSC_TraitorMode();
|
||||||
|
|
||||||
|
#endif // MOD_GAMEMODES_TRAITOR_H
|
||||||
@@ -47,10 +47,6 @@ public:
|
|||||||
|
|
||||||
sIndividualProgression->CheckAdjustments(player);
|
sIndividualProgression->CheckAdjustments(player);
|
||||||
|
|
||||||
if (sIndividualProgression->enabled)
|
|
||||||
{
|
|
||||||
ChatHandler(player->GetSession()).SendSysMessage("|cff00ff00Individual Progression: |cffccccccenabled|r");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnPlayerSetMaxLevel(Player* player, uint32& maxPlayerLevel) override
|
void OnPlayerSetMaxLevel(Player* player, uint32& maxPlayerLevel) override
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "QuestDef.h"
|
#include "QuestDef.h"
|
||||||
#include "ScriptMgr.h"
|
#include "ScriptMgr.h"
|
||||||
#include "SharedDefines.h"
|
#include "SharedDefines.h"
|
||||||
|
#include "WorldSession.h"
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -31,6 +32,8 @@ namespace MoonWell::PlayerGuide
|
|||||||
return;
|
return;
|
||||||
if (!player || !player->GetSession() || player->IsBeingTeleported())
|
if (!player || !player->GetSession() || player->IsBeingTeleported())
|
||||||
return;
|
return;
|
||||||
|
if (player->GetSession()->IsBot())
|
||||||
|
return;
|
||||||
|
|
||||||
Payload p = sPlayerGuideMgr->BuildFor(player);
|
Payload p = sPlayerGuideMgr->BuildFor(player);
|
||||||
std::string json = sPlayerGuideMgr->SerializeJson(p);
|
std::string json = sPlayerGuideMgr->SerializeJson(p);
|
||||||
|
|||||||
@@ -25,6 +25,29 @@ namespace MoonWell::PlayerGuide
|
|||||||
// The mgr clamps its configured chunk size into a safe range.
|
// The mgr clamps its configured chunk size into a safe range.
|
||||||
constexpr std::size_t HARD_LIMIT = 250;
|
constexpr std::size_t HARD_LIMIT = 250;
|
||||||
|
|
||||||
|
std::size_t Utf8ChunkSize(std::string_view body, std::size_t offset,
|
||||||
|
std::size_t budget)
|
||||||
|
{
|
||||||
|
std::size_t take = std::min(budget, body.size() - offset);
|
||||||
|
if (offset + take == body.size())
|
||||||
|
return take;
|
||||||
|
|
||||||
|
// A chunk must not end between a UTF-8 leading byte and one of
|
||||||
|
// its continuation bytes. Chat packet validation rejects such
|
||||||
|
// an individually invalid string before the addon can reassemble
|
||||||
|
// the complete JSON payload.
|
||||||
|
while (take > 0 &&
|
||||||
|
(static_cast<unsigned char>(body[offset + take]) & 0xC0) == 0x80)
|
||||||
|
{
|
||||||
|
--take;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The configured budget is clamped to at least 64 bytes, so a
|
||||||
|
// valid UTF-8 code point always fits. Keep a defensive fallback
|
||||||
|
// for malformed input to guarantee forward progress.
|
||||||
|
return take > 0 ? take : std::min(budget, body.size() - offset);
|
||||||
|
}
|
||||||
|
|
||||||
void SendOneChunk(Player* player, std::string_view command,
|
void SendOneChunk(Player* player, std::string_view command,
|
||||||
uint32 seq, uint32 total, std::string_view body)
|
uint32 seq, uint32 total, std::string_view body)
|
||||||
{
|
{
|
||||||
@@ -71,16 +94,20 @@ namespace MoonWell::PlayerGuide
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-chunk: split blindly by byte budget. JSON tolerates
|
// Calculate UTF-8-safe slices first because backing up from a
|
||||||
// concatenation on the client.
|
// continuation byte can increase the number of chunks.
|
||||||
uint32 total = static_cast<uint32>(
|
std::vector<std::string_view> chunks;
|
||||||
(body.size() + budget - 1) / budget);
|
for (std::size_t off = 0; off < body.size();)
|
||||||
uint32 seq = 1;
|
|
||||||
for (std::size_t off = 0; off < body.size(); off += budget, ++seq)
|
|
||||||
{
|
{
|
||||||
std::size_t take = std::min(budget, body.size() - off);
|
std::size_t take = Utf8ChunkSize(body, off, budget);
|
||||||
SendOneChunk(player, command, seq, total,
|
chunks.emplace_back(body.data() + off, take);
|
||||||
std::string_view(body.data() + off, take));
|
off += take;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 total = static_cast<uint32>(chunks.size());
|
||||||
|
for (uint32 i = 0; i < total; ++i)
|
||||||
|
{
|
||||||
|
SendOneChunk(player, command, i + 1, total, chunks[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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=<service-account API key>
|
||||||
|
YANDEX_AI_FOLDER_ID=<Yandex Cloud 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://<YANDEX_AI_FOLDER_ID>/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.
|
||||||
@@ -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
|
||||||
|
п
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This module uses AzerothCore's standard source and loader discovery.
|
||||||
@@ -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 <boost/asio/connect.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/asio/read.hpp>
|
||||||
|
#include <boost/asio/read_until.hpp>
|
||||||
|
#include <boost/asio/streambuf.hpp>
|
||||||
|
#include <boost/asio/write.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<uint32>(character) << std::dec;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output << static_cast<char>(character);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint32> DecodeAndCaseFold(std::string const& text)
|
||||||
|
{
|
||||||
|
std::vector<uint32> result;
|
||||||
|
result.reserve(text.size());
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < text.size();)
|
||||||
|
{
|
||||||
|
unsigned char first = static_cast<unsigned char>(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<unsigned char>(text[index + 1]) & 0x3F);
|
||||||
|
length = 2;
|
||||||
|
}
|
||||||
|
else if ((first & 0xF0) == 0xE0 && index + 2 < text.size())
|
||||||
|
{
|
||||||
|
codepoint = ((first & 0x0F) << 12)
|
||||||
|
| ((static_cast<unsigned char>(text[index + 1]) & 0x3F) << 6)
|
||||||
|
| (static_cast<unsigned char>(text[index + 2]) & 0x3F);
|
||||||
|
length = 3;
|
||||||
|
}
|
||||||
|
else if ((first & 0xF8) == 0xF0 && index + 3 < text.size())
|
||||||
|
{
|
||||||
|
codepoint = ((first & 0x07) << 18)
|
||||||
|
| ((static_cast<unsigned char>(text[index + 1]) & 0x3F) << 12)
|
||||||
|
| ((static_cast<unsigned char>(text[index + 2]) & 0x3F) << 6)
|
||||||
|
| (static_cast<unsigned char>(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<std::size_t> FindCompleteName(
|
||||||
|
std::vector<uint32> const& message,
|
||||||
|
std::vector<uint32> 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<char>(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<unsigned char>(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<std::string> 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<std::string> 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<std::mutex> lock(_requestMutex);
|
||||||
|
_stopping = true;
|
||||||
|
}
|
||||||
|
_requestReady.notify_all();
|
||||||
|
if (_worker.joinable())
|
||||||
|
_worker.join();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> 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<std::mutex> lock(_requestMutex);
|
||||||
|
if (_stopping || _requests.size() >= maxQueuedRequests)
|
||||||
|
return false;
|
||||||
|
_requests.push_back(std::move(request));
|
||||||
|
_requestReady.notify_one();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::deque<AiResponse> TakeResponses()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_responseMutex);
|
||||||
|
std::deque<AiResponse> responses;
|
||||||
|
responses.swap(_responses);
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
PlayerbotsAiBridge() = default;
|
||||||
|
~PlayerbotsAiBridge()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
AiRequest request;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> 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<std::mutex> 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<bool> _running{false};
|
||||||
|
bool _stopping{false};
|
||||||
|
std::thread _worker;
|
||||||
|
std::mutex _requestMutex;
|
||||||
|
std::condition_variable _requestReady;
|
||||||
|
std::deque<AiRequest> _requests;
|
||||||
|
std::mutex _responseMutex;
|
||||||
|
std::deque<AiResponse> _responses;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsEnabled()
|
||||||
|
{
|
||||||
|
return sConfigMgr->GetOption<bool>("PlayerbotsAI.Enabled", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsAvailableForAll()
|
||||||
|
{
|
||||||
|
return sConfigMgr->GetOption<bool>("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<uint32>(bot->getGender()) << ','
|
||||||
|
<< "\"player_gender\":" << static_cast<uint32>(player->getGender()) << ','
|
||||||
|
<< "\"bot_class\":" << static_cast<uint32>(bot->getClass()) << ','
|
||||||
|
<< "\"bot_race\":" << static_cast<uint32>(bot->getRace()) << ','
|
||||||
|
<< "\"bot_level\":" << static_cast<uint32>(bot->GetLevel()) << ','
|
||||||
|
<< "\"zone_id\":" << bot->GetZoneId()
|
||||||
|
<< '}';
|
||||||
|
|
||||||
|
return {
|
||||||
|
bot->GetGUID(),
|
||||||
|
player->GetGUID(),
|
||||||
|
bot->GetTeamId(),
|
||||||
|
std::move(channelName),
|
||||||
|
sConfigMgr->GetOption<std::string>("PlayerbotsAI.BridgeHost", "playerbots-ai"),
|
||||||
|
std::to_string(sConfigMgr->GetOption<uint32>("PlayerbotsAI.BridgePort", 8080)),
|
||||||
|
sConfigMgr->GetOption<std::string>("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<uint32> 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<std::size_t> 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<uint32>("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<uint32>(1, sConfigMgr->GetOption<uint32>("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<bool>("PlayerbotsAI.Debug", false))
|
||||||
|
{
|
||||||
|
LOG_INFO(
|
||||||
|
"server.loading",
|
||||||
|
"[PlayerbotsAI] {} addressed bot {} in {}",
|
||||||
|
player->GetName(),
|
||||||
|
selectedBot->GetName(),
|
||||||
|
channel ? channel->GetName() : "/say");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<ObjectGuid, std::chrono::steady_clock::time_point> _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<uint32>("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<bool>("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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
void AddSC_mod_playerbots_ai();
|
||||||
|
|
||||||
|
void Addmod_playerbots_aiScripts()
|
||||||
|
{
|
||||||
|
AddSC_mod_playerbots_ai();
|
||||||
|
}
|
||||||
@@ -84,6 +84,10 @@
|
|||||||
# Enable or disable Playerbots module
|
# Enable or disable Playerbots module
|
||||||
AiPlayerbot.Enabled = 1
|
AiPlayerbot.Enabled = 1
|
||||||
|
|
||||||
|
# Use the MoonWell Cyrillic name and guild-name pools.
|
||||||
|
# Existing RNDbot characters can be migrated with localize-playerbots-ru.sh.
|
||||||
|
AiPlayerbot.UseRussianNames = 0
|
||||||
|
|
||||||
# Enable randombot system
|
# Enable randombot system
|
||||||
AiPlayerbot.RandomBotAutologin = 1
|
AiPlayerbot.RandomBotAutologin = 1
|
||||||
|
|
||||||
|
|||||||
+4884
File diff suppressed because it is too large
Load Diff
+1746
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ bool AcceptInvitationAction::Execute(Event event)
|
|||||||
botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT);
|
botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT);
|
||||||
botAI->Reset();
|
botAI->Reset();
|
||||||
|
|
||||||
botAI->TellMaster("Hello");
|
botAI->TellMaster("Здравствуйте.");
|
||||||
|
|
||||||
if (sPlayerbotAIConfig.summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig.sightDistance)
|
if (sPlayerbotAIConfig.summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig.sightDistance)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
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());
|
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;
|
return true;
|
||||||
@@ -113,7 +113,7 @@ bool AcceptQuestShareAction::Execute(Event event)
|
|||||||
if (bot->HasQuest(quest))
|
if (bot->HasQuest(quest))
|
||||||
{
|
{
|
||||||
bot->SetDivider(ObjectGuid::Empty);
|
bot->SetDivider(ObjectGuid::Empty);
|
||||||
botAI->TellError("I have this quest");
|
botAI->TellError("У меня уже есть это задание");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ bool AcceptQuestShareAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
// can't take quest
|
// can't take quest
|
||||||
bot->SetDivider(ObjectGuid::Empty);
|
bot->SetDivider(ObjectGuid::Empty);
|
||||||
botAI->TellError("I can't take this quest");
|
botAI->TellError("Я не могу взять это задание");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ bool AcceptQuestShareAction::Execute(Event event)
|
|||||||
bot->CastSpell(bot, qInfo->GetSrcSpell(), true);
|
bot->CastSpell(bot, qInfo->GetSrcSpell(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster("Quest accepted");
|
botAI->TellMaster("Задание принято.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ bool ReachAreaTriggerAction::Execute(Event event)
|
|||||||
|
|
||||||
if (bot->GetMapId() != at->map)
|
if (bot->GetMapId() != at->map)
|
||||||
{
|
{
|
||||||
botAI->TellError("I won't follow: too far away");
|
botAI->TellError("Я не последую за вами: вы слишком далеко");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ bool ReachAreaTriggerAction::Execute(Event event)
|
|||||||
|
|
||||||
float distance = bot->GetDistance(at->x, at->y, at->z);
|
float distance = bot->GetDistance(at->x, at->y, at->z);
|
||||||
float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig.reactDelay;
|
float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig.reactDelay;
|
||||||
botAI->TellError("Wait for me");
|
botAI->TellError("Подождите меня");
|
||||||
botAI->SetNextCheckDelay(delay);
|
botAI->SetNextCheckDelay(delay);
|
||||||
context->GetValue<LastMovement&>("last area trigger")->Get().lastAreaTrigger = triggerId;
|
context->GetValue<LastMovement&>("last area trigger")->Get().lastAreaTrigger = triggerId;
|
||||||
|
|
||||||
@@ -76,6 +76,6 @@ bool AreaTriggerAction::Execute(Event /*event*/)
|
|||||||
p.rpos(0);
|
p.rpos(0);
|
||||||
bot->GetSession()->HandleAreaTriggerOpcode(p);
|
bot->GetSession()->HandleAreaTriggerOpcode(p);
|
||||||
|
|
||||||
botAI->TellMaster("Hello");
|
botAI->TellMaster("Здравствуйте.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ bool ArenaTeamAcceptAction::Execute(Event event)
|
|||||||
if (bot->GetArenaTeamId(at->GetSlot()))
|
if (bot->GetArenaTeamId(at->GetSlot()))
|
||||||
{
|
{
|
||||||
// bot is already in an arena team
|
// bot is already in an arena team
|
||||||
bot->Say("Sorry, I am already in such team", LANG_UNIVERSAL);
|
bot->Say("У меня уже есть такая команда", LANG_UNIVERSAL);
|
||||||
accept = false;
|
accept = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ bool ArenaTeamAcceptAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
WorldPacket data(CMSG_ARENA_TEAM_ACCEPT);
|
WorldPacket data(CMSG_ARENA_TEAM_ACCEPT);
|
||||||
bot->GetSession()->HandleArenaTeamAcceptOpcode(data);
|
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(),
|
LOG_INFO("playerbots", "Bot {} <{}> accepts Arena Team invite", bot->GetGUID().ToString().c_str(),
|
||||||
bot->GetName().c_str());
|
bot->GetName().c_str());
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ bool AttackMyTargetAction::Execute(Event /*event*/)
|
|||||||
if (!guid)
|
if (!guid)
|
||||||
{
|
{
|
||||||
if (verbose)
|
if (verbose)
|
||||||
botAI->TellError("You have no target");
|
botAI->TellError("У вас нет цели");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
|
|||||||
bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
|
bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
|
||||||
{
|
{
|
||||||
if (verbose)
|
if (verbose)
|
||||||
botAI->TellError("I cannot attack in flight");
|
botAI->TellError("Я не могу атаковать в полёте");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
|
|||||||
if (!target)
|
if (!target)
|
||||||
{
|
{
|
||||||
if (verbose)
|
if (verbose)
|
||||||
botAI->TellError("I have no target");
|
botAI->TellError("У меня нет цели");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
|
|||||||
sPlayerbotAIConfig.IsPvpProhibited(target->GetZoneId(), target->GetAreaId())))
|
sPlayerbotAIConfig.IsPvpProhibited(target->GetZoneId(), target->GetAreaId())))
|
||||||
{
|
{
|
||||||
if (verbose)
|
if (verbose)
|
||||||
botAI->TellError("I cannot attack other players in PvP prohibited areas.");
|
botAI->TellError("Я не могу атаковать игроков в зонах, где запрещено PvP.");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
|
|||||||
if (sameTarget && inCombat && sameAttackMode)
|
if (sameTarget && inCombat && sameAttackMode)
|
||||||
{
|
{
|
||||||
if (verbose)
|
if (verbose)
|
||||||
botAI->TellError("I am already attacking " + std::string(target->GetName()) + ".");
|
botAI->TellError("Я уже атакую: " + std::string(target->GetName()) + ".");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
|
|||||||
if (!bot->IsValidAttackTarget(target))
|
if (!bot->IsValidAttackTarget(target))
|
||||||
{
|
{
|
||||||
if (verbose)
|
if (verbose)
|
||||||
botAI->TellError("I cannot attack an invalid target.");
|
botAI->TellError("Я не могу атаковать недопустимую цель.");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ void AutoMaintenanceOnLevelupAction::AutoLearnSpell()
|
|||||||
{
|
{
|
||||||
std::string const temp = out.str();
|
std::string const temp = out.str();
|
||||||
out.seekp(0);
|
out.seekp(0);
|
||||||
out << "Learned spells: ";
|
out << "Изученные заклинания: ";
|
||||||
out << temp;
|
out << temp;
|
||||||
out.seekp(-2, out.cur);
|
out.seekp(-2, out.cur);
|
||||||
out << ".";
|
out << ".";
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ bool BankAction::Execute(Event event)
|
|||||||
return ExecuteBank(text, npc);
|
return ExecuteBank(text, npc);
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellError("Cannot find banker nearby");
|
botAI->TellError("Поблизости нет банкира");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ bool BankAction::Withdraw(uint32 itemid)
|
|||||||
bot->StoreItem(dest, pItem, true);
|
bot->StoreItem(dest, pItem, true);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "got " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " from bank";
|
out << "получено: " << chat->FormatItem(pItem->GetTemplate(), pItem->GetCount()) << " из банка";
|
||||||
botAI->TellMaster(out.str());
|
botAI->TellMaster(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -102,14 +102,14 @@ bool BankAction::Deposit(Item* pItem)
|
|||||||
bot->RemoveItem(pItem->GetBagSlot(), pItem->GetSlot(), true);
|
bot->RemoveItem(pItem->GetBagSlot(), pItem->GetSlot(), true);
|
||||||
bot->BankItem(dest, pItem, 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());
|
botAI->TellMaster(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BankAction::ListItems()
|
void BankAction::ListItems()
|
||||||
{
|
{
|
||||||
botAI->TellMaster("=== Bank ===");
|
botAI->TellMaster("=== Банк ===");
|
||||||
|
|
||||||
std::map<uint32, uint32> items;
|
std::map<uint32, uint32> items;
|
||||||
std::map<uint32, bool> soulbound;
|
std::map<uint32, bool> soulbound;
|
||||||
|
|||||||
@@ -1278,13 +1278,13 @@ bool BGTactics::HandleConsoleCommand(ChatHandler* handler, char const* args)
|
|||||||
{
|
{
|
||||||
if (!sPlayerbotAIConfig.enabled)
|
if (!sPlayerbotAIConfig.enabled)
|
||||||
{
|
{
|
||||||
handler->PSendSysMessage("|cffff0000Playerbot system is currently disabled!");
|
handler->PSendSysMessage("|cffff0000Система Playerbot сейчас отключена!");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
WorldSession* session = handler->GetSession();
|
WorldSession* session = handler->GetSession();
|
||||||
if (!session)
|
if (!session)
|
||||||
{
|
{
|
||||||
handler->PSendSysMessage("Command can only be used from an active session");
|
handler->PSendSysMessage("Команду можно использовать только из активной игровой сессии");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
std::string const commandOutput = HandleConsoleCommandPrivate(session, args);
|
std::string const commandOutput = HandleConsoleCommandPrivate(session, args);
|
||||||
@@ -4304,7 +4304,7 @@ bool ArenaTactics::Execute(Event /*event*/)
|
|||||||
|
|
||||||
float x, y, z;
|
float x, y, z;
|
||||||
target->GetPosition(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);
|
return MoveTo(target->GetMapId(), x + frand(-1, +1), y + frand(-1, +1), z, false, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,19 +64,19 @@ void BuffAction::TellHeader(uint32 subClass)
|
|||||||
switch (subClass)
|
switch (subClass)
|
||||||
{
|
{
|
||||||
case ITEM_SUBCLASS_ELIXIR:
|
case ITEM_SUBCLASS_ELIXIR:
|
||||||
botAI->TellMaster("--- Elixir ---");
|
botAI->TellMaster("--- Эликсиры ---");
|
||||||
return;
|
return;
|
||||||
case ITEM_SUBCLASS_FLASK:
|
case ITEM_SUBCLASS_FLASK:
|
||||||
botAI->TellMaster("--- Flask ---");
|
botAI->TellMaster("--- Настои ---");
|
||||||
return;
|
return;
|
||||||
case ITEM_SUBCLASS_SCROLL:
|
case ITEM_SUBCLASS_SCROLL:
|
||||||
botAI->TellMaster("--- Scroll ---");
|
botAI->TellMaster("--- Свитки ---");
|
||||||
return;
|
return;
|
||||||
case ITEM_SUBCLASS_FOOD:
|
case ITEM_SUBCLASS_FOOD:
|
||||||
botAI->TellMaster("--- Food ---");
|
botAI->TellMaster("--- Еда ---");
|
||||||
return;
|
return;
|
||||||
case ITEM_SUBCLASS_ITEM_ENHANCEMENT:
|
case ITEM_SUBCLASS_ITEM_ENHANCEMENT:
|
||||||
botAI->TellMaster("--- Enchant ---");
|
botAI->TellMaster("--- Чары ---");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ bool BuyAction::Execute(Event event)
|
|||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Nobody sells " << ChatHelper::FormatItem(proto) << " nearby";
|
out << "Никто не продаёт: " << ChatHelper::FormatItem(proto) << " поблизости";
|
||||||
botAI->TellMaster(out.str());
|
botAI->TellMaster(out.str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -215,7 +215,7 @@ bool BuyAction::Execute(Event event)
|
|||||||
|
|
||||||
if (!vendored)
|
if (!vendored)
|
||||||
{
|
{
|
||||||
botAI->TellError("There are no vendors nearby");
|
botAI->TellError("Поблизости нет торговцев");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, Ite
|
|||||||
if (newCount > oldCount)
|
if (newCount > oldCount)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Buying " << ChatHelper::FormatItem(proto);
|
out << "Покупаю: " << ChatHelper::FormatItem(proto);
|
||||||
botAI->TellMaster(out.str());
|
botAI->TellMaster(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ bool CastCustomSpellAction::Execute(Event event)
|
|||||||
std::ostringstream msg;
|
std::ostringstream msg;
|
||||||
if (!spell)
|
if (!spell)
|
||||||
{
|
{
|
||||||
msg << "Unknown spell " << text;
|
msg << "Неизвестное заклинание: " << text;
|
||||||
botAI->TellError(msg.str());
|
botAI->TellError(msg.str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ bool CastCustomSpellAction::Execute(Event event)
|
|||||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell);
|
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell);
|
||||||
if (!spellInfo)
|
if (!spellInfo)
|
||||||
{
|
{
|
||||||
msg << "Unknown spell " << text;
|
msg << "Неизвестное заклинание: " << text;
|
||||||
botAI->TellError(msg.str());
|
botAI->TellError(msg.str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ bool CastCustomSpellAction::Execute(Event event)
|
|||||||
|
|
||||||
if (!bot->GetTrader() && !botAI->CanCastSpell(spell, target, true, itemTarget))
|
if (!bot->GetTrader() && !botAI->CanCastSpell(spell, target, true, itemTarget))
|
||||||
{
|
{
|
||||||
msg << "Cannot cast " << spellName.str();
|
msg << "Не удалось применить: " << spellName.str();
|
||||||
botAI->TellError(msg.str());
|
botAI->TellError(msg.str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ bool CastCustomSpellAction::Execute(Event event)
|
|||||||
bool result = spell ? botAI->CastSpell(spell, target, itemTarget) : botAI->CastSpell(text, target, itemTarget);
|
bool result = spell ? botAI->CastSpell(spell, target, itemTarget) : botAI->CastSpell(text, target, itemTarget);
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
msg << "Casting " << spellName.str();
|
msg << "Применяю: " << spellName.str();
|
||||||
|
|
||||||
if (castCount > 1)
|
if (castCount > 1)
|
||||||
{
|
{
|
||||||
@@ -176,7 +176,7 @@ bool CastCustomSpellAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
msg << "Cast " << spellName.str() << " is failed";
|
msg << "Применение " << spellName.str() << " завершилось ошибкой";
|
||||||
botAI->TellError(msg.str());
|
botAI->TellError(msg.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ bool ChangeChatAction::Execute(Event event)
|
|||||||
if (parsed == CHAT_MSG_SYSTEM)
|
if (parsed == CHAT_MSG_SYSTEM)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Current chat is " << chat->FormatChat(*context->GetValue<ChatMsg>("chat"));
|
out << "Текущий канал чата: " << chat->FormatChat(*context->GetValue<ChatMsg>("chat"));
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -23,7 +23,7 @@ bool ChangeChatAction::Execute(Event event)
|
|||||||
context->GetValue<ChatMsg>("chat")->Set(parsed);
|
context->GetValue<ChatMsg>("chat")->Set(parsed);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Chat set to " << chat->FormatChat(parsed);
|
out << "Канал чата установлен: " << chat->FormatChat(parsed);
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ bool ChangeNonCombatStrategyAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
if (text.find("loot") != std::string::npos || text.find("gather") != std::string::npos)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ bool ChangeTalentsAction::Execute(Event event)
|
|||||||
if (param.find("switch 1") != std::string::npos)
|
if (param.find("switch 1") != std::string::npos)
|
||||||
{
|
{
|
||||||
bot->ActivateSpec(0);
|
bot->ActivateSpec(0);
|
||||||
out << "Active first talent";
|
out << "Активирован первый набор талантов";
|
||||||
botAI->ResetStrategies();
|
botAI->ResetStrategies();
|
||||||
}
|
}
|
||||||
else if (param.find("switch 2") != std::string::npos)
|
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->CastSpell(bot, 63624, true, nullptr, nullptr, bot->GetGUID());
|
||||||
}
|
}
|
||||||
bot->ActivateSpec(1);
|
bot->ActivateSpec(1);
|
||||||
out << "Active second talent";
|
out << "Активирован второй набор талантов";
|
||||||
botAI->ResetStrategies();
|
botAI->ResetStrategies();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ bool ChangeTalentsAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
PlayerbotFactory factory(bot, bot->GetLevel());
|
PlayerbotFactory factory(bot, bot->GetLevel());
|
||||||
factory.InitTalentsTree(true);
|
factory.InitTalentsTree(true);
|
||||||
out << "Auto pick talents";
|
out << "Автоматический выбор талантов включён";
|
||||||
botAI->ResetStrategies();
|
botAI->ResetStrategies();
|
||||||
}
|
}
|
||||||
else if (param.find("spec list") != std::string::npos)
|
else if (param.find("spec list") != std::string::npos)
|
||||||
@@ -78,13 +78,13 @@ bool ChangeTalentsAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
out << "Unknown command.";
|
out << "Неизвестная команда.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
uint32 tab = AiFactory::GetPlayerSpecTab(bot);
|
uint32 tab = AiFactory::GetPlayerSpecTab(bot);
|
||||||
out << "My current talent spec is: "
|
out << "Моя текущая специализация: "
|
||||||
<< "|h|cffffffff";
|
<< "|h|cffffffff";
|
||||||
out << chat->FormatClass(bot, tab) << "\n";
|
out << chat->FormatClass(bot, tab) << "\n";
|
||||||
out << TalentsHelp();
|
out << TalentsHelp();
|
||||||
@@ -98,7 +98,7 @@ bool ChangeTalentsAction::Execute(Event event)
|
|||||||
std::string ChangeTalentsAction::TalentsHelp()
|
std::string ChangeTalentsAction::TalentsHelp()
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
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 <specName>, talents apply <link>.";
|
"talents spec <specName>, talents apply <link>.";
|
||||||
return out.str();
|
return out.str();
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ std::string ChangeTalentsAction::SpecList()
|
|||||||
out << tabCount[0] << "-" << tabCount[1] << "-" << tabCount[2] << ")";
|
out << tabCount[0] << "-" << tabCount[1] << "-" << tabCount[2] << ")";
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
}
|
}
|
||||||
out << "Total " << specFound << " specs found";
|
out << "Найдено: " << specFound << " специализаций";
|
||||||
return out.str();
|
return out.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,12 +149,12 @@ std::string ChangeTalentsAction::SpecPick(std::string param)
|
|||||||
factory.InitGlyphs(false);
|
factory.InitGlyphs(false);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Picking " << sPlayerbotAIConfig.premadeSpecName[cls][specNo];
|
out << "Выбираю: " << sPlayerbotAIConfig.premadeSpecName[cls][specNo];
|
||||||
return out.str();
|
return out.str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Spec " << param << " not found";
|
out << "Специализация " << param << " не найдена";
|
||||||
return out.str();
|
return out.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +165,11 @@ std::string ChangeTalentsAction::SpecApply(std::string param)
|
|||||||
std::vector<std::vector<uint32>> parsedSpecLink = PlayerbotAIConfig::ParseTempTalentsOrder(cls, param);
|
std::vector<std::vector<uint32>> parsedSpecLink = PlayerbotAIConfig::ParseTempTalentsOrder(cls, param);
|
||||||
if (parsedSpecLink.size() == 0)
|
if (parsedSpecLink.size() == 0)
|
||||||
{
|
{
|
||||||
out << "Invalid link " << param;
|
out << "Неверная ссылка: " << param;
|
||||||
return out.str();
|
return out.str();
|
||||||
}
|
}
|
||||||
PlayerbotFactory::InitTalentsByParsedSpecLink(bot, parsedSpecLink, true);
|
PlayerbotFactory::InitTalentsByParsedSpecLink(bot, parsedSpecLink, true);
|
||||||
out << "Applying " << param;
|
out << "Применяю: " << param;
|
||||||
return out.str();
|
return out.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ bool FollowChatShortcutAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
if (moved)
|
if (moved)
|
||||||
{
|
{
|
||||||
botAI->TellMaster("Following");
|
botAI->TellMaster("Следую за целью.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,10 +101,10 @@ bool FollowChatShortcutAction::Execute(Event /*event*/)
|
|||||||
if (bot->isDead())
|
if (bot->isDead())
|
||||||
{
|
{
|
||||||
bot->ResurrectPlayer(1.0f, false);
|
bot->ResurrectPlayer(1.0f, false);
|
||||||
botAI->TellMasterNoFacing("Back from the grave!");
|
botAI->TellMasterNoFacing("Возвращение из могилы!");
|
||||||
}
|
}
|
||||||
else
|
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->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
||||||
bot->TeleportTo(master->GetMapId(), master->GetPositionX(), master->GetPositionY(), master->GetPositionZ(),
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ bool StayChatShortcutAction::Execute(Event /*event*/)
|
|||||||
SetReturnPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
SetReturnPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
||||||
SetStayPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
SetStayPosition(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ());
|
||||||
|
|
||||||
botAI->TellMaster("Staying");
|
botAI->TellMaster("Остаюсь на месте.");
|
||||||
return true;
|
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_NON_COMBAT);
|
||||||
botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT);
|
botAI->ChangeStrategy("+move from group", BOT_STATE_COMBAT);
|
||||||
|
|
||||||
botAI->TellMaster("Moving away from group");
|
botAI->TellMaster("Отхожу от группы");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +163,11 @@ bool FleeChatShortcutAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
if (bot->GetMapId() != master->GetMapId() || bot->GetDistance(master) > sPlayerbotAIConfig.sightDistance)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster("Fleeing");
|
botAI->TellMaster("Отступаю.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ bool GoawayChatShortcutAction::Execute(Event /*event*/)
|
|||||||
ResetReturnPosition();
|
ResetReturnPosition();
|
||||||
ResetStayPosition();
|
ResetStayPosition();
|
||||||
|
|
||||||
botAI->TellMaster("Running away");
|
botAI->TellMaster("Отступаю");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ bool GrindChatShortcutAction::Execute(Event /*event*/)
|
|||||||
ResetReturnPosition();
|
ResetReturnPosition();
|
||||||
ResetStayPosition();
|
ResetStayPosition();
|
||||||
|
|
||||||
botAI->TellMaster("Grinding");
|
botAI->TellMaster("Фармлю.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ bool TankAttackChatShortcutAction::Execute(Event /*event*/)
|
|||||||
ResetReturnPosition();
|
ResetReturnPosition();
|
||||||
ResetStayPosition();
|
ResetStayPosition();
|
||||||
|
|
||||||
botAI->TellMaster("Attacking");
|
botAI->TellMaster("Атакую.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ bool MaxDpsChatShortcutAction::Execute(Event /*event*/)
|
|||||||
botAI->Reset();
|
botAI->Reset();
|
||||||
|
|
||||||
botAI->ChangeStrategy("-threat,-conserve mana,-cast time,+dps debuff,+boost", BOT_STATE_COMBAT);
|
botAI->ChangeStrategy("-threat,-conserve mana,-cast time,+dps debuff,+boost", BOT_STATE_COMBAT);
|
||||||
botAI->TellMaster("Max DPS!");
|
botAI->TellMaster("Максимальный урон!");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -250,6 +250,6 @@ bool BwlChatShortcutAction::Execute(Event /*event*/)
|
|||||||
botAI->Reset();
|
botAI->Reset();
|
||||||
botAI->ChangeStrategy("+bwl", BOT_STATE_NON_COMBAT);
|
botAI->ChangeStrategy("+bwl", BOT_STATE_NON_COMBAT);
|
||||||
botAI->ChangeStrategy("+bwl", BOT_STATE_COMBAT);
|
botAI->ChangeStrategy("+bwl", BOT_STATE_COMBAT);
|
||||||
botAI->TellMasterNoFacing("Add Bwl Strategies!");
|
botAI->TellMasterNoFacing("Стратегии Логова Крыла Тьмы включены!");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ bool ChooseRpgTargetAction::Execute(Event /*event*/)
|
|||||||
if (botAI->HasStrategy("debug", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject())
|
if (botAI->HasStrategy("debug", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject())
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "found: ";
|
out << "найдено: ";
|
||||||
out << chat->FormatWorldobject(guidP.GetWorldObject());
|
out << chat->FormatWorldobject(guidP.GetWorldObject());
|
||||||
out << " " << relevances.front();
|
out << " " << relevances.front();
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ bool AttackRtiTargetAction::Execute(Event /*event*/)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
botAI->TellError("I dont see my rti attack target");
|
botAI->TellError("Я не вижу назначенную групповую цель для атаки");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,15 +244,15 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
|
|||||||
std::string Sub;
|
std::string Sub;
|
||||||
|
|
||||||
if (newTarget->isGroupCopy())
|
if (newTarget->isGroupCopy())
|
||||||
out << "Following group ";
|
out << "Следую за группой: ";
|
||||||
else if (oldDestination && oldDestination == destination)
|
else if (oldDestination && oldDestination == destination)
|
||||||
out << "Continuing ";
|
out << "Продолжаю: ";
|
||||||
else
|
else
|
||||||
out << "Traveling ";
|
out << "Путешествую: ";
|
||||||
|
|
||||||
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
||||||
|
|
||||||
out << " for " << chat->FormatQuest(quest);
|
out << " за " << chat->FormatQuest(quest);
|
||||||
|
|
||||||
out << " to " << QuestDestination->getTitle();
|
out << " to " << QuestDestination->getTitle();
|
||||||
|
|
||||||
@@ -265,20 +265,20 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
|
|||||||
WorldPosition botLocation(bot);
|
WorldPosition botLocation(bot);
|
||||||
|
|
||||||
if (newTarget->isGroupCopy())
|
if (newTarget->isGroupCopy())
|
||||||
out << "Following group ";
|
out << "Следую за группой: ";
|
||||||
else if (oldDestination && oldDestination == destination)
|
else if (oldDestination && oldDestination == destination)
|
||||||
out << "Continuing ";
|
out << "Продолжаю: ";
|
||||||
else
|
else
|
||||||
out << "Traveling ";
|
out << "Путешествую: ";
|
||||||
|
|
||||||
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
||||||
|
|
||||||
out << " for ";
|
out << " за ";
|
||||||
|
|
||||||
if (AI_VALUE2(bool, "group or", "should sell,can sell"))
|
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"))
|
else if (AI_VALUE2(bool, "group or", "should repair,can repair"))
|
||||||
out << "repairing";
|
out << "ремонтируюсь";
|
||||||
else
|
else
|
||||||
out << "rpg";
|
out << "rpg";
|
||||||
|
|
||||||
@@ -293,11 +293,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
|
|||||||
WorldPosition botLocation(bot);
|
WorldPosition botLocation(bot);
|
||||||
|
|
||||||
if (newTarget->isGroupCopy())
|
if (newTarget->isGroupCopy())
|
||||||
out << "Following group ";
|
out << "Следую за группой: ";
|
||||||
else if (oldDestination && oldDestination == destination)
|
else if (oldDestination && oldDestination == destination)
|
||||||
out << "Continuing ";
|
out << "Продолжаю: ";
|
||||||
else
|
else
|
||||||
out << "Traveling ";
|
out << "Путешествую: ";
|
||||||
|
|
||||||
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
||||||
|
|
||||||
@@ -314,11 +314,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
|
|||||||
WorldPosition botLocation(bot);
|
WorldPosition botLocation(bot);
|
||||||
|
|
||||||
if (newTarget->isGroupCopy())
|
if (newTarget->isGroupCopy())
|
||||||
out << "Following group ";
|
out << "Следую за группой: ";
|
||||||
else if (oldDestination && oldDestination == destination)
|
else if (oldDestination && oldDestination == destination)
|
||||||
out << "Continuing ";
|
out << "Продолжаю: ";
|
||||||
else
|
else
|
||||||
out << "Traveling ";
|
out << "Путешествую: ";
|
||||||
|
|
||||||
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
||||||
|
|
||||||
@@ -335,11 +335,11 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
|
|||||||
WorldPosition botLocation(bot);
|
WorldPosition botLocation(bot);
|
||||||
|
|
||||||
if (newTarget->isGroupCopy())
|
if (newTarget->isGroupCopy())
|
||||||
out << "Following group ";
|
out << "Следую за группой: ";
|
||||||
else if (oldDestination && oldDestination == destination)
|
else if (oldDestination && oldDestination == destination)
|
||||||
out << "Continuing ";
|
out << "Продолжаю: ";
|
||||||
else
|
else
|
||||||
out << "Traveling ";
|
out << "Путешествую: ";
|
||||||
|
|
||||||
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
out << round(newTarget->getDestination()->distanceTo(&botLocation)) << "y";
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ void ChooseTravelTargetAction::ReportTravelTarget(TravelTarget* newTarget, Trave
|
|||||||
{
|
{
|
||||||
if (!oldTarget->getDestination() || oldTarget->getDestination()->getName() != "NullTravelDestination")
|
if (!oldTarget->getDestination() || oldTarget->getDestination()->getName() != "NullTravelDestination")
|
||||||
{
|
{
|
||||||
botAI->TellMaster("No where to travel. Idling a bit.");
|
botAI->TellMaster("Некуда идти. Немного подожду.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ bool CustomStrategyEditAction::Execute(Event event)
|
|||||||
|
|
||||||
bool CustomStrategyEditAction::PrintHelp()
|
bool CustomStrategyEditAction::PrintHelp()
|
||||||
{
|
{
|
||||||
botAI->TellMaster("=== Custom strategies ===");
|
botAI->TellMaster("=== Пользовательские стратегии ===");
|
||||||
|
|
||||||
uint32 owner = botAI->GetBot()->GetGUID().GetCounter();
|
uint32 owner = botAI->GetBot()->GetGUID().GetCounter();
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ bool CustomStrategyEditAction::PrintHelp()
|
|||||||
} while (result->NextRow());
|
} while (result->NextRow());
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster("Usage: cs <name> <idx> <command>");
|
botAI->TellMaster("Использование: cs <имя> <индекс> <команда>");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot);
|
TravelNodeRoute route = TravelNodeMap::instance().getRoute(botPos, *points.front(), beginPath, bot);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Traveling to " << dest->getTitle() << ": ";
|
out << "Направляюсь к: " << dest->getTitle() << ": ";
|
||||||
|
|
||||||
for (auto node : route.getNodes())
|
for (auto node : route.getNodes())
|
||||||
{
|
{
|
||||||
@@ -92,7 +92,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Destination " + destination + " not found.");
|
botAI->TellMasterNoFacing("Точка назначения " + destination + " not found.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
|
|
||||||
if (!quest)
|
if (!quest)
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Quest " + text.substr(6) + " not found.");
|
botAI->TellMasterNoFacing("Задание " + text.substr(6) + " not found.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ bool DebugAction::Execute(Event event)
|
|||||||
else if (text.find("bquest") != std::string::npos)
|
else if (text.find("bquest") != std::string::npos)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "bad quests:";
|
out << "ошибочные задания:";
|
||||||
|
|
||||||
// uint32 noT = 0, noG = 0, noO = 0; //not used, line marked for removal.
|
// 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);
|
endNode->setLinked(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMasterNoFacing("Node " + name + " created.");
|
botAI->TellMasterNoFacing("Узел " + name + " created.");
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
TravelNodeMap::instance().setHasToGen();
|
||||||
|
|
||||||
@@ -222,12 +222,12 @@ bool DebugAction::Execute(Event event)
|
|||||||
|
|
||||||
if (startNode->isImportant())
|
if (startNode->isImportant())
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Node can not be removed.");
|
botAI->TellMasterNoFacing("Не удалось удалить узел.");
|
||||||
}
|
}
|
||||||
|
|
||||||
TravelNodeMap::instance().m_nMapMtx.lock();
|
TravelNodeMap::instance().m_nMapMtx.lock();
|
||||||
TravelNodeMap::instance().removeNode(startNode);
|
TravelNodeMap::instance().removeNode(startNode);
|
||||||
botAI->TellMasterNoFacing("Node removed.");
|
botAI->TellMasterNoFacing("Узел удалён.");
|
||||||
TravelNodeMap::instance().m_nMapMtx.unlock();
|
TravelNodeMap::instance().m_nMapMtx.unlock();
|
||||||
|
|
||||||
TravelNodeMap::instance().setHasToGen();
|
TravelNodeMap::instance().setHasToGen();
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ bool DropQuestAction::Execute(Event event)
|
|||||||
const Quest* pQuest = sObjectMgr->GetQuestTemplate(entry);
|
const Quest* pQuest = sObjectMgr->GetQuestTemplate(entry);
|
||||||
const std::string text_quest = ChatHelper::FormatQuest(pQuest);
|
const std::string text_quest = ChatHelper::FormatQuest(pQuest);
|
||||||
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), pQuest->GetTitle());
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ bool CleanQuestLogAction::Execute(Event event)
|
|||||||
Player* requester = event.getOwner() ? event.getOwner() : GetMaster();
|
Player* requester = event.getOwner() ? event.getOwner() : GetMaster();
|
||||||
if (!requester)
|
if (!requester)
|
||||||
{
|
{
|
||||||
botAI->TellMaster("No event owner detected");
|
botAI->TellMaster("Не удалось определить владельца события");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ bool CleanQuestLogAction::Execute(Event event)
|
|||||||
// Only output this message if "debug rpg" strategy is enabled
|
// Only output this message if "debug rpg" strategy is enabled
|
||||||
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
|
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
|
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
|
// Output only if "debug rpg" strategy is enabled
|
||||||
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
|
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
|
// Remove quest
|
||||||
@@ -143,12 +143,12 @@ bool CleanQuestLogAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
const std::string text_quest = ChatHelper::FormatQuest(quest);
|
const std::string text_quest = ChatHelper::FormatQuest(quest);
|
||||||
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle());
|
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))
|
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
|
else
|
||||||
@@ -156,7 +156,7 @@ bool CleanQuestLogAction::Execute(Event event)
|
|||||||
// Only output if "debug rpg" strategy is enabled
|
// Only output if "debug rpg" strategy is enabled
|
||||||
if (botAI->HasStrategy("debug rpg", BotState::BOT_STATE_COMBAT))
|
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);
|
const std::string text_quest = ChatHelper::FormatQuest(quest);
|
||||||
LOG_INFO("playerbots", "{} => Quest [ {} ] removed", bot->GetName(), quest->GetTitle());
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal)
|
|||||||
if (botAI->GetMaster() == source)
|
if (botAI->GetMaster() == source)
|
||||||
{
|
{
|
||||||
botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT);
|
botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT);
|
||||||
botAI->TellMasterNoFacing("Fine.. I'll stay right here..");
|
botAI->TellMasterNoFacing("Хорошо... Останусь здесь...");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_BECKON:
|
case TEXT_EMOTE_BECKON:
|
||||||
@@ -171,7 +171,7 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal)
|
|||||||
if (botAI->GetMaster() == source)
|
if (botAI->GetMaster() == source)
|
||||||
{
|
{
|
||||||
botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT);
|
botAI->ChangeStrategy("+follow", BOT_STATE_NON_COMBAT);
|
||||||
botAI->TellMasterNoFacing("Wherever you go, I'll follow..");
|
botAI->TellMasterNoFacing("Куда бы вы ни пошли, я последую за вами...");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_WAVE:
|
case TEXT_EMOTE_WAVE:
|
||||||
@@ -579,36 +579,36 @@ bool EmoteActionBase::ReceiveEmote(Player* source, uint32 emote, bool verbal)
|
|||||||
break;
|
break;
|
||||||
/*case TEXT_EMOTE_BADFEELING:
|
/*case TEXT_EMOTE_BADFEELING:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_QUESTION);
|
bot->HandleEmoteCommand(EMOTE_ONESHOT_QUESTION);
|
||||||
bot->Say("I'm just waiting for the ominous music now...", LANG_UNIVERSAL);
|
bot->Say("Теперь осталось дождаться зловещей музыки...", LANG_UNIVERSAL);
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_MAP:
|
case TEXT_EMOTE_MAP:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
|
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
|
||||||
bot->Say("Noooooooo.. you just couldn't ask for directions, huh?", LANG_UNIVERSAL);
|
bot->Say("Неееет... Нельзя было просто спросить дорогу?", LANG_UNIVERSAL);
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_IDEA:
|
case TEXT_EMOTE_IDEA:
|
||||||
case TEXT_EMOTE_THINK:
|
case TEXT_EMOTE_THINK:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
|
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
|
||||||
bot->Say("Oh boy.. another genius idea...", LANG_UNIVERSAL);
|
bot->Say("Ох... Ещё одна гениальная идея...", LANG_UNIVERSAL);
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_OFFER:
|
case TEXT_EMOTE_OFFER:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
|
bot->HandleEmoteCommand(EMOTE_ONESHOT_NO);
|
||||||
bot->Say("No thanks.. I had some back at the last village", LANG_UNIVERSAL);
|
bot->Say("Нет, спасибо... В прошлой деревне уже хватило", LANG_UNIVERSAL);
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_PET:
|
case TEXT_EMOTE_PET:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_ROAR);
|
bot->HandleEmoteCommand(EMOTE_ONESHOT_ROAR);
|
||||||
bot->Say("Do I look like a dog to you?!", LANG_UNIVERSAL);
|
bot->Say("Я похож на собаку?!", LANG_UNIVERSAL);
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_ROLLEYES:
|
case TEXT_EMOTE_ROLLEYES:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_POINT);
|
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;
|
break;
|
||||||
case TEXT_EMOTE_SING:
|
case TEXT_EMOTE_SING:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_APPLAUD);
|
bot->HandleEmoteCommand(EMOTE_ONESHOT_APPLAUD);
|
||||||
bot->Say("Lovely... just lovely..", LANG_UNIVERSAL);
|
bot->Say("Прекрасно... Просто прекрасно...", LANG_UNIVERSAL);
|
||||||
break;
|
break;
|
||||||
case TEXT_EMOTE_COVEREARS:
|
case TEXT_EMOTE_COVEREARS:
|
||||||
bot->HandleEmoteCommand(EMOTE_ONESHOT_EXCLAMATION);
|
bot->HandleEmoteCommand(EMOTE_ONESHOT_EXCLAMATION);
|
||||||
bot->Yell("You think that's going to help you?!", LANG_UNIVERSAL);
|
bot->Yell("Думаете, это вам поможет?!", LANG_UNIVERSAL);
|
||||||
break;*/
|
break;*/
|
||||||
default:
|
default:
|
||||||
// return false;
|
// return false;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ void EquipAction::EquipItem(Item* item)
|
|||||||
{
|
{
|
||||||
bot->SetAmmo(itemId);
|
bot->SetAmmo(itemId);
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "equipping " << chat->FormatItem(itemProto);
|
out << "Надеваю: " << chat->FormatItem(itemProto);
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ void EquipAction::EquipItem(Item* item)
|
|||||||
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
|
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Equipping " << chat->FormatItem(itemProto) << " in ranged slot";
|
out << "Надеваю: " << chat->FormatItem(itemProto) << " в ячейку оружия дальнего боя";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,7 @@ void EquipAction::EquipItem(Item* item)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Equipping " << chat->FormatItem(itemProto) << " in main hand";
|
out << "Надеваю: " << chat->FormatItem(itemProto) << " в правую руку";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ void EquipAction::EquipItem(Item* item)
|
|||||||
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
|
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Equipping " << chat->FormatItem(itemProto) << " in offhand";
|
out << "Надеваю: " << chat->FormatItem(itemProto) << " в левую руку";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ void EquipAction::EquipItem(Item* item)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Equipping " << chat->FormatItem(itemProto);
|
out << "Надеваю: " << chat->FormatItem(itemProto);
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ bool EquipGlyphsAction::Execute(Event event)
|
|||||||
std::vector<GlyphInfo const*> glyphs;
|
std::vector<GlyphInfo const*> glyphs;
|
||||||
if (!CollectGlyphs(itemIds, 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,12 +144,12 @@ bool EquipGlyphsAction::Execute(Event event)
|
|||||||
|
|
||||||
if (!placed)
|
if (!placed)
|
||||||
{
|
{
|
||||||
botAI->TellMaster("Not enought empty sockets for all glyphs.");
|
botAI->TellMaster("Недостаточно свободных ячеек для всех символов.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster("Glyphs updated.");
|
botAI->TellMaster("Символы обновлены.");
|
||||||
|
|
||||||
// Flag for custom glyphs
|
// Flag for custom glyphs
|
||||||
botAI->GetAiObjectContext()->GetValue<bool>("custom_glyphs")->Set(true);
|
botAI->GetAiObjectContext()->GetValue<bool>("custom_glyphs")->Set(true);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
bool FlagAction::TellUsage()
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,15 +130,15 @@ bool FleeToGroupLeaderAction::Execute(Event /*event*/)
|
|||||||
if (distance < sPlayerbotAIConfig.reactDistance * 3)
|
if (distance < sPlayerbotAIConfig.reactDistance * 3)
|
||||||
{
|
{
|
||||||
if (!urand(0, 3))
|
if (!urand(0, 3))
|
||||||
botAI->TellMaster("I am close, wait for me!");
|
botAI->TellMaster("Я рядом, подождите меня!");
|
||||||
}
|
}
|
||||||
else if (distance < 1000)
|
else if (distance < 1000)
|
||||||
{
|
{
|
||||||
if (!urand(0, 10))
|
if (!urand(0, 10))
|
||||||
botAI->TellMaster("I heading to your position.");
|
botAI->TellMaster("Я направляюсь к вам.");
|
||||||
}
|
}
|
||||||
else if (!urand(0, 20))
|
else if (!urand(0, 20))
|
||||||
botAI->TellMaster("I am traveling to your position.");
|
botAI->TellMaster("Я иду к вам.");
|
||||||
|
|
||||||
botAI->SetNextCheckDelay(3000);
|
botAI->SetNextCheckDelay(3000);
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ bool TogglePetSpellAutoCastAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
// Debug message if pet spells have been toggled and debug is enabled
|
// Debug message if pet spells have been toggled and debug is enabled
|
||||||
if (toggled && sPlayerbotAIConfig.petChatCommandDebug == 1)
|
if (toggled && sPlayerbotAIConfig.petChatCommandDebug == 1)
|
||||||
botAI->TellMaster("Pet autocast spells have been toggled.");
|
botAI->TellMaster("Автоматическое применение способностей питомца переключено.");
|
||||||
|
|
||||||
return toggled;
|
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 there are no controlled pets or guardians, notify the player and exit
|
||||||
if (targets.empty())
|
if (targets.empty())
|
||||||
{
|
{
|
||||||
botAI->TellError("You have no pet or guardian pet.");
|
botAI->TellError("У вас нет питомца или стража.");
|
||||||
return false;
|
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 debug is enabled in config, inform the master of the new stance
|
||||||
if (sPlayerbotAIConfig.petChatCommandDebug == 1)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ bool GiveItemAction::Execute(Event /*event*/)
|
|||||||
moved = true;
|
moved = true;
|
||||||
|
|
||||||
std::ostringstream out;
|
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());
|
receiverAi->TellMasterNoFacing(out.str());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
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";
|
<< bot->GetName() << "- my bags are full";
|
||||||
receiverAi->TellError(out.str());
|
receiverAi->TellError(out.str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ bool GoAction::Execute(Event event)
|
|||||||
Map2ZoneCoordinates(x, y, bot->GetZoneId());
|
Map2ZoneCoordinates(x, y, bot->GetZoneId());
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "I am at " << x << "," << y;
|
out << "Я нахожусь в точке " << x << "," << y;
|
||||||
botAI->TellMaster(out.str());
|
botAI->TellMaster(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -53,14 +53,14 @@ bool GoAction::Execute(Event event)
|
|||||||
target->setForced(true);
|
target->setForced(true);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Traveling to " << dest->getTitle();
|
out << "Направляюсь к: " << dest->getTitle();
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Clearing travel target");
|
botAI->TellMasterNoFacing("Цель путешествия сброшена");
|
||||||
target->setTarget(TravelMgr::instance().nullTravelDestination, TravelMgr::instance().nullWorldPosition);
|
target->setTarget(TravelMgr::instance().nullTravelDestination, TravelMgr::instance().nullWorldPosition);
|
||||||
target->setForced(false);
|
target->setForced(false);
|
||||||
return true;
|
return true;
|
||||||
@@ -78,12 +78,12 @@ bool GoAction::Execute(Event event)
|
|||||||
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, go),
|
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, go),
|
||||||
sPlayerbotAIConfig.reactDistance))
|
sPlayerbotAIConfig.reactDistance))
|
||||||
{
|
{
|
||||||
botAI->TellError("It is too far away");
|
botAI->TellError("Слишком далеко.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Moving to " << ChatHelper::FormatGameobject(go);
|
out << "Двигаюсь к: " << ChatHelper::FormatGameobject(go);
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return MoveNear(bot->GetMapId(), go->GetPositionX(), go->GetPositionY(), go->GetPositionZ() + 0.5f,
|
return MoveNear(bot->GetMapId(), go->GetPositionX(), go->GetPositionY(), go->GetPositionZ() + 0.5f,
|
||||||
sPlayerbotAIConfig.followDistance);
|
sPlayerbotAIConfig.followDistance);
|
||||||
@@ -103,7 +103,7 @@ bool GoAction::Execute(Event event)
|
|||||||
if (strstri(unit->GetName().c_str(), param.c_str()))
|
if (strstri(unit->GetName().c_str(), param.c_str()))
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Moving to " << unit->GetName();
|
out << "Двигаюсь к: " << unit->GetName();
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return MoveNear(bot->GetMapId(), unit->GetPositionX(), unit->GetPositionY(),
|
return MoveNear(bot->GetMapId(), unit->GetPositionX(), unit->GetPositionY(),
|
||||||
unit->GetPositionZ() + 0.5f, sPlayerbotAIConfig.followDistance);
|
unit->GetPositionZ() + 0.5f, sPlayerbotAIConfig.followDistance);
|
||||||
@@ -137,7 +137,7 @@ bool GoAction::Execute(Event event)
|
|||||||
|
|
||||||
out << x << ";" << y << ";" << z << " =";
|
out << x << ";" << y << ";" << z << " =";
|
||||||
|
|
||||||
out << "path is: ";
|
out << "путь: ";
|
||||||
|
|
||||||
out << type;
|
out << type;
|
||||||
|
|
||||||
@@ -179,20 +179,20 @@ bool GoAction::Execute(Event event)
|
|||||||
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, x, y),
|
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, x, y),
|
||||||
sPlayerbotAIConfig.reactDistance))
|
sPlayerbotAIConfig.reactDistance))
|
||||||
{
|
{
|
||||||
botAI->TellMaster("It is too far away");
|
botAI->TellMaster("Слишком далеко.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map->IsInWater(bot->GetPhaseMask(), x, y, z, bot->GetCollisionHeight()))
|
if (map->IsInWater(bot->GetPhaseMask(), x, y, z, bot->GetCollisionHeight()))
|
||||||
{
|
{
|
||||||
botAI->TellError("It is in water");
|
botAI->TellError("Цель находится в воде");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
float ground = map->GetHeight(x, y, z + 0.5f);
|
float ground = map->GetHeight(x, y, z + 0.5f);
|
||||||
if (ground <= INVALID_HEIGHT)
|
if (ground <= INVALID_HEIGHT)
|
||||||
{
|
{
|
||||||
botAI->TellError("I can't go there");
|
botAI->TellError("Я не могу туда пройти");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ bool GoAction::Execute(Event event)
|
|||||||
Map2ZoneCoordinates(x1, y1, bot->GetZoneId());
|
Map2ZoneCoordinates(x1, y1, bot->GetZoneId());
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Moving to " << x1 << "," << y1;
|
out << "Двигаюсь к: " << x1 << "," << y1;
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
|
|
||||||
return MoveNear(bot->GetMapId(), x, y, z + 0.5f, sPlayerbotAIConfig.followDistance);
|
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),
|
if (ServerFacade::instance().IsDistanceGreaterThan(ServerFacade::instance().GetDistance2d(bot, pos.x, pos.y),
|
||||||
sPlayerbotAIConfig.reactDistance))
|
sPlayerbotAIConfig.reactDistance))
|
||||||
{
|
{
|
||||||
botAI->TellError("It is too far away");
|
botAI->TellError("Слишком далеко.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Moving to position " << param;
|
out << "Двигаюсь к позиции: " << param;
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return MoveNear(bot->GetMapId(), pos.x, pos.y, pos.z + 0.5f, sPlayerbotAIConfig.followDistance);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ bool GossipHelloAction::ProcessGossip(int32 menuToSelect, bool silent)
|
|||||||
if (menuToSelect != -1 && !menu.GetItem(menuToSelect))
|
if (menuToSelect != -1 && !menu.GetItem(menuToSelect))
|
||||||
{
|
{
|
||||||
if (!silent)
|
if (!silent)
|
||||||
botAI->TellError("Unknown gossip option");
|
botAI->TellError("Неизвестный вариант диалога");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ bool GossipHelloAction::Execute(ObjectGuid guid, int32 menuToSelect, bool silent
|
|||||||
else if (!bot->PlayerTalkClass)
|
else if (!bot->PlayerTalkClass)
|
||||||
{
|
{
|
||||||
if (!silent)
|
if (!silent)
|
||||||
botAI->TellError("I need to talk first");
|
botAI->TellError("Сначала нужно поговорить");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -28,17 +28,17 @@ bool GuildAcceptAction::Execute(Event event)
|
|||||||
uint32 guildId = inviter->GetGuildId();
|
uint32 guildId = inviter->GetGuildId();
|
||||||
if (!guildId)
|
if (!guildId)
|
||||||
{
|
{
|
||||||
botAI->TellError("You are not in a guild!");
|
botAI->TellError("Вы не состоите в гильдии!");
|
||||||
accept = false;
|
accept = false;
|
||||||
}
|
}
|
||||||
else if (bot->GetGuildId())
|
else if (bot->GetGuildId())
|
||||||
{
|
{
|
||||||
botAI->TellError("Sorry, I am in a guild already");
|
botAI->TellError("Я уже состою в гильдии");
|
||||||
accept = false;
|
accept = false;
|
||||||
}
|
}
|
||||||
else if (!botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, inviter, true))
|
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;
|
accept = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ bool GuildBankAction::Execute(Event event)
|
|||||||
|
|
||||||
if (!bot->GetGuildId() || (GetMaster() && GetMaster()->GetGuildId() != bot->GetGuildId()))
|
if (!bot->GetGuildId() || (GetMaster() && GetMaster()->GetGuildId() != bot->GetGuildId()))
|
||||||
{
|
{
|
||||||
botAI->TellMaster("I'm not in your guild!");
|
botAI->TellMaster("Я не состою в вашей гильдии!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ bool GuildBankAction::Execute(Event event)
|
|||||||
return Execute(text, go);
|
return Execute(text, go);
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster("Cannot find the guild bank nearby");
|
botAI->TellMaster("Поблизости нет банка гильдии");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +65,11 @@ bool GuildBankAction::MoveFromCharToBank(Item* item, GameObject* bank)
|
|||||||
|
|
||||||
// check source pos rights (item moved to bank)
|
// check source pos rights (item moved to bank)
|
||||||
if (!guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM))
|
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";
|
<< " to guild bank. I have no rights to put items in the first guild bank tab";
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
out << chat->FormatItem(item->GetTemplate()) << " put to guild bank";
|
out << chat->FormatItem(item->GetTemplate()) << " положено в банк гильдии";
|
||||||
guild->SwapItemsWithInventory(bot, false, 0, 255, playerBag, playerSlot, 0);
|
guild->SwapItemsWithInventory(bot, false, 0, 255, playerBag, playerSlot, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ bool GuildLeaveAction::Execute(Event event)
|
|||||||
Player* owner = event.getOwner();
|
Player* owner = event.getOwner();
|
||||||
if (owner && !botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, owner, true))
|
if (owner && !botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, false, owner, true))
|
||||||
{
|
{
|
||||||
botAI->TellError("Sorry, I am happy in my guild :)");
|
botAI->TellError("Меня устраивает моя гильдия :)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ bool HelpAction::Execute(Event /*event*/)
|
|||||||
void HelpAction::TellChatCommands()
|
void HelpAction::TellChatCommands()
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Whisper any of: ";
|
out << "Напишите одну из команд: ";
|
||||||
out << CombineSupported(chatContext->supports());
|
out << CombineSupported(chatContext->supports());
|
||||||
out << ", [item], [quest] or [object] link";
|
out << ", [item], [quest] or [object] link";
|
||||||
botAI->TellError(out.str());
|
botAI->TellError(out.str());
|
||||||
@@ -32,7 +32,7 @@ void HelpAction::TellChatCommands()
|
|||||||
void HelpAction::TellStrategies()
|
void HelpAction::TellStrategies()
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Possible strategies (co/nc/dead commands): ";
|
out << "Доступные стратегии (команды co/nc/dead): ";
|
||||||
out << CombineSupported(botAI->GetAiObjectContext()->GetSupportedStrategies());
|
out << CombineSupported(botAI->GetAiObjectContext()->GetSupportedStrategies());
|
||||||
botAI->TellError(out.str());
|
botAI->TellError(out.str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ bool HireAction::Execute(Event /*event*/)
|
|||||||
|
|
||||||
if (charCount >= 10)
|
if (charCount >= 10)
|
||||||
{
|
{
|
||||||
botAI->TellMaster("You already have the maximum number of characters");
|
botAI->TellMaster("У вас уже максимальное количество персонажей");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bot->GetLevel() > master->GetLevel())
|
if (bot->GetLevel() > master->GetLevel())
|
||||||
{
|
{
|
||||||
botAI->TellMaster("You cannot hire higher level characters than you");
|
botAI->TellMaster("Нельзя нанять персонажа выше вашего уровня");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,13 +46,13 @@ bool HireAction::Execute(Event /*event*/)
|
|||||||
if (discount < moneyReq)
|
if (discount < moneyReq)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
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";
|
<< " as a trade discount";
|
||||||
botAI->TellMaster(out.str());
|
botAI->TellMaster(out.str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster("I will join you at your next relogin");
|
botAI->TellMaster("Я присоединюсь после вашего следующего входа в игру");
|
||||||
|
|
||||||
bot->SetMoney(moneyReq);
|
bot->SetMoney(moneyReq);
|
||||||
RandomPlayerbotMgr::instance().Remove(bot);
|
RandomPlayerbotMgr::instance().Remove(bot);
|
||||||
|
|||||||
@@ -130,40 +130,40 @@ void InventoryAction::TellItems(std::map<uint32, uint32> itemMap, std::map<uint3
|
|||||||
switch (proto->Class)
|
switch (proto->Class)
|
||||||
{
|
{
|
||||||
case ITEM_CLASS_CONSUMABLE:
|
case ITEM_CLASS_CONSUMABLE:
|
||||||
botAI->TellMaster("--- consumable ---");
|
botAI->TellMaster("--- Расходуемые предметы ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_CONTAINER:
|
case ITEM_CLASS_CONTAINER:
|
||||||
botAI->TellMaster("--- container ---");
|
botAI->TellMaster("--- Сумки ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_WEAPON:
|
case ITEM_CLASS_WEAPON:
|
||||||
botAI->TellMaster("--- weapon ---");
|
botAI->TellMaster("--- Оружие ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_ARMOR:
|
case ITEM_CLASS_ARMOR:
|
||||||
botAI->TellMaster("--- armor ---");
|
botAI->TellMaster("--- Доспехи ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_REAGENT:
|
case ITEM_CLASS_REAGENT:
|
||||||
botAI->TellMaster("--- reagent ---");
|
botAI->TellMaster("--- Реагенты ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_PROJECTILE:
|
case ITEM_CLASS_PROJECTILE:
|
||||||
botAI->TellMaster("--- projectile ---");
|
botAI->TellMaster("--- Боеприпасы ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_TRADE_GOODS:
|
case ITEM_CLASS_TRADE_GOODS:
|
||||||
botAI->TellMaster("--- trade goods ---");
|
botAI->TellMaster("--- Хозяйственные товары ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_RECIPE:
|
case ITEM_CLASS_RECIPE:
|
||||||
botAI->TellMaster("--- recipe ---");
|
botAI->TellMaster("--- Рецепты ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_QUIVER:
|
case ITEM_CLASS_QUIVER:
|
||||||
botAI->TellMaster("--- quiver ---");
|
botAI->TellMaster("--- Колчаны ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_QUEST:
|
case ITEM_CLASS_QUEST:
|
||||||
botAI->TellMaster("--- quest items ---");
|
botAI->TellMaster("--- Предметы заданий ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_KEY:
|
case ITEM_CLASS_KEY:
|
||||||
botAI->TellMaster("--- keys ---");
|
botAI->TellMaster("--- Ключи ---");
|
||||||
break;
|
break;
|
||||||
case ITEM_CLASS_MISC:
|
case ITEM_CLASS_MISC:
|
||||||
botAI->TellMaster("--- other ---");
|
botAI->TellMaster("--- Прочее ---");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ bool LfgAction::Execute(Event event)
|
|||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
if (allowedRoles[role] > 1)
|
if (allowedRoles[role] > 1)
|
||||||
{
|
{
|
||||||
out << "Joining as " << placeholders["%role"] << ", " << placeholders["%spotsleft"] << " "
|
out << "Присоединяюсь в роли: " << placeholders["%role"] << ", " << placeholders["%spotsleft"] << " "
|
||||||
<< placeholders["%role"] << " spots left.";
|
<< placeholders["%role"] << " spots left.";
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ bool LfgAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
out << "Joining as " << placeholders["%role"] << ".";
|
out << "Присоединяюсь в роли: " << placeholders["%role"] << ".";
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
|
|
||||||
//botAI->DoSpecificAction("autogear");
|
//botAI->DoSpecificAction("autogear");
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ bool LeaveGroupAction::Leave()
|
|||||||
|
|
||||||
Player* master = botAI -> GetMaster();
|
Player* master = botAI -> GetMaster();
|
||||||
if (master)
|
if (master)
|
||||||
botAI->TellMaster("Goodbye!", PLAYERBOT_SECURITY_TALK);
|
botAI->TellMaster("До свидания!", PLAYERBOT_SECURITY_TALK);
|
||||||
|
|
||||||
botAI->LeaveOrDisbandGroup();
|
botAI->LeaveOrDisbandGroup();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -44,19 +44,19 @@ void ListQuestsAction::ListQuests(QuestListFilter filter, QuestTravelDetail trav
|
|||||||
bool showCompleted = filter & QUEST_LIST_FILTER_COMPLETED;
|
bool showCompleted = filter & QUEST_LIST_FILTER_COMPLETED;
|
||||||
|
|
||||||
if (showIncompleted)
|
if (showIncompleted)
|
||||||
botAI->TellMaster("--- Incompleted quests ---");
|
botAI->TellMaster("--- Невыполненные задания ---");
|
||||||
|
|
||||||
uint32 incompleteCount = ListQuests(false, !showIncompleted, travelDetail);
|
uint32 incompleteCount = ListQuests(false, !showIncompleted, travelDetail);
|
||||||
|
|
||||||
if (showCompleted)
|
if (showCompleted)
|
||||||
botAI->TellMaster("--- Completed quests ---");
|
botAI->TellMaster("--- Выполненные задания ---");
|
||||||
|
|
||||||
uint32 completeCount = ListQuests(true, !showCompleted, travelDetail);
|
uint32 completeCount = ListQuests(true, !showCompleted, travelDetail);
|
||||||
|
|
||||||
botAI->TellMaster("--- Summary ---");
|
botAI->TellMaster("--- Итого ---");
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Total: " << (completeCount + incompleteCount) << " / 25 (incompleted: " << incompleteCount
|
out << "Всего: " << (completeCount + incompleteCount) << " / 25 (невыполнено: " << incompleteCount
|
||||||
<< ", completed: " << completeCount << ")";
|
<< ", completed: " << completeCount << ")";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,11 +284,11 @@ bool ListSpellsAction::Execute(Event event)
|
|||||||
if (spells.empty())
|
if (spells.empty())
|
||||||
{
|
{
|
||||||
// CHANGE: Give early feedback when no spells match the filter.
|
// CHANGE: Give early feedback when no spells match the filter.
|
||||||
botAI->TellMaster("No spells found.");
|
botAI->TellMaster("Заклинания не найдены.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster("=== Spells ===");
|
botAI->TellMaster("=== Заклинания ===");
|
||||||
|
|
||||||
std::sort(spells.begin(), spells.end(), CompareSpells);
|
std::sort(spells.begin(), spells.end(), CompareSpells);
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ bool LogLevelAction::Execute(Event event)
|
|||||||
if (param != "?")
|
if (param != "?")
|
||||||
{
|
{
|
||||||
value->Set(string2logLevel(param));
|
value->Set(string2logLevel(param));
|
||||||
out << "My log level set to " << logLevel2string(value->Get());
|
out << "Уровень журнала установлен: " << logLevel2string(value->Get());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
out << "My log level is " << logLevel2string(value->Get());
|
out << "Текущий уровень журнала: " << logLevel2string(value->Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ bool LootStrategyAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Loot strategy: ";
|
out << "Стратегия добычи: ";
|
||||||
out << lootStrategy->Get()->GetName();
|
out << lootStrategy->Get()->GetName();
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Always loot items: ";
|
out << "Всегда собирать: ";
|
||||||
|
|
||||||
for (uint32 itemId : alwaysLootItems)
|
for (uint32 itemId : alwaysLootItems)
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,7 @@ bool LootStrategyAction::Execute(Event event)
|
|||||||
lootStrategy->Set(LootStrategyValue::instance(strategy));
|
lootStrategy->Set(LootStrategyValue::instance(strategy));
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Loot strategy set to " << lootStrategy->Get()->GetName();
|
out << "Установлена стратегия добычи: " << lootStrategy->Get()->GetName();
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -80,12 +80,12 @@ bool LootStrategyAction::Execute(Event event)
|
|||||||
if (j != alwaysLootItems.end())
|
if (j != alwaysLootItems.end())
|
||||||
alwaysLootItems.erase(j);
|
alwaysLootItems.erase(j);
|
||||||
|
|
||||||
botAI->TellMaster("Item(s) removed from always loot list");
|
botAI->TellMaster("Предметы удалены из списка обязательного сбора");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
alwaysLootItems.insert(itemid);
|
alwaysLootItems.insert(itemid);
|
||||||
botAI->TellMaster("Item(s) added to always loot list");
|
botAI->TellMaster("Предметы добавлены в список обязательного сбора");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TellMailProcessor : public MailProcessor
|
|||||||
public:
|
public:
|
||||||
bool Before(PlayerbotAI* botAI) override
|
bool Before(PlayerbotAI* botAI) override
|
||||||
{
|
{
|
||||||
botAI->TellMaster("=== Mailbox ===");
|
botAI->TellMaster("=== Почта ===");
|
||||||
tells.clear();
|
tells.clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ public:
|
|||||||
Player* bot = botAI->GetBot();
|
Player* bot = botAI->GetBot();
|
||||||
if (!CheckBagSpace(bot))
|
if (!CheckBagSpace(bot))
|
||||||
{
|
{
|
||||||
botAI->TellError("Not enough bag space");
|
botAI->TellError("В сумках недостаточно места");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ bool MailAction::Execute(Event event)
|
|||||||
|
|
||||||
if (!MailProcessor::FindMailbox(botAI))
|
if (!MailProcessor::FindMailbox(botAI))
|
||||||
{
|
{
|
||||||
botAI->TellError("There is no mailbox nearby");
|
botAI->TellError("Поблизости нет почтового ящика");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ bool MoveToRpgTargetAction::Execute(Event /*event*/)
|
|||||||
if (botAI->HasStrategy("debug rpg", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject())
|
if (botAI->HasStrategy("debug rpg", BOT_STATE_NON_COMBAT) && guidP.GetWorldObject())
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Heading to: ";
|
out << "Направляюсь к цели: ";
|
||||||
out << chat->FormatWorldobject(guidP.GetWorldObject());
|
out << chat->FormatWorldobject(guidP.GetWorldObject());
|
||||||
botAI->TellMasterNoFacing(out);
|
botAI->TellMasterNoFacing(out);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ bool MoveToTravelTargetAction::Execute(Event /*event*/)
|
|||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
if (botAI->GetMaster() && !bot->GetGroup()->IsMember(botAI->GetMaster()->GetGUID()))
|
if (botAI->GetMaster() && !bot->GetGroup()->IsMember(botAI->GetMaster()->GetGUID()))
|
||||||
out << "Waiting a bit for ";
|
out << "Немного подожду: ";
|
||||||
else
|
else
|
||||||
out << "Please hurry up ";
|
out << "Пожалуйста, поторопитесь: ";
|
||||||
|
|
||||||
out << member->GetName();
|
out << member->GetName();
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ bool MovementAction::MoveToLOS(WorldObject* target, bool ranged)
|
|||||||
if (dest.isSet())
|
if (dest.isSet())
|
||||||
return MoveTo(dest.mapId, dest.x, dest.y, dest.z);
|
return MoveTo(dest.mapId, dest.x, dest.y, dest.z);
|
||||||
else
|
else
|
||||||
botAI->TellError("All paths not in LOS");
|
botAI->TellError("Ни один путь не находится в поле зрения");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1160,13 +1160,13 @@ bool MovementAction::Follow(Unit* target, float distance, float angle)
|
|||||||
if (bot->isDead() && botAI->GetMaster()->IsAlive())
|
if (bot->isDead() && botAI->GetMaster()->IsAlive())
|
||||||
{
|
{
|
||||||
bot->ResurrectPlayer(1.0f, false);
|
bot->ResurrectPlayer(1.0f, false);
|
||||||
botAI->TellMasterNoFacing("I live, again!");
|
botAI->TellMasterNoFacing("Я снова жив!");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
botAI->TellError("I am stuck while following");
|
botAI->TellError("Я застрял во время следования");
|
||||||
|
|
||||||
bot->CombatStop(true);
|
bot->CombatStop(true);
|
||||||
botAI->TellMasterNoFacing("I will there soon.");
|
botAI->TellMasterNoFacing("Я скоро буду там.");
|
||||||
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
||||||
bot->TeleportTo(target->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(),
|
bot->TeleportTo(target->GetMapId(), target->GetPositionX(), target->GetPositionY(), target->GetPositionZ(),
|
||||||
target->GetOrientation()); return false;
|
target->GetOrientation()); return false;
|
||||||
@@ -1379,7 +1379,7 @@ bool MovementAction::Flee(Unit* target)
|
|||||||
|
|
||||||
if (!IsMovingAllowed())
|
if (!IsMovingAllowed())
|
||||||
{
|
{
|
||||||
botAI->TellError("I am stuck while fleeing");
|
botAI->TellError("Я застрял при отступлении");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1535,7 +1535,7 @@ bool MovementAction::Flee(Unit* target)
|
|||||||
float rx, ry, rz;
|
float rx, ry, rz;
|
||||||
if (!manager.CalculateDestination(&rx, &ry, &rz))
|
if (!manager.CalculateDestination(&rx, &ry, &rz))
|
||||||
{
|
{
|
||||||
botAI->TellError("Nowhere to flee");
|
botAI->TellError("Некуда отступать");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1940,7 +1940,7 @@ bool AvoidAoeAction::AvoidAuraWithDynamicObj()
|
|||||||
lastTellTimer = time(NULL);
|
lastTellTimer = time(NULL);
|
||||||
lastMoveTimer = getMSTime();
|
lastMoveTimer = getMSTime();
|
||||||
std::ostringstream out;
|
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);
|
bot->Say(out.str(), LANG_UNIVERSAL);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -2008,7 +2008,7 @@ bool AvoidAoeAction::AvoidGameObjectWithDamage()
|
|||||||
lastTellTimer = time(NULL);
|
lastTellTimer = time(NULL);
|
||||||
lastMoveTimer = getMSTime();
|
lastMoveTimer = getMSTime();
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "I'm avoiding " << name.str() << " (" << spellInfo->Id << ")" << " Radius " << radius
|
out << "Избегаю: " << name.str() << " (" << spellInfo->Id << ")" << "; радиус " << radius
|
||||||
<< " - [Trap]";
|
<< " - [Trap]";
|
||||||
bot->Say(out.str(), LANG_UNIVERSAL);
|
bot->Say(out.str(), LANG_UNIVERSAL);
|
||||||
}
|
}
|
||||||
@@ -2075,7 +2075,7 @@ bool AvoidAoeAction::AvoidUnitWithDamageAura()
|
|||||||
lastTellTimer = time(NULL);
|
lastTellTimer = time(NULL);
|
||||||
lastMoveTimer = getMSTime();
|
lastMoveTimer = getMSTime();
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "I'm avoiding " << name.str() << " (" << triggerSpellInfo->Id << ")"
|
out << "Избегаю: " << name.str() << " (" << triggerSpellInfo->Id << ")"
|
||||||
<< " Radius " << radius << " - [Unit Trigger]";
|
<< " Radius " << radius << " - [Unit Trigger]";
|
||||||
bot->Say(out.str(), LANG_UNIVERSAL);
|
bot->Say(out.str(), LANG_UNIVERSAL);
|
||||||
}
|
}
|
||||||
@@ -2554,7 +2554,7 @@ bool DisperseSetAction::Execute(Event event)
|
|||||||
if (text == "disable")
|
if (text == "disable")
|
||||||
{
|
{
|
||||||
RESET_AI_VALUE(float, "disperse distance");
|
RESET_AI_VALUE(float, "disperse distance");
|
||||||
botAI->TellMasterNoFacing("Disable disperse");
|
botAI->TellMasterNoFacing("Рассредоточение отключено");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == "enable" || text == "reset")
|
if (text == "enable" || text == "reset")
|
||||||
@@ -2569,7 +2569,7 @@ bool DisperseSetAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
float dis = AI_VALUE(float, "disperse distance");
|
float dis = AI_VALUE(float, "disperse distance");
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Enable disperse distance " << std::setprecision(2) << dis;
|
out << "Включено рассредоточение с дистанцией " << std::setprecision(2) << dis;
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2579,13 +2579,13 @@ bool DisperseSetAction::Execute(Event event)
|
|||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
if (dis <= 0.0f)
|
if (dis <= 0.0f)
|
||||||
{
|
{
|
||||||
out << "Enable disperse first";
|
out << "Сначала включите рассредоточение";
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
dis += 1.0f;
|
dis += 1.0f;
|
||||||
SET_AI_VALUE(float, "disperse distance", dis);
|
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());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2599,7 +2599,7 @@ bool DisperseSetAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
SET_AI_VALUE(float, "disperse distance", dis);
|
SET_AI_VALUE(float, "disperse distance", dis);
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Increase disperse distance to " << std::setprecision(2) << dis;
|
out << "Дистанция рассредоточения увеличена до " << std::setprecision(2) << dis;
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2611,18 +2611,18 @@ bool DisperseSetAction::Execute(Event event)
|
|||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
if (dis < 0 || dis > 100.0f)
|
if (dis < 0 || dis > 100.0f)
|
||||||
{
|
{
|
||||||
out << "Invalid disperse distance " << std::setprecision(2) << dis;
|
out << "Недопустимая дистанция рассредоточения: " << std::setprecision(2) << dis;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SET_AI_VALUE(float, "disperse distance", dis);
|
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());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
std::ostringstream out;
|
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");
|
float dis = AI_VALUE(float, "disperse distance");
|
||||||
if (dis > 0.0f)
|
if (dis > 0.0f)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,6 @@ void OpenItemAction::OpenItem(Item* item, uint8 bag, uint8 slot)
|
|||||||
botAI->GetAiObjectContext()->GetValue<LootObject>("loot target")->Set(lootObject);
|
botAI->GetAiObjectContext()->GetValue<LootObject>("loot target")->Set(lootObject);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Opened item: " << item->GetTemplate()->Name1;
|
out << "Открыт предмет: " << item->GetTemplate()->Name1;
|
||||||
botAI->TellMaster(out.str());
|
botAI->TellMaster(out.str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ bool OutfitAction::Execute(Event event)
|
|||||||
if (param == "?")
|
if (param == "?")
|
||||||
{
|
{
|
||||||
List();
|
List();
|
||||||
botAI->TellMaster("outfit <name> +[item] to add items");
|
botAI->TellMaster("outfit <имя> +[предмет] — добавить предметы");
|
||||||
botAI->TellMaster("outfit <name> -[item] to remove items");
|
botAI->TellMaster("outfit <имя> -[предмет] — удалить предметы");
|
||||||
botAI->TellMaster("outfit <name> equip/replace to equip items");
|
botAI->TellMaster("outfit <имя> equip/replace — надеть предметы");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -30,7 +30,7 @@ bool OutfitAction::Execute(Event event)
|
|||||||
Save(name, items);
|
Save(name, items);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Setting outfit " << name << " as " << param;
|
out << "Настраиваю комплект: " << name << " как " << param;
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ bool OutfitAction::Execute(Event event)
|
|||||||
if (command == "equip")
|
if (command == "equip")
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Equipping outfit " << name;
|
out << "Надеваю комплект: " << name;
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
|
|
||||||
EquipItems(outfit);
|
EquipItems(outfit);
|
||||||
@@ -57,7 +57,7 @@ bool OutfitAction::Execute(Event event)
|
|||||||
else if (command == "replace")
|
else if (command == "replace")
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Replacing current equip with outfit " << name;
|
out << "Заменяю текущую экипировку комплектом: " << name;
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
|
|
||||||
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++)
|
for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; slot++)
|
||||||
@@ -82,7 +82,7 @@ bool OutfitAction::Execute(Event event)
|
|||||||
else if (command == "reset")
|
else if (command == "reset")
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Resetting outfit " << name;
|
out << "Сбрасываю комплект: " << name;
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
|
|
||||||
Save(name, ItemIds());
|
Save(name, ItemIds());
|
||||||
@@ -91,7 +91,7 @@ bool OutfitAction::Execute(Event event)
|
|||||||
else if (command == "update")
|
else if (command == "update")
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Updating with current items outfit " << name;
|
out << "Обновляю комплект текущими предметами: " << name;
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
|
|
||||||
Update(name);
|
Update(name);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ bool PetitionSignAction::Execute(Event event)
|
|||||||
if (bot->GetArenaTeamId(slot))
|
if (bot->GetArenaTeamId(slot))
|
||||||
{
|
{
|
||||||
// player is already in an arena team
|
// player is already in an arena team
|
||||||
botAI->TellError("Sorry, I am already in such team");
|
botAI->TellError("У меня уже есть такая команда");
|
||||||
accept = false;
|
accept = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,13 +46,13 @@ bool PetitionSignAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
if (bot->GetGuildId())
|
if (bot->GetGuildId())
|
||||||
{
|
{
|
||||||
botAI->TellError("Sorry, I am in a guild already");
|
botAI->TellError("Я уже состою в гильдии");
|
||||||
accept = false;
|
accept = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bot->GetGuildIdInvited())
|
if (bot->GetGuildIdInvited())
|
||||||
{
|
{
|
||||||
botAI->TellError("Sorry, I am invited to a guild already");
|
botAI->TellError("Меня уже пригласили в гильдию");
|
||||||
accept = false;
|
accept = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ bool PetitionSignAction::Execute(Event event)
|
|||||||
/*if (QueryResult* result = CharacterDatabase.Query("SELECT playerguid FROM petition_sign WHERE player_account =
|
/*if (QueryResult* result = CharacterDatabase.Query("SELECT playerguid FROM petition_sign WHERE player_account =
|
||||||
{} AND petitionguid = {}'", bot->GetSession()->GetAccountId(), petitionGuid.GetCounter()))
|
{} AND petitionguid = {}'", bot->GetSession()->GetAccountId(), petitionGuid.GetCounter()))
|
||||||
{
|
{
|
||||||
botAI->TellError("Sorry, I already signed this pettition");
|
botAI->TellError("Я уже подписал эту хартию");
|
||||||
accept = false;
|
accept = false;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
@@ -88,7 +88,7 @@ bool PetitionSignAction::Execute(Event event)
|
|||||||
WorldPacket data(CMSG_PETITION_SIGN, 20);
|
WorldPacket data(CMSG_PETITION_SIGN, 20);
|
||||||
data << petitionGuid << unk;
|
data << petitionGuid << unk;
|
||||||
bot->GetSession()->HandlePetitionSignOpcode(data);
|
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(),
|
LOG_INFO("playerbots", "Bot {} <{}> accepts {} invite", bot->GetGUID().ToString().c_str(),
|
||||||
bot->GetName().c_str(), isArena ? "Arena" : "Guild");
|
bot->GetName().c_str(), isArena ? "Arena" : "Guild");
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
void TellPosition(PlayerbotAI* botAI, std::string const name, PositionInfo pos)
|
void TellPosition(PlayerbotAI* botAI, std::string const name, PositionInfo pos)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Position " << name;
|
out << "Позиция " << name;
|
||||||
|
|
||||||
if (pos.isSet())
|
if (pos.isSet())
|
||||||
{
|
{
|
||||||
@@ -22,7 +22,7 @@ void TellPosition(PlayerbotAI* botAI, std::string const name, PositionInfo pos)
|
|||||||
out << " is set to " << x << "," << y;
|
out << " is set to " << x << "," << y;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
out << " is not set";
|
out << " не установлена";
|
||||||
|
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ bool PositionAction::Execute(Event event)
|
|||||||
std::vector<std::string> params = split(param, ' ');
|
std::vector<std::string> params = split(param, ' ');
|
||||||
if (params.size() != 2)
|
if (params.size() != 2)
|
||||||
{
|
{
|
||||||
botAI->TellMaster("Whisper position <name> ?/set/reset");
|
botAI->TellMaster("Напишите: position <имя> ?/set/reset");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ bool PositionAction::Execute(Event event)
|
|||||||
posMap[name] = pos;
|
posMap[name] = pos;
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Position " << name << " is set";
|
out << "Позиция " << name << " установлена";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ bool PositionAction::Execute(Event event)
|
|||||||
posMap[name] = pos;
|
posMap[name] = pos;
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Position " << name << " is set";
|
out << "Позиция " << name << " установлена";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ bool PositionAction::Execute(Event event)
|
|||||||
posMap[name] = pos;
|
posMap[name] = pos;
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Position " << name << " is reset";
|
out << "Позиция " << name << " сброшена";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ bool MoveToPositionAction::Execute(Event /*event*/)
|
|||||||
if (!pos.isSet())
|
if (!pos.isSet())
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Position " << qualifier << " is not set";
|
out << "Позиция " << qualifier << " не установлена";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ bool ReturnToStayPositionAction::isPossible()
|
|||||||
const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z);
|
const float distance = bot->GetDistance(stayPosition.x, stayPosition.y, stayPosition.z);
|
||||||
if (distance > sPlayerbotAIConfig.reactDistance)
|
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
|
// Set the stay position to current position
|
||||||
stayPosition.Set(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId());
|
stayPosition.Set(bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId());
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ std::string const QueryItemUsageAction::QueryItemPrice(ItemTemplate const* item)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sellPrice)
|
if (sellPrice)
|
||||||
msg << "Sell: " << chat->formatMoney(sellPrice);
|
msg << "Продажа: " << chat->formatMoney(sellPrice);
|
||||||
|
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << item->ItemId;
|
out << item->ItemId;
|
||||||
@@ -153,7 +153,7 @@ std::string const QueryItemUsageAction::QueryItemPrice(ItemTemplate const* item)
|
|||||||
if (sellPrice)
|
if (sellPrice)
|
||||||
msg << " ";
|
msg << " ";
|
||||||
|
|
||||||
msg << "Buy: " << chat->formatMoney(buyPrice);
|
msg << "Покупка: " << chat->formatMoney(buyPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg.str();
|
return msg.str();
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ void QueryQuestAction::TellObjectives(uint32 questId)
|
|||||||
// Checks if the questTemplate is valid
|
// Checks if the questTemplate is valid
|
||||||
if (!questTemplate)
|
if (!questTemplate)
|
||||||
{
|
{
|
||||||
botAI->TellMaster("Quest template not found.");
|
botAI->TellMaster("Шаблон задания не найден.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
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());
|
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);
|
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))
|
//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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,18 +220,18 @@ bool QuestAction::AcceptQuest(Quest const* quest, ObjectGuid questGiver)
|
|||||||
uint32 questId = quest->GetQuestId();
|
uint32 questId = quest->GetQuestId();
|
||||||
|
|
||||||
if (bot->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE)
|
if (bot->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE)
|
||||||
out << "Already completed";
|
out << "Уже выполнено";
|
||||||
else if (!bot->CanTakeQuest(quest, false))
|
else if (!bot->CanTakeQuest(quest, false))
|
||||||
{
|
{
|
||||||
if (!bot->SatisfyQuestStatus(quest, false))
|
if (!bot->SatisfyQuestStatus(quest, false))
|
||||||
out << "Already on";
|
out << "Уже выполняется";
|
||||||
else
|
else
|
||||||
out << "Can't take";
|
out << "Невозможно взять";
|
||||||
}
|
}
|
||||||
else if (!bot->SatisfyQuestLog(false))
|
else if (!bot->SatisfyQuestLog(false))
|
||||||
out << "Quest log is full";
|
out << "Журнал заданий заполнен";
|
||||||
else if (!bot->CanAddQuest(quest, false))
|
else if (!bot->CanAddQuest(quest, false))
|
||||||
out << "Bags are full";
|
out << "Сумки заполнены";
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WorldPacket p(CMSG_QUESTGIVER_ACCEPT_QUEST);
|
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)
|
if (bot->GetQuestStatus(questId) != QUEST_STATUS_NONE && bot->GetQuestStatus(questId) != QUEST_STATUS_REWARDED)
|
||||||
{
|
{
|
||||||
BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest);
|
BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest);
|
||||||
out << "Accepted " << chat->FormatQuest(quest);
|
out << "Принято: " << chat->FormatQuest(quest);
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
out << "Cannot accept";
|
out << "Невозможно принять";
|
||||||
}
|
}
|
||||||
|
|
||||||
out << " " << chat->FormatQuest(quest);
|
out << " " << chat->FormatQuest(quest);
|
||||||
@@ -288,7 +288,7 @@ bool QuestUpdateCompleteAction::Execute(Event event)
|
|||||||
// }
|
// }
|
||||||
const auto format = ChatHelper::FormatQuest(qInfo);
|
const auto format = ChatHelper::FormatQuest(qInfo);
|
||||||
if (botAI->GetMaster())
|
if (botAI->GetMaster())
|
||||||
botAI->TellMasterNoFacing("Quest completed " + format);
|
botAI->TellMasterNoFacing("Задание выполнено: " + format);
|
||||||
BroadcastHelper::BroadcastQuestUpdateComplete(botAI, bot, qInfo);
|
BroadcastHelper::BroadcastQuestUpdateComplete(botAI, bot, qInfo);
|
||||||
botAI->rpgStatistic.questCompleted++;
|
botAI->rpgStatistic.questCompleted++;
|
||||||
// LOG_DEBUG("playerbots", "[New rpg] {} complete quest {}", bot->GetName(), qInfo->GetQuestId());
|
// LOG_DEBUG("playerbots", "[New rpg] {} complete quest {}", bot->GetName(), qInfo->GetQuestId());
|
||||||
@@ -461,7 +461,7 @@ bool QuestUpdateFailedTimerAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
botAI->TellMaster("Failed timer for " + std::to_string(questId));
|
botAI->TellMaster("Не удалось запустить таймер задания: " + std::to_string(questId));
|
||||||
}
|
}
|
||||||
|
|
||||||
//drop quest
|
//drop quest
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ bool QuestConfirmAcceptAction::Execute(Event event)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Quest: " << chat->FormatQuest(quest) << " confirm accept";
|
out << "Задание: " << chat->FormatQuest(quest) << " — подтвердите принятие";
|
||||||
botAI->TellMaster(out);
|
botAI->TellMaster(out);
|
||||||
bot->GetSession()->HandleQuestConfirmAccept(sendPacket);
|
bot->GetSession()->HandleQuestConfirmAccept(sendPacket);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -97,19 +97,19 @@ public:
|
|||||||
{
|
{
|
||||||
if (!bot->GetUInt32Value(PLAYER_AMMO_ID))
|
if (!bot->GetUInt32Value(PLAYER_AMMO_ID))
|
||||||
{
|
{
|
||||||
botAI->TellError("Out of ammo!");
|
botAI->TellError("Закончились боеприпасы!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bot->GetPet())
|
if (!bot->GetPet())
|
||||||
{
|
{
|
||||||
botAI->TellError("No pet!");
|
botAI->TellError("Нет питомца!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bot->GetPet()->GetHappinessState() == UNHAPPY)
|
if (bot->GetPet()->GetHappinessState() == UNHAPPY)
|
||||||
{
|
{
|
||||||
botAI->TellError("Pet is unhappy!");
|
botAI->TellError("Питомец недоволен!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ bool ReleaseSpiritAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
if (!bot->InBattleground())
|
if (!bot->InBattleground())
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("I am not dead, will wait here");
|
botAI->TellMasterNoFacing("Мне не требуется воскрешение, подожду здесь");
|
||||||
// -follow in bg is overwriten each tick with +follow
|
// -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
|
// +stay in bg causes stuttering effect as bot is cycled between +stay and +follow each tick
|
||||||
botAI->ChangeStrategy("-follow,+stay", BOT_STATE_NON_COMBAT);
|
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))
|
if (bot->GetCorpse() && bot->HasPlayerFlag(PLAYER_FLAGS_GHOST))
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("I am already a spirit");
|
botAI->TellMasterNoFacing("Я уже дух");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ bool RepairAllAction::Execute(Event /*event*/)
|
|||||||
if (totalCost > 0)
|
if (totalCost > 0)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "Repair: " << chat->formatMoney(totalCost) << " (" << unit->GetName() << ")";
|
out << "Ремонт: " << chat->formatMoney(totalCost) << " (" << unit->GetName() << ")";
|
||||||
botAI->TellMasterNoFacing(out.str());
|
botAI->TellMasterNoFacing(out.str());
|
||||||
|
|
||||||
bot->PlayDistanceSound(1116);
|
bot->PlayDistanceSound(1116);
|
||||||
@@ -56,6 +56,6 @@ bool RepairAllAction::Execute(Event /*event*/)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
botAI->TellError("Cannot find any npc to repair at");
|
botAI->TellError("Не удалось найти персонажа для ремонта");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,6 @@ bool ResetAiAction::Execute(Event event)
|
|||||||
}
|
}
|
||||||
PlayerbotRepository::instance().Reset(botAI);
|
PlayerbotRepository::instance().Reset(botAI);
|
||||||
botAI->ResetStrategies(false);
|
botAI->ResetStrategies(false);
|
||||||
botAI->TellMaster("AI was reset to defaults");
|
botAI->TellMaster("Настройки ИИ сброшены");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,18 +62,18 @@ bool RevealGatheringItemAction::Execute(Event /*event*/)
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
std::ostringstream msg;
|
std::ostringstream msg;
|
||||||
msg << "I see a " << ChatHelper::FormatGameobject(go) << ". ";
|
msg << "Вижу: " << ChatHelper::FormatGameobject(go) << ". ";
|
||||||
|
|
||||||
switch (go->GetGoType())
|
switch (go->GetGoType())
|
||||||
{
|
{
|
||||||
case GAMEOBJECT_TYPE_CHEST:
|
case GAMEOBJECT_TYPE_CHEST:
|
||||||
msg << "Let's look at it.";
|
msg << "Давайте посмотрим.";
|
||||||
break;
|
break;
|
||||||
case GAMEOBJECT_TYPE_FISHINGNODE:
|
case GAMEOBJECT_TYPE_FISHINGNODE:
|
||||||
msg << "Let's fish a bit.";
|
msg << "Давайте немного порыбачим.";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
msg << "Should we go nearer?";
|
msg << "Подойти ближе?";
|
||||||
}
|
}
|
||||||
|
|
||||||
// everything is fine, do it
|
// everything is fine, do it
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ bool ReviveFromCorpseAction::Execute(Event event)
|
|||||||
{
|
{
|
||||||
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
|
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
|
||||||
{
|
{
|
||||||
botAI->TellMasterNoFacing("Welcome back!");
|
botAI->TellMasterNoFacing("С возвращением!");
|
||||||
botAI->ChangeStrategy("+follow,-stay", BOT_STATE_NON_COMBAT);
|
botAI->ChangeStrategy("+follow,-stay", BOT_STATE_NON_COMBAT);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ bool SpiritHealerAction::Execute(Event /*event*/)
|
|||||||
Corpse* corpse = bot->GetCorpse();
|
Corpse* corpse = bot->GetCorpse();
|
||||||
if (!corpse)
|
if (!corpse)
|
||||||
{
|
{
|
||||||
botAI->TellError("I am not a spirit");
|
botAI->TellError("Я не дух");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ bool SpiritHealerAction::Execute(Event /*event*/)
|
|||||||
bot->SpawnCorpseBones();
|
bot->SpawnCorpseBones();
|
||||||
context->GetValue<Unit*>("current target")->Set(nullptr);
|
context->GetValue<Unit*>("current target")->Set(nullptr);
|
||||||
bot->SetTarget();
|
bot->SetTarget();
|
||||||
botAI->TellMaster("Hello");
|
botAI->TellMaster("Здравствуйте.");
|
||||||
|
|
||||||
if (dCount > 20)
|
if (dCount > 20)
|
||||||
context->GetValue<uint32>("death count")->Set(0);
|
context->GetValue<uint32>("death count")->Set(0);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ bool RewardAction::Execute(Event event)
|
|||||||
if (groupLeaderUnit && Reward(itemId, groupLeaderUnit))
|
if (groupLeaderUnit && Reward(itemId, groupLeaderUnit))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
botAI->TellError("Cannot talk to quest giver");
|
botAI->TellError("Не удалось поговорить с персонажем, выдающим задание");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/)
|
|||||||
"You can use this " + chat->FormatItem(item->GetTemplate()) + " better than me, " +
|
"You can use this " + chat->FormatItem(item->GetTemplate()) + " better than me, " +
|
||||||
guidP.GetPlayer()->GetName() /*chat->FormatWorldobject(guidP.GetPlayer())*/ + ".");
|
guidP.GetPlayer()->GetName() /*chat->FormatWorldobject(guidP.GetPlayer())*/ + ".");
|
||||||
else
|
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)*/ + ".",
|
player->GetName() /*chat->FormatWorldobject(player)*/ + ".",
|
||||||
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
|
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
|
||||||
|
|
||||||
@@ -389,7 +389,7 @@ bool RpgTradeUsefulAction::Execute(Event /*event*/)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
bot->Say("Start trade with" + chat->FormatWorldobject(player),
|
bot->Say("Начинаю обмен с" + chat->FormatWorldobject(player),
|
||||||
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
|
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
|
||||||
|
|
||||||
botAI->SetNextCheckDelay(sPlayerbotAIConfig.rpgDelay);
|
botAI->SetNextCheckDelay(sPlayerbotAIConfig.rpgDelay);
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ bool RTSCAction::Execute(Event event)
|
|||||||
if (command != "reset" && !master->HasSpell(RTSC_MOVE_SPELL))
|
if (command != "reset" && !master->HasSpell(RTSC_MOVE_SPELL))
|
||||||
{
|
{
|
||||||
master->learnSpell(RTSC_MOVE_SPELL, false);
|
master->learnSpell(RTSC_MOVE_SPELL, false);
|
||||||
botAI->TellMasterNoFacing("RTS control enabled.");
|
botAI->TellMasterNoFacing("Управление RTS включено.");
|
||||||
botAI->TellMasterNoFacing("Aedm (Awesome energetic do move) spell trained.");
|
botAI->TellMasterNoFacing("Заклинание Aedm для управления перемещением изучено.");
|
||||||
}
|
}
|
||||||
else if (command == "reset")
|
else if (command == "reset")
|
||||||
{
|
{
|
||||||
if (master->HasSpell(RTSC_MOVE_SPELL))
|
if (master->HasSpell(RTSC_MOVE_SPELL))
|
||||||
{
|
{
|
||||||
master->removeSpell(RTSC_MOVE_SPELL, SPEC_MASK_ALL, false);
|
master->removeSpell(RTSC_MOVE_SPELL, SPEC_MASK_ALL, false);
|
||||||
botAI->TellMasterNoFacing("RTS control spell removed.");
|
botAI->TellMasterNoFacing("Заклинание управления RTS удалено.");
|
||||||
}
|
}
|
||||||
|
|
||||||
RESET_AI_VALUE(bool, "RTSC selected");
|
RESET_AI_VALUE(bool, "RTSC selected");
|
||||||
@@ -123,7 +123,7 @@ bool RTSCAction::Execute(Event event)
|
|||||||
if (command.find("show") != std::string::npos)
|
if (command.find("show") != std::string::npos)
|
||||||
{
|
{
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
out << "saved: ";
|
out << "сохранено: ";
|
||||||
|
|
||||||
for (auto value : botAI->GetAiObjectContext()->GetValues())
|
for (auto value : botAI->GetAiObjectContext()->GetValues())
|
||||||
if (value.find("RTSC saved location::") != std::string::npos)
|
if (value.find("RTSC saved location::") != std::string::npos)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user