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

FREE NecroZombies : 2.3.0 2.3.0

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

// NecroZombies - scarecrow-based zombies & hellhounds with BO-style drip waves, spawn sets,
// hop-like movement, fire/flies/gore VFX, and CUI wave/stage banners.
namespace Oxide.Plugins
{
    [Info("NecroZombies", "belisario-afk", "2.3.0")]
    [Description("Spawns fast, aggressive scarecrow-based zombies and hellhounds with spawn sets, drip waves, CUI, and horror VFX")]
    public class NecroZombies : RustPlugin
    {
        #region Configuration

        private ConfigData _config;

        private class ZombieProfile
        {
            public string ProfileName = "default";

            public float Health = 100f;
            public float Speed = 6.5f;

            public string MeleeShortname = "knife.bone";
            public ulong MeleeSkinId = 3612162757;

            public string ClothingShortname = "halloween.mummysuit";
            public ulong ClothingSkinId = 0;

            public string DisplayName = "Necro Zombie";

            public bool AlwaysOnFire = true;
        }

        private class WaveSettings
        {
            // Total zombies per wave
            public int BaseCount = 12;
            public int CountPerWaveIncrease = 4;

            // Stat scaling per wave
            public float HealthMultiplierPerWave = 0.1f;
            public float SpeedMultiplierPerWave = 0.05f;

            public int MaxWaves = 0;              // 0 = endless

            public int MaxActiveZombies = 40;     // global hard cap

            // Wave progression logic
            public float RequiredKillRatioToAdvance = 0.9f;
            public float WaveStartDelay = 5f;
            public float WaveTimeoutSeconds = 120f;

            // Drip spawning inside a wave
            public int GroupSize = 3;
            public float SpawnIntervalSeconds = 2.5f;
            public float SpawnIntervalMinSeconds = 1.0f;
            public float SpawnIntervalPerWaveMultiplier = 0.9f;

            // CUI wave banner settings
            public bool EnableWaveBanner = true;
            public float BannerDuration = 3.0f;       // seconds visible
            public string BannerTitleColor = "1 0.1 0.1 1"; // RGBA (bright red)
            public string BannerSubColor = "1 1 1 0.8";     // RGBA (white)
            public int BannerTitleSize = 32;
            public int BannerSubSize = 18;
        }

        private class ConfigData
        {
            public float ZombieHealth = 100f;
            public float ZombieSpeed = 6.5f;
            public string MeleeShortname = "knife.bone";
            public ulong MeleeSkinId = 3612162757;
            public string ClothingShortname = "halloween.mummysuit";
            public ulong ClothingSkinId = 0;
            public string ZombieName = "Necro Zombie";

            public Dictionary<string, ZombieProfile> Profiles = new Dictionary<string, ZombieProfile>();
            public WaveSettings Waves = new WaveSettings();

            public Dictionary<string, List<Vector3>> SpawnSets = new Dictionary<string, List<Vector3>>();
        }

        protected override void LoadDefaultConfig()
        {
            _config = new ConfigData();

            _config.Profiles["default"] = new ZombieProfile
            {
                ProfileName = "default",
                Health = _config.ZombieHealth,
                Speed = _config.ZombieSpeed,
                MeleeShortname = _config.MeleeShortname,
                MeleeSkinId = _config.MeleeSkinId,
                ClothingShortname = _config.ClothingShortname,
                ClothingSkinId = _config.ClothingSkinId,
                DisplayName = _config.ZombieName,
                AlwaysOnFire = true
            };

            _config.Profiles["runner"] = new ZombieProfile
            {
                ProfileName = "runner",
                Health = 75f,
                Speed = 10f,
                MeleeShortname = "knife.bone",
                MeleeSkinId = _config.MeleeSkinId,
                ClothingShortname = "halloween.mummysuit",
                ClothingSkinId = 0,
                DisplayName = "Necro Runner",
                AlwaysOnFire = true
            };

            _config.Profiles["brute"] = new ZombieProfile
            {
                ProfileName = "brute",
                Health = 250f,
                Speed = 5f,
                MeleeShortname = "mace.base",
                MeleeSkinId = 0,
                ClothingShortname = "halloween.mummysuit",
                ClothingSkinId = 0,
                DisplayName = "Necro Brute",
                AlwaysOnFire = true
            };

            _config.Profiles["burner"] = new ZombieProfile
            {
                ProfileName = "burner",
                Health = 120f,
                Speed = 7.5f,
                MeleeShortname = "knife.bone",
                MeleeSkinId = _config.MeleeSkinId,
                ClothingShortname = "halloween.mummysuit",
                ClothingSkinId = 0,
                DisplayName = "Necro Burner",
                AlwaysOnFire = true
            };

            _config.Profiles["stalker"] = new ZombieProfile
            {
                ProfileName = "stalker",
                Health = 125f,
                Speed = 8.0f,
                MeleeShortname = "knife.bone",
                MeleeSkinId = _config.MeleeSkinId,
                ClothingShortname = "halloween.mummysuit",
                ClothingSkinId = 0,
                DisplayName = "Necro Stalker",
                AlwaysOnFire = true
            };

            // Hellhound profile (used for wolves)
            _config.Profiles["hellhound"] = new ZombieProfile
            {
                ProfileName = "hellhound",
                Health = 150f,
                Speed = 9.0f,
                MeleeShortname = "",
                MeleeSkinId = 0,
                ClothingShortname = "",
                ClothingSkinId = 0,
                DisplayName = "Hellhound",
                AlwaysOnFire = true
            };

            _config.SpawnSets = new Dictionary<string, List<Vector3>>();

            SaveConfig();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();

            try
            {
                _config = Config.ReadObject<ConfigData>();
                if (_config == null)
                {
                    PrintWarning("Config was null, creating new default config.");
                    LoadDefaultConfig();
                }
                else
                {
                    if (_config.Profiles == null)
                        _config.Profiles = new Dictionary<string, ZombieProfile>();

                    if (!_config.Profiles.ContainsKey("default"))
                    {
                        _config.Profiles["default"] = new ZombieProfile
                        {
                            ProfileName = "default",
                            Health = _config.ZombieHealth,
                            Speed = _config.ZombieSpeed,
                            MeleeShortname = _config.MeleeShortname,
                            MeleeSkinId = _config.MeleeSkinId,
                            ClothingShortname = _config.ClothingShortname,
                            ClothingSkinId = _config.ClothingSkinId,
                            DisplayName = _config.ZombieName,
                            AlwaysOnFire = true
                        };
                    }

                    foreach (var kvp in _config.Profiles)
                    {
                        var p = kvp.Value;
                        if (string.IsNullOrEmpty(p.ProfileName)) p.ProfileName = kvp.Key;
                        if (string.IsNullOrEmpty(p.MeleeShortname)) p.MeleeShortname = _config.MeleeShortname;
                        if (string.IsNullOrEmpty(p.ClothingShortname)) p.ClothingShortname = _config.ClothingShortname;
                        if (string.IsNullOrEmpty(p.DisplayName)) p.DisplayName = _config.ZombieName;
                        if (p.Health <= 0f) p.Health = _config.ZombieHealth;
                        if (p.Speed <= 0f) p.Speed = _config.ZombieSpeed;
                    }

                    if (_config.SpawnSets == null)
                        _config.SpawnSets = new Dictionary<string, List<Vector3>>();
                }
            }
            catch (Exception e)
            {
                PrintWarning($"Config file invalid, creating new default config. Error: {e.Message}");
                LoadDefaultConfig();
            }
        }

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

