Files
necromants-tome-7d2d-3-2/HarmonySrc/Possession.cs
T

2544 lines
114 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using GamePath;
using 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;
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>Сколько чанков держать вокруг тела, пока игрок в зомби (указание 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;
// Пояс одержимому питомцу ни к чему (указание 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;
FirstPerson = false;
HideBody(null);
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;
_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;
}
if (input != null && !gui && (input.CameraChange.WasPressed || input.QuickMenu.WasPressed || input.PermanentActions.QuickMenu.WasPressed))
{
FirstPerson = !FirstPerson;
_fpEye = Vector3.zero;
if (!FirstPerson)
{
HideBody(null);
}
Debug.Log("[NecromancerTome] Possession: вид от " + (FirstPerson ? "первого" : "третьего") + " лица");
}
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;
if (moving)
{
// Бег - ночная скорость ярости (moveSpeedAggroMax) в любое время: днём GetMoveSpeedAggro отдаёт
// дневную, а у многих зомби она равна шагу (13.10: «не все зомби бегают по Shift»).
z.SetMoveForwardWithModifiers(run ? Mathf.Max(z.GetMoveSpeedAggro(),
EffectManager.GetValue(PassiveEffects.RunSpeed, null, z.moveSpeedAggroMax, z)) : z.GetMoveSpeed(), 1f, 0f, false);
}
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)
{
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;
if (meat <= 0f)
{
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") + ", " + 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>Тело одержимого в виде от первого лица - только тень (тест 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);
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>Одержимый зомби без своего ИИ: шаги ИИ, путь и 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);
}
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]);
}
}
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)
{
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;
}
}
}