- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
340 lines
12 KiB
C#
340 lines
12 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Ноша в когтях одержимого стервятника (указание 08.10: «сделать курицу в когтях стервятника без хитбокса
|
||
/// и отдельным элементом, чтобы крылья и шея свисали вниз»).
|
||
///
|
||
/// Раньше под лапы каждый кадр переставлялась сама курица, и было две беды. Рывки: модель сущности
|
||
/// догоняет её позицию с задержкой (Entity.updateTransform - Lerp), а позиция стервятника меняется
|
||
/// 20 раз в секунду - курица отставала и прыгала. Твёрдость: её коллайдеры мешали спускаться, и
|
||
/// стервятник садился на неё.
|
||
///
|
||
/// Теперь при захвате с курицы снимается слепок: кости крыльев и шеи развёрнуты вниз, сетки запечены
|
||
/// (BakeMesh) в неподвижные MeshRenderer без коллайдеров. Слепок висит под стервятником по его
|
||
/// видимой (сглаженной) позиции. Сама курица спрятана (forceRenderingOff - его не трогает
|
||
/// avatarController.SetVisible), коллайдеры выключены, ИИ стоит; её позиция идёт за слепком,
|
||
/// и когда стервятник её отпускает, она появляется ровно там, где висел слепок.
|
||
/// </summary>
|
||
public static class CarryVisual
|
||
{
|
||
static GameObject _holder;
|
||
static readonly List<Mesh> _meshes = new List<Mesh>();
|
||
static readonly List<Renderer> _hidden = new List<Renderer>();
|
||
static readonly List<Collider> _off = new List<Collider>();
|
||
/// <summary>Верх и низ слепка относительно его опоры, м.</summary>
|
||
static float _top, _bottom;
|
||
/// <summary>Сколько крыло отводится в сторону от вертикали и шея - вперёд.</summary>
|
||
const float WingOut = 0.35f;
|
||
const float NeckForward = 0.35f;
|
||
static bool _loggedBones;
|
||
|
||
public static void Begin(EntityAlive _c)
|
||
{
|
||
Clear();
|
||
Transform model = _c.emodel != null ? _c.emodel.GetModelTransform() : null;
|
||
if (model == null)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] CarryVisual: у " + _c.EntityName + " нет модели - несём как есть");
|
||
return;
|
||
}
|
||
// Не только свои: физическое тело для толкания (physicsRBT) игра выносит из иерархии сущности
|
||
// (Entity.PhysicsInit - SetParent наружу), и на нём стервятник «сидел» в воздухе (тест 09.10).
|
||
foreach (Transform root in new[] { _c.transform, _c.PhysicsTransform, _c.physicsRBT })
|
||
{
|
||
if (root == null)
|
||
{
|
||
continue;
|
||
}
|
||
foreach (Collider col in root.GetComponentsInChildren<Collider>(true))
|
||
{
|
||
if (col != null && col.enabled)
|
||
{
|
||
col.enabled = false;
|
||
_off.Add(col);
|
||
}
|
||
}
|
||
}
|
||
Limp(_c, model);
|
||
|
||
_holder = new GameObject("necroCarry_" + _c.entityId);
|
||
_holder.transform.SetPositionAndRotation(_c.transform.position, Quaternion.Euler(0f, _c.transform.eulerAngles.y, 0f));
|
||
bool any = false;
|
||
_top = float.MinValue;
|
||
_bottom = float.MaxValue;
|
||
foreach (Renderer r in model.GetComponentsInChildren<Renderer>(false))
|
||
{
|
||
if (r == null || !r.enabled || r.forceRenderingOff)
|
||
{
|
||
continue;
|
||
}
|
||
Mesh mesh;
|
||
if (r is SkinnedMeshRenderer smr)
|
||
{
|
||
if (smr.sharedMesh == null)
|
||
{
|
||
continue;
|
||
}
|
||
mesh = new Mesh();
|
||
smr.BakeMesh(mesh, true);
|
||
_meshes.Add(mesh);
|
||
}
|
||
else if (r is MeshRenderer && r.TryGetComponent(out MeshFilter mf) && mf.sharedMesh != null)
|
||
{
|
||
mesh = mf.sharedMesh;
|
||
}
|
||
else
|
||
{
|
||
continue;
|
||
}
|
||
GameObject part = new GameObject(r.name);
|
||
part.transform.SetPositionAndRotation(r.transform.position, r.transform.rotation);
|
||
part.transform.SetParent(_holder.transform, true);
|
||
if (!(r is SkinnedMeshRenderer))
|
||
{
|
||
part.transform.localScale = r.transform.lossyScale;
|
||
}
|
||
part.AddComponent<MeshFilter>().sharedMesh = mesh;
|
||
MeshRenderer mr = part.AddComponent<MeshRenderer>();
|
||
mr.sharedMaterials = r.sharedMaterials;
|
||
mr.shadowCastingMode = r.shadowCastingMode;
|
||
// Высота - по самой запечённой сетке: r.bounds у SkinnedMeshRenderer - рамка с запасом, по ней
|
||
// курица висела далеко под лапами (тест 09.10, «высота 0.99 м»).
|
||
foreach (Vector3 v in mesh.vertices)
|
||
{
|
||
float y = part.transform.TransformPoint(v).y - _holder.transform.position.y;
|
||
_top = Mathf.Max(_top, y);
|
||
_bottom = Mathf.Min(_bottom, y);
|
||
}
|
||
any = true;
|
||
}
|
||
if (!any)
|
||
{
|
||
_top = 0.3f;
|
||
_bottom = 0f;
|
||
}
|
||
foreach (Renderer r in _c.GetComponentsInChildren<Renderer>(true))
|
||
{
|
||
if (r != null && !r.forceRenderingOff)
|
||
{
|
||
r.forceRenderingOff = true;
|
||
_hidden.Add(r);
|
||
}
|
||
}
|
||
Debug.Log("[NecromancerTome] CarryVisual: слепок " + _c.EntityName + " - частей " + _holder.transform.childCount +
|
||
", высота " + (_top - _bottom).ToString("F2") + " м, выключено коллайдеров " + _off.Count);
|
||
}
|
||
|
||
/// <summary>Каждый кадр: слепок под лапами стервятника по его видимой позиции; у земли - стоит на ней.</summary>
|
||
public static void Place(EntityAlive _v, EntityAlive _c, float _gap)
|
||
{
|
||
Vector3 feet = Claws(_v);
|
||
Quaternion yaw = Quaternion.Euler(0f, _v.transform.eulerAngles.y, 0f);
|
||
float y = feet.y - _top - _gap;
|
||
if (Physics.Raycast(feet + Vector3.up * 0.1f, Vector3.down, out RaycastHit hit, _top - _bottom + _gap + 0.1f,
|
||
SnakeControl.WallMask, QueryTriggerInteraction.Ignore) && hit.point.y > y + _bottom)
|
||
{
|
||
y = hit.point.y - _bottom;
|
||
}
|
||
Vector3 at = new Vector3(feet.x, y, feet.z);
|
||
if (_holder != null)
|
||
{
|
||
_holder.transform.SetPositionAndRotation(at, yaw);
|
||
}
|
||
_c.SetPosition(at + Origin.position);
|
||
}
|
||
|
||
static int _clawsOf = -1;
|
||
static readonly List<Transform> _claws = new List<Transform>();
|
||
|
||
/// <summary>Точка хвата - середина между лапами стервятника (кости с «toe»/«foot» в имени, а без них - «leg»),
|
||
/// по анимации, каждый кадр (тест 08.10: «курица висит под стервятником, а не в его когтях»). Костей нет -
|
||
/// низ его модели.</summary>
|
||
static Vector3 Claws(EntityAlive _v)
|
||
{
|
||
if (_clawsOf != _v.entityId)
|
||
{
|
||
_clawsOf = _v.entityId;
|
||
_claws.Clear();
|
||
Transform model = _v.emodel != null ? _v.emodel.GetModelTransform() : null;
|
||
if (model != null)
|
||
{
|
||
List<string> all = new List<string>();
|
||
foreach (string key in new[] { "toe", "foot", "leg" })
|
||
{
|
||
foreach (Transform t in model.GetComponentsInChildren<Transform>(true))
|
||
{
|
||
string n = t.name.ToLowerInvariant();
|
||
if (key == "toe" || key == "foot" ? n.Contains(key) : n.Contains(key) && !n.Contains("up") && !n.Contains("thigh"))
|
||
{
|
||
_claws.Add(t);
|
||
}
|
||
}
|
||
if (_claws.Count > 0)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
foreach (Transform t in model.GetComponentsInChildren<Transform>(true))
|
||
{
|
||
all.Add(t.name);
|
||
}
|
||
Debug.Log("[NecromancerTome] CarryVisual: лапы " + _v.EntityName + ": " +
|
||
(_claws.Count > 0 ? string.Join(", ", _claws.ConvertAll(t => t.name)) : "не найдены") +
|
||
"; все кости: " + string.Join(", ", all));
|
||
}
|
||
}
|
||
if (_claws.Count == 0)
|
||
{
|
||
return _v.transform.position;
|
||
}
|
||
Vector3 sum = Vector3.zero;
|
||
float low = float.MaxValue;
|
||
foreach (Transform t in _claws)
|
||
{
|
||
sum += t.position;
|
||
low = Mathf.Min(low, t.position.y);
|
||
}
|
||
Vector3 c = sum / _claws.Count;
|
||
c.y = low;
|
||
return c;
|
||
}
|
||
|
||
/// <summary>Отпустить: курица снова видна и тверда там, где висел слепок.</summary>
|
||
public static void End(EntityAlive _c)
|
||
{
|
||
if (_c != null && _holder != null && !_c.IsDead())
|
||
{
|
||
Vector3 at = _holder.transform.position;
|
||
_c.SetPosition(at + Origin.position);
|
||
_c.transform.position = at;
|
||
}
|
||
foreach (Renderer r in _hidden)
|
||
{
|
||
if (r != null)
|
||
{
|
||
r.forceRenderingOff = false;
|
||
}
|
||
}
|
||
foreach (Collider col in _off)
|
||
{
|
||
if (col != null)
|
||
{
|
||
col.enabled = true;
|
||
}
|
||
}
|
||
_hidden.Clear();
|
||
_off.Clear();
|
||
Clear();
|
||
}
|
||
|
||
/// <summary>Убрать слепок, ничего не возвращая (выход из мира).</summary>
|
||
public static void Clear()
|
||
{
|
||
if (_holder != null)
|
||
{
|
||
Object.Destroy(_holder);
|
||
}
|
||
_holder = null;
|
||
foreach (Mesh m in _meshes)
|
||
{
|
||
if (m != null)
|
||
{
|
||
Object.Destroy(m);
|
||
}
|
||
}
|
||
_meshes.Clear();
|
||
}
|
||
|
||
/// <summary>Обвисшая поза: каждая кость крыла и шеи (от корня к концу) разворачивается так, чтобы её
|
||
/// дочерняя кость оказалась под ней - крылья свисают чуть в стороны, шея с головой - чуть вперёд.
|
||
/// Аниматор следующим кадром позу вернёт, но слепок уже запечён, а сама курица спрятана.</summary>
|
||
static void Limp(EntityAlive _c, Transform _model)
|
||
{
|
||
HashSet<Transform> bones = new HashSet<Transform>();
|
||
foreach (SkinnedMeshRenderer smr in _model.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||
{
|
||
if (smr.bones == null)
|
||
{
|
||
continue;
|
||
}
|
||
foreach (Transform b in smr.bones)
|
||
{
|
||
if (b != null)
|
||
{
|
||
bones.Add(b);
|
||
}
|
||
}
|
||
}
|
||
List<Transform> order = new List<Transform>(bones);
|
||
order.Sort((a, b) => Depth(a).CompareTo(Depth(b)));
|
||
if (!_loggedBones)
|
||
{
|
||
_loggedBones = true;
|
||
List<string> names = new List<string>();
|
||
foreach (Transform b in order)
|
||
{
|
||
names.Add(b.name);
|
||
}
|
||
Debug.Log("[NecromancerTome] CarryVisual: кости " + _c.EntityName + ": " + string.Join(", ", names));
|
||
}
|
||
Vector3 fwd = _c.transform.forward;
|
||
Vector3 right = _c.transform.right;
|
||
Vector3 center = _c.transform.position;
|
||
int wings = 0, necks = 0;
|
||
foreach (Transform b in order)
|
||
{
|
||
string n = b.name.ToLowerInvariant();
|
||
Vector3 want;
|
||
if (n.Contains("wing"))
|
||
{
|
||
float side = Mathf.Sign(Vector3.Dot(b.position - center, right));
|
||
want = (Vector3.down + right * side * WingOut).normalized;
|
||
wings++;
|
||
}
|
||
else if (n.Contains("neck") || n.Contains("head"))
|
||
{
|
||
want = (Vector3.down + fwd * NeckForward).normalized;
|
||
necks++;
|
||
}
|
||
else
|
||
{
|
||
continue;
|
||
}
|
||
Transform child = null;
|
||
for (int i = 0; i < b.childCount; i++)
|
||
{
|
||
if (bones.Contains(b.GetChild(i)))
|
||
{
|
||
child = b.GetChild(i);
|
||
break;
|
||
}
|
||
}
|
||
if (child == null)
|
||
{
|
||
continue;
|
||
}
|
||
Vector3 dir = child.position - b.position;
|
||
if (dir.sqrMagnitude < 1e-6f)
|
||
{
|
||
continue;
|
||
}
|
||
b.rotation = Quaternion.FromToRotation(dir, want) * b.rotation;
|
||
}
|
||
Debug.Log("[NecromancerTome] CarryVisual: обвисли костей крыльев " + wings + ", шеи/головы " + necks);
|
||
}
|
||
|
||
static int Depth(Transform _t)
|
||
{
|
||
int d = 0;
|
||
for (Transform p = _t.parent; p != null; p = p.parent)
|
||
{
|
||
d++;
|
||
}
|
||
return d;
|
||
}
|
||
}
|
||
}
|