d933484514
ИСПРАВЛЕНИЯ: - Полное сохранение/восстановление инвентаря (гранаты, шлем, дефузер, патроны в клипе и резерве — раньше всё это терялось) - RefillWeaponAmmo расширен на 25+ оружий - Убран двойной reset state в Event_RoundStart НОВЫЙ ФУНКЦИОНАЛ: - Случайное оружие в weapon menu - sm_duel_forceend — админ может принудительно завершить дуэль - Файловый лог addons/sourcemod/logs/arcane_duels.log - Статистика per-игрок (wins/losses/draws/rage-quits/total XP) в addons/sourcemod/data/arcane_duels_stats.cfg - Команды игрока: !duelstats и !duel_top - Rage-quit penalty: XP штраф + +1 поражение в статистике - Звуковые эффекты на окончании дуэли (победа/поражение) - ConVar agd_duel_block_external для интеграции с custom-rounds и другими плагинами КОСМЕТИКА: - Жёлтый префикс [DUELS] (был красный) - Author = deidara.dev
2739 lines
84 KiB
SourcePawn
2739 lines
84 KiB
SourcePawn
#pragma semicolon 1
|
|
#pragma newdecls required
|
|
|
|
#include <sourcemod>
|
|
#include <sdktools>
|
|
#include <sdkhooks>
|
|
#include <cstrike>
|
|
#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<DuelModifier>(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;
|
|
}
|