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

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

174 lines
5.9 KiB
C#
Raw Permalink 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 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;
}
}
}