Faça um descrição com poucas palavras para esse plugin, em ingles


using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using Rust;
using UnityEngine;
using Random = UnityEngine.Random;

namespace Oxide.Plugins
{
    [Info("Dungeon Events", "Marte6", "1.0.0")]
    public class DungeonEvents : RustPlugin
    {
        private ConfigData _configData;
        private DynamicConfigFile _dataFile;
        private DungeonData _dungeonData;
        private Timer _autoSpawnTimer;
        private Timer _dungeonCheckTimer;
        private Timer _updateMarkersTimer;
        private Timer _spawnPointScanner;
        private List<(Vector3 Position, Quaternion Rotation)> _validSpawnPoints = new List<(Vector3, Quaternion)>();
        private Dictionary<DungeonTierConfig, string> _tierNameMap;
        private const ulong OwnerID = 10203040505;
        private const float TimeTimers = 30f;

        #region Hooks
        [PluginReference]
        private readonly Plugin NpcSpawn;

        private void OnServerInitialized()
        {
            InitializeTierNameMap();
            LoadData();
            RemoveAllDungeons();
            StartSpawnPointScanner();
            StartTimerAutoSpawnDungeons();
            StartTimerRemoveInactiveDungeons();
            StartTimerUpdateMarkers();
            SaveData();
        }

        private void OnServerSave() => SaveData();

        private void Unload()
        {
            RemoveAllDungeons();
            _autoSpawnTimer?.Destroy();
            _dungeonCheckTimer?.Destroy();
            _updateMarkersTimer?.Destroy();
            _spawnPointScanner?.Destroy();
            SaveData();
        }

        object OnTurretTarget(AutoTurret turret, BaseCombatEntity target)
        {
            if (IsDungeonEntity(target))
            {
                return false;
            }

            return null;
        }

        object OnEntityTakeDamage(ScientistNPC scientistNPC, HitInfo info)
        {
            if (scientistNPC == null || info == null || info.Initiator == null)
                return null;

            AutoTurret autoTurret = info.Initiator as AutoTurret;
            if (autoTurret == null)
                return null;
            if (IsDungeonEntity(autoTurret))
                return true;

            return null;
        }

        object CanEntityTakeDamage(AutoTurret autoTurret, HitInfo hitinfo)
        {
            if (autoTurret == null || hitinfo == null)
                return null;

            if (IsDungeonEntity(autoTurret))
            {
                if (hitinfo.InitiatorPlayer == null || !IsPlayer(hitinfo.Initiator as BasePlayer))
                    return false;
                else
                    return true;
            }

            return null;
        }

        object CanEntityTakeDamage(StorageContainer storageContainer, HitInfo hitinfo)
        {
            if (storageContainer == null || hitinfo == null)
                return null;

            if (IsDungeonEntity(storageContainer))
            {
                if (hitinfo.InitiatorPlayer == null || !IsPlayer(hitinfo.Initiator as BasePlayer))
                    return false;
                else
                    return true;
            }

            return null;
        }

        private object OnPortalUse(BasePlayer player, BasePortal portal)
        {
            ActiveDungeon dungeon = GetDungeonByPortal(portal);
            if (dungeon == null)
            {
                return null;
            }

            if (!dungeon.Spawned)
            {
                Puts($"Player {player.displayName} tried to enter a dungeon that is still spawning.");
                player?.ChatMessage(Msg(player.UserIDString, "DungeonStillSpawningMessage"));

                return false;
            }

            return null;
        }

        #endregion

        #region Chat Commands

        [ChatCommand("dun")]
        private void CreateDungeonCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            LogDebug("Received dungeon creation request.", 1);
            player?.ChatMessage("Received dungeon creation request.");

            AutoSpawnDungeon(player);
        }

