- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
5.9 KiB
C#
174 lines
5.9 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Проход одержимой крысы сквозь постройки (BACKLOG, «Могильная крыса» п.4; тест 09.10: «почему не срабатывает
|
||
/// проход сквозь текстуры по ПКМ»). Пока держишь ПКМ - как дух змеи, но без полёта:
|
||
/// - стены, двери, мебель не держат: поднят Entity.IsStuck, и её собственный шаг (тот же бег по WASD)
|
||
/// идёт мимо контроллера столкновений (Entity.ccEntityCollisionStart);
|
||
/// - высоту ведём сами: IsStuck снимает и пол, поэтому каждый кадр крыса стоит на поверхности под собой
|
||
/// (луч вниз с полуметра - ступенька по пути, как у змеи), а над пустотой падает с ускорением на неё;
|
||
/// - грунт (земля, песок, камень) не пускает - как у змеи: шаг в грунт отменяется;
|
||
/// - отпустил ПКМ внутри построек - выталкивает на ближайшее свободное место (вбок, вверх, вниз - дороже).
|
||
/// </summary>
|
||
public static class RatPhase
|
||
{
|
||
public static bool On;
|
||
static Vector3 _last;
|
||
static float _vy;
|
||
const float Gravity = 20f;
|
||
/// <summary>Точка тела крысы для проверки «внутри» и радиус шара свободного места.</summary>
|
||
const float BodyUp = 0.2f;
|
||
const float BodyRadius = 0.12f;
|
||
/// <summary>С какой высоты над ногами ищем пол: всё ниже - ступенька, на которую она взбегает.</summary>
|
||
const float StepUp = 0.5f;
|
||
static Vector3[] _ejectOffsets;
|
||
|
||
public static void Tick(World _world, EntityAlive _rat, bool _held)
|
||
{
|
||
if (_held && !On)
|
||
{
|
||
On = true;
|
||
_vy = 0f;
|
||
_last = _rat.position;
|
||
_rat.IsStuck = true;
|
||
RatModel.SetGhost(_rat, true);
|
||
Debug.Log("[NecromancerTome] RatPhase: проход включён");
|
||
}
|
||
else if (!_held && On)
|
||
{
|
||
Stop(_world, _rat, "ПКМ отпущена");
|
||
return;
|
||
}
|
||
if (!On)
|
||
{
|
||
return;
|
||
}
|
||
_rat.IsStuck = true;
|
||
_rat.fallDistance = 0f;
|
||
Vector3 p = _rat.position;
|
||
if (!_world.IsChunkAreaCollidersLoaded(p))
|
||
{
|
||
_rat.SetPosition(_last);
|
||
return;
|
||
}
|
||
// В грунт - нельзя: шаг вбок отменяем.
|
||
if (SnakeControl.Inside(_world, p + Vector3.up * BodyUp, SnakeControl.TerrainMask, out bool ground) && ground)
|
||
{
|
||
p.x = _last.x;
|
||
p.z = _last.z;
|
||
}
|
||
// Высота - только наша: собственная гравитация сущности с IsStuck пронесла бы её сквозь пол.
|
||
float dt = Time.deltaTime;
|
||
p.y = _last.y;
|
||
if (SnakeControl.Ground(p + Vector3.up * StepUp, StepUp + 300f, out float floor))
|
||
{
|
||
if (floor >= p.y - 0.02f)
|
||
{
|
||
p.y = floor;
|
||
_vy = 0f;
|
||
}
|
||
else
|
||
{
|
||
_vy -= Gravity * dt;
|
||
p.y = Mathf.Max(floor, p.y + _vy * dt);
|
||
if (p.y <= floor)
|
||
{
|
||
_vy = 0f;
|
||
}
|
||
}
|
||
}
|
||
Vector3 m = _rat.motion;
|
||
m.y = 0f;
|
||
_rat.motion = m;
|
||
_rat.SetPosition(p);
|
||
_last = p;
|
||
}
|
||
|
||
/// <summary>Выключить проход; внутри построек - вытолкнуть наружу.</summary>
|
||
public static void Stop(World _world, EntityAlive _rat, string _why)
|
||
{
|
||
if (!On)
|
||
{
|
||
return;
|
||
}
|
||
On = false;
|
||
if (_rat == null)
|
||
{
|
||
return;
|
||
}
|
||
_rat.IsStuck = false;
|
||
_rat.motion = Vector3.zero;
|
||
RatModel.SetGhost(_rat, false);
|
||
Vector3 p = _rat.position;
|
||
if (_world != null && !Free(_world, p))
|
||
{
|
||
Vector3? to = Eject(_world, p);
|
||
if (to.HasValue)
|
||
{
|
||
p = to.Value;
|
||
Debug.Log("[NecromancerTome] RatPhase: вытолкнута из построек, сдвиг " + (p - _rat.position).magnitude.ToString("F1") + " м");
|
||
}
|
||
else
|
||
{
|
||
p = _world.FindSupportingBlockPos(p);
|
||
Debug.Log("[NecromancerTome] RatPhase: свободного места рядом нет - на опорный блок");
|
||
}
|
||
_rat.SetPosition(p);
|
||
}
|
||
Debug.Log("[NecromancerTome] RatPhase: проход выключен - " + _why);
|
||
}
|
||
|
||
static bool Free(World _world, Vector3 _p)
|
||
{
|
||
Vector3 body = _p + Vector3.up * BodyUp;
|
||
return _world.IsChunkAreaCollidersLoaded(_p) &&
|
||
!SnakeControl.Inside(_world, body, SnakeControl.WallMask, out _) &&
|
||
!Physics.CheckSphere(body - Origin.position, BodyRadius, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
|
||
}
|
||
|
||
/// <summary>Ближайшее свободное место с опорой: до 4 м вбок, до 1 м вниз, до 4 м вверх; вниз дороже.</summary>
|
||
static Vector3? Eject(World _world, Vector3 _p)
|
||
{
|
||
if (_ejectOffsets == null)
|
||
{
|
||
List<Vector3> list = new List<Vector3>();
|
||
for (float r = 0f; r <= 4f; r += 0.25f)
|
||
{
|
||
int dirs = r == 0f ? 1 : 16;
|
||
for (float h = -1f; h <= 4f; h += 0.5f)
|
||
{
|
||
for (int i = 0; i < dirs; i++)
|
||
{
|
||
list.Add(Quaternion.Euler(0f, i * 22.5f, 0f) * Vector3.forward * r + Vector3.up * h);
|
||
}
|
||
}
|
||
}
|
||
list.Sort((x, y) => (x.magnitude + (x.y < 0f ? -x.y : 0f)).CompareTo(y.magnitude + (y.y < 0f ? -y.y : 0f)));
|
||
_ejectOffsets = list.ToArray();
|
||
}
|
||
foreach (Vector3 o in _ejectOffsets)
|
||
{
|
||
Vector3 c = _p + o;
|
||
if (!Free(_world, c))
|
||
{
|
||
continue;
|
||
}
|
||
// Посадить на пол под этим местом; если пол сам внутри построек - место не годится.
|
||
if (SnakeControl.Ground(c + Vector3.up * 0.3f, 6f, out float y))
|
||
{
|
||
c.y = y;
|
||
if (!Free(_world, c))
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
return c;
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
}
|