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

FREE FallingTiles : 34.1.0 34.1.0

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

namespace Oxide.Plugins
{
    [Info("Falling Tiles", "Vic", "34.1.0")]
    public class FallingTiles : RustPlugin
    {
        // --- CONFIGURATION (Persists in oxide/config/FallingTiles.json) ---
        private ConfigData _config;
        
        // --- PLAYER DATA PERSISTENCE (Persists in oxide/data/FallingTiles_PlayerData.json) ---
        private DynamicConfigFile _playerDataFile;
        private Dictionary<string, StoredPlayerData> _storedPlayerData = new Dictionary<string, StoredPlayerData>();
        
        private class StoredPlayerData
        {
            public float BestSurvivalTime { get; set; }
            public int TotalTilesTouched { get; set; }
            public int TotalGamesPlayed { get; set; }
            public int TotalWins { get; set; }
            public string LastKnownName { get; set; }
        }
        
        private class GameModeConfig
        {
            public string Name { get; set; } = "Default";
            public string Description { get; set; } = "Default game mode";
            public Vector3 ArenaSpawn { get; set; } = Vector3.zero;
            public Vector3 TeleportSpherePos { get; set; } = Vector3.zero;
            public float SphereRadius { get; set; } = 2f;
            public string ArenaShape { get; set; } = "square";      // square, circle, hexagon, star, heart
            public string ArenaPattern { get; set; } = "solid";     // solid, checkerboard, stripes, rings, maze, spiral, cross, diamond
            public string ArenaSpacing { get; set; } = "normal";    // tight, normal, wide, random
            public string TileType { get; set; } = "square";        // square, triangle
            public int ArenaSize { get; set; } = 6;
            public int ArenaLayers { get; set; } = 3;               // 1-10 layers
            public ulong TileSkinId { get; set; } = 0; // Default skin for TopTier grade
            public BuildingGrade.Enum TileGrade { get; set; } = BuildingGrade.Enum.TopTier; // Starting grade
            
            // --- GAMEPLAY PARAMETERS (Differentiate modes) ---
            public float TouchCheckInterval { get; set; } = 0.25f;  // How often to check player touches (lower = faster response)
            public int DegradeSteps { get; set; } = 1;              // How many grades to degrade per touch (1=normal, 2=faster)
            public float RandomTileDropInterval { get; set; } = 0f; // Seconds between random tile drops (0=disabled)
            public bool InstantKillOnTouch { get; set; } = false;   // If true, tiles instantly break on touch
            public float TileDropChanceOnBass { get; set; } = 1f;   // Multiplier for bass drop tile count (1=normal, 2=double)
            public bool RandomDegradation { get; set; } = false;    // If true, random tiles degrade by themselves
            public bool TouchDegrades { get; set; } = true;         // If true, tiles degrade when stepped on (ONLY for Touch/Classic mode)
            public float VocalDropMultiplier { get; set; } = 1f;    // Multiplier for drops during vocal sections
            public bool ShowLyricsOnTiles { get; set; } = false;    // Show lyrics on tiles about to break
            
            // --- MUSIC INTEGRATION ---
            public bool MusicSynced { get; set; } = true;           // Whether mode responds to music events
            public bool BPMScaling { get; set; } = false;           // Scale tile drops with song BPM
            public bool BeatSyncEnabled { get; set; } = false;      // Visual pulse on bass beats
            public bool VocalSynced { get; set; } = false;          // React to vocal sections (faster drops)
            public bool AutoBPM { get; set; } = false;              // Auto-detect BPM from audio analysis
            
            // --- ADVANCED FEATURES ---
            public float ShrinkingArenaInterval { get; set; } = 0f; // Seconds between arena shrinks (0=disabled)
            public bool GrowingHolesEnabled { get; set; } = false;  // Random holes appear and expand
            
            // Grade-specific skins - different visual for each degradation level
            public ulong SkinTopTier { get; set; } = 0;    // Skin for TopTier (HQM)
            public ulong SkinMetal { get; set; } = 0;      // Skin for Metal grade
            public ulong SkinStone { get; set; } = 0;      // Skin for Stone grade
            public ulong SkinWood { get; set; } = 0;       // Skin for Wood grade
            public ulong SkinTwigs { get; set; } = 0;      // Skin for Twigs grade (before destruction)
            
            // Get the prefab path based on tile type
            public string GetTilePrefab()
            {
                return TileType?.ToLower() == "triangle" 
                    ? "assets/prefabs/building core/floor.triangle/floor.triangle.prefab"
                    : "assets/prefabs/building core/floor/floor.prefab";
            }
            
            // Get spacing value based on setting
            public float GetSpacingValue()
            {
                switch (ArenaSpacing?.ToLower())
                {
                    case "tight": return 2.0f;
                    case "wide": return 4.0f;
                    case "random": return Random.Range(2.0f, 4.0f);
                    default: return 3.0f; // normal
                }
            }
            
            // Get skin for a specific grade
            public ulong GetSkinForGrade(BuildingGrade.Enum grade)
            {
                switch (grade)
                {
                    case BuildingGrade.Enum.TopTier: return SkinTopTier > 0 ? SkinTopTier : TileSkinId;
                    case BuildingGrade.Enum.Metal: return SkinMetal > 0 ? SkinMetal : TileSkinId;
                    case BuildingGrade.Enum.Stone: return SkinStone > 0 ? SkinStone : TileSkinId;
                    case BuildingGrade.Enum.Wood: return SkinWood > 0 ? SkinWood : TileSkinId;
                    case BuildingGrade.Enum.Twigs: return SkinTwigs > 0 ? SkinTwigs : TileSkinId;
                    default: return TileSkinId;
                }
            }
        }
        
