using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
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.1")]
    [Description("Create dungeons populated with NPCs, turrets and loot containers.")]
    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();
        private Dictionary<DungeonTierConfig, string> _tierNameMap;
        private const ulong OwnerID = 10203040505;
        private const float TimeTimers = 60f;

        #region Hooks
        [PluginReference]
        private readonly Plugin NpcSpawn;

        private void OnServerInitialized()
        {
            if (!NpcSpawn)
            {
                NextTick(() =>
                {
                    PrintError("You don't have NpcSpawn free codefling.com plugin installed.");
                    Interface.Oxide.UnloadPlugin("DungeonEvents");
                });
                return;
            }

            InitializeTierNameMap();
            LoadData();
            RemoveAllDungeons();
            StartSpawnPointScanner();
            StartTimerAutoSpawnDungeons();
            StartTimerRemoveInactiveDungeons();
            StartTimerUpdateMarkers();
            SaveData();
        }

        private void OnServerSave() => SaveData();

        private void Unload()
        {
            if (CoroutineManager.Instance != null)
            {
                UnityEngine.Object.Destroy(CoroutineManager.Instance.gameObject);
            }

            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;
        }

        private object CanEntityTakeDamage(BasePlayer victim, HitInfo info)
        {
            if (info == null || info.Initiator == null || info.Initiator.net == null || !IsPlayer(victim))
                return null;

            if (info.Initiator is ScientistNPC)
            {
                if (IsDungeonEntity(info.Initiator))
                {
                    return true;
                }
            }

            if (info.Initiator is AutoTurret)
            {
                if (IsDungeonEntity(info.Initiator))
                {
                    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;
        }

        private void OnCorpsePopulate(ScientistNPC entity, NPCPlayerCorpse corpse)
        {
            if (entity == null)
                return;

            if (IsDungeonEntity(entity))
            {
                NextTick(() =>
                {
                    if (corpse == null)
                        return;

                    Effect.server.Run("assets/prefabs/weapons/flashbang/effects/fx-flashbang-boom.prefab", corpse.transform.position);

                    var container = corpse.containers[0];
                    container.Clear();
                    if (!corpse.IsDestroyed)
                        corpse.Kill();
                });
            }
        }

        #endregion

        #region Chat Commands

        [ChatCommand("createdungeon")]
        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}");
            CoroutineManager.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 CoroutineManager.Instance.StartCoroutine(RemoveOriginalEntities(dungeon));
            yield return new WaitForSeconds(3f);

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

            var spawnPoints = GetAllSpawnPoints(dungeon, dungeonPortal.GetType().Name);
            if (spawnPoints.Count == 0)
            {
                LogDebug("Dungeon spawn failed! Not enough spawn points inside for entities.", 1);
                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, string dungeonType)
        {
            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>();

            if (dungeonType == "HalloweenDungeon")
            {
                bool isNightmareOrHardActive = _dungeonData.ActiveDungeons.Any(dungeon => dungeon.DungeonType == "Nightmare" || dungeon.DungeonType == "Hard");
                int pointsToGenerate = isNightmareOrHardActive ? 1 : 8;

                foreach (var point in basePoints)
                {
                    for (int i = 0; i < pointsToGenerate; i++)
                    {
                        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(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(ActiveDungeon activeDungeon)
        {
            var npcConfig = activeDungeon.TierConfig.NpcConfig;
            var areaMask = activeDungeon.DungeonType == "XmasDungeon" ? -1 : 1;
            var agentTypeID = activeDungeon.DungeonType == "XmasDungeon" ? 0 : -1372625422;

            return new JObject
            {
                ["Name"] = npcConfig.Name,
                ["HostileTargetsOnly"] = false,
                ["DamageScale"] = npcConfig.DamageScale,
                ["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"] = areaMask,
                ["AgentTypeID"] = agentTypeID,
                ["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);
                    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}");
                        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}");
                        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],
                DungeonType = dungeonPortal.GetType().Name,
                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;
            PopulateStorage(storage.inventory, tierConfig, _configData.LootBoxConfig.LootItems);
            AddLock(storage);
        }

        private void AddLock(StorageContainer storage)
        {
            var prefabName = "assets/prefabs/locks/keylock/lock.key.prefab";
            var keyLock = GameManager.server.CreateEntity(prefabName) as KeyLock;
            if (keyLock == null)
                return;

            keyLock.gameObject.Identity();
            keyLock.SetParent(storage, storage.GetSlotAnchorName(BaseEntity.Slot.Lock));
            keyLock.Spawn();
            storage.SetSlot(BaseEntity.Slot.Lock, keyLock);
            keyLock.keyCode = Random.Range(1000, 9999);
            keyLock.OwnerID = 0;
            keyLock.firstKeyCreated = true;
            keyLock.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($"{Name}_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 finalPosition, out Quaternion finalRotation)
        {
            finalPosition = Vector3.zero;
            finalRotation = Quaternion.identity;

            if (!LocationCheck.TerrainDetection(position, out var hitInfo))
                return false;
            if (LocationCheck.IsInsideRock(position, 10f) || LocationCheck.InWater(position) || LocationCheck.IsOnRoad(position) || LocationCheck.InNoBuildZone(position, 10f))
                return false;

            if (LocationCheck.HasCollider(position, 10f))
                return false;
            if (LocationCheck.IsInMonument(position))
                return false;

            return SetSuitablePositionAndRotation(position, hitInfo, out finalPosition, out finalRotation);
        }

        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; } = false;

            [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,
                        DamageScale = 0.5f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "tshirt.long", SkinID = 10118 },
                                new() { ShortName = "burlap.gloves", SkinID = 949616124 },
                                new() { ShortName = "attire.snowman.helmet", SkinID = 0 },
                                new() { ShortName = "pants", SkinID = 2352962213 },
                                new() { ShortName = "shoes.boots", SkinID = 2380731293 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "pistol.revolver",
                                    Amount = 1,
                                    SkinID = 3140577175,
                                },
                            },
                        },
                    },
                };

            [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,
                        DamageScale = 0.8f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "bucket.helmet", SkinID = 1073904216 },
                                new() { ShortName = "jacket", SkinID = 2350426469 },
                                new() { ShortName = "tshirt", SkinID = 10039 },
                                new() { ShortName = "burlap.gloves.new", SkinID = 0 },
                                new() { ShortName = "pants", SkinID = 1441311938 },
                                new() { ShortName = "shoes.boots", SkinID = 2075527039 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "smg.2",
                                    Amount = 1,
                                    SkinID = 2386688842,
                                    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,
                        DamageScale = 1.1f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "coffeecan.helmet", SkinID = 1727561127 },
                                new() { ShortName = "roadsign.jacket", SkinID = 1727562915 },
                                new() { ShortName = "roadsign.gloves", SkinID = 2799639349 },
                                new() { ShortName = "pants", SkinID = 2814837980 },
                                new() { ShortName = "shoes.boots", SkinID = 1839313604 },
                                new() { ShortName = "tshirt.long", SkinID = 566893368 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "rifle.semiauto",
                                    Amount = 1,
                                    SkinID = 2617680693,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 10,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };

            [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,
                        DamageScale = 1.3f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "metal.facemask", SkinID = 3284864766 },
                                new() { ShortName = "metal.plate.torso", SkinID = 2105505757 },
                                new() { ShortName = "burlap.gloves", SkinID = 2090790324 },
                                new() { ShortName = "hoodie", SkinID = 2080975449 },
                                new() { ShortName = "shoes.boots", SkinID = 10023 },
                                new() { ShortName = "pants", SkinID = 2080977144 },
                                new() { ShortName = "roadsign.kilt", SkinID = 2120628865 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "rifle.ak",
                                    Amount = 1,
                                    SkinID = 3190379864,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 20,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };

            [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,
                        DamageScale = 1.5f,
                        RoamRange = 15f,
                        ChaseRange = 15f,
                        WearItems = new WearConfig
                        {
                            Items = new List<WearItemConfig>
                            {
                                new() { ShortName = "metal.facemask", SkinID = 3343860599 },
                                new() { ShortName = "metal.plate.torso", SkinID = 3343861569 },
                                new() { ShortName = "hoodie", SkinID = 1369835131 },
                                new() { ShortName = "roadsign.kilt", SkinID = 1727564168 },
                                new() { ShortName = "burlap.gloves", SkinID = 1210780157 },
                                new() { ShortName = "pants", SkinID = 810745264 },
                                new() { ShortName = "shoes.boots", SkinID = 1210771348 },
                            },
                        },
                        BeltItems = new BeltConfig
                        {
                            Items = new List<BeltItemConfig>
                            {
                                new()
                                {
                                    ShortName = "lmg.m249",
                                    Amount = 1,
                                    SkinID = 1883947256,
                                    Mods = new List<string> { "weapon.mod.flashlight" },
                                },
                                new()
                                {
                                    ShortName = "syringe.medical",
                                    Amount = 50,
                                    SkinID = 0,
                                },
                            },
                        },
                    },
                };
        }

        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 = "Damage Scale")]
            public float DamageScale { 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()
                    {
                        ShortName = "ammo.rifle",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "sulfur.ore",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "scrap",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 500,
                        MaximumAmount = 2000,
                    },
                    new()
                    {
                        ShortName = "rifle.ak",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rocket.launcher.dragon",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "grenade.f1",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "crude.oil",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 10,
                        MaximumAmount = 100,
                    },
                    new()
                    {
                        ShortName = "diesel_barrel",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "cctv.camera",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 5,
                        MaximumAmount = 20,
                    },
                    new()
                    {
                        ShortName = "gingerbreadsuit",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "gears",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 20,
                    },
                    new()
                    {
                        ShortName = "metal.refined",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 10,
                        MaximumAmount = 80,
                    },
                    new()
                    {
                        ShortName = "electric.furnace",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "fuse",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "xmas.door.garland",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "gunpowder",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 50,
                        MaximumAmount = 150,
                    },
                    new()
                    {
                        ShortName = "handcuffs",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "hazmatsuit",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.rocket.hv",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 20,
                    },
                    new()
                    {
                        ShortName = "ammo.rifle.hv",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "metal.facemask",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.rocket.fire",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "jackhammer",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.l96",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "furnace.large",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "largemedkit",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "xmas.present.large",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "locker",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "lowgradefuel",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 50,
                        MaximumAmount = 200,
                    },
                    new()
                    {
                        ShortName = "rifle.lr300",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.m39",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "pistol.m92",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "lmg.m249",
                        InclusionChancePercentage = 2,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "syringe.medical",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 10,
                        MaximumAmount = 30,
                    },
                    new()
                    {
                        ShortName = "metal.ore",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 100,
                        MaximumAmount = 300,
                    },
                    new()
                    {
                        ShortName = "metalpipe",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "metalspring",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "minigun",
                        InclusionChancePercentage = 2,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "grenade.molotov",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "smg.mp5",
                        InclusionChancePercentage = 4,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "multiplegrenadelauncher",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "mummymask",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "mushroom",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 20,
                        MaximumAmount = 40,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.muzzleboost",
                        InclusionChancePercentage = 35,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.muzzlebrake",
                        InclusionChancePercentage = 35,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "nightvisiongoggles",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "newyeargong",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "hazmatsuit.nomadsuit",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "firework.boomer.pattern",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "jar.pickle",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.pistol",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 100,
                        MaximumAmount = 500,
                    },
                    new()
                    {
                        ShortName = "pookie.bear",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "potato",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 100,
                        MaximumAmount = 100,
                    },
                    new()
                    {
                        ShortName = "pistol.revolver",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "riflebody",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "roadsign.gloves",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "roadsign.kilt",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "ammo.rocket.basic",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "rocket.launcher",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rope",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "icepick.salvaged",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "explosive.satchel",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "electric.seismicsensor",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "semibody",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "pistol.semiauto",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.semiauto",
                        InclusionChancePercentage = 5,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "sheetmetal",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 1,
                        MaximumAmount = 9,
                    },
                    new()
                    {
                        ShortName = "sewingkit",
                        InclusionChancePercentage = 65,
                        MinimumAmount = 1,
                        MaximumAmount = 10,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.silencer",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "weapon.mod.simplesight",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "knife.skinning",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "rifle.sks",
                        InclusionChancePercentage = 7,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "xmas.present.small",
                        InclusionChancePercentage = 40,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "smgbody",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 5,
                        MaximumAmount = 9,
                    },
                    new()
                    {
                        ShortName = "sofa",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "hazmatsuit.spacesuit",
                        InclusionChancePercentage = 10,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "spookyspeaker",
                        InclusionChancePercentage = 25,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "stones",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 100,
                        MaximumAmount = 600,
                    },
                    new()
                    {
                        ShortName = "sulfur",
                        InclusionChancePercentage = 80,
                        MinimumAmount = 100,
                        MaximumAmount = 600,
                    },
                    new()
                    {
                        ShortName = "tactical.gloves",
                        InclusionChancePercentage = 35,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "tarp",
                        InclusionChancePercentage = 60,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "vampire.stake",
                        InclusionChancePercentage = 20,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "vending.machine",
                        InclusionChancePercentage = 15,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "bottle.vodka",
                        InclusionChancePercentage = 9,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "gun.water",
                        InclusionChancePercentage = 9,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "piano",
                        InclusionChancePercentage = 9,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "firework.volcano",
                        InclusionChancePercentage = 30,
                        MinimumAmount = 1,
                        MaximumAmount = 5,
                    },
                    new()
                    {
                        ShortName = "hat.wolf",
                        InclusionChancePercentage = 50,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                    new()
                    {
                        ShortName = "worm",
                        InclusionChancePercentage = 70,
                        MinimumAmount = 1,
                        MaximumAmount = 1,
                    },
                };
        }

        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 Halloween Dungeon")]
            public bool EnableHalloweenDungeon { get; set; } = true;

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

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

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

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

            [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 string DungeonType { 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 IsInsideRock(Vector3 position, float radius)
            {
                List<Collider> colliders = Pool.GetList<Collider>();
                Vis.Colliders(position, radius, colliders, Layers.Mask.World, QueryTriggerInteraction.Ignore);

                bool result = false;
                foreach (Collider collider in colliders)
                {
                    if (
                        collider.name.Contains("rock", CompareOptions.OrdinalIgnoreCase)
                        || collider.name.Contains("cliff", CompareOptions.OrdinalIgnoreCase)
                        || collider.name.Contains("formation", CompareOptions.OrdinalIgnoreCase)
                    )
                    {
                        result = true;
                        break;
                    }
                }

                Pool.FreeUnmanaged(ref colliders);
                return result;
            }

            public static bool TerrainDetection(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 IsOnRoad(Vector3 position)
            {
                return (TerrainMeta.TopologyMap.GetTopology(position) & (int)TerrainTopology.Enum.Road) != 0;
            }

            public static bool InNoBuildZone(Vector3 position, float radius)
            {
                return Physics.CheckSphere(position, radius, Layers.Mask.Prevent_Building);
            }

            public static bool IsInMonument(Vector3 position)
            {
                return TerrainMeta.Path.Monuments.Any(monument => monument.IsInBounds(position) && monument.shouldDisplayOnMap) || OnSpecificTopology(position, TerrainTopology.Enum.Monument);
            }

            public static bool HasCollider(Vector3 position, float radius)
            {
                List<Collider> hitColliders = Pool.Get<List<Collider>>();
                GamePhysics.OverlapSphere(position, radius, hitColliders);

                bool hasColliderNearby = false;
                foreach (Collider collider in hitColliders)
                {
                    BaseEntity entity = collider.gameObject.ToBaseEntity();
                    if (entity != null)
                    {
                        hasColliderNearby = true;
                        break;
                    }
                }

                Pool.FreeUnmanaged(ref hitColliders);
                return hasColliderNearby;
            }

            private static bool OnSpecificTopology(Vector3 position, TerrainTopology.Enum topology)
            {
                return (TerrainMeta.TopologyMap.GetTopology(position) & (int)topology) != 0;
            }
        }

        #endregion

        public class CoroutineManager : MonoBehaviour
        {
            private static CoroutineManager _instance;

            public static CoroutineManager Instance
            {
                get
                {
                    if (_instance == null)
                    {
                        var obj = new GameObject("CoroutineManager");
                        _instance = obj.AddComponent<CoroutineManager>();
                        GameObject.DontDestroyOnLoad(obj);
                    }
                    return _instance;
                }
            }
        }
    }
}