commit 56ea8aa4cd0af8b2f1b2b4e795dbd8c792a832e7 Author: deidara Date: Fri May 1 06:57:31 2026 +0300 Initial commit: arcanegame-duels plugin with documentation and config diff --git a/README.md b/README.md new file mode 100644 index 0000000..a94135f --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# ArcaneGameDUELS Core + +Система дуэлей 1v1 для CS:GO серверов на SourceMod. Поддерживает арену, зону ограничения, выбор оружия, модификаторы и интеграцию с системой XP (lvl_ranks). + +## Функции + +- Дуэли **1v1** между любыми двумя игроками +- Автоматический запуск дуэли при обнаружении двух игроков на сервере +- **Арена**: фиксированные точки спавна из конфига `ArcaneGameDUELS_Arena.cfg` +- **Зона ограничения**: дуэлянты не могут покинуть арену (настраиваемый padding) +- Выбор оружия: Deagle, AK-47, M4A4, M4A1-S, AWP, Scout, Knife +- Модификаторы: обычный / NoZoom / Headshot Only +- Сохранение и восстановление инвентаря и здоровья после дуэли +- Таймер дуэли с ограничением по времени +- Интеграция XP через **lvl_ranks** (награда за победу) +- **Forwards** для других плагинов: `OnDuelStarted`, `OnDuelFinished`, `OnDuelDraw` +- Beacon-индикатор (звук + частицы) вокруг дуэлянтов + +## Зависимости + +- [SourceMod](https://www.sourcemod.net/) 1.10+ +- [SDKHooks](https://wiki.alliedmods.net/SDK_Hooks) (входит в SourceMod) +- [lvl_ranks](https://github.com/levans95/lvl_ranks) — для начисления XP + +## Установка + +1. Скомпилировать `scripting/ArcaneGameDUELS_Core.sp` +2. Положить `.smx` в `addons/sourcemod/plugins/` +3. Положить `cfg/sourcemod/ArcaneGameDUELS_Arena.cfg` в `cfg/sourcemod/` на сервере +4. Перезапустить сервер или загрузить плагин: `sm plugins load ArcaneGameDUELS_Core` + +## Конфиг арены + +Путь: `cfg/sourcemod/ArcaneGameDUELS_Arena.cfg` + +``` +// Позиция спавна игрока 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" +``` + +## 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: одиночный режим (без второго игрока) | + +## Forwards (для других плагинов) + +```sourcepawn +// Дуэль началась +forward void OnDuelStarted(int player1, int player2); + +// Дуэль завершилась +forward void OnDuelFinished(int winner, int loser); + +// Дуэль завершилась ничьей +forward void OnDuelDraw(int player1, int player2); +``` + +## Версия + +`1.5.3` — Автор: OpenAI / havno diff --git a/cfg/sourcemod/ArcaneGameDUELS_Arena.cfg b/cfg/sourcemod/ArcaneGameDUELS_Arena.cfg new file mode 100644 index 0000000..70fc035 --- /dev/null +++ b/cfg/sourcemod/ArcaneGameDUELS_Arena.cfg @@ -0,0 +1,26 @@ +// ArcaneGameDUELS Arena Configuration +// Укажите координаты точек спавна арены для текущей карты. +// Координаты можно узнать командой: getpos (в консоли CS:GO от имени игрока) + +// ── Спавн игрока 1 ──────────────────────────────────────────── +sm_duels_arena_spawn1_x "0.0" +sm_duels_arena_spawn1_y "0.0" +sm_duels_arena_spawn1_z "64.0" +sm_duels_arena_spawn1_yaw "0.0" + +// ── Спавн игрока 2 ──────────────────────────────────────────── +sm_duels_arena_spawn2_x "300.0" +sm_duels_arena_spawn2_y "0.0" +sm_duels_arena_spawn2_z "64.0" +sm_duels_arena_spawn2_yaw "180.0" + +// ── Зона арены (опционально, используется если sm_duels_zone_enable = 1) ─ +// Минимальная точка зоны (нижний левый угол) +sm_duels_arena_zone_min_x "-200.0" +sm_duels_arena_zone_min_y "-200.0" +sm_duels_arena_zone_min_z "0.0" + +// Максимальная точка зоны (верхний правый угол) +sm_duels_arena_zone_max_x "500.0" +sm_duels_arena_zone_max_y "200.0" +sm_duels_arena_zone_max_z "300.0" diff --git a/scripting/ArcaneGameDUELS_Core.sp b/scripting/ArcaneGameDUELS_Core.sp new file mode 100644 index 0000000..ee0508f --- /dev/null +++ b/scripting/ArcaneGameDUELS_Core.sp @@ -0,0 +1,2201 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include +#include +#include "lvl_ranks" + +#define AGD_LIBRARY "arcane_duels_core" +#define ARENA_CFG "cfg/sourcemod/ArcaneGameDUELS_Arena.cfg" +#define BEACON_SOUND "buttons/blip1.wav" +#define DUEL_PREFIX "\x02[DUELS]\x01" +#define DUEL_OUT_SOUND "buttons/button10.wav" + +public Plugin myinfo = +{ + name = "[CORE] ArcaneGameDUELS Core", + author = "OpenAI / havno", + description = "Core of ArcaneGameDUELS (duel deagle compatibility build)", + version = "1.5.3", + url = "" +}; + +bool g_bDuelActive = false; +bool g_bDuelPending = false; +bool g_bDuelPreparing = false; +bool g_bBombPlanted = false; +int g_iDuelP1 = 0; +int g_iDuelP2 = 0; +int g_iDuelController = 0; +int g_iParticipantXP[MAXPLAYERS + 1]; +bool g_bAccepted[MAXPLAYERS + 1]; +int g_iPrepareLeft = 0; +int g_iRoundStartPlayingHumans = 0; + +char g_sSelectedWeapon[32] = "weapon_ak47"; +enum DuelModifier +{ + DuelModifier_None = 0, + DuelModifier_NoZoom, + DuelModifier_HeadshotOnly +}; +DuelModifier g_iDuelModifier = DuelModifier_None; + +char g_sSavedWeapon[MAXPLAYERS + 1][3][32]; +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]; + +Handle g_hFwdOnDuelStarted = null; +Handle g_hFwdOnDuelFinished = null; +Handle g_hFwdOnDuelDraw = null; +Handle g_hBeaconTimer = null; +Handle g_hPrepareTimer = null; +Handle g_hInvulnTimer = null; +Handle g_hZoneTimer = null; +Handle g_hZonePreviewTimer = null; +Handle g_hPendingOneVsOneTimer = null; +Handle g_hDuelLimitTimer = null; +Handle g_hPrepZoneTimer = null; + +ConVar gCvarWinMoney; +ConVar gCvarEnable; +ConVar gCvarUseArena; +ConVar gCvarBeacon; +ConVar gCvarPrepareTime; +ConVar gCvarDuelWinXP; +ConVar gCvarPostUnfreezeInvuln; +ConVar gCvarAllowKnife; +ConVar gCvarAllowDeagle; +ConVar gCvarAllowAK47; +ConVar gCvarAllowM4A4; +ConVar gCvarAllowM4A1S; +ConVar gCvarAllowAWP; +ConVar gCvarAllowScout; +ConVar gCvarWeaponBypassFlags; +ConVar gCvarZoneEnable; +ConVar gCvarZoneGrace; +ConVar gCvarZonePaddingXY; +ConVar gCvarZonePaddingZDown; +ConVar gCvarZonePaddingZUp; +ConVar gCvarZonePreviewTime; +ConVar gCvarZoneRenderInterval; +ConVar gCvarDebugAllowSolo; +ConVar gCvarDuelTimeLimit; + +bool g_bArenaReady = false; +float g_vecArenaSpawn1[3]; +float g_angArenaSpawn1[3]; +float g_vecArenaSpawn2[3]; +float g_angArenaSpawn2[3]; +int g_iBeamSprite = -1; +int g_iHaloSprite = -1; + +bool g_bArenaZoneReady = false; +float g_vecArenaZoneMin[3]; +float g_vecArenaZoneMax[3]; +float g_vecManualZonePoint1[3]; +float g_vecManualZonePoint2[3]; +bool g_bManualZonePoint1Set = false; +bool g_bManualZonePoint2Set = false; +bool g_bZoneOutside[MAXPLAYERS + 1]; +int g_iZoneGraceLeft[MAXPLAYERS + 1]; +bool g_bDebugSoloMode = false; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary(AGD_LIBRARY); + + CreateNative("AGD_IsDuelActive", Native_AGD_IsDuelActive); + CreateNative("AGD_IsBettingOpen", Native_AGD_IsBettingOpen); + CreateNative("AGD_GetDuelPlayers", Native_AGD_GetDuelPlayers); + CreateNative("AGD_GetDuelParticipantXP", Native_AGD_GetDuelParticipantXP); + CreateNative("AGD_GetWinnerMoneyReward", Native_AGD_GetWinnerMoneyReward); + CreateNative("AGD_IsArenaReady", Native_AGD_IsArenaReady); + CreateNative("AGD_IsClientInDuel", Native_AGD_IsClientInDuel); + + return APLRes_Success; +} + +public void OnPluginStart() +{ + gCvarEnable = CreateConVar("agd_core_enable", "1", "Enable ArcaneGameDUELS core", _, true, 0.0, true, 1.0); + gCvarWinMoney = CreateConVar("agd_duel_win_money", "1500", "Money reward for duel winner", _, true, 0.0, true, 16000.0); + gCvarUseArena = CreateConVar("agd_use_arena", "1", "Teleport duel players to saved arena spawns", _, true, 0.0, true, 1.0); + gCvarBeacon = CreateConVar("agd_duel_beacon", "1", "Enable beacons during duel", _, true, 0.0, true, 1.0); + gCvarPrepareTime = CreateConVar("agd_duel_prepare_time", "10", "Prepare time before duel start", _, true, 3.0, true, 30.0); + gCvarDuelWinXP = CreateConVar("agd_duel_win_xp", "100", "XP reward for duel winner", _, true, 0.0, true, 1000000.0); + gCvarPostUnfreezeInvuln = CreateConVar("agd_duel_post_unfreeze_invuln", "2.0", "Invulnerability time after unfreeze", _, true, 0.0, true, 10.0); + gCvarAllowKnife = CreateConVar("agd_weapon_allow_knife", "1", "Allow Knife in duel weapon menu", _, true, 0.0, true, 1.0); + gCvarAllowDeagle = CreateConVar("agd_weapon_allow_deagle", "1", "Allow Deagle in duel weapon menu", _, true, 0.0, true, 1.0); + gCvarAllowAK47 = CreateConVar("agd_weapon_allow_ak47", "1", "Allow AK-47 in duel weapon menu", _, true, 0.0, true, 1.0); + gCvarAllowM4A4 = CreateConVar("agd_weapon_allow_m4a1", "1", "Allow M4A4 in duel weapon menu", _, true, 0.0, true, 1.0); + gCvarAllowM4A1S = CreateConVar("agd_weapon_allow_m4a1s", "1", "Allow M4A1-S in duel weapon menu", _, true, 0.0, true, 1.0); + gCvarAllowAWP = CreateConVar("agd_weapon_allow_awp", "1", "Allow AWP in duel weapon menu", _, true, 0.0, true, 1.0); + gCvarAllowScout = CreateConVar("agd_weapon_allow_scout", "1", "Allow Scout in duel weapon menu", _, true, 0.0, true, 1.0); + gCvarWeaponBypassFlags = CreateConVar("agd_duel_bypass_restrict_flags", "1", "Temporarily give duelists bypass flags during duel to ignore weapon restrictions", _, true, 0.0, true, 1.0); + gCvarZoneEnable = CreateConVar("agd_zone_enable", "1", "Enable duel zone restriction", _, true, 0.0, true, 1.0); + gCvarZoneGrace = CreateConVar("agd_zone_grace_time", "3", "Seconds to return to duel zone before losing", _, true, 1.0, true, 10.0); + gCvarZonePaddingXY = CreateConVar("agd_zone_padding_xy", "320.0", "Auto-generated duel zone XY padding from both spawns", _, true, 50.0, true, 2000.0); + gCvarZonePaddingZDown = CreateConVar("agd_zone_padding_z_down", "80.0", "Auto-generated duel zone downward padding", _, true, 0.0, true, 1000.0); + gCvarZonePaddingZUp = CreateConVar("agd_zone_padding_z_up", "180.0", "Auto-generated duel zone upward padding", _, true, 0.0, true, 2000.0); + gCvarZonePreviewTime = CreateConVar("agd_zone_preview_time", "10.0", "How long admin preview of duel zone is shown", _, true, 1.0, true, 60.0); + 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); + + AutoExecConfig(true, "ArcaneGameDUELS_Core"); + + g_hFwdOnDuelStarted = CreateGlobalForward("AGD_OnDuelStarted", ET_Ignore, Param_Cell, Param_Cell); + g_hFwdOnDuelFinished = CreateGlobalForward("AGD_OnDuelFinished", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_Cell); + g_hFwdOnDuelDraw = CreateGlobalForward("AGD_OnDuelDraw", ET_Ignore, Param_Cell, Param_Cell); + + RegConsoleCmd("sm_duel", Command_DuelMenu); + + RegAdminCmd("sm_duel_setspawn1", Command_SetSpawn1, ADMFLAG_ROOT); + RegAdminCmd("sm_duel_setspawn2", Command_SetSpawn2, ADMFLAG_ROOT); + RegAdminCmd("sm_duel_setzone1", Command_SetZonePoint1, ADMFLAG_ROOT); + RegAdminCmd("sm_duel_setzone2", Command_SetZonePoint2, ADMFLAG_ROOT); + RegAdminCmd("sm_duel_savearena", Command_SaveArena, ADMFLAG_ROOT); + RegAdminCmd("sm_duel_reloadarena", Command_ReloadArena, ADMFLAG_ROOT); + 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); + + HookEvent("player_death", Event_PlayerDeath, EventHookMode_Post); + HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy); + HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); + HookEvent("bomb_planted", Event_BombPlanted, EventHookMode_PostNoCopy); + HookEvent("bomb_defused", Event_BombDefused, EventHookMode_PostNoCopy); + HookEvent("bomb_exploded", Event_BombExploded, EventHookMode_PostNoCopy); + + LoadArenaForCurrentMap(); + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + OnClientPutInServer(i); + } +} + +public void OnMapStart() +{ + LoadArenaForCurrentMap(); + PrecacheSound(BEACON_SOUND, true); + PrecacheSound(DUEL_OUT_SOUND, true); + g_iBeamSprite = PrecacheModel("materials/sprites/laserbeam.vmt", true); + g_iHaloSprite = PrecacheModel("materials/sprites/glow01.vmt", true); +} + +public void OnClientPutInServer(int client) +{ + SDKHook(client, SDKHook_TraceAttack, OnTraceAttack); + SDKHook(client, SDKHook_WeaponCanUse, OnWeaponCanUse); + SDKHook(client, SDKHook_PostThinkPost, OnPlayerPostThink); +} + +public void OnClientDisconnect(int client) +{ + DisableDuelWeaponBypass(client); + + if ((g_bDuelPending || g_bDuelPreparing) && (client == g_iDuelP1 || client == g_iDuelP2)) + { + if (g_bDuelPreparing) + CancelPreparingDuel("Дуэль отменена: игрок вышел во время подготовки."); + else + CancelPendingDuel("Дуэль отменена: игрок вышел во время принятия."); + return; + } + + if (g_bDebugSoloMode && client == g_iDuelP1) + { + ResetDuelState(); + return; + } + + if (!g_bDuelActive) + return; + + if (client == g_iDuelP1 || client == g_iDuelP2) + { + int winner = (client == g_iDuelP1) ? g_iDuelP2 : g_iDuelP1; + int loser = client; + + if (IsValidHuman(winner)) + FinishDuel(winner, loser); + else + ResetDuelState(); + } +} + +public Action Command_DuelMenu(int client, int args) +{ + if (!IsValidHuman(client)) + return Plugin_Handled; + + PrintToChat(client, "%s Дуэли запускаются автоматически при 1x1, если на сервере больше 2 игроков. Для теста в одиночку: sm_duel_debugsolo", DUEL_PREFIX); + return Plugin_Handled; +} + +public Action Command_SetSpawn1(int client, int args) +{ + if (!IsValidHuman(client)) + return Plugin_Handled; + + GetClientAbsOrigin(client, g_vecArenaSpawn1); + GetClientAbsAngles(client, g_angArenaSpawn1); + PrintToChat(client, "%s Точка арены #1 сохранена.", DUEL_PREFIX); + g_bArenaReady = HasBothArenaSpawns(); + + if (g_bArenaReady && g_bArenaZoneReady) + { + StartZonePreview(client, gCvarZonePreviewTime.FloatValue); + PrintToChat(client, "%s Текущая зона показана для проверки.", DUEL_PREFIX); + } + return Plugin_Handled; +} + +public Action Command_SetSpawn2(int client, int args) +{ + if (!IsValidHuman(client)) + return Plugin_Handled; + + GetClientAbsOrigin(client, g_vecArenaSpawn2); + GetClientAbsAngles(client, g_angArenaSpawn2); + PrintToChat(client, "%s Точка арены #2 сохранена.", DUEL_PREFIX); + g_bArenaReady = HasBothArenaSpawns(); + + if (g_bArenaReady && g_bArenaZoneReady) + { + StartZonePreview(client, gCvarZonePreviewTime.FloatValue); + PrintToChat(client, "%s Текущая зона показана для проверки.", DUEL_PREFIX); + } + return Plugin_Handled; +} + +public Action Command_SetZonePoint1(int client, int args) +{ + if (!IsValidHuman(client)) + return Plugin_Handled; + + GetClientAbsOrigin(client, g_vecManualZonePoint1); + g_bManualZonePoint1Set = true; + + ReplyToCommand(client, "[DUELS] Точка зоны #1 сохранена: %.1f %.1f %.1f", + g_vecManualZonePoint1[0], g_vecManualZonePoint1[1], g_vecManualZonePoint1[2]); + + if (g_bManualZonePoint2Set) + { + BuildArenaZoneFromManualPoints(); + StartZonePreview(client, gCvarZonePreviewTime.FloatValue); + ReplyToCommand(client, "[DUELS] Зона пересобрана вручную по двум диагональным точкам. Высота: 200.0"); + } + else + { + ReplyToCommand(client, "[DUELS] Теперь встань во второй угол по диагонали и введи sm_duel_setzone2"); + } + + return Plugin_Handled; +} + +public Action Command_SetZonePoint2(int client, int args) +{ + if (!IsValidHuman(client)) + return Plugin_Handled; + + GetClientAbsOrigin(client, g_vecManualZonePoint2); + g_bManualZonePoint2Set = true; + + ReplyToCommand(client, "[DUELS] Точка зоны #2 сохранена: %.1f %.1f %.1f", + g_vecManualZonePoint2[0], g_vecManualZonePoint2[1], g_vecManualZonePoint2[2]); + + if (g_bManualZonePoint1Set) + { + BuildArenaZoneFromManualPoints(); + StartZonePreview(client, gCvarZonePreviewTime.FloatValue); + ReplyToCommand(client, "[DUELS] Зона пересобрана вручную по двум диагональным точкам. Высота: 200.0"); + } + else + { + ReplyToCommand(client, "[DUELS] Сначала поставь первую точку командой sm_duel_setzone1"); + } + + return Plugin_Handled; +} + +public Action Command_SaveArena(int client, int args) +{ + if (!HasBothArenaSpawns()) + { + ReplyToCommand(client, "[DUELS] Сначала задай обе точки спавна: sm_duel_setspawn1 и sm_duel_setspawn2"); + return Plugin_Handled; + } + + if (!g_bArenaZoneReady) + { + ReplyToCommand(client, "[DUELS] Сначала задай зону командами sm_duel_setzone1 и sm_duel_setzone2"); + return Plugin_Handled; + } + + if (SaveArenaForCurrentMap()) + ReplyToCommand(client, "[DUELS] Арена сохранена для текущей карты."); + else + ReplyToCommand(client, "[DUELS] Не удалось сохранить арену."); + + return Plugin_Handled; +} + +public Action Command_ReloadArena(int client, int args) +{ + LoadArenaForCurrentMap(); + ReplyToCommand(client, g_bArenaReady ? "[DUELS] Арена перезагружена." : "[DUELS] Для этой карты арена не найдена."); + return Plugin_Handled; +} + +public Action Command_ArenaInfo(int client, int args) +{ + if (!g_bArenaReady) + { + ReplyToCommand(client, "[DUELS] Арена для текущей карты не настроена."); + return Plugin_Handled; + } + + if (g_bArenaZoneReady) + { + ReplyToCommand(client, "[DUELS] Арена готова. Зона: min(%.1f %.1f %.1f) | max(%.1f %.1f %.1f)", + g_vecArenaZoneMin[0], g_vecArenaZoneMin[1], g_vecArenaZoneMin[2], + g_vecArenaZoneMax[0], g_vecArenaZoneMax[1], g_vecArenaZoneMax[2]); + } + else + { + ReplyToCommand(client, "[DUELS] Арена готова, но зона еще не задана."); + } + return Plugin_Handled; +} + +public Action Command_ShowZone(int client, int args) +{ + if (!IsValidHuman(client)) + return Plugin_Handled; + + if (!g_bArenaZoneReady) + { + ReplyToCommand(client, "[DUELS] Зона арены не настроена."); + return Plugin_Handled; + } + + StartZonePreview(client, gCvarZonePreviewTime.FloatValue); + ReplyToCommand(client, "[DUELS] Показываю зону на %.1f сек.", gCvarZonePreviewTime.FloatValue); + return Plugin_Handled; +} + +public Action Command_DebugSolo(int client, int args) +{ + if (!IsValidHuman(client)) + return Plugin_Handled; + + if (!gCvarDebugAllowSolo.BoolValue) + { + ReplyToCommand(client, "[DUELS] Соло-дебаг отключен cvar'ом agd_debug_allow_solo."); + return Plugin_Handled; + } + + if (g_bDuelActive || g_bDuelPending || g_bDuelPreparing) + { + ReplyToCommand(client, "[DUELS] Сейчас уже идет дуэль или подготовка."); + return Plugin_Handled; + } + + if (!g_bArenaReady) + { + ReplyToCommand(client, "[DUELS] Сначала настрой арены через sm_duel_setspawn1 / sm_duel_setspawn2."); + return Plugin_Handled; + } + + BeginSoloDebugPreparation(client); + return Plugin_Handled; +} + +public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) +{ + g_bBombPlanted = false; + CancelPendingDuel(""); + CancelPreparingDuel(""); + if (g_bDuelActive) + ResetDuelState(); + + g_iRoundStartPlayingHumans = GetPlayingHumanPlayersCount(); +} + +public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) +{ + g_bBombPlanted = false; + g_iRoundStartPlayingHumans = 0; + CancelPendingDuel(""); + CancelPreparingDuel(""); + + if (!g_bDuelActive) + return; + + if (g_bDebugSoloMode) + { + ResetDuelState(); + return; + } + + bool p1Alive = IsValidHuman(g_iDuelP1) && IsPlayerAlive(g_iDuelP1); + bool p2Alive = IsValidHuman(g_iDuelP2) && IsPlayerAlive(g_iDuelP2); + + if (p1Alive && p2Alive) + { + FinishDuelDraw(g_iDuelP1, g_iDuelP2); + return; + } + + ResetDuelState(); +} + +public void Event_BombPlanted(Event event, const char[] name, bool dontBroadcast) +{ + g_bBombPlanted = true; + + if (g_bDuelPending) + CancelPendingDuel("Дуэль отменена: во время принятия была установлена бомба."); + else if (g_bDuelPreparing) + CancelPreparingDuel("Дуэль отменена: во время подготовки была установлена бомба."); +} + +public void Event_BombDefused(Event event, const char[] name, bool dontBroadcast) +{ + g_bBombPlanted = false; +} + +public void Event_BombExploded(Event event, const char[] name, bool dontBroadcast) +{ + g_bBombPlanted = false; +} + +public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast) +{ + int victim = GetClientOfUserId(event.GetInt("userid")); + int attacker = GetClientOfUserId(event.GetInt("attacker")); + + if (!IsValidClient(victim)) + return; + + if (!g_bDuelActive) + { + if (!g_bBombPlanted && !g_bDuelPending && !g_bDuelPreparing && g_iRoundStartPlayingHumans > 2) + QueueOneVsOneCheck(); + return; + } + + if (victim != g_iDuelP1 && victim != g_iDuelP2) + return; + + int winner = 0; + int loser = victim; + + if (attacker == g_iDuelP1 || attacker == g_iDuelP2) + winner = attacker; + else + winner = (victim == g_iDuelP1) ? g_iDuelP2 : g_iDuelP1; + + if (IsValidHuman(winner)) + FinishDuel(winner, loser); + else if (g_bDebugSoloMode && victim == g_iDuelP1) + { + PrintToChat(victim, "%s Соло-дебаг завершен: ты умер.", DUEL_PREFIX); + RestoreLoadout(g_iDuelP1); + ResetDuelState(); + } +} + +void QueueOneVsOneCheck() +{ + if (g_hPendingOneVsOneTimer != null) + return; + + g_hPendingOneVsOneTimer = CreateTimer(0.2, Timer_CheckForOneVsOne, _, TIMER_FLAG_NO_MAPCHANGE); +} + +public Action Timer_CheckForOneVsOne(Handle timer, any data) +{ + g_hPendingOneVsOneTimer = null; + if (!gCvarEnable.BoolValue || g_bBombPlanted || g_bDuelActive || g_bDuelPending || g_bDuelPreparing) + return Plugin_Stop; + + if (g_iRoundStartPlayingHumans <= 2) + return Plugin_Stop; + + if (GetPlayingHumanPlayersCount() <= 2) + return Plugin_Stop; + + int tAlive = 0, ctAlive = 0; + int tClient = 0, ctClient = 0; + GetAliveOneVsOne(tAlive, ctAlive, tClient, ctClient); + + if (tAlive == 1 && ctAlive == 1 && IsValidHuman(tClient) && IsValidHuman(ctClient)) + OfferAutomaticDuel(tClient, ctClient); + + return Plugin_Stop; +} + +void OfferAutomaticDuel(int client1, int client2) +{ + if (!IsValidHuman(client1) || !IsValidHuman(client2) || client1 == client2) + return; + + g_bDuelPending = true; + g_bDuelPreparing = false; + g_bDuelActive = false; + g_iDuelP1 = client1; + g_iDuelP2 = client2; + g_iDuelController = 0; + g_bAccepted[client1] = false; + g_bAccepted[client2] = false; + + PrintToChatAll("\x02[DUELS]\x01 Остались 1 на 1: \x03%N\x01 vs \x03%N\x01. Ожидается подтверждение дуэли.", client1, client2); + ShowAcceptMenu(client1); + ShowAcceptMenu(client2); +} + +void ShowAcceptMenu(int client) +{ + if (!IsValidHuman(client)) + return; + + Menu menu = new Menu(MenuHandler_Accept); + menu.SetTitle("Принять дуэль? (10 сек)"); + menu.AddItem("1", "Принять"); + menu.AddItem("0", "Отклонить"); + menu.ExitButton = false; + menu.Display(client, 10); +} + +public int MenuHandler_Accept(Menu menu, MenuAction action, int client, int item) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Cancel: + { + if (g_bDuelPending && IsValidHuman(client)) + CancelPendingDuel("Дуэль отменена: один из игроков не ответил."); + } + case MenuAction_Select: + { + if (!g_bDuelPending || !IsValidHuman(client)) + return 0; + + char info[8]; + menu.GetItem(item, info, sizeof(info)); + + if (StringToInt(info) == 1) + { + g_bAccepted[client] = true; + PrintToChat(client, "\x02[DUELS]\x01 Ты принял дуэль."); + + if (g_bAccepted[g_iDuelP1] && g_bAccepted[g_iDuelP2]) + BeginPreparation(g_iDuelP1, g_iDuelP2); + } + else + { + CancelPendingDuel("Дуэль отклонена одним из игроков."); + } + } + } + + return 0; +} + +void BeginPreparation(int client1, int client2) +{ + g_bDuelPending = false; + g_bDuelPreparing = true; + g_bDuelActive = false; + g_iDuelP1 = client1; + g_iDuelP2 = client2; + g_iDuelController = GetRandomInt(0, 1) == 0 ? client1 : client2; + g_iPrepareLeft = gCvarPrepareTime.IntValue; + strcopy(g_sSelectedWeapon, sizeof(g_sSelectedWeapon), "weapon_ak47"); + g_iDuelModifier = DuelModifier_None; + g_iParticipantXP[client1] = 0; + g_iParticipantXP[client2] = 0; + + SaveLoadout(client1); + SaveLoadout(client2); + + if (gCvarUseArena.BoolValue && g_bArenaReady) + { + TeleportEntity(client1, g_vecArenaSpawn1, g_angArenaSpawn1, NULL_VECTOR); + TeleportEntity(client2, g_vecArenaSpawn2, g_angArenaSpawn2, NULL_VECTOR); + } + + SetDuelProtection(client1, true); + SetDuelProtection(client2, true); + + StartPrepZoneTimer(); + + PrintToChatAll("\x02[DUELS]\x01 Дуэль принята. Настройки выбирает случайный игрок: \x03%N", g_iDuelController); + PrintToChatAll("\x02[DUELS]\x01 Подготовка к дуэли: \x05%d\x01 сек. В это время доступны ставки.", g_iPrepareLeft); + + ShowWeaponMenu(g_iDuelController); + + StopPrepareTimer(); + g_hPrepareTimer = CreateTimer(1.0, Timer_PrepareCountdown, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +void BeginSoloDebugPreparation(int client) +{ + g_bDebugSoloMode = true; + g_bDuelPending = false; + g_bDuelPreparing = true; + g_bDuelActive = false; + g_iDuelP1 = client; + g_iDuelP2 = 0; + g_iDuelController = client; + g_iPrepareLeft = gCvarPrepareTime.IntValue; + strcopy(g_sSelectedWeapon, sizeof(g_sSelectedWeapon), "weapon_ak47"); + g_iDuelModifier = DuelModifier_None; + g_iParticipantXP[client] = 0; + g_bAccepted[client] = true; + + SaveLoadout(client); + + if (gCvarUseArena.BoolValue && g_bArenaReady) + TeleportEntity(client, g_vecArenaSpawn1, g_angArenaSpawn1, NULL_VECTOR); + + SetDuelProtection(client, true); + + StartPrepZoneTimer(); + + PrintToChat(client, "%s Запущен соло-дебаг дуэли. Можешь тестировать оружие, подготовку и зону даже один на карте.", DUEL_PREFIX); + ShowWeaponMenu(client); + + StopPrepareTimer(); + g_hPrepareTimer = CreateTimer(1.0, Timer_PrepareCountdown, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +void ShowWeaponMenu(int client) +{ + if (!IsValidHuman(client) || !g_bDuelPreparing) + return; + + Menu menu = new Menu(MenuHandler_WeaponMenu); + menu.SetTitle("Настройка дуэли: оружие"); + + int added = 0; + if (gCvarAllowKnife.BoolValue) { menu.AddItem("weapon_knife", "Ножи"); added++; } + if (gCvarAllowDeagle.BoolValue) { menu.AddItem("weapon_deagle", "Deagle"); added++; } + if (gCvarAllowAK47.BoolValue) { menu.AddItem("weapon_ak47", "AK-47"); added++; } + if (gCvarAllowM4A4.BoolValue) { menu.AddItem("weapon_m4a1", "M4A4/M4A1-S"); added++; } + if (gCvarAllowAWP.BoolValue) { menu.AddItem("weapon_awp", "AWP"); added++; } + if (gCvarAllowScout.BoolValue) { menu.AddItem("weapon_ssg08", "SSG08 / Scout"); added++; } + + if (added <= 0) + menu.AddItem("blocked", "Нет доступного оружия", ITEMDRAW_DISABLED); + + menu.ExitButton = false; + menu.Display(client, 10); +} + +public int MenuHandler_WeaponMenu(Menu menu, MenuAction action, int client, int item) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Select: + { + if (!g_bDuelPreparing || client != g_iDuelController) + return 0; + + char info[32]; + menu.GetItem(item, info, sizeof(info)); + strcopy(g_sSelectedWeapon, sizeof(g_sSelectedWeapon), info); + g_iDuelModifier = DuelModifier_None; + + if (!StrEqual(g_sSelectedWeapon, "weapon_knife")) + ShowModifierMenu(client); + else + AnnounceSelectedSettings(); + } + } + + return 0; +} + +void ShowModifierMenu(int client) +{ + Menu menu = new Menu(MenuHandler_ModifierMenu); + menu.SetTitle("Режим дуэли: обычный или только в голову"); + menu.AddItem("0", "Обычный"); + menu.AddItem("2", "Только в голову"); + menu.ExitButton = false; + menu.Display(client, 10); +} + +public int MenuHandler_ModifierMenu(Menu menu, MenuAction action, int client, int item) +{ + switch (action) + { + case MenuAction_End: + delete menu; + case MenuAction_Select: + { + if (!g_bDuelPreparing || client != g_iDuelController) + return 0; + + char info[8]; + menu.GetItem(item, info, sizeof(info)); + g_iDuelModifier = view_as(StringToInt(info)); + AnnounceSelectedSettings(); + } + } + + return 0; +} + +void AnnounceSelectedSettings() +{ + char weaponName[32]; + GetDuelWeaponName(weaponName, sizeof(weaponName)); + if (g_iDuelModifier == DuelModifier_HeadshotOnly) + PrintToChatAll("\x02[DUELS]\x01 Выбрана дуэль: \x03%s\x01 | режим: \x05Только в голову", weaponName); + else + PrintToChatAll("\x02[DUELS]\x01 Выбрана дуэль: \x03%s\x01 | режим: \x05Обычный", weaponName); +} + +public Action Timer_PrepareCountdown(Handle timer, any data) +{ + if (!g_bDuelPreparing) + return Plugin_Stop; + + if (g_bBombPlanted) + { + CancelPreparingDuel("Дуэль отменена: во время подготовки была установлена бомба."); + return Plugin_Stop; + } + + if (!IsValidHuman(g_iDuelP1) || (!g_bDebugSoloMode && !IsValidHuman(g_iDuelP2))) + { + CancelPreparingDuel("Дуэль отменена: один из игроков покинул сервер."); + return Plugin_Stop; + } + + g_iPrepareLeft--; + + if (g_iPrepareLeft > 0) + { + PrintCenterText(g_iDuelP1, g_bDebugSoloMode ? "Соло-дебаг начнется через %d" : "Дуэль начнется через %d", g_iPrepareLeft); + if (!g_bDebugSoloMode && IsValidHuman(g_iDuelP2)) + PrintCenterText(g_iDuelP2, "Дуэль начнется через %d", g_iPrepareLeft); + return Plugin_Continue; + } + + if (g_bDebugSoloMode) + StartSoloDebug(g_iDuelP1); + else + StartDuel(g_iDuelP1, g_iDuelP2); + return Plugin_Stop; +} + +void StartDuel(int client1, int client2) +{ + StopPrepareTimer(); + + if (!gCvarEnable.BoolValue) + return; + + g_bDuelPending = false; + g_bDuelPreparing = false; + g_bDuelActive = true; + g_iDuelP1 = client1; + g_iDuelP2 = client2; + + EquipPlayerForDuel(client1); + EquipPlayerForDuel(client2); + EnsureDuelWeaponPresent(client1); + EnsureDuelWeaponPresent(client2); + + // Fair duel start: restore health, armor, and helmet for both players + SetEntityHealth(client1, 100); + SetEntityHealth(client2, 100); + SetEntProp(client1, Prop_Send, "m_ArmorValue", 100); + SetEntProp(client2, Prop_Send, "m_ArmorValue", 100); + SetEntProp(client1, Prop_Send, "m_bHasHelmet", 1); + SetEntProp(client2, Prop_Send, "m_bHasHelmet", 1); + SetEntityMoveType(client1, MOVETYPE_WALK); + SetEntityMoveType(client2, MOVETYPE_WALK); + SetEntProp(client1, Prop_Data, "m_takedamage", 0); + SetEntProp(client2, Prop_Data, "m_takedamage", 0); + + StopInvulnTimer(); + StopZoneTimer(); + StopZonePreview(); + StopPrepZoneTimer(); + g_hInvulnTimer = CreateTimer(gCvarPostUnfreezeInvuln.FloatValue, Timer_DisablePostUnfreezeInvuln, _, TIMER_FLAG_NO_MAPCHANGE); + + StartBeaconTimer(); + StartZoneTimer(); + StartDuelLimitTimer(); + + 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); + + Call_StartForward(g_hFwdOnDuelStarted); + Call_PushCell(client1); + Call_PushCell(client2); + Call_Finish(); +} + +void StartSoloDebug(int client) +{ + StopPrepareTimer(); + + if (!gCvarEnable.BoolValue || !IsValidHuman(client)) + return; + + g_bDuelPending = false; + g_bDuelPreparing = false; + g_bDuelActive = true; + g_iDuelP1 = client; + g_iDuelP2 = 0; + + EquipPlayerForDuel(client); + SetEntityHealth(client, 100); + SetEntProp(client, Prop_Send, "m_ArmorValue", 100); + SetEntProp(client, Prop_Send, "m_bHasHelmet", 1); + SetEntityMoveType(client, MOVETYPE_WALK); + SetEntProp(client, Prop_Data, "m_takedamage", 0); + + StopInvulnTimer(); + StopZoneTimer(); + StopZonePreview(); + StopPrepZoneTimer(); + g_hInvulnTimer = CreateTimer(gCvarPostUnfreezeInvuln.FloatValue, Timer_DisablePostUnfreezeInvuln, _, TIMER_FLAG_NO_MAPCHANGE); + StartZoneTimer(); + StartZonePreview(client, gCvarZonePreviewTime.FloatValue); + + char weaponName[32]; + GetDuelWeaponName(weaponName, sizeof(weaponName)); + PrintToChat(client, "%s Соло-дебаг начался | %s", DUEL_PREFIX, weaponName); +} + +void FinishDuel(int winner, int loser, bool endRoundByTeamDefeat = false) +{ + int loserTeam = IsValidClient(loser) ? GetClientTeam(loser) : 0; + + StopBeaconTimer(); + StopPrepareTimer(); + StopInvulnTimer(); + StopZoneTimer(); + StopZonePreview(); + StopDuelLimitTimer(); + + if (!IsValidClient(winner)) + { + ResetDuelState(); + return; + } + + SetDuelProtection(g_iDuelP1, false); + SetDuelProtection(g_iDuelP2, false); + + int moneyReward = gCvarWinMoney.IntValue; + int duelWinXP = gCvarDuelWinXP.IntValue; + + if (IsValidHuman(winner)) + { + GivePlayerMoney(winner, moneyReward); + + if (duelWinXP > 0) + { + LR_GiveXP_Safe(winner, duelWinXP); + g_iParticipantXP[winner] += duelWinXP; + } + } + + PrintToChatAll("\x02[DUELS]\x01 Дуэль завершена. Победитель: \x03%N\x01 | Награда: \x05%d$\x01 | Базовый XP: \x05%d", winner, moneyReward, duelWinXP); + + Call_StartForward(g_hFwdOnDuelFinished); + Call_PushCell(winner); + Call_PushCell(loser); + Call_PushCell(g_iParticipantXP[winner]); + Call_PushCell(g_iParticipantXP[loser]); + Call_Finish(); + + RestoreLoadout(g_iDuelP1); + RestoreLoadout(g_iDuelP2); + ResetDuelState(); + + if (endRoundByTeamDefeat) + EndRoundWithTeamDefeat(loserTeam); +} + + +void FinishDuelDraw(int client1, int client2) +{ + StopBeaconTimer(); + StopPrepareTimer(); + StopInvulnTimer(); + StopZoneTimer(); + StopZonePreview(); + StopDuelLimitTimer(); + + SetDuelProtection(g_iDuelP1, false); + SetDuelProtection(g_iDuelP2, false); + + PrintToChatAll("%s Дуэль завершилась вничью. Ставки должны быть возвращены модулем ставок.", DUEL_PREFIX); + + Call_StartForward(g_hFwdOnDuelDraw); + Call_PushCell(client1); + Call_PushCell(client2); + Call_Finish(); + + RestoreLoadout(g_iDuelP1); + RestoreLoadout(g_iDuelP2); + ResetDuelState(); +} + +void EndRoundWithTeamDefeat(int loserTeam) +{ + CSRoundEndReason reason; + + switch (loserTeam) + { + case CS_TEAM_T: + { + reason = CSRoundEnd_CTWin; + } + case CS_TEAM_CT: + { + reason = CSRoundEnd_TerroristWin; + } + default: + { + return; + } + } + + CS_TerminateRound(0.1, reason, false); +} + +void CancelPendingDuel(const char[] reason) +{ + if (!g_bDuelPending) + return; + + if (reason[0] != '\0') + PrintToChatAll("\x02[DUELS]\x01 %s", reason); + + g_bDuelPending = false; + if (g_iDuelP1 > 0) + g_bAccepted[g_iDuelP1] = false; + if (g_iDuelP2 > 0) + g_bAccepted[g_iDuelP2] = false; + + g_iDuelP1 = 0; + g_iDuelP2 = 0; + g_iDuelController = 0; +} + +void CancelPreparingDuel(const char[] reason) +{ + if (!g_bDuelPreparing) + return; + + StopPrepareTimer(); + StopZoneTimer(); + StopZonePreview(); + StopPrepZoneTimer(); + SetDuelProtection(g_iDuelP1, false); + SetDuelProtection(g_iDuelP2, false); + + if (reason[0] != '\0') + PrintToChatAll("\x02[DUELS]\x01 %s", reason); + + RestoreLoadout(g_iDuelP1); + RestoreLoadout(g_iDuelP2); + ResetDuelState(); + +} + +void ResetDuelState() +{ + StopBeaconTimer(); + StopPrepareTimer(); + StopInvulnTimer(); + StopZoneTimer(); + StopZonePreview(); + StopPrepZoneTimer(); + StopDuelLimitTimer(); + + if (g_iDuelP1 > 0) + { + g_iParticipantXP[g_iDuelP1] = 0; + g_bAccepted[g_iDuelP1] = false; + SetDuelProtection(g_iDuelP1, false); + DisableDuelWeaponBypass(g_iDuelP1); + } + if (g_iDuelP2 > 0) + { + g_iParticipantXP[g_iDuelP2] = 0; + g_bAccepted[g_iDuelP2] = false; + SetDuelProtection(g_iDuelP2, false); + DisableDuelWeaponBypass(g_iDuelP2); + } + + g_bDuelActive = false; + g_bDuelPending = false; + g_bDuelPreparing = false; + g_iDuelP1 = 0; + g_iDuelP2 = 0; + g_iDuelController = 0; + g_iPrepareLeft = 0; + g_bDebugSoloMode = false; + strcopy(g_sSelectedWeapon, sizeof(g_sSelectedWeapon), "weapon_ak47"); + g_iDuelModifier = DuelModifier_None; +} + +void StartBeaconTimer() +{ + StopBeaconTimer(); + + if (!gCvarBeacon.BoolValue) + return; + + g_hBeaconTimer = CreateTimer(1.0, Timer_Beacon, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +void StopBeaconTimer() +{ + if (g_hBeaconTimer != null) + { + KillTimer(g_hBeaconTimer); + g_hBeaconTimer = null; + } +} + +void StopPrepareTimer() +{ + if (g_hPrepareTimer != null) + { + KillTimer(g_hPrepareTimer); + g_hPrepareTimer = null; + } +} + +void StopInvulnTimer() +{ + if (g_hInvulnTimer != null) + { + KillTimer(g_hInvulnTimer); + g_hInvulnTimer = null; + } +} + +void StartDuelLimitTimer() +{ + StopDuelLimitTimer(); + int limit = gCvarDuelTimeLimit.IntValue; + if (limit <= 0) + return; + + g_hDuelLimitTimer = CreateTimer(float(limit), Timer_DuelTimeLimit, _, TIMER_FLAG_NO_MAPCHANGE); +} + +void StopDuelLimitTimer() +{ + if (g_hDuelLimitTimer != null) + { + KillTimer(g_hDuelLimitTimer); + g_hDuelLimitTimer = null; + } +} + +public Action Timer_DuelTimeLimit(Handle timer, any data) +{ + g_hDuelLimitTimer = null; + + if (!g_bDuelActive) + return Plugin_Stop; + + PrintToChatAll("\x02[DUELS]\x01 Время дуэли истекло. Ничья!"); + FinishDuelDraw(g_iDuelP1, g_iDuelP2); + return Plugin_Stop; +} + +// ─── Таймер рендера зоны во время подготовки ─────────────────────────────── + +void StartPrepZoneTimer() +{ + StopPrepZoneTimer(); + + if (!g_bArenaZoneReady || g_iBeamSprite == -1) + return; + + g_hPrepZoneTimer = CreateTimer(gCvarZoneRenderInterval.FloatValue, Timer_PrepZoneRender, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +void StopPrepZoneTimer() +{ + if (g_hPrepZoneTimer != null) + { + KillTimer(g_hPrepZoneTimer); + g_hPrepZoneTimer = null; + } +} + +public Action Timer_PrepZoneRender(Handle timer, any data) +{ + if (!g_bDuelPreparing || !g_bArenaZoneReady) + return Plugin_Stop; + + if (IsValidHuman(g_iDuelP1)) + RenderArenaZone(g_iDuelP1); + + if (!g_bDebugSoloMode && IsValidHuman(g_iDuelP2)) + RenderArenaZone(g_iDuelP2); + + return Plugin_Continue; +} + +// ─── Физическая стена зоны ────────────────────────────────────────────────── + +public void OnPlayerPostThink(int client) +{ + if (!g_bDuelActive || !g_bArenaZoneReady) + return; + + if (!IsValidHuman(client) || !IsPlayerAlive(client)) + return; + + if (!IsDuelParticipant(client)) + return; + + float pos[3]; + GetClientAbsOrigin(client, pos); + + // Если далеко внутри зоны — не тратим ресурсы + if (pos[0] > g_vecArenaZoneMin[0] + 10.0 && pos[0] < g_vecArenaZoneMax[0] - 10.0 + && pos[1] > g_vecArenaZoneMin[1] + 10.0 && pos[1] < g_vecArenaZoneMax[1] - 10.0 + && pos[2] < g_vecArenaZoneMax[2] - 10.0) + return; + + float vel[3]; + GetEntPropVector(client, Prop_Data, "m_vecVelocity", vel); + + bool clamped = false; + float margin = 6.0; + + // X + if (pos[0] < g_vecArenaZoneMin[0] + margin) + { + pos[0] = g_vecArenaZoneMin[0] + margin; + if (vel[0] < 0.0) vel[0] = 0.0; + clamped = true; + } + else if (pos[0] > g_vecArenaZoneMax[0] - margin) + { + pos[0] = g_vecArenaZoneMax[0] - margin; + if (vel[0] > 0.0) vel[0] = 0.0; + clamped = true; + } + + // Y + if (pos[1] < g_vecArenaZoneMin[1] + margin) + { + pos[1] = g_vecArenaZoneMin[1] + margin; + if (vel[1] < 0.0) vel[1] = 0.0; + clamped = true; + } + else if (pos[1] > g_vecArenaZoneMax[1] - margin) + { + pos[1] = g_vecArenaZoneMax[1] - margin; + if (vel[1] > 0.0) vel[1] = 0.0; + clamped = true; + } + + // Z потолок (пол — геометрия карты) + if (pos[2] > g_vecArenaZoneMax[2] - margin) + { + pos[2] = g_vecArenaZoneMax[2] - margin; + if (vel[2] > 0.0) vel[2] = 0.0; + clamped = true; + } + + if (clamped) + TeleportEntity(client, pos, NULL_VECTOR, vel); +} + +public Action Timer_DisablePostUnfreezeInvuln(Handle timer, any data) +{ + g_hInvulnTimer = null; + + if (!g_bDuelActive) + return Plugin_Stop; + + if (IsValidHuman(g_iDuelP1)) + SetEntProp(g_iDuelP1, Prop_Data, "m_takedamage", 2); + if (!g_bDebugSoloMode && IsValidHuman(g_iDuelP2)) + SetEntProp(g_iDuelP2, Prop_Data, "m_takedamage", 2); + + if (g_bDebugSoloMode) + PrintToChat(g_iDuelP1, "%s Неуязвимость снята. Соло-дебаг активен.", DUEL_PREFIX); + else + PrintToChatAll("[DUELS] Неуязвимость снята. Бой начался."); + return Plugin_Stop; +} + +public Action Timer_Beacon(Handle timer, any data) +{ + if (!g_bDuelActive || !IsValidHuman(g_iDuelP1) || (g_iDuelP2 != 0 && !IsValidHuman(g_iDuelP2))) + return Plugin_Stop; + + if (g_bDebugSoloMode || g_iDuelP2 == 0) + return Plugin_Continue; + + EmitBeacon(g_iDuelP1, g_iDuelP2); + EmitBeacon(g_iDuelP2, g_iDuelP1); + return Plugin_Continue; +} + +void EmitBeacon(int target, int listener) +{ + float origin[3]; + GetClientAbsOrigin(target, origin); + origin[2] += 10.0; + + if (g_iBeamSprite != -1 && g_iHaloSprite != -1) + { + int color[4] = {255, 0, 0, 255}; + TE_SetupBeamRingPoint(origin, 10.0, 220.0, g_iBeamSprite, g_iHaloSprite, 0, 15, 0.5, 5.0, 0.0, color, 0, 0); + TE_SendToAll(); + } + + if (IsValidHuman(listener)) + EmitSoundToClient(listener, BEACON_SOUND, target, SNDCHAN_AUTO, SNDLEVEL_RAIDSIREN); +} + +void SetDuelProtection(int client, bool enable) +{ + if (!IsValidHuman(client)) + return; + + SetEntityMoveType(client, enable ? MOVETYPE_NONE : MOVETYPE_WALK); + SetEntProp(client, Prop_Data, "m_takedamage", enable ? 0 : 2); +} + +void EnableDuelWeaponBypass(int client) +{ + if (!IsValidHuman(client)) + return; + + if (!g_bSavedUserFlags[client]) + { + g_iSavedUserFlags[client] = GetUserFlagBits(client); + g_bSavedUserFlags[client] = true; + } + + if (gCvarWeaponBypassFlags != null && gCvarWeaponBypassFlags.BoolValue) + SetUserFlagBits(client, g_iSavedUserFlags[client] | ADMFLAG_ROOT); +} + +void DisableDuelWeaponBypass(int client) +{ + if (client <= 0 || client > MaxClients) + return; + + if (!g_bSavedUserFlags[client]) + return; + + if (IsClientInGame(client)) + SetUserFlagBits(client, g_iSavedUserFlags[client]); + + g_iSavedUserFlags[client] = 0; + g_bSavedUserFlags[client] = false; +} + +void ForceEquipDuelWeapon(int client) +{ + if (!IsValidHuman(client) || StrEqual(g_sSelectedWeapon, "weapon_knife", false)) + return; + + int weapon = -1; + for (int slot = 0; slot < 6; slot++) + { + int ent = GetPlayerWeaponSlot(client, slot); + if (ent <= MaxClients || !IsValidEdict(ent)) + continue; + + char current[64]; + GetEdictClassname(ent, current, sizeof(current)); + if (StrEqual(current, g_sSelectedWeapon, false)) + { + weapon = ent; + break; + } + } + + if (weapon > MaxClients && IsValidEdict(weapon)) + EquipPlayerWeapon(client, weapon); +} + +bool PlayerHasWeaponClass(int client, const char[] classname) +{ + if (!IsValidHuman(client)) + return false; + + for (int slot = 0; slot < 6; slot++) + { + int weapon = GetPlayerWeaponSlot(client, slot); + if (weapon <= MaxClients || !IsValidEdict(weapon)) + continue; + + char current[64]; + GetEdictClassname(weapon, current, sizeof(current)); + if (StrEqual(current, classname, false)) + return true; + } + + return false; +} + +void EnsureDuelWeaponPresent(int client) +{ + if (!g_bDuelActive || !IsValidHuman(client) || !IsDuelParticipant(client)) + return; + + EnableDuelWeaponBypass(client); + + if (StrEqual(g_sSelectedWeapon, "weapon_knife", false)) + return; + + if (!PlayerHasWeaponClass(client, g_sSelectedWeapon)) + { + int weapon = GivePlayerItem(client, g_sSelectedWeapon); + if (weapon > MaxClients) + RefillWeaponAmmo(weapon); + } + + ForceEquipDuelWeapon(client); + RefillPlayerAmmo(client); +} + +void EquipPlayerForDuel(int client) +{ + if (!IsValidHuman(client)) + return; + + EnableDuelWeaponBypass(client); + + RemoveAllWeapons(client); + GivePlayerItem(client, "weapon_knife"); + + if (!StrEqual(g_sSelectedWeapon, "weapon_knife")) + { + int weapon = GivePlayerItem(client, g_sSelectedWeapon); + if (weapon > MaxClients) + RefillWeaponAmmo(weapon); + } + + ForceEquipDuelWeapon(client); + RefillPlayerAmmo(client); +} + +stock bool IsWeaponAllowed(const char[] weapon) +{ + if (StrEqual(weapon, "weapon_knife")) + return gCvarAllowKnife.BoolValue; + if (StrEqual(weapon, "weapon_deagle")) + return gCvarAllowDeagle.BoolValue; + if (StrEqual(weapon, "weapon_ak47")) + return gCvarAllowAK47.BoolValue; + if (StrEqual(weapon, "weapon_m4a1")) + return gCvarAllowM4A4.BoolValue; + if (StrEqual(weapon, "weapon_m4a1_silencer")) + return gCvarAllowM4A1S.BoolValue; + if (StrEqual(weapon, "weapon_awp")) + return gCvarAllowAWP.BoolValue; + if (StrEqual(weapon, "weapon_ssg08")) + return gCvarAllowScout.BoolValue; + return false; +} + +void GetDuelWeaponName(char[] buffer, int maxlen) +{ + if (StrEqual(g_sSelectedWeapon, "weapon_knife")) + strcopy(buffer, maxlen, "Ножи"); + else if (StrEqual(g_sSelectedWeapon, "weapon_deagle")) + strcopy(buffer, maxlen, "Deagle"); + else if (StrEqual(g_sSelectedWeapon, "weapon_ak47")) + strcopy(buffer, maxlen, "AK-47"); + else if (StrEqual(g_sSelectedWeapon, "weapon_m4a1")) + strcopy(buffer, maxlen, "M4A4/M4A1-S"); + else if (StrEqual(g_sSelectedWeapon, "weapon_m4a1_silencer")) + strcopy(buffer, maxlen, "M4A1-S"); + else if (StrEqual(g_sSelectedWeapon, "weapon_awp")) + strcopy(buffer, maxlen, "AWP"); + else if (StrEqual(g_sSelectedWeapon, "weapon_ssg08")) + strcopy(buffer, maxlen, "SSG08 / Scout"); + else + strcopy(buffer, maxlen, "Оружие"); +} + +public Action OnTraceAttack(int victim, int &attacker, int &inflictor, float &damage, int &damagetype, int &ammotype, int hitbox, int hitgroup) +{ + if (!g_bDuelActive || g_iDuelModifier != DuelModifier_HeadshotOnly) + return Plugin_Continue; + + if (!IsValidHuman(victim) || !IsValidHuman(attacker)) + return Plugin_Continue; + + if ((victim != g_iDuelP1 && victim != g_iDuelP2) || (attacker != g_iDuelP1 && attacker != g_iDuelP2)) + return Plugin_Continue; + + if (hitgroup != 1) + { + damage = 0.0; + return Plugin_Handled; + } + + return Plugin_Continue; +} + +bool IsGrenadeWeapon(const char[] classname) +{ + return StrEqual(classname, "weapon_hegrenade", false) + || StrEqual(classname, "weapon_flashbang", false) + || StrEqual(classname, "weapon_smokegrenade", false) + || StrEqual(classname, "weapon_molotov", false) + || StrEqual(classname, "weapon_incgrenade", false) + || StrEqual(classname, "weapon_decoy", false); +} + +public Action OnWeaponCanUse(int client, int weapon) +{ + if (!g_bDuelActive) + return Plugin_Continue; + + if (!IsValidHuman(client) || !IsDuelParticipant(client)) + return Plugin_Continue; + + if (weapon <= MaxClients || !IsValidEdict(weapon)) + return Plugin_Handled; + + char classname[64]; + GetEdictClassname(weapon, classname, sizeof(classname)); + + if (StrEqual(classname, "weapon_knife", false)) + return Plugin_Continue; + + if (IsGrenadeWeapon(classname)) + return Plugin_Handled; + + if (StrEqual(g_sSelectedWeapon, "weapon_knife", false)) + return Plugin_Handled; + + if (StrEqual(classname, g_sSelectedWeapon, false)) + return Plugin_Continue; + + return Plugin_Handled; +} + +public Action CS_OnCSWeaponDrop(int client, int weapon) +{ + return OnWeaponCanDrop(client, weapon); +} + +public Action OnWeaponCanDrop(int client, int weapon) +{ + if (!g_bDuelActive) + return Plugin_Continue; + + if (!IsValidHuman(client) || !IsDuelParticipant(client)) + return Plugin_Continue; + + if (weapon <= MaxClients || !IsValidEdict(weapon)) + return Plugin_Handled; + + char classname[64]; + GetEdictClassname(weapon, classname, sizeof(classname)); + + if (StrEqual(classname, g_sSelectedWeapon, false) || StrEqual(classname, "weapon_knife", false)) + return Plugin_Handled; + + return Plugin_Continue; +} + +void SaveLoadout(int client) +{ + if (!IsValidHuman(client)) + return; + + SaveWeaponClass(client, 0, 0); + SaveWeaponClass(client, 1, 1); + SaveWeaponClass(client, 2, 2); + + g_iSavedHealth[client] = GetClientHealth(client); + g_iSavedArmor[client] = GetEntProp(client, Prop_Send, "m_ArmorValue"); + g_iSavedMoney[client] = GetEntProp(client, Prop_Send, "m_iAccount"); +} + +void SaveWeaponClass(int client, int slot, int saveIndex) +{ + int weapon = GetPlayerWeaponSlot(client, slot); + if (weapon > MaxClients && IsValidEdict(weapon)) + GetEdictClassname(weapon, g_sSavedWeapon[client][saveIndex], 32); + else + g_sSavedWeapon[client][saveIndex][0] = '\0'; +} + +void RestoreLoadout(int client) +{ + if (!IsValidHuman(client)) + return; + + DisableDuelWeaponBypass(client); + RemoveAllWeapons(client); + + for (int i = 0; i < 3; i++) + { + if (g_sSavedWeapon[client][i][0] == '\0') + continue; + + int weapon = GivePlayerItem(client, g_sSavedWeapon[client][i]); + if (weapon > MaxClients) + RefillWeaponAmmo(weapon); + } + + 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]); +} + +void RemoveAllWeapons(int client) +{ + for (int slot = 0; slot < 5; slot++) + { + int weapon = GetPlayerWeaponSlot(client, slot); + while (weapon > MaxClients && IsValidEdict(weapon)) + { + RemovePlayerItem(client, weapon); + AcceptEntityInput(weapon, "Kill"); + weapon = GetPlayerWeaponSlot(client, slot); + } + } +} + +void RefillPlayerAmmo(int client) +{ + if (!IsValidHuman(client)) + return; + + for (int slot = 0; slot < 5; slot++) + { + int weapon = GetPlayerWeaponSlot(client, slot); + if (weapon > MaxClients && IsValidEdict(weapon)) + RefillWeaponAmmo(weapon); + } +} + +void RefillWeaponAmmo(int weapon) +{ + if (!IsValidEdict(weapon)) + return; + + char classname[32]; + GetEdictClassname(weapon, classname, sizeof(classname)); + + 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 (HasEntProp(weapon, Prop_Send, "m_iClip1")) + SetEntProp(weapon, Prop_Send, "m_iClip1", clip); + if (HasEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount")) + SetEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount", reserve); + if (HasEntProp(weapon, Prop_Send, "m_iSecondaryReserveAmmoCount")) + SetEntProp(weapon, Prop_Send, "m_iSecondaryReserveAmmoCount", 0); +} + +void GivePlayerMoney(int client, int amount) +{ + if (!IsValidHuman(client)) + return; + + int money = GetEntProp(client, Prop_Send, "m_iAccount") + amount; + + if (money > 16000) + money = 16000; + if (money < 0) + money = 0; + + SetEntProp(client, Prop_Send, "m_iAccount", money); +} + +void GetAliveOneVsOne(int &tAlive, int &ctAlive, int &tClient, int &ctClient) +{ + tAlive = 0; + ctAlive = 0; + tClient = 0; + ctClient = 0; + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsValidHuman(i) || !IsPlayerAlive(i)) + continue; + + int team = GetClientTeam(i); + if (team == 2) + { + tAlive++; + tClient = i; + } + else if (team == 3) + { + ctAlive++; + ctClient = i; + } + } +} + +bool SaveArenaForCurrentMap() +{ + char map[64]; + GetCurrentMap(map, sizeof(map)); + + KeyValues kv = new KeyValues("ArcaneGameDUELS_Arenas"); + kv.ImportFromFile(ARENA_CFG); + + if (!kv.JumpToKey(map, true)) + { + delete kv; + return false; + } + + kv.SetVector("spawn1_origin", g_vecArenaSpawn1); + kv.SetVector("spawn1_angles", g_angArenaSpawn1); + kv.SetVector("spawn2_origin", g_vecArenaSpawn2); + kv.SetVector("spawn2_angles", g_angArenaSpawn2); + kv.SetVector("zone_min", g_vecArenaZoneMin); + kv.SetVector("zone_max", g_vecArenaZoneMax); + kv.SetVector("zone_point1", g_vecManualZonePoint1); + kv.SetVector("zone_point2", g_vecManualZonePoint2); + + kv.Rewind(); + bool ok = kv.ExportToFile(ARENA_CFG); + delete kv; + return ok; +} + +void LoadArenaForCurrentMap() +{ + g_bArenaReady = false; + ZeroVector(g_vecArenaSpawn1); + ZeroVector(g_angArenaSpawn1); + ZeroVector(g_vecArenaSpawn2); + ZeroVector(g_angArenaSpawn2); + ZeroVector(g_vecArenaZoneMin); + ZeroVector(g_vecArenaZoneMax); + ZeroVector(g_vecManualZonePoint1); + ZeroVector(g_vecManualZonePoint2); + g_bManualZonePoint1Set = false; + g_bManualZonePoint2Set = false; + g_bArenaZoneReady = false; + + char map[64]; + GetCurrentMap(map, sizeof(map)); + + KeyValues kv = new KeyValues("ArcaneGameDUELS_Arenas"); + if (!kv.ImportFromFile(ARENA_CFG)) + { + delete kv; + return; + } + + if (!kv.JumpToKey(map, false)) + { + delete kv; + return; + } + + kv.GetVector("spawn1_origin", g_vecArenaSpawn1); + kv.GetVector("spawn1_angles", g_angArenaSpawn1); + kv.GetVector("spawn2_origin", g_vecArenaSpawn2); + kv.GetVector("spawn2_angles", g_angArenaSpawn2); + kv.GetVector("zone_min", g_vecArenaZoneMin); + kv.GetVector("zone_max", g_vecArenaZoneMax); + kv.GetVector("zone_point1", g_vecManualZonePoint1); + kv.GetVector("zone_point2", g_vecManualZonePoint2); + + g_bArenaReady = HasBothArenaSpawns(); + g_bArenaZoneReady = !IsVectorZero(g_vecArenaZoneMin) || !IsVectorZero(g_vecArenaZoneMax); + g_bManualZonePoint1Set = !IsVectorZero(g_vecManualZonePoint1); + g_bManualZonePoint2Set = !IsVectorZero(g_vecManualZonePoint2); + delete kv; +} + +stock void AutoGenerateArenaZone() +{ + if (!HasBothArenaSpawns()) + { + g_bArenaZoneReady = false; + ZeroVector(g_vecArenaZoneMin); + ZeroVector(g_vecArenaZoneMax); + return; + } + + float padXY = gCvarZonePaddingXY.FloatValue; + float padZDown = gCvarZonePaddingZDown.FloatValue; + float padZUp = gCvarZonePaddingZUp.FloatValue; + + g_vecArenaZoneMin[0] = GetMinFloat(g_vecArenaSpawn1[0], g_vecArenaSpawn2[0]) - padXY; + g_vecArenaZoneMin[1] = GetMinFloat(g_vecArenaSpawn1[1], g_vecArenaSpawn2[1]) - padXY; + g_vecArenaZoneMin[2] = GetMinFloat(g_vecArenaSpawn1[2], g_vecArenaSpawn2[2]) - padZDown; + + g_vecArenaZoneMax[0] = GetMaxFloat(g_vecArenaSpawn1[0], g_vecArenaSpawn2[0]) + padXY; + g_vecArenaZoneMax[1] = GetMaxFloat(g_vecArenaSpawn1[1], g_vecArenaSpawn2[1]) + padXY; + g_vecArenaZoneMax[2] = GetMaxFloat(g_vecArenaSpawn1[2], g_vecArenaSpawn2[2]) + padZUp; + g_bArenaZoneReady = true; +} + +void BuildArenaZoneFromManualPoints() +{ + if (!g_bManualZonePoint1Set || !g_bManualZonePoint2Set) + { + g_bArenaZoneReady = false; + ZeroVector(g_vecArenaZoneMin); + ZeroVector(g_vecArenaZoneMax); + return; + } + + g_vecArenaZoneMin[0] = GetMinFloat(g_vecManualZonePoint1[0], g_vecManualZonePoint2[0]); + g_vecArenaZoneMin[1] = GetMinFloat(g_vecManualZonePoint1[1], g_vecManualZonePoint2[1]); + g_vecArenaZoneMin[2] = GetMinFloat(g_vecManualZonePoint1[2], g_vecManualZonePoint2[2]); + + g_vecArenaZoneMax[0] = GetMaxFloat(g_vecManualZonePoint1[0], g_vecManualZonePoint2[0]); + g_vecArenaZoneMax[1] = GetMaxFloat(g_vecManualZonePoint1[1], g_vecManualZonePoint2[1]); + g_vecArenaZoneMax[2] = g_vecArenaZoneMin[2] + 200.0; + + g_bArenaZoneReady = true; +} + +void StartZonePreview(int viewer, float duration) +{ + if (!g_bArenaZoneReady || g_iBeamSprite == -1) + return; + + StopZonePreview(); + RenderArenaZone(viewer); + g_hZonePreviewTimer = CreateTimer(gCvarZoneRenderInterval.FloatValue, Timer_ZonePreview, GetClientUserId(viewer), TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + CreateTimer(duration, Timer_StopZonePreview, _, TIMER_FLAG_NO_MAPCHANGE); +} + +void StopZonePreview() +{ + if (g_hZonePreviewTimer != null) + { + KillTimer(g_hZonePreviewTimer); + g_hZonePreviewTimer = null; + } +} + +void StartZoneTimer() +{ + StopZoneTimer(); + + if (!gCvarZoneEnable.BoolValue || !g_bArenaZoneReady) + return; + + for (int i = 1; i <= MaxClients; i++) + { + g_bZoneOutside[i] = false; + g_iZoneGraceLeft[i] = 0; + } + + g_hZoneTimer = CreateTimer(gCvarZoneRenderInterval.FloatValue, Timer_CheckDuelZone, _, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +void StopZoneTimer() +{ + if (g_hZoneTimer != null) + { + KillTimer(g_hZoneTimer); + g_hZoneTimer = null; + } + + for (int i = 1; i <= MaxClients; i++) + { + g_bZoneOutside[i] = false; + g_iZoneGraceLeft[i] = 0; + } +} + +public Action Timer_ZonePreview(Handle timer, any userid) +{ + int client = GetClientOfUserId(userid); + if (!IsValidHuman(client) || !g_bArenaZoneReady) + return Plugin_Stop; + + RenderArenaZone(client); + return Plugin_Continue; +} + +public Action Timer_StopZonePreview(Handle timer, any data) +{ + StopZonePreview(); + return Plugin_Stop; +} + +public Action Timer_CheckDuelZone(Handle timer, any data) +{ + if (!g_bDuelActive || !g_bArenaZoneReady) + return Plugin_Stop; + + if (IsValidHuman(g_iDuelP1)) + { + EnsureDuelWeaponPresent(g_iDuelP1); + RenderArenaZone(g_iDuelP1); + CheckClientZoneState(g_iDuelP1, g_iDuelP2); + } + + if (!g_bDebugSoloMode && IsValidHuman(g_iDuelP2)) + { + EnsureDuelWeaponPresent(g_iDuelP2); + RenderArenaZone(g_iDuelP2); + CheckClientZoneState(g_iDuelP2, g_iDuelP1); + } + + return Plugin_Continue; +} + +void CheckClientZoneState(int client, int opponent) +{ + if (!IsValidHuman(client) || !IsPlayerAlive(client)) + return; + + if (IsClientInsideArenaZone(client)) + { + if (g_bZoneOutside[client]) + { + g_bZoneOutside[client] = false; + g_iZoneGraceLeft[client] = 0; + PrintCenterText(client, "Ты вернулся в зону дуэли"); + } + return; + } + + if (!g_bZoneOutside[client]) + { + g_bZoneOutside[client] = true; + g_iZoneGraceLeft[client] = gCvarZoneGrace.IntValue; + } + else if (g_iZoneGraceLeft[client] > 0) + { + g_iZoneGraceLeft[client]--; + } + + EmitSoundToClient(client, DUEL_OUT_SOUND, client, SNDCHAN_AUTO, SNDLEVEL_RAIDSIREN); + + if (g_iZoneGraceLeft[client] > 0) + { + PrintCenterText(client, "Вернись в зону дуэли: %d", g_iZoneGraceLeft[client]); + return; + } + + if (g_bDebugSoloMode || opponent == 0 || !IsValidHuman(opponent)) + { + PrintToChat(client, "%s Ты не вернулся в зону вовремя. Соло-дебаг остановлен.", DUEL_PREFIX); + RestoreLoadout(client); + ResetDuelState(); + return; + } + + ForcePlayerSuicide(client); + PrintToChatAll("%s %N покинул зону дуэли и проиграл.", DUEL_PREFIX, client); + FinishDuel(opponent, client); +} + +bool IsClientInsideArenaZone(int client) +{ + float pos[3]; + GetClientAbsOrigin(client, pos); + + return (pos[0] >= g_vecArenaZoneMin[0] && pos[0] <= g_vecArenaZoneMax[0] + && pos[1] >= g_vecArenaZoneMin[1] && pos[1] <= g_vecArenaZoneMax[1] + && pos[2] >= g_vecArenaZoneMin[2] && pos[2] <= g_vecArenaZoneMax[2]); +} + +void RenderArenaZone(int client) +{ + if (!IsValidHuman(client) || !g_bArenaZoneReady || g_iBeamSprite == -1) + return; + + float p1[3], p2[3], p3[3], p4[3], p5[3], p6[3], p7[3], p8[3]; + p1[0] = g_vecArenaZoneMin[0]; p1[1] = g_vecArenaZoneMin[1]; p1[2] = g_vecArenaZoneMin[2]; + p2[0] = g_vecArenaZoneMax[0]; p2[1] = g_vecArenaZoneMin[1]; p2[2] = g_vecArenaZoneMin[2]; + p3[0] = g_vecArenaZoneMax[0]; p3[1] = g_vecArenaZoneMax[1]; p3[2] = g_vecArenaZoneMin[2]; + p4[0] = g_vecArenaZoneMin[0]; p4[1] = g_vecArenaZoneMax[1]; p4[2] = g_vecArenaZoneMin[2]; + p5[0] = g_vecArenaZoneMin[0]; p5[1] = g_vecArenaZoneMin[1]; p5[2] = g_vecArenaZoneMax[2]; + p6[0] = g_vecArenaZoneMax[0]; p6[1] = g_vecArenaZoneMin[1]; p6[2] = g_vecArenaZoneMax[2]; + p7[0] = g_vecArenaZoneMax[0]; p7[1] = g_vecArenaZoneMax[1]; p7[2] = g_vecArenaZoneMax[2]; + p8[0] = g_vecArenaZoneMin[0]; p8[1] = g_vecArenaZoneMax[1]; p8[2] = g_vecArenaZoneMax[2]; + + int color[4] = {160, 60, 255, 255}; + + // 12 рёбер каркаса + DrawZoneEdge(client, p1, p2, color); + DrawZoneEdge(client, p2, p3, color); + DrawZoneEdge(client, p3, p4, color); + DrawZoneEdge(client, p4, p1, color); + DrawZoneEdge(client, p5, p6, color); + DrawZoneEdge(client, p6, p7, color); + DrawZoneEdge(client, p7, p8, color); + DrawZoneEdge(client, p8, p5, color); + DrawZoneEdge(client, p1, p5, color); + DrawZoneEdge(client, p2, p6, color); + DrawZoneEdge(client, p3, p7, color); + DrawZoneEdge(client, p4, p8, color); + + // Полная сетка на 4 боковых гранях + float step = 40.0; + float xMin = g_vecArenaZoneMin[0], xMax = g_vecArenaZoneMax[0]; + float yMin = g_vecArenaZoneMin[1], yMax = g_vecArenaZoneMax[1]; + float zMin = g_vecArenaZoneMin[2], zMax = g_vecArenaZoneMax[2]; + + float a[3], b[3]; + + // Грань: y = yMin (горизонтальные + вертикальные) + for (float z = zMin + step; z < zMax; z += step) + { + a[0] = xMin; a[1] = yMin; a[2] = z; + b[0] = xMax; b[1] = yMin; b[2] = z; + DrawZoneEdge(client, a, b, color); + } + for (float x = xMin + step; x < xMax; x += step) + { + a[0] = x; a[1] = yMin; a[2] = zMin; + b[0] = x; b[1] = yMin; b[2] = zMax; + DrawZoneEdge(client, a, b, color); + } + + // Грань: y = yMax + for (float z = zMin + step; z < zMax; z += step) + { + a[0] = xMin; a[1] = yMax; a[2] = z; + b[0] = xMax; b[1] = yMax; b[2] = z; + DrawZoneEdge(client, a, b, color); + } + for (float x = xMin + step; x < xMax; x += step) + { + a[0] = x; a[1] = yMax; a[2] = zMin; + b[0] = x; b[1] = yMax; b[2] = zMax; + DrawZoneEdge(client, a, b, color); + } + + // Грань: x = xMin + for (float z = zMin + step; z < zMax; z += step) + { + a[0] = xMin; a[1] = yMin; a[2] = z; + b[0] = xMin; b[1] = yMax; b[2] = z; + DrawZoneEdge(client, a, b, color); + } + for (float y = yMin + step; y < yMax; y += step) + { + a[0] = xMin; a[1] = y; a[2] = zMin; + b[0] = xMin; b[1] = y; b[2] = zMax; + DrawZoneEdge(client, a, b, color); + } + + // Грань: x = xMax + for (float z = zMin + step; z < zMax; z += step) + { + a[0] = xMax; a[1] = yMin; a[2] = z; + b[0] = xMax; b[1] = yMax; b[2] = z; + DrawZoneEdge(client, a, b, color); + } + for (float y = yMin + step; y < yMax; y += step) + { + a[0] = xMax; a[1] = y; a[2] = zMin; + b[0] = xMax; b[1] = y; b[2] = zMax; + DrawZoneEdge(client, a, b, color); + } +} + +void DrawZoneEdge(int client, const float start[3], const float end[3], const int color[4]) +{ + TE_SetupBeamPoints(start, end, g_iBeamSprite, g_iHaloSprite, 0, 0, gCvarZoneRenderInterval.FloatValue + 0.05, 2.0, 2.0, 0, 0.0, color, 0); + TE_SendToClient(client); +} + +bool HasBothArenaSpawns() +{ + return !IsVectorZero(g_vecArenaSpawn1) && !IsVectorZero(g_vecArenaSpawn2); +} + +float GetMinFloat(float a, float b) +{ + return (a < b) ? a : b; +} + +float GetMaxFloat(float a, float b) +{ + return (a > b) ? a : b; +} + +bool IsVectorZero(const float vec[3]) +{ + return (vec[0] == 0.0 && vec[1] == 0.0 && vec[2] == 0.0); +} + +void ZeroVector(float vec[3]) +{ + vec[0] = 0.0; + vec[1] = 0.0; + vec[2] = 0.0; +} + +stock int GetHumanPlayersCount() +{ + int count = 0; + for (int i = 1; i <= MaxClients; i++) + { + if (IsValidHuman(i)) + count++; + } + return count; +} + +int GetPlayingHumanPlayersCount() +{ + int count = 0; + for (int i = 1; i <= MaxClients; i++) + { + if (!IsValidHuman(i)) + continue; + + int team = GetClientTeam(i); + if (team == 2 || team == 3) + count++; + } + return count; +} + +stock int GetRandomOtherPlayer(int client) +{ + for (int i = 1; i <= MaxClients; i++) + { + if (i == client) + continue; + if (IsValidHuman(i)) + return i; + } + return 0; +} + +bool IsValidClient(int client) +{ + return (client > 0 && client <= MaxClients); +} + +bool IsDuelParticipant(int client) +{ + return (client == g_iDuelP1 || client == g_iDuelP2); +} + +bool IsValidHuman(int client) +{ + return IsValidClient(client) && IsClientInGame(client) && !IsFakeClient(client); +} + +stock bool IsLRReady() +{ + return LR_IsLoaded(); +} + +stock void LR_GiveXP_Safe(int client, int amount) +{ + if (amount <= 0 || !IsLRReady() || !IsValidHuman(client) || !LR_GetClientStatus(client)) + return; + + LR_ChangeClientValue(client, amount); +} + +public any Native_AGD_IsDuelActive(Handle plugin, int numParams) +{ + return g_bDuelActive; +} + +public any Native_AGD_GetDuelPlayers(Handle plugin, int numParams) +{ + SetNativeCellRef(1, g_iDuelP1); + SetNativeCellRef(2, g_iDuelP2); + return (g_bDuelActive || g_bDuelPending || g_bDuelPreparing); +} + +public any Native_AGD_GetDuelParticipantXP(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + if (!IsValidClient(client)) + return 0; + return g_iParticipantXP[client]; +} + +public any Native_AGD_GetWinnerMoneyReward(Handle plugin, int numParams) +{ + return gCvarWinMoney.IntValue; +} + +public any Native_AGD_IsClientInDuel(Handle plugin, int numParams) +{ + int client = GetNativeCell(1); + if (client < 1 || client > MaxClients) + return false; + + return g_bDuelActive && (client == g_iDuelP1 || client == g_iDuelP2); +} + +public any Native_AGD_IsArenaReady(Handle plugin, int numParams) +{ + return g_bArenaReady; +} + +public any Native_AGD_IsBettingOpen(Handle plugin, int numParams) +{ + return g_bDuelPreparing; +}