using System.Collections.Generic; using UnityEngine; namespace NecromancerTome { /// /// Ноша в когтях одержимого стервятника (указание 08.10: «сделать курицу в когтях стервятника без хитбокса /// и отдельным элементом, чтобы крылья и шея свисали вниз»). /// /// Раньше под лапы каждый кадр переставлялась сама курица, и было две беды. Рывки: модель сущности /// догоняет её позицию с задержкой (Entity.updateTransform - Lerp), а позиция стервятника меняется /// 20 раз в секунду - курица отставала и прыгала. Твёрдость: её коллайдеры мешали спускаться, и /// стервятник садился на неё. /// /// Теперь при захвате с курицы снимается слепок: кости крыльев и шеи развёрнуты вниз, сетки запечены /// (BakeMesh) в неподвижные MeshRenderer без коллайдеров. Слепок висит под стервятником по его /// видимой (сглаженной) позиции. Сама курица спрятана (forceRenderingOff - его не трогает /// avatarController.SetVisible), коллайдеры выключены, ИИ стоит; её позиция идёт за слепком, /// и когда стервятник её отпускает, она появляется ровно там, где висел слепок. /// public static class CarryVisual { static GameObject _holder; static readonly List _meshes = new List(); static readonly List _hidden = new List(); static readonly List _off = new List(); /// Верх и низ слепка относительно его опоры, м. static float _top, _bottom; /// Сколько крыло отводится в сторону от вертикали и шея - вперёд. 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(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(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().sharedMesh = mesh; MeshRenderer mr = part.AddComponent(); 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(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); } /// Каждый кадр: слепок под лапами стервятника по его видимой позиции; у земли - стоит на ней. 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 _claws = new List(); /// Точка хвата - середина между лапами стервятника (кости с «toe»/«foot» в имени, а без них - «leg»), /// по анимации, каждый кадр (тест 08.10: «курица висит под стервятником, а не в его когтях»). Костей нет - /// низ его модели. 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 all = new List(); foreach (string key in new[] { "toe", "foot", "leg" }) { foreach (Transform t in model.GetComponentsInChildren(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(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; } /// Отпустить: курица снова видна и тверда там, где висел слепок. 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(); } /// Убрать слепок, ничего не возвращая (выход из мира). 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(); } /// Обвисшая поза: каждая кость крыла и шеи (от корня к концу) разворачивается так, чтобы её /// дочерняя кость оказалась под ней - крылья свисают чуть в стороны, шея с головой - чуть вперёд. /// Аниматор следующим кадром позу вернёт, но слепок уже запечён, а сама курица спрятана. static void Limp(EntityAlive _c, Transform _model) { HashSet bones = new HashSet(); foreach (SkinnedMeshRenderer smr in _model.GetComponentsInChildren(true)) { if (smr.bones == null) { continue; } foreach (Transform b in smr.bones) { if (b != null) { bones.Add(b); } } } List order = new List(bones); order.Sort((a, b) => Depth(a).CompareTo(Depth(b))); if (!_loggedBones) { _loggedBones = true; List names = new List(); 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; } } }