        private class ConfigData
        {
            public string NgrokLink { get; set; } = "https://gael-supersolar-elenora.ngrok-free.dev";
            public float TileSpacing { get; set; } = 3f;
            public float LayerHeight { get; set; } = 3.5f;
            public int CountdownSeconds { get; set; } = 5;
            public Vector3 LobbySpawn { get; set; } = Vector3.zero;
            public Vector3 SpectatorSpawn { get; set; } = Vector3.zero;
            public bool PreSpawnArenas { get; set; } = true; // Spawn all arenas on server start
            public bool TeleportToLobbyOnJoin { get; set; } = true; // Teleport players to lobby when they join server
            public bool SpawnWithRandomGun { get; set; } = true; // Give players a double barrel shotgun when joining
            public int GunAmmoAmount { get; set; } = 100; // Amount of ammo to give
            public string SpawnGun { get; set; } = "shotgun.double"; // Double barrel by default
            public List<string> RandomGuns { get; set; } = new List<string> {
                "shotgun.double", "pistol.semiauto", "pistol.python", "shotgun.pump"
            };
            public Dictionary<string, GameModeConfig> GameModes { get; set; } = new Dictionary<string, GameModeConfig>
            {
                { "classic", new GameModeConfig { 
                    Name = "🎮 Classic (Touch)", 
                    Description = "TOUCH MODE - Tiles degrade when YOU step on them!", 
                    TileGrade = BuildingGrade.Enum.TopTier,
                    ArenaShape = "square",
                    ArenaPattern = "solid",
                    ArenaSize = 6,
                    ArenaLayers = 3,
                    TouchCheckInterval = 0.25f,
                    DegradeSteps = 1,
                    RandomTileDropInterval = 0f,
                    TileDropChanceOnBass = 0.5f,  // Low bass drops - mainly touch based
                    TouchDegrades = true,         // THIS is the key difference!
                    MusicSynced = true,
                    BeatSyncEnabled = false,
                    BPMScaling = false
                } },
                { "speed", new GameModeConfig { 
                    Name = "⚡ Speed Mode", 
                    Description = "BASS DROPS! Fast touch + heavy bass reactions!", 
                    TileGrade = BuildingGrade.Enum.TopTier,
                    ArenaShape = "circle",
                    ArenaPattern = "checkerboard",
                    ArenaSize = 5,
                    ArenaLayers = 2,
                    TouchCheckInterval = 0.08f,
                    DegradeSteps = 2,
                    RandomTileDropInterval = 0f,
                    TileDropChanceOnBass = 3f,   // Heavy bass drops!
                    TouchDegrades = true,         // Also has touch
                    MusicSynced = true,
                    BPMScaling = true,            // Faster songs = more chaos
                    BeatSyncEnabled = true,
                    VocalSynced = true,
                    VocalDropMultiplier = 2f,
                    AutoBPM = true
                } },
                { "chaos", new GameModeConfig { 
                    Name = "🌪️ Chaos Mode", 
                    Description = "MUSIC CHAOS! No touch - ONLY music controls tiles!", 
                    TileGrade = BuildingGrade.Enum.TopTier,
                    ArenaShape = "hexagon",
                    ArenaPattern = "spiral",
                    ArenaSize = 7,
                    ArenaLayers = 3,
                    TouchCheckInterval = 0.5f,    // Slower touch check (doesn't matter - disabled)
                    DegradeSteps = 1,
                    RandomTileDropInterval = 2f,  // Tiles randomly fall
                    TileDropChanceOnBass = 4f,    // MASSIVE bass drops!
                    TouchDegrades = false,        // NO TOUCH DEGRADATION!
                    RandomDegradation = true,
                    MusicSynced = true,
                    GrowingHolesEnabled = true,
                    BeatSyncEnabled = true,
                    VocalSynced = true,
                    VocalDropMultiplier = 3f,     // Vocals cause chaos
                    ShowLyricsOnTiles = true,     // Show lyrics on breaking tiles!
                    AutoBPM = true
                } },
                { "zen", new GameModeConfig { 
                    Name = "🧘 Zen Mode", 
                    Description = "PURE MUSIC! Tiles ONLY fall on bass beats - no touch!", 
                    TileGrade = BuildingGrade.Enum.TopTier,
                    ArenaShape = "circle",
                    ArenaPattern = "rings",
                    ArenaSize = 10,              // Larger arena
                    ArenaLayers = 2,
                    TouchCheckInterval = 2.0f,   // Very slow (doesn't matter)
                    DegradeSteps = 1,
                    RandomTileDropInterval = 0f, // NO random drops
                    TileDropChanceOnBass = 1f,   // Normal bass drops
                    TouchDegrades = false,       // NO TOUCH! Pure music mode
                    BeatSyncEnabled = true,      // Nice visual pulse
                    MusicSynced = true,
                    BPMScaling = false,          // No BPM scaling - always calm
                    VocalSynced = false,         // No vocal reactions (calm)
                    ShowLyricsOnTiles = true,    // Show lyrics floating
                    AutoBPM = true
                } },
                { "hardcore", new GameModeConfig { 
                    Name = "💀 Hardcore Mode", 
                    Description = "SURVIVAL! Touch = INSTANT DEATH + heavy music!", 
                    TileGrade = BuildingGrade.Enum.TopTier,
                    ArenaShape = "star",
                    ArenaPattern = "solid",
                    ArenaSize = 5,
                    ArenaLayers = 2,
                    TouchCheckInterval = 0.03f,  // Ultra-fast touch detection
                    DegradeSteps = 1,
                    InstantKillOnTouch = true,   // INSTANT tile death
                    TileDropChanceOnBass = 2f,
                    TouchDegrades = true,        // Touch KILLS tiles
                    MusicSynced = true,
                    BPMScaling = true,
                    BeatSyncEnabled = true,
                    VocalSynced = true,
                    VocalDropMultiplier = 1.5f,
                    AutoBPM = true
                } },
                { "tower", new GameModeConfig { 
                    Name = "🗼 Tower Mode", 
                    Description = "CLIMB! 10 layers - survive as lower floors collapse!", 
                    TileGrade = BuildingGrade.Enum.TopTier,
                    ArenaShape = "square",
                    ArenaPattern = "solid",
                    ArenaSize = 4,               // Smaller footprint
                    ArenaLayers = 10,            // 10 LAYERS HIGH!
                    TouchCheckInterval = 0.3f,
                    DegradeSteps = 1,
                    TileDropChanceOnBass = 2f,
                    RandomTileDropInterval = 3f, // Tiles fall on their own too
                    TouchDegrades = false,       // NO touch - only music/random
                    MusicSynced = true,
                    BeatSyncEnabled = true,
                    VocalSynced = true,
                    VocalDropMultiplier = 1.5f,
                    AutoBPM = true
                } },
                { "tournament", new GameModeConfig { 
                    Name = "🏆 Tournament Mode", 
                    Description = "COMPETE! Arena shrinks + music + light touch!", 
                    TileGrade = BuildingGrade.Enum.TopTier,
                    ArenaShape = "circle",
                    ArenaPattern = "solid",
                    ArenaSize = 10,              // Larger start
                    ArenaLayers = 2,
                    TouchCheckInterval = 0.5f,
                    DegradeSteps = 1,
                    ShrinkingArenaInterval = 30f, // Arena shrinks!
                    TileDropChanceOnBass = 1.5f,
                    TouchDegrades = true,        // Light touch effect
                    MusicSynced = true,
                    BeatSyncEnabled = true,
                    VocalSynced = true,
                    VocalDropMultiplier = 1.2f,
                    AutoBPM = true
                } }
            };
        }

