Files
necromants-tome-7d2d-3-2/HarmonySrc/Possession.cs
T
AlexCubeandClaude Opus 5.5 fdd3782024 Змея: выход из текстур, полёт на ПКМ; одержимый: приседание, удар по прицелу; коза
- Змея: проверка «внутри текстуры» лучами (земля игры - поверхность), каждый кадр выброс
  на ближайшее свободное место (на земле - из любых текстур, в полёте - из грунта);
  шаг и прыжок в текстуры не пускают. Полёт пока зажата ПКМ: 1.1 м/с (половина шага
  зомби), сквозь постройки можно, в грунт нельзя; Пробел вверх, C/Ctrl вниз.
  E - аварийный выход. Детализация зданий и область тела - как у одержимого.
- Одержимый: будит спящих (комнаты зданий и сам вид одержимого), приседание на C и Ctrl,
  удар по точке под прицелом с наведением на грудь ближнего врага.
- Хваты: толчок руки к прицелу на ЛКМ; «пиф-паф» вселяет на пике толчка.
  «Отрицание» стало «Козой» (13 языков): средний и безымянный поджаты, большой прижат;
  звук - бросок змеи.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:28:40 +03:00

819 lines
34 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 м.
/// ПКМ у крикуньи - её крик (SoundAlert): все зомби в ScreamRange бросаются на неё, даже не видя (её
/// ванильный зов орды - часть разведки AIScoutHordeSpawner, напрямую не вызываем).
/// E - подрыв у EntityZombieCop (коп, мутант, подрывник): HandlePrimingDetonator, через несколько
/// секунд взрыв от имени зомби - он гибнет, игрок возвращается в тело.
///
/// ПАМЯТЬ. Ничего не сохраняется. Ссылки - только на зомби, тело и замороженные аниматоры, на время
/// одержимости; на выходе и при выходе из мира всё обнуляется.
/// </summary>
public static class Possession
{
public static bool Active;
public static EntityAlive Zombie;
/// <summary>Камера - позади и выше головы зомби.</summary>
public const float CamBack = 2.6f;
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>Сколько чанков держать вокруг тела, пока игрок в зомби (указание 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;
static float _nextScream;
static EntityPlayerLocal _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;
enum RState { Idle, StartAnim, Release, Fire }
static RState _rState;
static float _rTime, _rNext;
static GameObject _hud;
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>Только зомби-гуманоиды (решение 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)
{
if (Active || SnakeMode.Active || MindPalace.Active || _player0 == null || _player0.IsDead() ||
_player0.AttachedToEntity != null || !CanPossess(_zombie) || _player0.world.IsRemote())
{
return false;
}
Active = true;
_player = _player0;
Zombie = _zombie;
_yaw = _player0.rotation.y;
_bodyYaw = _zombie.rotation.y;
_pitch = 0f;
_nextAttack = 0f;
_nextTaunt = 0f;
_nextScream = 0f;
_loggedCamera = false;
// Зомби - в руки игрока: цели, путь и шаги ИИ сброшены.
_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;
}
ReadRanged(_zombie);
Hint(_player0, _zombie);
Debug.Log("[NecromancerTome] Possession: вселение в " + _zombie.EntityName + " (" + _zombie.entityId + ")");
return true;
}
public static void Exit(string _why)
{
if (!Active)
{
return;
}
Active = false;
Crouching = false;
_meleeLookUntil = 0f;
_crouchToggled = false;
EntityAlive z = Zombie;
z?.SetLookPosition(Vector3.zero);
EntityPlayerLocal player = _player;
if (_rState != RState.Idle && z != null && !z.IsDead())
{
StopRanged(z);
}
_rState = RState.Idle;
_hasRanged = false;
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);
}
}
// Сам зомби в ужасе бежит от тела игрока (решение 03.10: «пугается и убегает как при отрицании»).
if (!z.IsDead() && player != null)
{
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;
if (world == null)
{
if (Active || Zombie != null || _frozen.Count > 0)
{
Active = false;
Zombie = null;
_player = null;
_observer = null;
_bodyViewDim = -1;
_frozen.Clear();
_frozenSpeed.Clear();
}
return;
}
if (!Active)
{
return;
}
EntityAlive z = Zombie;
if (_player == null || _player.IsDead())
{
Exit("тело погибло");
return;
}
if (z == null || z.IsDead() || z.IsMarkedForUnload())
{
Exit("одержимый погиб");
return;
}
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.ToggleCrouch.WasPressed)
{
_crouchToggled = !_crouchToggled;
}
Crouching = input != null && !gui && (input.Crouch.IsPressed || _crouchToggled);
if (detonate && z is EntityZombieCop cop && !cop.isPrimed)
{
Debug.Log("[NecromancerTome] Possession: подрыв " + z.EntityName);
cop.HandlePrimingDetonator();
}
Ranged(z, ranged, rangedHeld);
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; // наклон головы - для луча удара
if (moving)
{
z.SetMoveForwardWithModifiers(run ? z.GetMoveSpeedAggro() : z.GetMoveSpeed(), 1f, 0f, false);
}
else
{
z.SetMoveForwardWithModifiers(0f, 0f, 0f, false);
}
if (jump && z.onGround && !z.Jumping)
{
z.Jumping = true;
z.SetJumpDistance(Mathf.Max(1.5f, z.jumpMaxDistance), 1.2f);
}
if (ranged && IsScreamer(z))
{
Scream(world, z);
}
if (attack && Time.time >= _nextAttack)
{
z.SetLookPosition(MeleeAim(world, z));
if (z.Attack(false))
{
z.Attack(true);
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).
SnakeControl.Sleepers(world, _player, z.position, ref _sleeperCheck, Time.deltaTime);
Taunt(world, z);
}
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);
_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 (_hasRanged)
{
text += ", " + Localization.Get("necroPossessHintRanged");
}
if (IsScreamer(_z))
{
text += ", " + Localization.Get("necroPossessHintScream");
}
if (_z.jumpMaxDistance >= 5f)
{
text += ", " + Localization.Get("necroPossessHintLeap");
}
if (_z is EntityZombieCop)
{
text += ", " + Localization.Get("necroPossessHintDetonate");
}
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 м.</summary>
static Vector3 AimPoint()
{
Transform cam = _player?.cameraTransform;
if (cam == null)
{
return Zombie.position + Quaternion.Euler(-_pitch, _yaw, 0f) * Vector3.forward * 30f;
}
if (Physics.Raycast(cam.position, cam.forward, out RaycastHit hit, 50f, -5, QueryTriggerInteraction.Ignore) &&
hit.distance > CamBack + 0.5f)
{
return hit.point + Origin.position;
}
return cam.position + Origin.position + cam.forward * 50f;
}
/// <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)
{
StopRanged(_z);
}
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;
}
static bool IsScreamer(EntityAlive _z)
{
return EntityClass.list.TryGetValue(_z.entityClass, out EntityClass c) && c.entityClassName.StartsWith("zombieScreamer");
}
/// <summary>Крик одержимой крикуньи: её звук тревоги и все зомби в ScreamRange - на неё (танкование).</summary>
static void Scream(World _world, EntityAlive _z)
{
if (Time.time < _nextScream)
{
return;
}
_nextScream = Time.time + ScreamCooldown;
string snd = _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);
e.SetAttackTarget(_z, TauntTicks);
n++;
}
Debug.Log("[NecromancerTome] Possession: крик крикуньи - на зов идут " + 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;
_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);
e.SetAttackTarget(_z, TauntTicks);
}
}
static void AllowZombieTarget(EntityAlive _e)
{
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 == typeof(EntityZombie);
}
if (!has)
{
t.targetClasses.Add(new EAIApproachAndAttackTarget.TargetClass { type = typeof(EntityZombie), chaseTimeMax = 0f });
}
}
}
/// <summary>Из постфикса vp_FPCamera.LateUpdate: камера позади и выше головы зомби, не сквозь стены.</summary>
public static void PlaceCamera(Transform _camera)
{
EntityAlive z = Zombie;
if (z == null)
{
return;
}
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);
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);
}
_camera.SetPositionAndRotation(eye, rot);
if (!_loggedCamera)
{
_loggedCamera = true;
Debug.Log("[NecromancerTome] Possession: камера за " + (head != null ? "головой " + head.name : "корнем (головы нет)"));
}
}
}
/// <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);
}
}
/// <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>Полоска здоровья одержимого (тест 03.10: «а индикатор здоровья у одержимого где?») -
/// внизу по центру, над поясом, с именем зомби. Своё здоровье игрок видит как обычно (тело беззащитно).
/// OnGUI, как прицел змеи (SnakeCrosshair), - без своего окна XUi.</summary>
public class PossessionHud : MonoBehaviour
{
const float Width = 420f, Height = 16f, Bottom = 150f;
static readonly Color Back = new Color(0f, 0f, 0f, 0.6f);
static readonly Color Fill = new Color(0.55f, 0.08f, 0.06f, 0.95f);
static GUIStyle _style;
void OnGUI()
{
EntityAlive z = Possession.Active ? Possession.Zombie : null;
if (z == null || Event.current.type != EventType.Repaint)
{
return;
}
float scale = Screen.height / 1080f;
float w = Width * scale, h = Height * scale;
Rect r = new Rect((Screen.width - w) / 2f, Screen.height - Bottom * scale - h, w, h);
float frac = Mathf.Clamp01(z.Health / (float)Mathf.Max(1, z.GetMaxHealth()));
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;
if (_style == null)
{
_style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.LowerCenter };
_style.normal.textColor = new Color(0.9f, 0.85f, 0.8f);
}
_style.fontSize = Mathf.RoundToInt(18f * scale);
string name = Localization.Get(z.EntityName);
GUI.Label(new Rect(r.x, r.y - 26f * scale, r.width, 24f * scale), name + " " + z.Health + " / " + z.GetMaxHealth(), _style);
}
}
/// <summary>Приседание одержимого: пока зажата кнопка, рост зомби опускается до CrouchHeight, как игра
/// опускает его под низким потолком (EntityAlive.CrouchHeightFixedUpdate, её же шаги). Отпустил - дальше
/// считает игра: встаёт, только если сверху свободно. Сама игра пригибает лишь под потолком над головой,
/// а проём впереди чует через moveHelper, который у одержимого остановлен.</summary>
[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 h = Utils.FastMoveTowards(__instance.physicsHeight, __instance.physicsBaseHeight * Possession.CrouchHeight, 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;
}
}
}