- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
7.0 KiB
C#
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 <0..1> - доля пути от позы сна к полётной (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;
|
|
}
|
|
}
|
|
}
|
|
}
|