        [ChatCommand("RemoveInactiveDungeons")]
        private void RemoveInactiveDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            var removedDungeons = RemoveInactiveDungeons();
            NotifyPlayerOnRemoval(player, removedDungeons);
        }

        [ChatCommand("RemoveAllDungeons")]
        private void RemoveAllDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            var removedDungeons = RemoveAllDungeons();
            NotifyPlayerOnRemoval(player, removedDungeons);
        }

        [ChatCommand("ForceRemoveAllDungeons")]
        private void ForceRemoveAllDungeonsCommand(BasePlayer player, string command, string[] args)
        {
            if (!IsAdmin(player))
                return;

            CleanupExistingEntities();
            player.ChatMessage("All entities related to this plugin have been cleaned up.");
        }

        #endregion

        #region Dungeon Management

        private class DungeonRequest
        {
            public BasePlayer Player { get; set; }
            public bool IsAutoSpawn { get; set; }
        }

        private void StartSpawnPointScanner()
        {
            _spawnPointScanner = timer.Every(
                TimeTimers,
                () =>
                {
                    ScanSpawnPoints();
                }
            );
        }

        private void StartTimerAutoSpawnDungeons()
        {
            if (_configData.DungeonSpawn.EnableAutoSpawn)
            {
                _autoSpawnTimer = timer.Every(
                    TimeTimers,
                    () =>
                    {
                        AutoSpawnDungeon();
                    }
                );
            }
        }

        private void StartTimerRemoveInactiveDungeons()
        {
            _dungeonCheckTimer = timer.Every(
                TimeTimers,
                () =>
                {
                    var removedDungeons = RemoveInactiveDungeons();
                    if (removedDungeons.Count > 0)
                    {
                        foreach (var dungeon in removedDungeons)
                        {
                            LogDebug($"Removed inactive dungeon of tier {dungeon.TierName} at grid {dungeon.Grid}.", 1);
                        }
                    }
                }
            );
        }

        private void StartTimerUpdateMarkers()
        {
            _updateMarkersTimer = timer.Every(TimeTimers, () => UpdateMarkers());
        }

        private void ScanSpawnPoints()
        {
            int pointsScanned = 0;
            int maxPointsToFind = 1000;

            int initialCount = _validSpawnPoints.Count;

            while (pointsScanned < 100 && _validSpawnPoints.Count < maxPointsToFind)
            {
                if (TryFindDungeonSpawnPoint(out var position, out var rotation))
                {
                    if (IsSafeFromOtherDungeons(position) && !IsCloseToMonuments(position) && IsFarFromExistingSpawnPoints(position, _configData.DungeonSpawn.MinDistanceBetweenDungeons))
                    {
                        _validSpawnPoints.Add((position, rotation));
                    }
                }

                pointsScanned++;
            }

            int addedPoints = _validSpawnPoints.Count - initialCount;
            if (addedPoints > 0)
            {
                LogDebug($"Added {addedPoints} dungeon spawn points. Total now: {_validSpawnPoints.Count}");
            }
        }

        private bool IsFarFromExistingSpawnPoints(Vector3 position, float minDistance)
        {
            foreach (var existingPoint in _validSpawnPoints)
            {
                if (Vector3.Distance(existingPoint.Position, position) < minDistance)
                {
                    return false;
                }
            }
            return true;
        }

        private (Vector3 Position, Quaternion Rotation)? GetNextSpawnPoint()
        {
            if (_validSpawnPoints.Count == 0)
            {
                LogDebug("No spawn points available. Please wait for automatic scanning.");
                return null;
            }

            var nextPoint = _validSpawnPoints.First();
            _validSpawnPoints.RemoveAt(0);
            return nextPoint;
        }

        private void RecycleUsedSpawnPoints()
        {
            if (_validSpawnPoints.Count == 0)
            {
                LogDebug("Recycling used spawn points.");
                ScanSpawnPoints();
            }
        }

        private void UpdateMarkers()
        {
            var mapMarkers = UnityEngine.Object.FindObjectsOfType<MapMarkerGenericRadius>();
            foreach (var marker in mapMarkers.Where(marker => marker.IsValid()))
            {
                marker.SendUpdate();
            }
        }

        private void AutoSpawnDungeon(BasePlayer player = null)
        {
            ScanSpawnPoints();

            if (_dungeonData.ActiveDungeons.Count >= _configData.DungeonSpawn.MaxTotalActiveDungeons)
            {
                LogDebug(lang.GetMessage("MaxActiveDungeonsReached", this), 2);
                player?.ChatMessage(Msg(player.UserIDString, "MaxActiveDungeonsReached"));
                return;
            }

            var spawnPoint = GetNextSpawnPoint();
            if (spawnPoint == null)
            {
                LogDebug("Failed to create dungeon: No spawn points available.");
                player?.ChatMessage(Msg(player.UserIDString, "NoSpawnPointsAvailable"));
                return;
            }

            var (position, rotation) = spawnPoint.Value;

            if (!IsSafeFromOtherDungeons(position))
            {
                LogDebug($"The point {position} is not safe for creation.");
                RecycleUsedSpawnPoints();
                return;
            }

            LogDebug($"Using spawn point: Position = {position}, Rotation = {rotation}");
            ServerMgr.Instance.StartCoroutine(CreateDungeon(position, rotation, player));
        }

        private IEnumerator CreateDungeon(Vector3 position, Quaternion rotation, BasePlayer player = null)
        {
            string prefabPath = GetRandomDungeonPrefab();
            if (!ValidatePrefabPath(prefabPath))
            {
                LogDebug("Invalid prefab path for the dungeon.", 2);
                yield break;
            }

            if (_configData.DungeonSpawn.MaxDungeonLifetime > 0)
            {
                XmasDungeon.xmaslifetime = _configData.DungeonSpawn.MaxDungeonLifetime;
                HalloweenDungeon.lifetime = _configData.DungeonSpawn.MaxDungeonLifetime;
            }
            else
            {
                XmasDungeon.xmaslifetime = 3600;
                HalloweenDungeon.lifetime = 3600;
            }

            if (!(GameManager.server.CreateEntity(prefabPath, position, rotation) is BasePortal dungeonPortal))
            {
                LogDebug("Failed to create the dungeon portal entity.", 2);
                yield break;
            }

            dungeonPortal.OwnerID = OwnerID;
            dungeonPortal.Spawn();

            var grid = PhoneController.PositionToGridCoord(position);

            LogDebug($"Trying to spawn Dungeon at grid: {grid}.");

            ProceduralDynamicDungeon dungeon = GetProceduralDungeon(dungeonPortal);
            if (!ValidateProceduralDungeon(dungeon, dungeonPortal, grid))
                yield break;

            yield return new WaitForSeconds(3f);
            yield return ServerMgr.Instance.StartCoroutine(RemoveOriginalEntities(dungeon));
            yield return new WaitForSeconds(3f);

            LogDebug($"SEED: {dungeon.baseseed} {dungeonPortal.GetType().Name}");

            var spawnPoints = GetAllSpawnPoints(dungeon);
            if (spawnPoints.Count == 0)
            {
                LogDebug("Dungeon Spawn failed! No valid spawn points found.", 2);
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                yield break;
            }

            DungeonTierConfig tierConfig = GetDungeonTier(spawnPoints.Count);
            LogDebug($"Selected dungeon tier: {_tierNameMap[tierConfig]} at grid: {grid}.");

            if (!tierConfig.IsValid())
            {
                LogDebug("Invalid tier configuration: Percentages must sum to 100%.", 2);
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                yield break;
            }

            var activeDungeon = RegisterActiveDungeon(dungeonPortal, dungeon, position, grid, tierConfig);

            int totalPoints = spawnPoints.Count;
            int turretCount = (int)(totalPoints * (tierConfig.TurretPercentage / 100f));
            int boxCount = (int)(totalPoints * (tierConfig.LootBoxPercentage / 100f));
            int npcCount = totalPoints - turretCount - boxCount;

            var turretSpawns = spawnPoints.Take(turretCount).ToList();
            var boxSpawns = spawnPoints.Skip(turretCount).Take(boxCount).ToList();
            var npcSpawns = spawnPoints.Skip(turretCount + boxCount).Take(npcCount).ToList();

            LogDebug($"Spawn points found: {totalPoints}, Turrets: {turretCount}, Boxes: {boxCount}, NPCs: {npcCount}");

            SpawnEntities(turretSpawns, "AutoTurret", activeDungeon, dungeon);
            SpawnEntities(boxSpawns, "StorageContainer", activeDungeon, dungeon);
            SpawnEntities(npcSpawns, "NPC", activeDungeon, dungeon);

            LogDebug($"Finished placing entities. Total placed: {activeDungeon.EntityIds.Count}/{spawnPoints.Count}.");

            CreateDungeonMarkers(activeDungeon);
            NotifyPlayersOfNewDungeon(activeDungeon);

            LogDebug($"{_tierNameMap[tierConfig]} Dungeon spawned successfully at grid: {activeDungeon.Grid}.", 1);

            activeDungeon.Spawned = true;

            player?.ChatMessage(Msg(player.UserIDString, "DungeonSpawnedSuccess", activeDungeon.Grid));
        }

        private ProceduralDynamicDungeon GetProceduralDungeon(BasePortal dungeonPortal)
        {
            return (dungeonPortal as XmasDungeon)?.dungeonInstance.Get(true) ?? (dungeonPortal as HalloweenDungeon)?.dungeonInstance.Get(true);
        }

        private List<Vector3> GetAllSpawnPoints(ProceduralDynamicDungeon dungeon)
        {
            var basePoints = dungeon
                .spawnedCells.SelectMany(cell => cell.spawnGroups)
                .SelectMany(group => group.spawnPoints)
                .Select(point =>
                {
                    point.GetLocation(out var position, out _);
                    return position;
                })
                .ToList();

            var additionalPoints = new List<Vector3>();
            var totalPoints = new List<Vector3>();

            foreach (var point in basePoints)
            {
                var nearbyPoint = GenerateNearbyPoint(point);
                if (IsValidPoint(nearbyPoint))
                    additionalPoints.Add(nearbyPoint);
            }

            totalPoints.AddRange(additionalPoints);
            totalPoints.AddRange(basePoints);

            LogDebug($"Total spawn points generated: {totalPoints.Count} (Base points: {basePoints.Count}, Additional points: {additionalPoints.Count})");

            return totalPoints;
        }

        private Vector3 GenerateNearbyPoint(Vector3 originalPosition)
        {
            float maxDistance = 3f;

            float offsetX = Random.Range(-maxDistance, maxDistance);
            float offsetZ = Random.Range(-maxDistance, maxDistance);

            Vector3 nearbyPoint = new Vector3(originalPosition.x + offsetX, originalPosition.y, originalPosition.z + offsetZ);

            return nearbyPoint;
        }

        private bool IsValidPoint(Vector3 position)
        {
            return GamePhysics.CheckSphere(position, 1f, Layers.Mask.Player_Server | Layers.Server.Deployed, QueryTriggerInteraction.Ignore);
        }

        private JObject NpcSpawnObjectConfig(NpcConfig npcConfig)
        {
            return new JObject
            {
                ["Name"] = npcConfig.Name,
                ["HostileTargetsOnly"] = false,
                ["States"] = new JArray { "RoamState", "ChaseState", "CombatState" },
                ["AttackRangeMultiplier"] = 1f,
                ["SenseRange"] = 60f,
                ["MemoryDuration"] = 30f,
                ["CheckVisionCone"] = false,
                ["VisionCone"] = 135f,
                ["Health"] = npcConfig.Health,
                ["RoamRange"] = npcConfig.RoamRange,
                ["ChaseRange"] = npcConfig.ChaseRange,
                ["WearItems"] = new JArray(npcConfig.WearItems.Items.Select(item => new JObject { ["ShortName"] = item.ShortName, ["SkinID"] = item.SkinID })),
                ["BeltItems"] = new JArray(
                    npcConfig.BeltItems.Items.Select(item => new JObject
                    {
                        ["ShortName"] = item.ShortName,
                        ["Amount"] = item.Amount,
                        ["SkinID"] = item.SkinID,
                        ["Mods"] = new JArray(item.Mods ?? new List<string>()),
                        ["Ammo"] = "",
                    })
                ),
                ["TurretDamageScale"] = 1f,
                ["AimConeScale"] = 1.5f,
                ["DisableRadio"] = false,
                ["Stationary"] = false,
                ["CanUseWeaponMounted"] = true,
                ["CanRunAwayWater"] = false,
                ["Speed"] = 5f,
                ["AreaMask"] = 1,
                ["AgentTypeID"] = -1372625422,
                ["HomePosition"] = string.Empty,
            };
        }

        private void SpawnEntities(List<Vector3> points, string entityType, ActiveDungeon activeDungeon, ProceduralDynamicDungeon dungeon)
        {
            foreach (var point in points)
            {
                if (entityType == "NPC")
                {
                    var npcConfig = NpcSpawnObjectConfig(activeDungeon.TierConfig.NpcConfig);
                    ScientistNPC entity = (ScientistNPC)NpcSpawn.Call("SpawnNpc", point, npcConfig);
                    entity.OwnerID = OwnerID;

                    if (entity != null && !entity.IsDestroyed)
                    {
                        LogDebug($"Successfully spawned NPC at {point} with entity ID: {entity.net.ID}", 1);
                        InitializeEntity(entity, activeDungeon);
                    }
                }
                else
                {
                    var prefabPath = entityType switch
                    {
                        "AutoTurret" => "assets/prefabs/npc/autoturret/autoturret_deployed.prefab",
                        "StorageContainer" => "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab",
                        _ => throw new ArgumentException($"Unknown entity type: {entityType}"),
                    };

                    var entity = GameManager.server.CreateEntity(prefabPath, point);
                    if (entity != null)
                    {
                        entity.OwnerID = OwnerID;
                        entity.SetParent(dungeon, true, true);
                        entity.Spawn();

                        LogDebug($"Successfully spawned {entityType} at {point} with entity ID: {entity.net.ID}", 1);
                        InitializeEntity(entity, activeDungeon);
                    }
                    else
                    {
                        LogDebug($"Failed to create {entityType} at position {point}. Prefab: {prefabPath}");
                    }
                }
            }
        }

        private bool ValidatePrefabPath(string prefabPath)
        {
            if (!string.IsNullOrEmpty(prefabPath))
                return true;

            LogDebug("No prefab path available for dungeon spawning.");
            return false;
        }

        private bool ValidateProceduralDungeon(ProceduralDynamicDungeon proceduralDungeon, BasePortal dungeonPortal, string grid)
        {
            if (proceduralDungeon == null)
            {
                LogDebug($"ProceduralDynamicDungeon not found, destroying the Dungeon at grid: {grid}.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return false;
            }

            if (proceduralDungeon.spawnedCells.Count < 1)
            {
                LogDebug($"No cells found, destroying the Dungeon at grid: {grid}.");
                dungeonPortal.Kill(BaseNetworkable.DestroyMode.None);
                return false;
            }

            return true;
        }

        private ActiveDungeon RegisterActiveDungeon(BasePortal dungeonPortal, ProceduralDynamicDungeon proceduralDungeon, Vector3 position, string grid, DungeonTierConfig selectedTier)
        {
            var activeDungeon = new ActiveDungeon
            {
                PortalId = dungeonPortal.net.ID.Value,
                DynamicDungeonId = proceduralDungeon.net.ID.Value,
                Position = position,
                Grid = grid,
                TierName = _tierNameMap[selectedTier],
                TierConfig = selectedTier,
                EntityIds = new List<ulong>(),
                Spawned = false,
            };

            _dungeonData.ActiveDungeons.Add(activeDungeon);

            return activeDungeon;
        }

        private List<(Vector3 Position, Quaternion Rotation)> GetAvailableSpawnLocations(ProceduralDynamicDungeon proceduralDungeon, string prefabPath)
        {
            GameObject prefab = GameManager.server.FindPrefab(prefabPath);
            if (prefab == null)
            {
                Puts($"Prefab not found: {prefabPath}");
                return new List<(Vector3, Quaternion)>();
            }

            BaseEntity prefabEntity = prefab.GetComponent<BaseEntity>();
            if (prefabEntity == null)
            {
                Puts($"Prefab does not have a BaseEntity: {prefabPath}");
                return new List<(Vector3, Quaternion)>();
            }

            return proceduralDungeon
                .spawnedCells.Where(cell => cell != null)
                .SelectMany(cell => cell.spawnGroups)
                .Where(group => group != null)
                .SelectMany(group => group.spawnPoints)
                .Where(spawnPoint => spawnPoint != null && spawnPoint.IsAvailableTo(prefab))
                .Select(spawnPoint =>
                {
                    spawnPoint.GetLocation(out var position, out var rotation);
                    return (position, rotation);
                })
                .ToList();
        }

        private void InitializeEntity(BaseEntity entity, ActiveDungeon activeDungeon)
        {
            if (entity != null)
            {
                activeDungeon.EntityIds.Add(entity.net.ID.Value);

                if (entity is NPCPlayer)
                {
                    activeDungeon.NpcIds.Add(entity.net.ID.Value);
                }
                else if (entity is AutoTurret turret)
                {
                    ConfigureTurret(turret, activeDungeon.TierConfig.AutoTurretConfig);
                }
                else if (entity is StorageContainer storage)
                {
                    ConfigureStorage(storage, activeDungeon.TierConfig);
                }
            }
        }

        private IEnumerator RemoveOriginalEntities(ProceduralDynamicDungeon proceduralDungeon)
        {
            foreach (var cell in proceduralDungeon.spawnedCells)
            {
                foreach (var group in cell.spawnGroups)
                {
                    for (int i = group.spawnInstances.Count - 1; i >= 0; i--)
                    {
                        SpawnPointInstance spawnPointInstance = group.spawnInstances[i];
                        BaseEntity entity = spawnPointInstance?.gameObject?.ToBaseEntity();

                        if (entity != null && !IsDungeonEntity(entity) && (entity is NPCDwelling || entity is ScarecrowNPC || entity is LootContainer))
                        {
                            LogDebug($"Removing original entity: {entity.ShortPrefabName} at {entity.transform.position}");
                            entity.Kill();
                        }
                    }
                }
            }

            yield return null;
        }

        private void ConfigureTurret(AutoTurret turret, TurretConfig turretConfig)
        {
            turret.InitializeHealth(turretConfig.Health, turretConfig.Health);

            var weapon = ItemManager.CreateByName(turretConfig.WeaponShortName);
            weapon?.MoveToContainer(turret.inventory, 0);

            var laserSight = ItemManager.CreateByName("weapon.mod.lasersight");
            laserSight?.MoveToContainer(weapon.contents);

            turret.UpdateAttachedWeapon();
            turret.gameObject.AddComponent<TurretMonoBehaviour>();
        }

        public class TurretMonoBehaviour : MonoBehaviour
        {
            private AutoTurret _turret;

            private void Awake()
            {
                _turret = GetComponent<AutoTurret>();

                _turret.SetPeacekeepermode(false);
                _turret.InitiateStartup();
                _turret.isLootable = false;
                _turret.dropFloats = false;
                _turret.dropsLoot = false;

                InvokeRepeating(nameof(RefillAmmo), 2f, 30f);
            }

            private void RefillAmmo()
            {
                int maxReserveAmmo = 1000;

                if (_turret.AttachedWeapon is not BaseProjectile baseProjectile || baseProjectile.primaryMagazine?.ammoType == null)
                    return;

                var ammoType = baseProjectile.primaryMagazine.ammoType;
                int currentAmmoCount = _turret.inventory.itemList.Where(item => item.info == ammoType).Sum(item => item.amount);

                if (currentAmmoCount < maxReserveAmmo)
                {
                    int ammoNeeded = maxReserveAmmo - currentAmmoCount;
                    Item ammoItem = ItemManager.Create(ammoType, ammoNeeded);
                    ammoItem?.MoveToContainer(_turret.inventory);

                    _turret.UpdateTotalAmmo();
                    _turret.EnsureReloaded();
                    _turret.SendNetworkUpdateImmediate();
                }
            }

            private void OnDestroy()
            {
                CancelInvoke(nameof(RefillAmmo));
            }
        }

        private void ConfigureStorage(StorageContainer storage, DungeonTierConfig tierConfig)
        {
            storage.skinID = tierConfig.SmallWoodBoxSkinID;
            AddCodeLock(storage);
            PopulateStorage(storage.inventory, tierConfig, _configData.LootBoxConfig.LootItems);
        }

        private void AddCodeLock(StorageContainer storage)
        {
            var prefabName = "assets/prefabs/locks/keypad/lock.code.prefab";
            var codeLock = GameManager.server.CreateEntity(prefabName) as CodeLock;
            if (codeLock == null)
                return;

            codeLock.SetParent(storage, storage.GetSlotAnchorName(BaseEntity.Slot.Lock));
            codeLock.Spawn();
            codeLock.code = Random.Range(1000, 9999).ToString();
            codeLock.hasCode = true;
            codeLock.SetFlag(BaseEntity.Flags.Locked, true);
        }

        private void PopulateStorage(ItemContainer container, DungeonTierConfig tierConfig, List<ItemConfig> lootItems)
        {
            var shuffledItems = lootItems.OrderBy(_ => Random.value).ToList();
            var differentItemsCount = 0;

            foreach (var itemConfig in shuffledItems)
            {
                if (differentItemsCount >= tierConfig.MaxItemsPerBox)
                {
                    break;
                }

                if (Random.value * 100f <= itemConfig.InclusionChancePercentage)
                {
                    var itemDefinition = ItemManager.FindItemDefinition(itemConfig.ShortName);
                    if (itemDefinition != null)
                    {
                        var amount = Random.Range(itemConfig.MinimumAmount, itemConfig.MaximumAmount + 1);
                        if (container.itemList.Count < container.capacity)
                        {
                            var item = ItemManager.Create(itemDefinition, amount);
                            item.MoveToContainer(container);
                            differentItemsCount++;
                        }
                    }
                }
            }
        }

        private List<ActiveDungeon> RemoveInactiveDungeons()
        {
            var removedDungeons = new List<ActiveDungeon>();

            foreach (var dungeon in _dungeonData.ActiveDungeons.ToList())
            {
                if (CanBeRemoved(dungeon))
                {
                    removedDungeons.Add(dungeon);
                    DestroyDungeon(dungeon);
                }
            }

            SaveData();

            return removedDungeons;
        }

        private void DestroyDungeon(ActiveDungeon dungeon)
        {
            var proceduralDynamicDungeon = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.DynamicDungeonId)) as ProceduralDynamicDungeon;
            if (proceduralDynamicDungeon != null)
            {
                RemoveLootableCorpse(proceduralDynamicDungeon);
                RemoveItemDrop(proceduralDynamicDungeon);
                RemoveDroppedItemContainer(proceduralDynamicDungeon);
            }

            var dungeonEntity = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.PortalId)) as HalloweenDungeon;
            if (dungeonEntity != null)
            {
                dungeonEntity?.Kill(BaseNetworkable.DestroyMode.None);
            }

            DestroyEntities(dungeon);
            RemoveDungeonMarkers(dungeon);
            _dungeonData.ActiveDungeons.Remove(dungeon);
        }

        private void RemoveDroppedItemContainer(ProceduralDynamicDungeon dungeon)
        {
            List<DroppedItemContainer> obj = Pool.Get<List<DroppedItemContainer>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (DroppedItemContainer item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void RemoveItemDrop(ProceduralDynamicDungeon dungeon)
        {
            List<DroppedItem> obj = Pool.Get<List<DroppedItem>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (DroppedItem item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void RemoveLootableCorpse(ProceduralDynamicDungeon dungeon)
        {
            List<LootableCorpse> obj = Pool.Get<List<LootableCorpse>>();
            Vis.Entities(dungeon.transform.position, 80f, obj);
            foreach (LootableCorpse item in obj)
            {
                if (item.IsValid() && !item.IsDestroyed)
                {
                    item?.Kill();
                }
            }

            Pool.FreeUnmanaged(ref obj);
        }

        private void DestroyEntities(ActiveDungeon dungeon)
        {
            foreach (var entityId in dungeon.EntityIds)
            {
                var entity = BaseNetworkable.serverEntities.Find(new NetworkableId(entityId)) as BaseEntity;
                entity?.Kill(BaseNetworkable.DestroyMode.None);
            }

            dungeon.EntityIds.Clear();
        }

        private void RemoveDungeonMarkers(ActiveDungeon dungeon)
        {
            foreach (var marker in _dungeonData.ActiveDungeonMarkers.Where(m => m.Position == dungeon.Position).ToList())
            {
                var vendingMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.VendingMarker)) as BaseEntity;
                var radiusMarker = BaseNetworkable.serverEntities.Find(new NetworkableId(marker.RadiusMarker)) as BaseEntity;

                vendingMarker?.Kill(BaseNetworkable.DestroyMode.None);
                radiusMarker?.Kill(BaseNetworkable.DestroyMode.None);

                _dungeonData.ActiveDungeonMarkers.Remove(marker);
            }
        }

        private IEnumerable<ActiveDungeon> RemoveAllDungeons()
        {
            var removedDungeons = new List<ActiveDungeon>(_dungeonData.ActiveDungeons);
            foreach (var dungeon in removedDungeons)
            {
                DestroyDungeon(dungeon);
            }
            _dungeonData = new DungeonData();
            SaveData();
            return removedDungeons;
        }

        #endregion

        #region Utility Methods

        private ActiveDungeon GetDungeonByPortal(BasePortal portal)
        {
            return _dungeonData.ActiveDungeons.FirstOrDefault(d => d.PortalId == portal.net.ID.Value);
        }

        private void LogDebug(string message, int level = 0)
        {
            if (string.IsNullOrEmpty(message))
                return;

            string formattedMessage = $"[{Name}] {message}";

            switch (level)
            {
                case 1:
                    if (_configData.EnableWarningLogs)
                        Debug.LogWarning(formattedMessage);
                    break;
                case 2:
                    if (_configData.EnableWarningLogs)
                        Debug.LogError(formattedMessage);
                    break;
                default:
                    if (_configData.EnableDebugLogs)
                        Debug.Log(formattedMessage);
                    break;
            }
        }

        private void CleanupExistingEntities()
        {
            foreach (
                var entityType in new Type[]
                {
                    typeof(VendingMachineMapMarker),
                    typeof(MapMarkerGenericRadius),
                    typeof(BasePortal),
                    typeof(ProceduralDynamicDungeon),
                    typeof(AutoTurret),
                    typeof(NPCPlayer),
                    typeof(StorageContainer),
                }
            )
            {
                foreach (var obj in UnityEngine.Object.FindObjectsOfType(entityType))
                {
                    if (obj is BaseEntity baseEntity && baseEntity.OwnerID == OwnerID)
                    {
                        baseEntity.Kill(BaseNetworkable.DestroyMode.None);
                    }
                }
            }

            _dungeonData = new DungeonData();
        }

        private void InitializeTierNameMap()
        {
            _tierNameMap = new Dictionary<DungeonTierConfig, string>
            {
                { _configData.Tiers.Easy, "Easy" },
                { _configData.Tiers.Normal, "Normal" },
                { _configData.Tiers.Medium, "Medium" },
                { _configData.Tiers.Hard, "Hard" },
                { _configData.Tiers.Nightmare, "Nightmare" },
            };
        }

        private void LoadData()
        {
            _dataFile = Interface.Oxide.DataFileSystem.GetFile("Dungeons_Data");
            _dungeonData = _dataFile.ReadObject<DungeonData>() ?? new DungeonData();
        }

        private void SaveData()
        {
            _dataFile.WriteObject(_dungeonData);
        }

        private static bool IsPlayer(BasePlayer targetPlayer) => targetPlayer != null && targetPlayer.userID.IsSteamId();

        private void NotifyPlayersOfNewDungeon(ActiveDungeon activeDungeon)
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                var message = Msg(player.UserIDString, "DungeonAppeared", activeDungeon.TierName, activeDungeon.Grid);
                player.ChatMessage(message);

                if (_configData.EnableShowToast)
                {
                    player.ShowToast(GameTip.Styles.Blue_Normal, message);
                }
            }
        }

        private void NotifyPlayerOnRemoval(BasePlayer player, IEnumerable<ActiveDungeon> removedDungeons)
        {
            if (!removedDungeons.Any())
            {
                player.ChatMessage(Msg(player.UserIDString, "NoInactiveDungeonsToRemove"));
                return;
            }

            foreach (var dungeon in removedDungeons)
            {
                player.ChatMessage(Msg(player.UserIDString, "DungeonRemovedDetail", dungeon.TierName, dungeon.Grid));
            }
        }

        private bool IsAdmin(BasePlayer player) => player != null && player.IsAdmin;

        private bool IsDungeonEntity(BaseEntity entity) => entity != null && _dungeonData.ActiveDungeons.Any(d => d.EntityIds.Contains(entity.net.ID.Value));

        private string GetRandomDungeonPrefab()
        {
            var prefabs = new List<string>();
            if (_configData.DungeonSpawn.EnableXmasDungeon)
                prefabs.Add("assets/prefabs/missions/portal/xmasportalentry.prefab");
            if (_configData.DungeonSpawn.EnableHalloweenDungeon)
                prefabs.Add("assets/prefabs/missions/portal/halloweenportalentry.prefab");
            return prefabs.Count > 0 ? prefabs[Random.Range(0, prefabs.Count)] : null;
        }

        private static bool IsValidPosition(Vector3 position, out Vector3 suitablePosition, out Quaternion suitableRotation)
        {
            suitablePosition = Vector3.zero;
            suitableRotation = Quaternion.identity;

            if (!LocationCheck.GetTerrainInfo(position, out var hitInfo))
                return false;
            if (LocationCheck.InsideRock(position, 10f))
                return false;
            if (LocationCheck.InWater(position))
                return false;
            if (LocationCheck.OnRoadOrRail(position))
                return false;
            if (LocationCheck.InNoBuildZone(position, 10f))
                return false;

            return SetSuitablePositionAndRotation(position, hitInfo, out suitablePosition, out suitableRotation);
        }

        private static bool SetSuitablePositionAndRotation(Vector3 position, RaycastHit hitInfo, out Vector3 suitablePosition, out Quaternion suitableRotation)
        {
            suitablePosition = hitInfo.point;
            suitableRotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
            return true;
        }

        private void CreateDungeonMarkers(ActiveDungeon activeDungeon)
        {
            var prefabVendingMarker = "assets/prefabs/deployable/vendingmachine/vending_mapmarker.prefab";
            var vendingMarker = CreateMapMarker<VendingMachineMapMarker>(prefabVendingMarker, activeDungeon.Position);
            vendingMarker.OwnerID = OwnerID;
            vendingMarker.markerShopName = $"Dungeon: {activeDungeon.TierName}";
            vendingMarker.SendNetworkUpdate();

            var prefabRadiusMarker = "assets/prefabs/tools/map/genericradiusmarker.prefab";
            var radiusMarker = CreateMapMarker<MapMarkerGenericRadius>(prefabRadiusMarker, activeDungeon.Position);
            radiusMarker.OwnerID = OwnerID;
            radiusMarker.alpha = 0.75f;
            radiusMarker.radius = 0.5f;
            radiusMarker.color2 = GetMarkerColorByTier(activeDungeon.TierName);
            radiusMarker.SendUpdate();
            radiusMarker.SendNetworkUpdate();

            _dungeonData.ActiveDungeonMarkers.Add(
                new DungeonMarker
                {
                    VendingMarker = vendingMarker.net.ID.Value,
                    RadiusMarker = radiusMarker.net.ID.Value,
                    Position = activeDungeon.Position,
                    Tier = activeDungeon.TierName,
                }
            );
        }

        private T CreateMapMarker<T>(string prefab, Vector3 position)
            where T : BaseEntity
        {
            var entity = GameManager.server.CreateEntity(prefab, position) as T;
            entity?.Spawn();
            return entity;
        }

        private static Color GetMarkerColorByTier(string tierName)
        {
            return tierName switch
            {
                "Easy" => new Color(0.2f, 0.8f, 0.2f, 1f),
                "Normal" => new Color(1f, 0.8f, 0f, 1f),
                "Medium" => new Color(1f, 0.5f, 0f, 1f),
                "Hard" => new Color(0.8f, 0.1f, 0.1f, 1f),
                "Nightmare" => new Color(0.6f, 0.3f, 1f, 1f),
                _ => new Color(0.5f, 0.5f, 0.5f, 1f),
            };
        }

        private bool CanBeRemoved(ActiveDungeon dungeon)
        {
            bool canRemove = AreAllNpcsGone(dungeon);

            if (!canRemove)
            {
                if (_configData.DungeonSpawn.MaxDungeonLifetime > 0)
                {
                    canRemove = HasDungeonExceededMaxLifetime(dungeon);
                }
            }

            canRemove &= NoPlayersInside(dungeon);
            canRemove &= dungeon.Spawned;

            return canRemove;
        }

        private bool HasDungeonExceededMaxLifetime(ActiveDungeon dungeon)
        {
            var timeElapsed = DateTime.UtcNow - dungeon.CreationTime;
            return timeElapsed.TotalSeconds >= _configData.DungeonSpawn.MaxDungeonLifetime;
        }

        private bool AreAllNpcsGone(ActiveDungeon dungeon)
        {
            return dungeon.NpcIds.All(npcId =>
            {
                var npc = BaseNetworkable.serverEntities.Find(new NetworkableId(npcId)) as BaseEntity;
                return npc == null || npc.IsDestroyed;
            });
        }

        private bool NoPlayersInside(ActiveDungeon dungeon)
        {
            var dynamicDungeon = BaseNetworkable.serverEntities.Find(new NetworkableId(dungeon.DynamicDungeonId)) as ProceduralDynamicDungeon;
            return dynamicDungeon == null || !dynamicDungeon.ContainsAnyPlayers();
        }

        private bool TryFindDungeonSpawnPoint(out Vector3 position, out Quaternion rotation)
        {
            var mapSize = World.Size;
            for (var attempt = 0; attempt < 1000; attempt++)
            {
                var candidatePosition = new Vector3(Random.Range(-mapSize / 2, mapSize / 2), 0, Random.Range(-mapSize / 2, mapSize / 2));

                if (IsValidPosition(candidatePosition, out position, out rotation) && IsSafeFromOtherDungeons(position) && !IsCloseToMonuments(position))
                {
                    return true;
                }
            }

            position = Vector3.zero;
            rotation = Quaternion.identity;
            return false;
        }

        private bool IsSafeFromOtherDungeons(Vector3 position)
        {
            return _dungeonData.ActiveDungeons.All(dungeon => Vector3.Distance(position, dungeon.Position) >= _configData.DungeonSpawn.MinDistanceBetweenDungeons);
        }

        private bool IsCloseToMonuments(Vector3 position)
        {
            return TerrainMeta.Path.Monuments.Any(mon => Vector3.Distance(position, mon.transform.position) < _configData.DungeonSpawn.MinDistanceFromMonuments);
        }

        private DungeonTierConfig GetDungeonTier(int spawnPointCount)
        {
            if (spawnPointCount < _configData.Tiers.Normal.MinSpawnPoints)
                return _configData.Tiers.Easy;
            if (spawnPointCount < _configData.Tiers.Medium.MinSpawnPoints)
                return _configData.Tiers.Normal;
            if (spawnPointCount < _configData.Tiers.Hard.MinSpawnPoints)
                return _configData.Tiers.Medium;
            if (spawnPointCount < _configData.Tiers.Nightmare.MinSpawnPoints)
                return _configData.Tiers.Hard;

            return _configData.Tiers.Nightmare;
        }

        #endregion

        #region Configuration

        private class ConfigData
        {
            [JsonProperty(PropertyName = "Enable Debug Logs")]
            public bool EnableDebugLogs { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Warning Logs")]
            public bool EnableWarningLogs { get; set; } = true;

            [JsonProperty(PropertyName = "Enable Toast Messages")]
            public bool EnableShowToast { get; set; } = true;

            [JsonProperty(PropertyName = "Dungeon Settings")]
            public DungeonSpawnSettings DungeonSpawn { get; set; } = new DungeonSpawnSettings();

            [JsonProperty(PropertyName = "Tiers")]
            public DungeonTiers Tiers { get; set; } = new DungeonTiers();

            [JsonProperty(PropertyName = "Loot Box Config")]
            public LootBoxConfig LootBoxConfig { get; set; } = new LootBoxConfig();

            [JsonProperty(PropertyName = "Version")]
            public VersionNumber Version { get; set; }
        }

        private class DungeonTiers
        {
            [JsonProperty(PropertyName = "Easy")]
            public DungeonTierConfig Easy { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 3,
                    LootBoxPercentage = 30,
                    TurretPercentage = 20,
                    NpcPercentage = 50,
                    AutoTurretConfig = new TurretConfig { Health = 300, WeaponShortName = "pistol.revolver" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 3,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Wild Scavenger",
                        Health = 100,
                        RoamRange = 15f,
                        ChaseRange = 80f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new WearItemConfig { ShortName = "mask.bandana", SkinID = 3255213783 },
                                new WearItemConfig { ShortName = "hat.boonie", SkinID = 2557702256 },
                                new WearItemConfig { ShortName = "hoodie", SkinID = 1282142258 },
                                new WearItemConfig { ShortName = "pants", SkinID = 2080977144 },
                                new WearItemConfig { ShortName = "shoes.boots", SkinID = 0 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new BeltItemConfig
                                {
                                    ShortName = "pistol.m92",
                                    Amount = 1,
                                    SkinID = 0,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Normal")]
            public DungeonTierConfig Normal { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 6,
                    LootBoxPercentage = 30,
                    TurretPercentage = 25,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 400, WeaponShortName = "pistol.revolver" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 4,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Battle Forager",
                        Health = 150,
                        RoamRange = 20f,
                        ChaseRange = 90f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new WearItemConfig { ShortName = "mask.bandana", SkinID = 3255213783 },
                                new WearItemConfig { ShortName = "hat.boonie", SkinID = 2557702256 },
                                new WearItemConfig { ShortName = "hoodie", SkinID = 1282142258 },
                                new WearItemConfig { ShortName = "pants", SkinID = 2080977144 },
                                new WearItemConfig { ShortName = "shoes.boots", SkinID = 0 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new BeltItemConfig
                                {
                                    ShortName = "pistol.m92",
                                    Amount = 1,
                                    SkinID = 0,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Medium")]
            public DungeonTierConfig Medium { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 10,
                    LootBoxPercentage = 25,
                    TurretPercentage = 30,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 600, WeaponShortName = "rifle.ak" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 5,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Crimson Raider",
                        Health = 200,
                        RoamRange = 25f,
                        ChaseRange = 100f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new WearItemConfig { ShortName = "mask.bandana", SkinID = 3255213783 },
                                new WearItemConfig { ShortName = "hat.boonie", SkinID = 2557702256 },
                                new WearItemConfig { ShortName = "hoodie", SkinID = 1282142258 },
                                new WearItemConfig { ShortName = "pants", SkinID = 2080977144 },
                                new WearItemConfig { ShortName = "shoes.boots", SkinID = 0 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new BeltItemConfig
                                {
                                    ShortName = "rifle.ak",
                                    Amount = 1,
                                    SkinID = 0,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Hard")]
            public DungeonTierConfig Hard { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 15,
                    LootBoxPercentage = 20,
                    TurretPercentage = 35,
                    NpcPercentage = 45,
                    AutoTurretConfig = new TurretConfig { Health = 800, WeaponShortName = "rifle.bolt" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 7,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Elite Outlaw",
                        Health = 250,
                        RoamRange = 30f,
                        ChaseRange = 120f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new WearItemConfig { ShortName = "mask.bandana", SkinID = 3255213783 },
                                new WearItemConfig { ShortName = "hat.boonie", SkinID = 2557702256 },
                                new WearItemConfig { ShortName = "hoodie", SkinID = 1282142258 },
                                new WearItemConfig { ShortName = "pants", SkinID = 2080977144 },
                                new WearItemConfig { ShortName = "shoes.boots", SkinID = 0 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new BeltItemConfig
                                {
                                    ShortName = "rifle.ak",
                                    Amount = 1,
                                    SkinID = 0,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                            },
                        },
                    },
                };

            [JsonProperty(PropertyName = "Nightmare")]
            public DungeonTierConfig Nightmare { get; set; } =
                new DungeonTierConfig
                {
                    MinSpawnPoints = 20,
                    LootBoxPercentage = 30,
                    TurretPercentage = 30,
                    NpcPercentage = 40,
                    AutoTurretConfig = new TurretConfig { Health = 1000, WeaponShortName = "rifle.bolt" },
                    SmallWoodBoxSkinID = 2998755525,
                    MaxItemsPerBox = 10,
                    NpcConfig = new NpcConfig
                    {
                        Name = "Nightmare Hunter",
                        Health = 300,
                        RoamRange = 35f,
                        ChaseRange = 140f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new WearItemConfig { ShortName = "mask.bandana", SkinID = 3255213783 },
                                new WearItemConfig { ShortName = "hat.boonie", SkinID = 2557702256 },
                                new WearItemConfig { ShortName = "hoodie", SkinID = 1282142258 },
                                new WearItemConfig { ShortName = "pants", SkinID = 2080977144 },
                                new WearItemConfig { ShortName = "shoes.boots", SkinID = 0 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new BeltItemConfig
                                {
                                    ShortName = "rifle.ak",
                                    Amount = 1,
                                    SkinID = 0,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                            },
                        },
                    },
                };
        }

        private class DungeonTierConfig
        {
            [JsonProperty(PropertyName = "Minimum Entity Count for Dungeon Tier")]
            public int MinSpawnPoints { get; set; }

            [JsonProperty(PropertyName = "Loot Box Percentage")]
            public int LootBoxPercentage { get; set; }

            [JsonProperty(PropertyName = "Turret Percentage")]
            public int TurretPercentage { get; set; }

            [JsonProperty(PropertyName = "NPC Percentage")]
            public int NpcPercentage { get; set; }

            [JsonProperty(PropertyName = "NPC Config")]
            public NpcConfig NpcConfig { get; set; } = new NpcConfig();

            [JsonProperty(PropertyName = "Small Wood Box Skin ID")]
            public ulong SmallWoodBoxSkinID { get; set; } = 2998755525;

            [JsonProperty(PropertyName = "Max Items Per Box")]
            public int MaxItemsPerBox { get; set; } = 6;

            [JsonProperty(PropertyName = "Auto Turret Config")]
            public TurretConfig AutoTurretConfig { get; set; } = new TurretConfig();

            public bool IsValid() => LootBoxPercentage + TurretPercentage + NpcPercentage == 100;
        }

        public class NpcConfig
        {
            [JsonProperty(PropertyName = "NPC Name")]
            public string Name { get; set; }

            [JsonProperty(PropertyName = "NPC Health")]
            public int Health { get; set; }

            [JsonProperty(PropertyName = "NPC Roam Range")]
            public float RoamRange { get; set; }

            [JsonProperty(PropertyName = "NPC Chase Range")]
            public float ChaseRange { get; set; }

            [JsonProperty(PropertyName = "Wear Items")]
            public WearConfig WearItems { get; set; } = new WearConfig();

            [JsonProperty(PropertyName = "Belt Items")]
            public BeltConfig BeltItems { get; set; } = new BeltConfig();
        }

        public class WearConfig
        {
            [JsonProperty(PropertyName = "Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<WearItemConfig> Items { get; set; } = new List<WearItemConfig>();
        }

        public class BeltConfig
        {
            [JsonProperty(PropertyName = "Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<BeltItemConfig> Items { get; set; } = new List<BeltItemConfig>();
        }

        public class BeltItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "Amount")]
            public int Amount { get; set; }

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; }

            [JsonProperty(PropertyName = "Mods")]
            public List<string> Mods { get; set; } = new List<string>();
        }

        public class WearItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "SkinID")]
            public ulong SkinID { get; set; }
        }

        public class LootBoxConfig
        {
            [JsonProperty(PropertyName = "Loot Items", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<ItemConfig> LootItems { get; set; } =
                new List<ItemConfig>
                {
                    new ItemConfig
                    {
                        ShortName = "ammo.rifle",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                };
        }

        private class TurretConfig
        {
            [JsonProperty(PropertyName = "Health")]
            public float Health { get; set; }

            [JsonProperty(PropertyName = "Weapon Short Name")]
            public string WeaponShortName { get; set; }
        }

        private class DungeonSpawnSettings
        {
            [JsonProperty(PropertyName = "Enable Auto Spawn")]
            public bool EnableAutoSpawn { get; set; } = false;

            [JsonProperty(PropertyName = "Enable Xmas Dungeon")]
            public bool EnableXmasDungeon { get; set; } = false;

            [JsonProperty(PropertyName = "Enable Halloween Dungeon")]
            public bool EnableHalloweenDungeon { get; set; } = true;

            [JsonProperty(PropertyName = "Max Total Active Dungeons")]
            public int MaxTotalActiveDungeons { get; set; } = 10;

            [JsonProperty(PropertyName = "Min Distance From Monuments")]
            public float MinDistanceFromMonuments { get; set; } = 60f;

            [JsonProperty(PropertyName = "Min Distance Between Dungeons")]
            public float MinDistanceBetweenDungeons { get; set; } = 60f;

            [JsonProperty(PropertyName = "Remove Dungeon if All NPCs Are Dead")]
            public bool RemoveIfAllNpcsDead { get; set; } = true;

            [JsonProperty(PropertyName = "Max Time (in seconds) to Keep Dungeon Alive")]
            public int MaxDungeonLifetime { get; set; } = 1800;
        }

        public class ItemConfig
        {
            [JsonProperty(PropertyName = "ShortName")]
            public string ShortName { get; set; }

            [JsonProperty(PropertyName = "Inclusion Chance Percentage")]
            public float InclusionChancePercentage { get; set; }

            [JsonProperty(PropertyName = "Minimum Amount")]
            public int MinimumAmount { get; set; }

            [JsonProperty(PropertyName = "Maximum Amount")]
            public int MaximumAmount { get; set; }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                _configData = Config.ReadObject<ConfigData>() ?? new ConfigData();
            }
            catch (Exception ex)
            {
                PrintError($"The configuration file is corrupt: {ex}");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void LoadDefaultConfig()
        {
            PrintWarning("Creating a new configuration file.");
            _configData = new ConfigData { Version = Version };
        }

        protected override void SaveConfig()
        {
            Config.WriteObject(_configData, true);
        }

        #endregion

        #region Localization

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(
                new Dictionary<string, string>
                {
                    ["NoInactiveDungeonsToRemove"] = "There are no inactive dungeons to remove.",
                    ["NoDungeonsToRemove"] = "There are no dungeons to remove.",
                    ["DungeonRemovedDetail"] = "Removed dungeon of tier {0} at grid {1}.",
                    ["AttemptSpawnDungeons"] = "Attempting to spawn dungeons.",
                    ["DungeonAppeared"] = "A {0} Dungeon has appeared at grid {1}!",
                    ["DungeonSpawnFailed"] = "Could not find a valid location to spawn a new dungeon.",
                    ["DungeonSpawnedSuccess"] = "Dungeon spawned successfully at grid {0}!",
                    ["MaxActiveDungeonsReached"] = "Cannot spawn: Maximum number of active dungeons reached.",
                    ["DungeonStillSpawningMessage"] = "The dungeon is still spawning. Please wait until it's ready before entering.",
                },
                this
            );
        }

        private string Msg(string userId, string key, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

        #endregion

        #region Data Classes

        private class DungeonData
        {
            public List<ActiveDungeon> ActiveDungeons { get; set; } = new List<ActiveDungeon>();
            public List<DungeonMarker> ActiveDungeonMarkers { get; set; } = new List<DungeonMarker>();
        }

        private class ActiveDungeon
        {
            public ulong PortalId { get; set; }
            public ulong DynamicDungeonId { get; set; }
            public Vector3 Position { get; set; }
            public string Grid { get; set; }
            public string TierName { get; set; }
            public DungeonTierConfig TierConfig { get; set; }
            public List<ulong> EntityIds { get; set; } = new List<ulong>();
            public List<ulong> NpcIds { get; set; } = new List<ulong>();
            public bool Spawned { get; set; } = false;
            public DateTime CreationTime { get; set; } = DateTime.UtcNow;
        }

        private class DungeonMarker
        {
            public ulong VendingMarker { get; set; }
            public ulong RadiusMarker { get; set; }
            public Vector3 Position { get; set; }
            public string Tier { get; set; }
        }

        #endregion

        #region Collider Checks

        public static class LocationCheck
        {
            public static bool InsideRock(Vector3 position, float radius)
            {
                List<Collider> colliders = Pool.GetList<Collider>();

                Vis.Colliders(position, radius, colliders, Layers.Mask.World, QueryTriggerInteraction.Ignore);

                bool isInsideRock = colliders.Any(collider =>
                    collider.name.Contains("rock", StringComparison.OrdinalIgnoreCase)
                    || collider.name.Contains("cliff", StringComparison.OrdinalIgnoreCase)
                    || collider.name.Contains("formation", StringComparison.OrdinalIgnoreCase)
                );

                Pool.FreeList(ref colliders);
                return isInsideRock;
            }

            public static bool GetTerrainInfo(Vector3 position, out RaycastHit hitInfo)
            {
                return Physics.Linecast(position + Vector3.up, position - Vector3.up, out hitInfo, Layers.Mask.Terrain);
            }

            public static bool InWater(Vector3 position) => WaterLevel.Test(position, false, false);

            public static bool OnRoadOrRail(Vector3 position) => (TerrainMeta.TopologyMap.GetTopology(position) & (int)TerrainTopology.Enum.Road) != 0;

            public static bool InNoBuildZone(Vector3 position, float radius) => Physics.CheckSphere(position, radius, Layers.Mask.Prevent_Building, QueryTriggerInteraction.Ignore);
        }

        #endregion
    }
}