        #endregion

        #region Constants & State

        private const string ZombiePrefab = "assets/prefabs/npc/scarecrow/scarecrow_dungeonnoroam.prefab";
        private const string WolfPrefab = "assets/rust.ai/agents/wolf/wolf.prefab";

        private const float RaycastMaxDistance = 200f;

        // Fire & gore VFX
        private const string BurnEffectPrefab = "assets/bundled/prefabs/fx/fire/fire_v3.prefab";
        private const string BloodSlashEffect = "assets/bundled/prefabs/fx/impacts/slash/blood14slash.prefab";
        private const string FliesMediumEffect = "assets/bundled/prefabs/fx/animals/flies/flies_medium.prefab";
        private const string FliesLoopEffect = "assets/bundled/prefabs/fx/animals/flies/flies_looping.prefab";
        private const string EatCeleryEffect = "assets/bundled/prefabs/fx/gestures/eat_celery.prefab";
        private const string DrinkVomitEffect = "assets/bundled/prefabs/fx/gestures/drink_vomit.prefab";

        // CUI IDs
        private const string WaveBannerPanel = "NecroWaveBanner.Panel";
        private const string WaveBannerTitle = "NecroWaveBanner.Title";
        private const string WaveBannerSubtitle = "NecroWaveBanner.Subtitle";

        private readonly HashSet<BaseEntity> _activeZombies = new HashSet<BaseEntity>();

        // Wave state
        private bool _waveModeActive;
        private int _currentWave;
        private Vector3 _fallbackCenter;
        private string _waveProfileName = "default";
        private string _waveSpawnSetName = null;

        private readonly HashSet<BaseEntity> _currentWaveZombies = new HashSet<BaseEntity>();
        private int _currentWaveTotalToSpawn;
        private int _currentWaveSpawned;
        private int _currentWaveInitialCount;
        private float _currentWaveStartTime;
        private float _currentWaveSpawnInterval;

        private Timer _waveSpawnTimer;
        private Timer _waveCheckTimer;
        private Timer _hopTimer;

        private bool _loggedTypeOnce;

        #endregion

        #region Hooks

        private void Init()
        {
            Puts($"[NecroZombies] Using zombie prefab: {ZombiePrefab}");
            _hopTimer = timer.Every(1f, HopTick);
        }

        private void Unload()
        {
            _hopTimer?.Destroy();
            _hopTimer = null;

            _waveSpawnTimer?.Destroy();
            _waveSpawnTimer = null;

            _waveCheckTimer?.Destroy();
            _waveCheckTimer = null;

            DestroyWaveBannerForAll();
            KillAllZombiesInternal();
        }

        private void OnEntityKill(BaseNetworkable entity)
        {
            var be = entity as BaseEntity;
            if (be == null)
                return;

            if (_activeZombies.Contains(be))
                _activeZombies.Remove(be);

            if (_currentWaveZombies.Contains(be))
                _currentWaveZombies.Remove(be);
        }