        protected override void LoadDefaultConfig()
        {
            _config = new ConfigData();
            SaveConfig();
            Puts("Created new config file with default settings.");
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try { 
                _config = Config.ReadObject<ConfigData>();
                
                // CRITICAL: Ensure all game modes have correct TouchDegrades values
                // Old configs may not have this property or may have it set incorrectly
                EnsureCorrectModeSettings();
                
                Puts("Config loaded successfully.");
            }
            catch (Exception ex) { 
                PrintWarning($"Failed to load config: {ex.Message}. Loading defaults.");
                LoadDefaultConfig(); 
            }
        }
        
        // Ensure game modes have correct settings (especially for existing configs)
        private void EnsureCorrectModeSettings()
        {
            // Define which modes should NOT have touch degradation
            var musicOnlyModes = new HashSet<string> { "chaos", "zen", "tower" };
            
            foreach (var kvp in _config.GameModes)
            {
                string modeKey = kvp.Key.ToLower();
                var mode = kvp.Value;
                
                // Force correct TouchDegrades setting based on mode type
                if (musicOnlyModes.Contains(modeKey))
                {
                    if (mode.TouchDegrades)
                    {
                        Puts($"[FallingTiles] Fixing mode '{modeKey}': Setting TouchDegrades=false (music-only mode)");
                        mode.TouchDegrades = false;
                    }
                }
                
                // Log the mode settings for debugging
                Puts($"[FallingTiles] Mode '{modeKey}': TouchDegrades={mode.TouchDegrades}, MusicSynced={mode.MusicSynced}");
            }
            
            // Save the corrected config
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(_config);
        
        private void LoadPlayerData()
        {
            _playerDataFile = Interface.Oxide.DataFileSystem.GetFile(PlayerDataFileName);
            try {
                _storedPlayerData = _playerDataFile.ReadObject<Dictionary<string, StoredPlayerData>>() ?? new Dictionary<string, StoredPlayerData>();
                Puts($"Loaded {_storedPlayerData.Count} player records from data file.");
            }
            catch {
                _storedPlayerData = new Dictionary<string, StoredPlayerData>();
                Puts("Created new player data file.");
            }
        }
        
        private void SavePlayerData()
        {
            _playerDataFile.WriteObject(_storedPlayerData);
        }

        // --- GAME STATE ---
        private Dictionary<string, HashSet<BuildingBlock>> _arenasByMode = new Dictionary<string, HashSet<BuildingBlock>>(); // Tiles per game mode
        private bool _gameActive = false;
        private string _currentSong = "Waiting for Request...";
        private string _lastLyric = "FALLING TILES";
        private Timer _touchTimer;
        private Timer _gameTimer;
        private Timer _hudUpdateTimer;
        private Timer _sphereCheckTimer;
        private Timer _randomTileDropTimer;  // For chaos mode random tile drops
        private Timer _shrinkingArenaTimer;  // For shrinking arena effect
        private Timer _growingHolesTimer;    // For growing holes effect
        private Timer _ddrawRefreshTimer;    // For refreshing DDRAW text above spheres
        private Timer _songDecayTimer;       // For song-based tile decay
        private float _gameStartTime;
        private string _currentGameMode = "classic";
        private float _currentBPM = 120f;    // Current song BPM for scaling
        private float _songDuration = 180f;  // Song duration in seconds (default 3 min)
        private float _songStartTime = 0f;   // When the current song started
        private int _initialTileCount = 0;   // Tiles at song start for decay calculation
        private HashSet<Vector3> _holePositions = new HashSet<Vector3>(); // Track growing holes
        
        // --- LAYER ACTIVATION SYSTEM ---
        // Layers only become active (affected by music/random drops) once a player has touched them
        private Dictionary<string, HashSet<int>> _activatedLayers = new Dictionary<string, HashSet<int>>(); // mode -> set of activated layer indices
        
        // --- TELEPORT SPHERES ---
        private Dictionary<string, SphereEntity> _teleportSpheres = new Dictionary<string, SphereEntity>();
        private HashSet<ulong> _playersInSphere = new HashSet<ulong>();
        
        // --- PLAYER DATA (Session) ---
        private Dictionary<ulong, PlayerData> _playerData = new Dictionary<ulong, PlayerData>();
        private Dictionary<ulong, string> _playerGameMode = new Dictionary<ulong, string>(); // Track which mode each player is in
        private HashSet<ulong> _activePlayers = new HashSet<ulong>();
        private HashSet<ulong> _spectators = new HashSet<ulong>();
        private List<PlayerScore> _leaderboard = new List<PlayerScore>();
        
        private class PlayerData
        {
            public float SurvivalTime { get; set; }
            public int TilesTouched { get; set; }
            public int GamesPlayed { get; set; }
            public int Wins { get; set; }
            public float JoinTime { get; set; }
            public string GameMode { get; set; } = "classic"; // Track player's game mode
        }
        
        private class PlayerScore
        {
            public string Name { get; set; }
            public float SurvivalTime { get; set; }
            public int TilesTouched { get; set; }
        }

        private const string MetalFloorPrefab = "assets/prefabs/building core/floor/floor.prefab";
        private const string TriangleFloorPrefab = "assets/prefabs/building core/floor.triangle/floor.triangle.prefab";
        private const string ExplodeEffect = "assets/content/effects/explosions/explosion piercing.prefab";
        private const string SpherePrefab = "assets/prefabs/visualization/sphere.prefab";
        private const string PlayerDataFileName = "FallingTiles_PlayerData";

        void Init() {
            permission.RegisterPermission("fallingtiles.admin", this);
            LoadPlayerData();
        }
        
        void OnServerInitialized()
        {
            Puts("=== FALLING TILES v29.0.0 ===");
            Puts($"Lobby Spawn: {(_config.LobbySpawn != Vector3.zero ? _config.LobbySpawn.ToString() : "Not Set")}");
            Puts($"Spectator Spawn: {(_config.SpectatorSpawn != Vector3.zero ? _config.SpectatorSpawn.ToString() : "Not Set")}");
            Puts($"Spawn With Random Gun: {_config.SpawnWithRandomGun}");
            
            // Log and spawn teleport spheres for each game mode
            foreach (var mode in _config.GameModes)
            {
                string sphereStatus = mode.Value.TeleportSpherePos != Vector3.zero ? $"Spawning at {mode.Value.TeleportSpherePos}" : "Not Set";
                string arenaStatus = mode.Value.ArenaSpawn != Vector3.zero ? mode.Value.ArenaSpawn.ToString() : "Not Set";
                string shapeInfo = $"{mode.Value.ArenaShape}/{mode.Value.ArenaPattern}/{mode.Value.ArenaSpacing}";
                Puts($"  [{mode.Value.Name}] Shape: {shapeInfo} | Sphere: {sphereStatus} | Arena: {arenaStatus}");
            }
            
            SpawnAllTeleportSpheres();
            
            // Pre-spawn all arenas if configured
            if (_config.PreSpawnArenas)
            {
                SpawnAllArenas();
            }
            
            // Start sphere detection timer
            _sphereCheckTimer = timer.Every(0.5f, CheckPlayersInSpheres);
            
            // Start DDRAW text refresh timer (text expires after ~5 seconds, so refresh every 4)
            _ddrawRefreshTimer = timer.Every(4f, DrawSphereLabels);
            // Initial draw
            DrawSphereLabels();
            
            Puts("=== FALLING TILES READY ===");
        }
        
        // Draw floating text labels above teleport spheres
        private void DrawSphereLabels()
        {
            foreach (var kvp in _config.GameModes)
            {
                if (kvp.Value.TeleportSpherePos == Vector3.zero) continue;
                
                // Position text above the sphere
                Vector3 textPos = kvp.Value.TeleportSpherePos + new Vector3(0, kvp.Value.SphereRadius + 1f, 0);
                
                // Draw text for all active players
                foreach (var player in BasePlayer.activePlayerList)
                {
                    if (player == null || !player.IsConnected) continue;
                    
                    // Draw mode name in green
                    player.SendConsoleCommand("ddraw.text", 5f, Color.green, textPos, $"<size=20>{kvp.Value.Name}</size>");
                    
                    // Draw description below in white
                    Vector3 descPos = textPos - new Vector3(0, 0.3f, 0);
                    player.SendConsoleCommand("ddraw.text", 5f, Color.white, descPos, $"<size=12>{kvp.Value.Description}</size>");
                }
            }
        }
        
        void OnServerSave()
        {
            // Save player data on server save
            SavePlayerData();
        }
        
        // Teleport new players to lobby when they connect
        void OnPlayerConnected(BasePlayer player)
        {
            if (player == null) return;
            
            // Delay the teleport slightly to ensure player is fully loaded
            timer.Once(2f, () => {
                if (player != null && player.IsConnected && _config.TeleportToLobbyOnJoin)
                {
                    TeleportToLobby(player);
                    player.ChatMessage("<color=#1DB954>[FALLING TILES]</color> Welcome! Walk into a teleport sphere to join a game mode. Use /fthelp for commands.");
                }
            });
        }
        
        // Also teleport to lobby on respawn if they die outside of a game
        void OnPlayerRespawned(BasePlayer player)
        {
            if (player == null) return;
            
            // If player is not in an active game, send them to lobby
            if (!_activePlayers.Contains(player.userID) && !_spectators.Contains(player.userID) && _config.TeleportToLobbyOnJoin)
            {
                timer.Once(0.5f, () => {
                    if (player != null && player.IsConnected)
                    {
                        TeleportToLobby(player);
                    }
                });
            }
        }
        
        private void SpawnAllArenas()
        {
            Puts("Pre-spawning all arenas...");
            foreach (var mode in _config.GameModes)
            {
                if (mode.Value.ArenaSpawn != Vector3.zero)
                {
                    BuildArenaForMode(mode.Key);
                    Puts($"  Spawned arena for {mode.Value.Name} at {mode.Value.ArenaSpawn}");
                }
            }
        }
        
        private void SpawnAllTeleportSpheres()
        {
            DestroyAllTeleportSpheres();
            
            foreach (var mode in _config.GameModes)
            {
                if (mode.Value.TeleportSpherePos != Vector3.zero)
                {
                    SpawnTeleportSphere(mode.Key, mode.Value);
                }
            }
        }
        
        private void SpawnTeleportSphere(string modeKey, GameModeConfig mode)
        {
            var sphere = GameManager.server.CreateEntity(SpherePrefab, mode.TeleportSpherePos) as SphereEntity;
            if (sphere == null) return;
            
            sphere.currentRadius = mode.SphereRadius;
            sphere.lerpRadius = mode.SphereRadius;
            sphere.Spawn();
            
            _teleportSpheres[modeKey] = sphere;
            Puts($"Spawned teleport sphere for {mode.Name} at {mode.TeleportSpherePos}");
        }
        
        private void DestroyAllTeleportSpheres()
        {
            foreach (var sphere in _teleportSpheres.Values)
            {
                if (sphere != null && !sphere.IsDestroyed)
                    sphere.Kill();
            }
            _teleportSpheres.Clear();
        }
        
        private void CheckPlayersInSpheres()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                bool foundInSphere = false;
                string foundModeKey = null;
                GameModeConfig foundMode = null;
                
                // Check all spheres once per player
                foreach (var kvp in _config.GameModes)
                {
                    if (kvp.Value.TeleportSpherePos == Vector3.zero) continue;
                    
                    float distance = Vector3.Distance(player.transform.position, kvp.Value.TeleportSpherePos);
                    
                    if (distance <= kvp.Value.SphereRadius)
                    {
                        foundInSphere = true;
                        foundModeKey = kvp.Key;
                        foundMode = kvp.Value;
                        break; // Found a sphere, no need to check more
                    }
                }
                
                if (foundInSphere)
                {
                    // Player is in a sphere
                    if (!_playersInSphere.Contains(player.userID))
                    {
                        _playersInSphere.Add(player.userID);
                        ShowJoinConfirmUI(player, foundModeKey, foundMode);
                    }
                }
                else if (_playersInSphere.Contains(player.userID))
                {
                    // Player left all spheres
                    _playersInSphere.Remove(player.userID);
                    CuiHelper.DestroyUi(player, "FT_JoinConfirm");
                }
            }
        }
        
