2026-05-01 06:57:31 +03:00
# 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"
2026-05-01 20:39:01 +03:00
# define STATS_CFG "data / arcane_duels_stats.cfg"
# define DUEL_LOG_FILE "addons / sourcemod / logs / arcane_duels.log"
2026-05-01 06:57:31 +03:00
# define BEACON_SOUND "buttons / blip1.wav"
# define DUEL_OUT_SOUND "buttons / button10.wav"
2026-05-01 20:39:01 +03:00
# 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
2026-05-01 06:57:31 +03:00
public Plugin myinfo =
{
name = " [CORE] ArcaneGameDUELS Core " ,
2026-05-01 20:39:01 +03:00
author = " deidara.dev " ,
description = " 1v1 дуэли с ареной, зоной, выбором оружия, статистикой и логированием " ,
version = " 1.6.0 " ,
url = " https://deidara.dev "
2026-05-01 06:57:31 +03:00
} ;
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 ] ;
2026-05-01 20:39:01 +03:00
int g_iSavedClip [ MAXPLAYERS + 1 ] [ 3 ] ;
int g_iSavedReserve [ MAXPLAYERS + 1 ] [ 3 ] ;
2026-05-01 06:57:31 +03:00
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 ] ;
2026-05-01 20:39:01 +03:00
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 ;
2026-05-01 06:57:31 +03:00
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 ) ;
2026-05-01 20:39:01 +03:00
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 ) ;
2026-05-01 06:57:31 +03:00
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 ) ;
2026-05-01 20:39:01 +03:00
RegConsoleCmd ( " sm_duelstats " , Command_DuelStats , " Показать свою статистику дуэлей " ) ;
RegConsoleCmd ( " sm_duel_top " , Command_DuelTop , " Топ-10 игроков по победам " ) ;
2026-05-01 06:57:31 +03:00
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 ) ;
2026-05-01 20:39:01 +03:00
RegAdminCmd ( " sm_duel_forceend " , Command_ForceEnd , ADMFLAG_ROOT , " Принудительно завершить активную/готовящуюся дуэль " ) ;
2026-05-01 06:57:31 +03:00
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 ( ) ;
2026-05-01 20:39:01 +03:00
g_smStats = new StringMap ( ) ;
LoadDuelStats ( ) ;
2026-05-01 06:57:31 +03:00
for ( int i = 1 ; i < = MaxClients ; i + + )
{
if ( IsClientInGame ( i ) )
OnClientPutInServer ( i ) ;
}
}
2026-05-01 20:39:01 +03:00
public void OnMapEnd ( )
{
SaveDuelStats ( ) ;
}
2026-05-01 06:57:31 +03:00
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 ;
2026-05-01 20:39:01 +03:00
// Rage-quit penalty: записываем поражение и снимаем XP, если cvar > 0
ApplyRageQuitPenalty ( loser ) ;
2026-05-01 06:57:31 +03:00
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 ( " " ) ;
2026-05-01 20:39:01 +03:00
// ResetDuelState уже вызывается внутри CancelPreparingDuel; повторный вызов оставляем
// только если дуэль реально активна (не в подготовке)
2026-05-01 06:57:31 +03:00
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 ;
2026-05-01 20:39:01 +03:00
// Внешняя блокировка (например custom-rounds может выставить cvar в 1)
if ( gCvarBlockExternal ! = null & & gCvarBlockExternal . BoolValue )
return Plugin_Stop ;
2026-05-01 06:57:31 +03:00
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 ;
2026-05-01 20:39:01 +03:00
PrintToChatAll ( " \x09 [DUELS] \x01 Остались 1 на 1: \x03 %N \x01 vs \x03 %N \x01 . Ожидается подтверждение дуэли. " , client1 , client2 ) ;
2026-05-01 06:57:31 +03:00
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 ;
2026-05-01 20:39:01 +03:00
PrintToChat ( client , " \x09 [DUELS] \x01 Ты принял дуэль. " ) ;
2026-05-01 06:57:31 +03:00
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 ( ) ;
2026-05-01 20:39:01 +03:00
PrintToChatAll ( " \x09 [DUELS] \x01 Дуэль принята. Настройки выбирает случайный игрок: \x03 %N " , g_iDuelController ) ;
PrintToChatAll ( " \x09 [DUELS] \x01 Подготовка к дуэли: \x05 %d \x01 сек. В это время доступны ставки. " , g_iPrepareLeft ) ;
2026-05-01 06:57:31 +03:00
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 + + ; }
2026-05-01 20:39:01 +03:00
if ( added > 1 )
menu . AddItem ( " __random__ " , " Случайное оружие " ) ;
2026-05-01 06:57:31 +03:00
if ( added < = 0 )
menu . AddItem ( " blocked " , " Нет доступного оружия " , ITEMDRAW_DISABLED ) ;
menu . ExitButton = false ;
menu . Display ( client , 10 ) ;
}
2026-05-01 20:39:01 +03:00
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 ) ] ) ;
}
2026-05-01 06:57:31 +03:00
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 ) ) ;
2026-05-01 20:39:01 +03:00
if ( StrEqual ( info , " __random__ " ) )
{
PickRandomAllowedWeapon ( g_sSelectedWeapon , sizeof ( g_sSelectedWeapon ) ) ;
}
else
{
strcopy ( g_sSelectedWeapon , sizeof ( g_sSelectedWeapon ) , info ) ;
}
2026-05-01 06:57:31 +03:00
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 )
2026-05-01 20:39:01 +03:00
PrintToChatAll ( " \x09 [DUELS] \x01 Выбрана дуэль: \x03 %s \x01 | режим: \x05 Только в голову " , weaponName ) ;
2026-05-01 06:57:31 +03:00
else
2026-05-01 20:39:01 +03:00
PrintToChatAll ( " \x09 [DUELS] \x01 Выбрана дуэль: \x03 %s \x01 | режим: \x05 Обычный " , weaponName ) ;
2026-05-01 06:57:31 +03:00
}
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 ) ) ;
2026-05-01 20:39:01 +03:00
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 ) ;
2026-05-01 06:57:31 +03:00
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 ;
}
}
2026-05-01 20:39:01 +03:00
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 ) ;
2026-05-01 06:57:31 +03:00
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 ) ;
2026-05-01 20:39:01 +03:00
// Лог + статистика
LogDuelAction ( " DRAW: %N vs %N | weapon=%s " ,
IsValidClient ( client1 ) ? client1 : 0 , IsValidClient ( client2 ) ? client2 : 0 , g_sSelectedWeapon ) ;
RecordDuelDraw ( client1 , client2 ) ;
2026-05-01 06:57:31 +03:00
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' )
2026-05-01 20:39:01 +03:00
PrintToChatAll ( " \x09 [DUELS] \x01 %s " , reason ) ;
2026-05-01 06:57:31 +03:00
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' )
2026-05-01 20:39:01 +03:00
PrintToChatAll ( " \x09 [DUELS] \x01 %s " , reason ) ;
2026-05-01 06:57:31 +03:00
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 ;
2026-05-01 20:39:01 +03:00
PrintToChatAll ( " \x09 [DUELS] \x01 Время дуэли истекло. Ничья! " ) ;
2026-05-01 06:57:31 +03:00
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 ;
2026-05-01 20:39:01 +03:00
SaveWeaponClassAndAmmo ( client , 0 , 0 ) ;
SaveWeaponClassAndAmmo ( client , 1 , 1 ) ;
SaveWeaponClassAndAmmo ( client , 2 , 2 ) ;
SaveAllGrenadesForDuel ( client ) ;
2026-05-01 06:57:31 +03:00
g_iSavedHealth [ client ] = GetClientHealth ( client ) ;
g_iSavedArmor [ client ] = GetEntProp ( client , Prop_Send , " m_ArmorValue " ) ;
g_iSavedMoney [ client ] = GetEntProp ( client , Prop_Send , " m_iAccount " ) ;
2026-05-01 20:39:01 +03:00
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 ;
2026-05-01 06:57:31 +03:00
}
2026-05-01 20:39:01 +03:00
void SaveWeaponClassAndAmmo ( int client , int slot , int saveIndex )
2026-05-01 06:57:31 +03:00
{
2026-05-01 20:39:01 +03:00
g_sSavedWeapon [ client ] [ saveIndex ] [ 0 ] = '\0' ;
g_iSavedClip [ client ] [ saveIndex ] = 0 ;
g_iSavedReserve [ client ] [ saveIndex ] = 0 ;
2026-05-01 06:57:31 +03:00
int weapon = GetPlayerWeaponSlot ( client , slot ) ;
2026-05-01 20:39:01 +03:00
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 ] + + ;
}
}
2026-05-01 06:57:31 +03:00
}
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 ] ) ;
2026-05-01 20:39:01 +03:00
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 ] ) ;
2026-05-01 06:57:31 +03:00
}
2026-05-01 20:39:01 +03:00
g_iSavedGrenadeCount [ client ] = 0 ;
2026-05-01 06:57:31 +03:00
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 ] ) ;
2026-05-01 20:39:01 +03:00
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 ) ;
2026-05-01 06:57:31 +03:00
}
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 ;
2026-05-01 20:39:01 +03:00
// Пистолеты
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 ; }
2026-05-01 06:57:31 +03:00
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 ;
}
2026-05-01 20:39:01 +03:00
// ============================================================================
// 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 ;
}