        private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
        {
            if (entity == null || info == null)
                return;

            var victim = entity as BasePlayer;
            if (victim == null)
                return;

            var attackerEntity = info.Initiator as BaseEntity;
            if (attackerEntity == null || !_activeZombies.Contains(attackerEntity))
                return;

            if (!string.IsNullOrEmpty(BloodSlashEffect))
            {
                Vector3 hitPos = info.HitPositionWorld;
                if (hitPos == Vector3.zero)
                    hitPos = victim.transform.position + Vector3.up * 1.2f;

                Effect.server.Run(BloodSlashEffect, hitPos, Vector3.up, null);
            }
        }

        #endregion

        #region Chat Commands

        [ChatCommand("necro")]
        private void CmdNecro(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsValid())
                return;

            if (!player.IsAdmin)
            {
                SendReply(player, "<color=#ff5555>You must be an admin to use this command.</color>");
                return;
            }

            int amount = 1;
            string profile = "default";

            if (args.Length >= 1)
            {
                if (!int.TryParse(args[0], out amount) || amount < 1)
                {
                    SendReply(player, "<color=#ff5555>Usage:</color> /necro [amount] [profile]");
                    return;
                }
            }

            if (args.Length >= 2)
                profile = args[1];

            if (!TryGetLookPoint(player, out var hitPos))
            {
                SendReply(player, "<color=#ff5555>Could not find a valid spawn point in your line of sight.</color>");
                return;
            }

            int spawned = SpawnHordeInternal(hitPos, amount, profile, trackForWave: false);
            SendReply(player, $"<color=#55ff55>Spawned {spawned} Necro zombie(s) using profile '{profile}'.</color>");
        }

        [ChatCommand("necro_killall")]
        private void CmdNecroKillAll(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsValid())
                return;

            if (!player.IsAdmin)
            {
                SendReply(player, "<color=#ff5555>You must be an admin to use this command.</color>");
                return;
            }