        private void ShowJoinConfirmUI(BasePlayer player, string modeKey, GameModeConfig mode)
        {
            CuiHelper.DestroyUi(player, "FT_JoinConfirm");
            
            var c = new CuiElementContainer();
            
            // Main panel - slightly larger to fit more info
            c.Add(new CuiPanel { 
                Image = { Color = "0.05 0.05 0.05 0.95" }, 
                RectTransform = { AnchorMin = "0.32 0.26", AnchorMax = "0.68 0.64" },
                CursorEnabled = true
            }, "Overlay", "FT_JoinConfirm");
            
            // Header
            c.Add(new CuiPanel { 
                Image = { Color = "0.1 0.4 0.1 1" }, 
                RectTransform = { AnchorMin = "0 0.85", AnchorMax = "1 1" } 
            }, "FT_JoinConfirm", "FT_JC_Header");
            
            c.Add(new CuiLabel { 
                Text = { Text = $"{mode.Name.ToUpper()}", FontSize = 20, Align = TextAnchor.MiddleCenter, Color = "0.1 1 0.3 1" }, 
                RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } 
            }, "FT_JC_Header");
            
            // Description
            c.Add(new CuiLabel { 
                Text = { Text = mode.Description, FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "0.9 0.9 0.9 1" }, 
                RectTransform = { AnchorMin = "0.05 0.72", AnchorMax = "0.95 0.83" } 
            }, "FT_JoinConfirm");
            
