- ЛКМ от первого лица - удар рук; через RaycastTime их состояния удара (как impactStart у персонажа) зомби бьёт сразу (ForceImpact в IsAttackImpact, задержка луча снята). Замер задержки урона убран. - Положение: в покое 0,-1.55,0.05 без сжатия; на удар - плавно к -1.7 со сжатием под FOV 45. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2705 lines
122 KiB
C#
2705 lines
122 KiB
C#
using GamePath;
|
||
using System.Collections.Generic;
|
||
using HarmonyLib;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Одержимость - заклинание хвата «пиф-паф» (решения пользователя 03.10, BACKLOG «Одержимость»).
|
||
/// ЛКМ по зомби-гуманоиду (EntityZombie и наследники; не звери и не стервятники) - игрок перехватывает
|
||
/// управление им: бегает, прыгает, бьёт. Тело игрока не падает, а цепенеет и стоит (аниматор на паузе,
|
||
/// ввод снят) - его могут атаковать, это и есть цена. Любое время суток, и в кровавую луну.
|
||
/// Остальные зомби, увидев одержимого, бросаются на него, как на игрока, - можно танковать.
|
||
/// Выход - R, гибель одержимого или смерть тела. После выхода зомби пугается и убегает, как при
|
||
/// «отрицании». Убийства одержимым игроку не засчитываются (урон от его имени), его собственная
|
||
/// смерть - тоже (NecromancyKillCreditPatch).
|
||
///
|
||
/// КАК ВЕДЁМ ЗОМБИ. Его ИИ выключен (EntityAlive.updateTasks пропущен), а движение - родное:
|
||
/// SetMoveForwardWithModifiers, как делает сама игра при DebugStopEnemiesMoving и EntityMoveHelper;
|
||
/// прыжок - Jumping + SetJumpDistance, как EntityMoveHelper.StartJump; удар - Attack(false/true),
|
||
/// как EAIApproachAndAttackTarget. Походка, анимация и урон - самого зомби.
|
||
///
|
||
/// КАК СТОИТ ТЕЛО. PlayerMoveController.Update пропущен (и сам вызывает stopMoving, как у тела в
|
||
/// рэгдолле), вид - от третьего лица, аниматор модели на паузе. Камера - за головой зомби (постфикс
|
||
/// к vp_FPCamera.LateUpdate, как у змеи).
|
||
///
|
||
/// ПРИЁМЫ ЗОМБИ (указание 03.10: «у копа есть плевок и подрыв - чтобы такие спеллы становились доступны»):
|
||
/// ПКМ - дальняя атака, если у зомби есть задача ИИ EAIRangedAttackTarget (Ранчер, Чак, коп, мутант):
|
||
/// её параметры берутся из задачи захваченного зомби, а шаги повторяют её Start/Update/Reset -
|
||
/// замах (StartAnimAction startAnimType+3000), отпуск (ContinueAnimAction +1), выстрел родным
|
||
/// оружием (UseHoldingItem itemType), пока держишь кнопку и не вышло attackDuration. Направление -
|
||
/// точка под прицелом камеры (SetLookPosition), как у ИИ - голова цели.
|
||
/// Пробел - прыжок на собственную дальность зомби (jumpMaxDistance из JumpMaxDistance): у паука 7-9 м,
|
||
/// как его EAILeap, у остальных их 1-4 м.
|
||
/// Чак, ранчер, крикунья - свои приёмы на E и ПКМ (указание 13.10), см. PossessionPowers.
|
||
/// E - подрыв у EntityZombieCop (коп, мутант, подрывник): HandlePrimingDetonator, через несколько
|
||
/// секунд взрыв от имени зомби - он гибнет, игрок возвращается в тело.
|
||
///
|
||
/// ОДЕРЖИМОСТЬ ПИТОМЦА (указание 06.10): ПКМ с книгой призыва - вселение в своего питомца (все, включая
|
||
/// стервятника и Духа крысы), тем же механизмом. Отличия: питомец не пугается на выходе, а возвращается
|
||
/// к хозяину своим ИИ (охрана, поводок - PetFollowPatch пропускает одержимого); стервятник летает
|
||
/// (EntityVulture ведёт полёт сам в своём updateTasks - его мы подменяем на FlyTick). Удаляться от тела
|
||
/// можно на любое расстояние, область тела сжата так же, тело так же уязвимо.
|
||
///
|
||
/// ВРАГ У ТЕЛА (указание 06.10, как у змеи): раз в полсекунды - есть ли враг ближе
|
||
/// SnakeControl.BodyDangerRange к телу; надпись necroSnakeBodyDanger под прицелом (PossessionHud).
|
||
/// Для одержимого зомби - тоже.
|
||
///
|
||
/// ПАМЯТЬ. Ничего не сохраняется. Ссылки - только на зомби, тело и замороженные аниматоры, на время
|
||
/// одержимости; на выходе и при выходе из мира всё обнуляется.
|
||
/// </summary>
|
||
public static class Possession
|
||
{
|
||
public static bool Active;
|
||
public static EntityAlive Zombie;
|
||
/// <summary>Вселились в своего питомца (книга призыва), а не в чужого зомби.</summary>
|
||
public static bool IsPet;
|
||
/// <summary>Питомец-стервятник: летает (FlyTick из его updateTasks).</summary>
|
||
public static bool Flying;
|
||
/// <summary>Враг у тела (CheckBody) - для надписи в PossessionHud.</summary>
|
||
public static bool BodyDanger;
|
||
static float _bodyCheck;
|
||
// Ввод полёта - кадр пишет, тик стервятника (20 в секунду) читает.
|
||
static Vector2 _flyMove;
|
||
static bool _flyUp, _flyDown, _flyRun;
|
||
/// <summary>Разгон полёта - как у ванильного стервятника (EntityVulture.updateTasks: 0.55 обычный,
|
||
/// 0.95 пикирование); на бегу - пикирующий.</summary>
|
||
public const float FlyAccel = 0.55f, FlyAccelRun = 0.95f;
|
||
/// <summary>Удар стервятника - его же пауза между клевками (cAttackDelay 18 тиков).</summary>
|
||
public const float FlyAttackDelay = 0.9f;
|
||
/// <summary>Рывок стервятника на ЛКМ (указание 06.10: «как у змеи, но с 5 метров и на простой
|
||
/// удар» - иначе в полёте никого не достать). Цель - на луче прицела не дальше FlyDashRange от
|
||
/// стервятника; летит к её голове FlyDashSpeed м за тик (20 тиков в секунду), долетел ближе
|
||
/// FlyDashHit (у ванильного клевка 0.9 м) - обычный клевок. Не долетел за FlyDashTime - отбой.</summary>
|
||
public const float FlyDashRange = 10f; // было 5 м; 10 - указание 06.10 (с высоты полёта 5 м не хватало)
|
||
/// <summary>Насколько луч прицела может пройти мимо тела цели (у змеи 0.9 м от груди - с высоты мало).</summary>
|
||
public const float FlyDashAimRadius = 1.5f;
|
||
public const float FlyDashSpeed = 0.6f;
|
||
public const float FlyDashHit = 0.9f;
|
||
public const float FlyDashTime = 1f; // 10 м при 12 м/с - 0.85 с
|
||
/// <summary>Урон, если оружие стервятника не прочиталось (у грифа-питомца - 20, items.xml).</summary>
|
||
public const float FlyDashDamage = 20f;
|
||
/// <summary>Бафф кровотечения от клевка грифа-питомца (necroMeleeHandZombieGriffin).</summary>
|
||
public const string FlyDashBleedBuff = "buffNecroRatBleed";
|
||
/// <summary>Звук попадания рывка - ванильный удар рукой зомби по телу (sounds.xml).</summary>
|
||
public const string FlyDashHitSound = "organichitorganic";
|
||
static EntityAlive _flyDash;
|
||
/// <summary>Ноша стервятника (указание 08.10): рывок на курицу или кролика (EntityAnimalRabbit - только
|
||
/// они; злая курица - другой класс) не клюёт, а хватает. Несёт под лапами, его ИИ стоит; повторная ЛКМ -
|
||
/// отпускает (падает сам; у земли - стоит на ней). Курицу так приносят в загон, где игрок её ловит
|
||
/// для курятника (wildChicken).</summary>
|
||
static EntityAlive _carry;
|
||
/// <summary>«Зоркий глаз» (указание 08.10): пока держишь ПКМ, камера уходит к голове, а угол обзора сужается в ZoomFactor раз
|
||
/// за ZoomTime, как у прицела; мышь во столько же раз медленнее. Свой угол игра ставит только на
|
||
/// своём приближении (vp_FPCamera), поэтому исходный запоминаем и возвращаем сами.</summary>
|
||
public const float ZoomFactor = 8f; // x8 - указание 08.10 (было x4)
|
||
public const float ZoomTime = 0.2f;
|
||
/// <summary>Камера в приближении - на столько впереди головы, м (вне собственного клюва).</summary>
|
||
public const float ZoomEyeForward = 0.3f;
|
||
static bool _flyZoomHeld;
|
||
static float _zoom;
|
||
static float _zoomBaseFov = -1f;
|
||
static Camera _zoomCam;
|
||
/// <summary>Угол обзора «зоркого глаза», -1 - нет. Ставит Patch_EntityPlayerLocal_LateUpdate_Possession:
|
||
/// игра каждый кадр задаёт свой угол в EntityPlayerLocal.LateUpdate (тест 08.10: «x8 не работает» -
|
||
/// угол из PlaceCamera затирался).</summary>
|
||
public static float ZoomFov = -1f;
|
||
/// <summary>Зазор между когтями стервятника и спиной ноши, м; минус - когти впиваются в спину
|
||
/// (тест 08.10: «курица висит под стервятником, а не в его когтях»).</summary>
|
||
public const float CarryGap = -0.06f;
|
||
/// <summary>Цель рывка под прицелом сейчас (каждый кадр) - обычный прицел игры краснеет (AimTint).</summary>
|
||
public static EntityAlive FlyAim;
|
||
static float _flyDashUntil;
|
||
/// <summary>Сел на землю без ввода - поза спящего стервятника (указание 06.10), как у ванильного
|
||
/// sleeperVultureAnimal (поза 0, sleeperSit). Только анимация: IsSleeping не трогаем - его читают
|
||
/// ИИ и звуки. Взлёт - поза «проснулся» (-1).</summary>
|
||
public const int VultureSitPose = 0;
|
||
public const float FlySitGround = 0.3f;
|
||
/// <summary>Остановился - скорость меньше этой, м за тик (0.02 = 0.4 м/с).</summary>
|
||
public const float FlySitSpeed = 0.02f;
|
||
/// <summary>Толчок вверх при взлёте с земли на пробел, м за тик.</summary>
|
||
public const float FlyTakeOff = 0.3f;
|
||
static bool _flySitting;
|
||
/// <summary>Питание падалью (указание 06.10; у медведя и собаки - 09.10): сидя или стоя у трупа, E - EatTime секунд клюёт его и лечится на
|
||
/// EatHealFrac здоровья. У трупа запас мяса - его полное здоровье при жизни; поедание отнимает
|
||
/// столько же, сколько лечит (и на полном здоровье тоже). Запас кончился - труп исчезает (указание
|
||
/// 06.10), вместе с содержимым, если это красный труп-добыча. Сдвинулся, взлетел, ударил - еда
|
||
/// прервана, вылеченное и съеденное остаются; к трупу можно вернуться.</summary>
|
||
public const float EatRange = 2.5f;
|
||
public const float EatTime = 3f;
|
||
public const float EatHealFrac = 0.125f; // было 0.25 - указание 09.10: «лечится слишком быстро, вдвое»
|
||
static EntityAlive _eatCorpse;
|
||
static float _eatLeft, _eatHeal, _eatBite;
|
||
static int _eatCount;
|
||
static readonly Dictionary<int, float> _carrion = new Dictionary<int, float>();
|
||
static readonly Dictionary<Vector3i, float> _carrionBlocks = new Dictionary<Vector3i, float>();
|
||
static readonly Dictionary<int, bool> _isCarrion = new Dictionary<int, bool>();
|
||
static bool _eating;
|
||
static Vector3i _eatBlock;
|
||
static int _eatBlockType;
|
||
/// <summary>Признак блока-падали: среди его добычи есть гнилая плоть.</summary>
|
||
public const string CarrionItem = "foodRottingFlesh";
|
||
|
||
/// <summary>Камера - позади и выше головы зомби.</summary>
|
||
public const float CamBack = 2.6f;
|
||
/// <summary>Вид от первого лица (указание 13.10): клавиша ~/ё - как переключение вида у персонажа в игре
|
||
/// (QuickMenu, по умолчанию Backquote; и CameraChange геймпада), каждое вселение начинается от третьего лица. Камера - в голове, тело - только тень
|
||
/// (HideBody), положение глаз сглажено (FpSmooth) - голова зомби на ходу мотается.</summary>
|
||
public static bool FirstPerson;
|
||
/// <summary>Пояс одержимого (13.10): 2 ячейки, пока пустые; выбор 1/2 или колесом, только от первого лица.</summary>
|
||
public const int BeltSlots = 2;
|
||
public static int BeltSlot;
|
||
public const float FpForward = 0.12f, FpSmooth = 12f;
|
||
static Vector3 _fpEye;
|
||
static readonly List<Renderer> _hiddenRenderers = new List<Renderer>();
|
||
static readonly List<UnityEngine.Rendering.ShadowCastingMode> _hiddenModes = new List<UnityEngine.Rendering.ShadowCastingMode>();
|
||
static EntityAlive _hiddenBody;
|
||
public const float CamUp = 0.6f;
|
||
/// <summary>Как далеко зомби замечают одержимого: зрение зомби - 30 м (entityclasses.xml SightRange).</summary>
|
||
public const float TauntRange = 30f;
|
||
public const float TauntPeriod = 1f;
|
||
public const int TauntTicks = 1200;
|
||
/// <summary>Спящие просыпаются от одержимого (тест 05.10: «не разбудил спящего»). Игра будит их
|
||
/// только игроком - увидел или услышал (EAISetNearestEntityAsTarget.FindTargetPlayer), одержимый для
|
||
/// неё просто зомби. Будим сами: видит одержимого ближе SleeperWakeRange (пригнувшегося - ближе
|
||
/// SleeperWakeCrouched) или тот вплотную, ближе SleeperHearRange, хоть и за спиной.</summary>
|
||
public const float SleeperWakeRange = 8f;
|
||
public const float SleeperWakeCrouched = 3f;
|
||
public const float SleeperHearRange = 2f;
|
||
/// <summary>Приседание (указание 05.10): пока зажата кнопка приседания. Высота - доля роста зомби;
|
||
/// ниже 0.7 игра включает ходьбу пригнувшись (walkType 8), как у её зомби под низким потолком.</summary>
|
||
public static bool Crouching;
|
||
/// <summary>Удар по прицелу (тест 05.10: «не попадаю по другим зомби»). Удар зомби - луч ~1.65 м из
|
||
/// головы по взгляду (ItemActionMelee, EntityHuman.GetLookVector), а взгляд брался из наклона камеры,
|
||
/// которая сзади и выше: под прицелом враг, а луч из головы уходит ниже и мимо. На время замаха
|
||
/// взгляд зомби - на точку под прицелом, а если рядом враг почти по прицелу - на его грудь.</summary>
|
||
public const float MeleeAssistRange = 2.5f;
|
||
public const float MeleeAssistDeg = 25f;
|
||
static float _meleeLookUntil;
|
||
/// <summary>Ctrl в игре - приседание переключателем (ToggleCrouch), C - пока держишь (Crouch).</summary>
|
||
static bool _crouchToggled;
|
||
public const float CrouchHeight = 0.6f;
|
||
/// <summary>Потолок роста пригнувшегося (тест 13.10: «зомби в приседе не проходят в 1 блок»): 0.6 роста - это
|
||
/// ~1.08 м, больше блока. Игра под низким потолком сжимает до высоты проёма минус 0.21 - под блок ~0.8 м.</summary>
|
||
public const float CrouchMaxHeight = 0.7f;
|
||
/// <summary>Куда идёт одержимый (по земле), иначе ноль - для выправления движения анимации
|
||
/// (Patch_EntityAlive_MoveEntityHeaded_Possession).</summary>
|
||
public static Vector3 MoveDir;
|
||
/// <summary>Лестница: 1 - вверх (шаг вперёд у лестницы), -1 - вниз (приседание), 0 - нет. Сам подъём - в
|
||
/// Patch_EntityAlive_MoveEntityHeaded_Possession: признак _climb у игры вверх не поднимает (поднимает её навигатор).</summary>
|
||
public static int Ladder;
|
||
public const float LadderSpeed = 0.1f; // за тик (20 в секунду) - 2 м/с
|
||
/// <summary>Сколько после последней ступени ещё помогать перешагнуть через край (Ladder 2).</summary>
|
||
public const float LadderTopTime = 0.6f;
|
||
static float _ladderTop;
|
||
/// <summary>Сколько чанков держать вокруг тела, пока игрок в зомби (указание 03.10: «сильно ограничить
|
||
/// видимость у тела - ему вообще нет смысла что-либо видеть»). Тест 03.10: две полные области обзора
|
||
/// (тело + одержимый) - память росла с каждым шагом зомби, а выход давал подвисание ~50 с. Трёх чанков
|
||
/// хватает, чтобы тело стояло на земле и зомби могли к нему подойти.</summary>
|
||
public const int BodyViewDim = 3;
|
||
static int _bodyViewDim = -1;
|
||
/// <summary>Полный радиус области игрока (до сжатия) - для систем, рисующих мир у одержимого
|
||
/// (SnakeAudioSwap: дальний ландшафт, Dynamic Mesh, детализация зданий). -1 - не сжата.</summary>
|
||
public static int FullViewDim => _bodyViewDim;
|
||
public const float ScreamRange = 50f;
|
||
public const float ScreamCooldown = 10f;
|
||
/// <summary>Клич грифа - крик паникующей курицы (указание 06.10; сначала был chickenattack), ванильный
|
||
/// sounds.xml, 6 вариантов. Перезарядка - полсекунды, а не 10 с крикуньи (тест 06.10: «у звука КД»).</summary>
|
||
public const string FlyScreamSound = "chickenstressmax";
|
||
public const float FlyScreamCooldown = 0.5f;
|
||
static float _nextScream;
|
||
|
||
static EntityPlayerLocal _player;
|
||
/// <summary>Игрок, который сейчас в одержимом (тело).</summary>
|
||
public static EntityPlayerLocal Player => _player;
|
||
static float _yaw, _pitch;
|
||
/// <summary>Куда повёрнуто тело зомби. Шага вбок и назад у зомби нет (тест 03.10: «не работают A, D и S») -
|
||
/// при движении тело разворачивается в сторону шага и идёт вперёд, без движения - смотрит туда же, куда камера.</summary>
|
||
static float _bodyYaw;
|
||
public const float TurnSpeed = 540f;
|
||
static float _nextAttack;
|
||
static float _nextTaunt;
|
||
static float _sleeperCheck;
|
||
static ChunkManager.ChunkObserver _observer;
|
||
static readonly List<Animator> _frozen = new List<Animator>();
|
||
static readonly List<float> _frozenSpeed = new List<float>();
|
||
static bool _loggedCamera;
|
||
// Дальняя атака захваченного зомби (копия параметров его EAIRangedAttackTarget).
|
||
static bool _hasRanged;
|
||
static int _rItem, _rStartAnim;
|
||
static float _rRelease, _rDuration, _rCooldown;
|
||
static string _rSoundStart, _rSoundRelease;
|
||
// Held - валун Чака поднят и ждёт ПКМ.
|
||
enum RState { Idle, StartAnim, Held, Release, Fire }
|
||
static RState _rState;
|
||
static float _rTime, _rNext;
|
||
/// <summary>Сколько ждать позы готовности после замаха; не дождались - валун всё равно считается поднятым,
|
||
/// а в лог - что показывал аниматор (тест 12.10: замах не начинался, кнопки больше не отвечали).</summary>
|
||
public const float WindupTimeout = 2.5f;
|
||
/// <summary>Куда летит валун Чака - выбрано в момент броска (Patch_ItemActionVomit_GetActionEffectsValues_Possession).</summary>
|
||
public static Vector3 ThrowAim;
|
||
|
||
static GameObject _hud;
|
||
/// <summary>Метка одержимого на карте (указание 06.10, как у змеи) - NecroMapMarker.</summary>
|
||
static NecroMapMarker.Marker _mapNav;
|
||
/// <summary>Разведка стервятника (указание 08.10: «карта следует за ним как за игроком»; только он, не
|
||
/// другие одержимые): туман карты открывается вокруг него так же, как вокруг игрока (EntityPlayer.OnUpdateLive: при смене чанка
|
||
/// ChunkObserver.mapDatabase.Add - 4 чанка во все стороны), а карта открывается на нём
|
||
/// (Patch_XUiC_MapArea_*_Possession). Это обычная карта игрока - сохраняется как у него.</summary>
|
||
static Vector3i _mapChunk;
|
||
static bool _mapChunkSet;
|
||
|
||
public static void Init()
|
||
{
|
||
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
|
||
if (_hud == null)
|
||
{
|
||
_hud = new GameObject("NecroPossessionHud");
|
||
Object.DontDestroyOnLoad(_hud);
|
||
_hud.AddComponent<PossessionHud>();
|
||
}
|
||
}
|
||
|
||
public static bool IsPossessed(Entity _e) => Active && _e != null && Zombie != null && _e.entityId == Zombie.entityId;
|
||
|
||
/// <summary>Несёт ли стервятник это существо (его ИИ стоит).</summary>
|
||
public static bool IsCarried(Entity _e) => _carry != null && _e != null && _e.entityId == _carry.entityId;
|
||
|
||
static bool Carryable(EntityAlive _e) => _e is EntityAnimalRabbit;
|
||
|
||
/// <summary>Только зомби-гуманоиды (решение 03.10: «одержимая собака или стервятник - такое себе»).</summary>
|
||
public static bool CanPossess(EntityAlive _e) => _e is EntityZombie && !_e.IsDead() && HandHolds.IsZombie(_e);
|
||
|
||
/// <summary>True - вселились (для звука «пиф-пафа»).</summary>
|
||
public static bool Enter(EntityPlayerLocal _player0, EntityAlive _zombie)
|
||
{
|
||
return CanPossess(_zombie) && Begin(_player0, _zombie, false);
|
||
}
|
||
|
||
/// <summary>Вселение в своего питомца (ПКМ с книгой призыва, SummonPatch).</summary>
|
||
public static bool EnterPet(EntityPlayerLocal _player0, EntityAlive _pet)
|
||
{
|
||
return _pet != null && !_pet.IsDead() && Begin(_player0, _pet, true);
|
||
}
|
||
|
||
static bool Begin(EntityPlayerLocal _player0, EntityAlive _zombie, bool _pet)
|
||
{
|
||
if (Active || SnakeMode.Active || MindPalace.Active || _player0 == null || _player0.IsDead() ||
|
||
_player0.AttachedToEntity != null || _player0.world.IsRemote())
|
||
{
|
||
return false;
|
||
}
|
||
// Кровавый прилив (указание 10.10): дух не выходит из тела - ни в зомби, ни в своего питомца.
|
||
if (SnakeMode.IsBloodTide())
|
||
{
|
||
SnakeMode.BloodTideHint(_player0);
|
||
Debug.Log("[NecromancerTome] Possession: кровавый прилив - дух не покидает тело");
|
||
return false;
|
||
}
|
||
Active = true;
|
||
IsPet = _pet;
|
||
Flying = _zombie is EntityVulture;
|
||
BodyDanger = false;
|
||
_bodyCheck = 0f;
|
||
_flyMove = Vector2.zero;
|
||
_flyUp = _flyDown = _flyRun = false;
|
||
_flyDash = null;
|
||
_flySitting = false;
|
||
_player = _player0;
|
||
Zombie = _zombie;
|
||
_yaw = _player0.rotation.y;
|
||
_bodyYaw = _zombie.rotation.y;
|
||
_pitch = 0f;
|
||
_nextAttack = 0f;
|
||
_nextTaunt = 0f;
|
||
_nextScream = 0f;
|
||
_loggedCamera = false;
|
||
FirstPerson = false;
|
||
BeltSlot = 0;
|
||
// Пояс одержимому питомцу ни к чему (указание 06.10); открыться заново не даёт
|
||
// Patch_GUIWindowManager_openInternal_SnakeToolbelt, после выхода игра откроет его сама.
|
||
if (_pet)
|
||
{
|
||
_player0.PlayerUI?.windowManager.Close(SnakeControl.ToolbeltWindow);
|
||
}
|
||
|
||
// Зомби - в руки игрока: цели, путь и шаги ИИ сброшены.
|
||
_zombie.SetAttackTarget(null, 0);
|
||
_zombie.SetRevengeTarget(null);
|
||
PathFinderThread.Instance.RemovePathsFor(_zombie.entityId);
|
||
_zombie.navigator?.clearPath();
|
||
_zombie.moveHelper?.Stop();
|
||
_zombie.ConditionalTriggerSleeperWakeUp();
|
||
_zombie.SetRotation(new Vector3(0f, _yaw, 0f));
|
||
|
||
// Тело цепенеет: вид от третьего лица, аниматоры модели на паузе.
|
||
_player0.SetFirstPersonView(false, true);
|
||
FreezeBody(_player0, true);
|
||
_observer = GameManager.Instance.AddChunkObserver(_zombie.position, true, GameUtils.GetViewDistance(), -1);
|
||
ChunkManager.ChunkObserver body = _player0.ChunkObserver;
|
||
if (body != null && body.viewDim > BodyViewDim)
|
||
{
|
||
_bodyViewDim = body.viewDim;
|
||
body.viewDim = BodyViewDim;
|
||
}
|
||
PossessionPowers.Begin(_zombie, _pet);
|
||
ReadRanged(_zombie);
|
||
Hint(_player0, _zombie);
|
||
NecroMapMarker.Hide(ref _mapNav); // старая ссылка после закрытия мира - снимется, только если ещё наша
|
||
_mapNav = NecroMapMarker.Show(_zombie, NecroMapMarker.PossessedClass);
|
||
_mapChunkSet = false;
|
||
if (Flying)
|
||
{
|
||
_zombie.motion = Vector3.zero;
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: вселение в " + (_pet ? "питомца " : "") + _zombie.EntityName + " (" + _zombie.entityId + ")" + (Flying ? ", полёт" : ""));
|
||
NecroChallenges.Award(_pet ? NecroChallenges.PossessPet : NecroChallenges.PossessZombie);
|
||
return true;
|
||
}
|
||
|
||
public static void Exit(string _why)
|
||
{
|
||
if (!Active)
|
||
{
|
||
return;
|
||
}
|
||
Active = false;
|
||
ForceImpact = false;
|
||
FirstPerson = false;
|
||
HideBody(null);
|
||
FpArms.Hide(_player);
|
||
ShowPlayerBelt(true);
|
||
RatLoot.CloseIfViewing();
|
||
WolfBag.CloseWindow();
|
||
RatPhase.Stop(GameManager.Instance?.World, Zombie, "выход из одержимости");
|
||
DogSense.Clear();
|
||
WolfControl.Clear();
|
||
NecroMapMarker.Hide(ref _mapNav);
|
||
bool pet = IsPet;
|
||
IsPet = false;
|
||
Flying = false;
|
||
BodyDanger = false;
|
||
Crouching = false;
|
||
MoveDir = Vector3.zero;
|
||
Ladder = 0;
|
||
_meleeLookUntil = 0f;
|
||
_crouchToggled = false;
|
||
EntityAlive z = Zombie;
|
||
z?.SetLookPosition(Vector3.zero);
|
||
_flyDash = null;
|
||
FlyAim = null;
|
||
AimTint(false);
|
||
DropCarry("выход из стервятника");
|
||
ZoomReset();
|
||
StopEat(z, null);
|
||
VultureNeck.Reset();
|
||
if (_flySitting)
|
||
{
|
||
_flySitting = false;
|
||
SetSitPose(z, false);
|
||
}
|
||
EntityPlayerLocal player = _player;
|
||
if (_rState != RState.Idle && z != null && !z.IsDead())
|
||
{
|
||
StopRanged(z);
|
||
}
|
||
_rState = RState.Idle;
|
||
_hasRanged = false;
|
||
PossessionPowers.End(z);
|
||
Zombie = null;
|
||
_player = null;
|
||
Debug.Log("[NecromancerTome] Possession: выход - " + _why);
|
||
FreezeBody(player, false);
|
||
if (_bodyViewDim > 0 && player?.ChunkObserver != null)
|
||
{
|
||
player.ChunkObserver.viewDim = _bodyViewDim;
|
||
}
|
||
_bodyViewDim = -1;
|
||
if (player != null && !player.IsDead())
|
||
{
|
||
player.SetFirstPersonView(true, true);
|
||
}
|
||
if (_observer != null && GameManager.Instance?.World != null)
|
||
{
|
||
GameManager.Instance.RemoveChunkObserver(_observer);
|
||
}
|
||
_observer = null;
|
||
World world = GameManager.Instance?.World;
|
||
if (world == null || z == null)
|
||
{
|
||
return;
|
||
}
|
||
// Кто гнался за одержимым - отпускает его.
|
||
for (int i = 0; i < world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = world.EntityAlives[i];
|
||
if (e != null && e != z && e.GetAttackTarget() == z)
|
||
{
|
||
e.SetAttackTarget(null, 0);
|
||
}
|
||
}
|
||
// Питомец просто отпущен: его ИИ (следование, охрана, поводок) сам вернёт его к хозяину.
|
||
if (pet)
|
||
{
|
||
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
|
||
z.SetAttackTarget(null, 0);
|
||
return;
|
||
}
|
||
// Сам зомби в ужасе бежит от тела игрока (решение 03.10: «пугается и убегает как при отрицании»).
|
||
// В кровавый прилив леденящий ужас не действует (10.10) - выброшенный дух зомби не пугает.
|
||
if (!z.IsDead() && player != null && !SnakeMode.IsBloodTide())
|
||
{
|
||
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
|
||
NecroFleeSnakeTask.ScareFrom(z, player, HandHolds.DenyMaxSeconds, HandHolds.DenySpeed, HandHolds.DenyFarDistance);
|
||
}
|
||
}
|
||
|
||
static void FreezeBody(EntityPlayerLocal _p, bool _freeze)
|
||
{
|
||
if (_freeze)
|
||
{
|
||
_frozen.Clear();
|
||
_frozenSpeed.Clear();
|
||
Transform model = _p?.emodel?.GetModelTransform();
|
||
if (model == null)
|
||
{
|
||
return;
|
||
}
|
||
foreach (Animator a in model.GetComponentsInChildren<Animator>(true))
|
||
{
|
||
_frozen.Add(a);
|
||
_frozenSpeed.Add(a.speed);
|
||
a.speed = 0f;
|
||
}
|
||
return;
|
||
}
|
||
for (int i = 0; i < _frozen.Count; i++)
|
||
{
|
||
if (_frozen[i] != null)
|
||
{
|
||
_frozen[i].speed = _frozenSpeed[i];
|
||
}
|
||
}
|
||
_frozen.Clear();
|
||
_frozenSpeed.Clear();
|
||
}
|
||
|
||
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
|
||
{
|
||
World world = GameManager.Instance?.World;
|
||
// Замер утечки сущностей (09.10) - не связан с одержимостью, просто ежекадровый крючок мода.
|
||
if (world == null)
|
||
{
|
||
EntityLeakProbe.Reset();
|
||
}
|
||
else
|
||
{
|
||
EntityLeakProbe.Tick();
|
||
}
|
||
if (world == null)
|
||
{
|
||
if (Active || Zombie != null || _frozen.Count > 0)
|
||
{
|
||
Active = false;
|
||
IsPet = false;
|
||
Flying = false;
|
||
BodyDanger = false;
|
||
_mapNav = null;
|
||
Zombie = null;
|
||
_player = null;
|
||
_observer = null;
|
||
_bodyViewDim = -1;
|
||
_frozen.Clear();
|
||
_frozenSpeed.Clear();
|
||
_flyDash = null;
|
||
FlyAim = null;
|
||
_carry = null;
|
||
CarryVisual.Clear();
|
||
RatPhase.On = false;
|
||
DogSense.Clear();
|
||
WolfControl.Clear();
|
||
WolfBag.Forget();
|
||
_zoomCam = null;
|
||
_zoomBaseFov = -1f;
|
||
_zoom = 0f;
|
||
ZoomFov = -1f;
|
||
_eating = false;
|
||
_eatCorpse = null;
|
||
}
|
||
PossessionPowers.Clear();
|
||
PossessionHud.Forget();
|
||
// Цвет прицела - общая таблица игры, переживает выход из мира: вернуть всегда.
|
||
AimTint(false);
|
||
return;
|
||
}
|
||
PossessionPowers.SpawnTick(world); // орда крикуньи досыпается и после выхода из неё
|
||
if (!Active)
|
||
{
|
||
return;
|
||
}
|
||
EntityAlive z = Zombie;
|
||
if (_player == null || _player.IsDead())
|
||
{
|
||
Exit("тело погибло");
|
||
return;
|
||
}
|
||
if (z == null || z.IsDead() || z.IsMarkedForUnload())
|
||
{
|
||
Exit("одержимый погиб");
|
||
return;
|
||
}
|
||
if (SnakeMode.IsBloodTide())
|
||
{
|
||
SnakeMode.BloodTideHint(_player);
|
||
Exit("кровавый прилив - дух выброшен в тело");
|
||
return;
|
||
}
|
||
if (Flying)
|
||
{
|
||
RevealMap(world, z); // разведка - только стервятник (указание 08.10)
|
||
}
|
||
// Подсказка тела (тест 08.10: «стоял перед дверью - подсказка осталась в стервятнике»): её гасит
|
||
// PlayerMoveController.Update, а он на время одержимости выключен. Прочие сообщения (голод и т.п.) -
|
||
// другие окна, их не трогаем.
|
||
LocalPlayerUI ui = _player.PlayerUI;
|
||
if (ui != null && ui.windowManager.IsWindowOpen(XUiC_InteractionPrompt.ID))
|
||
{
|
||
XUiC_InteractionPrompt.SetText(ui, null);
|
||
}
|
||
PlayerActionsLocal input = _player.playerInput;
|
||
bool gui = _player.PlayerUI != null && _player.PlayerUI.windowManager.IsModalWindowOpen();
|
||
if (input != null && !gui && (input.Reload.WasPressed || input.PermanentActions.Reload.WasPressed))
|
||
{
|
||
Exit("R");
|
||
return;
|
||
}
|
||
Vector2 move = Vector2.zero;
|
||
bool run = false, jump = false, attack = false, ranged = false, rangedHeld = false, detonate = false;
|
||
if (input != null && !gui)
|
||
{
|
||
Look();
|
||
move = new Vector2(input.Move.X, input.Move.Y);
|
||
run = input.Run.IsPressed;
|
||
jump = input.Jump.WasPressed;
|
||
attack = input.Primary.WasPressed;
|
||
ranged = input.Secondary.WasPressed;
|
||
rangedHeld = input.Secondary.IsPressed;
|
||
// E на клавиатуре - в PlayerActionsPermanent.Activate; у PlayerActionsLocal.Activate только
|
||
// кнопка геймпада (тест 03.10: «подрыва нету»).
|
||
detonate = input.Activate.WasPressed || input.PermanentActions.Activate.WasPressed;
|
||
}
|
||
// Только гуманоиды обычной комплекции (указание 13.10, FpArms.Allows) - у остальных ~/ё ничего не делает.
|
||
if (input != null && !gui && FpArms.Allows(z) &&
|
||
(input.CameraChange.WasPressed || input.QuickMenu.WasPressed || input.PermanentActions.QuickMenu.WasPressed))
|
||
{
|
||
FirstPerson = !FirstPerson;
|
||
_fpEye = Vector3.zero;
|
||
if (FirstPerson)
|
||
{
|
||
FpArms.Show(_player, z);
|
||
}
|
||
else
|
||
{
|
||
HideBody(null);
|
||
FpArms.Hide(_player);
|
||
ShowPlayerBelt(true);
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: вид от " + (FirstPerson ? "первого" : "третьего") + " лица");
|
||
}
|
||
if (FirstPerson && input != null && !gui)
|
||
{
|
||
int was = BeltSlot;
|
||
if (input.InventorySlot1.WasPressed)
|
||
{
|
||
BeltSlot = 0;
|
||
}
|
||
else if (input.InventorySlot2.WasPressed)
|
||
{
|
||
BeltSlot = 1;
|
||
}
|
||
else if (input.ScrollUp.WasPressed || input.ScrollDown.WasPressed)
|
||
{
|
||
BeltSlot = (BeltSlot + 1) % BeltSlots;
|
||
}
|
||
if (BeltSlot != was)
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: пояс - ячейка " + (BeltSlot + 1));
|
||
}
|
||
}
|
||
if (input != null && !gui && input.ToggleCrouch.WasPressed)
|
||
{
|
||
_crouchToggled = !_crouchToggled;
|
||
}
|
||
Crouching = input != null && !gui && (input.Crouch.IsPressed || _crouchToggled);
|
||
// E у питомца больше не отзыв (указание 08.10: «R - всегда вернуться в тело у всех одержимых,
|
||
// E - для других целей»). Отозвать питомца - E с книгой в руке, уже из тела.
|
||
// Мутант тоже EntityZombieCop, но в игре не взрывается (ExplodeHealthThreshold 0) - у него E плевок.
|
||
if (detonate && z is EntityZombieCop cop && !cop.isPrimed && PossessionPowers.Current != PossessionPowers.Kind.Mutant)
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: подрыв " + z.EntityName);
|
||
cop.HandlePrimingDetonator();
|
||
NecroChallenges.Award(NecroChallenges.LivingBomb);
|
||
}
|
||
switch (PossessionPowers.Current)
|
||
{
|
||
case PossessionPowers.Kind.Thrower:
|
||
Throw(world, z, detonate, ranged); // E - замах, ПКМ - бросок
|
||
break;
|
||
case PossessionPowers.Kind.Rancher:
|
||
if (detonate)
|
||
{
|
||
PossessionPowers.StartInvis(z);
|
||
}
|
||
if (ranged && _rState == RState.Idle && Time.time >= _rNext)
|
||
{
|
||
PossessionPowers.BreakInvis(z, "плевок");
|
||
}
|
||
Ranged(z, ranged, rangedHeld); // ПКМ - Рой фараона (Patch_ItemActionSpawnEntity_Spawn_Possession)
|
||
break;
|
||
case PossessionPowers.Kind.Screamer:
|
||
if (detonate)
|
||
{
|
||
PossessionPowers.Scream(_player, z);
|
||
}
|
||
break;
|
||
case PossessionPowers.Kind.Mutant:
|
||
Ranged(z, detonate, detonate); // E - плевок (указание 13.10), в прицел - Patch_ProjectileMoveScript_Fire_PossessedSpit
|
||
break;
|
||
default:
|
||
Ranged(z, ranged, rangedHeld);
|
||
break;
|
||
}
|
||
PossessionPowers.Tick(world, z);
|
||
|
||
if (Flying)
|
||
{
|
||
// Полёт: направление - камера, пробел - вверх, присесть - вниз; сам шаг - в FlyTick.
|
||
_flyMove = move;
|
||
_flyRun = run;
|
||
_flyUp = input != null && !gui && input.Jump.IsPressed;
|
||
// Только C (пока держишь): Ctrl-переключатель у стервятника не используется (указание 09.10:
|
||
// «снижается, пока не прожмёшь Ctrl снова», а индикатора, как у приседания, нет).
|
||
_flyDown = input != null && !gui && input.Crouch.IsPressed;
|
||
_crouchToggled = false;
|
||
Crouching = false;
|
||
if (attack && _carry != null)
|
||
{
|
||
// Повторная ЛКМ с ношей - отпустить (не клевок и не новый рывок).
|
||
DropCarry("ЛКМ");
|
||
attack = false;
|
||
_nextAttack = Time.time + FlyAttackDelay;
|
||
}
|
||
HoldCarry(z);
|
||
FlyAim = _flyDash == null && _carry == null ? FlyDashTarget(world, z) : null;
|
||
AimTint(FlyAim != null);
|
||
if (attack && Time.time >= _nextAttack && FlyAim is EntityAlive dashTo)
|
||
{
|
||
// Рывок к цели под прицелом; клевок - по прилёте, в FlyTick.
|
||
_flyDash = dashTo;
|
||
_flyDashUntil = Time.time + FlyDashTime;
|
||
_nextAttack = Time.time + FlyDashTime + FlyAttackDelay;
|
||
Debug.Log("[NecromancerTome] Possession: рывок стервятника к " + dashTo.EntityName + " (" + dashTo.entityId + "), " +
|
||
(dashTo.getHeadPosition() - z.position).magnitude.ToString("F1") + " м");
|
||
}
|
||
else if (attack && Time.time >= _nextAttack && _flyDash == null)
|
||
{
|
||
if (_aimMissNear != null)
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: рывка нет - ближайший " + _aimMissNear.EntityName + " в " +
|
||
_aimMissDist.ToString("F1") + " м (нужно до " + FlyDashRange + "), мимо прицела на " +
|
||
_aimMissOff.ToString("F1") + " м (нужно до " + FlyDashAimRadius + ")");
|
||
}
|
||
// Клевок стервятника - его оружие (UseHoldingItem 0), как EntityVulture.AttackAndAdjust;
|
||
// куда клюёт - GetLookVector стервятника берёт lookAtPosition.
|
||
z.SetLookPosition(MeleeAim(world, z));
|
||
if (z.UseHoldingItem(0, false))
|
||
{
|
||
z.UseHoldingItem(0, true);
|
||
}
|
||
_nextAttack = Time.time + FlyAttackDelay;
|
||
_meleeLookUntil = Time.time + FlyAttackDelay;
|
||
}
|
||
// E - клич-приманка (указание 06.10; кнопка - 08.10 F, была ПКМ; 09.10 - E: «на E действие, у стервятника крик»):
|
||
// крик крикуньи, только голосом паникующей курицы - зомби в ScreamRange идут на него; с воздуха уводит
|
||
// толпу от базы или от тела. detonate - это и есть нажатие E.
|
||
bool scream = detonate;
|
||
if (scream)
|
||
{
|
||
Scream(world, z);
|
||
}
|
||
// ПКМ (держать) - «зоркий глаз» (указание 08.10: «приближение как у прицела»), в PlaceCamera.
|
||
_flyZoomHeld = rangedHeld;
|
||
// F - питаться падалью (указание 06.10; кнопка 08.10 - E, 09.10 - F у всех одержимых питомцев).
|
||
// F у игрока - фонарик, но его читает только выключенный PlayerMoveController.Update.
|
||
bool eat = FKey(input, gui);
|
||
// Есть можно сидя или просто остановившись рядом (зависнув над падалью): без ввода и почти без скорости.
|
||
bool still = _flySitting || _flyMove.sqrMagnitude < 0.01f && !_flyUp &&
|
||
new Vector2(z.motion.x, z.motion.z).sqrMagnitude < FlySitSpeed * FlySitSpeed;
|
||
Eat(world, z, eat, attack || scream || _flyMove.sqrMagnitude > 0.01f || _flyUp || !_flySitting, still);
|
||
if (_meleeLookUntil > 0f && Time.time >= _meleeLookUntil)
|
||
{
|
||
_meleeLookUntil = 0f;
|
||
z.SetLookPosition(Vector3.zero);
|
||
}
|
||
_observer?.SetPosition(z.position);
|
||
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
|
||
Taunt(world, z);
|
||
CheckBody(world, z, Time.deltaTime);
|
||
return;
|
||
}
|
||
|
||
bool moving = move.sqrMagnitude > 0.01f;
|
||
float wantYaw = moving ? _yaw + Mathf.Atan2(move.x, move.y) * Mathf.Rad2Deg : _yaw;
|
||
_bodyYaw = Mathf.MoveTowardsAngle(_bodyYaw, wantYaw, TurnSpeed * Time.deltaTime);
|
||
z.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||
z.rotation.x = -_pitch; // наклон головы - для луча удара
|
||
MoveDir = moving ? Quaternion.Euler(0f, _bodyYaw, 0f) * Vector3.forward : Vector3.zero;
|
||
Ladder = z.bCanClimbLadders && z.IsInElevator() && Crouching ? -1 : 0;
|
||
if (moving)
|
||
{
|
||
// Бег - ночная скорость ярости (moveSpeedAggroMax) в любое время: днём GetMoveSpeedAggro отдаёт
|
||
// дневную, а у многих зомби она равна шагу (13.10: «не все зомби бегают по Shift»).
|
||
// Лестница (тест 13.10: «одержимые не могут лазить по лестнице»): у игры лезет навигатор - в клетке с
|
||
// лестницей (IsInElevator) и цель выше - _climb; у нас - у лестницы и шаг вперёд.
|
||
bool climb = z.bCanClimbLadders && z.IsInElevator() && move.y > 0.1f;
|
||
if (climb && !Crouching)
|
||
{
|
||
Ladder = 1;
|
||
_ladderTop = Time.time + LadderTopTime;
|
||
}
|
||
else if (Ladder == 0 && move.y > 0.1f && !z.IsInElevator() && Time.time < _ladderTop)
|
||
{
|
||
// Верх лестницы (тест 13.10: «лезет, но не может с неё сойти»): ступени под ногами кончились, а до
|
||
// пола ещё не хватает - ещё немного вверх и вперёд, как перешагнуть через край.
|
||
Ladder = 2;
|
||
}
|
||
z.SetMoveForwardWithModifiers(run ? Mathf.Max(z.GetMoveSpeedAggro(),
|
||
EffectManager.GetValue(PassiveEffects.RunSpeed, null, z.moveSpeedAggroMax, z)) : z.GetMoveSpeed(), 1f, 0f, climb);
|
||
}
|
||
else
|
||
{
|
||
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
|
||
}
|
||
// E у прыгунов (паук, jumpMaxDistance от 5 м) - тот же дальний прыжок (13.10: «спайдер не прыгает по E»).
|
||
if (detonate && z.jumpMaxDistance >= 5f)
|
||
{
|
||
jump = true;
|
||
}
|
||
if (jump && z.onGround && !z.Jumping)
|
||
{
|
||
z.Jumping = true;
|
||
z.SetJumpDistance(Mathf.Max(1.5f, z.jumpMaxDistance), 1.2f);
|
||
}
|
||
if (IsPet && RatModel.Is(z))
|
||
{
|
||
// E - осмотреть контейнер без права взять (указание 08.10: крыса - разведчик).
|
||
RatLoot.Tick(world, _player, z, detonate);
|
||
// ПКМ (держать) - сквозь постройки, без полёта (BACKLOG, «Могильная крыса» п.4).
|
||
RatPhase.Tick(world, z, rangedHeld);
|
||
}
|
||
if (IsPet && WolfControl.Is(z))
|
||
{
|
||
// Tab в волке - окно его сумки, а не рюкзак персонажа (тест 10.10: «в волке Tab - вижу сумку игрока»).
|
||
GUIWindowManager wm = LocalPlayerUI.GetUIForPlayer(_player)?.windowManager;
|
||
if (wm != null && wm.IsWindowOpen("windowpaging") && !WolfBag.WindowOpen && !RatLoot.Viewing)
|
||
{
|
||
wm.CloseAllOpenModalWindows();
|
||
wm.Close("windowpaging");
|
||
WolfBag.OpenWindow(_player, z);
|
||
}
|
||
// ПКМ - бросок на цель, след крови раненых (решено 09.10).
|
||
WolfControl.Tick(world, _player, z, ranged, ref _bodyYaw);
|
||
// E: на контейнер или мешок с зомби - лутать в сумку (3 ячейки); у тела - выгрузить сумку в рюкзак;
|
||
// иначе - «багажник», окно сумки (указание 09.10).
|
||
bool looted = RatLoot.Tick(world, _player, z, detonate, true);
|
||
// E на предмет на земле - в сумку (тест 10.10: «не могу подбирать выброшенные предметы»).
|
||
if (detonate && !looted && !RatLoot.Viewing && !WolfBag.TryPickup(world, _player, z) && !WolfBag.TryUnload(_player, z))
|
||
{
|
||
WolfBag.OpenWindow(_player, z);
|
||
}
|
||
}
|
||
if (ranged && IsPet && BearControl.Is(z))
|
||
{
|
||
BearControl.Roar(world, z); // ПКМ - рёв-провокация (решено 09.10)
|
||
}
|
||
if (detonate && IsPet && BearControl.Is(z))
|
||
{
|
||
BearControl.RearUp(world, z); // E - на дыбы, враги рядом падают (указание 09.10)
|
||
}
|
||
if (EatsCarrion(z))
|
||
{
|
||
// F - есть падаль, как стервятник (указание 09.10: «поедание падали на F одержимым питомцам, на E -
|
||
// действие»; и одержимым зомби - их ИИ в игре и так ест трупы), стоя на месте рядом с трупом.
|
||
Eat(world, z, FKey(input, gui), moving || jump || attack || ranged, !moving && z.onGround);
|
||
}
|
||
AimTint(GroundAimTarget(world, z) != null);
|
||
if (IsPet && DogSense.Is(z))
|
||
{
|
||
// Нюх (указание 09.10): все существа рядом с собакой - на компасе и карте.
|
||
DogSense.Tick(world, z, Time.deltaTime);
|
||
}
|
||
if (attack && Time.time >= _nextAttack && FpArms.Active)
|
||
{
|
||
// От первого лица - как у персонажа (указание 13.10): ЛКМ - удар рук, урон зомби - когда их анимация дошла
|
||
// до точки удара (FpArms -> FpImpact), а не по его собственному замаху.
|
||
PossessionPowers.BreakInvis(z, "удар");
|
||
FpArms.Punch();
|
||
_nextAttack = Time.time + Mathf.Max(FpArms.SwingTime, z.GetAttackTimeoutTicks() / 20f);
|
||
}
|
||
else if (attack && Time.time >= _nextAttack)
|
||
{
|
||
PossessionPowers.BreakInvis(z, "удар");
|
||
z.SetLookPosition(MeleeAim(world, z));
|
||
if (z.Attack(false))
|
||
{
|
||
z.Attack(true);
|
||
if (IsPet && BearControl.Is(z))
|
||
{
|
||
BearControl.Smash(world, _player, z); // удар лапой и по блоку (решено 09.10)
|
||
}
|
||
float timeout = Mathf.Max(0.3f, z.GetAttackTimeoutTicks() / 20f);
|
||
_nextAttack = Time.time + timeout;
|
||
_meleeLookUntil = Time.time + Mathf.Max(0.6f, timeout);
|
||
}
|
||
}
|
||
if (_meleeLookUntil > 0f && _rState == RState.Idle)
|
||
{
|
||
if (Time.time < _meleeLookUntil)
|
||
{
|
||
z.SetLookPosition(MeleeAim(world, z));
|
||
}
|
||
else
|
||
{
|
||
_meleeLookUntil = 0f;
|
||
z.SetLookPosition(Vector3.zero);
|
||
}
|
||
}
|
||
_observer?.SetPosition(z.position);
|
||
// Спящие в зданиях просыпаются от одержимого, как от змеи (указание 05.10).
|
||
if (!PossessionPowers.Invisible)
|
||
{
|
||
// Невидимого ранчера никто не замечает и спящие от него не просыпаются.
|
||
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
|
||
Taunt(world, z);
|
||
}
|
||
CheckBody(world, z, Time.deltaTime);
|
||
}
|
||
|
||
/// <summary>Тик полёта одержимого стервятника (из его updateTasks, 20 в секунду) - формула его же
|
||
/// полёта: motion = motion * 0.9 + направление * (разгон * 0.1), разгон умножен на скорость класса.
|
||
/// Без ввода гасим скорость - зависает: у EntityFlying нет гравитации.</summary>
|
||
public static void FlyTick(EntityVulture _v)
|
||
{
|
||
if (_flyDash != null)
|
||
{
|
||
FlyDashTick(_v);
|
||
return;
|
||
}
|
||
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
|
||
Vector3 dir = rot * Vector3.forward * _flyMove.y + Quaternion.Euler(0f, _yaw, 0f) * Vector3.right * _flyMove.x;
|
||
if (_flyUp)
|
||
{
|
||
dir += Vector3.up;
|
||
}
|
||
if (_flyDown)
|
||
{
|
||
dir += Vector3.down;
|
||
}
|
||
bool moving = dir.sqrMagnitude > 0.01f;
|
||
float accel = _flyRun ? FlyAccelRun * _v.moveSpeedAggro : FlyAccel * _v.moveSpeed;
|
||
if (moving)
|
||
{
|
||
_v.motion = _v.motion * 0.9f + dir.normalized * (accel * 0.1f);
|
||
}
|
||
else
|
||
{
|
||
_v.motion *= 0.8f;
|
||
}
|
||
Vector3 flat = new Vector3(_v.motion.x, 0f, _v.motion.z);
|
||
float wantYaw = flat.sqrMagnitude > 0.0004f ? Mathf.Atan2(flat.x, flat.z) * Mathf.Rad2Deg : _yaw;
|
||
_bodyYaw = Mathf.MoveTowardsAngle(_bodyYaw, wantYaw, 20f);
|
||
_v.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||
if (_v.emodel != null && _v.emodel.avatarController != null)
|
||
{
|
||
_v.glidingCurrentPercent = Mathf.MoveTowards(_v.glidingCurrentPercent, _flyRun && moving ? 1f : 0f, 0.06f);
|
||
_v.emodel.avatarController.UpdateFloat("Gliding", _v.glidingCurrentPercent);
|
||
}
|
||
// «Вниз» на земле посадке не мешает: Ctrl - переключатель, и с ним стервятник не сел бы никогда.
|
||
// Сесть = на земле И остановился (указание 06.10: «опуститься на землю - это ещё и остановиться»):
|
||
// без ввода скорость гаснет за несколько тиков, поза - когда она почти ноль.
|
||
// За едой сидит всегда - даже там, где опору не видно (тест 06.10: мешок с трупом на прицепе -
|
||
// модель машины, луч вниз её не находит, и «стервятник не сидит»).
|
||
bool sit = _eating || _flyMove.sqrMagnitude < 0.01f && !_flyUp && _meleeLookUntil <= 0f &&
|
||
(_flySitting || flat.sqrMagnitude < FlySitSpeed * FlySitSpeed) && OnGround(_v);
|
||
if (sit)
|
||
{
|
||
_v.motion = Vector3.zero;
|
||
}
|
||
if (sit != _flySitting)
|
||
{
|
||
if (!sit && _flyUp)
|
||
{
|
||
// Взлёт с земли на пробел (указание 06.10) - толчок вверх, а не медленный отрыв.
|
||
_v.motion.y = Mathf.Max(_v.motion.y, FlyTakeOff);
|
||
}
|
||
_flySitting = sit;
|
||
SetSitPose(_v, sit);
|
||
}
|
||
}
|
||
|
||
/// <summary>Едят падаль на F, кроме стервятника (указание 09.10): чужие одержимые зомби и питомцы медведь,
|
||
/// собака, волк. Крыса - дух, не ест.</summary>
|
||
static bool EatsCarrion(EntityAlive _z) => !IsPet || BearControl.Is(_z) || DogSense.Is(_z) || WolfControl.Is(_z);
|
||
|
||
/// <summary>F на клавиатуре или геймпаде (у игрока - фонарик).</summary>
|
||
static bool FKey(PlayerActionsLocal _input, bool _gui) =>
|
||
_input != null && !_gui && (_input.PermanentActions.ToggleFlashlight.WasPressed || _input.ToggleFlashlight.WasPressed);
|
||
|
||
/// <summary>_moved - сдвинулся или что-то сделал (еда прерывается), _still - стоит/сидит, можно начать.</summary>
|
||
static void Eat(World _world, EntityAlive _v, bool _pressed, bool _moved, bool _still)
|
||
{
|
||
if (_eating)
|
||
{
|
||
bool moved = _moved;
|
||
bool gone = _eatCorpse != null ? _eatCorpse.IsMarkedForUnload() : _world.GetBlock(_eatBlock).type != _eatBlockType;
|
||
if (moved || gone)
|
||
{
|
||
StopEat(_v, gone ? "падаль пропала" : "прервано");
|
||
return;
|
||
}
|
||
// Как едят зомби в игре (EAIApproachAndAttackTarget, isEating): раз в 1.25-1.75 с брызги крови
|
||
// blood_eat у головы, через раз - звук eat_player.
|
||
_eatBite -= Time.deltaTime;
|
||
if (_eatBite <= 0f)
|
||
{
|
||
_eatBite = 1.25f + Random.value * 0.5f;
|
||
if ((_eatCount & 1) == 0)
|
||
{
|
||
_v.PlayOneShot("eat_player");
|
||
}
|
||
ParticleEffect pe = new ParticleEffect("blood_eat", new Vector3(0f, 0.04f, 0.08f), 1f, Color.white, null, _v.entityId, ParticleEffect.Attachment.Head);
|
||
GameManager.Instance.SpawnParticleEffectServer(pe, _v.entityId);
|
||
_eatCount++;
|
||
}
|
||
float dt = Mathf.Min(Time.deltaTime, _eatLeft);
|
||
_eatLeft -= dt;
|
||
float bite = _v.GetMaxHealth() * EatHealFrac * dt / EatTime;
|
||
_eatHeal += bite;
|
||
float meat = EatMeat(-bite);
|
||
int add = Mathf.Min((int)_eatHeal, _v.GetMaxHealth() - _v.Health);
|
||
if (add > 0)
|
||
{
|
||
_v.AddHealth(add);
|
||
}
|
||
_eatHeal -= (int)_eatHeal;
|
||
// Огрызок меньше одного подхода доедается сразу (тест 13.10: у трупа осталось 5 из 111 при подходе ~17 -
|
||
// с виду целый труп «не исчезает»).
|
||
if (meat <= 0f || _eatLeft <= 0f && meat < _v.GetMaxHealth() * EatHealFrac)
|
||
{
|
||
string what = EatName();
|
||
EntityAlive corpse = _eatCorpse;
|
||
Vector3i pos = _eatBlock;
|
||
StopEat(_v, "доедено");
|
||
if (corpse != null)
|
||
{
|
||
_carrion.Remove(corpse.entityId);
|
||
_world.RemoveEntity(corpse.entityId, EnumRemoveEntityReason.Despawned);
|
||
}
|
||
else
|
||
{
|
||
_carrionBlocks.Remove(pos);
|
||
_world.SetBlocksRPC(new List<BlockChangeInfo> { new BlockChangeInfo(pos, BlockValue.Air, MarchingCubes.DensityAir) });
|
||
LogCarrionLeft(_world, pos);
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: падаль " + what + " исчезла");
|
||
}
|
||
else if (_eatLeft <= 0f)
|
||
{
|
||
StopEat(_v, "кусок съеден, у падали осталось " + Mathf.CeilToInt(meat));
|
||
}
|
||
return;
|
||
}
|
||
if (!_pressed)
|
||
{
|
||
return;
|
||
}
|
||
if (!_still || !FindCarrion(_world, _v))
|
||
{
|
||
GameManager.ShowTooltip(_player, Localization.Get(Flying ? "necroPossessNoCarrion" : "necroPossessNoCarrionGround"));
|
||
Debug.Log("[NecromancerTome] Possession: падали нет" + (_still ? "" : " - " + _v.EntityName + " не остановился"));
|
||
return;
|
||
}
|
||
_eating = true;
|
||
if (Flying && !_flySitting)
|
||
{
|
||
// Сел за еду сразу, не дожидаясь тика полёта: иначе проверка «не сидит» тут же прервала бы еду.
|
||
_flySitting = true;
|
||
_v.motion = Vector3.zero;
|
||
SetSitPose(_v, true);
|
||
}
|
||
_eatLeft = EatTime;
|
||
_eatHeal = 0f;
|
||
_eatBite = 0f;
|
||
_eatCount = 0;
|
||
_v.SetLookPosition(_eatCorpse != null ? _eatCorpse.getChestPosition() : _eatBlock.ToVector3() + new Vector3(0.5f, 0.3f, 0.5f));
|
||
_v.IsEating = true; // как у зомби: анимация поедания и синхронизация по сети
|
||
Debug.Log("[NecromancerTome] Possession: " + _v.EntityName + " ест " + EatName() + ", здоровье " + _v.Health + "/" + _v.GetMaxHealth() +
|
||
", мяса у падали " + Mathf.CeilToInt(EatMeat(0f)));
|
||
}
|
||
|
||
/// <summary>Диагностика (тест 13.10: «останки визуально не исчезли» после «доедено»): что стоит на месте съеденного
|
||
/// блока сразу после замены и какие блоки падали остались в 2 блоках вокруг - одна куча может быть из нескольких.</summary>
|
||
static void LogCarrionLeft(World _world, Vector3i _pos)
|
||
{
|
||
string near = "";
|
||
for (int x = -2; x <= 2; x++)
|
||
for (int y = -2; y <= 2; y++)
|
||
for (int z = -2; z <= 2; z++)
|
||
{
|
||
Vector3i p = new Vector3i(_pos.x + x, _pos.y + y, _pos.z + z);
|
||
BlockValue bv = _world.GetBlock(p);
|
||
if (!bv.isair && IsCarrionBlock(bv.type))
|
||
{
|
||
near += " " + bv.Block.GetBlockName() + (bv.ischild ? "(часть)" : "") + "@" + p;
|
||
}
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: после еды на " + _pos + " - " + _world.GetBlock(_pos).Block.GetBlockName() +
|
||
", падаль рядом:" + (near.Length > 0 ? near : " нет"));
|
||
}
|
||
|
||
/// <summary>Запас мяса текущей падали плюс _delta; первый укус - запас с нуля: у трупа - полное
|
||
/// здоровье при жизни, у блока - его прочность (MaxDamage минус уже нанесённый урон).</summary>
|
||
static float EatMeat(float _delta)
|
||
{
|
||
float meat;
|
||
if (_eatCorpse != null)
|
||
{
|
||
if (!_carrion.TryGetValue(_eatCorpse.entityId, out meat))
|
||
{
|
||
meat = Mathf.Max(1, _eatCorpse.GetMaxHealth());
|
||
}
|
||
meat += _delta;
|
||
_carrion[_eatCorpse.entityId] = meat;
|
||
return meat;
|
||
}
|
||
if (!_carrionBlocks.TryGetValue(_eatBlock, out meat))
|
||
{
|
||
BlockValue bv = GameManager.Instance.World.GetBlock(_eatBlock);
|
||
meat = Mathf.Max(1, bv.Block.MaxDamage - bv.damage);
|
||
}
|
||
meat += _delta;
|
||
_carrionBlocks[_eatBlock] = meat;
|
||
return meat;
|
||
}
|
||
|
||
static string EatName()
|
||
{
|
||
return _eatCorpse != null
|
||
? _eatCorpse.EntityName + " (" + _eatCorpse.entityId + ")"
|
||
: Block.list[_eatBlockType].GetBlockName() + " " + _eatBlock;
|
||
}
|
||
|
||
static void StopEat(EntityAlive _v, string _why)
|
||
{
|
||
if (!_eating)
|
||
{
|
||
return;
|
||
}
|
||
_eating = false;
|
||
_eatCorpse = null;
|
||
if (_v != null && !_v.IsDead())
|
||
{
|
||
_v.IsEating = false;
|
||
_v.SetLookPosition(Vector3.zero);
|
||
}
|
||
if (_why != null)
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: еда - " + _why + ", здоровье " + (_v != null ? _v.Health + "/" + _v.GetMaxHealth() : "?"));
|
||
}
|
||
}
|
||
|
||
/// <summary>Ближайшая падаль ближе EatRange. Сначала трупы - любое мёртвое существо, кроме игроков и
|
||
/// существ мода (питомцы, рой, змея); нет трупа - блок-падаль (указание 06.10: «в игре кроме трупов
|
||
/// зомби много другой падали»): тела людей, останки животных, мешки с трупами - всё, с чего при
|
||
/// разделке падает гнилая плоть (IsCarrionBlock). У составного блока берём его главный блок.</summary>
|
||
static bool FindCarrion(World _world, EntityAlive _v)
|
||
{
|
||
_eatCorpse = null;
|
||
float bestD = EatRange * EatRange;
|
||
List<Entity> list = _world.Entities.list;
|
||
for (int i = 0; i < list.Count; i++)
|
||
{
|
||
if (!(list[i] is EntityAlive e) || e == _v || !e.IsDead() || e is EntityPlayer || SnakeControl.IsNecroEntity(e) ||
|
||
e.IsMarkedForUnload())
|
||
{
|
||
continue;
|
||
}
|
||
float d = (e.position - _v.position).sqrMagnitude;
|
||
if (d < bestD)
|
||
{
|
||
bestD = d;
|
||
_eatCorpse = e;
|
||
}
|
||
}
|
||
if (_eatCorpse != null)
|
||
{
|
||
return true;
|
||
}
|
||
bool found = false;
|
||
Vector3i c = World.worldToBlockPos(_v.position);
|
||
int r = Mathf.CeilToInt(EatRange);
|
||
for (int x = -r; x <= r; x++)
|
||
{
|
||
for (int y = -r; y <= r; y++)
|
||
{
|
||
for (int z = -r; z <= r; z++)
|
||
{
|
||
Vector3i p = new Vector3i(c.x + x, c.y + y, c.z + z);
|
||
BlockValue bv = _world.GetBlock(p);
|
||
if (bv.isair || !IsCarrionBlock(bv.type))
|
||
{
|
||
continue;
|
||
}
|
||
float d = (p.ToVector3() + new Vector3(0.5f, 0.5f, 0.5f) - _v.position).sqrMagnitude;
|
||
if (d >= bestD)
|
||
{
|
||
continue;
|
||
}
|
||
if (bv.ischild)
|
||
{
|
||
p += bv.parent;
|
||
bv = _world.GetBlock(p);
|
||
if (bv.isair || !IsCarrionBlock(bv.type))
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
bestD = d;
|
||
_eatBlock = p;
|
||
_eatBlockType = bv.type;
|
||
found = true;
|
||
}
|
||
}
|
||
}
|
||
return found;
|
||
}
|
||
|
||
/// <summary>Блок - падаль, если хоть одна его добыча - гнилая плоть (foodRottingFlesh).</summary>
|
||
static bool IsCarrionBlock(int _type)
|
||
{
|
||
if (_isCarrion.TryGetValue(_type, out bool yes))
|
||
{
|
||
return yes;
|
||
}
|
||
Block b = Block.list[_type];
|
||
if (b?.itemsToDrop != null)
|
||
{
|
||
foreach (KeyValuePair<EnumDropEvent, List<Block.SItemDropProb>> drops in b.itemsToDrop)
|
||
{
|
||
for (int i = 0; drops.Value != null && i < drops.Value.Count && !yes; i++)
|
||
{
|
||
yes = drops.Value[i].name == CarrionItem;
|
||
}
|
||
}
|
||
}
|
||
_isCarrion[_type] = yes;
|
||
return yes;
|
||
}
|
||
|
||
/// <summary>Прицел краснеет на цели рывка (указание 06.10: прицел обычный, не змеиный). Игра каждый кадр
|
||
/// красит прицел цветом crosshairColors[OptionsCrosshairColor] (EntityPlayerLocal.guiDrawCrosshair) -
|
||
/// пока цель под прицелом, этот цвет в таблице красный, без цели и на выходе - прежний. (Первая версия -
|
||
/// подмена на время рисования патчем guiDrawCrosshair - в игре не сработала, тест 06.10.)</summary>
|
||
static void AimTint(bool _on)
|
||
{
|
||
List<Color> colors = EntityPlayerLocal.crosshairColors;
|
||
if (_on == (_tintIndex >= 0) || colors == null || colors.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
if (_on)
|
||
{
|
||
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
|
||
_tintIndex = i >= 0 && i < colors.Count ? i : 0;
|
||
_tintSaved = colors[_tintIndex];
|
||
colors[_tintIndex] = AimOnTarget;
|
||
}
|
||
else
|
||
{
|
||
colors[_tintIndex] = _tintSaved;
|
||
_tintIndex = -1;
|
||
}
|
||
}
|
||
|
||
static readonly Color AimOnTarget = new Color(1f, 0.15f, 0.1f, 1f);
|
||
static int _tintIndex = -1;
|
||
static Color _tintSaved;
|
||
|
||
/// <summary>Стоит ли стервятник на земле: флаг движка или опора ближе FlySitGround под ногами
|
||
/// (у EntityFlying нет гравитации - сам он на землю не «падает» и onGround бывает не всегда).</summary>
|
||
static bool OnGround(EntityAlive _v)
|
||
{
|
||
if (_v.onGround)
|
||
{
|
||
return true;
|
||
}
|
||
Vector3 from = _v.position - Origin.position + Vector3.up * 0.1f;
|
||
return Physics.Raycast(from, Vector3.down, FlySitGround + 0.1f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
|
||
}
|
||
|
||
static void SetSitPose(EntityAlive _v, bool _sit)
|
||
{
|
||
if (_v?.emodel?.avatarController == null || _v.IsDead())
|
||
{
|
||
return;
|
||
}
|
||
_v.emodel.avatarController.TriggerSleeperPose(_sit ? VultureSitPose : AvatarController.cSleeperPoseAwake);
|
||
Debug.Log("[NecromancerTome] Possession: стервятник " + (_sit ? "сел" : "взлетел"));
|
||
}
|
||
|
||
/// <summary>Тик рывка: прямо к голове цели; долетел - клевок, как ванильный AttackAndAdjust.</summary>
|
||
static void FlyDashTick(EntityVulture _v)
|
||
{
|
||
EntityAlive t = _flyDash;
|
||
if (_flySitting)
|
||
{
|
||
_flySitting = false;
|
||
SetSitPose(_v, false);
|
||
}
|
||
if (t == null || t.IsDead() || t.IsMarkedForUnload() || Time.time > _flyDashUntil)
|
||
{
|
||
_flyDash = null;
|
||
_v.motion *= 0.5f;
|
||
Debug.Log("[NecromancerTome] Possession: рывок стервятника сорван");
|
||
return;
|
||
}
|
||
Vector3 head = t.getHeadPosition();
|
||
Vector3 to = head - _v.position;
|
||
Vector3 flat = new Vector3(to.x, 0f, to.z);
|
||
if (flat.sqrMagnitude > 0.0001f)
|
||
{
|
||
_bodyYaw = Mathf.Atan2(flat.x, flat.z) * Mathf.Rad2Deg;
|
||
_v.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||
}
|
||
float dist = to.magnitude;
|
||
if (dist > FlyDashHit)
|
||
{
|
||
_v.motion = to / dist * Mathf.Min(FlyDashSpeed, dist - FlyDashHit * 0.5f);
|
||
return;
|
||
}
|
||
// Удар - прямым уроном его оружия (тест 06.10: «рывок вообще никак»). Клевок игры бьёт лучом из
|
||
// головы через ~0.5 с после нажатия, а стервятник к тому времени пролетал мимо - ни одного
|
||
// попадания. Анимация и звук клевка - его же; урон - от стервятника, убийство засчитает
|
||
// PetKillCreditPatch (питомец) хозяину.
|
||
_flyDash = null;
|
||
_v.motion = Vector3.zero;
|
||
_v.SetLookPosition(head);
|
||
_v.emodel?.avatarController?.StartAnimationAttack();
|
||
if (Carryable(t))
|
||
{
|
||
// Курица или кролик - не клевок, а захват: ИИ встаёт (updateTasks пропущен), путь сброшен.
|
||
_carry = t;
|
||
t.SetAttackTarget(null, 0);
|
||
t.SetRevengeTarget(null);
|
||
PathFinderThread.Instance.RemovePathsFor(t.entityId);
|
||
t.navigator?.clearPath();
|
||
t.moveHelper?.Stop();
|
||
t.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||
t.transform.rotation = Quaternion.Euler(0f, _bodyYaw, 0f);
|
||
CarryVisual.Begin(t);
|
||
HoldCarry(_v);
|
||
Debug.Log("[NecromancerTome] Possession: стервятник схватил " + t.EntityName + " (" + t.entityId + ")");
|
||
return;
|
||
}
|
||
string snd = _v.GetSoundAttack();
|
||
if (!string.IsNullOrEmpty(snd))
|
||
{
|
||
Audio.Manager.BroadcastPlay(_v, snd);
|
||
}
|
||
float dmg = FlyDashDamage;
|
||
if (_v.inventory?.holdingItem?.Actions != null && _v.inventory.holdingItem.Actions.Length > 0 &&
|
||
_v.inventory.holdingItem.Actions[0] is ItemActionAttack melee)
|
||
{
|
||
dmg = melee.GetDamageEntity(_v.inventory.holdingItemItemValue, _v, 0);
|
||
}
|
||
int dealt = t.DamageEntity(new DamageSourceEntity(EnumDamageSource.External, EnumDamageTypes.Piercing, _v.entityId),
|
||
Mathf.Max(1, Mathf.RoundToInt(dmg)), false);
|
||
// Звук попадания (тест 06.10: «нет звука в момент удара - дезориентирует»): прямой урон звуков
|
||
// удара не даёт, их играет только клевок игры - удар органики об органику, как у руки зомби.
|
||
Audio.Manager.BroadcastPlay(t, FlyDashHitSound);
|
||
// Кровотечение - как у клевка грифа (items.xml, onSelfAttackedOther; указание 06.10).
|
||
if (!t.IsDead() && t.Buffs != null)
|
||
{
|
||
t.Buffs.AddBuff(FlyDashBleedBuff, _v.entityId);
|
||
}
|
||
_meleeLookUntil = Time.time + FlyAttackDelay;
|
||
Debug.Log("[NecromancerTome] Possession: клевок после рывка по " + t.EntityName + " - урон " + dealt + " из " + dmg.ToString("F0") +
|
||
(t.IsDead() ? ", убит" : ", здоровье " + t.Health));
|
||
}
|
||
|
||
/// <summary>Ноша - под лапами стервятника, каждый кадр: верх слепка ноши на CarryGap ниже его ног
|
||
/// (CarryVisual). Ниже уже земля (схватил у земли, сел) - ноша стоит на земле под ним, но не отпущена.</summary>
|
||
static void HoldCarry(EntityAlive _v)
|
||
{
|
||
EntityAlive c = _carry;
|
||
if (c == null)
|
||
{
|
||
return;
|
||
}
|
||
if (c.IsDead() || c.IsMarkedForUnload())
|
||
{
|
||
_carry = null;
|
||
CarryVisual.End(c);
|
||
Debug.Log("[NecromancerTome] Possession: ноша погибла");
|
||
return;
|
||
}
|
||
CarryVisual.Place(_v, c, CarryGap);
|
||
c.SetRotation(new Vector3(0f, _bodyYaw, 0f));
|
||
c.motion = Vector3.zero;
|
||
c.fallDistance = 0f;
|
||
}
|
||
|
||
/// <summary>Отпустить ношу: ИИ снова идёт, падает она сама (с высоты - может разбиться).</summary>
|
||
static void DropCarry(string _why)
|
||
{
|
||
EntityAlive c = _carry;
|
||
if (c == null)
|
||
{
|
||
return;
|
||
}
|
||
_carry = null;
|
||
CarryVisual.End(c);
|
||
if (!c.IsDead())
|
||
{
|
||
c.motion = Vector3.zero;
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: стервятник отпустил " + c.EntityName + " - " + _why);
|
||
}
|
||
|
||
/// <summary>Цель рывка: ближайшая к стервятнику по лучу прицела, не дальше FlyDashRange от него и
|
||
/// не дальше TargetRadius змеи от луча. Свои (питомцы, игроки, зачарованные) - не цель.</summary>
|
||
static EntityAlive FlyDashTarget(World _world, EntityAlive _v)
|
||
{
|
||
Transform cam = _player?.cameraTransform;
|
||
if (cam == null)
|
||
{
|
||
return null;
|
||
}
|
||
Vector3 from = cam.position + Origin.position;
|
||
Vector3 dir = cam.forward;
|
||
EntityAlive best = null;
|
||
float bestAlong = float.MaxValue;
|
||
_aimMissNear = null;
|
||
_aimMissDist = _aimMissOff = float.MaxValue;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (e == null || e == _v || e.IsDead() || e is EntityPlayer || SnakeControl.IsNecroEntity(e) ||
|
||
NecroCharmSide.IsCharmed(e) || GhostTraderPatch.ShouldBeGhost(e))
|
||
{
|
||
continue;
|
||
}
|
||
// Тело цели - отрезок от ног до головы (тест 06.10: «прицел не краснеет, рывок не срабатывает» -
|
||
// по одной точке груди с высоты почти не попасть). Берём точку отрезка ближе всех к лучу прицела;
|
||
// дальность - от стервятника до ближайшей точки тела.
|
||
Vector3 feet = e.position, head = e.getHeadPosition();
|
||
float dist = float.MaxValue, off = float.MaxValue, along = 0f;
|
||
for (int k = 0; k <= 4; k++)
|
||
{
|
||
Vector3 p = Vector3.Lerp(feet, head, k / 4f);
|
||
dist = Mathf.Min(dist, (p - _v.position).magnitude);
|
||
float a = Vector3.Dot(p - from, dir);
|
||
float o = Vector3.Cross(p - from, dir).magnitude;
|
||
if (a > 0f && o < off)
|
||
{
|
||
off = o;
|
||
along = a;
|
||
}
|
||
}
|
||
if (dist < FlyDashRange + 5f && dist < _aimMissDist)
|
||
{
|
||
_aimMissNear = e;
|
||
_aimMissDist = dist;
|
||
_aimMissOff = off;
|
||
}
|
||
if (dist > FlyDashRange || off > FlyDashAimRadius || along >= bestAlong)
|
||
{
|
||
continue;
|
||
}
|
||
best = e;
|
||
bestAlong = along;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
// Для лога промаха: ближайший к стервятнику враг (до FlyDashRange + 5 м), его дальность и отступ от луча прицела.
|
||
static EntityAlive _aimMissNear;
|
||
static float _aimMissDist, _aimMissOff;
|
||
|
||
/// <summary>Враг под прицелом наземного одержимого (указание 09.10: прицел краснеет на противнике у
|
||
/// всех одержимых): тело врага - отрезок от ног до головы - не дальше GroundAimRadius от луча
|
||
/// камеры и не дальше GroundAimRange от одержимого, и не за стеной - не дальше первого попадания
|
||
/// луча (AimPoint).</summary>
|
||
static EntityAlive GroundAimTarget(World _world, EntityAlive _z)
|
||
{
|
||
Transform cam = _player?.cameraTransform;
|
||
if (cam == null)
|
||
{
|
||
return null;
|
||
}
|
||
Vector3 from = cam.position + Origin.position;
|
||
Vector3 dir = cam.forward;
|
||
float wall = Vector3.Dot(AimPoint() - from, dir) + 0.5f;
|
||
EntityAlive best = null;
|
||
float bestAlong = float.MaxValue;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (!SnakeControl.IsFoe(e, _z) || (e.position - _z.position).sqrMagnitude > GroundAimRange * GroundAimRange)
|
||
{
|
||
continue;
|
||
}
|
||
Vector3 feet = e.position, head = e.getHeadPosition();
|
||
for (int k = 0; k <= 4; k++)
|
||
{
|
||
Vector3 p = Vector3.Lerp(feet, head, k / 4f);
|
||
float a = Vector3.Dot(p - from, dir);
|
||
if (a > 0f && a < wall && a < bestAlong && Vector3.Cross(p - from, dir).magnitude < GroundAimRadius)
|
||
{
|
||
best = e;
|
||
bestAlong = a;
|
||
}
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
public const float GroundAimRange = 30f; // зрение зомби, как TauntRange
|
||
public const float GroundAimRadius = 0.6f;
|
||
|
||
/// <summary>Девианты и питомцы мода не бьют одержимого (13.10: «девианты не должны нападать на
|
||
/// одержимого»). Новую цель им гасят заплатки на выборе и присвоении (CharmPatch), но зомби, на которого
|
||
/// девиант или питомец уже шёл до вселения, оставался его целью - SetAttackTarget больше не звался.
|
||
/// Такую цель снимаем здесь, раз в полсекунды (из CheckBody).</summary>
|
||
static void DropAllyTargets(World _world, EntityAlive _z)
|
||
{
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (e == null || e == _z || e.IsDead() ||
|
||
e.GetAttackTarget() != _z && e.GetRevengeTarget() != _z ||
|
||
!NecroCharmSide.IsCharmed(e) && !SnakeControl.IsNecroEntity(e))
|
||
{
|
||
continue;
|
||
}
|
||
e.SetRevengeTarget(null);
|
||
e.SetAttackTarget(null, 0);
|
||
Debug.Log("[NecromancerTome] Possession: " + e.EntityName + " (" + e.entityId + ") больше не нападает на одержимого");
|
||
}
|
||
}
|
||
|
||
/// <summary>Раз в полсекунды: есть ли живой враг ближе BodyDangerRange к телу (как у змеи).</summary>
|
||
static void CheckBody(World _world, EntityAlive _z, float _dt)
|
||
{
|
||
_bodyCheck -= _dt;
|
||
if (_bodyCheck > 0f)
|
||
{
|
||
return;
|
||
}
|
||
_bodyCheck = 0.5f;
|
||
DropAllyTargets(_world, _z);
|
||
bool danger = false;
|
||
Vector3 body = _player.position;
|
||
float r2 = SnakeControl.BodyDangerRange * SnakeControl.BodyDangerRange;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (SnakeControl.IsFoe(e, _z) && (e.position - body).sqrMagnitude < r2)
|
||
{
|
||
danger = true;
|
||
break;
|
||
}
|
||
}
|
||
if (danger != BodyDanger)
|
||
{
|
||
BodyDanger = danger;
|
||
Debug.Log("[NecromancerTome] Possession: " + (danger ? "враг у тела" : "у тела чисто"));
|
||
}
|
||
}
|
||
|
||
static void ReadRanged(EntityAlive _z)
|
||
{
|
||
_hasRanged = false;
|
||
_rState = RState.Idle;
|
||
_rNext = 0f;
|
||
List<EAIRangedAttackTarget> tasks = _z.aiManager?.GetTasks<EAIRangedAttackTarget>();
|
||
if (tasks == null || tasks.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
EAIRangedAttackTarget t = tasks[0];
|
||
_hasRanged = true;
|
||
_rItem = t.itemActionType;
|
||
_rStartAnim = t.startAnimType;
|
||
_rRelease = t.releaseDelay;
|
||
_rDuration = t.attackDuration > 0f ? t.attackDuration : 3f;
|
||
// Вдвое быстрее ИИ (тест 03.10: «перезаряд медленный»).
|
||
_rCooldown = Mathf.Max(1f, t.baseCooldown * 0.5f);
|
||
if (PossessionPowers.Current == PossessionPowers.Kind.Rancher)
|
||
{
|
||
// Рой фараона - раз в 30 с (решение 13.10); откат не сбрасывается перевселением.
|
||
_rCooldown = PossessionPowers.SwarmCooldown;
|
||
_rNext = PossessionPowers.SwarmReadyAt;
|
||
}
|
||
_rSoundStart = t.soundStartName;
|
||
_rSoundRelease = t.soundReleaseName;
|
||
Debug.Log("[NecromancerTome] Possession: дальняя атака - оружие " + _rItem + ", замах " + _rStartAnim + ", длит. " + _rDuration + " с, откат " + _rCooldown + " с");
|
||
}
|
||
|
||
/// <summary>Подсказка при вселении: что умеет этот зомби.</summary>
|
||
static void Hint(EntityPlayerLocal _p, EntityAlive _z)
|
||
{
|
||
string text = Localization.Get("necroPossessHintBase");
|
||
if (FpArms.Allows(_z))
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintView");
|
||
}
|
||
if (Flying)
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintFly");
|
||
}
|
||
switch (PossessionPowers.Current)
|
||
{
|
||
case PossessionPowers.Kind.Thrower:
|
||
text += ", " + Localization.Get("necroPossessHintThrow");
|
||
break;
|
||
case PossessionPowers.Kind.Rancher:
|
||
text += ", " + Localization.Get("necroPossessHintSwarm") + ", " + Localization.Get("necroPossessHintInvis");
|
||
break;
|
||
case PossessionPowers.Kind.Screamer:
|
||
text += ", " + Localization.Get("necroPossessHintScream");
|
||
break;
|
||
case PossessionPowers.Kind.Mutant:
|
||
text += ", " + Localization.Get("necroPossessHintSpitE");
|
||
break;
|
||
default:
|
||
if (_hasRanged)
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintRanged");
|
||
}
|
||
break;
|
||
}
|
||
if (Flying)
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintZoom") + ", " + Localization.Get("necroPossessHintScreamE");
|
||
}
|
||
if (Flying)
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintEatF");
|
||
}
|
||
if (IsPet && RatModel.Is(_z))
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintLook") + ", " + Localization.Get("necroPossessHintPhase");
|
||
}
|
||
if (IsPet && DogSense.Is(_z))
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintSmell");
|
||
}
|
||
if (IsPet && WolfControl.Is(_z))
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintPounce") + ", " + Localization.Get("necroPossessHintBlood") + ", " +
|
||
Localization.Get("necroPossessHintWolfBag");
|
||
}
|
||
if (IsPet && BearControl.Is(_z))
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintRoar") + ", " + Localization.Get("necroPossessHintRear") + ", " +
|
||
Localization.Get("necroPossessHintSmash") + ", " +
|
||
Localization.Get("necroPossessHintSkin");
|
||
}
|
||
if (EatsCarrion(_z))
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintEatGroundF");
|
||
}
|
||
if (_z.jumpMaxDistance >= 5f)
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintLeap");
|
||
}
|
||
if (_z is EntityZombieCop && PossessionPowers.Current != PossessionPowers.Kind.Mutant)
|
||
{
|
||
text += ", " + Localization.Get("necroPossessHintDetonate");
|
||
}
|
||
if (!IsPet)
|
||
{
|
||
// Решение 03.10, подтверждено 09.10: после выхода бывший одержимый в леденящем ужасе бежит от тела.
|
||
text += ", " + Localization.Get("necroPossessHintAfterFear");
|
||
}
|
||
GameManager.ShowTooltip(_p, text);
|
||
}
|
||
|
||
/// <summary>Куда бить: грудь врага ближе MeleeAssistRange к голове и не дальше MeleeAssistDeg от
|
||
/// направления на прицел (ближайший к прицелу), иначе - точка под прицелом.</summary>
|
||
static Vector3 MeleeAim(World _world, EntityAlive _z)
|
||
{
|
||
Vector3 aim = AimPoint();
|
||
Vector3 head = _z.getHeadPosition();
|
||
Vector3 dir = (aim - head).normalized;
|
||
EntityAlive best = null;
|
||
float bestAngle = MeleeAssistDeg;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (e == null || e == _z || e.IsDead() || e is EntityPlayer || SnakeControl.IsNecroEntity(e))
|
||
{
|
||
continue;
|
||
}
|
||
Vector3 to = e.getChestPosition() - head;
|
||
if (to.sqrMagnitude > MeleeAssistRange * MeleeAssistRange)
|
||
{
|
||
continue;
|
||
}
|
||
float angle = Vector3.Angle(dir, to);
|
||
if (angle < bestAngle)
|
||
{
|
||
bestAngle = angle;
|
||
best = e;
|
||
}
|
||
}
|
||
return best != null ? best.getChestPosition() : aim;
|
||
}
|
||
|
||
/// <summary>Точка под прицелом камеры (в мировых координатах), до 50 м. Тело самого одержимого луч
|
||
/// пропускает (13.10: «у всех зомби должен краснеть прицел», «у мутанта атака не целится»): камера
|
||
/// за головой, и у крупных зомби луч упирался в собственную спину - точка прицела была на себе.</summary>
|
||
static Vector3 AimPoint()
|
||
{
|
||
Transform cam = _player?.cameraTransform;
|
||
if (cam == null)
|
||
{
|
||
return Zombie.position + Quaternion.Euler(-_pitch, _yaw, 0f) * Vector3.forward * 30f;
|
||
}
|
||
int n = Physics.RaycastNonAlloc(cam.position, cam.forward, _aimHits, 50f, -5, QueryTriggerInteraction.Ignore);
|
||
Transform self = Zombie != null ? Zombie.transform : null;
|
||
float best = float.MaxValue;
|
||
Vector3 point = Vector3.zero;
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
RaycastHit hit = _aimHits[i];
|
||
if (hit.distance <= (FirstPerson ? 0.1f : CamBack + 0.5f) || hit.distance >= best ||
|
||
self != null && (hit.transform == self || hit.transform.IsChildOf(self)))
|
||
{
|
||
continue;
|
||
}
|
||
best = hit.distance;
|
||
point = hit.point;
|
||
}
|
||
if (best < float.MaxValue)
|
||
{
|
||
return point + Origin.position;
|
||
}
|
||
return cam.position + Origin.position + cam.forward * 50f;
|
||
}
|
||
|
||
static readonly RaycastHit[] _aimHits = new RaycastHit[32];
|
||
|
||
/// <summary>Куда летит плевок мутанта (указание 13.10: «у мутанта атака не целится»): грудь врага под
|
||
/// прицелом, иначе точка под прицелом; дальше SpitRange - точка на луче к ней.</summary>
|
||
public static Vector3 SpitTarget(Vector3 _from)
|
||
{
|
||
EntityAlive foe = Zombie != null && Zombie.world != null ? GroundAimTarget(Zombie.world, Zombie) : null;
|
||
Vector3 to = foe != null ? foe.getChestPosition() : AimPoint();
|
||
Vector3 d = to - _from;
|
||
return d.magnitude > SpitRange ? _from + d.normalized * SpitRange : to;
|
||
}
|
||
|
||
public const float SpitRange = 15f;
|
||
|
||
/// <summary>ПКМ - дальняя атака зомби по шагам ванильной EAIRangedAttackTarget.</summary>
|
||
static void Ranged(EntityAlive _z, bool _pressed, bool _held)
|
||
{
|
||
if (!_hasRanged)
|
||
{
|
||
return;
|
||
}
|
||
float dt = Time.deltaTime;
|
||
switch (_rState)
|
||
{
|
||
case RState.Idle:
|
||
if (!_pressed)
|
||
{
|
||
return;
|
||
}
|
||
if (Time.time < _rNext || !_z.IsAttackValid())
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: дальняя атака не готова - откат " + Mathf.Max(0f, _rNext - Time.time).ToString("F1") + " с, атака " + (_z.IsAttackValid() ? "можно" : "нельзя"));
|
||
return;
|
||
}
|
||
RearmRanged(_z);
|
||
_z.emodel.avatarController.hitWeightMax = 0.5f;
|
||
_rTime = 0f;
|
||
if (!string.IsNullOrEmpty(_rSoundStart))
|
||
{
|
||
Audio.Manager.BroadcastPlay(_z, _rSoundStart);
|
||
}
|
||
if (_rStartAnim >= 0)
|
||
{
|
||
_z.StartAnimAction(_rStartAnim + 3000);
|
||
_rState = RState.StartAnim;
|
||
}
|
||
else
|
||
{
|
||
_rState = RState.Fire;
|
||
}
|
||
return;
|
||
case RState.StartAnim:
|
||
_z.SetLookPosition(AimPoint());
|
||
if (_z.GetAnimActionState() != AvatarController.ActionState.Ready)
|
||
{
|
||
return;
|
||
}
|
||
_z.ContinueAnimAction(_rStartAnim + 1 + 3000);
|
||
if (!string.IsNullOrEmpty(_rSoundRelease))
|
||
{
|
||
Audio.Manager.BroadcastPlay(_z, _rSoundRelease);
|
||
}
|
||
_rState = RState.Release;
|
||
_rTime = 0f;
|
||
return;
|
||
case RState.Release:
|
||
_z.SetLookPosition(AimPoint());
|
||
_rTime += dt;
|
||
if (_rTime >= _rRelease)
|
||
{
|
||
_rState = RState.Fire;
|
||
_rTime = 0f;
|
||
}
|
||
return;
|
||
case RState.Fire:
|
||
_z.SetLookPosition(AimPoint());
|
||
_rTime += dt;
|
||
_z.UseHoldingItem(_rItem, false);
|
||
// Доводим до конца по одному нажатию: у плевка сначала 1-3 предупреждения по 1.2 с
|
||
// (ItemActionVomit), и с отпущенной кнопкой он сбрасывался (тест 03.10: «если зажать
|
||
// ПКМ - плюёт»). Конец - оружие закончило или вышла длительность задачи.
|
||
// Рой ранчера: действие «появления» само не кончается, пока держат кнопку, - вылетел, и хватит.
|
||
if (_rTime > 0.2f && !_z.IsHoldingItemInUse(_rItem) || _rTime >= _rDuration ||
|
||
PossessionPowers.SwarmSpawned && _rTime > 0.5f)
|
||
{
|
||
StopRanged(_z);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
/// <summary>Валун Чака (указание 13.10): E - замах, поднять валун над собой (шаг StartAnim ванильной задачи);
|
||
/// ПКМ - бросок (ContinueAnimAction, отпуск, выстрел - общие шаги Ranged) в точку под прицелом или в существо
|
||
/// у прицела. Замах - прямо через avatarController.StartAction: EntityAlive.StartAnimAction молча ничего не
|
||
/// делает, пока аниматор считает, что идёт другое действие (IsActionActive), - так, похоже, и было 12.10.</summary>
|
||
static void Throw(World _world, EntityAlive _z, bool _windup, bool _throw)
|
||
{
|
||
if (!_hasRanged || _rStartAnim < 0)
|
||
{
|
||
Ranged(_z, _throw, _throw);
|
||
return;
|
||
}
|
||
switch (_rState)
|
||
{
|
||
case RState.Idle:
|
||
if (!_windup)
|
||
{
|
||
if (_throw)
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: броска нет - валун не поднят (сначала E)");
|
||
}
|
||
return;
|
||
}
|
||
if (Time.time < _rNext || !_z.IsAttackValid())
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: замах не готов - откат " + Mathf.Max(0f, _rNext - Time.time).ToString("F1") + " с, атака " + (_z.IsAttackValid() ? "можно" : "нельзя"));
|
||
return;
|
||
}
|
||
RearmRanged(_z);
|
||
_z.emodel.avatarController.hitWeightMax = 0.5f;
|
||
if (!string.IsNullOrEmpty(_rSoundStart))
|
||
{
|
||
Audio.Manager.BroadcastPlay(_z, _rSoundStart);
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: замах Чака, аниматор до - " + _z.GetAnimActionState());
|
||
_z.emodel.avatarController.StartAction(_rStartAnim + 3000);
|
||
_rState = RState.StartAnim;
|
||
_rTime = 0f;
|
||
return;
|
||
case RState.StartAnim:
|
||
_z.SetLookPosition(AimPoint());
|
||
_rTime += Time.deltaTime;
|
||
AvatarController.ActionState s = _z.GetAnimActionState();
|
||
if (s != AvatarController.ActionState.Ready && _rTime < WindupTimeout)
|
||
{
|
||
return;
|
||
}
|
||
Debug.Log(s == AvatarController.ActionState.Ready
|
||
? "[NecromancerTome] Possession: валун поднят за " + _rTime.ToString("F2") + " с"
|
||
: "[NecromancerTome] Possession: замах не дошёл до готовности за " + WindupTimeout + " с (аниматор - " + s + "), бросок всё равно разрешён");
|
||
_rState = RState.Held;
|
||
return;
|
||
case RState.Held:
|
||
_z.SetLookPosition(AimPoint());
|
||
if (!_throw)
|
||
{
|
||
return;
|
||
}
|
||
ThrowAim = PossessionPowers.ThrowTarget(_world, _z, AimPoint());
|
||
_z.ContinueAnimAction(_rStartAnim + 1 + 3000);
|
||
if (!string.IsNullOrEmpty(_rSoundRelease))
|
||
{
|
||
Audio.Manager.BroadcastPlay(_z, _rSoundRelease);
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: бросок валуна, до цели " + (ThrowAim - _z.position).magnitude.ToString("F1") + " м");
|
||
_rState = RState.Release;
|
||
_rTime = 0f;
|
||
return;
|
||
default:
|
||
Ranged(_z, false, false); // отпуск и выстрел - как у ИИ
|
||
return;
|
||
}
|
||
}
|
||
|
||
/// <summary>Перед новой дальней атакой - оружие в исходное (тест 03.10: «плюёт, но один раз»).
|
||
/// У ИИ это делает его собственный цикл; здесь - сброс очереди выстрелов (ResetBurstShot), отпущенный
|
||
/// курок, отменённая перезарядка и патрон в магазине (Infinite_ammo, магазин 1).</summary>
|
||
static void RearmRanged(EntityAlive _z)
|
||
{
|
||
ItemInventoryData inv = _z.inventory?.holdingItemData;
|
||
if (inv == null || _rItem >= inv.actionData.Count)
|
||
{
|
||
return;
|
||
}
|
||
ItemValue iv = _z.inventory.holdingItemItemValue;
|
||
string before = "патронов " + (iv != null ? iv.Meta.ToString() : "?");
|
||
if (inv.actionData[_rItem] is ItemActionRanged.ItemActionDataRanged d && _z.inventory.holdingItem.Actions[_rItem] is ItemActionRanged ranged)
|
||
{
|
||
before += ", очередь " + d.curBurstCount + ", перезарядка " + d.isReloading;
|
||
ranged.ResetBurstShot(d);
|
||
d.bReleased = true;
|
||
d.bPressed = false;
|
||
d.isReloading = false;
|
||
d.isReloadRequested = false;
|
||
}
|
||
// Плевок: без лишних «позывов» - сразу последнее предупреждение (ItemActionVomit: 1-3 по 1.2 с).
|
||
if (inv.actionData[_rItem] is ItemActionVomit.ItemActionDataVomit dv && _z.inventory.holdingItem.Actions[_rItem] is ItemActionVomit vomit)
|
||
{
|
||
dv.numWarningsPlayed = Mathf.Max(0, Traverse.Create(vomit).Field("warningMax").GetValue<int>() - 1);
|
||
dv.warningTime = 0f;
|
||
}
|
||
if (iv != null && iv.Meta < 1)
|
||
{
|
||
iv.Meta = 1;
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: дальняя атака - взвод (" + before + ")");
|
||
}
|
||
|
||
static void StopRanged(EntityAlive _z)
|
||
{
|
||
_z.ShowHoldingItem(false);
|
||
_z.UseHoldingItem(_rItem, true);
|
||
_z.StartAnimAction(9999);
|
||
_z.SetLookPosition(Vector3.zero);
|
||
_z.emodel.avatarController.hitWeightMax = 1f;
|
||
_rState = RState.Idle;
|
||
_rNext = Time.time + _rCooldown;
|
||
if (PossessionPowers.Current == PossessionPowers.Kind.Rancher)
|
||
{
|
||
PossessionPowers.SwarmReadyAt = _rNext;
|
||
}
|
||
PossessionPowers.SwarmSpawned = false;
|
||
}
|
||
|
||
/// <summary>Откат приёма для PossessionHud: Rmb - кнопка ПКМ (иначе E), Left/Total - секунды; Active - приём идёт
|
||
/// (невидимость), Left - сколько ему осталось.</summary>
|
||
public struct Cooldown
|
||
{
|
||
public bool Rmb;
|
||
public float Left, Total;
|
||
public bool Active;
|
||
}
|
||
|
||
/// <summary>Откаты одержимого (до двух) - сколько записано в _out.</summary>
|
||
public static int Cooldowns(Cooldown[] _out)
|
||
{
|
||
if (!Active || Zombie == null)
|
||
{
|
||
return 0;
|
||
}
|
||
float now = Time.time;
|
||
int n = 0;
|
||
switch (PossessionPowers.Current)
|
||
{
|
||
case PossessionPowers.Kind.Thrower:
|
||
_out[n++] = new Cooldown { Rmb = false, Left = Mathf.Max(0f, _rNext - now), Total = _rCooldown };
|
||
break;
|
||
case PossessionPowers.Kind.Rancher:
|
||
_out[n++] = PossessionPowers.Invisible
|
||
? new Cooldown { Rmb = false, Left = PossessionPowers.InvisLeft, Total = PossessionPowers.InvisTime, Active = true }
|
||
: new Cooldown { Rmb = false, Left = PossessionPowers.InvisCooldownLeft, Total = PossessionPowers.InvisCooldown };
|
||
_out[n++] = new Cooldown { Rmb = true, Left = Mathf.Max(0f, _rNext - now), Total = _rCooldown };
|
||
break;
|
||
case PossessionPowers.Kind.Screamer:
|
||
_out[n++] = new Cooldown { Rmb = false, Left = PossessionPowers.HordeCooldownLeft, Total = PossessionPowers.HordeCooldown };
|
||
break;
|
||
case PossessionPowers.Kind.Mutant:
|
||
_out[n++] = new Cooldown { Rmb = false, Left = Mathf.Max(0f, _rNext - now), Total = _rCooldown };
|
||
break;
|
||
default:
|
||
if (_hasRanged)
|
||
{
|
||
_out[n++] = new Cooldown { Rmb = true, Left = Mathf.Max(0f, _rNext - now), Total = _rCooldown };
|
||
}
|
||
break;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/// <summary>Клич одержимого стервятника (у крикуньи с 13.10 - орда, PossessionPowers.Scream): звук и все
|
||
/// зомби в ScreamRange - на него (приманка).</summary>
|
||
static void Scream(World _world, EntityAlive _z)
|
||
{
|
||
if (Time.time < _nextScream)
|
||
{
|
||
return;
|
||
}
|
||
_nextScream = Time.time + (Flying ? FlyScreamCooldown : ScreamCooldown);
|
||
string snd = Flying ? FlyScreamSound : _z.GetSoundAlert();
|
||
if (!string.IsNullOrEmpty(snd))
|
||
{
|
||
Audio.Manager.BroadcastPlay(_z, snd);
|
||
}
|
||
int n = 0;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (e == null || e == _z || e.IsDead() || !(e is EntityZombie) || e.aiManager == null ||
|
||
PetFollowPatch.FindOwnerOfPet(e.entityId) != null || (e.position - _z.position).sqrMagnitude > ScreamRange * ScreamRange)
|
||
{
|
||
continue;
|
||
}
|
||
e.ConditionalTriggerSleeperWakeUp();
|
||
AllowZombieTarget(e, _z);
|
||
e.SetAttackTarget(_z, TauntTicks);
|
||
n++;
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: крик " + _z.EntityName + " - на зов идут " + n);
|
||
}
|
||
|
||
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;
|
||
if (_zoomCam != null && _zoomBaseFov > 0f)
|
||
{
|
||
sens *= _zoomCam.fieldOfView / _zoomBaseFov; // в «зорком глазе» мышь медленнее, как в прицеле
|
||
}
|
||
_yaw += Input.GetAxis("Mouse X") * sens.x;
|
||
_pitch = Mathf.Clamp(_pitch + Input.GetAxis("Mouse Y") * sens.y * invert, -70f, 70f);
|
||
}
|
||
|
||
/// <summary>Раз в секунду: зомби, которые видят одержимого, бросаются на него (решение 03.10:
|
||
/// «зомби начинают считать этого зомби ещё одним игроком»). Ванильный подход к цели принимает
|
||
/// только свои классы (EntityPlayer...) - добавляем EntityZombie, как CharmPatch; это безопасно:
|
||
/// цель-зомби им ставим только мы (поиск цели EAISetNearestEntityAsTarget не трогаем).</summary>
|
||
static void Taunt(World _world, EntityAlive _z)
|
||
{
|
||
if (Time.time < _nextTaunt)
|
||
{
|
||
return;
|
||
}
|
||
_nextTaunt = Time.time + TauntPeriod;
|
||
for (int i = 0; i < _world.EntityAlives.Count; i++)
|
||
{
|
||
EntityAlive e = _world.EntityAlives[i];
|
||
if (e == null || e == _z || e.IsDead() || !(e is EntityZombie) || e.GetAttackTarget() == _z ||
|
||
e.aiManager == null || PetFollowPatch.FindOwnerOfPet(e.entityId) != null)
|
||
{
|
||
continue;
|
||
}
|
||
float d2 = (e.position - _z.position).sqrMagnitude;
|
||
if (d2 > TauntRange * TauntRange)
|
||
{
|
||
continue;
|
||
}
|
||
if (e.IsSleeping)
|
||
{
|
||
float wake = Crouching ? SleeperWakeCrouched : SleeperWakeRange;
|
||
if (d2 > SleeperHearRange * SleeperHearRange && (d2 > wake * wake || !e.CanSee(_z)))
|
||
{
|
||
continue;
|
||
}
|
||
e.ConditionalTriggerSleeperWakeUp();
|
||
Debug.Log("[NecromancerTome] Possession: проснулся " + e.EntityName + " (" + e.entityId + "), " + Mathf.Sqrt(d2).ToString("F1") + " м");
|
||
}
|
||
else if (!e.CanSee(_z))
|
||
{
|
||
continue;
|
||
}
|
||
AllowZombieTarget(e, _z);
|
||
e.SetAttackTarget(_z, TauntTicks);
|
||
}
|
||
}
|
||
|
||
/// <summary>Разрешить зомби подход к цели класса одержимого: EntityZombie, у питомцев - их класс
|
||
/// (EntityZombieDog, EntityVulture).</summary>
|
||
internal static void AllowZombieTarget(EntityAlive _e, EntityAlive _target)
|
||
{
|
||
System.Type type = _target is EntityZombie ? typeof(EntityZombie) : _target.GetType();
|
||
List<EAIApproachAndAttackTarget> tasks = _e.aiManager.GetTasks<EAIApproachAndAttackTarget>();
|
||
if (tasks == null)
|
||
{
|
||
return;
|
||
}
|
||
foreach (EAIApproachAndAttackTarget t in tasks)
|
||
{
|
||
bool has = false;
|
||
foreach (EAIApproachAndAttackTarget.TargetClass c in t.targetClasses)
|
||
{
|
||
has |= c.type == type;
|
||
}
|
||
if (!has)
|
||
{
|
||
t.targetClasses.Add(new EAIApproachAndAttackTarget.TargetClass { type = type, chaseTimeMax = 0f });
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Открыть карту вокруг одержимого - как игра открывает её вокруг игрока, при смене чанка.</summary>
|
||
static void RevealMap(World _world, EntityAlive _z)
|
||
{
|
||
IMapChunkDatabase map = _player.ChunkObserver?.mapDatabase;
|
||
if (map == null || !_z.IsSpawned() || _mapChunkSet && _z.chunkPosAddedEntityTo == _mapChunk)
|
||
{
|
||
return;
|
||
}
|
||
_mapChunk = _z.chunkPosAddedEntityTo;
|
||
_mapChunkSet = true;
|
||
map.Add(_mapChunk, _world);
|
||
}
|
||
|
||
/// <summary>Шаг «зоркого глаза» за кадр; отпустил - угол плавно возвращается и восстанавливается точно.</summary>
|
||
static void Zoom(bool _held)
|
||
{
|
||
Camera cam = _player != null ? _player.playerCamera : null;
|
||
if (_held && cam != null && _zoomBaseFov < 0f)
|
||
{
|
||
_zoomCam = cam;
|
||
_zoomBaseFov = cam.fieldOfView;
|
||
}
|
||
if (_zoomBaseFov < 0f)
|
||
{
|
||
return;
|
||
}
|
||
_zoom = Mathf.MoveTowards(_zoom, _held ? 1f : 0f, Time.deltaTime / ZoomTime);
|
||
if (_zoom <= 0f)
|
||
{
|
||
ZoomReset();
|
||
return;
|
||
}
|
||
ZoomFov = Mathf.Lerp(_zoomBaseFov, _zoomBaseFov / ZoomFactor, Mathf.SmoothStep(0f, 1f, _zoom));
|
||
}
|
||
|
||
static void ZoomReset()
|
||
{
|
||
if (_zoomCam != null && _zoomBaseFov > 0f)
|
||
{
|
||
_zoomCam.fieldOfView = _zoomBaseFov;
|
||
}
|
||
_zoomCam = null;
|
||
_zoomBaseFov = -1f;
|
||
_zoom = 0f;
|
||
ZoomFov = -1f;
|
||
_flyZoomHeld = false;
|
||
}
|
||
|
||
/// <summary>Миг удара рук от первого лица (FpArms): зомби бьёт сразу - его удар начат и сразу «дошёл до точки
|
||
/// удара» (ForceImpact в EntityAlive.IsAttackImpact, задержка луча ItemActionMelee снята). Урон, дальность,
|
||
/// цель - его, взгляд - на прицел (MeleeAim).</summary>
|
||
public static bool ForceImpact;
|
||
|
||
public static void FpImpact()
|
||
{
|
||
EntityAlive z = Zombie;
|
||
if (!Active || z == null || z.IsDead())
|
||
{
|
||
return;
|
||
}
|
||
z.SetLookPosition(MeleeAim(z.world, z));
|
||
_meleeLookUntil = Time.time + 0.6f;
|
||
if (!z.Attack(false))
|
||
{
|
||
Debug.Log("[NecromancerTome] Possession: удар рук - зомби не может бить сейчас");
|
||
return;
|
||
}
|
||
z.Attack(true);
|
||
ForceImpact = true;
|
||
if (z.inventory?.holdingItemData?.actionData != null && z.inventory.holdingItemData.actionData.Count > 0 &&
|
||
z.inventory.holdingItemData.actionData[0] is ItemActionMelee.InventoryDataMelee d)
|
||
{
|
||
d.lastUseTime -= 10f;
|
||
}
|
||
}
|
||
|
||
/// <summary>Ячейки пояса персонажа (окно windowToolbelt) - скрыты в виде от первого лица (указание 13.10: «пояс игрока
|
||
/// нужно прятать», у зомби свой пояс). Только это окно: группа toolbelt держит и полоски здоровья и сытости персонажа
|
||
/// (тест 13.10 - пропали вместе с поясом). Ставится каждый кадр - игра может показать окно снова.</summary>
|
||
static void ShowPlayerBelt(bool _show)
|
||
{
|
||
XUiV_Window w = _player?.PlayerUI?.xui?.GetWindow("windowToolbelt");
|
||
if (w != null && w.IsVisible != _show)
|
||
{
|
||
w.IsVisible = _show;
|
||
}
|
||
}
|
||
|
||
/// <summary>Тело одержимого в виде от первого лица - только тень (тест 13.10: изнутри видно тело, у паука на
|
||
/// бегу лапы и череп в кадре; сжатие одной головы не спасало). Видимость - дело своего экрана: другие игроки
|
||
/// рисуют этого зомби сами и видят его как обычно; так же игра прячет тело персонажа от первого лица.
|
||
/// Рендеры собираются один раз на вход в вид; null - вернуть каждому прежний режим тени.
|
||
///
|
||
/// ПАМЯТЬ. Ссылки на рендеры держим только пока вид от первого лица (null чистит список).</summary>
|
||
static void HideBody(EntityAlive _z)
|
||
{
|
||
if (_hiddenBody == _z)
|
||
{
|
||
return;
|
||
}
|
||
for (int i = 0; i < _hiddenRenderers.Count; i++)
|
||
{
|
||
if (_hiddenRenderers[i] != null)
|
||
{
|
||
_hiddenRenderers[i].shadowCastingMode = _hiddenModes[i];
|
||
}
|
||
}
|
||
_hiddenRenderers.Clear();
|
||
_hiddenModes.Clear();
|
||
_hiddenBody = _z;
|
||
if (_z == null || _z.emodel == null)
|
||
{
|
||
return;
|
||
}
|
||
_z.emodel.GetComponentsInChildren(true, _hiddenRenderers);
|
||
for (int i = 0; i < _hiddenRenderers.Count; i++)
|
||
{
|
||
_hiddenModes.Add(_hiddenRenderers[i].shadowCastingMode);
|
||
_hiddenRenderers[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly;
|
||
}
|
||
Debug.Log("[NecromancerTome] Possession: тело " + _z.EntityName + " - только тень, рендеров " + _hiddenRenderers.Count);
|
||
}
|
||
|
||
/// <summary>Из постфикса vp_FPCamera.LateUpdate: камера позади и выше головы зомби, не сквозь стены.</summary>
|
||
public static void PlaceCamera(Transform _camera)
|
||
{
|
||
EntityAlive z = Zombie;
|
||
if (z == null)
|
||
{
|
||
return;
|
||
}
|
||
if (Flying)
|
||
{
|
||
// Шея - до камеры: та берёт положение головы.
|
||
VultureNeck.Apply(z, _flySitting, !_eating);
|
||
}
|
||
Zoom(Flying && _flyZoomHeld);
|
||
Transform head = z.emodel != null ? z.emodel.GetHeadTransform() : null;
|
||
Vector3 anchor = head != null ? head.position : z.position - Origin.position + Vector3.up * 1.6f;
|
||
Quaternion rot = Quaternion.Euler(-_pitch, _yaw, 0f);
|
||
if (FirstPerson)
|
||
{
|
||
// Положение головы - уже после аниматора (мы в LateUpdate камеры).
|
||
Vector3 want = anchor + rot * Vector3.forward * FpForward;
|
||
_fpEye = _fpEye == Vector3.zero || (want - _fpEye).sqrMagnitude > 4f
|
||
? want
|
||
: Vector3.Lerp(_fpEye, want, 1f - Mathf.Exp(-FpSmooth * Time.deltaTime));
|
||
HideBody(z);
|
||
_camera.SetPositionAndRotation(_fpEye, rot);
|
||
FpArms.Tick(_player?.playerCamera);
|
||
ShowPlayerBelt(false);
|
||
return;
|
||
}
|
||
Vector3 eye = anchor + Vector3.up * CamUp - rot * Vector3.forward * CamBack;
|
||
Vector3 to = eye - anchor;
|
||
if (Physics.Raycast(anchor, to.normalized, out RaycastHit hit, to.magnitude + 0.2f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore))
|
||
{
|
||
eye = anchor + to.normalized * Mathf.Max(0.2f, hit.distance - 0.2f);
|
||
}
|
||
if (_zoom > 0f)
|
||
{
|
||
// В «зорком глазе» камера уходит к голове (чуть впереди неё): из-за спины при x8 весь кадр
|
||
// закрыла бы сама птица.
|
||
eye = Vector3.Lerp(eye, anchor + rot * Vector3.forward * ZoomEyeForward, Mathf.SmoothStep(0f, 1f, _zoom));
|
||
}
|
||
_camera.SetPositionAndRotation(eye, rot);
|
||
if (!_loggedCamera)
|
||
{
|
||
_loggedCamera = true;
|
||
Debug.Log("[NecromancerTome] Possession: камера за " + (head != null ? "головой " + head.name : "корнем (головы нет)"));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Тело не вертится за мышью (тест 13.10: «персонаж поворачивается синхронно с зомби»). Камера игры
|
||
/// читает мышь сама (vp_FPInput - InputSmoothLook - vp_FPCamera.UpdateInput, m_Yaw) и в LateUpdate
|
||
/// доворачивает своего родителя - тело игрока. Взгляд одержимого мы считаем сами (Possession.Look), так что
|
||
/// на время одержимости ввод камеры тела пропускаем: после выхода тело смотрит, куда смотрело до вселения.</summary>
|
||
[HarmonyPatch(typeof(vp_FPCamera), nameof(vp_FPCamera.UpdateInput))]
|
||
public static class Patch_vpFPCamera_UpdateInput_Possession
|
||
{
|
||
public static bool Prefix() => !Possession.Active;
|
||
}
|
||
|
||
/// <summary>То же для вида от третьего лица, в котором тело стоит всю одержимость (тест 13.10: правка выше не
|
||
/// помогла - «персонаж всё ещё поворачивается синхронно с зомби»). vp_FPCamera.Update3rdPerson каждый кадр
|
||
/// берёт поворот камеры (а его ставим мы - взгляд зомби) как targetRotation3P, и тело доворачивается к нему.
|
||
/// Камеру одержимости целиком ставит PlaceCamera, так что этот шаг на время одержимости пропускаем.</summary>
|
||
[HarmonyPatch(typeof(vp_FPCamera), nameof(vp_FPCamera.Update3rdPerson))]
|
||
public static class Patch_vpFPCamera_Update3rdPerson_Possession
|
||
{
|
||
public static bool Prefix() => !Possession.Active;
|
||
}
|
||
|
||
/// <summary>Одержимый зомби без своего ИИ: шаги ИИ, путь и moveHelper не идут, движение задаём мы.</summary>
|
||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.updateTasks))]
|
||
public static class Patch_EntityAlive_updateTasks_Possession
|
||
{
|
||
public static bool Prefix(EntityAlive __instance)
|
||
{
|
||
return !Possession.IsPossessed(__instance) && !Possession.IsCarried(__instance);
|
||
}
|
||
}
|
||
|
||
/// <summary>Одержимый стервятник: его собственный полёт-ИИ (EntityVulture.updateTasks - своё, без
|
||
/// вызова базового) заменён полётом по вводу игрока.</summary>
|
||
[HarmonyPatch(typeof(EntityVulture), nameof(EntityVulture.updateTasks))]
|
||
public static class Patch_EntityVulture_updateTasks_Possession
|
||
{
|
||
public static bool Prefix(EntityVulture __instance)
|
||
{
|
||
if (!Possession.IsPossessed(__instance))
|
||
{
|
||
return true;
|
||
}
|
||
Possession.FlyTick(__instance);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Карта в одержимом стервятнике открывается на нём, а не на теле (указание 08.10: разведка -
|
||
/// только у стервятника).</summary>
|
||
[HarmonyPatch(typeof(XUiC_MapArea), nameof(XUiC_MapArea.OnOpen))]
|
||
public static class Patch_XUiC_MapArea_OnOpen_Possession
|
||
{
|
||
public static void Postfix(XUiC_MapArea __instance)
|
||
{
|
||
if (Possession.Active && Possession.Flying && Possession.Zombie != null)
|
||
{
|
||
__instance.PositionMapAt(Possession.Zombie.GetPosition());
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Кнопка «к игроку» на карте при одержимости - к одержимому.</summary>
|
||
[HarmonyPatch(typeof(XUiC_MapArea), nameof(XUiC_MapArea.onPlayerIconPressed))]
|
||
public static class Patch_XUiC_MapArea_onPlayerIconPressed_Possession
|
||
{
|
||
public static bool Prefix(XUiC_MapArea __instance)
|
||
{
|
||
if (!Possession.Active || !Possession.Flying || Possession.Zombie == null)
|
||
{
|
||
return true;
|
||
}
|
||
__instance.PositionMapAt(Possession.Zombie.GetPosition());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>«Зоркий глаз» стервятника: свой угол обзора - после того, как игра поставила свой.</summary>
|
||
[HarmonyPatch(typeof(EntityPlayerLocal), "LateUpdate")]
|
||
public static class Patch_EntityPlayerLocal_LateUpdate_Possession
|
||
{
|
||
public static void Postfix(EntityPlayerLocal __instance)
|
||
{
|
||
if (Possession.ZoomFov > 0f && __instance == Possession.Player && __instance.playerCamera != null)
|
||
{
|
||
__instance.playerCamera.fieldOfView = Possession.ZoomFov;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Прицел одержимого - всегда перекрестие (указание 09.10: «то перекрестие, то точка»). Вид прицела
|
||
/// игра берёт у предмета в руке тела, а у книги призыва и пустой руки он зависит от проверки камеры от
|
||
/// третьего лица (ItemAction.GetCrosshairType - EntityPlayerLocal.CharacterCameraAngleValid): камера дальше
|
||
/// 20° от того, куда повёрнуто тело, - прицела нет, и игра рисует точку. В одержимости камера - глаза
|
||
/// одержимого, тело стоит, так что вид менялся от поворота мыши. Рисуем сами то же, что ветка Plus
|
||
/// guiDrawCrosshair; цвет - из таблицы игры, его красит Possession.AimTint.</summary>
|
||
[HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.guiDrawCrosshair))]
|
||
public static class Patch_EntityPlayerLocal_guiDrawCrosshair_Possession
|
||
{
|
||
public static bool Prefix(EntityPlayerLocal __instance, NGuiWdwInGameHUD _guiInGame, bool bModalWindowOpen)
|
||
{
|
||
if (!Possession.Active || __instance != Possession.Player)
|
||
{
|
||
return true;
|
||
}
|
||
if (!_guiInGame.showCrosshair || Event.current.type != EventType.Repaint || __instance.IsDead() ||
|
||
!GamePrefs.GetBool(EnumGamePrefs.OptionsCrosshairEnabled) || bModalWindowOpen || _guiInGame.CrosshairTexture == null)
|
||
{
|
||
return false;
|
||
}
|
||
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
|
||
if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count)
|
||
{
|
||
i = 0;
|
||
}
|
||
Color c = EntityPlayerLocal.crosshairColors[i];
|
||
Vector2 center = __instance.GetCrosshairPosition2D();
|
||
center.y = Screen.height - center.y;
|
||
float size = Screen.height * 0.059f * GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairScale);
|
||
Color was = GUI.color;
|
||
GUI.color = new Color(c.r, c.g, c.b, GamePrefs.GetFloat(EnumGamePrefs.OptionsCrosshairOpacity));
|
||
GUI.DrawTexture(new Rect(center.x - size / 2f, center.y - size / 2f, size, size), _guiInGame.CrosshairTexture, ScaleMode.StretchToFill);
|
||
GUI.color = was;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Тело оцепенело: ввод игрока не идёт (как у тела в рэгдолле - stopMoving и выход).</summary>
|
||
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
|
||
public static class Patch_PlayerMoveController_Update_Possession
|
||
{
|
||
public static bool Prefix(PlayerMoveController __instance)
|
||
{
|
||
if (!Possession.Active)
|
||
{
|
||
return true;
|
||
}
|
||
__instance.stopMoving();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Полоски одержимого (указание 13.10): здоровье - справа от полоски здоровья игрока, откаты приёмов -
|
||
/// справа от полоски выносливости (HUDLeftStatBars игры); свои полоски игрока видны. Место берётся у самих полосок игры, на экране
|
||
/// (их UiTransform через камеру NGUI): так оно совпадает при любом масштабе интерфейса. Если полоски игры
|
||
/// сейчас не видно - последнее известное место, а до него - внизу по центру, как было с 03.10.
|
||
/// OnGUI, как прицел змеи (SnakeCrosshair), - без своего окна XUi.
|
||
///
|
||
/// ПАМЯТЬ. Ссылки на две полоски игры (их контроллеры) держим только до выхода из мира (Forget из
|
||
/// Possession) или до пересборки окна (UiTransform == null). Строки подписей собираются, только когда
|
||
/// меняется число, - не каждый кадр.</summary>
|
||
public class PossessionHud : MonoBehaviour
|
||
{
|
||
const float Width = 420f, Height = 16f, Bottom = 150f;
|
||
static readonly Color Back = new Color(0f, 0f, 0f, 0.85f);
|
||
static readonly Color Fill = new Color(0.55f, 0.08f, 0.06f, 0.95f);
|
||
static readonly Color CdReady = new Color(0.75f, 0.62f, 0.95f, 0.95f);
|
||
static readonly Color CdCharging = new Color(0.35f, 0.3f, 0.45f, 0.95f);
|
||
static readonly Color CdActive = new Color(0.55f, 0.2f, 0.85f, 0.95f);
|
||
static GUIStyle _style;
|
||
|
||
static readonly Color Danger = new Color(1f, 0.15f, 0.1f, 1f);
|
||
static GUIStyle _dangerStyle;
|
||
|
||
static XUiC_HUDStatBar _healthBar, _staminaBar;
|
||
static Rect _healthRect, _staminaRect;
|
||
static bool _healthKnown, _staminaKnown;
|
||
|
||
static readonly Possession.Cooldown[] _cd = new Possession.Cooldown[2];
|
||
static readonly string[] _cdText = new string[2];
|
||
static readonly int[] _cdSec = { -1, -1 };
|
||
static readonly bool[] _cdRmb = new bool[2];
|
||
static string _hpText;
|
||
static int _hpId = -1, _hpCur = -1, _hpMax = -1;
|
||
|
||
/// <summary>Полоска игры обновилась - запомнить её (Patch_XUiC_HUDStatBar_Update_PossessionHud).</summary>
|
||
public static void Remember(XUiC_HUDStatBar _bar)
|
||
{
|
||
if (_bar.statType == HUDStatTypes.Health)
|
||
{
|
||
_healthBar = _bar;
|
||
}
|
||
else if (_bar.statType == HUDStatTypes.Stamina)
|
||
{
|
||
_staminaBar = _bar;
|
||
}
|
||
}
|
||
|
||
public static void Forget()
|
||
{
|
||
_healthBar = _staminaBar = null;
|
||
_healthKnown = _staminaKnown = false;
|
||
_hpText = null;
|
||
_hpId = -1;
|
||
for (int i = 0; i < _cdText.Length; i++)
|
||
{
|
||
_cdText[i] = null;
|
||
_cdSec[i] = -1;
|
||
}
|
||
}
|
||
|
||
/// <summary>Где на экране полоска игры (координаты GUI: y сверху).</summary>
|
||
static bool ScreenRect(ref XUiC_HUDStatBar _bar, ref Rect _rect, ref bool _known)
|
||
{
|
||
Transform t = _bar?.ViewComponent?.UiTransform;
|
||
if (t == null)
|
||
{
|
||
_bar = null; // окно пересобрано или мир закрыт
|
||
return _known;
|
||
}
|
||
if (!t.gameObject.activeInHierarchy)
|
||
{
|
||
return _known;
|
||
}
|
||
Camera cam = NGUITools.FindCameraForLayer(t.gameObject.layer);
|
||
if (cam == null)
|
||
{
|
||
return _known;
|
||
}
|
||
Vector2i size = _bar.ViewComponent.Size;
|
||
Vector3 a = cam.WorldToScreenPoint(t.position);
|
||
Vector3 b = cam.WorldToScreenPoint(t.TransformPoint(new Vector3(size.x, -size.y, 0f)));
|
||
Rect r = Rect.MinMaxRect(Mathf.Min(a.x, b.x), Screen.height - Mathf.Max(a.y, b.y), Mathf.Max(a.x, b.x), Screen.height - Mathf.Min(a.y, b.y));
|
||
if (r.width < 8f || r.height < 4f)
|
||
{
|
||
return _known;
|
||
}
|
||
_rect = r;
|
||
_known = true;
|
||
return true;
|
||
}
|
||
|
||
void OnGUI()
|
||
{
|
||
EntityAlive z = Possession.Active ? Possession.Zombie : null;
|
||
if (z == null || Event.current.type != EventType.Repaint)
|
||
{
|
||
return;
|
||
}
|
||
if (Possession.BodyDanger)
|
||
{
|
||
DrawBodyDanger();
|
||
}
|
||
float scale = Screen.height / 1080f;
|
||
if (_style == null)
|
||
{
|
||
_style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Clip };
|
||
_style.normal.textColor = new Color(0.95f, 0.9f, 0.85f);
|
||
}
|
||
if (Possession.FirstPerson)
|
||
{
|
||
DrawBelt(scale);
|
||
}
|
||
Rect hr;
|
||
// Справа от полосок игрока, а не поверх (указание 13.10: «можем вывести где-нибудь и здоровье игрока»):
|
||
// над ними значки баффов, справа до панели предметов пусто.
|
||
float shift = 0f;
|
||
if (ScreenRect(ref _healthBar, ref _healthRect, ref _healthKnown))
|
||
{
|
||
shift = _healthRect.width + 8f * scale;
|
||
hr = _healthRect;
|
||
hr.x += shift;
|
||
}
|
||
else
|
||
{
|
||
float w = Width * scale, h = Height * scale * 1.6f;
|
||
hr = new Rect((Screen.width - w) / 2f, Screen.height - Bottom * scale - h, w, h);
|
||
}
|
||
int hp = z.Health, max = z.GetMaxHealth();
|
||
if (_hpText == null || _hpId != z.entityId || _hpCur != hp || _hpMax != max)
|
||
{
|
||
_hpId = z.entityId;
|
||
_hpCur = hp;
|
||
_hpMax = max;
|
||
// Только числа: имя зомби в полоске не помещалось и переносилось (скриншоты 13.10: «название зомби
|
||
// в индикаторе здоровья - плохая идея»).
|
||
_hpText = hp + "/" + max;
|
||
}
|
||
DrawBar(hr, Mathf.Clamp01(hp / (float)Mathf.Max(1, max)), Fill, _hpText);
|
||
|
||
int n = Possession.Cooldowns(_cd);
|
||
if (n == 0)
|
||
{
|
||
return;
|
||
}
|
||
Rect sr;
|
||
if (ScreenRect(ref _staminaBar, ref _staminaRect, ref _staminaKnown))
|
||
{
|
||
sr = _staminaRect;
|
||
sr.x += shift;
|
||
}
|
||
else
|
||
{
|
||
sr = new Rect(hr.x, hr.y - hr.height - 4f * scale, hr.width, hr.height);
|
||
}
|
||
float gap = 4f * scale;
|
||
float cw = (sr.width - gap * (n - 1)) / n;
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
Possession.Cooldown c = _cd[i];
|
||
int sec = c.Left > 0f ? Mathf.CeilToInt(c.Left) : 0;
|
||
if (_cdText[i] == null || _cdSec[i] != sec || _cdRmb[i] != c.Rmb)
|
||
{
|
||
_cdSec[i] = sec;
|
||
_cdRmb[i] = c.Rmb;
|
||
string key = c.Rmb ? Localization.Get("necroPossessKeyRmb") : "E";
|
||
_cdText[i] = sec > 0 ? key + " " + sec : key;
|
||
}
|
||
float frac;
|
||
Color col;
|
||
if (c.Active)
|
||
{
|
||
frac = c.Total > 0f ? Mathf.Clamp01(c.Left / c.Total) : 0f;
|
||
col = CdActive;
|
||
}
|
||
else if (c.Left > 0f)
|
||
{
|
||
frac = c.Total > 0f ? 1f - Mathf.Clamp01(c.Left / c.Total) : 0f;
|
||
col = CdCharging;
|
||
}
|
||
else
|
||
{
|
||
frac = 1f;
|
||
col = CdReady;
|
||
}
|
||
DrawBar(new Rect(sr.x + i * (cw + gap), sr.y, cw, sr.height), frac, col, _cdText[i]);
|
||
}
|
||
}
|
||
|
||
/// <summary>Пояс одержимого от первого лица (указание 13.10: «собственный пояс на 2 ячейки», пока пустой) - на месте
|
||
/// пояса персонажа, внизу по центру; выбранная ячейка в рамке. Ячейки пустые - бьёт кулаками.</summary>
|
||
static void DrawBelt(float _scale)
|
||
{
|
||
float size = 64f * _scale, gap = 6f * _scale, frame = 3f * _scale;
|
||
float x0 = (Screen.width - (size * Possession.BeltSlots + gap * (Possession.BeltSlots - 1))) / 2f;
|
||
float y = Screen.height - 14f * _scale - size;
|
||
Color prev = GUI.color;
|
||
for (int i = 0; i < Possession.BeltSlots; i++)
|
||
{
|
||
Rect r = new Rect(x0 + i * (size + gap), y, size, size);
|
||
if (i == Possession.BeltSlot)
|
||
{
|
||
GUI.color = CdReady;
|
||
GUI.DrawTexture(new Rect(r.x - frame, r.y - frame, r.width + frame * 2f, r.height + frame * 2f), Texture2D.whiteTexture);
|
||
}
|
||
GUI.color = Back;
|
||
GUI.DrawTexture(r, Texture2D.whiteTexture);
|
||
GUI.color = prev;
|
||
GUI.Label(new Rect(r.x + 4f * _scale, r.y + 2f * _scale, r.width, 18f * _scale), (i + 1).ToString(), _style);
|
||
}
|
||
GUI.color = prev;
|
||
}
|
||
|
||
static void DrawBar(Rect _r, float _frac, Color _fill, string _text)
|
||
{
|
||
Color prev = GUI.color;
|
||
GUI.color = Back;
|
||
GUI.DrawTexture(_r, Texture2D.whiteTexture);
|
||
GUI.color = _fill;
|
||
GUI.DrawTexture(new Rect(_r.x + 2f, _r.y + 2f, (_r.width - 4f) * _frac, _r.height - 4f), Texture2D.whiteTexture);
|
||
GUI.color = prev;
|
||
_style.fontSize = Mathf.Max(8, Mathf.RoundToInt(_r.height * 0.62f));
|
||
GUI.Label(new Rect(_r.x + 6f, _r.y, _r.width - 8f, _r.height), _text, _style);
|
||
}
|
||
|
||
/// <summary>Надпись «враг у тела» - та же, что у змеи (SnakeCrosshair.DrawBodyDanger): под прицелом,
|
||
/// красная с тенью.</summary>
|
||
static 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 = Danger;
|
||
GUI.Label(rect, text, _dangerStyle);
|
||
GUI.color = old;
|
||
}
|
||
}
|
||
|
||
/// <summary>Запомнить полоски здоровья и выносливости игры - на их место PossessionHud кладёт свои.</summary>
|
||
[HarmonyPatch(typeof(XUiC_HUDStatBar), nameof(XUiC_HUDStatBar.Update))]
|
||
public static class Patch_XUiC_HUDStatBar_Update_PossessionHud
|
||
{
|
||
public static void Postfix(XUiC_HUDStatBar __instance)
|
||
{
|
||
if (__instance.statType == HUDStatTypes.Health || __instance.statType == HUDStatTypes.Stamina)
|
||
{
|
||
PossessionHud.Remember(__instance);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Приседание одержимого: пока зажата кнопка, рост зомби опускается до CrouchHeight, как игра
|
||
/// опускает его под низким потолком (EntityAlive.CrouchHeightFixedUpdate, её же шаги). Отпустил - дальше
|
||
/// считает игра: встаёт, только если сверху свободно. Сама игра пригибает лишь под потолком над головой,
|
||
/// а проём впереди чует через moveHelper, который у одержимого остановлен.</summary>
|
||
/// <summary>Движение анимации - туда, куда идёт одержимый (тест 13.10: «паук не может бежать вперёд, его сносит
|
||
/// влево»). У зомби с RootMotion (паук) шаг задаёт сама анимация, а у бега паука в ней уклон вбок; в игре его
|
||
/// каждый тик выправляет EntityMoveHelper - MakeMotionMoveToward к цели, скорость анимации та же. У одержимого
|
||
/// moveHelper стоит, поэтому то же самое делаем здесь, прямо перед тем, как MoveEntityHeaded превратит
|
||
/// накопленное движение анимации в шаг.</summary>
|
||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.MoveEntityHeaded))]
|
||
public static class Patch_EntityAlive_MoveEntityHeaded_Possession
|
||
{
|
||
public static void Prefix(EntityAlive __instance)
|
||
{
|
||
if (Possession.Ladder == 2 && Possession.IsPossessed(__instance))
|
||
{
|
||
__instance.motion.y = Mathf.Max(__instance.motion.y, Possession.LadderSpeed);
|
||
__instance.motion += Possession.MoveDir * 0.05f;
|
||
}
|
||
else if (Possession.Ladder != 0 && Possession.IsPossessed(__instance) && __instance.IsInElevator())
|
||
{
|
||
__instance.motion.y = Possession.Ladder * Possession.LadderSpeed;
|
||
}
|
||
Vector3 d = Possession.MoveDir;
|
||
if (d == Vector3.zero || !__instance.RootMotion || !Possession.IsPossessed(__instance) || __instance.Jumping)
|
||
{
|
||
return;
|
||
}
|
||
// Длина 10 - больше любого шага: MakeMotionMoveToward укоротит направление до длины движения анимации.
|
||
__instance.MakeMotionMoveToward(d.x * 10f, d.z * 10f, 0.02f, 1f);
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.CrouchHeightFixedUpdate))]
|
||
public static class Patch_EntityAlive_CrouchHeightFixedUpdate_Possession
|
||
{
|
||
public static bool Prefix(EntityAlive __instance)
|
||
{
|
||
if (!Possession.Crouching || !Possession.IsPossessed(__instance) || __instance.crouchType == 0 ||
|
||
__instance.physicsBaseHeight <= 1.3f)
|
||
{
|
||
return true;
|
||
}
|
||
float target = Mathf.Min(__instance.physicsBaseHeight * Possession.CrouchHeight, Possession.CrouchMaxHeight);
|
||
float h = Utils.FastMoveTowards(__instance.physicsHeight, target, 0.099999994f);
|
||
__instance.SetHeight(h);
|
||
if (h <= __instance.physicsBaseHeight * 0.7f)
|
||
{
|
||
__instance.crouchBendPerTarget = 0f;
|
||
if (__instance.walkType != 8 && __instance.walkType != 21)
|
||
{
|
||
int was = __instance.walkType;
|
||
__instance.SetWalkType(8);
|
||
__instance.walkTypeBeforeCrouch = was;
|
||
}
|
||
}
|
||
__instance.crouchBendPer = Mathf.MoveTowards(__instance.crouchBendPer, __instance.crouchBendPerTarget, 0.099999994f);
|
||
return false;
|
||
}
|
||
}
|
||
}
|