Иконка ресурса

FREE HellisPlugin : 1.1.0 1.1.0

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Hellis Plugin", "Arena2", "1.1.0")]
    [Description("Arena duel plugin with random mode selection, global leaderboard, customizable loadouts, and statistics tracking")]
    class HellisPlugin : RustPlugin
    {
        #region Fields
        
        private QueueManager queueManager;
        private ArenaManager arenaManager;
        private LoadoutManager loadoutManager;
        private AimTrainManager aimTrainManager;
        private Dictionary<ulong, ActiveMatch> activeMatches = new Dictionary<ulong, ActiveMatch>();
        private Dictionary<ulong, PlayerData> playerData = new Dictionary<ulong, PlayerData>();
        private Dictionary<ulong, ArenaBuilder> arenaBuilders = new Dictionary<ulong, ArenaBuilder>();
        private HashSet<ulong> defeatedThisTick = new HashSet<ulong>(); // Track defeated players to prevent race conditions
        private HashSet<ulong> activeLeaderboardUIs = new HashSet<ulong>(); // Track players with leaderboard UI
        private HashSet<ulong> activeJoinButtons = new HashSet<ulong>(); // Track players with join button
        private HashSet<ulong> activeLeaveButtons = new HashSet<ulong>(); // Track players with leave button
        private HashSet<ulong> autoRequeueOptOut = new HashSet<ulong>(); // Track players who opted out of auto-requeue
        private List<ArenaConfig> arenas = new List<ArenaConfig>(); // Arena storage (stored in data file, not config)
        
        // Lobby browser system
        private Dictionary<QueueType, List<ulong>> queuesByType = new Dictionary<QueueType, List<ulong>>();
        private Dictionary<string, PrivateRoom> privateRooms = new Dictionary<string, PrivateRoom>();
        
        #endregion
        
        #region Configuration
        
        private Configuration config;
        
        // Configuration class - stores all server settings
        // Note: Player data is stored in HellisPlugin_PlayerData
        // Note: Arena data is stored in HellisPlugin_Arenas
        // Note: Loadouts can be customized per weapon mode (see ADMIN_GUIDE.md)
        public class Configuration
        {
            [JsonProperty("Server Name")]
            public string ServerName = "Hellis Server";
            
            [JsonProperty("Enable Aim Training")]
            public bool EnableAimTraining = true;
            
            [JsonProperty("Enable 1v1 Duels")]
            public bool EnableDuels = true;
            
            [JsonProperty("Enable Speargun Mode")]
            public bool EnableSpeargun = true;
            
            [JsonProperty("Countdown Duration (seconds)")]
            public int CountdownDuration = 3;
            
            [JsonProperty("Best of X Rounds")]
            public int BestOfRounds = 3;
            
            [JsonProperty("Auto Requeue After Match")]
            public bool AutoRequeue = true;
            
            [JsonProperty("Max Instances Per Arena")]
            public int MaxInstancesPerArena = 5;
            
            // NOTE: Lobby configuration has been moved to ArenaConfig (HellisPlugin_Arenas data file)
            // NOTE: Duel arenas are now stored in HellisPlugin_Arenas data file, not in config
            
            [JsonProperty("Aim Train Arena Position")]
            public Vector3 AimTrainArena = new Vector3(-100, 0, -100);
            
            [JsonProperty("Aim Train Arena Radius")]
            public float AimTrainArenaRadius = 30f;
            
            [JsonProperty("Enable Zone Enforcement")]
            public bool EnableZoneEnforcement = false;
            
            [JsonProperty("Leaderboard Time Window (minutes)")]
            public int LeaderboardTimeWindowMinutes = 5;
            
            [JsonProperty("UI Auto-Refresh Interval (seconds, 0 = disabled)")]
            public int UIRefreshInterval = 30;
            
            [JsonProperty("Loadouts")]
            public Dictionary<string, LoadoutConfig> Loadouts = GetDefaultLoadouts();
            
            private static Dictionary<string, LoadoutConfig> GetDefaultLoadouts()
            {
                return new Dictionary<string, LoadoutConfig>
                {
                    ["AK47"] = new LoadoutConfig
                    {
                        Items = new List<LoadoutItemConfig>
                        {
                            new LoadoutItemConfig { ShortName = "rifle.ak", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "ammo.rifle", Amount = 120 },
                            new LoadoutItemConfig { ShortName = "metal.plate.torso", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "metal.facemask", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "roadsign.kilt", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "syringe.medical", Amount = 4 }
                        }
                    },
                    ["SAR"] = new LoadoutConfig
                    {
                        Items = new List<LoadoutItemConfig>
                        {
                            new LoadoutItemConfig { ShortName = "rifle.semiauto", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "ammo.rifle", Amount = 96 },
                            new LoadoutItemConfig { ShortName = "metal.plate.torso", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "coffeecan.helmet", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "roadsign.kilt", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "syringe.medical", Amount = 4 }
                        }
                    },
                    ["Speargun"] = new LoadoutConfig
                    {
                        Items = new List<LoadoutItemConfig>
                        {
                            new LoadoutItemConfig { ShortName = "speargun", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "speargun.spear", Amount = 16 },
                            new LoadoutItemConfig { ShortName = "diving.fins", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "diving.mask", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "diving.tank", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "diving.wetsuit", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "syringe.medical", Amount = 3 }
                        }
                    },
                    ["Bow"] = new LoadoutConfig
                    {
                        Items = new List<LoadoutItemConfig>
                        {
                            new LoadoutItemConfig { ShortName = "bow.hunting", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "arrow.wooden", Amount = 60 },
                            new LoadoutItemConfig { ShortName = "attire.hide.vest", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "bandage", Amount = 6 }
                        }
                    },
                    ["Revolver"] = new LoadoutConfig
                    {
                        Items = new List<LoadoutItemConfig>
                        {
                            new LoadoutItemConfig { ShortName = "pistol.revolver", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "ammo.pistol", Amount = 64 },
                            new LoadoutItemConfig { ShortName = "burlap.shirt", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "burlap.trousers", Amount = 1 },
                            new LoadoutItemConfig { ShortName = "bandage", Amount = 4 }
                        }
                    }
                };
            }
        }
        
        // Loadout configuration for a specific weapon mode
        // Each mode (AK47, SAR, Bow, Revolver, Speargun) has its own loadout
        // Customize items, amounts, armor, and attachments via config (see ADMIN_GUIDE.md)
        public class LoadoutConfig
        {
            [JsonProperty("Items")]
            public List<LoadoutItemConfig> Items = new List<LoadoutItemConfig>();
        }
        
        // Individual item in a loadout
        // ShortName: Rust item shortname (e.g., "rifle.ak", "ammo.rifle", "metal.plate.torso")
        // Amount: Quantity to give player
        public class LoadoutItemConfig
        {
            [JsonProperty("ShortName")]
            public string ShortName;
            
            [JsonProperty("Amount")]
            public int Amount;
        }
        
        // Arena configuration - defines duel arena spawn points
        // Stored in HellisPlugin_Arenas data file (not in main config)
        public class ArenaConfig
        {
            public string Name;
            public Vector3 Spawn1;
            public Vector3 Spawn2;
            
            [JsonProperty("Radius")]
            public float Radius = 30f;
            
            // Lobby configuration (moved from main config)
            public Vector3 LobbyPosition;
            public float LobbyRadius = 10f;
            public bool LobbyPositionSet;
        }
        
        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<Configuration>();
                if (config == null)
                {
                    throw new Exception();
                }
            }
            catch
            {
                // Only write error config if it's not null
                if (config != null)
                {
                    Config.WriteObject(config, false, $"{Interface.Oxide.ConfigDirectory}/{Name}.jsonError");
                }
                PrintError("The configuration file contains an error and has been replaced with a default config.\n" +
                           "The error configuration file was saved in the .jsonError extension");
                LoadDefaultConfig();
                SaveConfig(); // Only save when creating new default config
            }
        }
        
        protected override void LoadDefaultConfig() => config = new Configuration();
        
        protected override void SaveConfig() => Config.WriteObject(config);
        
        #endregion
        
        #region Hooks
        
        private void Init()
        {
            queueManager = new QueueManager();
            arenaManager = new ArenaManager(arenas, config.MaxInstancesPerArena);
            loadoutManager = new LoadoutManager(config);
            aimTrainManager = new AimTrainManager();
            
            // Initialize lobby browser queues
            foreach (QueueType queueType in Enum.GetValues(typeof(QueueType)))
            {
                queuesByType[queueType] = new List<ulong>();
            }
            
            // Migration: Move lobby from old config to arena data (one-time)
            // This happens automatically when plugin loads with old config
            // Note: We can't access config.LobbyPosition here since it's been removed
            // Migration is handled by preserving data through ArenaConfig loading
            
            permission.RegisterPermission("hellisplugin.use", this);
            permission.RegisterPermission("hellisplugin.admin", this);
            
            // Load persistent player data
            LoadData();
            
            Puts($"Hellis Plugin v1.0.0 loaded - {config.ServerName}");
            Puts($"Multi-instance arenas enabled: {config.MaxInstancesPerArena} instances per arena");
            
            // Auto-save player data every 5 minutes
            timer.Every(300f, () => SaveData());
            
            // Refresh UI for all connected players after config loads (fixes missing buttons after reload)
            timer.Once(2f, () => RefreshAllPlayerUI());
            
            // Auto-refresh leaderboard for rolling 5-minute window (if enabled)
            if (config.UIRefreshInterval > 0)
            {
                timer.Repeat(config.UIRefreshInterval, 0, () => RefreshAllPlayerUI());
            }
            
            // Cleanup old stat events every 60 seconds to keep data manageable
            timer.Repeat(60f, 0, () => CleanupOldEvents());
            
            // Zone enforcement check every 5 seconds (if enabled)
            if (config.EnableZoneEnforcement)
            {
                timer.Every(5f, () => CheckZoneEnforcement());
                Puts("Zone enforcement enabled - players will be kept in designated zones");
            }
        }
        
        private void OnServerInitialized()
        {
            if (config.EnableAimTraining)
            {
                Puts("Aim Training mode enabled");
            }
            
            if (config.EnableDuels)
            {
                Puts("1v1 Duel mode enabled");
            }
            
            timer.Every(1f, () => ProcessMatchmaking());
        }
        
        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
        {
            if (entity is BasePlayer player && player != null)
            {
                // Check if player is in an active match
                if (activeMatches.ContainsKey(player.userID))
                {
                    // Check if player already defeated this tick (prevents multiple headshots race condition)
                    if (defeatedThisTick.Contains(player.userID))
                    {
                        // Player is already being defeated - ignore all subsequent damage
                        info.damageTypes = new Rust.DamageTypeList();
                        info.DoHitEffects = false;
                        info.HitMaterial = 0;
                        return true; // Block all damage
                    }
                    
                    // Calculate if this damage would bring player below 10 HP threshold
                    float totalDamage = info?.damageTypes?.Total() ?? 0f;
                    
                    // Intercept at 10 HP threshold to prevent death screen and ensure clean reset
                    // This also prevents bleeding/poison from killing the player after the hit
                    if (player.health <= 10f || player.health - totalDamage <= 10f)
                    {
                        // Player is below/would be below 10 HP - intercept and reset!
                        var match = activeMatches[player.userID];
                        
                        // Mark player as defeated IMMEDIATELY to prevent race conditions with rapid hits
                        defeatedThisTick.Add(player.userID);
                        
                        // Cancel all damage to prevent any effects
                        info.damageTypes = new Rust.DamageTypeList();
                        info.DoHitEffects = false;
                        info.HitMaterial = 0;
                        
                        // Handle the "defeat" with full reset IMMEDIATELY (not NextTick!)
                        // This prevents race conditions where multiple headshots could kill the player
                        // before the deferred action runs
                        HandleMatchPlayerDefeat(player, match);
                        
                        // Clear the defeated flag after a short delay to allow for round reset
                        timer.Once(0.5f, () => defeatedThisTick.Remove(player.userID));
                        
                        // Prevent the damage from being applied
                        return true;
                    }
                }
            }
            
            return null; // Allow normal damage processing
        }
        
        private void HandleMatchPlayerDefeat(BasePlayer player, ActiveMatch match)
        {
            if (player == null || !player.IsConnected) return;
            
            // FULL RESET - Set health to maximum (100 HP by default)
            player.health = player.MaxHealth();
            
            // STOP ALL BLEEDING - Critical to prevent death after reset
            player.metabolism.bleeding.value = 0f;
            
            // CLEAR ALL DAMAGE OVER TIME EFFECTS
            player.metabolism.poison.value = 0f;
            player.metabolism.radiation_level.value = 0f;
            player.metabolism.radiation_poison.value = 0f;
            
            // RESET METABOLISM - Full health state
            player.metabolism.calories.value = player.metabolism.calories.max;
            player.metabolism.hydration.value = player.metabolism.hydration.max;
            player.metabolism.temperature.value = 32f; // Normal body temperature
            player.metabolism.wetness.value = 0f;
            player.metabolism.dirtyness.value = 0f;
            player.metabolism.oxygen.value = player.metabolism.oxygen.max;
            
            // Make player invisible/frozen during transition
            player.SetPlayerFlag(BasePlayer.PlayerFlags.Spectating, true);
            player.SetPlayerFlag(BasePlayer.PlayerFlags.ThirdPersonViewmode, true);
            
            // Clear inventory to prevent item usage during transition
            player.inventory.Strip();
            
            // Record the defeat for match logic
            match.OnPlayerDeath(player.userID);
            
            // Note: Kills/Deaths are tracked in UpdatePlayerStats based on round scores
            // to avoid double-counting
            
            // Check if round/match should end
            CheckMatchEnd(match);
        }
        
        private object OnPlayerDeath(BasePlayer player, HitInfo info)
        {
            // CRITICAL: Prevent death screen for match players
            // This is a failsafe in case damage interception somehow fails
            if (player != null && activeMatches.ContainsKey(player.userID))
            {
                // Player should NEVER actually die during a match due to damage interception
                // But if they somehow do, prevent the death and death screen
                var match = activeMatches[player.userID];
                
                // Record the defeat
                match.OnPlayerDeath(player.userID);
                
                // Check match end
                CheckMatchEnd(match);
                
                // PREVENT DEATH - cancel the death event and death screen
                // Returning false prevents the death from processing
                return false;
            }
            
            // Allow normal deaths for non-match players
            return null;
        }
        
        // OnPlayerRespawn hook removed - no longer needed!
        // With damage interception, players never die during matches, so they never respawn.
        // Players are frozen and reset in place instead.
        // This also prevents conflicts with other spawn plugins like SpawnCore.
        
        private void OnPlayerDisconnected(BasePlayer player)
        {
            if (player == null) return;
            
            // Remove from queue if queued (old system)
            queueManager.LeaveQueue(player.userID);
            
            // Remove from new queue system (Phase 3)
            LeaveQueueInternal(player, false);
            
            // Clean up private rooms (Phase 4)
            CleanupPlayerRooms(player.userID);
            
            // End match if in one
            if (activeMatches.ContainsKey(player.userID))
            {
                var match = activeMatches[player.userID];
                EndMatch(match, true);
            }
            
            // Clean up UIs
            DestroyLobbyBrowser(player); // Phase 2: Use lobby browser
            DestroyLeaderboardUI(player);
        }
        
        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null) return;
            
            // Teleport to lobby and show UI after a small delay to ensure player is fully loaded
            timer.Once(2f, () =>
            {
                if (player != null && player.IsConnected)
                {
                    TeleportToLobby(player);
                    
                    // Phase 2: Show lobby browser instead of simple button
                    ShowLobbyBrowser(player);
                    
                    ShowLeaderboardUI(player); // Show persistent leaderboard (always)
                    
                    // Send welcome and instructions
                    SendReply(player, "═══════════════════════════════════════");
                    SendReply(player, "Welcome to Hellis Duels!");
                    SendReply(player, "Use the lobby browser to join or create matches!");
                    SendReply(player, "Type /help to see all commands");
                    
                    if (arenaManager.GetAllArenas().Count == 0)
                    {
                        if (permission.UserHasPermission(player.UserIDString, "hellisplugin.admin"))
                        {
                            SendReply(player, "No arenas found! Use /arena create to set up arenas");
                        }
                        else
                        {
                            SendReply(player, "No arenas available yet. Ask an admin to create some!");
                        }
                    }
                    SendReply(player, "═══════════════════════════════════════");
                }
            });
        }
        
        private void TeleportToLobby(BasePlayer player)
        {
            if (player == null || !player.IsConnected) return;
            
            if (arenaManager.IsLobbySet())
            {
                player.Teleport(arenaManager.GetLobbyPosition());
                SendReply(player, "Welcome to the lobby!");
            }
            else
            {
                SendReply(player, "⚠ Lobby not configured. Admin: use /lobby setpos to set lobby location.");
            }
        }
        
        private object CanNetworkTo(BaseNetworkable entity, BasePlayer target)
        {
            // Player visibility isolation for multi-instance arenas
            // Players in a match can only see their opponent
            if (entity is BasePlayer player)
            {
                // Check if the player is in an active match
                if (activeMatches.ContainsKey(player.userID))
                {
                    var match = activeMatches[player.userID];
                    
                    // Allow visibility to opponent in the same match
                    if (target.userID == match.Player1ID || target.userID == match.Player2ID)
                    {
                        return null; // Allow default behavior (visible)
                    }
                    
                    // Hide from all other players
                    return false;
                }
                
                // Check if the target is in an active match
                if (activeMatches.ContainsKey(target.userID))
                {
                    var match = activeMatches[target.userID];
                    
                    // Only show to their opponent
                    if (player.userID == match.Player1ID || player.userID == match.Player2ID)
                    {
                        return null; // Allow default behavior (visible)
                    }
                    
                    // Hide from all other players
                    return false;
                }
            }
            
            return null; // Default behavior
        }
        
        #endregion
        
        #region Commands
        
        // /duel command removed - use JOIN QUEUE button instead (always random mode)
        
        [ChatCommand("leave")]
        private void LeaveCommand(BasePlayer player, string command, string[] args)
        {
            if (queueManager.LeaveQueue(player.userID))
            {
                SendReply(player, "Left the queue.");
                DestroyLeaveButton(player);
                TeleportToLobby(player);
                // ShowJoinButton(player); // Removed - lobby browser handles this
                ShowLobbyBrowser(player); // Show lobby browser instead
            }
            else
            {
                SendReply(player, "You're not in queue.");
            }
        }
        
        [ChatCommand("aimtrain")]
        private void AimTrainCommand(BasePlayer player, string command, string[] args)
        {
            if (!config.EnableAimTraining)
            {
                SendReply(player, "Aim training is currently disabled.");
                return;
            }
            
            if (!permission.UserHasPermission(player.UserIDString, "hellisplugin.use"))
            {
                SendReply(player, "You don't have permission to use this command.");
                return;
            }
            
            StartAimTraining(player);
        }
        
        [ChatCommand("stats")]
        private void StatsCommand(BasePlayer player, string command, string[] args)
        {
            if (!playerData.ContainsKey(player.userID))
            {
                SendReply(player, "No stats yet. Play some matches!");
                return;
            }
            
            var data = playerData[player.userID];
            SendReply(player, $"=== Your Stats ===\n" +
                             $"Matches: {data.TotalMatches}\n" +
                             $"Wins: {data.Wins}\n" +
                             $"Losses: {data.Losses}\n" +
                             $"Win Rate: {data.WinRate:F1}%\n" +
                             $"Rounds Won: {data.RoundsWon}\n" +
                             $"Rounds Lost: {data.RoundsLost}");
        }
        
        [ChatCommand("leaderboard")]
        private void LeaderboardCommand(BasePlayer player, string command, string[] args)
        {
            if (playerData.Count == 0)
            {
                SendReply(player, "No stats yet. Be the first to duel!");
                return;
            }
            
            // Sort by win rate, then by total wins - GLOBAL (no minimum matches)
            var topPlayers = playerData
                .OrderByDescending(p => p.Value.WinRate)
                .ThenByDescending(p => p.Value.Wins)
                .Take(10)
                .ToList();
            
            var message = "=== GLOBAL LEADERBOARD - TOP 10 ===\n";
            int rank = 1;
            foreach (var entry in topPlayers)
            {
                var playerName = covalence.Players.FindPlayerById(entry.Key.ToString())?.Name ?? "Unknown";
                var stats = entry.Value;
                message += $"{rank}. {playerName} - {stats.Wins}W/{stats.Losses}L ({stats.WinRate:F1}%)\n";
                rank++;
            }
            
            // Show player's own rank if not in top 10
            if (playerData.ContainsKey(player.userID))
            {
                var playerRank = playerData
                    .OrderByDescending(p => p.Value.WinRate)
                    .ThenByDescending(p => p.Value.Wins)
                    .ToList()
                    .FindIndex(p => p.Key == player.userID) + 1;
                
                if (playerRank > 10)
                {
                    var yourStats = playerData[player.userID];
                    message += $"\nYour Rank: #{playerRank} - {yourStats.Wins}W/{yourStats.Losses}L ({yourStats.WinRate:F1}%)";
                }
            }
            
            SendReply(player, message);
        }
        
        [ChatCommand("top")]
        private void TopCommand(BasePlayer player, string command, string[] args)
        {
            LeaderboardCommand(player, command, args);
        }
        
        [ChatCommand("toggleleaderboard")]
        private void ToggleLeaderboardCommand(BasePlayer player, string command, string[] args)
        {
            if (activeLeaderboardUIs.Contains(player.userID))
            {
                DestroyLeaderboardUI(player);
                SendReply(player, "Leaderboard UI hidden. Type /toggleleaderboard to show it again.");
            }
            else
            {
                ShowLeaderboardUI(player);
                SendReply(player, "Leaderboard UI shown. Type /toggleleaderboard to hide it.");
            }
        }
        
        [ChatCommand("showui")]
        private void ShowUICommand(BasePlayer player, string command, string[] args)
        {
            ShowLobbyBrowser(player); // Show lobby browser instead of old button
            ShowLeaderboardUI(player);
            SendReply(player, "UI elements refreshed!");
            SendReply(player, "Lobby Browser: Right side");
            SendReply(player, "Leaderboard: Top left");
            SendReply(player, "If you still don't see them, you may need to enable your cursor with F1 menu.");
        }
        
        // Helper method to refresh UI for all connected players (used after config reloads)
        private void RefreshAllPlayerUI()
        {
            int refreshed = 0;
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected) continue;
                
                // Skip players in active matches (they don't see lobby UI anyway)
                if (activeMatches.Values.Any(d => d.Player1ID == player.userID || d.Player2ID == player.userID))
                    continue;
                
                ShowLobbyBrowser(player); // Show lobby browser instead of old button
                ShowLeaderboardUI(player);
                refreshed++;
            }
            if (refreshed > 0)
            {
                Puts($"Refreshed UI for {refreshed} player(s) in lobby");
            }
        }
        
        // Rolling stat calculation methods - calculate stats from events within time window
        private int GetRecentKills(PlayerData data, int minutes)
        {
            var cutoff = DateTime.Now.AddMinutes(-minutes);
            return data.StatEvents.Count(e => e.Type == "Kill" && e.Timestamp >= cutoff);
        }
        
        private int GetRecentDeaths(PlayerData data, int minutes)
        {
            var cutoff = DateTime.Now.AddMinutes(-minutes);
            return data.StatEvents.Count(e => e.Type == "Death" && e.Timestamp >= cutoff);
        }
        
        private int GetRecentWins(PlayerData data, int minutes)
        {
            var cutoff = DateTime.Now.AddMinutes(-minutes);
            return data.StatEvents.Count(e => e.Type == "Win" && e.Timestamp >= cutoff);
        }
        
        private int GetRecentLosses(PlayerData data, int minutes)
        {
            var cutoff = DateTime.Now.AddMinutes(-minutes);
            return data.StatEvents.Count(e => e.Type == "Loss" && e.Timestamp >= cutoff);
        }
        
        // Cleanup old stat events to prevent data bloat
        private void CleanupOldEvents()
        {
            var cutoff = DateTime.Now.AddMinutes(-config.LeaderboardTimeWindowMinutes);
            int totalRemoved = 0;
            
            foreach (var kvp in playerData)
            {
                int before = kvp.Value.StatEvents.Count;
                kvp.Value.StatEvents.RemoveAll(e => e.Timestamp < cutoff);
                totalRemoved += (before - kvp.Value.StatEvents.Count);
            }
            
            if (totalRemoved > 0)
            {
                Puts($"Cleaned up {totalRemoved} old stat event(s)");
                SavePlayerData();
            }
        }
        
        [ChatCommand("autorequeue")]
        private void AutoRequeueCommand(BasePlayer player, string command, string[] args)
        {
            if (args.Length == 0)
            {
                // Show current status
                bool enabled = !autoRequeueOptOut.Contains(player.userID);
                SendReply(player, $"Auto-requeue is currently {(enabled ? "ENABLED" : "DISABLED")}");
                SendReply(player, "Use '/autorequeue on' or '/autorequeue off' to change");
                return;
            }
            
            string action = args[0].ToLower();
            if (action == "on" || action == "enable")
            {
                autoRequeueOptOut.Remove(player.userID);
                SendReply(player, "✓ Auto-requeue ENABLED - You'll automatically rejoin queue after matches");
            }
            else if (action == "off" || action == "disable")
            {
                autoRequeueOptOut.Add(player.userID);
                SendReply(player, "✓ Auto-requeue DISABLED - Click JOIN QUEUE manually after matches");
            }
            else
            {
                SendReply(player, "Usage: /autorequeue [on/off]");
            }
        }
        
        [ChatCommand("clearleaderboard")]
        private void ClearLeaderboardCommand(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            
            // Admin check
            if (!player.IsAdmin)
            {
                SendReply(player, "You must be an admin to use this command.");
                return;
            }
            
            // Require confirmation
            if (args == null || args.Length == 0 || args[0] != "confirm")
            {
                SendReply(player, "⚠️ WARNING: This will clear ALL player statistics!");
                SendReply(player, "Type '/clearleaderboard confirm' to proceed.");
                return;
            }
            
            // Clear all player data
            int count = playerData.Count;
            playerData.Clear();
            SavePlayerData();
            RefreshAllPlayerUI();
            
            Puts($"{player.displayName} cleared all leaderboard data ({count} players)");
            SendReply(player, $"✅ Leaderboard cleared! Removed {count} player records.");
        }
        
        [ChatCommand("help")]
        private void HelpCommand(BasePlayer player, string command, string[] args)
        {
            SendReply(player, "═══════════════════════════════════════");
            SendReply(player, "Hellis Duels - Available Commands");
            SendReply(player, "═══════════════════════════════════════");
            SendReply(player, "");
            SendReply(player, "─── Queue Commands ───");
            SendReply(player, "Click JOIN QUEUE button (top-right) to join!");
            SendReply(player, "/leave - Leave the queue or match");
            SendReply(player, "/autorequeue [on/off] - Toggle auto-requeue after matches");
            SendReply(player, "");
            SendReply(player, "─── Arena Commands ───");
            SendReply(player, "/lobby - Teleport to lobby");
            SendReply(player, "");
            SendReply(player, "─── Stats Commands ───");
            SendReply(player, "/stats - View your statistics");
            SendReply(player, "/leaderboard or /top - View top players");
            SendReply(player, "/toggleleaderboard - Show/hide leaderboard UI");
            SendReply(player, "/showui - Refresh UI elements (if not visible)");
            SendReply(player, "");
            
            if (permission.UserHasPermission(player.UserIDString, "hellisplugin.admin"))
            {
                SendReply(player, "─── Admin Commands ───");
                SendReply(player, "/arena create <name> - Start creating arena");
                SendReply(player, "/arena setspawn1 - Set first spawn");
                SendReply(player, "/arena setspawn2 - Set second spawn");
                SendReply(player, "/arena save - Save the arena");
                SendReply(player, "/arena list - List all arenas");
                SendReply(player, "/arena delete <name> - Delete arena");
                SendReply(player, "/arena setradius <name> <radius> - Set arena zone radius");
                SendReply(player, "/lobby setpos - Set lobby position");
                SendReply(player, "/lobby setradius <radius> - Set lobby zone radius");
                SendReply(player, "/clearleaderboard - Clear all leaderboard data (requires confirm)");
                SendReply(player, "");
            }
            
            SendReply(player, "═══════════════════════════════════════");
            SendReply(player, "TIP: Click the green JOIN QUEUE button to start dueling!");
            SendReply(player, "═══════════════════════════════════════");
        }
        
        [ChatCommand("forfeit")]
        private void ForfeitCommand(BasePlayer player, string command, string[] args)
        {
            if (activeMatches.ContainsKey(player.userID))
            {
                var match = activeMatches[player.userID];
                EndMatch(match, false, player.userID);
                SendReply(player, "You forfeited the match.");
            }
            else
            {
                SendReply(player, "You're not in a match.");
            }
        }
        
        [ChatCommand("lobby")]
        private void LobbyCommand(BasePlayer player, string command, string[] args)
        {
            if (args.Length == 0)
            {
                TeleportToLobby(player);
                return;
            }
            
            if (!permission.UserHasPermission(player.UserIDString, "hellisplugin.admin"))
            {
                SendReply(player, "You don't have permission to configure the lobby.");
                return;
            }
            
            switch (args[0].ToLower())
            {
                case "setpos":
                    arenaManager.SetLobby(player.transform.position, arenaManager.GetLobbyRadius());
                    SaveArenas();
                    SendReply(player, $"Lobby position set to: {player.transform.position}");
                    break;
                    
                case "setradius":
                    if (args.Length < 2)
                    {
                        SendReply(player, $"Current lobby radius: {arenaManager.GetLobbyRadius()}m\nUsage: /lobby setradius <radius>");
                        return;
                    }
                    
                    float lobbyRadius;
                    if (float.TryParse(args[1], out lobbyRadius) && lobbyRadius > 0)
                    {
                        arenaManager.SetLobby(arenaManager.GetLobbyPosition(), lobbyRadius);
                        SaveArenas();
                        SendReply(player, $"Lobby radius set to: {lobbyRadius}m");
                    }
                    else
                    {
                        SendReply(player, "Invalid radius. Please enter a positive number.");
                    }
                    break;
                    
                default:
                    SendReply(player, "Lobby Commands:\n" +
                                     "/lobby - Teleport to lobby\n" +
                                     "/lobby setpos - Set lobby position (Admin)\n" +
                                     "/lobby setradius <radius> - Set lobby zone radius (Admin)");
                    break;
            }
        }
        
        [ChatCommand("arena")]
        private void ArenaCommand(BasePlayer player, string command, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, "hellisplugin.admin"))
            {
                SendReply(player, "You don't have permission to use this command.");
                return;
            }
            
            if (args.Length == 0)
            {
                SendReply(player, "Arena Commands:\n" +
                                 "/arena create <name> - Start creating a new arena\n" +
                                 "/arena setspawn1 - Set first spawn point\n" +
                                 "/arena setspawn2 - Set second spawn point\n" +
                                 "/arena save - Save the arena\n" +
                                 "/arena cancel - Cancel arena creation\n" +
                                 "/arena list - List all arenas\n" +
                                 "/arena delete <name> - Delete an arena\n" +
                                 "/arena setradius <name> <radius> - Set arena zone radius\n" +
                                 "/arena tp <name> [1|2] - Teleport to arena spawn");
                return;
            }
            
            switch (args[0].ToLower())
            {
                case "create":
                    if (args.Length < 2)
                    {
                        SendReply(player, "Usage: /arena create <name>");
                        return;
                    }
                    CreateArena(player, string.Join(" ", args.Skip(1)));
                    break;
                    
                case "setspawn1":
                    SetSpawn1(player);
                    break;
                    
                case "setspawn2":
                    SetSpawn2(player);
                    break;
                    
                case "save":
                    SaveArena(player);
                    break;
                    
                case "cancel":
                    CancelArena(player);
                    break;
                    
                case "list":
                    ListArenas(player);
                    break;
                    
                case "delete":
                    if (args.Length < 2)
                    {
                        SendReply(player, "Usage: /arena delete <name>");
                        return;
                    }
                    DeleteArena(player, string.Join(" ", args.Skip(1)));
                    break;
                    
                case "tp":
                    if (args.Length < 2)
                    {
                        SendReply(player, "Usage: /arena tp <name> [1|2]");
                        return;
                    }
                    int spawnNum = 1;
                    string arenaName;
                    if (args.Length >= 3 && int.TryParse(args[args.Length - 1], out spawnNum))
                    {
                        // Last arg is spawn number
                        arenaName = string.Join(" ", args.Skip(1).Take(args.Length - 2));
                    }
                    else
                    {
                        // No spawn number, use all remaining args as name
                        arenaName = string.Join(" ", args.Skip(1));
                    }
                    TeleportToArena(player, arenaName, spawnNum);
                    break;
                    
                case "setradius":
                    if (args.Length < 3)
                    {
                        SendReply(player, "Usage: /arena setradius <name> <radius>");
                        return;
                    }
                    
                    string arenaNameForRadius = string.Join(" ", args.Skip(1).Take(args.Length - 2));
                    float arenaRadius;
                    
                    if (!float.TryParse(args[args.Length - 1], out arenaRadius) || arenaRadius <= 0)
                    {
                        SendReply(player, "Invalid radius. Please enter a positive number.");
                        return;
                    }
                    
                    var arenaToUpdate = arenas.FirstOrDefault(a => a.Name.Equals(arenaNameForRadius, StringComparison.OrdinalIgnoreCase));
                    if (arenaToUpdate == null)
                    {
                        SendReply(player, $"Arena '{arenaNameForRadius}' not found.");
                        return;
                    }
                    
                    arenaToUpdate.Radius = arenaRadius;
                    SaveArenas();
                    SendReply(player, $"Arena '{arenaNameForRadius}' radius set to: {arenaRadius}m");
                    break;
                    
                default:
                    SendReply(player, "Unknown command. Use /arena for help.");
                    break;
            }
        }
        
        #endregion
        
        #region Matchmaking
        
        private void ProcessMatchmaking()
        {
            // Process old queue system
            var matches = queueManager.TryMatchAll();
            
            foreach (var match in matches)
            {
                var player1 = BasePlayer.FindByID(match.Player1ID);
                var player2 = BasePlayer.FindByID(match.Player2ID);
                
                if (player1 == null || player2 == null) continue;
                
                StartDuel(player1, player2, match.Mode);
            }
            
            // Process new queue system (Phase 3)
            ProcessAllQueues();
        }
        
        private void StartDuel(BasePlayer player1, BasePlayer player2, DuelMode mode)
        {
            var arena = arenaManager.GetAvailableArena();
            if (arena == null)
            {
                SendReply(player1, "No arenas available. Please wait.");
                SendReply(player2, "No arenas available. Please wait.");
                queueManager.JoinQueue(player1.userID, player1.displayName, mode);
                queueManager.JoinQueue(player2.userID, player2.displayName, mode);
                return;
            }
            
            // Get instance ID for this match
            int instanceId = arena.GetNextInstanceId();
            if (instanceId == -1)
            {
                SendReply(player1, "No arena instances available. Please wait.");
                SendReply(player2, "No arena instances available. Please wait.");
                queueManager.JoinQueue(player1.userID, player1.displayName, mode);
                queueManager.JoinQueue(player2.userID, player2.displayName, mode);
                return;
            }
            
            var match = new ActiveMatch(player1.userID, player2.userID, mode, arena, instanceId, config.BestOfRounds);
            activeMatches[player1.userID] = match;
            activeMatches[player2.userID] = match;
            
            // Clean up any arrows from previous matches
            var arrows1 = new List<BaseEntity>();
            foreach (var child in player1.children)
            {
                if (child != null && child.ShortPrefabName != null && child.ShortPrefabName.Contains("arrow"))
                    arrows1.Add(child);
            }
            foreach (var arrow in arrows1) arrow.Kill();
            
            var arrows2 = new List<BaseEntity>();
            foreach (var child in player2.children)
            {
                if (child != null && child.ShortPrefabName != null && child.ShortPrefabName.Contains("arrow"))
                    arrows2.Add(child);
            }
            foreach (var arrow in arrows2) arrow.Kill();
            
            // Teleport players to arena
            TeleportPlayer(player1, arena.Spawn1);
            TeleportPlayer(player2, arena.Spawn2);
            
            // Apply loadouts
            GiveLoadout(player1, mode);
            GiveLoadout(player2, mode);
            
            // Hide join button and show leave button during match
            DestroyJoinButton(player1);
            DestroyJoinButton(player2);
            ShowLeaveButton(player1);
            ShowLeaveB
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху