#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 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_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 = "deidara.dev", description = "1v1 дуэли с ареной, зоной, выбором оружия, статистикой и логированием", version = "1.6.0", url = "https://deidara.dev" }; 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_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; 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); 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"); 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); 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); 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); RegAdminCmd("sm_duel_forceend", Command_ForceEnd, 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(); g_smStats = new StringMap(); LoadDuelStats(); for (int i = 1; i <= MaxClients; i++) { if (IsClientInGame(i)) OnClientPutInServer(i); } } public void OnMapEnd() { SaveDuelStats(); } 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; // Rage-quit penalty: записываем поражение и снимаем XP, если cvar > 0 ApplyRageQuitPenalty(loser); 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(""); // ResetDuelState уже вызывается внутри 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; // Внешняя блокировка (например custom-rounds может выставить cvar в 1) if (gCvarBlockExternal != null && gCvarBlockExternal.BoolValue) 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("\x09[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, "\x09[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("\x09[DUELS]\x01 Дуэль принята. Настройки выбирает случайный игрок: \x03%N", g_iDuelController); PrintToChatAll("\x09[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 > 1) menu.AddItem("__random__", "Случайное оружие"); if (added <= 0) menu.AddItem("blocked", "Нет доступного оружия", ITEMDRAW_DISABLED); menu.ExitButton = false; 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) { 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)); 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")) 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("\x09[DUELS]\x01 Выбрана дуэль: \x03%s\x01 | режим: \x05Только в голову", weaponName); else PrintToChatAll("\x09[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("\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); 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("\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); 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); // Лог + статистика 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); 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("\x09[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("\x09[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("\x09[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; 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 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)) 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) { 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 && 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) { 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_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); 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; } // ============================================================================ // 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; }