Чертоги разума, хваты пустой руки, одержимость, книга с оглавлением
- Чертоги разума: дух в префабе chertogi под землёй, книга памяти, нож и плакат; кровать префаба - мебель necroPalaceBed (иначе становилась точкой возрождения); охрана выбрасывает и игроков (их нет в EntityAlives). - Хваты пустой руки по R: «отрицание» и «пиф-паф» - поза рук костями поверх аниматора, остальные кости заморожены, ЛКМ - заклинание, прицел краснеет на зомби. - «Отрицание»: зомби в ужасе бежит дальше 40 м и теряет игрока как цель. - «Пиф-паф»: одержимость зомби-гуманоидом - бег, прыжок, удар, дальняя атака, подрыв, крик крикуньи; тело цепенеет, зомби танкуют одержимого; область вокруг тела сжата; полоска здоровья; спавн и прорисовка у одержимого. - Книга с оглавлением (окно necroTome), глава «Некроэнергия». - Прорисовка дверей у змеи и одержимого (CheckLODs от камеры, а не от тела). - Почтовый ящик консоли и команды necrobones/necropose/necroshot для разработки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c828202c66
commit
66a0dcf403
@@ -0,0 +1,688 @@
|
||||
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>Сколько чанков держать вокруг тела, пока игрок в зомби (указание 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 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;
|
||||
EntityAlive z = Zombie;
|
||||
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 (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.Attack(false))
|
||||
{
|
||||
z.Attack(true);
|
||||
_nextAttack = Time.time + Mathf.Max(0.3f, z.GetAttackTimeoutTicks() / 20f);
|
||||
}
|
||||
_observer?.SetPosition(z.position);
|
||||
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>Точка под прицелом камеры (в мировых координатах), до 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 ||
|
||||
(e.position - _z.position).sqrMagnitude > TauntRange * TauntRange || !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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user