using System.Collections.Generic; using System.Globalization; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Шея одержимого стервятника (указание 08.10). Сидя он стоит в позе 0 ванильного спящего /// (sleeperSit) - голова опущена, как во сне. Это верно для еды, а бодрствующему нужна голова /// вперёд: шея и голова - посередине между полётом и сном. /// /// КАК. Модель и анимации игры не трогаем (в мод не идёт ни одного ассета игры): после аниматора /// доворачиваем кости шеи и головы. В полёте запоминаем их локальные повороты (сглаженно - крылья /// машут), сидя - каждый кадр берём то, что поставил аниматор (поза сна), и ставим долю Mix пути к /// полётным. Вход и выход - плавно за BlendTime. Зовётся из Possession.PlaceCamera (постфикс /// vp_FPCamera.LateUpdate) - после аниматора и до того, как камера читает положение головы. /// /// ПАМЯТЬ. Ничего не сохраняется; ссылки на кости - только пока стервятник одержим (Reset на выходе). /// public static class VultureNeck { /// Доля пути от позы сна к полётной (0.5 - посередине). Подбор - necroneck mix. public static float Mix = 0.5f; public static bool Enabled = true; public const float BlendTime = 0.3f; /// Полётную позу берём, когда стервятник летит хотя бы столько (без переходов анимации). public const float FlightSettle = 0.5f; /// Если костей с «neck» в имени нет - голова и столько её родителей. public const int FallbackChain = 3; static EntityAlive _v; static Transform[] _chain; static Quaternion[] _flight; static bool _hasFlight; static float _flyTime; static float _weight; public static void Reset() { _v = null; _chain = null; _flight = null; _hasFlight = false; _flyTime = 0f; _weight = 0f; } /// Каждый кадр одержимости стервятником, после аниматора. _awake - сидит, не ест. public static void Apply(EntityAlive _vulture, bool _sitting, bool _awake) { if (_vulture != _v) { Reset(); _v = _vulture; } if (_chain == null && !FindChain()) { return; } if (!_sitting) { _flyTime += Time.deltaTime; if (_flyTime >= FlightSettle) { float k = _hasFlight ? Mathf.Min(1f, Time.deltaTime * 4f) : 1f; for (int i = 0; i < _chain.Length; i++) { if (_chain[i] == null) { _chain = null; // модель пересобрана - найти кости заново return; } _flight[i] = Quaternion.Slerp(_flight[i], _chain[i].localRotation, k); } _hasFlight = true; } } else { _flyTime = 0f; } float want = Enabled && _sitting && _awake && _hasFlight ? 1f : 0f; _weight = Mathf.MoveTowards(_weight, want, Time.deltaTime / BlendTime); if (_weight <= 0f || !_sitting) { return; } float t = Mix * Mathf.SmoothStep(0f, 1f, _weight); for (int i = 0; i < _chain.Length; i++) { if (_chain[i] == null) { _chain = null; return; } _chain[i].localRotation = Quaternion.Slerp(_chain[i].localRotation, _flight[i], t); } } /// Цепочка от головы вверх: все кости с «neck» в имени (без них - FallbackChain родителей). static bool FindChain() { Transform head = _v?.emodel != null ? _v.emodel.GetHeadTransform() : null; if (head == null) { return false; } var list = new List { head }; for (Transform p = head.parent; p != null && p.name.ToLowerInvariant().Contains("neck"); p = p.parent) { list.Add(p); } if (list.Count == 1) { for (Transform p = head.parent; p != null && list.Count <= FallbackChain; p = p.parent) { list.Add(p); } } _chain = list.ToArray(); if (_flight == null || _flight.Length != _chain.Length) { _flight = new Quaternion[_chain.Length]; _hasFlight = false; } string names = ""; foreach (Transform t in _chain) { names += (names.Length > 0 ? " < " : "") + t.name; } Debug.Log("[NecromancerTome] VultureNeck: кости " + names); return true; } public static string Dump() { if (_chain == null) { return "костей нет - вселитесь в стервятника"; } string s = "mix " + F(Mix) + (Enabled ? "" : " (выкл)") + ", вес " + F(_weight) + ", полёт " + (_hasFlight ? "снят" : "не снят"); for (int i = 0; i < _chain.Length; i++) { if (_chain[i] != null) { s += "\n " + _chain[i].name + " сейчас " + HandPose.F(_chain[i].localEulerAngles) + ", в полёте " + HandPose.F(_flight[i].eulerAngles); } } return s; } static string F(float _f) => _f.ToString("0.##", CultureInfo.InvariantCulture); } /// /// necroneck - подбор шеи сидящего одержимого стервятника: /// mix <0..1> - доля пути от позы сна к полётной (0 - как спит, 1 - как в полёте) /// on | off - включить / выключить доворот /// show - кости и их повороты (в консоль и лог) /// [Preserve] public class ConsoleCmdNecroNeck : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necroneck" }; public override string getDescription() => "NecromancerTome: шея сидящего одержимого стервятника - mix <0..1>|on|off|show"; void Out(string _s) { SdtdConsole.Instance.Output(_s); Debug.Log("[NecromancerTome] necroneck: " + _s); } public override void Execute(List _params, CommandSenderInfo _senderInfo) { string cmd = _params.Count > 0 ? _params[0] : "show"; switch (cmd) { case "on": case "off": VultureNeck.Enabled = cmd == "on"; Out("доворот шеи " + (VultureNeck.Enabled ? "включён" : "выключен")); return; case "mix": if (_params.Count > 1 && float.TryParse(_params[1].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out float m)) { VultureNeck.Mix = Mathf.Clamp(m, -0.5f, 1.5f); } Out("mix " + VultureNeck.Mix.ToString("0.##", CultureInfo.InvariantCulture)); return; default: Out(VultureNeck.Dump()); return; } } } }