- Чертоги разума: дух в префабе chertogi под землёй, книга памяти, нож и плакат; кровать префаба - мебель necroPalaceBed (иначе становилась точкой возрождения); охрана выбрасывает и игроков (их нет в EntityAlives). - Хваты пустой руки по R: «отрицание» и «пиф-паф» - поза рук костями поверх аниматора, остальные кости заморожены, ЛКМ - заклинание, прицел краснеет на зомби. - «Отрицание»: зомби в ужасе бежит дальше 40 м и теряет игрока как цель. - «Пиф-паф»: одержимость зомби-гуманоидом - бег, прыжок, удар, дальняя атака, подрыв, крик крикуньи; тело цепенеет, зомби танкуют одержимого; область вокруг тела сжата; полоска здоровья; спавн и прорисовка у одержимого. - Книга с оглавлением (окно necroTome), глава «Некроэнергия». - Прорисовка дверей у змеи и одержимого (CheckLODs от камеры, а не от тела). - Почтовый ящик консоли и команды necrobones/necropose/necroshot для разработки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
245 lines
11 KiB
C#
245 lines
11 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;
|
||
/// <summary>Своя угроза вместо змеи (хват «отрицание», 03.10): бежать от этой точки.</summary>
|
||
public bool HasThreat;
|
||
public Vector3 Threat;
|
||
public float Speed = FleeSpeed;
|
||
/// <summary>Кого забыть, когда бегство кончится (хват «отрицание»: «должен терять меня как цель»).
|
||
/// Он же угроза: бежать от его ТЕКУЩЕГО места.</summary>
|
||
public EntityAlive Forget;
|
||
/// <summary>Больше нуля - бежать, пока Forget ближе этого (и не дольше Until).</summary>
|
||
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();
|
||
}
|
||
|
||
/// <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()
|
||
{
|
||
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;
|
||
}
|
||
|
||
/// <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.HasThreat = false;
|
||
task.Speed = FleeSpeed;
|
||
task.Forget = null;
|
||
task.FarDistance = 0f;
|
||
task.Until = Time.time + FearSeconds;
|
||
}
|
||
|
||
/// <summary>Хват «отрицание» (03.10): напугать зомби жестом игрока - бежит от игрока быстрее обычного
|
||
/// _seconds секунд, сразу и в конце бегства теряет игрока как цель.</summary>
|
||
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<NecroFleeSnakeTask>();
|
||
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";
|
||
}
|
||
}
|
||
}
|