diff --git a/README.md b/README.md index 21b067d..2a556f6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Функции -- 10 режимов кастомных раундов: +- 11 режимов кастомных раундов: - **AWP Only** — только AWP + нож - **AWP NoScope** — AWP без прицела (zoom заблокирован) - **Scout Only** — только SSG-08 + нож @@ -15,6 +15,7 @@ - **Ножевой раунд** — только ножи - **Низкая гравитация** — гравитация уменьшена (настраивается) - **1 HP** — у всех игроков минимальное здоровье + - **1 vs All** (от 15 игроков) — 1 ТТшник против всех CT. У ТТшника 5000 HP, 500 брони, +25% скорости, $16000 на покупку любого оружия. CT — XM1014 + 4 гранаты (HE, флеш, смок, инсентари). Админ выбирает игрока вручную или рандомно. - Полное сохранение и восстановление инвентаря после кастомного раунда (основное оружие + патроны, запасное + патроны, нож, **все гранаты**, **броня**, **шлем**, **дефузер**) - Блокировка покупок во время режима - Блокировка зума для NoScope-режимов (через CommandListener + OnPlayerRunCmd) @@ -96,10 +97,20 @@ addons/sourcemod/logs/custom_rounds.log ## Версия -`1.2.4` — Автор: deidara.dev +`1.3.0` — Автор: deidara.dev ### Changelog +- **1.3.0** + - Добавлен режим **1 vs All** (требуется 15+ активных игроков): + - 1 ТТшник против всех CT + - Админ выбирает игрока вручную через подменю или рандомно + - У ТТшника: 5000 HP, 500 брони, +25% скорости, $16000 (свободная покупка) + - У CT: XM1014 + HE + флеш + смок + incgrenade, покупка отключена + - Если ТТшник умрёт — CT побеждают (стандартная логика CSGO) + - Если ТТшник вышел — раунд завершается ничьёй (`CS_TerminateRound`), оружие восстанавливается + - После раунда команды восстанавливаются в исходные (CT возвращаются к T если были T до 1vAll) + - Если игроков стало <15 в момент старта — режим автоматически отменяется с уведомлением в чат - **1.2.4** - Убраны `ShowMOTDPanel` и `PrintCenterText` (на CSGO/MyArena не показывались стабильно) - Оставлен только `PrintHintText` — он работает у всех игроков diff --git a/scripting/ArcaneGame_CustomRounds_Core.sp b/scripting/ArcaneGame_CustomRounds_Core.sp index 623a39a..ed43cd1 100644 --- a/scripting/ArcaneGame_CustomRounds_Core.sp +++ b/scripting/ArcaneGame_CustomRounds_Core.sp @@ -10,6 +10,7 @@ #define CR_PREFIX "\x04[ArcaneGame CR]\x01" #define CR_REASON_MAX 128 #define MAX_SAVED_GRENADES 6 +#define MIN_PLAYERS_ONEVSALL 15 enum CustomRoundType { @@ -23,7 +24,8 @@ enum CustomRoundType CR_Deagle, CR_DeagleHS, CR_LowGravity, - CR_OneHP + CR_OneHP, + CR_OneVsAll }; public Plugin myinfo = @@ -31,7 +33,7 @@ public Plugin myinfo = name = "ArcaneGame Custom Rounds Core", author = "deidara.dev", description = "Core plugin for custom rounds with AG Coin integration", - version = "1.2.4", + version = "1.3.0", url = "https://deidara.dev" }; @@ -68,6 +70,12 @@ bool g_SavedDefuser[MAXPLAYERS + 1]; int g_RoundCounter = 0; int g_LastCustomRoundNumber = -1000; +// 1vAll state +int g_OneVsAllChoice = 0; // userid выбранного админом T (0 = рандом) +int g_OneVsAllTUserId = 0; // userid реально выбранного T в текущем раунде +int g_OriginalTeam[MAXPLAYERS + 1]; // команда игрока до 1vAll +bool g_PendingTeamRestore = false; // нужно восстановить команды в начале следующего раунда + ConVar gCvarAccessFlag; ConVar gCvarAccessUseOverrides; ConVar gCvarCoinsEnable; @@ -132,6 +140,20 @@ public void OnClientPutInServer(int client) public void OnClientDisconnect(int client) { + // Если ТТшник вышел во время 1vAll — раунд заканчивается ничьёй + if (g_CurrentRound == CR_OneVsAll && g_RoundLive + && client > 0 && client <= MaxClients + && GetClientUserId(client) == g_OneVsAllTUserId) + { + char name[MAX_NAME_LENGTH]; + GetClientName(client, name, sizeof(name)); + PrintToChatAll("%s \x02ТТшник \x03%s\x02 вышел — раунд завершён ничьёй, оружие будет восстановлено.", CR_PREFIX, name); + LogCRAction(0, "1vAll: ТТшник %s вышел, раунд завершён ничьёй", name); + + g_OneVsAllTUserId = 0; + CS_TerminateRound(0.5, CSRoundEnd_RoundDraw, false); + } + FullResetClientState(client); } @@ -145,6 +167,7 @@ void FullResetClientState(int client) g_PlayerParticipated[client] = false; g_PlayerKills[client] = 0; g_PlayerHeadshots[client] = 0; + g_OriginalTeam[client] = 0; g_LoadoutSaved[client] = false; g_RestoreLoadoutOnSpawn[client] = false; @@ -367,6 +390,7 @@ void OpenMainMenu(int client) menu.AddItem("8", "Ножевой раунд"); menu.AddItem("9", "Низкая гравитация"); menu.AddItem("10", "Режим 1 HP"); + menu.AddItem("11", "1 vs All (от 15 игроков)"); menu.AddItem("c", "Отменить следующий кастомный раунд"); menu.ExitButton = true; @@ -403,6 +427,14 @@ public int MenuHandler_Main(Menu menu, MenuAction action, int client, int item) } int value = StringToInt(info); + + // 1 vs All — открывает подменю выбора игрока + if (value == 11) + { + ShowOneVsAllMenu(client); + return 0; + } + CustomRoundType selected = CR_None; switch (value) @@ -427,6 +459,95 @@ public int MenuHandler_Main(Menu menu, MenuAction action, int client, int item) return 0; } +void ShowOneVsAllMenu(int client) +{ + if (!HasCustomRoundsAccess(client)) + { + return; + } + + int activeCount = CountActivePlayers(); + if (activeCount < MIN_PLAYERS_ONEVSALL) + { + PrintToChat(client, "%s \x02Для режима 1 vs All нужно \x04%d\x01\x02 игроков. Сейчас: \x04%d\x01\x02.", + CR_PREFIX, MIN_PLAYERS_ONEVSALL, activeCount); + return; + } + + Menu menu = new Menu(MenuHandler_OneVsAll); + + char title[128]; + Format(title, sizeof(title), "1 vs All\nВыбор ТТшника (игроков: %d)", activeCount); + menu.SetTitle(title); + + menu.AddItem("rand", "Случайный игрок"); + + char info[16], name[MAX_NAME_LENGTH]; + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + if (GetClientTeam(i) < CS_TEAM_T) + { + continue; + } + + Format(info, sizeof(info), "p:%d", GetClientUserId(i)); + GetClientName(i, name, sizeof(name)); + menu.AddItem(info, name); + } + + menu.ExitButton = true; + menu.ExitBackButton = true; + menu.Display(client, 30); +} + +public int MenuHandler_OneVsAll(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + return 0; + } + + if (action == MenuAction_Cancel) + { + if (item == MenuCancel_ExitBack) + { + OpenMainMenu(client); + } + return 0; + } + + if (action != MenuAction_Select) + { + return 0; + } + + if (!HasCustomRoundsAccess(client)) + { + return 0; + } + + char info[16]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "rand")) + { + g_OneVsAllChoice = 0; // 0 = рандом при старте раунда + } + else + { + // p: + g_OneVsAllChoice = StringToInt(info[2]); + } + + QueueCustomRound(client, CR_OneVsAll); + return 0; +} + void QueueCustomRound(int client, CustomRoundType roundType) { if (!HasCustomRoundsAccess(client)) @@ -442,7 +563,8 @@ void QueueCustomRound(int client, CustomRoundType roundType) return; } - if (g_PendingRound == roundType) + // Для CR_OneVsAll разрешаем перевыбор (admin может менять цель) + if (g_PendingRound == roundType && roundType != CR_OneVsAll) { PrintToChat(client, "%s \x02Этот режим уже в очереди.", CR_PREFIX); return; @@ -496,8 +618,42 @@ public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) ResetAllPlayerStats(); + // Восстанавливаем команды после предыдущего 1vAll (до спавна игроков) + if (g_PendingTeamRestore) + { + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + + if (g_OriginalTeam[i] >= CS_TEAM_T && GetClientTeam(i) != g_OriginalTeam[i]) + { + CS_SwitchTeam(i, g_OriginalTeam[i]); + } + g_OriginalTeam[i] = 0; + } + g_PendingTeamRestore = false; + g_OneVsAllTUserId = 0; + } + if (g_PendingRound != CR_None) { + // Валидация для 1vAll: нужно минимум 15 активных игроков + if (g_PendingRound == CR_OneVsAll) + { + int activeCount = CountActivePlayers(); + if (activeCount < MIN_PLAYERS_ONEVSALL) + { + PrintToChatAll("%s \x02Режим 1 vs All отменён: нужно минимум \x04%d\x01\x02 игроков (сейчас \x04%d\x01\x02).", + CR_PREFIX, MIN_PLAYERS_ONEVSALL, activeCount); + LogCRAction(0, "1vAll отменён (игроков %d < %d)", activeCount, MIN_PLAYERS_ONEVSALL); + g_PendingRound = CR_None; + return; + } + } + g_CurrentRound = g_PendingRound; g_PendingRound = CR_None; g_LastCustomRoundNumber = g_RoundCounter; @@ -661,6 +817,12 @@ public Action Timer_ApplyRoundMode(Handle timer) g_ModeApplied = true; ApplyGlobalModeSettings(true); + if (g_CurrentRound == CR_OneVsAll) + { + SetupOneVsAll(); + return Plugin_Stop; + } + for (int client = 1; client <= MaxClients; client++) { if (!IsClientInGame(client) || !IsPlayerAlive(client)) @@ -673,6 +835,169 @@ public Action Timer_ApplyRoundMode(Handle timer) return Plugin_Stop; } +void SetupOneVsAll() +{ + int chosenT = ResolveOneVsAllTarget(); + if (chosenT <= 0) + { + PrintToChatAll("%s \x02Не удалось выбрать ТТшника, режим 1 vs All отменён.", CR_PREFIX); + g_CurrentRound = CR_None; + return; + } + + g_OneVsAllTUserId = GetClientUserId(chosenT); + + // 1) Сохраняем команды и инвентарь у живых игроков ДО смены команд + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + + int team = GetClientTeam(i); + if (team < CS_TEAM_T) + { + // Игрок в спеке — пропускаем, команду не меняем + g_OriginalTeam[i] = 0; + continue; + } + + g_OriginalTeam[i] = team; + g_PlayerParticipated[i] = true; + + if (IsPlayerAlive(i)) + { + SaveClientLoadout(i); + } + } + g_PendingTeamRestore = true; + + // 2) Меняем команды и респавним + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + if (g_OriginalTeam[i] < CS_TEAM_T) + { + continue; + } + + int target = (i == chosenT) ? CS_TEAM_T : CS_TEAM_CT; + if (GetClientTeam(i) != target) + { + CS_SwitchTeam(i, target); + } + if (!IsPlayerAlive(i)) + { + CS_RespawnPlayer(i); + } + } + + // 3) Через короткую задержку применяем 1vAll-лоадауты (после респавна игроков) + CreateTimer(0.3, Timer_OneVsAll_ApplyLoadouts, GetClientUserId(chosenT), TIMER_FLAG_NO_MAPCHANGE); + + PrintToChatAll("%s \x011 vs All! \x04%N\x01 против всех. У него: \x045000 HP\x01, \x04500 брони\x01, \x04+25%% скорости\x01.", + CR_PREFIX, chosenT); + + LogCRAction(0, "1vAll: ТТшник %N (userid %d)", chosenT, g_OneVsAllTUserId); +} + +public Action Timer_OneVsAll_ApplyLoadouts(Handle timer, any tUserId) +{ + if (g_CurrentRound != CR_OneVsAll) + { + return Plugin_Stop; + } + + int chosenT = GetClientOfUserId(tUserId); + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || !IsPlayerAlive(i) || IsFakeClient(i)) + { + continue; + } + + StripPlayerWeapons(i); + GivePlayerItem(i, "weapon_knife"); + + if (i == chosenT) + { + // ТТшник: 5000 HP, 500 брони, +25% скорость, $16000, без оружия (купит сам) + SetEntProp(i, Prop_Data, "m_iHealth", 5000); + SetEntProp(i, Prop_Send, "m_ArmorValue", 500); + SetEntProp(i, Prop_Send, "m_bHasHelmet", 1); + SetEntPropFloat(i, Prop_Send, "m_flLaggedMovementValue", 1.25); + SetEntProp(i, Prop_Send, "m_iAccount", 16000); + SetEntityGravity(i, 1.0); + } + else + { + // CT: XM1014 + 4 гранаты, обычные HP/броня, нормальная скорость + SetEntProp(i, Prop_Data, "m_iHealth", 100); + SetEntProp(i, Prop_Send, "m_ArmorValue", 100); + SetEntProp(i, Prop_Send, "m_bHasHelmet", 1); + SetEntPropFloat(i, Prop_Send, "m_flLaggedMovementValue", 1.0); + SetEntityGravity(i, 1.0); + + GivePlayerItem(i, "weapon_xm1014"); + GivePlayerItem(i, "weapon_hegrenade"); + GivePlayerItem(i, "weapon_flashbang"); + GivePlayerItem(i, "weapon_smokegrenade"); + GivePlayerItem(i, "weapon_incgrenade"); + } + } + + return Plugin_Stop; +} + +int ResolveOneVsAllTarget() +{ + // 1) Если админ выбрал конкретного — пробуем его + if (g_OneVsAllChoice > 0) + { + int client = GetClientOfUserId(g_OneVsAllChoice); + if (client > 0 && IsClientInGame(client) && !IsFakeClient(client) && GetClientTeam(client) >= CS_TEAM_T) + { + return client; + } + } + + // 2) Рандом из активных игроков + int candidates[MAXPLAYERS + 1]; + int count = 0; + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && GetClientTeam(i) >= CS_TEAM_T) + { + candidates[count++] = i; + } + } + + if (count == 0) + { + return -1; + } + + return candidates[GetRandomInt(0, count - 1)]; +} + +int CountActivePlayers() +{ + int count = 0; + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && !IsFakeClient(i) && GetClientTeam(i) >= CS_TEAM_T) + { + count++; + } + } + return count; +} + public Action Timer_ApplyModeToClient(Handle timer, any userid) { int client = GetClientOfUserId(userid); @@ -800,6 +1125,7 @@ void ResetRoundState(bool fullReset) if (IsClientInGame(client)) { SetEntityGravity(client, 1.0); + SetEntPropFloat(client, Prop_Send, "m_flLaggedMovementValue", 1.0); } ResetClientStats(client); } @@ -1071,6 +1397,9 @@ void RestoreClientLoadout(int client) SetEntProp(client, Prop_Send, "m_bHasDefuser", g_SavedDefuser[client] ? 1 : 0); } + // Сбрасываем скорость и здоровье до дефолтных + SetEntPropFloat(client, Prop_Send, "m_flLaggedMovementValue", 1.0); + if (!gaveSomething) { GivePlayerItem(client, "weapon_knife"); @@ -1142,6 +1471,16 @@ public Action CommandListener_BuyBlock(int client, const char[] command, int arg PrintCenterText(client, "Покупка отключена во время кастомного раунда"); return Plugin_Handled; } + case CR_OneVsAll: + { + // ТТшник может покупать что угодно, остальным — нельзя + if (GetClientUserId(client) == g_OneVsAllTUserId) + { + return Plugin_Continue; + } + PrintCenterText(client, "Покупка отключена для CT в режиме 1 vs All"); + return Plugin_Handled; + } } return Plugin_Continue; @@ -1190,6 +1529,7 @@ void GetRoundDisplayName(CustomRoundType roundType, char[] buffer, int maxlen) case CR_DeagleHS: strcopy(buffer, maxlen, "Deagle HS Only"); case CR_LowGravity: strcopy(buffer, maxlen, "Низкая гравитация"); case CR_OneHP: strcopy(buffer, maxlen, "1 HP"); + case CR_OneVsAll: strcopy(buffer, maxlen, "1 vs All"); default: strcopy(buffer, maxlen, "Нет"); } } @@ -1226,6 +1566,7 @@ void GetRoundDescription(CustomRoundType roundType, char[] buffer, int maxlen) case CR_DeagleHS: strcopy(buffer, maxlen, "Только хедшоты наносят урон"); case CR_LowGravity: strcopy(buffer, maxlen, "Низкая гравитация"); case CR_OneHP: strcopy(buffer, maxlen, "У всех 1 HP"); + case CR_OneVsAll: strcopy(buffer, maxlen, "1 против всех. У ТТшника 5000 HP, 500 брони, +25% скорости. CT — XM1014 + 4 гранаты."); default: strcopy(buffer, maxlen, ""); } }