v1.6.0: полные доработки
ИСПРАВЛЕНИЯ: - Полное сохранение/восстановление инвентаря (гранаты, шлем, дефузер, патроны в клипе и резерве — раньше всё это терялось) - RefillWeaponAmmo расширен на 25+ оружий - Убран двойной reset state в Event_RoundStart НОВЫЙ ФУНКЦИОНАЛ: - Случайное оружие в weapon menu - sm_duel_forceend — админ может принудительно завершить дуэль - Файловый лог addons/sourcemod/logs/arcane_duels.log - Статистика per-игрок (wins/losses/draws/rage-quits/total XP) в addons/sourcemod/data/arcane_duels_stats.cfg - Команды игрока: !duelstats и !duel_top - Rage-quit penalty: XP штраф + +1 поражение в статистике - Звуковые эффекты на окончании дуэли (победа/поражение) - ConVar agd_duel_block_external для интеграции с custom-rounds и другими плагинами КОСМЕТИКА: - Жёлтый префикс [DUELS] (был красный) - Author = deidara.dev
This commit is contained in:
@@ -1,20 +1,25 @@
|
||||
# ArcaneGameDUELS Core
|
||||
|
||||
Система дуэлей 1v1 для CS:GO серверов на SourceMod. Поддерживает арену, зону ограничения, выбор оружия, модификаторы и интеграцию с системой XP (lvl_ranks).
|
||||
Система дуэлей 1v1 для CS:GO серверов на SourceMod. Поддерживает арену, зону ограничения, выбор оружия, модификаторы, статистику и интеграцию с системой XP (lvl_ranks).
|
||||
|
||||
## Функции
|
||||
|
||||
- Дуэли **1v1** между любыми двумя игроками
|
||||
- Автоматический запуск дуэли при обнаружении двух игроков на сервере
|
||||
- Дуэли **1v1** между любыми двумя игроками (автозапуск при 1v1 на сервере с >2 игроками)
|
||||
- **Арена**: фиксированные точки спавна из конфига `ArcaneGameDUELS_Arena.cfg`
|
||||
- **Зона ограничения**: дуэлянты не могут покинуть арену (настраиваемый padding)
|
||||
- Выбор оружия: Deagle, AK-47, M4A4, M4A1-S, AWP, Scout, Knife
|
||||
- Модификаторы: обычный / NoZoom / Headshot Only
|
||||
- Сохранение и восстановление инвентаря и здоровья после дуэли
|
||||
- Выбор оружия: Knife / Deagle / AK-47 / M4A4 / M4A1-S / AWP / Scout + **случайное оружие**
|
||||
- Модификаторы: обычный / Headshot Only
|
||||
- **Полное сохранение и восстановление инвентаря** после дуэли (оружие, патроны в магазине и резерве, гранаты, броня, шлем, дефузер, деньги)
|
||||
- Таймер дуэли с ограничением по времени
|
||||
- Интеграция XP через **lvl_ranks** (награда за победу)
|
||||
- **Forwards** для других плагинов: `OnDuelStarted`, `OnDuelFinished`, `OnDuelDraw`
|
||||
- Интеграция XP через **lvl_ranks** (награда за победу + штраф за rage-quit)
|
||||
- **Forwards** для других плагинов: `AGD_OnDuelStarted`, `AGD_OnDuelFinished`, `AGD_OnDuelDraw`
|
||||
- Beacon-индикатор (звук + частицы) вокруг дуэлянтов
|
||||
- **Звуковые эффекты**: победителю — фанфара, проигравшему — death-камера
|
||||
- **Файловый лог** всех дуэлей в `addons/sourcemod/logs/arcane_duels.log`
|
||||
- **Статистика per-игрок** (wins / losses / draws / rage-quits / total XP) в файл `addons/sourcemod/data/arcane_duels_stats.cfg`
|
||||
- Команды `!duelstats` и `!duel_top` — статистика и топ-10 игроков
|
||||
- Админ-команда `!duel_forceend` для принудительной остановки дуэли
|
||||
- ConVar `agd_duel_block_external` — другие плагины могут временно блокировать авто-дуэли (например, во время кастомного раунда)
|
||||
|
||||
## Зависимости
|
||||
|
||||
@@ -31,52 +36,89 @@
|
||||
|
||||
## Конфиг арены
|
||||
|
||||
Путь: `cfg/sourcemod/ArcaneGameDUELS_Arena.cfg`
|
||||
Путь: `cfg/sourcemod/ArcaneGameDUELS_Arena.cfg` (ключ-значения через KeyValues, на каждую карту своя секция).
|
||||
|
||||
```
|
||||
// Позиция спавна игрока 1
|
||||
sm_duels_arena_spawn1_x "0.0"
|
||||
sm_duels_arena_spawn1_y "0.0"
|
||||
sm_duels_arena_spawn1_z "0.0"
|
||||
sm_duels_arena_spawn1_yaw "0.0"
|
||||
## Команды игрока
|
||||
|
||||
// Позиция спавна игрока 2
|
||||
sm_duels_arena_spawn2_x "200.0"
|
||||
sm_duels_arena_spawn2_y "0.0"
|
||||
sm_duels_arena_spawn2_z "0.0"
|
||||
sm_duels_arena_spawn2_yaw "180.0"
|
||||
```
|
||||
| Команда | Описание |
|
||||
|---|---|
|
||||
| `!duel` / `sm_duel` | Информация о системе дуэлей |
|
||||
| `!duelstats` / `sm_duelstats` | Личная статистика (победы/поражения/винрейт) |
|
||||
| `!duel_top` / `sm_duel_top` | Топ-10 игроков по победам |
|
||||
|
||||
## Команды админа (флаг `z` / ROOT)
|
||||
|
||||
| Команда | Описание |
|
||||
|---|---|
|
||||
| `sm_duel_setspawn1` / `sm_duel_setspawn2` | Сохранить точку спавна #1/#2 |
|
||||
| `sm_duel_setzone1` / `sm_duel_setzone2` | Углы зоны ограничения |
|
||||
| `sm_duel_savearena` | Сохранить арену для текущей карты |
|
||||
| `sm_duel_reloadarena` | Перезагрузить конфиг арены |
|
||||
| `sm_duel_arena_info` | Показать данные арены |
|
||||
| `sm_duel_showzone` | Показать визуально зону |
|
||||
| `sm_duel_debugsolo` | Соло-дебаг (без 2-го игрока) |
|
||||
| `sm_duel_forceend` | Принудительно завершить активную дуэль |
|
||||
|
||||
## ConVars
|
||||
|
||||
| ConVar | По умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `sm_duels_enable` | `1` | Включить/выключить дуэли |
|
||||
| `sm_duels_use_arena` | `1` | Использовать арену |
|
||||
| `sm_duels_beacon` | `1` | Включить beacon у дуэлянтов |
|
||||
| `sm_duels_prepare_time` | `5` | Время подготовки перед дуэлью (сек) |
|
||||
| `sm_duels_win_xp` | `50` | XP за победу |
|
||||
| `sm_duels_time_limit` | `120` | Лимит времени дуэли (сек) |
|
||||
| `sm_duels_zone_enable` | `1` | Включить зону ограничения |
|
||||
| `sm_duels_zone_grace` | `3.0` | Время предупреждения перед кикбеком (сек) |
|
||||
| `sm_duels_allow_deagle` | `1` | Разрешить Deagle |
|
||||
| `sm_duels_allow_ak47` | `1` | Разрешить AK-47 |
|
||||
| `sm_duels_allow_awp` | `1` | Разрешить AWP |
|
||||
| `sm_duels_debug_solo` | `0` | Debug: одиночный режим (без второго игрока) |
|
||||
| `agd_core_enable` | `1` | Включить дуэли |
|
||||
| `agd_use_arena` | `1` | Использовать арену |
|
||||
| `agd_duel_beacon` | `1` | Включить beacon |
|
||||
| `agd_duel_prepare_time` | `10` | Время подготовки (сек) |
|
||||
| `agd_duel_post_unfreeze_invuln` | `2.0` | Неуязвимость после анфриза |
|
||||
| `agd_duel_win_money` | `1500` | Деньги за победу |
|
||||
| `agd_duel_win_xp` | `100` | XP за победу |
|
||||
| `agd_duel_time_limit` | `30` | Лимит дуэли (сек, 0 = нет) |
|
||||
| `agd_weapon_allow_*` | `1` | Разрешить оружие (knife/deagle/ak47/m4a1/m4a1s/awp/scout) |
|
||||
| `agd_duel_bypass_restrict_flags` | `1` | Дать ROOT-флаг во время дуэли (обход других плагинов) |
|
||||
| `agd_zone_*` | разные | Параметры зоны ограничения |
|
||||
| `agd_debug_allow_solo` | `1` | Разрешить sm_duel_debugsolo |
|
||||
| `agd_duel_block_external` | `0` | **Внешняя блокировка** (другой плагин может выставить в `1`) |
|
||||
| `agd_duel_ragequit_xp_penalty` | `50` | Штраф XP за выход во время дуэли |
|
||||
| `agd_duel_stats_enable` | `1` | Сохранять статистику в файл |
|
||||
| `agd_duel_log_enable` | `1` | Логировать дуэли в файл |
|
||||
|
||||
## Forwards (для других плагинов)
|
||||
## Forwards / Natives
|
||||
|
||||
```sourcepawn
|
||||
// Дуэль началась
|
||||
forward void OnDuelStarted(int player1, int player2);
|
||||
forward void AGD_OnDuelStarted(int player1, int player2);
|
||||
forward void AGD_OnDuelFinished(int winner, int loser, int winnerXP, int loserXP);
|
||||
forward void AGD_OnDuelDraw(int player1, int player2);
|
||||
|
||||
// Дуэль завершилась
|
||||
forward void OnDuelFinished(int winner, int loser);
|
||||
|
||||
// Дуэль завершилась ничьей
|
||||
forward void OnDuelDraw(int player1, int player2);
|
||||
native bool AGD_IsDuelActive();
|
||||
native bool AGD_IsBettingOpen();
|
||||
native bool AGD_GetDuelPlayers(int &p1, int &p2);
|
||||
native int AGD_GetDuelParticipantXP(int client);
|
||||
native int AGD_GetWinnerMoneyReward();
|
||||
native bool AGD_IsArenaReady();
|
||||
native bool AGD_IsClientInDuel(int client);
|
||||
```
|
||||
|
||||
## Файлы данных
|
||||
|
||||
- **Статистика:** `addons/sourcemod/data/arcane_duels_stats.cfg` (KeyValues per Steam ID)
|
||||
- **Лог:** `addons/sourcemod/logs/arcane_duels.log` (текстовый, формат `[YYYY-MM-DD HH:MM:SS] EVENT: detail`)
|
||||
|
||||
## Версия
|
||||
|
||||
`1.5.3` — Автор: OpenAI / havno
|
||||
`1.6.0` — Автор: deidara.dev
|
||||
|
||||
### Changelog
|
||||
|
||||
- **1.6.0**
|
||||
- Жёлтый префикс `[DUELS]` (был красный)
|
||||
- **Полное** сохранение/восстановление инвентаря: гранаты, шлем, дефузер, патроны в клипе и резерве
|
||||
- `RefillWeaponAmmo` теперь знает 25+ оружий (все винтовки, SMG, дробовики, пистолеты, снайперки)
|
||||
- Добавлен пункт «Случайное оружие» в weapon menu
|
||||
- Новая админ-команда `sm_duel_forceend` — принудительно остановить любую активную/готовящуюся дуэль
|
||||
- **Файловый лог** дуэлей `addons/sourcemod/logs/arcane_duels.log`
|
||||
- **Статистика** per-игрок: wins/losses/draws/rage-quits + персистенция через `addons/sourcemod/data/arcane_duels_stats.cfg`
|
||||
- Команды `!duelstats` (личная стата) и `!duel_top` (топ-10 по победам)
|
||||
- **Rage-quit penalty**: XP-штраф (по cvar) и +1 поражение/rage-quit в стате
|
||||
- **Звуковые эффекты**: победителю — фанфара, проигравшему — death-камера
|
||||
- ConVar `agd_duel_block_external` — внешние плагины могут временно блокировать авто-дуэли
|
||||
- Убран двойной reset state в Event_RoundStart
|
||||
- Author = `deidara.dev` (был `OpenAI / havno`)
|
||||
- **1.5.3** — предыдущая версия
|
||||
|
||||
@@ -9,17 +9,22 @@
|
||||
|
||||
#define AGD_LIBRARY "arcane_duels_core"
|
||||
#define ARENA_CFG "cfg/sourcemod/ArcaneGameDUELS_Arena.cfg"
|
||||
#define STATS_CFG "data/arcane_duels_stats.cfg"
|
||||
#define DUEL_LOG_FILE "addons/sourcemod/logs/arcane_duels.log"
|
||||
#define BEACON_SOUND "buttons/blip1.wav"
|
||||
#define DUEL_PREFIX "\x02[DUELS]\x01"
|
||||
#define DUEL_OUT_SOUND "buttons/button10.wav"
|
||||
#define DUEL_WIN_SOUND "ui/csgo_ui_contract_type1.wav"
|
||||
#define DUEL_LOSE_SOUND "ui/deathcam.wav"
|
||||
#define DUEL_PREFIX "\x09[DUELS]\x01"
|
||||
#define MAX_SAVED_GRENADES 6
|
||||
|
||||
public Plugin myinfo =
|
||||
{
|
||||
name = "[CORE] ArcaneGameDUELS Core",
|
||||
author = "OpenAI / havno",
|
||||
description = "Core of ArcaneGameDUELS (duel deagle compatibility build)",
|
||||
version = "1.5.3",
|
||||
url = ""
|
||||
author = "deidara.dev",
|
||||
description = "1v1 дуэли с ареной, зоной, выбором оружия, статистикой и логированием",
|
||||
version = "1.6.0",
|
||||
url = "https://deidara.dev"
|
||||
};
|
||||
|
||||
bool g_bDuelActive = false;
|
||||
@@ -44,11 +49,26 @@ enum DuelModifier
|
||||
DuelModifier g_iDuelModifier = DuelModifier_None;
|
||||
|
||||
char g_sSavedWeapon[MAXPLAYERS + 1][3][32];
|
||||
int g_iSavedClip[MAXPLAYERS + 1][3];
|
||||
int g_iSavedReserve[MAXPLAYERS + 1][3];
|
||||
int g_iSavedHealth[MAXPLAYERS + 1];
|
||||
int g_iSavedArmor[MAXPLAYERS + 1];
|
||||
int g_iSavedMoney[MAXPLAYERS + 1];
|
||||
int g_iSavedUserFlags[MAXPLAYERS + 1];
|
||||
bool g_bSavedUserFlags[MAXPLAYERS + 1];
|
||||
bool g_bSavedHelmet[MAXPLAYERS + 1];
|
||||
bool g_bSavedDefuser[MAXPLAYERS + 1];
|
||||
char g_sSavedGrenades[MAXPLAYERS + 1][MAX_SAVED_GRENADES][32];
|
||||
int g_iSavedGrenadeCount[MAXPLAYERS + 1];
|
||||
|
||||
// Статистика дуэлей
|
||||
StringMap g_smStats = null;
|
||||
|
||||
// ConVars (новые)
|
||||
ConVar gCvarBlockExternal;
|
||||
ConVar gCvarRageQuitPenalty;
|
||||
ConVar gCvarStatsEnable;
|
||||
ConVar gCvarLogEnable;
|
||||
|
||||
Handle g_hFwdOnDuelStarted = null;
|
||||
Handle g_hFwdOnDuelFinished = null;
|
||||
@@ -147,6 +167,10 @@ public void OnPluginStart()
|
||||
gCvarZoneRenderInterval = CreateConVar("agd_zone_render_interval", "0.5", "Interval for duel zone rendering and checks", _, true, 0.1, true, 2.0);
|
||||
gCvarDebugAllowSolo = CreateConVar("agd_debug_allow_solo", "1", "Allow root admins to start solo duel debug session", _, true, 0.0, true, 1.0);
|
||||
gCvarDuelTimeLimit = CreateConVar("agd_duel_time_limit", "30", "Time limit for duel in seconds (0 = no limit)", _, true, 0.0, true, 300.0);
|
||||
gCvarBlockExternal = CreateConVar("agd_duel_block_external", "0", "External plugins (custom-rounds и т.п.) могут установить в 1 чтобы временно блокировать авто-дуэли", _, true, 0.0, true, 1.0);
|
||||
gCvarRageQuitPenalty = CreateConVar("agd_duel_ragequit_xp_penalty", "50", "XP penalty for disconnecting during active duel (0 = none)", _, true, 0.0, true, 1000000.0);
|
||||
gCvarStatsEnable = CreateConVar("agd_duel_stats_enable", "1", "Сохранять статистику дуэлей (wins/losses) в файл", _, true, 0.0, true, 1.0);
|
||||
gCvarLogEnable = CreateConVar("agd_duel_log_enable", "1", "Логировать дуэли в addons/sourcemod/logs/arcane_duels.log", _, true, 0.0, true, 1.0);
|
||||
|
||||
AutoExecConfig(true, "ArcaneGameDUELS_Core");
|
||||
|
||||
@@ -155,6 +179,8 @@ public void OnPluginStart()
|
||||
g_hFwdOnDuelDraw = CreateGlobalForward("AGD_OnDuelDraw", ET_Ignore, Param_Cell, Param_Cell);
|
||||
|
||||
RegConsoleCmd("sm_duel", Command_DuelMenu);
|
||||
RegConsoleCmd("sm_duelstats", Command_DuelStats, "Показать свою статистику дуэлей");
|
||||
RegConsoleCmd("sm_duel_top", Command_DuelTop, "Топ-10 игроков по победам");
|
||||
|
||||
RegAdminCmd("sm_duel_setspawn1", Command_SetSpawn1, ADMFLAG_ROOT);
|
||||
RegAdminCmd("sm_duel_setspawn2", Command_SetSpawn2, ADMFLAG_ROOT);
|
||||
@@ -165,6 +191,7 @@ public void OnPluginStart()
|
||||
RegAdminCmd("sm_duel_arena_info", Command_ArenaInfo, ADMFLAG_ROOT);
|
||||
RegAdminCmd("sm_duel_showzone", Command_ShowZone, ADMFLAG_ROOT);
|
||||
RegAdminCmd("sm_duel_debugsolo", Command_DebugSolo, ADMFLAG_ROOT);
|
||||
RegAdminCmd("sm_duel_forceend", Command_ForceEnd, ADMFLAG_ROOT, "Принудительно завершить активную/готовящуюся дуэль");
|
||||
|
||||
HookEvent("player_death", Event_PlayerDeath, EventHookMode_Post);
|
||||
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
|
||||
@@ -175,6 +202,9 @@ public void OnPluginStart()
|
||||
|
||||
LoadArenaForCurrentMap();
|
||||
|
||||
g_smStats = new StringMap();
|
||||
LoadDuelStats();
|
||||
|
||||
for (int i = 1; i <= MaxClients; i++)
|
||||
{
|
||||
if (IsClientInGame(i))
|
||||
@@ -182,6 +212,11 @@ public void OnPluginStart()
|
||||
}
|
||||
}
|
||||
|
||||
public void OnMapEnd()
|
||||
{
|
||||
SaveDuelStats();
|
||||
}
|
||||
|
||||
public void OnMapStart()
|
||||
{
|
||||
LoadArenaForCurrentMap();
|
||||
@@ -225,6 +260,9 @@ public void OnClientDisconnect(int client)
|
||||
int winner = (client == g_iDuelP1) ? g_iDuelP2 : g_iDuelP1;
|
||||
int loser = client;
|
||||
|
||||
// Rage-quit penalty: записываем поражение и снимаем XP, если cvar > 0
|
||||
ApplyRageQuitPenalty(loser);
|
||||
|
||||
if (IsValidHuman(winner))
|
||||
FinishDuel(winner, loser);
|
||||
else
|
||||
@@ -425,6 +463,8 @@ public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
|
||||
g_bBombPlanted = false;
|
||||
CancelPendingDuel("");
|
||||
CancelPreparingDuel("");
|
||||
// ResetDuelState уже вызывается внутри CancelPreparingDuel; повторный вызов оставляем
|
||||
// только если дуэль реально активна (не в подготовке)
|
||||
if (g_bDuelActive)
|
||||
ResetDuelState();
|
||||
|
||||
@@ -529,6 +569,10 @@ public Action Timer_CheckForOneVsOne(Handle timer, any data)
|
||||
if (!gCvarEnable.BoolValue || g_bBombPlanted || g_bDuelActive || g_bDuelPending || g_bDuelPreparing)
|
||||
return Plugin_Stop;
|
||||
|
||||
// Внешняя блокировка (например custom-rounds может выставить cvar в 1)
|
||||
if (gCvarBlockExternal != null && gCvarBlockExternal.BoolValue)
|
||||
return Plugin_Stop;
|
||||
|
||||
if (g_iRoundStartPlayingHumans <= 2)
|
||||
return Plugin_Stop;
|
||||
|
||||
@@ -559,7 +603,7 @@ void OfferAutomaticDuel(int client1, int client2)
|
||||
g_bAccepted[client1] = false;
|
||||
g_bAccepted[client2] = false;
|
||||
|
||||
PrintToChatAll("\x02[DUELS]\x01 Остались 1 на 1: \x03%N\x01 vs \x03%N\x01. Ожидается подтверждение дуэли.", client1, client2);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Остались 1 на 1: \x03%N\x01 vs \x03%N\x01. Ожидается подтверждение дуэли.", client1, client2);
|
||||
ShowAcceptMenu(client1);
|
||||
ShowAcceptMenu(client2);
|
||||
}
|
||||
@@ -599,7 +643,7 @@ public int MenuHandler_Accept(Menu menu, MenuAction action, int client, int item
|
||||
if (StringToInt(info) == 1)
|
||||
{
|
||||
g_bAccepted[client] = true;
|
||||
PrintToChat(client, "\x02[DUELS]\x01 Ты принял дуэль.");
|
||||
PrintToChat(client, "\x09[DUELS]\x01 Ты принял дуэль.");
|
||||
|
||||
if (g_bAccepted[g_iDuelP1] && g_bAccepted[g_iDuelP2])
|
||||
BeginPreparation(g_iDuelP1, g_iDuelP2);
|
||||
@@ -642,8 +686,8 @@ void BeginPreparation(int client1, int client2)
|
||||
|
||||
StartPrepZoneTimer();
|
||||
|
||||
PrintToChatAll("\x02[DUELS]\x01 Дуэль принята. Настройки выбирает случайный игрок: \x03%N", g_iDuelController);
|
||||
PrintToChatAll("\x02[DUELS]\x01 Подготовка к дуэли: \x05%d\x01 сек. В это время доступны ставки.", g_iPrepareLeft);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Дуэль принята. Настройки выбирает случайный игрок: \x03%N", g_iDuelController);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Подготовка к дуэли: \x05%d\x01 сек. В это время доступны ставки.", g_iPrepareLeft);
|
||||
|
||||
ShowWeaponMenu(g_iDuelController);
|
||||
|
||||
@@ -698,6 +742,9 @@ void ShowWeaponMenu(int client)
|
||||
if (gCvarAllowAWP.BoolValue) { menu.AddItem("weapon_awp", "AWP"); added++; }
|
||||
if (gCvarAllowScout.BoolValue) { menu.AddItem("weapon_ssg08", "SSG08 / Scout"); added++; }
|
||||
|
||||
if (added > 1)
|
||||
menu.AddItem("__random__", "Случайное оружие");
|
||||
|
||||
if (added <= 0)
|
||||
menu.AddItem("blocked", "Нет доступного оружия", ITEMDRAW_DISABLED);
|
||||
|
||||
@@ -705,6 +752,26 @@ void ShowWeaponMenu(int client)
|
||||
menu.Display(client, 10);
|
||||
}
|
||||
|
||||
void PickRandomAllowedWeapon(char[] buffer, int maxlen)
|
||||
{
|
||||
char candidates[8][32];
|
||||
int count = 0;
|
||||
if (gCvarAllowKnife.BoolValue) { strcopy(candidates[count++], 32, "weapon_knife"); }
|
||||
if (gCvarAllowDeagle.BoolValue) { strcopy(candidates[count++], 32, "weapon_deagle"); }
|
||||
if (gCvarAllowAK47.BoolValue) { strcopy(candidates[count++], 32, "weapon_ak47"); }
|
||||
if (gCvarAllowM4A4.BoolValue) { strcopy(candidates[count++], 32, "weapon_m4a1"); }
|
||||
if (gCvarAllowAWP.BoolValue) { strcopy(candidates[count++], 32, "weapon_awp"); }
|
||||
if (gCvarAllowScout.BoolValue) { strcopy(candidates[count++], 32, "weapon_ssg08"); }
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
strcopy(buffer, maxlen, "weapon_ak47");
|
||||
return;
|
||||
}
|
||||
|
||||
strcopy(buffer, maxlen, candidates[GetRandomInt(0, count - 1)]);
|
||||
}
|
||||
|
||||
public int MenuHandler_WeaponMenu(Menu menu, MenuAction action, int client, int item)
|
||||
{
|
||||
switch (action)
|
||||
@@ -718,7 +785,15 @@ public int MenuHandler_WeaponMenu(Menu menu, MenuAction action, int client, int
|
||||
|
||||
char info[32];
|
||||
menu.GetItem(item, info, sizeof(info));
|
||||
strcopy(g_sSelectedWeapon, sizeof(g_sSelectedWeapon), info);
|
||||
|
||||
if (StrEqual(info, "__random__"))
|
||||
{
|
||||
PickRandomAllowedWeapon(g_sSelectedWeapon, sizeof(g_sSelectedWeapon));
|
||||
}
|
||||
else
|
||||
{
|
||||
strcopy(g_sSelectedWeapon, sizeof(g_sSelectedWeapon), info);
|
||||
}
|
||||
g_iDuelModifier = DuelModifier_None;
|
||||
|
||||
if (!StrEqual(g_sSelectedWeapon, "weapon_knife"))
|
||||
@@ -767,9 +842,9 @@ void AnnounceSelectedSettings()
|
||||
char weaponName[32];
|
||||
GetDuelWeaponName(weaponName, sizeof(weaponName));
|
||||
if (g_iDuelModifier == DuelModifier_HeadshotOnly)
|
||||
PrintToChatAll("\x02[DUELS]\x01 Выбрана дуэль: \x03%s\x01 | режим: \x05Только в голову", weaponName);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Выбрана дуэль: \x03%s\x01 | режим: \x05Только в голову", weaponName);
|
||||
else
|
||||
PrintToChatAll("\x02[DUELS]\x01 Выбрана дуэль: \x03%s\x01 | режим: \x05Обычный", weaponName);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Выбрана дуэль: \x03%s\x01 | режим: \x05Обычный", weaponName);
|
||||
}
|
||||
|
||||
public Action Timer_PrepareCountdown(Handle timer, any data)
|
||||
@@ -848,8 +923,8 @@ void StartDuel(int client1, int client2)
|
||||
|
||||
char weaponName[32];
|
||||
GetDuelWeaponName(weaponName, sizeof(weaponName));
|
||||
PrintToChatAll("\x02[DUELS]\x01 Дуэль началась: \x03%N\x01 vs \x03%N\x01 | \x05%s", client1, client2, weaponName);
|
||||
PrintToChatAll("\x02[DUELS]\x01 Заморозка снята. Неуязвимость будет убрана через \x05%.0f\x01 сек. | Лимит дуэли: \x05%d\x01 сек.", gCvarPostUnfreezeInvuln.FloatValue, gCvarDuelTimeLimit.IntValue);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Дуэль началась: \x03%N\x01 vs \x03%N\x01 | \x05%s", client1, client2, weaponName);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Заморозка снята. Неуязвимость будет убрана через \x05%.0f\x01 сек. | Лимит дуэли: \x05%d\x01 сек.", gCvarPostUnfreezeInvuln.FloatValue, gCvarDuelTimeLimit.IntValue);
|
||||
|
||||
Call_StartForward(g_hFwdOnDuelStarted);
|
||||
Call_PushCell(client1);
|
||||
@@ -924,7 +999,18 @@ void FinishDuel(int winner, int loser, bool endRoundByTeamDefeat = false)
|
||||
}
|
||||
}
|
||||
|
||||
PrintToChatAll("\x02[DUELS]\x01 Дуэль завершена. Победитель: \x03%N\x01 | Награда: \x05%d$\x01 | Базовый XP: \x05%d", winner, moneyReward, duelWinXP);
|
||||
PrintToChatAll("\x09[DUELS]\x01 Дуэль завершена. Победитель: \x03%N\x01 | Награда: \x05%d$\x01 | Базовый XP: \x05%d", winner, moneyReward, duelWinXP);
|
||||
|
||||
// Звуки победителю/проигравшему
|
||||
if (IsValidHuman(winner))
|
||||
EmitSoundToClient(winner, DUEL_WIN_SOUND);
|
||||
if (IsValidHuman(loser))
|
||||
EmitSoundToClient(loser, DUEL_LOSE_SOUND);
|
||||
|
||||
// Лог + статистика
|
||||
LogDuelAction("WIN: %N (winner) vs %N (loser) | weapon=%s | XP=%d | money=%d",
|
||||
winner, loser, g_sSelectedWeapon, duelWinXP, moneyReward);
|
||||
RecordDuelResult(winner, loser, duelWinXP);
|
||||
|
||||
Call_StartForward(g_hFwdOnDuelFinished);
|
||||
Call_PushCell(winner);
|
||||
@@ -956,6 +1042,11 @@ void FinishDuelDraw(int client1, int client2)
|
||||
|
||||
PrintToChatAll("%s Дуэль завершилась вничью. Ставки должны быть возвращены модулем ставок.", DUEL_PREFIX);
|
||||
|
||||
// Лог + статистика
|
||||
LogDuelAction("DRAW: %N vs %N | weapon=%s",
|
||||
IsValidClient(client1) ? client1 : 0, IsValidClient(client2) ? client2 : 0, g_sSelectedWeapon);
|
||||
RecordDuelDraw(client1, client2);
|
||||
|
||||
Call_StartForward(g_hFwdOnDuelDraw);
|
||||
Call_PushCell(client1);
|
||||
Call_PushCell(client2);
|
||||
@@ -995,7 +1086,7 @@ void CancelPendingDuel(const char[] reason)
|
||||
return;
|
||||
|
||||
if (reason[0] != '\0')
|
||||
PrintToChatAll("\x02[DUELS]\x01 %s", reason);
|
||||
PrintToChatAll("\x09[DUELS]\x01 %s", reason);
|
||||
|
||||
g_bDuelPending = false;
|
||||
if (g_iDuelP1 > 0)
|
||||
@@ -1021,7 +1112,7 @@ void CancelPreparingDuel(const char[] reason)
|
||||
SetDuelProtection(g_iDuelP2, false);
|
||||
|
||||
if (reason[0] != '\0')
|
||||
PrintToChatAll("\x02[DUELS]\x01 %s", reason);
|
||||
PrintToChatAll("\x09[DUELS]\x01 %s", reason);
|
||||
|
||||
RestoreLoadout(g_iDuelP1);
|
||||
RestoreLoadout(g_iDuelP2);
|
||||
@@ -1129,7 +1220,7 @@ public Action Timer_DuelTimeLimit(Handle timer, any data)
|
||||
if (!g_bDuelActive)
|
||||
return Plugin_Stop;
|
||||
|
||||
PrintToChatAll("\x02[DUELS]\x01 Время дуэли истекло. Ничья!");
|
||||
PrintToChatAll("\x09[DUELS]\x01 Время дуэли истекло. Ничья!");
|
||||
FinishDuelDraw(g_iDuelP1, g_iDuelP2);
|
||||
return Plugin_Stop;
|
||||
}
|
||||
@@ -1540,22 +1631,61 @@ void SaveLoadout(int client)
|
||||
if (!IsValidHuman(client))
|
||||
return;
|
||||
|
||||
SaveWeaponClass(client, 0, 0);
|
||||
SaveWeaponClass(client, 1, 1);
|
||||
SaveWeaponClass(client, 2, 2);
|
||||
SaveWeaponClassAndAmmo(client, 0, 0);
|
||||
SaveWeaponClassAndAmmo(client, 1, 1);
|
||||
SaveWeaponClassAndAmmo(client, 2, 2);
|
||||
SaveAllGrenadesForDuel(client);
|
||||
|
||||
g_iSavedHealth[client] = GetClientHealth(client);
|
||||
g_iSavedArmor[client] = GetEntProp(client, Prop_Send, "m_ArmorValue");
|
||||
g_iSavedMoney[client] = GetEntProp(client, Prop_Send, "m_iAccount");
|
||||
g_bSavedHelmet[client] = GetEntProp(client, Prop_Send, "m_bHasHelmet") != 0;
|
||||
g_bSavedDefuser[client] = HasEntProp(client, Prop_Send, "m_bHasDefuser") && GetEntProp(client, Prop_Send, "m_bHasDefuser") != 0;
|
||||
}
|
||||
|
||||
void SaveWeaponClass(int client, int slot, int saveIndex)
|
||||
void SaveWeaponClassAndAmmo(int client, int slot, int saveIndex)
|
||||
{
|
||||
g_sSavedWeapon[client][saveIndex][0] = '\0';
|
||||
g_iSavedClip[client][saveIndex] = 0;
|
||||
g_iSavedReserve[client][saveIndex] = 0;
|
||||
|
||||
int weapon = GetPlayerWeaponSlot(client, slot);
|
||||
if (weapon > MaxClients && IsValidEdict(weapon))
|
||||
GetEdictClassname(weapon, g_sSavedWeapon[client][saveIndex], 32);
|
||||
else
|
||||
g_sSavedWeapon[client][saveIndex][0] = '\0';
|
||||
if (weapon <= MaxClients || !IsValidEdict(weapon))
|
||||
return;
|
||||
|
||||
GetEdictClassname(weapon, g_sSavedWeapon[client][saveIndex], 32);
|
||||
|
||||
if (HasEntProp(weapon, Prop_Send, "m_iClip1"))
|
||||
g_iSavedClip[client][saveIndex] = GetEntProp(weapon, Prop_Send, "m_iClip1");
|
||||
if (HasEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount"))
|
||||
g_iSavedReserve[client][saveIndex] = GetEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount");
|
||||
}
|
||||
|
||||
void SaveAllGrenadesForDuel(int client)
|
||||
{
|
||||
g_iSavedGrenadeCount[client] = 0;
|
||||
|
||||
int offset = FindSendPropInfo("CCSPlayer", "m_hMyWeapons");
|
||||
if (offset <= 0)
|
||||
return;
|
||||
|
||||
char classname[32];
|
||||
for (int i = 0; i < 64; i++)
|
||||
{
|
||||
int weapon = GetEntDataEnt2(client, offset + (i * 4));
|
||||
if (weapon == -1 || !IsValidEntity(weapon))
|
||||
continue;
|
||||
|
||||
GetEntityClassname(weapon, classname, sizeof(classname));
|
||||
if (!IsGrenadeWeapon(classname))
|
||||
continue;
|
||||
|
||||
if (g_iSavedGrenadeCount[client] < MAX_SAVED_GRENADES)
|
||||
{
|
||||
strcopy(g_sSavedGrenades[client][g_iSavedGrenadeCount[client]], 32, classname);
|
||||
g_iSavedGrenadeCount[client]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RestoreLoadout(int client)
|
||||
@@ -1572,13 +1702,39 @@ void RestoreLoadout(int client)
|
||||
continue;
|
||||
|
||||
int weapon = GivePlayerItem(client, g_sSavedWeapon[client][i]);
|
||||
if (weapon > MaxClients)
|
||||
RefillWeaponAmmo(weapon);
|
||||
if (weapon > MaxClients && IsValidEntity(weapon))
|
||||
{
|
||||
// Восстанавливаем сохранённый клип/резерв; если их не было — refill дефолтом
|
||||
bool restored = false;
|
||||
if (g_iSavedClip[client][i] > 0 && HasEntProp(weapon, Prop_Send, "m_iClip1"))
|
||||
{
|
||||
SetEntProp(weapon, Prop_Send, "m_iClip1", g_iSavedClip[client][i]);
|
||||
restored = true;
|
||||
}
|
||||
if (g_iSavedReserve[client][i] > 0 && HasEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount"))
|
||||
{
|
||||
SetEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount", g_iSavedReserve[client][i]);
|
||||
restored = true;
|
||||
}
|
||||
if (!restored)
|
||||
RefillWeaponAmmo(weapon);
|
||||
}
|
||||
}
|
||||
|
||||
// Гранаты
|
||||
for (int i = 0; i < g_iSavedGrenadeCount[client]; i++)
|
||||
{
|
||||
if (g_sSavedGrenades[client][i][0] != '\0')
|
||||
GivePlayerItem(client, g_sSavedGrenades[client][i]);
|
||||
}
|
||||
g_iSavedGrenadeCount[client] = 0;
|
||||
|
||||
SetEntityHealth(client, g_iSavedHealth[client] > 0 ? g_iSavedHealth[client] : 100);
|
||||
SetEntProp(client, Prop_Send, "m_ArmorValue", g_iSavedArmor[client]);
|
||||
SetEntProp(client, Prop_Send, "m_iAccount", g_iSavedMoney[client]);
|
||||
SetEntProp(client, Prop_Send, "m_bHasHelmet", g_bSavedHelmet[client] ? 1 : 0);
|
||||
if (GetClientTeam(client) == CS_TEAM_CT && HasEntProp(client, Prop_Send, "m_bHasDefuser"))
|
||||
SetEntProp(client, Prop_Send, "m_bHasDefuser", g_bSavedDefuser[client] ? 1 : 0);
|
||||
}
|
||||
|
||||
void RemoveAllWeapons(int client)
|
||||
@@ -1619,31 +1775,44 @@ void RefillWeaponAmmo(int weapon)
|
||||
int clip = 30;
|
||||
int reserve = 90;
|
||||
|
||||
if (StrEqual(classname, "weapon_deagle"))
|
||||
{
|
||||
clip = 7;
|
||||
reserve = 35;
|
||||
}
|
||||
else if (StrEqual(classname, "weapon_awp"))
|
||||
{
|
||||
clip = 10;
|
||||
reserve = 30;
|
||||
}
|
||||
else if (StrEqual(classname, "weapon_ssg08"))
|
||||
{
|
||||
clip = 10;
|
||||
reserve = 90;
|
||||
}
|
||||
else if (StrEqual(classname, "weapon_m4a1_silencer"))
|
||||
{
|
||||
clip = 20;
|
||||
reserve = 60;
|
||||
}
|
||||
else if (StrEqual(classname, "weapon_m4a1") || StrEqual(classname, "weapon_ak47"))
|
||||
{
|
||||
clip = 30;
|
||||
reserve = 90;
|
||||
}
|
||||
// Пистолеты
|
||||
if (StrEqual(classname, "weapon_deagle")) { clip = 7; reserve = 35; }
|
||||
else if (StrEqual(classname, "weapon_revolver")) { clip = 8; reserve = 8; }
|
||||
else if (StrEqual(classname, "weapon_glock")) { clip = 20; reserve = 120; }
|
||||
else if (StrEqual(classname, "weapon_hkp2000")) { clip = 13; reserve = 52; }
|
||||
else if (StrEqual(classname, "weapon_usp_silencer")) { clip = 12; reserve = 24; }
|
||||
else if (StrEqual(classname, "weapon_p250")) { clip = 13; reserve = 26; }
|
||||
else if (StrEqual(classname, "weapon_fiveseven")) { clip = 20; reserve = 100; }
|
||||
else if (StrEqual(classname, "weapon_tec9")) { clip = 18; reserve = 90; }
|
||||
else if (StrEqual(classname, "weapon_cz75a")) { clip = 12; reserve = 12; }
|
||||
else if (StrEqual(classname, "weapon_elite")) { clip = 30; reserve = 120; }
|
||||
// Снайперки
|
||||
else if (StrEqual(classname, "weapon_awp")) { clip = 10; reserve = 30; }
|
||||
else if (StrEqual(classname, "weapon_ssg08")) { clip = 10; reserve = 90; }
|
||||
else if (StrEqual(classname, "weapon_scar20")) { clip = 20; reserve = 90; }
|
||||
else if (StrEqual(classname, "weapon_g3sg1")) { clip = 20; reserve = 90; }
|
||||
// Винтовки
|
||||
else if (StrEqual(classname, "weapon_m4a1_silencer")) { clip = 20; reserve = 80; }
|
||||
else if (StrEqual(classname, "weapon_m4a1") || StrEqual(classname, "weapon_ak47")) { clip = 30; reserve = 90; }
|
||||
else if (StrEqual(classname, "weapon_aug")) { clip = 30; reserve = 90; }
|
||||
else if (StrEqual(classname, "weapon_sg556")) { clip = 30; reserve = 90; }
|
||||
else if (StrEqual(classname, "weapon_galilar")) { clip = 35; reserve = 90; }
|
||||
else if (StrEqual(classname, "weapon_famas")) { clip = 25; reserve = 90; }
|
||||
// SMG
|
||||
else if (StrEqual(classname, "weapon_mp9")) { clip = 30; reserve = 120; }
|
||||
else if (StrEqual(classname, "weapon_mp7")) { clip = 30; reserve = 120; }
|
||||
else if (StrEqual(classname, "weapon_mp5sd")) { clip = 30; reserve = 120; }
|
||||
else if (StrEqual(classname, "weapon_mac10")) { clip = 30; reserve = 100; }
|
||||
else if (StrEqual(classname, "weapon_ump45")) { clip = 25; reserve = 100; }
|
||||
else if (StrEqual(classname, "weapon_p90")) { clip = 50; reserve = 100; }
|
||||
else if (StrEqual(classname, "weapon_bizon")) { clip = 64; reserve = 120; }
|
||||
// Дробовики / тяжёлое
|
||||
else if (StrEqual(classname, "weapon_nova")) { clip = 8; reserve = 32; }
|
||||
else if (StrEqual(classname, "weapon_xm1014")) { clip = 7; reserve = 32; }
|
||||
else if (StrEqual(classname, "weapon_mag7")) { clip = 5; reserve = 32; }
|
||||
else if (StrEqual(classname, "weapon_sawedoff")) { clip = 7; reserve = 32; }
|
||||
else if (StrEqual(classname, "weapon_m249")) { clip = 100; reserve = 200; }
|
||||
else if (StrEqual(classname, "weapon_negev")) { clip = 150; reserve = 200; }
|
||||
|
||||
if (HasEntProp(weapon, Prop_Send, "m_iClip1"))
|
||||
SetEntProp(weapon, Prop_Send, "m_iClip1", clip);
|
||||
@@ -2199,3 +2368,371 @@ public any Native_AGD_IsBettingOpen(Handle plugin, int numParams)
|
||||
{
|
||||
return g_bDuelPreparing;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Force-end (admin)
|
||||
// ============================================================================
|
||||
|
||||
public Action Command_ForceEnd(int client, int args)
|
||||
{
|
||||
if (!g_bDuelActive && !g_bDuelPending && !g_bDuelPreparing)
|
||||
{
|
||||
ReplyToCommand(client, "%s Сейчас нет активной/готовящейся дуэли.", DUEL_PREFIX);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
PrintToChatAll("%s Админ \x03%N\x01 принудительно завершил дуэль.", DUEL_PREFIX, client);
|
||||
LogDuelAction("FORCE_END by %N (admin)", client);
|
||||
|
||||
if (g_bDuelPending)
|
||||
{
|
||||
CancelPendingDuel("");
|
||||
}
|
||||
else if (g_bDuelPreparing)
|
||||
{
|
||||
CancelPreparingDuel("");
|
||||
}
|
||||
else if (g_bDuelActive)
|
||||
{
|
||||
StopBeaconTimer();
|
||||
StopInvulnTimer();
|
||||
StopZoneTimer();
|
||||
StopZonePreview();
|
||||
StopDuelLimitTimer();
|
||||
SetDuelProtection(g_iDuelP1, false);
|
||||
SetDuelProtection(g_iDuelP2, false);
|
||||
RestoreLoadout(g_iDuelP1);
|
||||
RestoreLoadout(g_iDuelP2);
|
||||
ResetDuelState();
|
||||
}
|
||||
|
||||
ReplyToCommand(client, "%s Дуэль остановлена.", DUEL_PREFIX);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Файловое логирование
|
||||
// ============================================================================
|
||||
|
||||
void LogDuelAction(const char[] format, any ...)
|
||||
{
|
||||
if (gCvarLogEnable == null || !gCvarLogEnable.BoolValue)
|
||||
return;
|
||||
|
||||
char message[512];
|
||||
VFormat(message, sizeof(message), format, 2);
|
||||
|
||||
char timestamp[32];
|
||||
FormatTime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S");
|
||||
|
||||
File logFile = OpenFile(DUEL_LOG_FILE, "a");
|
||||
if (logFile != null)
|
||||
{
|
||||
logFile.WriteLine("[%s] %s", timestamp, message);
|
||||
delete logFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError("[DUELS] Не удалось открыть %s на запись.", DUEL_LOG_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Статистика дуэлей
|
||||
// ============================================================================
|
||||
|
||||
enum struct DuelStats
|
||||
{
|
||||
int wins;
|
||||
int losses;
|
||||
int draws;
|
||||
int rageQuits;
|
||||
int totalXP;
|
||||
char name[MAX_NAME_LENGTH];
|
||||
}
|
||||
|
||||
void LoadDuelStats()
|
||||
{
|
||||
if (g_smStats == null)
|
||||
g_smStats = new StringMap();
|
||||
|
||||
g_smStats.Clear();
|
||||
|
||||
char path[PLATFORM_MAX_PATH];
|
||||
BuildPath(Path_SM, path, sizeof(path), "../../" ... STATS_CFG);
|
||||
|
||||
KeyValues kv = new KeyValues("ArcaneDuelsStats");
|
||||
if (!kv.ImportFromFile(path))
|
||||
{
|
||||
delete kv;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kv.GotoFirstSubKey())
|
||||
{
|
||||
delete kv;
|
||||
return;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
char steam[32];
|
||||
kv.GetSectionName(steam, sizeof(steam));
|
||||
|
||||
DuelStats stats;
|
||||
kv.GetString("name", stats.name, sizeof(DuelStats::name), "");
|
||||
stats.wins = kv.GetNum("wins", 0);
|
||||
stats.losses = kv.GetNum("losses", 0);
|
||||
stats.draws = kv.GetNum("draws", 0);
|
||||
stats.rageQuits = kv.GetNum("rage_quits", 0);
|
||||
stats.totalXP = kv.GetNum("total_xp", 0);
|
||||
|
||||
g_smStats.SetArray(steam, stats, sizeof(stats));
|
||||
}
|
||||
while (kv.GotoNextKey());
|
||||
|
||||
delete kv;
|
||||
}
|
||||
|
||||
void SaveDuelStats()
|
||||
{
|
||||
if (g_smStats == null)
|
||||
return;
|
||||
|
||||
char path[PLATFORM_MAX_PATH];
|
||||
BuildPath(Path_SM, path, sizeof(path), "../../" ... STATS_CFG);
|
||||
|
||||
KeyValues kv = new KeyValues("ArcaneDuelsStats");
|
||||
|
||||
StringMapSnapshot snap = g_smStats.Snapshot();
|
||||
for (int i = 0; i < snap.Length; i++)
|
||||
{
|
||||
char steam[32];
|
||||
snap.GetKey(i, steam, sizeof(steam));
|
||||
|
||||
DuelStats stats;
|
||||
if (!g_smStats.GetArray(steam, stats, sizeof(stats)))
|
||||
continue;
|
||||
|
||||
kv.JumpToKey(steam, true);
|
||||
kv.SetString("name", stats.name);
|
||||
kv.SetNum("wins", stats.wins);
|
||||
kv.SetNum("losses", stats.losses);
|
||||
kv.SetNum("draws", stats.draws);
|
||||
kv.SetNum("rage_quits", stats.rageQuits);
|
||||
kv.SetNum("total_xp", stats.totalXP);
|
||||
kv.GoBack();
|
||||
}
|
||||
delete snap;
|
||||
|
||||
kv.Rewind();
|
||||
kv.ExportToFile(path);
|
||||
delete kv;
|
||||
}
|
||||
|
||||
bool GetClientStats(int client, DuelStats stats)
|
||||
{
|
||||
if (!IsValidHuman(client))
|
||||
return false;
|
||||
|
||||
char steam[32];
|
||||
if (!GetClientAuthId(client, AuthId_Steam2, steam, sizeof(steam)))
|
||||
return false;
|
||||
|
||||
if (g_smStats == null)
|
||||
return false;
|
||||
|
||||
if (!g_smStats.GetArray(steam, stats, sizeof(stats)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateClientStats(int client, DuelStats stats)
|
||||
{
|
||||
if (!IsValidHuman(client) || g_smStats == null)
|
||||
return;
|
||||
|
||||
char steam[32];
|
||||
if (!GetClientAuthId(client, AuthId_Steam2, steam, sizeof(steam)))
|
||||
return;
|
||||
|
||||
GetClientName(client, stats.name, sizeof(DuelStats::name));
|
||||
g_smStats.SetArray(steam, stats, sizeof(stats));
|
||||
}
|
||||
|
||||
void RecordDuelResult(int winner, int loser, int xp)
|
||||
{
|
||||
if (gCvarStatsEnable == null || !gCvarStatsEnable.BoolValue)
|
||||
return;
|
||||
|
||||
if (IsValidHuman(winner))
|
||||
{
|
||||
DuelStats s;
|
||||
GetClientStats(winner, s);
|
||||
s.wins++;
|
||||
s.totalXP += xp;
|
||||
UpdateClientStats(winner, s);
|
||||
}
|
||||
|
||||
if (IsValidHuman(loser))
|
||||
{
|
||||
DuelStats s;
|
||||
GetClientStats(loser, s);
|
||||
s.losses++;
|
||||
UpdateClientStats(loser, s);
|
||||
}
|
||||
|
||||
SaveDuelStats();
|
||||
}
|
||||
|
||||
void RecordDuelDraw(int p1, int p2)
|
||||
{
|
||||
if (gCvarStatsEnable == null || !gCvarStatsEnable.BoolValue)
|
||||
return;
|
||||
|
||||
if (IsValidHuman(p1))
|
||||
{
|
||||
DuelStats s;
|
||||
GetClientStats(p1, s);
|
||||
s.draws++;
|
||||
UpdateClientStats(p1, s);
|
||||
}
|
||||
if (IsValidHuman(p2))
|
||||
{
|
||||
DuelStats s;
|
||||
GetClientStats(p2, s);
|
||||
s.draws++;
|
||||
UpdateClientStats(p2, s);
|
||||
}
|
||||
SaveDuelStats();
|
||||
}
|
||||
|
||||
void ApplyRageQuitPenalty(int client)
|
||||
{
|
||||
if (!IsValidHuman(client))
|
||||
return;
|
||||
|
||||
int penalty = (gCvarRageQuitPenalty != null) ? gCvarRageQuitPenalty.IntValue : 0;
|
||||
|
||||
if (gCvarStatsEnable != null && gCvarStatsEnable.BoolValue)
|
||||
{
|
||||
DuelStats s;
|
||||
GetClientStats(client, s);
|
||||
s.losses++;
|
||||
s.rageQuits++;
|
||||
UpdateClientStats(client, s);
|
||||
SaveDuelStats();
|
||||
}
|
||||
|
||||
if (penalty > 0 && IsLRReady() && LR_GetClientStatus(client))
|
||||
{
|
||||
LR_ChangeClientValue(client, -penalty);
|
||||
}
|
||||
|
||||
LogDuelAction("RAGE_QUIT: %N | penalty=%d", client, penalty);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Команды для игроков: !duelstats и !duel_top
|
||||
// ============================================================================
|
||||
|
||||
public Action Command_DuelStats(int client, int args)
|
||||
{
|
||||
if (!IsValidHuman(client))
|
||||
{
|
||||
ReplyToCommand(client, "%s Команда доступна только в игре.", DUEL_PREFIX);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
DuelStats s;
|
||||
if (!GetClientStats(client, s))
|
||||
{
|
||||
PrintToChat(client, "%s У тебя пока нет дуэлей.", DUEL_PREFIX);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
int total = s.wins + s.losses + s.draws;
|
||||
int winRate = (total > 0) ? (s.wins * 100 / total) : 0;
|
||||
|
||||
PrintToChat(client, "%s Твоя статистика дуэлей:", DUEL_PREFIX);
|
||||
PrintToChat(client, "%s Победы: \x05%d\x01 | Поражения: \x05%d\x01 | Ничьи: \x05%d", DUEL_PREFIX, s.wins, s.losses, s.draws);
|
||||
PrintToChat(client, "%s Винрейт: \x04%d%%\x01 | Rage-quits: \x05%d\x01 | Всего XP: \x05%d", DUEL_PREFIX, winRate, s.rageQuits, s.totalXP);
|
||||
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
public Action Command_DuelTop(int client, int args)
|
||||
{
|
||||
if (!IsValidHuman(client))
|
||||
return Plugin_Handled;
|
||||
|
||||
if (g_smStats == null || g_smStats.Size == 0)
|
||||
{
|
||||
PrintToChat(client, "%s Статистика пока пуста.", DUEL_PREFIX);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
StringMapSnapshot snap = g_smStats.Snapshot();
|
||||
int count = snap.Length;
|
||||
if (count > 64) count = 64;
|
||||
|
||||
char[][] keys = new char[count][32];
|
||||
int[] wins = new int[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
char steam[32];
|
||||
snap.GetKey(i, steam, sizeof(steam));
|
||||
strcopy(keys[i], 32, steam);
|
||||
|
||||
DuelStats s;
|
||||
g_smStats.GetArray(steam, s, sizeof(s));
|
||||
wins[i] = s.wins;
|
||||
}
|
||||
delete snap;
|
||||
|
||||
// Простая сортировка пузырьком (топ-10 не критично по производительности)
|
||||
for (int i = 0; i < count - 1; i++)
|
||||
{
|
||||
for (int j = 0; j < count - 1 - i; j++)
|
||||
{
|
||||
if (wins[j] < wins[j + 1])
|
||||
{
|
||||
int tmpW = wins[j]; wins[j] = wins[j + 1]; wins[j + 1] = tmpW;
|
||||
char tmpK[32]; strcopy(tmpK, 32, keys[j]);
|
||||
strcopy(keys[j], 32, keys[j + 1]);
|
||||
strcopy(keys[j + 1], 32, tmpK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu menu = new Menu(MenuHandler_DuelTop);
|
||||
menu.SetTitle("Топ дуэлянтов сервера");
|
||||
|
||||
int show = (count < 10) ? count : 10;
|
||||
for (int i = 0; i < show; i++)
|
||||
{
|
||||
DuelStats s;
|
||||
g_smStats.GetArray(keys[i], s, sizeof(s));
|
||||
|
||||
char line[128];
|
||||
Format(line, sizeof(line), "%d. %s — %d побед / %d пораж.",
|
||||
i + 1, s.name[0] == '\0' ? "Unknown" : s.name, s.wins, s.losses);
|
||||
menu.AddItem("", line, ITEMDRAW_DISABLED);
|
||||
}
|
||||
|
||||
if (show == 0)
|
||||
menu.AddItem("", "Никто ещё не сыграл ни одной дуэли", ITEMDRAW_DISABLED);
|
||||
|
||||
menu.ExitButton = true;
|
||||
menu.Display(client, 30);
|
||||
return Plugin_Handled;
|
||||
}
|
||||
|
||||
public int MenuHandler_DuelTop(Menu menu, MenuAction action, int client, int item)
|
||||
{
|
||||
if (action == MenuAction_End)
|
||||
delete menu;
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user