            int killed = KillAllZombiesInternal();
            SendReply(player, $"<color=#ff5555>Killed {killed} active Necro zombie(s).</color>");
        }

        // /necro_wave start <profile> [spawnSetName]
        [ChatCommand("necro_wave")]
        private void CmdNecroWave(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsValid())
                return;

            if (!player.IsAdmin)
            {
                SendReply(player, "<color=#ff5555>You must be an admin to use this command.</color>");
                return;
            }

            if (args.Length == 0)
            {
                SendReply(player, "<color=#ffcc55>Usage:</color> /necro_wave start <profile> [spawnSetName] OR /necro_wave stop");
                return;
            }

            string action = args[0].ToLower();

            if (action == "stop")
            {
                StopWaveModeInternal();
                SendReply(player, "<color=#ff5555>Stopped Necro wave mode.</color>");
                return;
            }

            if (action == "start")
            {
                string profile = "default";
                string setName = null;

                if (args.Length >= 2)
                    profile = args[1];

                if (args.Length >= 3)
                    setName = args[2].ToLower();

                if (!TryGetLookPoint(player, out var center))
                {
                    SendReply(player, "<color=#ff5555>Could not find a valid center point from your view.</color>");
                    return;
                }

                if (StartWaveModeInternal(center, profile, setName))
                {
                    string setMsg = string.IsNullOrEmpty(setName) ? "no spawn set (single center)" : $"spawn set '{setName}'";
                    SendReply(player, $"<color=#55ff55>Started Necro drip-wave mode with profile '{profile}', {setMsg}.</color>");
                }
                else
                {
                    SendReply(player, "<color=#ff5555>Failed to start wave mode. Check server console for details.</color>");
                }

                return;
            }

            SendReply(player, "<color=#ffcc55>Usage:</color> /necro_wave start <profile> [spawnSetName] OR /necro_wave stop");
        }

        // /zspawnadd <setName>
        [ChatCommand("zspawnadd")]
        private void CmdZSpawnAdd(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsValid())
                return;

            if (!player.IsAdmin)
            {
                SendReply(player, "<color=#ff5555>You must be an admin to use this command.</color>");
                return;
            }

            if (args.Length < 1)
            {
                SendReply(player, "<color=#ffcc55>Usage:</color> /zspawnadd <setName>");
                return;
            }

            string setName = args[0].ToLower();

            if (!TryGetLookPoint(player, out var pos))
            {
                SendReply(player, "<color=#ff5555>Could not find a valid spawn point from your view.</color>");
                return;
            }

            if (!_config.SpawnSets.ContainsKey(setName))
                _config.SpawnSets[setName] = new List<Vector3>();

            _config.SpawnSets[setName].Add(pos);
            SaveConfig();

            SendReply(player, $"<color=#55ff55>Added spawn point to set '{setName}' at {pos}.</color>");
        }

        // /zspawndebug
        [ChatCommand("zspawndebug")]
        private void CmdZSpawnDebug(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsValid())
                return;

            if (!player.IsAdmin)
            {
                SendReply(player, "<color=#ff5555>You must be an admin to use this command.</color>");
                return;
            }

            if (_config.SpawnSets == null || _config.SpawnSets.Count == 0)
            {
                SendReply(player, "<color=#ffcc55>No spawn sets configured.</color>");
                return;
            }

            Puts("[NecroZombies] Spawn sets:");
            foreach (var kvp in _config.SpawnSets)
            {
                Puts($"  Set '{kvp.Key}': {kvp.Value.Count} point(s)");
                int idx = 0;
                foreach (var p in kvp.Value)
                {
                    Puts($"    [{idx++}] {p}");
                }
            }

            SendReply(player, "<color=#55ff55>Spawn set info printed to server console.</color>");
        }

        #endregion

        #region Core Spawn / Hop Logic

        private bool TryGetLookPoint(BasePlayer player, out Vector3 hitPoint)
        {
            hitPoint = Vector3.zero;
            if (player == null || !player.IsValid())
                return false;

            Ray ray = player.eyes.HeadRay();
            RaycastHit hit;

            int mask = Layers.Mask.World | Layers.Mask.Terrain | Layers.Mask.Deployed | Layers.Mask.Default;
            if (Physics.Raycast(ray, out hit, RaycastMaxDistance, mask))
            {
                hitPoint = hit.point;
                return true;
            }

            return false;
        }

        private ZombieProfile GetProfile(string profileName)
        {
            if (string.IsNullOrEmpty(profileName))
                profileName = "default";

            ZombieProfile profile;
            if (_config.Profiles != null && _config.Profiles.TryGetValue(profileName, out profile))
                return profile;

            if (_config.Profiles != null && _config.Profiles.TryGetValue("default", out profile))
                return profile;

            return new ZombieProfile
            {
                ProfileName = profileName,
                Health = _config.ZombieHealth,
                Speed = _config.ZombieSpeed,
                MeleeShortname = _config.MeleeShortname,
                MeleeSkinId = _config.MeleeSkinId,
                ClothingShortname = _config.ClothingShortname,
                ClothingSkinId = _config.ClothingSkinId,
                DisplayName = _config.ZombieName,
                AlwaysOnFire = true
            };
        }

        private bool SpawnNecroZombie(Vector3 position, ZombieProfile profile, bool trackForWave)
        {
            var waves = _config.Waves;

            if (_activeZombies.Count >= waves.MaxActiveZombies)
                return false;

            BaseEntity entity = GameManager.server.CreateEntity(ZombiePrefab, position, Quaternion.identity, true);
            if (entity == null)
            {
                PrintWarning($"[NecroZombies] Failed to create entity. Prefab path: {ZombiePrefab}");
                return false;
            }

            entity.Spawn();
            _activeZombies.Add(entity);

            if (trackForWave)
                _currentWaveZombies.Add(entity);

            if (!_loggedTypeOnce)
            {
                _loggedTypeOnce = true;
                Puts($"[NecroZombies] Spawned entity type: {entity.GetType().FullName}");
            }

            if (profile.AlwaysOnFire)
            {
                entity.SetFlag(BaseEntity.Flags.OnFire, true);
                entity.SendNetworkUpdate();
            }

            if (!string.IsNullOrEmpty(FliesMediumEffect))
            {
                Effect.server.Run(FliesMediumEffect, entity.transform.position + Vector3.up * 1.2f, Vector3.up, null);
            }

            var npc = entity as NPCPlayer;
            if (npc != null)
            {
                npc.startHealth = profile.Health;
                npc.health = profile.Health;
                npc.InitializeHealth(profile.Health, profile.Health);

                ConfigureNpcMovement(npc, profile);
                ConfigureNpcLoadout(npc, profile);

                if (!string.IsNullOrEmpty(profile.DisplayName))
                    npc.displayName = profile.DisplayName;
            }

            return true;
        }

        private bool SpawnHellhound(Vector3 position, ZombieProfile profile, bool trackForWave)
        {
            var waves = _config.Waves;

            if (_activeZombies.Count >= waves.MaxActiveZombies)
                return false;

            BaseEntity entity = GameManager.server.CreateEntity(WolfPrefab, position, Quaternion.identity, true);
            if (entity == null)
            {
                PrintWarning($"[NecroZombies] Failed to create hellhound entity. Prefab path: {WolfPrefab}");
                return false;
            }

            entity.Spawn();
            _activeZombies.Add(entity);

            if (trackForWave)
                _currentWaveZombies.Add(entity);

            if (!_loggedTypeOnce)
            {
                _loggedTypeOnce = true;
                Puts($"[NecroZombies] Spawned entity type: {entity.GetType().FullName} (hellhound)");
            }

            if (profile.AlwaysOnFire)
            {
                entity.SetFlag(BaseEntity.Flags.OnFire, true);
                entity.SendNetworkUpdate();
            }

            if (!string.IsNullOrEmpty(BurnEffectPrefab))
            {
                Effect.server.Run(BurnEffectPrefab, entity.transform.position + Vector3.up * 0.1f, Vector3.up, null);
            }

            // Optional: small flies at body height
            if (!string.IsNullOrEmpty(FliesMediumEffect))
            {
                Effect.server.Run(FliesMediumEffect, entity.transform.position + Vector3.up * 0.5f, Vector3.up, null);
            }

            // Most wolf prefabs are BaseNpc; if you want to directly tweak HP/speed we can experiment further.
            // For now we rely on base stats + AlwaysOnFire VFX.

            return true;
        }

        private void ConfigureNpcMovement(NPCPlayer npc, ZombieProfile profile)
        {
            if (npc == null)
                return;

            if (npc.NavAgent != null)
            {
                npc.NavAgent.speed = profile.Speed;
                npc.NavAgent.acceleration = Math.Max(npc.NavAgent.acceleration, profile.Speed * 3f);
                npc.NavAgent.stoppingDistance = 0.5f;
            }
        }

        private void ConfigureNpcLoadout(NPCPlayer npc, ZombieProfile profile)
        {
            if (npc == null || npc.inventory == null)
                return;

            try
            {
                npc.inventory.Strip();
            }
            catch (Exception e)
            {
                PrintWarning($"[NecroZombies] Error stripping NPC inventory: {e}");
            }

            if (!string.IsNullOrEmpty(profile.MeleeShortname))
            {
                Item melee = ItemManager.CreateByName(profile.MeleeShortname, 1, profile.MeleeSkinId);
                if (melee != null)
                {
                    melee.condition = melee.maxCondition;
                    melee.MoveToContainer(npc.inventory.containerBelt);
                    npc.UpdateActiveItem(melee.uid);
                }
                else
                {
                    PrintWarning($"[NecroZombies] Failed to create melee item: '{profile.MeleeShortname}'");
                }
            }

            if (!string.IsNullOrEmpty(profile.ClothingShortname))
            {
                Item clothing = ItemManager.CreateByName(profile.ClothingShortname, 1, profile.ClothingSkinId);
                if (clothing != null)
                {
                    clothing.condition = clothing.maxCondition;
                    clothing.MoveToContainer(npc.inventory.containerWear);
                }
                else
                {
                    PrintWarning($"[NecroZombies] Failed to create clothing item: '{profile.ClothingShortname}'");
                }
            }

            npc.inventory.ServerUpdate(0f);
        }

        private int SpawnHordeInternal(Vector3 center, int amount, string profileName, bool trackForWave)
        {
            var profile = GetProfile(profileName);
            int spawned = 0;

            for (int i = 0; i < amount; i++)
            {
                if (_activeZombies.Count >= _config.Waves.MaxActiveZombies)
                    break;

                Vector2 circle = UnityEngine.Random.insideUnitCircle.normalized * UnityEngine.Random.Range(2f, 3f);
                Vector3 spawnPos = center + new Vector3(circle.x, 0f, circle.y);

                if (SpawnNecroZombie(spawnPos, profile, trackForWave))
                    spawned++;
            }

            return spawned;
        }

        private int KillAllZombiesInternal()
        {
            int count = 0;
            var snapshot = new List<BaseEntity>(_activeZombies);
            foreach (var be in snapshot)
            {
                if (be != null && !be.IsDestroyed)
                {
                    be.Kill();
                    count++;
                }
            }

            _activeZombies.Clear();
            _currentWaveZombies.Clear();
            _currentWaveSpawned = 0;
            _currentWaveTotalToSpawn = 0;
            return count;
        }

        private void HopTick()
        {
            if (_activeZombies.Count == 0)
                return;

            float now = Time.realtimeSinceStartup;

            const float hopCooldown = 4f;
            const float maxLungeDistance = 8f;
            const float hopHeight = 0.6f;
            const float playerSearchRange = 30f;
            const float minFlatDistance = 4f;

            var snapshot = new List<BaseEntity>(_activeZombies);
            foreach (var be in snapshot)
            {
                if (be == null || be.IsDestroyed)
                    continue;

                var npc = be as NPCPlayer;
                if (npc == null)
                    continue;

                Vector3 npcPos = npc.transform.position;

                if (!string.IsNullOrEmpty(BurnEffectPrefab))
                {
                    Effect.server.Run(BurnEffectPrefab, npcPos + Vector3.up * 0.1f, Vector3.up, null);
                }

                if (!string.IsNullOrEmpty(FliesLoopEffect))
                {
                    Effect.server.Run(FliesLoopEffect, npcPos + Vector3.up * 1.2f, Vector3.up, null);
                }

                BasePlayer target = null;
                float bestDist = float.MaxValue;

                foreach (var player in BasePlayer.activePlayerList)
                {
                    if (player == null || player.IsDead() || player.IsSleeping())
                        continue;

                    float dist = Vector3.Distance(player.transform.position, npcPos);
                    if (dist < bestDist && dist <= playerSearchRange)
                    {
                        bestDist = dist;
                        target = player;
                    }
                }

                if (target == null)
                    continue;

                if (bestDist < minFlatDistance)
                {
                    if (!string.IsNullOrEmpty(EatCeleryEffect) && UnityEngine.Random.Range(0f, 1f) < 0.2f)
                    {
                        Effect.server.Run(EatCeleryEffect, npcPos + Vector3.up * 1.4f, Vector3.up, null);
                    }

                    if (!string.IsNullOrEmpty(DrinkVomitEffect) && UnityEngine.Random.Range(0f, 1f) < 0.05f)
                    {
                        Effect.server.Run(DrinkVomitEffect, npcPos + Vector3.up * 1.4f, Vector3.up, null);
                    }

                    continue;
                }

                uint uid = npc.net != null ? (uint)(npc.net.ID.Value & 0xFFFFFFFF) : 0u;
                int bucket = (int)(now + uid) % (int)hopCooldown;
                if (bucket != 0)
                    continue;

                Vector3 to = target.transform.position;
                Vector3 dir = (to - npcPos);
                dir.y = 0f;
                float distFlat = dir.magnitude;
                if (distFlat < 0.1f)
                    continue;

                dir /= distFlat;

                float lungeDist = Mathf.Min(maxLungeDistance, distFlat * 0.75f);
                Vector3 candidate = npcPos + dir * lungeDist + Vector3.up * hopHeight;

                RaycastHit hit;
                if (Physics.Raycast(candidate + Vector3.up * 2f, Vector3.down, out hit, 10f,
                    Layers.Mask.World | Layers.Mask.Terrain))
                {
                    candidate = hit.point + Vector3.up * 0.1f;
                }

                npc.MovePosition(candidate);
                npc.TransformChanged();
                npc.SendNetworkUpdateImmediate();
            }
        }

        #endregion

        #region Wave System + Hellhounds + CUI

        private bool IsHellhoundWave(int waveNumber)
        {
            // Every 3rd wave is a hellhound wave (mixed wolves + zombies)
            return waveNumber > 0 && (waveNumber % 3 == 0);
        }

        private bool StartWaveModeInternal(Vector3 center, string profileName, string spawnSetName)
        {
            if (_waveModeActive)
            {
                PrintWarning("[NecroZombies] Wave mode already active.");
                return false;
            }

            _waveModeActive = true;
            _currentWave = 0;
            _fallbackCenter = center;
            _waveProfileName = profileName;
            _waveSpawnSetName = string.IsNullOrEmpty(spawnSetName) ? null : spawnSetName.ToLower();

            if (_waveSpawnSetName != null)
            {
                if (_config.SpawnSets == null || !_config.SpawnSets.ContainsKey(_waveSpawnSetName) ||
                    _config.SpawnSets[_waveSpawnSetName].Count == 0)
                {
                    PrintWarning($"[NecroZombies] Spawn set '{_waveSpawnSetName}' not found or empty, falling back to single center.");
                    _waveSpawnSetName = null;
                }
                else
                {
                    Puts($"[NecroZombies] Using spawn set '{_waveSpawnSetName}' with {_config.SpawnSets[_waveSpawnSetName].Count} point(s).");
                }
            }

            _currentWaveZombies.Clear();
            _currentWaveSpawned = 0;
            _currentWaveTotalToSpawn = 0;

            _waveSpawnTimer?.Destroy();
            _waveSpawnTimer = null;
            _waveCheckTimer?.Destroy();
            _waveCheckTimer = null;

            StartNextWave();
            return true;
        }

        private void StopWaveModeInternal()
        {
            _waveModeActive = false;

            _waveSpawnTimer?.Destroy();
            _waveSpawnTimer = null;

            _waveCheckTimer?.Destroy();
            _waveCheckTimer = null;

            _currentWaveZombies.Clear();
            _currentWaveSpawned = 0;
            _currentWaveTotalToSpawn = 0;

            DestroyWaveBannerForAll();
        }

        private void StartNextWave()
        {
            if (!_waveModeActive)
                return;

            var waves = _config.Waves;

            _currentWave++;

            if (waves.MaxWaves > 0 && _currentWave > waves.MaxWaves)
            {
                Puts("[NecroZombies] Reached max waves, stopping wave mode.");
                StopWaveModeInternal();
                return;
            }

            _currentWaveZombies.Clear();
            _currentWaveSpawned = 0;

            int baseCount = waves.BaseCount;
            int countIncrease = waves.CountPerWaveIncrease;
            _currentWaveTotalToSpawn = Mathf.Max(1, baseCount + (_currentWave - 1) * countIncrease);
            _currentWaveInitialCount = _currentWaveTotalToSpawn;

            float interval = waves.SpawnIntervalSeconds * Mathf.Pow(waves.SpawnIntervalPerWaveMultiplier, _currentWave - 1);
            _currentWaveSpawnInterval = Mathf.Max(waves.SpawnIntervalMinSeconds, interval);

            _currentWaveStartTime = Time.realtimeSinceStartup;

            bool hellWave = IsHellhoundWave(_currentWave);
            if (hellWave)
                ShowStageBanner("HELLHOUNDS", "Stay sharp", waves.BannerDuration);
            else
                ShowWaveBanner(_currentWave, _waveProfileName);

            Puts($"[NecroZombies] Wave {_currentWave} starting. Hellhounds: {hellWave}. Will spawn ~{_currentWaveTotalToSpawn} total.");

            _waveSpawnTimer?.Destroy();
            _waveSpawnTimer = timer.Every(_currentWaveSpawnInterval, DripSpawnWave);

            _waveCheckTimer?.Destroy();
            _waveCheckTimer = timer.Every(1f, CheckWaveProgress);
        }

        private Vector3 GetRandomSpawnPosition()
        {
            if (_waveSpawnSetName != null &&
                _config.SpawnSets != null &&
                _config.SpawnSets.TryGetValue(_waveSpawnSetName, out var list) &&
                list != null &&
                list.Count > 0)
            {
                var point = list[UnityEngine.Random.Range(0, list.Count)];
                Vector2 circle = UnityEngine.Random.insideUnitCircle * UnityEngine.Random.Range(0.5f, 2f);
                return point + new Vector3(circle.x, 0f, circle.y);
            }

            Vector2 fallbackCircle = UnityEngine.Random.insideUnitCircle * UnityEngine.Random.Range(3f, 6f);
            return _fallbackCenter + new Vector3(fallbackCircle.x, 0f, fallbackCircle.y);
        }

        private void DripSpawnWave()
        {
            if (!_waveModeActive)
            {
                _waveSpawnTimer?.Destroy();
                _waveSpawnTimer = null;
                return;
            }

            var waves = _config.Waves;

            if (_currentWaveTotalToSpawn <= 0)
            {
                _waveSpawnTimer?.Destroy();
                _waveSpawnTimer = null;
                return;
            }

            if (_activeZombies.Count >= waves.MaxActiveZombies)
                return;

            bool hellWave = IsHellhoundWave(_currentWave);

            int groupSize = waves.GroupSize;
            int spawnedThisTick = 0;

            for (int i = 0; i < groupSize; i++)
            {
                if (_currentWaveTotalToSpawn <= 0)
                    break;

                if (_activeZombies.Count >= waves.MaxActiveZombies)
                    break;

                Vector3 spawnPos = GetRandomSpawnPosition();

                bool spawnedOk;

                if (hellWave)
                {
                    // Mixed: 50% chance wolves, 50% scarecrows
                    bool spawnWolf = UnityEngine.Random.Range(0f, 1f) < 0.5f;

                    if (spawnWolf)
                    {
                        var baseHell = GetProfile("hellhound");
                        var hellProfile = new ZombieProfile
                        {
                            ProfileName = baseHell.ProfileName,
                            MeleeShortname = baseHell.MeleeShortname,
                            MeleeSkinId = baseHell.MeleeSkinId,
                            ClothingShortname = baseHell.ClothingShortname,
                            ClothingSkinId = baseHell.ClothingSkinId,
                            DisplayName = baseHell.DisplayName,
                            Health = baseHell.Health * (1f + _config.Waves.HealthMultiplierPerWave * (_currentWave - 1)),
                            Speed = baseHell.Speed * (1f + _config.Waves.SpeedMultiplierPerWave * (_currentWave - 1)),
                            AlwaysOnFire = baseHell.AlwaysOnFire
                        };

                        spawnedOk = SpawnHellhound(spawnPos, hellProfile, trackForWave: true);
                    }
                    else
                    {
                        // normal zombies as mix
                        var baseProfile = GetProfile(_waveProfileName);
                        var waveProfile = new ZombieProfile
                        {
                            ProfileName = baseProfile.ProfileName,
                            MeleeShortname = baseProfile.MeleeShortname,
                            MeleeSkinId = baseProfile.MeleeSkinId,
                            ClothingShortname = baseProfile.ClothingShortname,
                            ClothingSkinId = baseProfile.ClothingSkinId,
                            DisplayName = $"{baseProfile.DisplayName} [Wave {_currentWave}]",
                            Health = baseProfile.Health * (1f + _config.Waves.HealthMultiplierPerWave * (_currentWave - 1)),
                            Speed = baseProfile.Speed * (1f + _config.Waves.SpeedMultiplierPerWave * (_currentWave - 1)),
                            AlwaysOnFire = baseProfile.AlwaysOnFire
                        };

                        spawnedOk = SpawnNecroZombie(spawnPos, waveProfile, trackForWave: true);
                    }
                }
                else
                {
                    var baseProfile = GetProfile(_waveProfileName);
                    var waveProfile = new ZombieProfile
                    {
                        ProfileName = baseProfile.ProfileName,
                        MeleeShortname = baseProfile.MeleeShortname,
                        MeleeSkinId = baseProfile.MeleeSkinId,
                        ClothingShortname = baseProfile.ClothingShortname,
                        ClothingSkinId = baseProfile.ClothingSkinId,
                        DisplayName = $"{baseProfile.DisplayName} [Wave {_currentWave}]",
                        Health = baseProfile.Health * (1f + _config.Waves.HealthMultiplierPerWave * (_currentWave - 1)),
                        Speed = baseProfile.Speed * (1f + _config.Waves.SpeedMultiplierPerWave * (_currentWave - 1)),
                        AlwaysOnFire = baseProfile.AlwaysOnFire
                    };

                    spawnedOk = SpawnNecroZombie(spawnPos, waveProfile, trackForWave: true);
                }

                if (spawnedOk)
                {
                    _currentWaveTotalToSpawn--;
                    _currentWaveSpawned++;
                    spawnedThisTick++;
                }
            }

            if (spawnedThisTick > 0)
            {
                Puts($"[NecroZombies] Wave {_currentWave}: drip-spawned {spawnedThisTick}, " +
                     $"{_currentWaveSpawned}/{_currentWaveInitialCount} spawned total, " +
                     $"{_currentWaveTotalToSpawn} remaining.");
            }

            if (_currentWaveTotalToSpawn <= 0)
            {
                _waveSpawnTimer?.Destroy();
                _waveSpawnTimer = null;
            }
        }

        private void CheckWaveProgress()
        {
            if (!_waveModeActive)
            {
                _waveCheckTimer?.Destroy();
                _waveCheckTimer = null;
                return;
            }

            var waves = _config.Waves;

            if (_currentWaveInitialCount <= 0)
                return;

            int aliveInWave = 0;
            foreach (var be in _currentWaveZombies)
            {
                if (be != null && !be.IsDestroyed)
                    aliveInWave++;
            }

            int deadInWave = _currentWaveInitialCount - aliveInWave;
            float killRatio = (float)deadInWave / _currentWaveInitialCount;

            if (killRatio >= waves.RequiredKillRatioToAdvance)
            {
                Puts($"[NecroZombies] Wave {_currentWave} kill ratio reached ({killRatio:P0}). Next wave in {waves.WaveStartDelay:F1}s.");
                _waveCheckTimer?.Destroy();
                _waveCheckTimer = null;

                // Intermission banner between waves
                ShowStageBanner("INTERMISSION", $"Wave {_currentWave} cleared", waves.WaveStartDelay);
                timer.Once(waves.WaveStartDelay, StartNextWave);
                return;
            }

            float elapsed = Time.realtimeSinceStartup - _currentWaveStartTime;
            if (elapsed >= waves.WaveTimeoutSeconds && waves.WaveTimeoutSeconds > 0f)
            {
                Puts($"[NecroZombies] Wave {_currentWave} timed out after {elapsed:F1}s. Forcing next wave in {waves.WaveStartDelay:F1}s.");
                _waveCheckTimer?.Destroy();
                _waveCheckTimer = null;

                ShowStageBanner("INTERMISSION", "Time's up", waves.WaveStartDelay);
                timer.Once(waves.WaveStartDelay, StartNextWave);
            }
        }

        #endregion

        #region CUI Wave / Stage Banner

        private void ShowWaveBanner(int waveNumber, string profileName)
        {
            var waves = _config.Waves;
            if (!waves.EnableWaveBanner)
                return;

            string titleText = $"WAVE {waveNumber}";
            string subText = string.IsNullOrEmpty(profileName)
                ? ""
                : profileName.ToUpperInvariant();

            string json = BuildWaveBannerJson(titleText, subText);

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected)
                    continue;

                DestroyWaveBanner(player);
                CommunityEntity.ServerInstance.ClientRPCEx(
                    new Network.SendInfo { connection = player.net.connection },
                    null,
                    "AddUI",
                    json
                );
            }

            timer.Once(waves.BannerDuration, DestroyWaveBannerForAll);
        }

        private void ShowStageBanner(string title, string subtitle, float durationSeconds)
        {
            var waves = _config.Waves;
            if (!waves.EnableWaveBanner)
                return;

            string json = BuildWaveBannerJson(title, subtitle);

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected)
                    continue;

                DestroyWaveBanner(player);
                CommunityEntity.ServerInstance.ClientRPCEx(
                    new Network.SendInfo { connection = player.net.connection },
                    null,
                    "AddUI",
                    json
                );
            }

            timer.Once(durationSeconds, DestroyWaveBannerForAll);
        }

        private void DestroyWaveBanner(BasePlayer player)
        {
            if (player == null || !player.IsConnected)
                return;

            CommunityEntity.ServerInstance.ClientRPCEx(
                new Network.SendInfo { connection = player.net.connection },
                null,
                "DestroyUI",
                WaveBannerPanel
            );
        }

        private void DestroyWaveBannerForAll()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                DestroyWaveBanner(player);
            }
        }

        private string BuildWaveBannerJson(string title, string subtitle)
        {
            var waves = _config.Waves;

            var container = new CuiElementContainer();

            // Root panel
            var panel = new CuiElement
            {
                Name = WaveBannerPanel,
                Parent = "Hud",
                Components =
                {
                    new CuiImageComponent
                    {
                        Color = "0 0 0 0.55"
                    },
                    new CuiRectTransformComponent
                    {
                        AnchorMin = "0.2 0.9",
                        AnchorMax = "0.8 0.98"
                    }
                }
            };
            container.elements.Add(panel);

            // Title
            var titleElement = new CuiElement
            {
                Name = WaveBannerTitle,
                Parent = WaveBannerPanel,
                Components =
                {
                    new CuiTextComponent
                    {
                        Text = title,
                        FontSize = waves.BannerTitleSize,
                        Align = (int)TextAnchor.MiddleCenter,
                        Color = waves.BannerTitleColor
                    },
                    new CuiRectTransformComponent
                    {
                        AnchorMin = "0 0.3",
                        AnchorMax = "1 1"
                    }
                }
            };
            container.elements.Add(titleElement);

            if (!string.IsNullOrEmpty(subtitle))
            {
                var subElement = new CuiElement
                {
                    Name = WaveBannerSubtitle,
                    Parent = WaveBannerPanel,
                    Components =
                    {
                        new CuiTextComponent
                        {
                            Text = subtitle,
                            FontSize = waves.BannerSubSize,
                            Align = (int)TextAnchor.MiddleCenter,
                            Color = waves.BannerSubColor
                        },
                        new CuiRectTransformComponent
                        {
                            AnchorMin = "0 0",
                            AnchorMax = "1 0.5"
                        }
                    }
                };
                container.elements.Add(subElement);
            }

            return JsonConvert.SerializeObject(container);
        }

        // Minimal CUI support types
        private class CuiElementContainer
        {
            [JsonProperty("elements")]
            public List<CuiElement> elements = new List<CuiElement>();
        }

        private class CuiElement
        {
            [JsonProperty("name", NullValueHandl
// ... [Code preview truncated due to length. Full source in attached file] ...

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