            // TOUCH MODE indicator (MOST IMPORTANT DIFFERENCE)
            string touchIndicator = mode.TouchDegrades 
                ? "👟 TOUCH MODE: Tiles break when you step on them!" 
                : "🎵 MUSIC MODE: Tiles ONLY break from music - no touch!";
            string touchColor = mode.TouchDegrades ? "1 0.5 0.2 1" : "0.2 0.8 1 1";
            
            c.Add(new CuiLabel { 
                Text = { Text = touchIndicator, FontSize = 10, Align = TextAnchor.MiddleCenter, Color = touchColor }, 
                RectTransform = { AnchorMin = "0.05 0.62", AnchorMax = "0.95 0.71" } 
            }, "FT_JoinConfirm");
            
            // KEY DIFFERENCES - Show what makes this mode unique
            List<string> keyFeatures = new List<string>();
            if (mode.InstantKillOnTouch && mode.TouchDegrades) keyFeatures.Add("💥 INSTANT KILL ON TOUCH");
            if (mode.VocalSynced) keyFeatures.Add($"🎤 Vocals speed up drops ({mode.VocalDropMultiplier}x)");
            if (mode.BPMScaling) keyFeatures.Add("🎵 Faster songs = more drops");
            if (mode.TileDropChanceOnBass >= 2f) keyFeatures.Add($"🔥 {mode.TileDropChanceOnBass}x drops on bass beats");
            if (mode.RandomTileDropInterval > 0) keyFeatures.Add($"🎲 Random drops every {mode.RandomTileDropInterval}s");
            if (mode.ShrinkingArenaInterval > 0) keyFeatures.Add($"📉 Arena shrinks every {mode.ShrinkingArenaInterval}s");
            if (mode.GrowingHolesEnabled) keyFeatures.Add("🕳️ Holes expand over time");
            if (mode.ShowLyricsOnTiles) keyFeatures.Add("📝 Lyrics appear on breaking tiles");
            
