Режим змеи: леденящий ужас, Стиратель, радиация без таймера
- Радиация: проверка раз в 2 с, в поясе змея сразу развеивается (таймер 30 с убран). - Леденящий ужас: учуянные змеёй зомби (30 м) бегут, ломая препятствия; очередь испуга, отметка места змеи для поиска пути, выключен в кровавую луну. «Шум» (зомби шли к змее) удалён. Диагностика бегства в логе пока оставлена. - Стиратель (zombieEraser): в орде игрока-змеи с шансом 0.5; взрывается на любой атаке и при разрыве змеёй, отбрасывает змею; тело - чёрный разрыв. Вид: своя раскраска на модели гавайца (Resources/Eraser), свечение маджента, звуки спайдера. - Фиолетовая змея на карте (necroSnakeSpirit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6ded684d2c
commit
c828202c66
+130
-84
@@ -40,8 +40,9 @@ namespace NecromancerTome
|
||||
/// позади головы: голова впереди и чуть ниже, в поле зрения.
|
||||
public const float EyeUp = 0.5f;
|
||||
public const float EyeForward = -1.2f;
|
||||
/// <summary>Сколько змея живёт в радиации края карты (указание 28.09: «таймер 30 сек»).</summary>
|
||||
public const float RadiationTime = 30f;
|
||||
/// <summary>Как часто змея проверяет радиацию края карты. Указание 02.10: без таймера, сразу
|
||||
/// в тело; проверка раз в 2 с ради нагрузки (было 30 с отсчёта и проверка раз в 0.5 с).</summary>
|
||||
public const float RadiationCheckPeriod = 2f;
|
||||
/// <summary>Порог игры: buffRadiation03 вешается при _biomeradiation > 2.5
|
||||
/// (buffs.xml, buffStatusCheck01), значение - biomeProvider.GetRadiationAt.</summary>
|
||||
public const float RadiationThreshold = 2.5f;
|
||||
@@ -78,6 +79,11 @@ namespace NecromancerTome
|
||||
public const float KillFreeze = 0.35f;
|
||||
public const float BurstFollow = 1f;
|
||||
static float _freeze;
|
||||
/// <summary>Отброс взрывом Стирателя (02.10): горизонтальная скорость, гаснет сама.</summary>
|
||||
static Vector3 _kick;
|
||||
public const float KickSpeed = 14f;
|
||||
public const float KickUp = 8f;
|
||||
public const float KickDamping = 3f;
|
||||
static int _killId = -1;
|
||||
static Transform _burst;
|
||||
static Vector3 _burstOffset;
|
||||
@@ -92,6 +98,15 @@ namespace NecromancerTome
|
||||
public const string SenseNavObject = "zombie";
|
||||
static readonly System.Collections.Generic.HashSet<int> _sensed = new System.Collections.Generic.HashSet<int>();
|
||||
static float _senseCheck;
|
||||
/// <summary>Очередь испуга (указание 02.10: «пусть пугаются по очереди» - толпа, напуганная
|
||||
/// разом, давала фризы): новые учуянные встают в очередь, пугается по одному раз в
|
||||
/// FearInterval. Уже напуганным страх продлевается сразу, это дёшево.</summary>
|
||||
static readonly System.Collections.Generic.Queue<int> _fearQueue = new System.Collections.Generic.Queue<int>();
|
||||
static readonly System.Collections.Generic.HashSet<int> _fearQueued = new System.Collections.Generic.HashSet<int>();
|
||||
static float _fearCheck;
|
||||
public const string SnakeMapNavObject = "necroSnakeSpirit";
|
||||
static EntityAlive _mapSnake;
|
||||
public const float FearInterval = 0.25f;
|
||||
/// <summary>Убитые змеёй: с них не падает мешок с добычей (указание 28.09: «в режиме змеи
|
||||
/// лут не выпадал вообще»). Снимается в патче dropItemOnDeath.</summary>
|
||||
public static readonly System.Collections.Generic.HashSet<int> NoLoot = new System.Collections.Generic.HashSet<int>();
|
||||
@@ -99,18 +114,11 @@ namespace NecromancerTome
|
||||
/// или лута, она для повышения уровня Некромантии»). Снимается в патче AddKillXP.</summary>
|
||||
public static readonly System.Collections.Generic.HashSet<int> NoXp = new System.Collections.Generic.HashSet<int>();
|
||||
|
||||
/// <summary>Зомби, которые заметили змею (указание 28.09: «встречные зомби просто стоят,
|
||||
/// скучно - пусть начинают ходить, если змея рядом»): не дальше StirRange идут к ней, как на
|
||||
/// шум. Нападать всё равно не станут - змей зомби не трогают.</summary>
|
||||
public const float StirRange = 20f;
|
||||
static float _stirCheck;
|
||||
|
||||
public const string ToolbeltWindow = "toolbelt";
|
||||
|
||||
public static bool Controlling;
|
||||
public static float AnimForward;
|
||||
/// <summary>Секунды до развеивания; меньше нуля - змея не в радиации.</summary>
|
||||
public static float Countdown = -1f;
|
||||
static float _radiationCheck;
|
||||
/// <summary>Свой наблюдатель чанков у змеи: мир подгружается вокруг наблюдателей, а у игрока
|
||||
/// он стоит у тела (тест 28.09: «змея не может уйти далеко от тела»). Так же игра грузит
|
||||
@@ -144,14 +152,19 @@ namespace NecromancerTome
|
||||
_loggedCamera = false;
|
||||
_headForward = -1f;
|
||||
_dashTarget = null;
|
||||
_stirCheck = 0f;
|
||||
_freeze = 0f;
|
||||
_kick = Vector3.zero;
|
||||
_killId = -1;
|
||||
_burst = null;
|
||||
_sleeperCheck = 0f;
|
||||
_senseCheck = 0f;
|
||||
_fearQueue.Clear();
|
||||
_fearQueued.Clear();
|
||||
_fearCheck = 0f;
|
||||
// Фиолетовая змея на карте (nav_objects.xml, necroSnakeSpirit), указание 02.10.
|
||||
_snake.AddNavObject(SnakeMapNavObject, null, null);
|
||||
_mapSnake = _snake;
|
||||
Target = null;
|
||||
Countdown = -1f;
|
||||
_radiationCheck = 0f;
|
||||
BodyDanger = false;
|
||||
_bodyCheck = 0f;
|
||||
@@ -210,11 +223,17 @@ namespace NecromancerTome
|
||||
public static void End()
|
||||
{
|
||||
ClearSense();
|
||||
if (_mapSnake != null)
|
||||
{
|
||||
_mapSnake.RemoveNavObject(SnakeMapNavObject);
|
||||
_mapSnake = null;
|
||||
}
|
||||
_fearQueue.Clear();
|
||||
_fearQueued.Clear();
|
||||
Controlling = false;
|
||||
SnakeVision.Off(GameManager.Instance?.World?.GetPrimaryPlayer());
|
||||
_dashTarget = null;
|
||||
Target = null;
|
||||
Countdown = -1f;
|
||||
BodyDanger = false;
|
||||
if (_observer != null)
|
||||
{
|
||||
@@ -305,6 +324,20 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_kick.sqrMagnitude > 0.01f)
|
||||
{
|
||||
Vector3 kd = _kick * dt;
|
||||
int ks = Mathf.Max(1, Mathf.CeilToInt(kd.magnitude / MaxStep));
|
||||
for (int i = 0; i < ks; i++)
|
||||
{
|
||||
if (!TryStep(world, kd / ks))
|
||||
{
|
||||
_kick = Vector3.zero;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_kick *= Mathf.Exp(-KickDamping * dt);
|
||||
}
|
||||
// Вид от первого лица: змея смотрит туда же, куда игрок.
|
||||
_snakeYaw = Mathf.LerpAngle(_snakeYaw, _yaw, dt * 12f);
|
||||
Fall(dt, jump);
|
||||
@@ -317,12 +350,37 @@ namespace NecromancerTome
|
||||
_snake.moveDirection = Vector3.zero;
|
||||
AnimForward = moving ? (run ? AnimRun : AnimWalk) : 0f;
|
||||
CheckBody(world, _player, _snake, dt);
|
||||
Stir(world, _snake, dt);
|
||||
Sleepers(world, _player, dt);
|
||||
Sense(world, _snake, dt);
|
||||
FearQueue(world, _snake, dt);
|
||||
return Radiation(world, dt);
|
||||
}
|
||||
|
||||
static void FearQueue(World _world, EntityAlive _snake, float _dt)
|
||||
{
|
||||
_fearCheck -= _dt;
|
||||
if (_fearCheck > 0f || _fearQueue.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_fearCheck = FearInterval;
|
||||
bool bloodMoon = _world.aiDirector?.BloodMoonComponent?.BloodMoonActive ?? false;
|
||||
while (_fearQueue.Count > 0)
|
||||
{
|
||||
int id = _fearQueue.Dequeue();
|
||||
_fearQueued.Remove(id);
|
||||
EntityAlive e = _world.GetEntity(id) as EntityAlive;
|
||||
// Ушёл из чутья, умер, стал своим, началась луна - пропускаем и берём следующего.
|
||||
if (e == null || bloodMoon || _world.IsRemote() || !IsFoe(e, _snake) ||
|
||||
(e.position - _pos).sqrMagnitude > SenseRange * SenseRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
NecroFleeSnakeTask.Scare(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Шаг рывка: по прямой к ногам цели, без земли и стен. Долетела - убийство: цель
|
||||
/// помечается для чёрного разрыва (NecroBurst.Pending) и получает смертельный урон ОТ ИГРОКА -
|
||||
/// тогда игра сама выдаёт опыт, а NecromancyKillCreditPatch засчитывает убийство в Некромантию.</summary>
|
||||
@@ -349,6 +407,11 @@ namespace NecromancerTome
|
||||
}
|
||||
_pos = target.position - (flat.sqrMagnitude > 0.0001f ? flat.normalized * DashStop : Vector3.zero);
|
||||
_dashTarget = null;
|
||||
// Стиратель от разрыва взрывается; отброс змеи - из EraserPatch.Explode.
|
||||
if (EraserPatch.IsEraser(target) && EraserPatch.Explode(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_freeze = KillFreeze;
|
||||
_killId = target.entityId;
|
||||
NoLoot.Add(target.entityId);
|
||||
@@ -365,6 +428,32 @@ namespace NecromancerTome
|
||||
Debug.Log("[NecromancerTome] SnakeControl: удар по " + target.EntityName + ", " + (target.IsDead() ? "убит" : "ВЫЖИЛ"));
|
||||
}
|
||||
|
||||
/// <summary>Из EraserPatch.Explode: змею ближе _range от взрыва отбрасывает прочь и вверх.
|
||||
/// Не ранит - змея бессмертна.</summary>
|
||||
public static void Knockback(Vector3 _from, float _range)
|
||||
{
|
||||
if (!Controlling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Vector3 away = _pos - _from;
|
||||
if (away.sqrMagnitude > _range * _range)
|
||||
{
|
||||
return;
|
||||
}
|
||||
away.y = 0f;
|
||||
if (away.sqrMagnitude < 0.01f)
|
||||
{
|
||||
away = Quaternion.Euler(0f, _yaw, 0f) * Vector3.back;
|
||||
}
|
||||
_dashTarget = null;
|
||||
_kick = away.normalized * KickSpeed;
|
||||
_vy = KickUp;
|
||||
_airborne = true;
|
||||
_freeze = 0.6f;
|
||||
Debug.Log("[NecromancerTome] SnakeControl: отброшена взрывом Стирателя");
|
||||
}
|
||||
|
||||
/// <summary>Из NecroBurst.OnBurst: наш разрыв - запоминаем частицы, чтобы вести их за змеёй.</summary>
|
||||
public static void OnBurst(EntityAlive _victim, Transform _effect)
|
||||
{
|
||||
@@ -426,6 +515,19 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
_senseCheck = 1f;
|
||||
var senseWatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
// Леденящий ужас (02.10): учуянный зомби сразу бежит. ИИ живёт на сервере. В кровавую
|
||||
// луну ужаса нет (указание 02.10: иначе змеёй распугивают орду).
|
||||
bool scare = !_world.IsRemote() && !(_world.aiDirector?.BloodMoonComponent?.BloodMoonActive ?? false);
|
||||
NecroFleeSnakeTask.SnakePos = _pos;
|
||||
// Сетка поиска пути (AstarManager) строится только вокруг игроков (76 блоков) и отмеченных
|
||||
// мест; тело далеко - у змеи сетки нет, и путь бегства не находился (тест 02.10, лог
|
||||
// «путь нет»). Отмечаем место змеи, как игра отмечает точки орды (AddLocation(pos, 40) в
|
||||
// AIDirectorBloodMoonParty). Отметка живёт 4 с и сливается с соседней - не копится.
|
||||
if (!_world.IsRemote())
|
||||
{
|
||||
AstarManager.Instance?.AddLocation(_pos, 76);
|
||||
}
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
@@ -437,6 +539,12 @@ namespace NecromancerTome
|
||||
bool foe = IsFoe(e, _snake);
|
||||
if (foe && d2 < SenseRange * SenseRange)
|
||||
{
|
||||
// Только наземные: зомби и звери-зомби. Стервятникам пафайндер не нужен.
|
||||
if (scare && (e is EntityZombie || e is EntityEnemyAnimal) && !NecroFleeSnakeTask.Refresh(e) &&
|
||||
_fearQueued.Add(e.entityId))
|
||||
{
|
||||
_fearQueue.Enqueue(e.entityId);
|
||||
}
|
||||
if (_sensed.Add(e.entityId))
|
||||
{
|
||||
e.AddNavObject(SenseNavObject, null, null);
|
||||
@@ -453,6 +561,10 @@ namespace NecromancerTome
|
||||
}
|
||||
// Исчезнувшие (разорванные, выгруженные) - просто забываем: метку игра снимет сама.
|
||||
_sensed.RemoveWhere(id => _world.GetEntity(id) == null);
|
||||
if (senseWatch.Elapsed.TotalMilliseconds > 3.0)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] SnakeControl: долгое чутьё/ужас - " + senseWatch.Elapsed.TotalMilliseconds.ToString("F1") + " мс");
|
||||
}
|
||||
}
|
||||
|
||||
static void ClearSense()
|
||||
@@ -470,28 +582,6 @@ namespace NecromancerTome
|
||||
_sensed.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Раз в 2 с: спокойные зомби рядом со змеёй идут к ней, как на шум.</summary>
|
||||
static void Stir(World _world, EntityAlive _snake, float _dt)
|
||||
{
|
||||
_stirCheck -= _dt;
|
||||
if (_stirCheck > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_stirCheck = 2f;
|
||||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||||
{
|
||||
EntityAlive e = _world.EntityAlives[i];
|
||||
if (!(e is EntityZombie) || !IsFoe(e, _snake) || e.IsSleeping || e.GetAttackTarget() != null ||
|
||||
e.HasInvestigatePosition || (e.position - _pos).sqrMagnitude > StirRange * StirRange)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3 spot = _pos + new Vector3(Random.Range(-3f, 3f), 0f, Random.Range(-3f, 3f));
|
||||
e.SetInvestigatePosition(spot, 300, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Враг для змеи: живой EntityEnemy, не питомец (призрачный) и не девиант - зомби под
|
||||
/// баффом Девиатора на стороне некроманта (указание 28.09: «если под бафом девианта, то это
|
||||
/// не враг»). Общее для тревоги у тела и для прицела.</summary>
|
||||
@@ -539,32 +629,17 @@ namespace NecromancerTome
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Отсчёт в радиации края карты. Вышла из зоны - отсчёт сбрасывается.</summary>
|
||||
/// <summary>Радиация края карты: змея в ней сразу развеивается, игрок в теле.</summary>
|
||||
static bool Radiation(World _world, float _dt)
|
||||
{
|
||||
_radiationCheck -= _dt;
|
||||
if (_radiationCheck <= 0f)
|
||||
{
|
||||
_radiationCheck = 0.5f;
|
||||
IBiomeProvider biomes = _world.ChunkCache?.ChunkProvider?.GetBiomeProvider();
|
||||
bool inside = biomes != null && biomes.GetRadiationAt((int)_pos.x, (int)_pos.z) > RadiationThreshold;
|
||||
if (inside && Countdown < 0f)
|
||||
{
|
||||
Countdown = RadiationTime;
|
||||
Debug.Log("[NecromancerTome] SnakeControl: радиация, отсчёт " + RadiationTime + " с");
|
||||
}
|
||||
else if (!inside && Countdown >= 0f)
|
||||
{
|
||||
Countdown = -1f;
|
||||
Debug.Log("[NecromancerTome] SnakeControl: вышла из радиации");
|
||||
}
|
||||
}
|
||||
if (Countdown < 0f)
|
||||
if (_radiationCheck > 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Countdown -= _dt;
|
||||
if (Countdown > 0f)
|
||||
_radiationCheck = RadiationCheckPeriod;
|
||||
IBiomeProvider biomes = _world.ChunkCache?.ChunkProvider?.GetBiomeProvider();
|
||||
if (biomes == null || biomes.GetRadiationAt((int)_pos.x, (int)_pos.z) <= RadiationThreshold)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -892,11 +967,6 @@ namespace NecromancerTome
|
||||
{
|
||||
DrawBodyDanger();
|
||||
}
|
||||
if (SnakeControl.Countdown >= 0f)
|
||||
{
|
||||
DrawCountdown();
|
||||
return;
|
||||
}
|
||||
Texture tex = _custom != null ? (Texture)_custom : _atlas.texture;
|
||||
Rect uv = _custom != null
|
||||
? new Rect(0f, 0f, 1f, 1f)
|
||||
@@ -918,30 +988,6 @@ namespace NecromancerTome
|
||||
GUI.color = old;
|
||||
}
|
||||
|
||||
GUIStyle _countdownStyle;
|
||||
|
||||
/// <summary>Вместо глаза - секунды до развеивания, красным, с тенью.</summary>
|
||||
void DrawCountdown()
|
||||
{
|
||||
if (_countdownStyle == null)
|
||||
{
|
||||
_countdownStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontStyle = FontStyle.Bold
|
||||
};
|
||||
}
|
||||
_countdownStyle.fontSize = Mathf.RoundToInt(44f * Screen.height / 1080f);
|
||||
string text = Mathf.CeilToInt(SnakeControl.Countdown).ToString();
|
||||
Rect rect = new Rect(Screen.width * 0.5f - 100f, Screen.height * 0.5f - 50f, 200f, 100f);
|
||||
Color old = GUI.color;
|
||||
GUI.color = new Color(0f, 0f, 0f, 0.8f);
|
||||
GUI.Label(new Rect(rect.x + 2f, rect.y + 2f, rect.width, rect.height), text, _countdownStyle);
|
||||
GUI.color = OnTarget;
|
||||
GUI.Label(rect, text, _countdownStyle);
|
||||
GUI.color = old;
|
||||
}
|
||||
|
||||
GUIStyle _dangerStyle;
|
||||
|
||||
/// <summary>Надпись под прицелом, пока враг у тела.</summary>
|
||||
|
||||
Reference in New Issue
Block a user