- Аниматор: IsAlive, IsFPV, WeaponHoldType 0 (FP_Unarmed_Hold) по умолчанию; сдвиг 0,-1.55,0.05. - Кожа: _Albedo убран, _Tint 0.6 серый (necroarms albedo on|off - сравнить). - Пояс персонажа скрыт в одержимости от первого лица. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1531 lines
68 KiB
C#
1531 lines
68 KiB
C#
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 > 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>Точка тела для проверки «внутри текстуры»: над корнем змеи, ниже головы.</summary>
|
||
public const float BodyUp = 0.35f;
|
||
/// <summary>Как далеко вверх искать поверхность, под которой змея может быть зарыта.</summary>
|
||
public const float InsideProbe = 32f;
|
||
/// <summary>Полёт (указание 05.10): пока зажата правая кнопка. Скорость - вдвое ниже шага
|
||
/// зомби (MindControl.WalkSpeed), и в воздухе, и сквозь постройки; Shift не ускоряет.
|
||
/// Сквозь грунт (земля, песок, камень) - никак.</summary>
|
||
static bool _flying;
|
||
public const float FlySpeed = MindControl.WalkSpeed * 0.5f;
|
||
/// <summary>Не нашлось, куда выбросить змею из текстур, - пауза, а не поиск каждый кадр.</summary>
|
||
static float _ejectWait;
|
||
static Vector3[] _ejectOffsets;
|
||
|
||
/// <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;
|
||
static NecroMapMarker.Marker _mapNav;
|
||
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;
|
||
/// <summary>Область тела сжата до Possession.BodyViewDim, как у одержимого (указание 05.10:
|
||
/// «делай как у одержимого»): две полные области - лишняя память. Полный радиус - для систем,
|
||
/// рисующих мир у змеи (SnakeAudioSwap). -1 - не сжата.</summary>
|
||
static int _bodyViewDim = -1;
|
||
public static int FullViewDim => _bodyViewDim;
|
||
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;
|
||
_flying = false;
|
||
_ejectWait = 0f;
|
||
_killId = -1;
|
||
_burst = null;
|
||
_sleeperCheck = 0f;
|
||
_senseCheck = 0f;
|
||
_fearQueue.Clear();
|
||
_fearQueued.Clear();
|
||
_fearCheck = 0f;
|
||
// Фиолетовая змея на карте (nav_objects.xml, necroSnakeSpirit), указание 02.10. С 06.10 - своя
|
||
// метка по Transform (NecroMapMarker): через AddNavObject она так и не показалась.
|
||
NecroMapMarker.Hide(ref _mapNav); // старая ссылка после закрытия мира - снимется, только если ещё наша
|
||
_mapNav = NecroMapMarker.Show(_snake, NecroMapMarker.SnakeClass);
|
||
Target = null;
|
||
_radiationCheck = 0f;
|
||
BodyDanger = false;
|
||
_bodyCheck = 0f;
|
||
_observer = GameManager.Instance.AddChunkObserver(_pos, true, GameUtils.GetViewDistance(), -1);
|
||
ChunkManager.ChunkObserver bodyObserver = _player.ChunkObserver;
|
||
if (bodyObserver != null && bodyObserver.viewDim > Possession.BodyViewDim)
|
||
{
|
||
_bodyViewDim = bodyObserver.viewDim;
|
||
bodyObserver.viewDim = Possession.BodyViewDim;
|
||
}
|
||
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();
|
||
NecroMapMarker.Hide(ref _mapNav);
|
||
_fearQueue.Clear();
|
||
_fearQueued.Clear();
|
||
Controlling = false;
|
||
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||
SnakeVision.Off(player);
|
||
if (_bodyViewDim > 0 && player?.ChunkObserver != null)
|
||
{
|
||
player.ChunkObserver.viewDim = _bodyViewDim;
|
||
}
|
||
_bodyViewDim = -1;
|
||
_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 jumpHeld = false;
|
||
bool downHeld = 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;
|
||
jumpHeld = input.Jump.IsPressed;
|
||
downHeld = input.Crouch.IsPressed || input.ToggleCrouch.IsPressed;
|
||
// Указание 28.09: атака на левую кнопку. 05.10: E - аварийный выход из текстур
|
||
// (E в игре на PermanentActions.Activate, см. MindControl).
|
||
attack = input.Primary.WasPressed;
|
||
if ((input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed) && _dashTarget == null)
|
||
{
|
||
Eject(world, false, 0.5f);
|
||
}
|
||
}
|
||
// Полёт - пока зажата правая кнопка (указание 05.10); отпустил - змея падает.
|
||
bool fly = !gui && input != null && input.Secondary.IsPressed && _dashTarget == null;
|
||
if (fly != _flying)
|
||
{
|
||
_flying = fly;
|
||
_vy = 0f;
|
||
_airborne = !fly;
|
||
Debug.Log("[NecromancerTome] SnakeControl: полёт " + (fly ? "вкл" : "выкл"));
|
||
}
|
||
|
||
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);
|
||
if (_dashTarget == null)
|
||
{
|
||
KeepOut(world, dt);
|
||
}
|
||
_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 (_flying)
|
||
{
|
||
// В полёте - куда смотрит камера, Пробел вверх, приседание вниз.
|
||
dir = Quaternion.Euler(-_pitch, _yaw, 0f) * Vector3.forward * move.y + yawRot * Vector3.right * move.x +
|
||
Vector3.up * ((jumpHeld ? 1f : 0f) - (downHeld ? 1f : 0f));
|
||
}
|
||
if (dir.sqrMagnitude > 1f)
|
||
{
|
||
dir.Normalize();
|
||
}
|
||
bool moving = dir.sqrMagnitude > 0.01f;
|
||
if (moving)
|
||
{
|
||
float speed = _flying ? FlySpeed : run ? RunSpeed : WalkSpeed;
|
||
Vector3 delta = dir * speed * 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)) &&
|
||
!(_flying && TryStep(world, new Vector3(0f, d.y, 0f))))
|
||
{
|
||
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);
|
||
if (!_flying)
|
||
{
|
||
Fall(world, dt, jump);
|
||
}
|
||
KeepOut(world, dt);
|
||
_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, _pos, ref _sleeperCheck, 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) || !IsUndead(e) ||
|
||
(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;
|
||
_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())
|
||
{
|
||
NecroChallenges.Award(NecroChallenges.SnakeKill);
|
||
}
|
||
else
|
||
{
|
||
NecroBurst.Pending.Remove(target.entityId);
|
||
NoLoot.Remove(target.entityId);
|
||
NoXp.Remove(target.entityId);
|
||
}
|
||
Debug.Log("[NecromancerTome] SnakeControl: удар по " + target.EntityName + ", " + (target.IsDead() ? "убит" : "ВЫЖИЛ"));
|
||
}
|
||
|
||
/// <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 с зовём их же, подставив позицию змеи
|
||
/// (и одержимого - Possession, 05.10).</summary>
|
||
public static void Sleepers(World _world, EntityPlayerLocal _player, Vector3 _at, ref float _timer, float _dt)
|
||
{
|
||
_timer -= _dt;
|
||
if (_timer > 0f || _world.IsRemote())
|
||
{
|
||
return;
|
||
}
|
||
_timer = 0.25f;
|
||
Vector3 body = _player.position;
|
||
_player.position = _at;
|
||
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;
|
||
// Чутьё и ужас - власть над мёртвыми (указание 06.10): только нежить, живых змея не чует и не пугает.
|
||
bool foe = IsFoe(e, _snake) && IsUndead(e);
|
||
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: «если под бафом девианта, то это
|
||
/// не враг»). Общее для тревоги у тела и для прицела. Одержимый зомби - тоже не враг (13.10: «питомцы
|
||
/// нападают на одержимых»), охрана питомцев выбирает цель отсюда же.</summary>
|
||
public static bool IsFoe(EntityAlive _e, EntityAlive _snake)
|
||
{
|
||
return _e != null && _e != _snake && _e is EntityEnemy && !_e.IsDead() &&
|
||
!IsNecroEntity(_e) && !Possession.IsPossessed(_e) &&
|
||
!GhostTraderPatch.ShouldBeGhost(_e) && !NecroCharmSide.IsCharmed(_e);
|
||
}
|
||
|
||
/// <summary>Нежить - поднятые мертвецы: всё с меткой zombie (зомби-люди, зомби-собаки, -медведи,
|
||
/// -кабаны, стервятники, Стиратель). Живые звери, бандиты, игроки и торговцы - не нежить. Та же
|
||
/// метка, по которой считаются убийства в Некромантию (NecromancyKillCreditPatch.ZombieTag).</summary>
|
||
static readonly FastTags<TagGroup.Global> UndeadTag = FastTags<TagGroup.Global>.Parse("zombie");
|
||
|
||
public static bool IsUndead(EntityAlive _e) => _e != null && _e.HasAnyTags(UndeadTag);
|
||
|
||
/// <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;
|
||
}
|
||
if (_flying)
|
||
{
|
||
// Сквозь постройки можно, в грунт - нет.
|
||
if (Inside(_world, c + Vector3.up * BodyUp, TerrainMask, out bool ground) && ground)
|
||
{
|
||
return false;
|
||
}
|
||
_pos = c;
|
||
return true;
|
||
}
|
||
// Один блок - ступенька, два - стена. Проверяем настоящую геометрию, а не тип блока:
|
||
// у блока есть только флаг «твёрдый», и открытая дверь для него такая же стена, как
|
||
// закрытая (тест 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;
|
||
}
|
||
}
|
||
// Ни в грунт, ни в блоки: обрыв выше подъёма или стена, которую шар изнутри не чует.
|
||
if (Inside(_world, c + Vector3.up * BodyUp, WallMask, out _))
|
||
{
|
||
return false;
|
||
}
|
||
_pos = c;
|
||
return true;
|
||
}
|
||
|
||
static void Fall(World _world, 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;
|
||
}
|
||
// Прыжок не должен заносить тело в текстуры (указание 05.10).
|
||
if (dy > 0f && Inside(_world, _pos + Vector3.up * (dy + BodyUp), WallMask, out _))
|
||
{
|
||
_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>Каждый кадр: змея в грунте (в полёте) или в любой текстуре (на земле) -
|
||
/// выбрасываем на ближайшее свободное место (указание 05.10).</summary>
|
||
static void KeepOut(World _world, float _dt)
|
||
{
|
||
if (_ejectWait > 0f)
|
||
{
|
||
_ejectWait -= _dt;
|
||
return;
|
||
}
|
||
Vector3 body = _pos + Vector3.up * BodyUp;
|
||
if (!_world.IsChunkAreaCollidersLoaded(body))
|
||
{
|
||
return;
|
||
}
|
||
bool ground;
|
||
bool stuck = _flying ? Inside(_world, body, TerrainMask, out ground) && ground : Inside(_world, body, WallMask, out ground);
|
||
if (stuck && !Eject(_world, _flying, 0f))
|
||
{
|
||
_ejectWait = 1f;
|
||
}
|
||
}
|
||
|
||
/// <summary>Ближайшее свободное место: до 4 м вбок, до 1 м вниз и до 6 м вверх; вниз
|
||
/// дороже, чем вверх. _groundOnly - мешает только грунт (полёт). E зовёт с _minShift 0.5:
|
||
/// проверка может счесть место свободным, а змея не идёт (тест 28.09: «сдвиг 0.0 м»).</summary>
|
||
static bool Eject(World _world, bool _groundOnly, float _minShift)
|
||
{
|
||
if (_ejectOffsets == null)
|
||
{
|
||
System.Collections.Generic.List<Vector3> list = new System.Collections.Generic.List<Vector3>();
|
||
for (float r = 0f; r <= 4f; r += 0.5f)
|
||
{
|
||
int dirs = r == 0f ? 1 : 8;
|
||
for (float h = -1f; h <= 6f; h += 0.5f)
|
||
{
|
||
for (int i = 0; i < dirs; i++)
|
||
{
|
||
list.Add(Quaternion.Euler(0f, i * 45f, 0f) * Vector3.forward * r + Vector3.up * h);
|
||
}
|
||
}
|
||
}
|
||
list.Sort((x, y) => EjectCost(x).CompareTo(EjectCost(y)));
|
||
_ejectOffsets = list.ToArray();
|
||
}
|
||
for (int i = 0; i < _ejectOffsets.Length; i++)
|
||
{
|
||
Vector3 o = _ejectOffsets[i];
|
||
if (o.magnitude < _minShift || !Free(_world, _pos + o, _groundOnly))
|
||
{
|
||
continue;
|
||
}
|
||
Debug.Log("[NecromancerTome] SnakeControl: выброшена из текстур" + (_groundOnly ? " (грунт)" : "") + ", сдвиг " + o.magnitude.ToString("F1") + " м");
|
||
_pos += o;
|
||
_vy = 0f;
|
||
_airborne = !_flying;
|
||
return true;
|
||
}
|
||
Debug.Log("[NecromancerTome] SnakeControl: выбраться некуда");
|
||
return false;
|
||
}
|
||
|
||
static float EjectCost(Vector3 _o)
|
||
{
|
||
return _o.magnitude + (_o.y < 0f ? -_o.y : 0f);
|
||
}
|
||
|
||
public static bool Free(World _world, Vector3 _p, bool _groundOnly)
|
||
{
|
||
if (!_world.IsChunkAreaCollidersLoaded(_p))
|
||
{
|
||
return false;
|
||
}
|
||
bool ground;
|
||
if (_groundOnly)
|
||
{
|
||
return !(Inside(_world, _p + Vector3.up * BodyUp, TerrainMask, out ground) && ground);
|
||
}
|
||
return !Inside(_world, _p + Vector3.up * BodyUp, WallMask, out ground) &&
|
||
!Blocked(_p + Vector3.up * 0.4f) && !Blocked(_p + Vector3.up * 1f);
|
||
}
|
||
|
||
/// <summary>Точка внутри текстуры? Земля игры - поверхность, а не сплошная масса, и шар
|
||
/// (Blocked) изнутри её не чует. Поэтому так: луч вверх до первого потолка (поверхность
|
||
/// лицом вниз), потом луч от потолка вниз до точки. Попалась поверхность лицом вверх -
|
||
/// точка под ней, то есть внутри. Лучи видят только лицевые стороны, так что пары хватает
|
||
/// и в пещере, и в доме. _ground - это грунт (блок-земля под той поверхностью или в самой
|
||
/// точке), а не постройка.</summary>
|
||
public static bool Inside(World _world, Vector3 _p, int _mask, out bool _ground)
|
||
{
|
||
_ground = false;
|
||
Vector3 o = _p - Origin.position;
|
||
float top = InsideProbe;
|
||
if (Physics.Raycast(o, Vector3.up, out RaycastHit ceil, InsideProbe, _mask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
top = ceil.distance;
|
||
}
|
||
if (top < 0.05f ||
|
||
!Physics.Raycast(o + Vector3.up * (top - 0.02f), Vector3.down, out RaycastHit floor, top - 0.02f, _mask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
return false;
|
||
}
|
||
_ground = IsGround(_world, floor.point + Origin.position + Vector3.down * 0.05f) || IsGround(_world, _p);
|
||
return true;
|
||
}
|
||
|
||
static bool IsGround(World _world, Vector3 _p)
|
||
{
|
||
BlockValue b = _world.GetBlock(World.worldToBlockPos(_p));
|
||
return !b.isair && b.Block.shape.IsTerrain();
|
||
}
|
||
|
||
static bool Solid(World _world, Vector3 _p)
|
||
{
|
||
return _world.GetBlock(World.worldToBlockPos(_p)).Block.IsCollideMovement;
|
||
}
|
||
|
||
public 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];
|
||
// Атака духа змеи - только по нежити (указание 06.10): живые звери и люди ей не подвластны.
|
||
if (!IsFoe(e, _snake) || !IsUndead(e))
|
||
{
|
||
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 (MindControl.Controlling)
|
||
{
|
||
MindControl.PlaceCamera(__instance.cameraTransform);
|
||
return false;
|
||
}
|
||
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);
|
||
}
|
||
else if (Possession.Active)
|
||
{
|
||
Possession.PlaceCamera(__instance.transform);
|
||
}
|
||
else if (MindControl.Controlling)
|
||
{
|
||
MindControl.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;
|
||
World world = GameManager.Instance.World;
|
||
// И у одержимого зомби (03.10: «если одержимый отошёл подальше, новые не спавнятся возле него»).
|
||
EntityAlive snake = SnakeControl.Controlling ? SnakeMode.Snake(world) : Possession.Active ? Possession.Zombie : null;
|
||
EntityPlayerLocal player = world?.GetPrimaryPlayer();
|
||
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)
|
||
{
|
||
EntityAlive snake = SnakeControl.Controlling ? SnakeMode.Snake(__instance.world as World) : Possession.Active ? Possession.Zombie : null;
|
||
if (snake == null)
|
||
{
|
||
return true;
|
||
}
|
||
// Одержимый сам не исчезает, как бы далеко от тела ни ушёл.
|
||
if (snake == __instance)
|
||
{
|
||
___ticksNoPlayerAdjacent = 0;
|
||
return !Possession.IsPossessed(__instance);
|
||
}
|
||
if ((__instance.position - snake.position).sqrMagnitude > 6400f)
|
||
{
|
||
return true;
|
||
}
|
||
___ticksNoPlayerAdjacent = 0;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Пояс в режиме змеи скрыт (указание 28.09: «прятать оверлей с поясом»), так же в
|
||
/// чертогах, у одержимого питомца (06.10) и у одержимого от первого лица (13.10: «пояс игрока нужно прятать
|
||
/// в одержимости от первого лица»; потом у зомби будут свои пояса). Игра каждый
|
||
/// кадр в 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 || MindControl.Controlling || (Possession.Active && (Possession.IsPet || Possession.FirstPerson))) && _w != null && _w.Id == SnakeControl.ToolbeltWindow);
|
||
}
|
||
}
|
||
|
||
/// <summary>Звуки у змеи слышны (тест 28.09: «звук при убийстве не срабатывает»). Audio.Manager
|
||
/// мерит расстояние до ИГРОКА (LocalPlayer().position - тело) и не создаёт звук дальше
|
||
/// maxDistance его источника (LoadAudio). Громкость потом считает сам Unity от слушателя - камеры
|
||
/// змеи. На время вызова переносим позицию игрока к змее.</summary>
|
||
public static class SnakeAudioSwap
|
||
{
|
||
/// <summary>Вложенность подмены: CheckLODs зовётся и изнутри WorldEnvironment.Update, где позиция уже
|
||
/// подменена. Подменяет и возвращает только внешний вызов, вложенные ничего не трогают.</summary>
|
||
static int _swapped, _swapFrame;
|
||
|
||
public static Vector3 Prefix()
|
||
{
|
||
if (_swapped > 0 && _swapFrame != Time.frameCount)
|
||
{
|
||
_swapped = 0; // подмена не вернулась (исключение посреди вызова) - не держим её дольше кадра
|
||
}
|
||
if (_swapped > 0)
|
||
{
|
||
_swapped++;
|
||
return Vector3.zero;
|
||
}
|
||
Vector3 body = PrefixOuter();
|
||
if (body != Vector3.zero)
|
||
{
|
||
_swapped = 1;
|
||
_swapFrame = Time.frameCount;
|
||
}
|
||
return body;
|
||
}
|
||
|
||
static Vector3 PrefixOuter()
|
||
{
|
||
if (MindControl.Controlling)
|
||
{
|
||
// Дух чертогов: звук, мир и компас - от него (MindControl.cs).
|
||
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||
if (p == null)
|
||
{
|
||
return Vector3.zero;
|
||
}
|
||
Vector3 bodyPos = p.position;
|
||
p.position = MindControl.FeetPos;
|
||
return bodyPos;
|
||
}
|
||
if (Possession.Active && Possession.Zombie != null)
|
||
{
|
||
// Одержимость (тест 03.10: «без звуков») - звуки мерятся от одержимого зомби, а не от тела.
|
||
EntityPlayerLocal pp = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||
if (pp == null)
|
||
{
|
||
return Vector3.zero;
|
||
}
|
||
Vector3 bodyAt = pp.position;
|
||
pp.position = Possession.Zombie.position;
|
||
// Область тела сжата (Possession.BodyViewDim) - этим системам радиус полный, они рисуют у зомби.
|
||
if (Possession.FullViewDim > 0 && pp.ChunkObserver != null)
|
||
{
|
||
pp.ChunkObserver.viewDim = Possession.FullViewDim;
|
||
}
|
||
return bodyAt;
|
||
}
|
||
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;
|
||
if (SnakeControl.FullViewDim > 0 && player.ChunkObserver != null)
|
||
{
|
||
player.ChunkObserver.viewDim = SnakeControl.FullViewDim;
|
||
}
|
||
return body;
|
||
}
|
||
|
||
public static void Postfix(Vector3 _body)
|
||
{
|
||
if (_body == Vector3.zero)
|
||
{
|
||
if (_swapped > 1)
|
||
{
|
||
_swapped--;
|
||
}
|
||
return;
|
||
}
|
||
_swapped = 0;
|
||
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||
if (player != null)
|
||
{
|
||
player.position = _body;
|
||
if (((Possession.Active && Possession.FullViewDim > 0) || (SnakeControl.Controlling && SnakeControl.FullViewDim > 0)) &&
|
||
player.ChunkObserver != null)
|
||
{
|
||
player.ChunkObserver.viewDim = Possession.BodyViewDim;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Полная модель блоков (двери, люки, мебель) ближе 41 м, дальше - упрощённая замена:
|
||
/// ChunkGameObject.CheckLODs меряет расстояние до ИГРОКА - тела. Его зовёт не только WorldEnvironment.Update
|
||
/// (там позиция уже подменена), но и сам чанк при появлении - тест 03.10: «змея и одержимый видят вместо
|
||
/// дверей деревянные блоки». Подменяем позицию и здесь: змея, одержимый, дух чертогов.</summary>
|
||
[HarmonyPatch(typeof(ChunkGameObject), nameof(ChunkGameObject.CheckLODs))]
|
||
public static class Patch_ChunkGameObject_CheckLODs_Snake
|
||
{
|
||
public static void Prefix(out Vector3 __state) { __state = SnakeAudioSwap.Prefix(); }
|
||
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
|
||
}
|
||
|
||
/// <summary>Детализация зданий (POI LOD) - вокруг одержимого, с полным радиусом (03.10, сжатая область тела), и вокруг змеи (05.10: «текстуры вокруг неё отображаются коряво»).</summary>
|
||
[HarmonyPatch(typeof(PrefabLODManager), nameof(PrefabLODManager.FrameUpdate))]
|
||
public static class Patch_PrefabLODManager_FrameUpdate_Possession
|
||
{
|
||
public static void Prefix(out Vector3 __state) { __state = Possession.Active || SnakeControl.Controlling ? SnakeAudioSwap.Prefix() : Vector3.zero; }
|
||
public static void Postfix(Vector3 __state) { SnakeAudioSwap.Postfix(__state); }
|
||
}
|
||
|
||
[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);
|
||
}
|
||
}
|
||
}
|