using GamePath; using UnityEngine; namespace NecromancerTome { /// /// «Леденящий ужас» (указание 02.10): зомби, которого учуяла змея (тёмное чутьё, 30 м, сквозь /// стены), сразу пугается и бежит от неё как можно дальше, ломая препятствия. Спящих будим - /// «пусть будет хаос». Заменяет «шум» (зомби шли к змее, 28.09). /// /// ОБРАЗЕЦ - ванильный EAIRunAway (основа RunawayFromEntity у зверей): точка в FleeDistance от /// угрозы через RandomPositionGenerator.CalcPositionInDirection, пересчёт по таймеру. Отличия: /// - путь с canBreak: true. У ванили false, и зверь не ломает ничего. Ломает не эта задача, а /// ванильный BreakBlock (AITask-1 у зомби): он включается, когда moveHelper упёрся /// (BlockedTime ≥ 0.35) на пути с CanBreakBlocks - а этот флаг приходит из нашего пути. /// - угроза - позиция змеи, её даёт SnakeControl.Sense; задача сама ничего не ищет. /// - работает, пока Until не истёк: Sense продлевает его, пока змея чует зомби. /// /// ПРИОРИТЕТ 1, MutexBits 3. Задачи у зомби: 1 BreakBlock (биты 8), 2 ApproachAndAttackTarget /// (3), 3 ApproachSpot (3) ... EAITaskList.isBestTask: задача с меньшим номером и общими битами /// вытесняет остальные, с BreakBlock биты не пересекаются - он работает вместе с бегством. /// Добавляется в рантайме (AddTask), как NecroFollowOwnerTask; из списка не убирается - /// с истёкшим Until CanExecute сразу false, а AI-задачи в сейв не пишутся. /// public class NecroFleeSnakeTask : EAIBase { public const int Priority = 1; /// На сколько блоков вперёд выбирается точка бегства; как у ванили. public const float FleeDistance = 20f; /// Сколько секунд зомби ещё бежит после последнего «учуяла» (Sense - раз в 1 с). public const float FearSeconds = 4f; /// Тиков ИИ (20 в секунду) между выборами новой точки и между запросами пути. public const int RetargetTicks = 40; /// Скорость бегства - максимум бега зомби-людей в entityclasses.xml (MoveSpeedAggro до /// 1.5; быстрее только звери: медведь 2.0, пума 2.3). Указание 02.10: «пусть убегают быстро». public const float FleeSpeed = 1.5f; public static Vector3 SnakePos; public float Until; Vector3 targetPos; int retargetTicks; public override void Init(EntityAlive _theEntity) { base.Init(_theEntity); MutexBits = 3; executeDelay = 0.1f; } public override bool CanExecute() { return Time.time < Until && FindFleePos(); } /// Сколько криков тревоги уже было в этом кадре: у толпы кричат не все. static int _alertFrame = -1, _alertCount; public const int MaxAlertsPerFrame = 2; public override void Start() { // Тест 02.10: «если пугать зомби одновременно - фризы». Все испуганные за один проход // чутья стартовали в одном кадре: каждый кричал и сразу просил путь с ломанием блоков // (дорогой для пафайндера). Теперь первый путь - через случайные 0-1.5 с, крик - не // больше двух за кадр. retargetTicks = theEntity.rand.RandomRange(0, 30); PathFinderThread.Instance.RemovePathsFor(theEntity.entityId); if (_alertFrame != Time.frameCount) { _alertFrame = Time.frameCount; _alertCount = 0; } string alert = theEntity.GetSoundAlert(); if (alert != null && ++_alertCount <= MaxAlertsPerFrame) { theEntity.PlayOneShot(alert); } } public override void Reset() { theEntity.moveHelper.Stop(); theEntity.SetLookPosition(Vector3.zero); } public override bool Continue() { return Time.time < Until && !theEntity.IsDead(); } float _diagTime; public override void Update() { PathEntity path = theEntity.navigator.getPath(); if (path != null) { theEntity.moveHelper.CalcIfUnreachablePos(); } theEntity.SetLookPosition(targetPos + Vector3.up * 0.8f); if (Time.time > _diagTime) { // Диагностика (тест 02.10: «зомби не убегает, стоит на месте») - убрать после проверки. _diagTime = Time.time + 3f; Debug.Log("[NecromancerTome] NecroFleeSnakeTask: " + theEntity.entityId + " поз " + theEntity.position.ToString("F0") + " цель " + targetPos.ToString("F0") + " путь " + (path == null ? "нет" : path.NodeCountRemaining() + " узл.") + " считается " + PathFinderThread.Instance.IsCalculatingPath(theEntity.entityId) + " скор " + FleeSpeed.ToString("F2")); } if (--retargetTicks > 0 || PathFinderThread.Instance.IsCalculatingPath(theEntity.entityId)) { return; } retargetTicks = RetargetTicks; var sw = System.Diagnostics.Stopwatch.StartNew(); FindFleePos(); theEntity.FindPath(targetPos, FleeSpeed, true, this); if (sw.Elapsed.TotalMilliseconds > 3.0) { Debug.Log("[NecromancerTome] NecroFleeSnakeTask: долгий перерасчёт у " + theEntity.entityId + " - " + sw.Elapsed.TotalMilliseconds.ToString("F1") + " мс"); } } bool FindFleePos() { Vector3 away = theEntity.position - SnakePos; away.y = 0f; if (away.sqrMagnitude < 0.01f) { away = UnityEngine.Random.insideUnitSphere; away.y = 0f; } Vector3 p = RandomPositionGenerator.CalcPositionInDirection(theEntity, theEntity.position, away, FleeDistance, 60f); // Ванильный подбор вернул ноль (нет чанка, не нашлось опоры) - всё равно прочь по прямой: // путь найдёт пафайндер, а препятствия на нём сломает BreakBlock. targetPos = p.y != 0f ? p : theEntity.position + away.normalized * FleeDistance; // Как ванильный ApproachSpot: цель - на опорном блоке, а не в воздухе/стене. targetPos = theEntity.world.FindSupportingBlockPos(targetPos); return true; } /// Уже напуган - продлить испуг (дёшево). False - ещё не напуган, нужна очередь. public static bool Refresh(EntityAlive _e) { NecroFleeSnakeTask task = _e.aiManager?.tasks.GetTask(); if (task == null) { return false; } task.Until = Time.time + FearSeconds; return true; } /// Из очереди SnakeControl, только на сервере: напугать (или продлить испуг). public static void Scare(EntityAlive _e) { if (_e.aiManager == null) { return; } _e.ConditionalTriggerSleeperWakeUp(); NecroFleeSnakeTask task = _e.aiManager.tasks.GetTask(); if (task == null) { task = new NecroFleeSnakeTask(); task.Init(_e); _e.aiManager.tasks.AddTask(Priority, task); Debug.Log("[NecromancerTome] NecroFleeSnakeTask: зомби " + _e.entityId + " в ужасе"); } task.Until = Time.time + FearSeconds; } public override string ToString() { return base.ToString() + ", flee to " + targetPos + ", " + (Until - Time.time).ToString("F1") + " s"; } } }