- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
6.0 KiB
C#
173 lines
6.0 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|