Одержимость питомцев: роли медведя, волка, собаки и крысы; ноша стервятника; череп при обыске

- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы);
  E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь
  заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная.
- Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса.
- Зомбособака: нюх - все существа рядом на компасе и карте.
- Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам.
- Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки
  (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва).
- Череп (останки): 2% при обычном обыске красного трупа вместо браслета.
- Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках.
- Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-09 00:58:40 +03:00
co-authored by Claude Opus 5.5
parent beab1640b0
commit 31aa99860c
25 changed files with 2461 additions and 210 deletions
+172
View File
@@ -0,0 +1,172 @@
using System.Collections.Generic;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Одержимый лютоволк - охотник (решено 09.10, BACKLOG «Медведь и лютоволк в одержимости»):
/// - бросок на ПКМ: враг под прицелом не дальше LeapRange - волк разворачивается и прыгает прямо на
/// него (прыжок самой игры: SetJumpDistance на точную дальность и перепад высоты); приземлился рядом -
/// цель падает на LeapStun секунд;
/// - след крови: раненые враги (здоровье ниже BloodHealth) отмечены на компасе и карте до BloodRange,
/// пока не умрут или не залечатся, - отличие от нюха собаки: та чует всех рядом, волк - подранков издалека.
/// </summary>
public static class WolfControl
{
public const string WolfClass = "necroZombieWolf";
public const float LeapRange = 10f;
public const float LeapMin = 2f;
public const float LeapConeDeg = 10f;
public const float LeapCooldown = 3f;
public const float LeapHit = 2.5f;
public const float LeapStun = 2.5f;
public const float BloodHealth = 0.5f;
public const float BloodRange = 150f;
public const float BloodDropRange = 200f;
static EntityAlive _leapTarget;
static float _leapUntil, _leapStart, _nextLeap;
static readonly HashSet<int> _blood = new HashSet<int>();
static float _bloodCheck;
public static bool Is(EntityAlive _e)
{
return _e != null && EntityClass.list.TryGetValue(_e.entityClass, out EntityClass cls) && cls != null &&
cls.entityClassName == WolfClass;
}
/// <summary>Каждый кадр одержимости волком.</summary>
public static void Tick(World _world, EntityPlayerLocal _player, EntityAlive _wolf, bool _leap, ref float _bodyYaw)
{
if (_leap && _leapTarget == null && Time.time >= _nextLeap && _wolf.onGround && !_wolf.Jumping)
{
EntityAlive t = Target(_world, _player, _wolf);
if (t != null)
{
Vector3 to = t.position - _wolf.position;
Vector3 flat = new Vector3(to.x, 0f, to.z);
_bodyYaw = Mathf.Atan2(flat.x, flat.z) * Mathf.Rad2Deg;
_wolf.SetRotation(new Vector3(0f, _bodyYaw, 0f));
_wolf.Jumping = true;
_wolf.SetJumpDistance(flat.magnitude, Mathf.Max(0.5f, to.y + 0.5f));
_leapTarget = t;
_leapStart = Time.time;
_leapUntil = Time.time + 2f;
_nextLeap = Time.time + LeapCooldown;
Debug.Log("[NecromancerTome] WolfControl: бросок на " + t.EntityName + " (" + t.entityId + "), " + flat.magnitude.ToString("F1") + " м");
}
}
if (_leapTarget != null)
{
bool landed = Time.time - _leapStart > 0.3f && _wolf.onGround && !_wolf.Jumping;
if (_leapTarget.IsDead() || Time.time > _leapUntil)
{
_leapTarget = null;
}
else if (landed || (_leapTarget.getChestPosition() - _wolf.getHeadPosition()).sqrMagnitude < LeapHit * LeapHit * 0.5f)
{
if ((_leapTarget.position - _wolf.position).sqrMagnitude < LeapHit * LeapHit)
{
KnockDown(_leapTarget, _wolf);
}
_leapTarget = null;
}
}
BloodTick(_world, _wolf);
}
static void KnockDown(EntityAlive _t, EntityAlive _wolf)
{
_wolf.emodel?.avatarController?.StartAnimationAttack();
if (_t.bodyDamage.CurrentStun == EnumEntityStunType.None && _t.emodel?.avatarController != null)
{
_t.emodel.avatarController.BeginStun(EnumEntityStunType.Prone, EnumBodyPartHit.Torso, Utils.EnumHitDirection.Front, false, 0.5f);
_t.SetStun(EnumEntityStunType.Prone);
_t.bodyDamage.StunDuration = LeapStun;
}
Debug.Log("[NecromancerTome] WolfControl: " + _t.EntityName + " сбит с ног");
}
/// <summary>Враг ближе всех к лучу прицела: не дальше LeapRange от волка и в конусе LeapConeDeg.</summary>
static EntityAlive Target(World _world, EntityPlayerLocal _player, EntityAlive _wolf)
{
Transform cam = _player.cameraTransform;
if (cam == null)
{
return null;
}
Vector3 eye = cam.position + Origin.position;
EntityAlive best = null;
float bestAngle = LeapConeDeg;
for (int i = 0; i < _world.EntityAlives.Count; i++)
{
EntityAlive e = _world.EntityAlives[i];
if (!SnakeControl.IsFoe(e, _wolf))
{
continue;
}
float d = (e.position - _wolf.position).magnitude;
if (d > LeapRange || d < LeapMin)
{
continue;
}
float angle = Vector3.Angle(cam.forward, e.getChestPosition() - eye);
if (angle < bestAngle)
{
bestAngle = angle;
best = e;
}
}
return best;
}
static void BloodTick(World _world, EntityAlive _wolf)
{
_bloodCheck -= Time.deltaTime;
if (_bloodCheck > 0f)
{
return;
}
_bloodCheck = 1f;
for (int i = 0; i < _world.EntityAlives.Count; i++)
{
EntityAlive e = _world.EntityAlives[i];
if (e == null)
{
continue;
}
float d2 = (e.position - _wolf.position).sqrMagnitude;
bool bleeding = SnakeControl.IsFoe(e, _wolf) && e.Health < e.GetMaxHealth() * BloodHealth;
if (bleeding && d2 < BloodRange * BloodRange)
{
if (_blood.Add(e.entityId))
{
e.AddNavObject(SnakeControl.SenseNavObject, null, null);
}
}
else if (_blood.Contains(e.entityId) && (!bleeding || d2 > BloodDropRange * BloodDropRange))
{
e.RemoveNavObject(SnakeControl.SenseNavObject);
_blood.Remove(e.entityId);
}
}
_blood.RemoveWhere(id => _world.GetEntity(id) == null);
}
/// <summary>Выход из волка - снять метки, забыть бросок.</summary>
public static void Clear()
{
World world = GameManager.Instance?.World;
if (world != null)
{
foreach (int id in _blood)
{
(world.GetEntity(id) as EntityAlive)?.RemoveNavObject(SnakeControl.SenseNavObject);
}
}
_blood.Clear();
_bloodCheck = 0f;
_leapTarget = null;
}
}
}