Files
necromants-tome-7d2d-3-2/HarmonySrc/NecroFleeSnakeTask.cs
T
AlexCubeandClaude Opus 5.5 66a0dcf403 Чертоги разума, хваты пустой руки, одержимость, книга с оглавлением
- Чертоги разума: дух в префабе chertogi под землёй, книга памяти, нож и плакат;
  кровать префаба - мебель necroPalaceBed (иначе становилась точкой возрождения);
  охрана выбрасывает и игроков (их нет в EntityAlives).
- Хваты пустой руки по R: «отрицание» и «пиф-паф» - поза рук костями поверх аниматора,
  остальные кости заморожены, ЛКМ - заклинание, прицел краснеет на зомби.
- «Отрицание»: зомби в ужасе бежит дальше 40 м и теряет игрока как цель.
- «Пиф-паф»: одержимость зомби-гуманоидом - бег, прыжок, удар, дальняя атака,
  подрыв, крик крикуньи; тело цепенеет, зомби танкуют одержимого; область
  вокруг тела сжата; полоска здоровья; спавн и прорисовка у одержимого.
- Книга с оглавлением (окно necroTome), глава «Некроэнергия».
- Прорисовка дверей у змеи и одержимого (CheckLODs от камеры, а не от тела).
- Почтовый ящик консоли и команды necrobones/necropose/necroshot для разработки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:10:05 +03:00

245 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";
}
}
}