Files
necromants-tome-7d2d-3-2/HarmonySrc/NecroFleeSnakeTask.cs
T
AlexCubeandClaude Opus 5.5 c828202c66 Режим змеи: леденящий ужас, Стиратель, радиация без таймера
- Радиация: проверка раз в 2 с, в поясе змея сразу развеивается (таймер 30 с убран).
- Леденящий ужас: учуянные змеёй зомби (30 м) бегут, ломая препятствия;
  очередь испуга, отметка места змеи для поиска пути, выключен в кровавую луну.
  «Шум» (зомби шли к змее) удалён. Диагностика бегства в логе пока оставлена.
- Стиратель (zombieEraser): в орде игрока-змеи с шансом 0.5; взрывается на любой
  атаке и при разрыве змеёй, отбрасывает змею; тело - чёрный разрыв. Вид: своя
  раскраска на модели гавайца (Resources/Eraser), свечение маджента, звуки спайдера.
- Фиолетовая змея на карте (necroSnakeSpirit).

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

178 lines
8.1 KiB
C#
Raw 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;
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";
}
}
}