            string featuresText = keyFeatures.Count > 0 ? string.Join("\n", keyFeatures.Take(4)) : "Standard gameplay";
            
            c.Add(new CuiLabel { 
                Text = { Text = featuresText, FontSize = 9, Align = TextAnchor.MiddleCenter, Color = "1 0.8 0.2 1" }, 
                RectTransform = { AnchorMin = "0.05 0.38", AnchorMax = "0.95 0.61" } 
            }, "FT_JoinConfirm");
            
            c.Add(new CuiLabel { 
                Text = { Text = $"Arena: {mode.ArenaShape} {mode.ArenaSize}x{mode.ArenaSize} | {mode.ArenaLayers} Layers | {mode.ArenaPattern}", FontSize = 9, Align = TextAnchor.MiddleCenter, Color = "0.6 0.6 0.6 1" }, 
                RectTransform = { AnchorMin = "0.05 0.28", AnchorMax = "0.95 0.37" } 
            }, "FT_JoinConfirm");
            
            // Gun spawn info
            if (_config.SpawnWithRandomGun)
            {
                c.Add(new CuiLabel { 
                    Text = { Text = "🔫 Double Barrel Shotgun provided!", FontSize = 9, Align = TextAnchor.MiddleCenter, Color = "0.5 0.8 1 1" }, 
                    RectTransform = { AnchorMin = "0.05 0.18", AnchorMax = "0.95 0.27" } 
                }, "FT_JoinConfirm");
            }
            
            // Join button
            c.Add(new CuiButton { 
                Button = { Command = $"ft.confirmjoin {modeKey}", Color = "0.1 0.5 0.1 1" }, 
                RectTransform = { AnchorMin = "0.1 0.04", AnchorMax = "0.45 0.16" }, 
                Text = { Text = "✓ JOIN", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } 
            }, "FT_JoinConfirm");
            
            // Cancel button
            c.Add(new CuiButton { 
                Button = { Command = "ft.canceljoin", Color = "0.5 0.1 0.1 1" }, 
                RectTransform = { AnchorMin = "0.55 0.04", AnchorMax = "0.9 0.16" }, 
                Text = { Text = "✕ CANCEL", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } 
            }, "FT_JoinConfirm");
            
