Files
necromants-tome-7d2d-3-2/HarmonySrc/SnakeControl.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

1288 lines
55 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 HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Режим змеи, этап 2а (2026-09-28): камера, управление и прицел. Рывок на F и пояс «Press R» -
/// позже.
///
/// ПОЧЕМУ ДВИЖЕНИЕ СВОЁ, А НЕ ФИЗИКОЙ ИГРЫ. Змея - животное с RootMotion: скорость ей даёт
/// анимация, и 6-16 м/с из неё не выжать. Поэтому змея переведена в режим полёта без коллизий
/// (как `dm` у игрока - её собственный тик больше не тянет её вниз и не толкает), а положение
/// ведём сами: шаг по горизонтали не длиннее 0,25 м, стена - это два твёрдых блока друг над
/// другом (на один блок змея вползает), землю ищем лучом по слою коллизий ландшафта (65536,
/// как у самой игры для дронов и грифов), без земли под собой - падает с ускорением.
///
/// ПОЧЕМУ МЫШЬ ЧИТАЕМ САМИ. Пока тело игрока в рэгдолле, PlayerMoveController.Update выходит
/// в самом начале, и взгляд игры не обновляется. Берём оси мыши и чувствительность из тех же
/// мест, что и он (Input "Mouse X/Y", mouseLookSensitivity, invertMouse).
///
/// КАМЕРА. vp_FPCamera в третьем лице каждый LateUpdate ставит камеру за телом игрока; постфикс
/// к нему переставляет её за змею. Порядок гарантирован - постфикс идёт сразу после.
///
/// ГРАНИЦА. Змея не заходит туда, где у чанков нет коллизий: мир подгружается вокруг ТЕЛА, и
/// за его пределами она провалилась бы сквозь пустоту.
/// </summary>
public static class SnakeControl
{
public const float WalkSpeed = 6f; // велосипед, vehicles.xml velocityMax
public const float RunSpeed = 16.5f; // мотоцикл турбо с нагнетателем
public const float Gravity = 20f;
/// <summary>Прыжок на Пробел (указание 28.09: «иначе не выбраться из ям»). 10 м/с при g = 20 -
/// подъём на 2,5 м: вместе с заползанием на блок хватает на стену в три блока.</summary>
public const float JumpSpeed = 10f;
public const float MaxStep = 0.25f;
/// <summary>Глаза змеи: над головой и чуть впереди, чтобы не смотреть изнутри своей морды.</summary>
/// Тест 28.09: «упирается в складки местности - камера уходит под текстуры» → глаза выше и
/// позади головы, плюс не пускаем камеру в землю (см. PlaceCamera).
/// Тест 28.09 (второй): «камеру чуть выше, чтобы видеть голову змеи» → глаза над телом,
/// позади головы: голова впереди и чуть ниже, в поле зрения.
public const float EyeUp = 0.5f;
public const float EyeForward = -1.2f;
/// <summary>Как часто змея проверяет радиацию края карты. Указание 02.10: без таймера, сразу
/// в тело; проверка раз в 2 с ради нагрузки (было 30 с отсчёта и проверка раз в 0.5 с).</summary>
public const float RadiationCheckPeriod = 2f;
/// <summary>Порог игры: buffRadiation03 вешается при _biomeradiation &gt; 2.5
/// (buffs.xml, buffStatusCheck01), значение - biomeProvider.GetRadiationAt.</summary>
public const float RadiationThreshold = 2.5f;
public const float TargetRange = 30f; // дальность будущего рывка
public const float TargetRadius = 0.9f; // насколько мимо центра противника можно целиться
public const int TerrainMask = 65536;
/// <summary>Слои, о которые змея упирается: ландшафт и блоки (16) плюс модели блоков - двери,
/// люки, мебель (30). Та же маска, что у игры для лучей от головы игрока
/// (EntityPlayerLocal: 1073807360).</summary>
public const int WallMask = 1073807360;
/// <summary>Радиус проверочных сфер при шаге. Меньше полублока - в дверной проём и щель в
/// один блок змея пролезает.</summary>
public const float BodyRadius = 0.25f;
/// <summary>Параметр Forward аниматора змеи при ползании и на Shift. Подобраны на глаз:
/// у животного с RootMotion полный ход даёт landMovementFactor * 2.5 = 0.25.</summary>
public static float AnimWalk = 0.25f;
public static float AnimRun = 0.6f;
/// <summary>Тревога у тела (указание 28.09): враг ближе 10 блоков к лежащему телу - прицел
/// мигает раз в секунду и надпись «Враг приближается к телу». Орды и прочие идут к телу, не к змее.</summary>
public const float BodyDangerRange = 10f;
public static bool BodyDanger;
static float _bodyCheck;
/// <summary>Рывок на правую кнопку мыши (указание 28.09: «там обычно атака»): по прямой к
/// цели в прицеле, сквозь препятствия, со звуком атаки змеи; цель убита чёрным разрывом.</summary>
public const float DashSpeed = 60f;
public const float DashStop = 0.6f;
public const string AttackSound = "snakeattack";
static EntityAlive _dashTarget;
/// <summary>После удара змея замирает (указание 28.09: «резко останавливалась, иначе
/// проскакивает частицы»), а частицы разрыва секунду едут вместе с ней.</summary>
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;
static float _burstFollow;
static float _sleeperCheck;
/// <summary>Тёмное чутьё змеи (указание 28.09: «змее можно добавить тёмное чутьё, это логично»):
/// то же, что бафф ножа buffNecroDarkSense (buffs.xml), только от змеи - враги ближе 30 м
/// получают метку "zombie" на компасе и карте, дальше 60 м и мёртвые её теряют. Питомцы и
/// девианты не метятся (IsFoe). Бафф ножа считает от тела и тут не годится.</summary>
public const float SenseRange = 30f;
public const float SenseDropRange = 60f;
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>();
/// <summary>Убитые змеёй: опыт за них - ноль (указание 28.09: «форма не для повышения уровней
/// или лута, она для повышения уровня Некромантии»). Снимается в патче AddKillXP.</summary>
public static readonly System.Collections.Generic.HashSet<int> NoXp = new System.Collections.Generic.HashSet<int>();
public const string ToolbeltWindow = "toolbelt";
public static bool Controlling;
public static float AnimForward;
static float _radiationCheck;
/// <summary>Свой наблюдатель чанков у змеи: мир подгружается вокруг наблюдателей, а у игрока
/// он стоит у тела (тест 28.09: «змея не может уйти далеко от тела»). Так же игра грузит
/// место под сброс припасов (AIAirDrop) - только здесь с видимой сеткой и дальностью
/// обзора из настроек.</summary>
static ChunkManager.ChunkObserver _observer;
public static EntityAlive Target;
static Vector3 _pos;
static float _vy;
static bool _airborne;
static float _yaw;
static float _pitch;
static float _snakeYaw;
static bool _loggedCamera;
/// <summary>Где голова относительно корня змеи (вперёд и вверх), замеряется один раз, пока
/// змея стоит. Камера держится этой точки на ровно едущем корне, а не самой головы: голова
/// при ползании ходит из стороны в сторону (тест 28.09: «камеру мотает, укачивает»).</summary>
static float _headForward = -1f;
static float _headUp;
public static void Begin(EntityPlayerLocal _player, EntityAlive _snake)
{
_pos = _snake.position;
_vy = 0f;
_airborne = false;
_yaw = _player.rotation.y;
_pitch = -15f;
_snakeYaw = _snake.rotation.y;
_snake.IsFlyMode.Value = true;
_snake.IsNoCollisionMode.Value = true;
_loggedCamera = false;
_headForward = -1f;
_dashTarget = null;
_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;
_radiationCheck = 0f;
BodyDanger = false;
_bodyCheck = 0f;
_observer = GameManager.Instance.AddChunkObserver(_pos, true, GameUtils.GetViewDistance(), -1);
Controlling = true;
// Пояс прячется на весь режим (указание 28.09); обратно его откроет сама игра в OnHUD.
_player.PlayerUI?.windowManager.Close(ToolbeltWindow);
LogScreenEffectsOnce(_player);
SnakeVision.On(_player);
Debug.Log("[NecromancerTome] SnakeControl: управление на змее, наблюдатель чанков " + _observer.id +
", дальность " + _observer.viewDim);
}
static bool _screenEffectsLogged;
/// <summary>Разведка под «зрение змеи» (28.09: красные тона, контраст, «как комикс»): все
/// экранные эффекты игры (Resources/ScreenEffects) с шейдером и цветовыми/числовыми
/// свойствами - из чего можно собрать своё. Раз за сессию.</summary>
static void LogScreenEffectsOnce(EntityPlayerLocal _player)
{
if (_screenEffectsLogged || _player.ScreenEffectManager == null)
{
return;
}
_screenEffectsLogged = true;
foreach (ScreenEffects.ScreenEffect e in _player.ScreenEffectManager.loadedEffects)
{
Material m = e?.Material;
if (m == null || m.shader == null)
{
continue;
}
System.Text.StringBuilder sb = new System.Text.StringBuilder();
Shader sh = m.shader;
for (int i = 0; i < sh.GetPropertyCount(); i++)
{
string name = sh.GetPropertyName(i);
switch (sh.GetPropertyType(i))
{
case UnityEngine.Rendering.ShaderPropertyType.Color:
sb.Append(' ').Append(name).Append('=').Append(m.GetColor(name).ToString("F2"));
break;
case UnityEngine.Rendering.ShaderPropertyType.Float:
case UnityEngine.Rendering.ShaderPropertyType.Range:
sb.Append(' ').Append(name).Append('=').Append(m.GetFloat(name).ToString("0.###"));
break;
case UnityEngine.Rendering.ShaderPropertyType.Texture:
sb.Append(' ').Append(name).Append("=tex");
break;
}
}
Debug.Log("[NecromancerTome] ScreenEffect '" + e.Name + "' shader '" + sh.name + "':" + sb);
}
}
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;
BodyDanger = false;
if (_observer != null)
{
if (GameManager.Instance != null && GameManager.Instance.World != null)
{
GameManager.Instance.RemoveChunkObserver(_observer);
}
_observer = null;
}
}
/// <summary>Каждый кадр, из SnakeMode.OnUnityUpdate. True - змея развеялась, пора в тело.</summary>
public static bool Update(EntityPlayerLocal _player, EntityAlive _snake)
{
if (!Controlling)
{
return false;
}
float dt = Time.deltaTime;
bool gui = _player.PlayerUI != null && _player.PlayerUI.windowManager.IsModalWindowOpen();
PlayerActionsLocal input = _player.playerInput;
Vector2 move = Vector2.zero;
bool run = false;
bool jump = false;
bool attack = false;
World world = GameManager.Instance.World;
if (_freeze > 0f)
{
_freeze -= dt;
}
FollowBurst(dt);
if (!gui && input != null)
{
Look();
}
if (!gui && input != null && _freeze <= 0f)
{
move = new Vector2(input.Move.X, input.Move.Y);
run = input.Run.IsPressed;
jump = input.Jump.WasPressed;
// Указание 28.09: атака на левую кнопку, правая - выбраться из застревания.
attack = input.Primary.WasPressed;
if (input.Secondary.WasPressed)
{
Unstick(world);
}
}
if (_dashTarget == null && attack && Target != null)
{
_dashTarget = Target;
_vy = 0f;
_airborne = false;
Audio.Manager.BroadcastPlay(_snake, _snake.position, AttackSound);
Debug.Log("[NecromancerTome] SnakeControl: рывок к " + _dashTarget.EntityName + " (" + _dashTarget.entityId +
"), " + (_dashTarget.position - _pos).magnitude.ToString("F1") + " м");
}
if (_dashTarget != null)
{
Dash(_player);
_observer?.SetPosition(_pos);
_snake.SetPosition(_pos);
_snake.SetRotation(new Vector3(0f, _snakeYaw, 0f));
_snake.motion = Vector3.zero;
_snake.moveDirection = Vector3.zero;
AnimForward = AnimRun;
CheckBody(world, _player, _snake, dt);
return Radiation(world, dt);
}
Quaternion yawRot = Quaternion.Euler(0f, _yaw, 0f);
Vector3 dir = yawRot * Vector3.forward * move.y + yawRot * Vector3.right * move.x;
if (dir.sqrMagnitude > 1f)
{
dir.Normalize();
}
bool moving = dir.sqrMagnitude > 0.01f;
if (moving)
{
Vector3 delta = dir * (run ? RunSpeed : WalkSpeed) * dt;
int steps = Mathf.Max(1, Mathf.CeilToInt(delta.magnitude / MaxStep));
Vector3 d = delta / steps;
for (int i = 0; i < steps; i++)
{
// Упёрлись - пробуем скользить вдоль стены по одной из осей.
if (!TryStep(world, d) && !TryStep(world, new Vector3(d.x, 0f, 0f)) && !TryStep(world, new Vector3(0f, 0f, d.z)))
{
break;
}
}
}
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);
_observer?.SetPosition(_pos);
_snake.SetPosition(_pos);
_snake.SetRotation(new Vector3(0f, _snakeYaw, 0f));
_snake.motion = Vector3.zero;
// Анимацию ползания даёт патч AvatarAnimalController.LateUpdate ниже (AnimForward).
_snake.moveDirection = Vector3.zero;
AnimForward = moving ? (run ? AnimRun : AnimWalk) : 0f;
CheckBody(world, _player, _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>
static void Dash(EntityPlayerLocal _player)
{
EntityAlive target = _dashTarget;
if (target == null || target.IsDead() || target.IsMarkedForUnload())
{
_dashTarget = null;
return;
}
Vector3 to = target.position - _pos;
float dist = to.magnitude;
Vector3 flat = new Vector3(to.x, 0f, to.z);
if (flat.sqrMagnitude > 0.0001f)
{
_snakeYaw = Mathf.Atan2(flat.x, flat.z) * Mathf.Rad2Deg;
}
float step = DashSpeed * Time.deltaTime;
if (dist - DashStop > step)
{
_pos += to / dist * step;
return;
}
_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);
NoXp.Add(target.entityId);
NecroBurst.Pending.Add(target.entityId);
target.DamageEntity(new DamageSourceEntity(EnumDamageSource.External, EnumDamageTypes.Piercing, _player.entityId),
99999, false);
if (!target.IsDead())
{
NecroBurst.Pending.Remove(target.entityId);
NoLoot.Remove(target.entityId);
NoXp.Remove(target.entityId);
}
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)
{
if (!Controlling || _victim == null || _victim.entityId != _killId || _effect == null)
{
return;
}
_killId = -1;
_burst = _effect;
_burstOffset = _effect.position - (_pos - Origin.position);
_burstFollow = BurstFollow;
}
static void FollowBurst(float _dt)
{
if (_burst == null)
{
return;
}
_burstFollow -= _dt;
if (_burstFollow <= 0f)
{
_burst = null;
return;
}
_burst.position = _pos - Origin.position + _burstOffset;
}
/// <summary>Комнаты со спящими в зданиях просыпаются, когда в них входит ИГРОК:
/// EntityPlayer.CheckSleeperTriggers каждый тик зовёт World.CheckSleeperVolumeTouching и
/// CheckTriggerVolumeTrigger, а те берут player.position (тест 28.09: «зомби в локациях не
/// спавнятся, если змея туда заходит»). Раз в 0,25 с зовём их же, подставив позицию змеи.</summary>
static void Sleepers(World _world, EntityPlayerLocal _player, float _dt)
{
_sleeperCheck -= _dt;
if (_sleeperCheck > 0f || _world.IsRemote())
{
return;
}
_sleeperCheck = 0.25f;
Vector3 body = _player.position;
_player.position = _pos;
try
{
_world.CheckSleeperVolumeTouching(_player);
_world.CheckTriggerVolumeTrigger(_player);
}
finally
{
_player.position = body;
}
}
static void Sense(World _world, EntityAlive _snake, float _dt)
{
_senseCheck -= _dt;
if (_senseCheck > 0f)
{
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];
if (e == null)
{
continue;
}
float d2 = (e.position - _pos).sqrMagnitude;
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);
// Подсветка врагов (buffNecroSnakeSight) снята 28.09: фильтр зрения глушит её цвет.
// Бафф и его снятие оставлены - на случай возврата и для уже сохранённых зомби.
}
}
else if (_sensed.Contains(e.entityId) && (!foe || d2 > SenseDropRange * SenseDropRange))
{
e.RemoveNavObject(SenseNavObject);
e.Buffs?.RemoveBuff(Patch_AttachParticleEffectToEntity_ShrinkCharmGlow.SnakeSightBuffName);
_sensed.Remove(e.entityId);
}
}
// Исчезнувшие (разорванные, выгруженные) - просто забываем: метку игра снимет сама.
_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()
{
World world = GameManager.Instance?.World;
if (world != null)
{
foreach (int id in _sensed)
{
EntityAlive e = world.GetEntity(id) as EntityAlive;
e?.RemoveNavObject(SenseNavObject);
e?.Buffs?.RemoveBuff(Patch_AttachParticleEffectToEntity_ShrinkCharmGlow.SnakeSightBuffName);
}
}
_sensed.Clear();
}
/// <summary>Враг для змеи: живой EntityEnemy, не питомец (призрачный) и не девиант - зомби под
/// баффом Девиатора на стороне некроманта (указание 28.09: «если под бафом девианта, то это
/// не враг»). Общее для тревоги у тела и для прицела.</summary>
public static bool IsFoe(EntityAlive _e, EntityAlive _snake)
{
return _e != null && _e != _snake && _e is EntityEnemy && !_e.IsDead() &&
!IsNecroEntity(_e) &&
!GhostTraderPatch.ShouldBeGhost(_e) && !NecroCharmSide.IsCharmed(_e);
}
/// <summary>Питомцы и змея мода - тоже EntityEnemy (зомби-основа), и без этой проверки
/// охрана выбирала целью самого питомца: он ближе всех к хозяину (тест 30.09, лог
/// «pet 3838 идёт на 3838»). Все наши классы сущностей начинаются с necro.</summary>
public static bool IsNecroEntity(EntityAlive _e)
{
EntityClass cls;
return EntityClass.list.TryGetValue(_e.entityClass, out cls) && cls != null &&
cls.entityClassName != null && cls.entityClassName.StartsWith("necro");
}
/// <summary>Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу игрока.</summary>
static void CheckBody(World _world, EntityPlayerLocal _player, EntityAlive _snake, float _dt)
{
_bodyCheck -= _dt;
if (_bodyCheck > 0f)
{
return;
}
_bodyCheck = 0.5f;
bool danger = false;
Vector3 body = _player.position;
for (int i = 0; i < _world.EntityAlives.Count; i++)
{
EntityAlive e = _world.EntityAlives[i];
if (IsFoe(e, _snake) && (e.position - body).sqrMagnitude < BodyDangerRange * BodyDangerRange)
{
danger = true;
break;
}
}
if (danger != BodyDanger)
{
BodyDanger = danger;
Debug.Log("[NecromancerTome] SnakeControl: " + (danger ? "враг у тела" : "у тела чисто"));
}
}
/// <summary>Радиация края карты: змея в ней сразу развеивается, игрок в теле.</summary>
static bool Radiation(World _world, float _dt)
{
_radiationCheck -= _dt;
if (_radiationCheck > 0f)
{
return false;
}
_radiationCheck = RadiationCheckPeriod;
IBiomeProvider biomes = _world.ChunkCache?.ChunkProvider?.GetBiomeProvider();
if (biomes == null || biomes.GetRadiationAt((int)_pos.x, (int)_pos.z) <= RadiationThreshold)
{
return false;
}
Debug.Log("[NecromancerTome] SnakeControl: змея развеялась в радиации");
return true;
}
static void Look()
{
PlayerMoveController pmc = PlayerMoveController.Instance;
Vector2 sens = pmc != null ? pmc.mouseLookSensitivity : Vector2.one * 5f;
int invert = pmc != null && pmc.invertMouse != 0 ? pmc.invertMouse : 1;
_yaw += Input.GetAxis("Mouse X") * sens.x;
_pitch = Mathf.Clamp(_pitch + Input.GetAxis("Mouse Y") * sens.y * invert, -70f, 60f);
}
static bool TryStep(World _world, Vector3 _d)
{
if (_d.sqrMagnitude < 0.000001f)
{
return false;
}
Vector3 c = _pos + _d;
if (!_world.IsChunkAreaCollidersLoaded(c))
{
return false;
}
// Один блок - ступенька, два - стена. Проверяем настоящую геометрию, а не тип блока:
// у блока есть только флаг «твёрдый», и открытая дверь для него такая же стена, как
// закрытая (тест 28.09: «змея не может пройти в открытые двери»).
if (Blocked(c + Vector3.up * 0.4f) && Blocked(c + Vector3.up * 1.35f))
{
return false;
}
// В полёте землю под собой не ищем - иначе прыжок обрывался бы на первом шаге.
// Вверх подтягиваем и в полёте: так змея цепляется за край уступа.
if (Ground(c + Vector3.up * 1.6f, 2.4f, out float y) && (!_airborne || y > c.y))
{
c.y = y;
if (_airborne && _vy <= 0f)
{
_airborne = false;
_vy = 0f;
}
}
_pos = c;
return true;
}
static void Fall(float _dt, bool _jump)
{
if (!Ground(_pos + Vector3.up * 0.3f, 300f, out float ground))
{
_vy = 0f;
_airborne = false;
return;
}
if (!_airborne && _jump && ground >= _pos.y - 0.05f)
{
_vy = JumpSpeed;
_airborne = true;
}
if (!_airborne && ground >= _pos.y - 0.02f)
{
_pos.y = ground;
_vy = 0f;
return;
}
_airborne = true;
_vy -= Gravity * _dt;
float dy = _vy * _dt;
// Потолок: удар головой гасит подъём.
if (dy > 0f && Physics.Raycast(_pos + Vector3.up * 0.3f - Origin.position, Vector3.up, dy + 0.3f, TerrainMask))
{
_vy = 0f;
dy = 0f;
}
_pos.y += dy;
if (_pos.y <= ground)
{
_pos.y = ground;
_vy = 0f;
_airborne = false;
}
}
static bool Blocked(Vector3 _p)
{
return Physics.CheckSphere(_p - Origin.position, BodyRadius, WallMask, QueryTriggerInteraction.Ignore);
}
/// <summary>Правая кнопка: выбраться из текстур. Ближайшее свободное место - сначала выше
/// на месте, потом кольцами до 3 м и до 4 м вверх; там, где тело змеи ни во что не упирается.
/// Дальше змея сама падает на землю.</summary>
static void Unstick(World _world)
{
if (_dashTarget != null)
{
return;
}
for (float r = 0f; r <= 3f; r += 1f)
{
int dirs = r == 0f ? 1 : 8;
// На месте - минимум на полметра вверх: проверка может счесть место свободным, а
// змея всё равно не идёт (тест 28.09: дважды «выбралась, сдвиг 0.0 м»).
for (float h = r == 0f ? 0.5f : 0f; h <= 4f; h += 0.5f)
{
for (int i = 0; i < dirs; i++)
{
Vector3 p = _pos + Quaternion.Euler(0f, _yaw + i * 45f, 0f) * Vector3.forward * r + Vector3.up * h;
if (!_world.IsChunkAreaCollidersLoaded(p) || Blocked(p + Vector3.up * 0.4f) || Blocked(p + Vector3.up * 1f))
{
continue;
}
Debug.Log("[NecromancerTome] SnakeControl: выбралась, сдвиг " + (p - _pos).magnitude.ToString("F1") + " м");
_pos = p;
_vy = 0f;
_airborne = true;
return;
}
}
}
Debug.Log("[NecromancerTome] SnakeControl: выбраться некуда");
}
static bool Solid(World _world, Vector3 _p)
{
return _world.GetBlock(World.worldToBlockPos(_p)).Block.IsCollideMovement;
}
static bool Ground(Vector3 _from, float _dist, out float _y)
{
if (Physics.Raycast(_from - Origin.position, Vector3.down, out RaycastHit hit, _dist, TerrainMask))
{
_y = hit.point.y + Origin.position.y;
return true;
}
_y = 0f;
return false;
}
/// <summary>Из патчей камеры: вид из глаз змеи и поиск цели под прицелом.</summary>
public static void PlaceCamera(Transform _camera)
{
World world = GameManager.Instance?.World;
EntityAlive snake = SnakeMode.Snake(world);
if (snake == null)
{
return;
}
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
Quaternion yawRot = Quaternion.Euler(0f, _yaw, 0f);
Vector3 root = _pos - Origin.position;
Transform head = snake.emodel != null ? snake.emodel.GetHeadTransform() : null;
if (_headForward < 0f)
{
if (head != null)
{
Vector3 rel = head.position - (snake.position - Origin.position);
Vector3 snakeForward = Quaternion.Euler(0f, snake.rotation.y, 0f) * Vector3.forward;
_headForward = Mathf.Clamp(Vector3.Dot(rel, snakeForward), 0f, 1.5f);
_headUp = Mathf.Clamp(rel.y, 0f, 1f);
}
else
{
_headForward = 0.5f;
_headUp = 0.3f;
}
}
Vector3 eye = root + Vector3.up * (_headUp + EyeUp) + yawRot * Vector3.forward * (_headForward + EyeForward);
// Между телом змеи и глазом - земля? Камеру к телу, до препятствия.
Vector3 anchor = snake.position - Origin.position + Vector3.up * 0.5f;
Vector3 toEye = eye - anchor;
if (Physics.Raycast(anchor, toEye.normalized, out RaycastHit hit, toEye.magnitude + 0.15f, TerrainMask))
{
eye = anchor + toEye.normalized * Mathf.Max(0f, hit.distance - 0.15f);
}
// И не ниже поверхности прямо под глазом.
if (Physics.Raycast(eye + Vector3.up * 1.5f, Vector3.down, out hit, 1.5f + 0.2f, TerrainMask) && hit.point.y + 0.2f > eye.y)
{
eye.y = hit.point.y + 0.2f;
}
_camera.SetPositionAndRotation(eye, rot);
if (!_loggedCamera)
{
_loggedCamera = true;
Debug.Log("[NecromancerTome] SnakeControl: камера в глазах змеи" + (head != null ? " (голова " + head.name + ")" : " (головы нет)") +
", голова впереди " + _headForward.ToString("F2") + " м, выше " + _headUp.ToString("F2") + " м");
}
Target = FindTarget(world, snake, _camera.position, _camera.forward);
}
/// <summary>Ближайший живой противник на луче прицела не дальше TargetRange от змеи.
/// Сквозь стены - так же, как пойдёт рывок. Питомцы (призрачные) не цель.</summary>
static EntityAlive FindTarget(World _world, EntityAlive _snake, Vector3 _from, Vector3 _dir)
{
EntityAlive best = null;
float bestAlong = float.MaxValue;
Vector3 snakePos = _snake.position - Origin.position;
for (int i = 0; i < _world.EntityAlives.Count; i++)
{
EntityAlive e = _world.EntityAlives[i];
if (!IsFoe(e, _snake))
{
continue;
}
Vector3 p = e.GetChestTransformPosition();
if ((p - snakePos).sqrMagnitude > TargetRange * TargetRange)
{
continue;
}
float along = Vector3.Dot(p - _from, _dir);
if (along <= 0f || along >= bestAlong)
{
continue;
}
if (Vector3.Cross(p - _from, _dir).magnitude > TargetRadius)
{
continue;
}
best = e;
bestAlong = along;
}
return best;
}
}
/// <summary>Пока тело в рэгдолле, игра выключает vp_FPCamera и сама ведёт «камеру на себя»
/// (EntityPlayerLocal.OnRagdoll -> StartSelfCamera, каждый кадр SelfCameraFrameUpdate из Update).
/// Тест 28.09: «камера всё ещё у игрока» - постфикс к vp_FPCamera успел сработать один кадр,
/// до падения. Здесь её место занимает камера змеи.</summary>
[HarmonyPatch(typeof(EntityPlayerLocal), "SelfCameraFrameUpdate")]
public static class Patch_EntityPlayerLocal_SelfCamera_Snake
{
public static bool Prefix(EntityPlayerLocal __instance)
{
if (!SnakeControl.Controlling)
{
return true;
}
SnakeControl.PlaceCamera(__instance.cameraTransform);
return false;
}
}
/// <summary>Аниматор животного в режиме полёта получает скорость 0 (AvatarAnimalController.LateUpdate:
/// `if (!entity.IsFlyMode.Value) num = entity.speedForward`) - змея скользила неподвижной
/// (тест 28.09: «не извивается»). На время этого метода снимаем полёт и подставляем скорость.</summary>
[HarmonyPatch(typeof(AvatarAnimalController), "LateUpdate")]
public static class Patch_AvatarAnimalController_LateUpdate_Snake
{
public static void Prefix(AvatarAnimalController __instance, out bool __state)
{
__state = false;
EntityAlive e = __instance.entity;
if (!SnakeControl.Controlling || e == null || e.entityClass != EntityClass.FromString(SnakeMode.SnakeClassName) || !e.IsFlyMode.Value)
{
return;
}
__state = true;
e.IsFlyMode.Value = false;
e.speedForward = SnakeControl.AnimForward;
e.speedStrafe = 0f;
}
public static void Postfix(AvatarAnimalController __instance, bool __state)
{
if (__state)
{
__instance.entity.IsFlyMode.Value = true;
}
}
}
[HarmonyPatch(typeof(vp_FPCamera), "LateUpdate")]
public static class Patch_vpFPCamera_LateUpdate_Snake
{
public static void Postfix(vp_FPCamera __instance)
{
if (SnakeControl.Controlling)
{
SnakeControl.PlaceCamera(__instance.transform);
}
}
}
/// <summary>Прицел змеи: пиктограмма глаза из атласа игры посреди экрана, красная, когда в
/// прицеле противник. Рисуется через OnGUI, чтобы не заводить своё окно XUi.</summary>
public class SnakeCrosshair : MonoBehaviour
{
public const string Sprite = "ui_game_symbol_twitch_vision_effects";
static readonly Color Idle = new Color(0.85f, 0.95f, 1f, 0.8f);
static readonly Color OnTarget = new Color(1f, 0.15f, 0.1f, 1f);
/// <summary>Свой значок (указание 28.09: «вытащить из игры, чтобы я смог переделать»): если в
/// Resources мода лежит этот PNG, рисуется он, а не спрайт игры. Высота на экране та же,
/// ширина - по пропорциям картинки.</summary>
public const string CustomFile = "necrosnake_crosshair.png";
/// <summary>Куда один раз выгружается спрайт игры для перерисовки: закрытая папка рядом с
/// модом. У игроков её нет - тогда выгрузка просто не делается.</summary>
public const string ExportFile = "snake_crosshair_original.png";
INGUIAtlas _atlas;
UISpriteData _sprite;
Texture2D _custom;
bool _failed;
static string ModDir => System.IO.Path.GetDirectoryName(typeof(SnakeCrosshair).Assembly.Location);
public static void Create()
{
GameObject go = new GameObject("NecroSnakeCrosshair");
DontDestroyOnLoad(go);
go.AddComponent<SnakeCrosshair>();
}
void OnGUI()
{
if (!SnakeControl.Controlling || GameManager.Instance == null || GameManager.Instance.World == null ||
Event.current.type != EventType.Repaint || !Resolve())
{
return;
}
if (SnakeControl.BodyDanger)
{
DrawBodyDanger();
}
Texture tex = _custom != null ? (Texture)_custom : _atlas.texture;
Rect uv = _custom != null
? new Rect(0f, 0f, 1f, 1f)
: new Rect((float)_sprite.x / tex.width, 1f - (float)(_sprite.y + _sprite.height) / tex.height,
(float)_sprite.width / tex.width, (float)_sprite.height / tex.height);
float aspect = _custom != null ? (float)_custom.width / _custom.height : (float)_sprite.width / _sprite.height;
float h = 32f * Screen.height / 1080f;
float w = h * aspect;
Rect rect = new Rect((Screen.width - w) * 0.5f, (Screen.height - h) * 0.5f, w, h);
Color old = GUI.color;
// Враг у тела не красит прицел, а заставляет его мигать: секунду виден, секунду нет
// (указание 28.09). Красный остаётся только за целью в прицеле.
if (SnakeControl.BodyDanger && Mathf.FloorToInt(Time.unscaledTime) % 2 == 1)
{
return;
}
GUI.color = SnakeControl.Target != null ? OnTarget : Idle;
GUI.DrawTextureWithTexCoords(rect, tex, uv);
GUI.color = old;
}
GUIStyle _dangerStyle;
/// <summary>Надпись под прицелом, пока враг у тела.</summary>
void DrawBodyDanger()
{
if (_dangerStyle == null)
{
_dangerStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.UpperCenter,
fontStyle = FontStyle.Bold
};
}
_dangerStyle.fontSize = Mathf.RoundToInt(26f * Screen.height / 1080f);
string text = Localization.Get("necroSnakeBodyDanger");
float top = Screen.height * 0.5f + 40f * Screen.height / 1080f;
Rect rect = new Rect(0f, top, Screen.width, 60f * Screen.height / 1080f);
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, _dangerStyle);
GUI.color = OnTarget;
GUI.Label(rect, text, _dangerStyle);
GUI.color = old;
}
bool Resolve()
{
if (_sprite != null)
{
return true;
}
if (_failed)
{
return false;
}
XUi xui = LocalPlayerUI.GetUIForPrimaryPlayer()?.xui;
if (xui == null)
{
return false;
}
_atlas = xui.GetAtlasByName("UIAtlas", Sprite);
_sprite = _atlas?.GetSprite(Sprite);
if (_sprite == null || _atlas.texture == null)
{
_failed = true;
_sprite = null;
Debug.LogWarning("[NecromancerTome] SnakeCrosshair: спрайт '" + Sprite + "' не найден");
return false;
}
Debug.Log("[NecromancerTome] SnakeCrosshair: прицел '" + Sprite + "' " + _sprite.width + "x" + _sprite.height);
LoadCustom();
ExportOnce();
return true;
}
void LoadCustom()
{
string path = System.IO.Path.Combine(ModDir, "Resources", CustomFile);
if (!System.IO.File.Exists(path))
{
return;
}
Texture2D t = new Texture2D(2, 2, TextureFormat.RGBA32, false);
if (ImageConversion.LoadImage(t, System.IO.File.ReadAllBytes(path)))
{
t.filterMode = FilterMode.Bilinear;
_custom = t;
Debug.Log("[NecromancerTome] SnakeCrosshair: свой значок " + t.width + "x" + t.height);
}
else
{
Debug.LogWarning("[NecromancerTome] SnakeCrosshair: не прочитался " + path);
}
}
/// <summary>Вырезает спрайт из атласа в PNG. Атлас на видеокарте и нечитаем напрямую -
/// тот же приём, что GhostTraderPatch.Desaturate: копия в RenderTexture и ReadPixels.
/// У NGUI y спрайта считается сверху, у Unity - снизу.</summary>
void ExportOnce()
{
string dir = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(ModDir), "_NecromancerTome_private", "ForPainting");
string path = System.IO.Path.Combine(dir, ExportFile);
if (!System.IO.Directory.Exists(dir) || System.IO.File.Exists(path))
{
return;
}
Texture atlas = _atlas.texture;
RenderTexture previous = RenderTexture.active;
RenderTexture rt = RenderTexture.GetTemporary(atlas.width, atlas.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
try
{
Graphics.Blit(atlas, rt);
RenderTexture.active = rt;
Texture2D icon = new Texture2D(_sprite.width, _sprite.height, TextureFormat.RGBA32, false);
icon.ReadPixels(new Rect(_sprite.x, atlas.height - _sprite.y - _sprite.height, _sprite.width, _sprite.height), 0, 0);
icon.Apply();
System.IO.File.WriteAllBytes(path, ImageConversion.EncodeToPNG(icon));
Destroy(icon);
Debug.Log("[NecromancerTome] SnakeCrosshair: спрайт выгружен в " + path);
}
catch (System.Exception e)
{
Debug.LogWarning("[NecromancerTome] SnakeCrosshair: выгрузка не удалась: " + e.Message);
}
finally
{
RenderTexture.active = previous;
RenderTexture.ReleaseTemporary(rt);
}
}
}
/// <summary>Зомби вокруг змеи (тест 28.09: «вокруг змеи не спавнятся зомби»). Биомный спавн
/// работает в чанках у игрока: SpawnManagerBiomes.SpawnUpdate пропускает область, если ни один
/// игрок не стоит ближе 40 м, и ставит зомби не ближе 28 м от игроков и вне их взгляда. Чанки
/// вокруг змеи загружены её наблюдателем и перебираются миром, но эти проверки смотрят на тело.
/// На время вызова переносим позицию игрока к змее - все проверки считаются от неё.</summary>
[HarmonyPatch(typeof(SpawnManagerBiomes), "SpawnUpdate")]
public static class Patch_SpawnManagerBiomes_SpawnUpdate_Snake
{
public static void Prefix(out Vector3 __state)
{
__state = Vector3.zero;
if (!SnakeControl.Controlling)
{
return;
}
World world = GameManager.Instance.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
EntityAlive snake = SnakeMode.Snake(world);
if (player == null || snake == null)
{
return;
}
__state = player.position;
player.position = snake.position;
}
public static void Postfix(Vector3 __state)
{
if (__state == Vector3.zero)
{
return;
}
EntityPlayerLocal player = GameManager.Instance.World?.GetPrimaryPlayer();
if (player != null)
{
player.position = __state;
}
}
}
/// <summary>Зомби у змеи не исчезают (тест 28.09: «зомби почему-то исчезают возле змеи»).
/// EntityAlive.CheckDespawn мерит расстояние до ближайшего ИГРОКА, то есть до тела: биомный
/// зомби дальше 128 м от него, которого давно не видели, удаляется. Если зомби не дальше 80 м от
/// змеи (тот же порог, при котором игра обнуляет счётчик «рядом нет игрока»), проверку
/// пропускаем и счётчик сбрасываем. Остальные - по правилам игры, от тела.</summary>
[HarmonyPatch(typeof(EntityAlive), "CheckDespawn")]
public static class Patch_EntityAlive_CheckDespawn_Snake
{
public static bool Prefix(EntityAlive __instance, ref int ___ticksNoPlayerAdjacent)
{
if (!SnakeControl.Controlling)
{
return true;
}
EntityAlive snake = SnakeMode.Snake(__instance.world as World);
if (snake == null || snake == __instance || (__instance.position - snake.position).sqrMagnitude > 6400f)
{
return true;
}
___ticksNoPlayerAdjacent = 0;
return false;
}
}
/// <summary>Пояс в режиме змеи скрыт (указание 28.09: «прятать оверлей с поясом»). Игра каждый
/// кадр в EntityPlayerLocal.OnHUD открывает окно "toolbelt", если оно закрыто, - поэтому не
/// закрываем его раз за разом, а не даём открыться: все Open сходятся в openInternal.</summary>
[HarmonyPatch(typeof(GUIWindowManager), "openInternal")]
public static class Patch_GUIWindowManager_openInternal_SnakeToolbelt
{
public static bool Prefix(GUIWindow _w)
{
return !(SnakeControl.Controlling && _w != null && _w.Id == SnakeControl.ToolbeltWindow);
}
}
/// <summary>Звуки у змеи слышны (тест 28.09: «звук при убийстве не срабатывает»). Audio.Manager
/// мерит расстояние до ИГРОКА (LocalPlayer().position - тело) и не создаёт звук дальше
/// maxDistance его источника (LoadAudio). Громкость потом считает сам Unity от слушателя - камеры
/// змеи. На время вызова переносим позицию игрока к змее.</summary>
public static class SnakeAudioSwap
{
public static Vector3 Prefix()
{
if (!SnakeControl.Controlling)
{
return Vector3.zero;
}
World world = GameManager.Instance?.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
EntityAlive snake = SnakeMode.Snake(world);
if (player == null || snake == null)
{
return Vector3.zero;
}
Vector3 body = player.position;
player.position = snake.position;
return body;
}
public static void Postfix(Vector3 _body)
{
if (_body == Vector3.zero)
{
return;
}
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
if (player != null)
{
player.position = _body;
}
}
}
[HarmonyPatch(typeof(Audio.Manager), nameof(Audio.Manager.Play), new[] { typeof(Vector3), typeof(string), typeof(int), typeof(bool), typeof(float) })]
public static class Patch_AudioManager_PlayAt_Snake
{
public static void Prefix(out Vector3 __state) { __state = SnakeAudioSwap.Prefix(); }
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
}
[HarmonyPatch(typeof(Audio.Manager), nameof(Audio.Manager.Play), new[] { typeof(Entity), typeof(string), typeof(float), typeof(bool) })]
public static class Patch_AudioManager_PlayOn_Snake
{
public static void Prefix(out Vector3 __state) { __state = SnakeAudioSwap.Prefix(); }
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
}
/// <summary>Отрисовка мира вокруг змеи (тест 28.09: «есть блоки, которые прорисовываются, только
/// если рядом игрок»). Сами чанки у змеи грузит и строит её наблюдатель, но несколько систем
/// каждый кадр смотрят на позицию ИГРОКА - тела:
/// - WorldEnvironment.Update: дальний ландшафт (UnityDistantTerrainTest.FrameUpdate - вокруг
/// игрока в нём вырез под настоящие чанки) и ChunkGameObject.CheckLODs (декали ближе 41 м,
/// трава ближе 48-112 м);
/// - DynamicMeshManager.Update: упрощённые модели зданий вдали (Dynamic Mesh) - какие регионы
/// грузить и выгружать и где показывать настоящие чанки вместо упрощённых.
/// На время этих вызовов позиция игрока - у змеи, как и для звука (SnakeAudioSwap).</summary>
[HarmonyPatch(typeof(WorldEnvironment), "Update")]
public static class Patch_WorldEnvironment_Update_Snake
{
public static void Prefix(out Vector3 __state) { __state = SnakeAudioSwap.Prefix(); }
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
}
[HarmonyPatch(typeof(DynamicMeshManager), "Update")]
public static class Patch_DynamicMeshManager_Update_Snake
{
public static void Prefix(out Vector3 __state) { __state = SnakeAudioSwap.Prefix(); }
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
}
/// <summary>Мешок с добычей падает в EntityAlive.dropItemOnDeath (шанс LootDropProb). Для
/// убитых змеёй - не зовём вовсе.</summary>
[HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")]
public static class Patch_EntityAlive_dropItemOnDeath_Snake
{
public static bool Prefix(EntityAlive __instance)
{
return !SnakeControl.NoLoot.Remove(__instance.entityId);
}
}
/// <summary>Компас в режиме змеи: направление он и так берёт у камеры (то есть у змеи), а точку
/// отсчёта - у игрока, то есть у тела. На время его Update позиция игрока - у змеи, иначе
/// метки Тёмного чутья показывали бы направление от тела.</summary>
[HarmonyPatch(typeof(XUiC_CompassWindow), nameof(XUiC_CompassWindow.Update))]
public static class Patch_XUiC_CompassWindow_Update_Snake
{
public static void Prefix(out Vector3 __state) { __state = SnakeAudioSwap.Prefix(); }
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
}
/// <summary>Ноль опыта за убийство змеёй. EntityPlayer.AddKillXP - единственное место, где игра
/// начисляет опыт за убийство; префикс пропускает его целиком. Некромантия НЕ теряется: её счёт -
/// постфикс на том же методе (NecromancyKillCreditPatch), а Harmony вызывает постфиксы и тогда,
/// когда префикс отменил оригинал. Рост урона ножа идёт от того же счётчика.</summary>
[HarmonyPatch(typeof(EntityPlayer), "AddKillXP")]
public static class Patch_EntityPlayer_AddKillXP_SnakeNoXp
{
public static bool Prefix(EntityAlive killedEntity)
{
return killedEntity == null || !SnakeControl.NoXp.Remove(killedEntity.entityId);
}
}
}