- Радиация: проверка раз в 2 с, в поясе змея сразу развеивается (таймер 30 с убран). - Леденящий ужас: учуянные змеёй зомби (30 м) бегут, ломая препятствия; очередь испуга, отметка места змеи для поиска пути, выключен в кровавую луну. «Шум» (зомби шли к змее) удалён. Диагностика бегства в логе пока оставлена. - Стиратель (zombieEraser): в орде игрока-змеи с шансом 0.5; взрывается на любой атаке и при разрыве змеёй, отбрасывает змею; тело - чёрный разрыв. Вид: своя раскраска на модели гавайца (Resources/Eraser), свечение маджента, звуки спайдера. - Фиолетовая змея на карте (necroSnakeSpirit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
178 lines
8.1 KiB
C#
178 lines
8.1 KiB
C#
using GamePath;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// «Леденящий ужас» (указание 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-задачи в сейв не пишутся.
|
||
/// </summary>
|
||
public class NecroFleeSnakeTask : EAIBase
|
||
{
|
||
public const int Priority = 1;
|
||
/// <summary>На сколько блоков вперёд выбирается точка бегства; как у ванили.</summary>
|
||
public const float FleeDistance = 20f;
|
||
/// <summary>Сколько секунд зомби ещё бежит после последнего «учуяла» (Sense - раз в 1 с).</summary>
|
||
public const float FearSeconds = 4f;
|
||
/// <summary>Тиков ИИ (20 в секунду) между выборами новой точки и между запросами пути.</summary>
|
||
public const int RetargetTicks = 40;
|
||
/// <summary>Скорость бегства - максимум бега зомби-людей в entityclasses.xml (MoveSpeedAggro до
|
||
/// 1.5; быстрее только звери: медведь 2.0, пума 2.3). Указание 02.10: «пусть убегают быстро».</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>Сколько криков тревоги уже было в этом кадре: у толпы кричат не все.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Уже напуган - продлить испуг (дёшево). False - ещё не напуган, нужна очередь.</summary>
|
||
public static bool Refresh(EntityAlive _e)
|
||
{
|
||
NecroFleeSnakeTask task = _e.aiManager?.tasks.GetTask<NecroFleeSnakeTask>();
|
||
if (task == null)
|
||
{
|
||
return false;
|
||
}
|
||
task.Until = Time.time + FearSeconds;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>Из очереди SnakeControl, только на сервере: напугать (или продлить испуг).</summary>
|
||
public static void Scare(EntityAlive _e)
|
||
{
|
||
if (_e.aiManager == null)
|
||
{
|
||
return;
|
||
}
|
||
_e.ConditionalTriggerSleeperWakeUp();
|
||
NecroFleeSnakeTask task = _e.aiManager.tasks.GetTask<NecroFleeSnakeTask>();
|
||
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";
|
||
}
|
||
}
|
||
}
|