Files
necromants-tome-7d2d-3-2/HarmonySrc/VultureNeck.cs
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

198 lines
7.0 KiB
C#

using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Шея одержимого стервятника (указание 08.10). Сидя он стоит в позе 0 ванильного спящего
/// (sleeperSit) - голова опущена, как во сне. Это верно для еды, а бодрствующему нужна голова
/// вперёд: шея и голова - посередине между полётом и сном.
///
/// КАК. Модель и анимации игры не трогаем (в мод не идёт ни одного ассета игры): после аниматора
/// доворачиваем кости шеи и головы. В полёте запоминаем их локальные повороты (сглаженно - крылья
/// машут), сидя - каждый кадр берём то, что поставил аниматор (поза сна), и ставим долю Mix пути к
/// полётным. Вход и выход - плавно за BlendTime. Зовётся из Possession.PlaceCamera (постфикс
/// vp_FPCamera.LateUpdate) - после аниматора и до того, как камера читает положение головы.
///
/// ПАМЯТЬ. Ничего не сохраняется; ссылки на кости - только пока стервятник одержим (Reset на выходе).
/// </summary>
public static class VultureNeck
{
/// <summary>Доля пути от позы сна к полётной (0.5 - посередине). Подбор - necroneck mix.</summary>
public static float Mix = 0.5f;
public static bool Enabled = true;
public const float BlendTime = 0.3f;
/// <summary>Полётную позу берём, когда стервятник летит хотя бы столько (без переходов анимации).</summary>
public const float FlightSettle = 0.5f;
/// <summary>Если костей с «neck» в имени нет - голова и столько её родителей.</summary>
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;
}
/// <summary>Каждый кадр одержимости стервятником, после аниматора. _awake - сидит, не ест.</summary>
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);
}
}
/// <summary>Цепочка от головы вверх: все кости с «neck» в имени (без них - FallbackChain родителей).</summary>
static bool FindChain()
{
Transform head = _v?.emodel != null ? _v.emodel.GetHeadTransform() : null;
if (head == null)
{
return false;
}
var list = new List<Transform> { 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);
}
/// <summary>
/// necroneck - подбор шеи сидящего одержимого стервятника:
/// mix &lt;0..1&gt; - доля пути от позы сна к полётной (0 - как спит, 1 - как в полёте)
/// on | off - включить / выключить доворот
/// show - кости и их повороты (в консоль и лог)
/// </summary>
[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<string> _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;
}
}
}
}