            CuiHelper.AddUi(player, c);
        }
        
        [ConsoleCommand("ft.confirmjoin")]
        private void ConsoleConfirmJoin(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null || arg.Args == null || arg.Args.Length == 0) return;
            
            string modeKey = arg.Args[0];
            CuiHelper.DestroyUi(player, "FT_JoinConfirm");
            _playersInSphere.Remove(player.userID);
            
            if (!_config.GameModes.ContainsKey(modeKey))
            {
                player.ChatMessage("<color=#FF0000>[FALLING TILES]</color> Invalid game mode!");
                return;
            }
            
            var mode = _config.GameModes[modeKey];
            
            // Check if arena spawn is set
            if (mode.ArenaSpawn == Vector3.zero)
            {
                player.ChatMessage("<color=#FF0000>[FALLING TILES]</color> Arena not configured! Admin needs to use /ftsetarena first.");
                return;
            }
            
            // Check if arena for this mode exists, rebuild if needed
            if (!_arenasByMode.ContainsKey(modeKey) || _arenasByMode[modeKey].Count == 0)
            {
                BuildArenaForMode(modeKey);
            }
            
            // Check if player is already in a game
            if (_activePlayers.Contains(player.userID))
            {
                player.ChatMessage("<color=#FFD700>[FALLING TILES]</color> You're already in a game! Use /ftleave first.");
                return;
            }
            
            // Check if game is active in this mode - join as spectator
            if (_gameActive && _currentGameMode == modeKey)
            {
                player.ChatMessage("<color=#FF0000>[FALLING TILES]</color> Game in progress! Added as spectator.");
                AddSpectator(player);
                return;
            }
            
            // Add player to the game
            _activePlayers.Add(player.userID);
            _playerGameMode[player.userID] = modeKey;
            
            // Activate the top layer (layer 0) so game can start dropping tiles there
            ActivateLayer(modeKey, 0);
            
            if (!_playerData.ContainsKey(player.userID))
                _playerData[player.userID] = new PlayerData();
            _playerData[player.userID].GameMode = modeKey;
            
            // Set current game mode to this mode (first player sets the mode)
            _currentGameMode = modeKey;
            
            // Teleport player to arena
            TeleportToArena(player, modeKey);
                
            player.ChatMessage($"<color=#1DB954>[FALLING TILES]</color> You joined {mode.Name}!");
            player.ChatMessage($"<color=#FFD700>[FALLING TILES]</color> Request a song with /sr <song name> to start the game!");
            UpdateHUD();
        }
        
        private void BuildArenaForMode(string modeKey)
        {
            if (!_config.GameModes.ContainsKey(modeKey)) return;
            
            // Clear only this mode's arena, not all arenas
            ClearArenaForMode(modeKey);
            
            var mode = _config.GameModes[modeKey];
            
            if (mode.ArenaSpawn == Vector3.zero) return;
            
            // Initialize the tiles HashSet for this mode
            if (!_arenasByMode.ContainsKey(modeKey))
                _arenasByMode[modeKey] = new HashSet<BuildingBlock>();
            
            int halfSize = mode.ArenaSize / 2;
            
            // Create arena based on shape
            switch (mode.ArenaShape?.ToLower())
            {
                case "circle":
                    CreateCircleArena(modeKey, mode.ArenaSpawn, halfSize, mode.ArenaLayers);
                    break;
                case "hexagon":
                    CreateHexagonArena(modeKey, mode.ArenaSpawn, halfSize, mode.ArenaLayers);
                    break;
                case "star":
                    CreateStarArena(modeKey, mode.ArenaSpawn, halfSize, mode.ArenaLayers);
                    break;
                case "heart":
                    CreateHeartArena(modeKey, mode.ArenaSpawn, halfSize, mode.ArenaLayers);
                    break;
                default: // square
                    CreateSquareArena(modeKey, mode.ArenaSpawn, halfSize, mode.ArenaLayers);
                    break;
            }
            
            Puts($"Arena built for {mode.Name} with {_arenasByMode[modeKey].Count} tiles ({mode.ArenaShape} shape, {mode.ArenaPattern} pattern)");
        }
        
        private void ClearArenaForMode(string modeKey)
        {
            if (_arenasByMode.ContainsKey(modeKey))
            {
                foreach (var tile in _arenasByMode[modeKey].Where(x => x != null))
                    tile.Kill();
                _arenasByMode[modeKey].Clear();
            }
        }
        
        // Helper to get current mode's tiles
        private HashSet<BuildingBlock> GetCurrentTiles()
        {
            if (_arenasByMode.ContainsKey(_currentGameMode))
                return _arenasByMode[_currentGameMode];
            return new HashSet<BuildingBlock>();
        }
        
        // --- LAYER ACTIVATION HELPERS ---
        // Calculate which layer index a tile is on based on its Y position relative to arena spawn
        private int GetTileLayerIndex(string modeKey, BuildingBlock tile)
        {
            if (!_config.GameModes.ContainsKey(modeKey)) return 0;
            var mode = _config.GameModes[modeKey];
            if (mode.ArenaSpawn == Vector3.zero) return 0;
            
            float yOffset = mode.ArenaSpawn.y - tile.transform.position.y;
            int layerIndex = Mathf.RoundToInt(yOffset / _config.LayerHeight);
            return Mathf.Clamp(layerIndex, 0, mode.ArenaLayers - 1);
        }
        
        // Check if a layer is activated (a player has been on it)
        private bool IsLayerActivated(string modeKey, int layerIndex)
        {
            if (!_activatedLayers.ContainsKey(modeKey)) return false;
            return _activatedLayers[modeKey].Contains(layerIndex);
        }
        
        // Activate a layer (called when player touches it)
        private void ActivateLayer(string modeKey, int layerIndex)
        {
            if (!_activatedLayers.ContainsKey(modeKey))
                _activatedLayers[modeKey] = new HashSet<int>();
            
            if (!_activatedLayers[modeKey].Contains(layerIndex))
            {
                _activatedLayers[modeKey].Add(layerIndex);
                Puts($"[LAYER] Activated layer {layerIndex} in {modeKey} mode");
            }
        }
        
        // Reset all activated layers for a mode (called when game ends/resets)
        private void ResetActivatedLayers(string modeKey)
        {
            if (_activatedLayers.ContainsKey(modeKey))
                _activatedLayers[modeKey].Clear();
        }
        
        // Get tiles only from activated layers
        private HashSet<BuildingBlock> GetActivatedLayerTiles()
        {
            var allTiles = GetCurrentTiles();
            var result = new HashSet<BuildingBlock>();
            
            foreach (var tile in allTiles)
            {
                if (tile == null) continue;
                int layerIndex = GetTileLayerIndex(_currentGameMode, tile);
                if (IsLayerActivated(_currentGameMode, layerIndex))
                {
                    result.Add(tile);
                }
            }
            
            return result;
        }
        
        // Get tiles only from the top layer (layer 0) - fallback when no layers activated
        private HashSet<BuildingBlock> GetTopLayerTiles()
        {
            var allTiles = GetCurrentTiles();
            var result = new HashSet<BuildingBlock>();
            
            foreach (var tile in allTiles)
            {
                if (tile == null) continue;
                int layerIndex = GetTileLayerIndex(_currentGameMode, tile);
                if (layerIndex == 0)
                {
                    result.Add(tile);
                }
            }
            
            return result;
        }
        
        [ConsoleCommand("ft.canceljoin")]
        private void ConsoleCancelJoin(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player == null) return;
            
            CuiHelper.DestroyUi(player, "FT_JoinConfirm");
            _playersInSphere.Remove(player.userID);
            TeleportToLobby(player);
        }
        
        void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            // Clean up UI for disconnected players
            CuiHelper.DestroyUi(player, "FT_Menu");
            CuiHelper.DestroyUi(player, "FT_HUD");
            CuiHelper.DestroyUi(player, "FT_LYR");
            CuiHelper.DestroyUi(player, "FT_Leaderboard");
            CuiHelper.DestroyUi(player, "FT_Countdown");
            CuiHelper.DestroyUi(player, "FT_JoinConfirm");
            
            // Remove from sphere check
            _playersInSphere.Remove(player.userID);
            
            // Remove from active players if in game
            if (_activePlayers.Contains(player.userID))
            {
                _activePlayers.Remove(player.userID);
                _playerGameMode.Remove(player.userID);
                UpdateHUD();
            }
            
            // Remove from spectators
            if (_spectators.Contains(player.userID))
            {
                _spectators.Remove(player.userID);
                _playerGameMode.Remove(player.userID);
            }
        }

        // --- COMMANDS ---
        [ChatCommand("sr")]
        private void CmdSongRequest(BasePlayer player, string cmd, string[] args) {
            if (args.Length == 0) { player.ChatMessage("<color=#1DB954>[FALLING TILES]</color> Usage: /sr <song name>"); return; }
            
            // Check if arena is ready
            if (GetCurrentTiles().Count == 0) {
                player.ChatMessage("<color=#FF0000>[FALLING TILES]</color> No arena ready! Join a game mode first via the teleport spheres.");
                return;
            }
            
            // Check if there are players waiting
            if (_activePlayers.Count == 0) {
                player.ChatMessage("<color=#FF0000>[FALLING TILES]</color> No players in the arena! Join a game mode first.");
                return;
            }
            
            string query = UnityWebRequest.EscapeURL(string.Join(" ", args));
            webrequest.Enqueue($"{_config.NgrokLink}/request-song?q={query}", null, (code, res) => {
                if (code != 200) {
                    player.ChatMessage("<color=#FF0000>[FALLING TILES]</color> Failed to request song!");
                } else {
                    player.ChatMessage("<color=#1DB954>[FALLING TILES]</color> Song requested!");
                    
                    // Start the game if not already active (arena and players already validated above)
                    if (!_gameActive) {
                        StartCountdown();
                    }
                }
            }, this);
        }

        [ChatCommand("srl")]
        private void CmdLink(BasePlayer player) {
            Item n = ItemManager.CreateByName("note", 1);
            if (n == null) return;
            n.name = "Falling Tiles Access Pass";
            n.text = _config.NgrokLink;
            if (!n.MoveToContainer(player.inventory.containerBelt)) n.MoveToContainer(player.inventory.containerMain);
            player.ChatMessage("<color=#1DB954>[FALLING TILES]</color> Link forced to your hotbar!");
        }
        
        [ChatCommand("ftjoin")]
        private void CmdJoin(BasePlayer player, string cmd, string[] args) {
            if (_gameActive) {
                player.ChatMessage("<color=#FF0000>[FALLING TILES]</color> Game already in progress! You'll be added as spectator.");
                AddSpectator(player);
                return;
            }
            if (!_activePlayers.Contains(player.userID)) {
                _activePlayers.Add(player.userID);
                if (!_playerData.ContainsKey(player.userID))
                    _playerData[player.userID] = new PlayerData();
                _playerGameMode[player.userID] = _currentGameMode;
                player.ChatMessage("<color=#1DB954>[FALLING TILES]</color> You joined the game!");
                UpdateHUD();
            }
        }
        
        [ChatCommand("ftleave")]
        private void CmdLeave(BasePlayer player, string cmd, string[] args) {
            bool wasInGame = false;
            
            if (_activePlayers.Contains(player.userID)) {
                _activePlayers.Remove(player.use
// ... [Code preview truncated due to length. Full source in attached file] ...

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