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;
/// Своя угроза вместо змеи (хват «отрицание», 03.10): бежать от этой точки.
public bool HasThreat;
public Vector3 Threat;
public float Speed = FleeSpeed;
/// Кого забыть, когда бегство кончится (хват «отрицание»: «должен терять меня как цель»).
/// Он же угроза: бежать от его ТЕКУЩЕГО места.
public EntityAlive Forget;
/// Больше нуля - бежать, пока Forget ближе этого (и не дольше Until).
public float FarDistance;
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 && !FarEnough() && 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()
{
if (Forget != null)
{
ForgetNow(theEntity, Forget);
Forget = null;
}
theEntity.moveHelper.Stop();
theEntity.SetLookPosition(Vector3.zero);
}
public override bool Continue()
{
return Time.time < Until && !theEntity.IsDead() && !FarEnough();
}
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) +
" скор " + Speed.ToString("F2"));
}
if (--retargetTicks > 0 || PathFinderThread.Instance.IsCalculatingPath(theEntity.entityId))
{
return;
}
retargetTicks = RetargetTicks;
var sw = System.Diagnostics.Stopwatch.StartNew();
FindFleePos();
theEntity.FindPath(targetPos, Speed, true, this);
if (sw.Elapsed.TotalMilliseconds > 3.0)
{
Debug.Log("[NecromancerTome] NecroFleeSnakeTask: долгий перерасчёт у " + theEntity.entityId + " - " + sw.Elapsed.TotalMilliseconds.ToString("F1") + " мс");
}
}
bool FindFleePos()
{
if (Forget != null && FarDistance > 0f)
{
Threat = Forget.position;
}
Vector3 away = theEntity.position - (HasThreat ? Threat : 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.HasThreat = false;
task.Speed = FleeSpeed;
task.Forget = null;
task.FarDistance = 0f;
task.Until = Time.time + FearSeconds;
}
/// Хват «отрицание» (03.10): напугать зомби жестом игрока - бежит от игрока быстрее обычного
/// _seconds секунд, сразу и в конце бегства теряет игрока как цель.
bool FarEnough()
{
return FarDistance > 0f && Forget != null && (theEntity.position - Forget.position).sqrMagnitude > FarDistance * FarDistance;
}
public static void ScareFrom(EntityAlive _e, EntityAlive _threat, float _seconds, float _speed, float _farDistance = 0f)
{
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);
}
task.HasThreat = true;
task.Threat = _threat.position;
task.Speed = _speed;
task.Forget = _threat;
task.FarDistance = _farDistance;
task.Until = Time.time + _seconds;
ForgetNow(_e, _threat);
// Сетка поиска пути строится вокруг игроков - здесь она есть; но зомби может быть на краю.
AstarManager.Instance?.AddLocation(_e.position, 40);
Debug.Log("[NecromancerTome] NecroFleeSnakeTask: зомби " + _e.entityId + " отрицание - бежит до " + _farDistance + " м (не дольше " + _seconds + " с), скор " + _speed);
}
public static void ForgetNow(EntityAlive _e, EntityAlive _who)
{
if (_e.GetAttackTarget() == _who)
{
_e.SetAttackTarget(null, 0);
}
if (_e.GetRevengeTarget() == _who)
{
_e.SetRevengeTarget(null);
}
}
public override string ToString()
{
return base.ToString() + ", flee to " + targetPos + ", " + (Until - Time.time).ToString("F1") + " s";
}
}
}