810 lines
35 KiB
C#
810 lines
35 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.Scripting;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Дух-работник урны - «джинн из урны» (решение 16.10): кончик хвоста уходит в горлышко, дух вырастает над урной,
|
||
/// без работы висит, чуть покачиваясь и медленно поворачиваясь. Этап 3 некротехнолога.
|
||
///
|
||
/// Не сущность игры, а видимая модель - дочерний объект модели блока урны, как каркас зоны (решение 16.10 вместо
|
||
/// плана 15.10 «сущность на префабе медсестры»): хитбокса, ИИ и сохранения у неё нет, поэтому «убить нельзя»,
|
||
/// «зомби не замечают», «сквозь стены» выходят сами, а сейвы дух не трогает - он появляется по черепу в урне
|
||
/// (TEFeatureCinerarium.HasSkull) и пропадает без него. Сдвиги Origin и выгрузку чанка переживает с моделью урны.
|
||
///
|
||
/// Модель - бандл necrotechnologist (NecroTechnologistSetup.cs): своя сетка на своём скелете NT_rig в позе покоя,
|
||
/// глаза - отдельные рендереры (в имени «eye»). Тело - светящаяся дымка (16.10: Fade давал пятна; сложение One/One,
|
||
/// холодный оттенок Tint, своё слабое свечение Glow); текстура уже серая (сделана под обесцвечивание). Глаза - свой
|
||
/// непрозрачный светящийся материал из бандла, очередь отрисовки 3100 - после тела.
|
||
/// </summary>
|
||
public static class CinerariumGenie
|
||
{
|
||
public const string ModelPath = "#@modfolder(NecromancerTome):Resources/necrotechnologist?necroTechnologistPrefab.prefab";
|
||
|
||
/// <summary>Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»).
|
||
/// Подгоняется командой necrogenie.</summary>
|
||
public static float OffsetY = -0.15f;
|
||
/// <summary>У урны рабочего горлышко ниже (земля в чаше на 0.45 м против 0.53 у урны сборщика) - дух ниже на столько же.</summary>
|
||
public static float WorkerDrop = -0.08f;
|
||
/// <summary>Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади»
|
||
/// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе
|
||
/// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.</summary>
|
||
public static float Forward = 0.07f;
|
||
/// <summary>Поворот без работы, градусов в секунду.</summary>
|
||
public static float TurnSpeed = 6f;
|
||
/// <summary>Покачивание: размах (м) и период (с).</summary>
|
||
public static float BobHeight = 0.04f;
|
||
public static float BobPeriod = 4f;
|
||
public static float Alpha = 0.3f;
|
||
/// <summary>Засыпание (16.10): игрок дальше SleepDistance - дух замирает дома, огоньки нитей стоят. Работа идёт.</summary>
|
||
public static float SleepDistance = 40f;
|
||
public static bool Hidden;
|
||
|
||
/// <summary>Далеко ли игрок от точки сцены (без Origin). Для засыпания духа и нитей.</summary>
|
||
public static bool PlayerFar(Vector3 _scenePos)
|
||
{
|
||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||
if (pl == null)
|
||
{
|
||
return true;
|
||
}
|
||
return (pl.GetPosition() - Origin.position - _scenePos).sqrMagnitude > SleepDistance * SleepDistance;
|
||
}
|
||
/// <summary>Руки в жесте: плечо от вертикали вбок, локоть согнут вперёд, градусы. Покой - «смирно» (18.10: «Всё Смирно и
|
||
/// будет позой для бездельничающих некротехов») запечён в позу покоя модели (pose_attention.json при выгрузке FBX),
|
||
/// аниматора нет - без жеста кости не трогаются.</summary>
|
||
public static float ArmOut = 3f;
|
||
public static float Elbow = 0f;
|
||
|
||
static GameObject _prefab;
|
||
static bool _tried;
|
||
static readonly Dictionary<Material, Material> _ghostMats = new Dictionary<Material, Material>();
|
||
/// <summary>Все джинны на сцене - команда necrogenie правит их сразу.</summary>
|
||
public static readonly List<CinerariumGenieMotion> Live = new List<CinerariumGenieMotion>();
|
||
|
||
static bool Load()
|
||
{
|
||
if (!_tried)
|
||
{
|
||
_tried = true;
|
||
_prefab = DataLoader.LoadAsset<GameObject>(ModelPath);
|
||
Debug.Log("[NecromancerTome] CinerariumGenie: модель " + (_prefab != null ? "загружена" : "НЕ загружена: " + ModelPath));
|
||
}
|
||
return _prefab != null;
|
||
}
|
||
|
||
/// <summary>Дух логиста (18.10: «Пусть дух вылетает когда нужно что-то нести. Но уменьшим его в три раза»): размер.</summary>
|
||
public static float LogistScale = 1f / 3f;
|
||
/// <summary>Дом духа логиста в осях модели урны - корень модели духа внутри кадила, середина тела на высоте окон крышки
|
||
/// (по центру - окна на 0.63 м, у стены - 0.73 м, кадило в 0.22 м от стены, стена - -z).</summary>
|
||
public static Vector3 LogistHome = new Vector3(0f, 0.25f, 0f);
|
||
public static Vector3 LogistWallHome = new Vector3(0f, 0.35f, -0.22f);
|
||
/// <summary>Окна крышки - на расстоянии LogistExit от оси кадила (радиус крышки 0.21 м), по осям модели.</summary>
|
||
public static float LogistExit = 0.3f;
|
||
/// <summary>Лутер: сдвиг духа вперёд (подобран в Blender 18.10).</summary>
|
||
public static float LootForward = 0.08f;
|
||
|
||
/// <summary>_logist: 0 - обычный дух над горлышком, 1 - дух логиста в кадиле по центру, 2 - в кадиле у стены.</summary>
|
||
public static GameObject Spawn(Transform _urnModel, float _drop, int _logist = 0)
|
||
{
|
||
if (_urnModel == null || !Load())
|
||
{
|
||
return null;
|
||
}
|
||
GameObject g = Object.Instantiate(_prefab, _urnModel, false);
|
||
g.name = "NecroCinerariumGenie";
|
||
foreach (Renderer r in g.GetComponentsInChildren<Renderer>(true))
|
||
{
|
||
r.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
r.receiveShadows = false;
|
||
if (r is SkinnedMeshRenderer smr)
|
||
{
|
||
smr.updateWhenOffscreen = false; // 16.10: вне экрана скелет не считать (экономия); границы - от сетки
|
||
}
|
||
if (r.name.Contains("eye"))
|
||
{
|
||
continue;
|
||
}
|
||
Material[] mats = r.sharedMaterials;
|
||
for (int i = 0; i < mats.Length; i++)
|
||
{
|
||
mats[i] = Ghost(mats[i]);
|
||
}
|
||
r.sharedMaterials = mats;
|
||
}
|
||
foreach (Collider c in g.GetComponentsInChildren<Collider>(true))
|
||
{
|
||
Object.Destroy(c);
|
||
}
|
||
CinerariumGenieMotion m = g.AddComponent<CinerariumGenieMotion>();
|
||
m.Drop = _drop;
|
||
if (_logist > 0)
|
||
{
|
||
m.Small = true;
|
||
m.Wall = _logist == 2;
|
||
g.transform.localScale = Vector3.one * LogistScale;
|
||
}
|
||
m.Pose();
|
||
return g;
|
||
}
|
||
|
||
/// <summary>Полупрозрачная копия материала тела - одна на исходный материал на всю игру.</summary>
|
||
static Material Ghost(Material _src)
|
||
{
|
||
if (_src == null)
|
||
{
|
||
return null;
|
||
}
|
||
if (_ghostMats.TryGetValue(_src, out Material m) && m != null)
|
||
{
|
||
return m;
|
||
}
|
||
m = new Material(_src) { name = _src.name + "_ghost" };
|
||
Shader std = Shader.Find("Standard");
|
||
if (std != null && m.shader != std)
|
||
{
|
||
m.shader = std;
|
||
}
|
||
// 16.10: Fade (SrcAlpha/OneMinusSrcAlpha) давал «прозрачный, но какими-то пятнами» и «не выглядит бесплотным
|
||
// духом»: без записи глубины треугольники одной сетки ложатся в случайном порядке. Свечение (One/One) от порядка
|
||
// не зависит: дух только добавляет свет к тому, что за ним, - дымка. Альфа здесь не нужна, яркость - цветом.
|
||
m.SetFloat("_Mode", 2f);
|
||
m.SetOverrideTag("RenderType", "Transparent");
|
||
m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
|
||
m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.One);
|
||
m.SetInt("_ZWrite", 0);
|
||
m.DisableKeyword("_ALPHATEST_ON");
|
||
m.DisableKeyword("_ALPHABLEND_ON");
|
||
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||
if (m.HasProperty("_Glossiness"))
|
||
{
|
||
m.SetFloat("_Glossiness", 0f); // без бликов: блик при сложении - белое пятно
|
||
}
|
||
if (m.HasProperty("_Metallic"))
|
||
{
|
||
m.SetFloat("_Metallic", 0f);
|
||
}
|
||
if (m.HasProperty("_EmissionMap") && m.HasProperty("_MainTex"))
|
||
{
|
||
m.SetTexture("_EmissionMap", m.GetTexture("_MainTex")); // своё свечение - чтобы ночью не пропадал
|
||
m.EnableKeyword("_EMISSION");
|
||
m.globalIlluminationFlags = MaterialGlobalIlluminationFlags.None;
|
||
}
|
||
SetLook(m); // смешивание, глубина и очередь - тоже там (Solid)
|
||
_ghostMats[_src] = m;
|
||
return m;
|
||
}
|
||
|
||
/// <summary>Холодный бледный оттенок духа (умножается на серую текстуру).</summary>
|
||
public static Color Tint = new Color(0.55f, 0.75f, 1f, 1f);
|
||
/// <summary>Своё свечение тела, доля от Tint.</summary>
|
||
public static float Glow = 0.35f;
|
||
|
||
/// <summary>ВРЕМЕННО (17.10: «Временно убери им прозрачность. А то плохо видно» - пока подбирается поза рук): дух
|
||
/// непрозрачный, обычная заливка с записью глубины. Перед релизом вернуть false (BACKLOG). Консоль: necrogenie solid 0|1.</summary>
|
||
public static bool Solid = false; // 18.10: прозрачность вернули («Прозрачность верни сразу»)
|
||
|
||
static void SetLook(Material _m)
|
||
{
|
||
if (_m == null)
|
||
{
|
||
return;
|
||
}
|
||
_m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
|
||
_m.SetInt("_DstBlend", (int)(Solid ? UnityEngine.Rendering.BlendMode.Zero : UnityEngine.Rendering.BlendMode.One));
|
||
_m.SetInt("_ZWrite", Solid ? 1 : 0);
|
||
_m.SetOverrideTag("RenderType", Solid ? "Opaque" : "Transparent");
|
||
_m.renderQueue = (int)(Solid ? UnityEngine.Rendering.RenderQueue.Geometry : UnityEngine.Rendering.RenderQueue.Transparent);
|
||
if (Solid)
|
||
{
|
||
if (_m.HasProperty("_Color"))
|
||
{
|
||
_m.color = new Color(Tint.r, Tint.g, Tint.b, 1f);
|
||
}
|
||
if (_m.HasProperty("_EmissionColor"))
|
||
{
|
||
_m.SetColor("_EmissionColor", new Color(Tint.r * Glow * 0.5f, Tint.g * Glow * 0.5f, Tint.b * Glow * 0.5f, 1f));
|
||
}
|
||
return;
|
||
}
|
||
if (_m.HasProperty("_Color"))
|
||
{
|
||
_m.color = new Color(Tint.r * Alpha, Tint.g * Alpha, Tint.b * Alpha, 1f);
|
||
}
|
||
if (_m.HasProperty("_EmissionColor"))
|
||
{
|
||
_m.SetColor("_EmissionColor", new Color(Tint.r * Glow, Tint.g * Glow, Tint.b * Glow, 1f));
|
||
}
|
||
}
|
||
|
||
public static void ApplyAlpha()
|
||
{
|
||
foreach (Material m in _ghostMats.Values)
|
||
{
|
||
SetLook(m);
|
||
}
|
||
}
|
||
|
||
static readonly Dictionary<Material, Material> _stuckMats = new Dictionary<Material, Material>();
|
||
|
||
/// <summary>Почерневший дух (класть некуда): свечением чёрное не нарисовать, поэтому обычная Fade-копия, тёмная.</summary>
|
||
public static Material Stuck(Material _src)
|
||
{
|
||
if (_src == null)
|
||
{
|
||
return null;
|
||
}
|
||
if (_stuckMats.TryGetValue(_src, out Material m) && m != null)
|
||
{
|
||
return m;
|
||
}
|
||
m = new Material(_src) { name = _src.name + "_stuck" };
|
||
Shader std = Shader.Find("Standard");
|
||
if (std != null && m.shader != std)
|
||
{
|
||
m.shader = std;
|
||
}
|
||
m.SetFloat("_Mode", 2f);
|
||
m.SetOverrideTag("RenderType", "Transparent");
|
||
m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||
m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||
m.SetInt("_ZWrite", 1); // с глубиной: пятен нет, тёмный и почти плотный
|
||
m.DisableKeyword("_ALPHATEST_ON");
|
||
m.EnableKeyword("_ALPHABLEND_ON");
|
||
m.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||
m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;
|
||
if (m.HasProperty("_Color"))
|
||
{
|
||
m.color = new Color(0.04f, 0.035f, 0.045f, 0.8f);
|
||
}
|
||
_stuckMats[_src] = m;
|
||
return m;
|
||
}
|
||
|
||
/// <summary>Исходный материал тела по его светящейся копии.</summary>
|
||
public static Material Source(Material _ghost)
|
||
{
|
||
foreach (KeyValuePair<Material, Material> p in _ghostMats)
|
||
{
|
||
if (p.Value == _ghost)
|
||
{
|
||
return p.Key;
|
||
}
|
||
}
|
||
foreach (KeyValuePair<Material, Material> p in _stuckMats)
|
||
{
|
||
if (p.Value == _ghost)
|
||
{
|
||
return p.Key;
|
||
}
|
||
}
|
||
return _ghost;
|
||
}
|
||
|
||
/// <summary>Шейдер материала пропал - дух рисуется пурпурным (23.10.2026, скриншот пользователя: после выхода из
|
||
/// питомца в 218 блоках от базы все духи некротехнологов пурпурные; раньше - #70, дух закопанной урны).</summary>
|
||
public static bool Broken(Material _m)
|
||
{
|
||
return _m != null && (_m.shader == null || !_m.shader.isSupported || _m.shader.name == "Hidden/InternalErrorShader");
|
||
}
|
||
|
||
/// <summary>Сломанную копию - из кэша вон и собрать заново из исходного материала модели.</summary>
|
||
public static Material Repair(Material _broken, bool _stuck)
|
||
{
|
||
Material src = Source(_broken);
|
||
Debug.Log("[NecromancerTome] CinerariumGenie: шейдер духа сломан (" + (_broken.shader != null ? _broken.shader.name : "null") +
|
||
", исходный материал '" + (src != null ? src.name : "null") + "' - шейдер " +
|
||
(src != null && src.shader != null ? src.shader.name + (src.shader.isSupported ? "" : " НЕ поддерживается") : "null") +
|
||
", Standard " + (Shader.Find("Standard") != null ? "есть" : "НЕ найден") + ") - материал пересоздан");
|
||
if (src == null || src == _broken)
|
||
{
|
||
return _broken;
|
||
}
|
||
_ghostMats.Remove(src);
|
||
_stuckMats.Remove(src);
|
||
return _stuck ? Stuck(src) : Ghost(src);
|
||
}
|
||
}
|
||
|
||
/// <summary>Покачивание и поворот джинна над урной. Только вид - никакой логики.</summary>
|
||
[Preserve]
|
||
public partial class CinerariumGenieMotion : MonoBehaviour
|
||
{
|
||
float _yaw;
|
||
float _phase;
|
||
/// <summary>Своя поправка высоты урны (CinerariumGenie.WorkerDrop у рабочего).</summary>
|
||
public float Drop;
|
||
/// <summary>Свой сдвиг вперёд (NaN - общий CinerariumGenie.Forward). Лутер (18.10, банка с крышкой-кадилом): 0.08 - хвост
|
||
/// по центру отверстия на макушке («Вот сейчас было идеально»).</summary>
|
||
public float ForwardOverride = float.NaN;
|
||
/// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
|
||
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
|
||
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary>
|
||
struct Stop
|
||
{
|
||
public Vector3 Hover, Look; // в осях модели урны
|
||
public float Fly;
|
||
public bool Pass; // точка пути (окно кадила) - без жеста
|
||
}
|
||
|
||
/// <summary>Дух логиста (18.10): втрое меньше, дома (в кадиле) не виден, вылетает и влетает через окно крышки.</summary>
|
||
public bool Small;
|
||
/// <summary>Кадило у стены: дом сдвинут к кронштейну, окно в стену (-z) не используется.</summary>
|
||
public bool Wall;
|
||
|
||
readonly List<Stop> _stops = new List<Stop>();
|
||
float _backFly;
|
||
float _t = -1f;
|
||
/// <summary>Скорость полёта, м/с: время перелёта - не меньше OutSeconds/BackSeconds, дальше - по расстоянию (нити до 15 м).</summary>
|
||
public static float FlySpeed = 4f;
|
||
public static float OutSeconds = 1.3f;
|
||
public static float WorkSeconds = 1.6f;
|
||
public static float BackSeconds = 1.3f;
|
||
/// <summary>Жест: плечи вперёд до GestureFwd градусов, локти ходят на ±GestureWave вразнобой.</summary>
|
||
public static float GestureFwd = 55f;
|
||
public static float GestureWave = 30f;
|
||
bool _gesturing;
|
||
/// <summary>Почернел и замер (готовое класть некуда, решение 06.10).</summary>
|
||
bool _stuck;
|
||
/// <summary>Кости рук (плечо, предплечье, кисть по сторонам) и их поворот в позе покоя.</summary>
|
||
Transform[] _arm;
|
||
Quaternion[] _armRest;
|
||
static bool _armLogged;
|
||
|
||
/// <summary>Покой - руки в позе модели («смирно»).</summary>
|
||
public void Pose()
|
||
{
|
||
Pose(0f, CinerariumGenie.Elbow, CinerariumGenie.Elbow);
|
||
}
|
||
|
||
/// <summary>Жест долей _w от покоя: плечо вперёд на GestureFwd, направление плеча и предплечья задаётся в осях модели
|
||
/// (вперёд - +Z), поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют.</summary>
|
||
void Pose(float _w, float _elbowL, float _elbowR)
|
||
{
|
||
if (_arm == null)
|
||
{
|
||
string[] names = { "LeftArm", "LeftForeArm", "LeftHand", "RightArm", "RightForeArm", "RightHand" };
|
||
_arm = new Transform[names.Length];
|
||
_armRest = new Quaternion[names.Length];
|
||
foreach (Transform t in GetComponentsInChildren<Transform>(true))
|
||
{
|
||
for (int i = 0; i < names.Length; i++)
|
||
{
|
||
if (_arm[i] == null && t.name.EndsWith(names[i]))
|
||
{
|
||
_arm[i] = t;
|
||
_armRest[i] = t.localRotation;
|
||
}
|
||
}
|
||
}
|
||
if (!_armLogged)
|
||
{
|
||
_armLogged = true;
|
||
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
||
for (int i = 0; i < names.Length; i++)
|
||
{
|
||
sb.Append(names[i]).Append('=').Append(_arm[i] != null ? "ok" : "НЕТ").Append(' ');
|
||
}
|
||
Debug.Log("[NecromancerTome] CinerariumGenie: кости рук " + sb);
|
||
}
|
||
}
|
||
for (int i = 0; i < _arm.Length; i++)
|
||
{
|
||
if (_arm[i] != null)
|
||
{
|
||
_arm[i].localRotation = _armRest[i];
|
||
}
|
||
}
|
||
if (_w <= 0f)
|
||
{
|
||
return;
|
||
}
|
||
for (int side = 0; side < 2; side++)
|
||
{
|
||
Transform up = _arm[side * 3], fore = _arm[side * 3 + 1], hand = _arm[side * 3 + 2];
|
||
if (up == null || fore == null || hand == null)
|
||
{
|
||
continue;
|
||
}
|
||
float sign = Mathf.Sign(transform.InverseTransformPoint(up.position).x);
|
||
float o = CinerariumGenie.ArmOut * Mathf.Deg2Rad, f = GestureFwd * Mathf.Deg2Rad;
|
||
Vector3 upDir = new Vector3(sign * Mathf.Sin(o), -Mathf.Cos(o) * Mathf.Cos(f), Mathf.Sin(f)).normalized;
|
||
Vector3 foreDir = Vector3.RotateTowards(upDir, Vector3.forward, (side == 0 ? _elbowL : _elbowR) * Mathf.Deg2Rad, 0f);
|
||
up.rotation = Quaternion.Slerp(Quaternion.identity, Quaternion.FromToRotation(fore.position - up.position, transform.TransformDirection(upDir)), _w) * up.rotation;
|
||
fore.rotation = Quaternion.Slerp(Quaternion.identity, Quaternion.FromToRotation(hand.position - fore.position, transform.TransformDirection(foreDir)), _w) * fore.rotation;
|
||
}
|
||
}
|
||
|
||
/// <summary>Светящиеся материалы рендереров - вернуть после почернения.</summary>
|
||
readonly Dictionary<Renderer, Material[]> _glowing = new Dictionary<Renderer, Material[]>();
|
||
|
||
/// <summary>Облёт мест работы по порядку (у перевозки по нити - ящик «откуда», потом «куда»), у каждого - жест, затем
|
||
/// назад к урне. Точки - в координатах сцены (без Origin): stand - где встать (низ клетки), look - низ клетки места,
|
||
/// beside - встал сбоку, а не над местом. Возвращает, через сколько секунд дело (середина последнего жеста).</summary>
|
||
public float Visit(List<(Vector3 stand, Vector3 look, bool beside)> _list)
|
||
{
|
||
if (transform.parent == null || _list == null || _list.Count == 0)
|
||
{
|
||
return 0f;
|
||
}
|
||
_stops.Clear();
|
||
Vector3 home = Home();
|
||
Vector3 prev = home;
|
||
float delay = 0f;
|
||
Vector3 exit = home;
|
||
if (Small)
|
||
{
|
||
// Сначала - к окну крышки в сторону первого места (у стены окно в стену не годится), оттуда - к месту.
|
||
exit = Window(home, transform.parent.InverseTransformPoint(_list[0].stand) - home);
|
||
_stops.Add(new Stop { Hover = exit, Look = exit + (exit - home), Fly = 0.35f, Pass = true });
|
||
delay += 0.35f;
|
||
prev = exit;
|
||
}
|
||
foreach ((Vector3 stand, Vector3 look, bool beside) in _list)
|
||
{
|
||
// Высота корня духа над низом клетки места: обычный дух - над станцией 0.8 (сбоку 0.15) с поправкой урны;
|
||
// маленький - над верхом клетки (1.05) или у её середины сбоку (0.4).
|
||
float up = Small ? (beside ? 0.4f : 1.05f) : CinerariumGenie.OffsetY + Drop + (beside ? 0.15f : 0.8f);
|
||
Stop st = new Stop
|
||
{
|
||
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, up, 0f),
|
||
Look = transform.parent.InverseTransformPoint(look)
|
||
};
|
||
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
|
||
delay += st.Fly + WorkSeconds;
|
||
prev = st.Hover;
|
||
_stops.Add(st);
|
||
}
|
||
if (Small)
|
||
{
|
||
// Назад - тем же окном.
|
||
_stops.Add(new Stop { Hover = exit, Look = home, Fly = Mathf.Max(BackSeconds, Vector3.Distance(prev, exit) / FlySpeed), Pass = true });
|
||
prev = exit;
|
||
}
|
||
_backFly = Small ? 0.35f : Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
|
||
_t = 0f;
|
||
return delay - WorkSeconds * 0.5f;
|
||
}
|
||
|
||
/// <summary>Окно крышки кадила по направлению _dir (оси модели): одно из четырёх по осям x/z, у стены - не в стену.</summary>
|
||
Vector3 Window(Vector3 _home, Vector3 _dir)
|
||
{
|
||
Vector3 best = Vector3.forward;
|
||
float bestDot = float.MinValue;
|
||
foreach (Vector3 w in new[] { Vector3.forward, Vector3.back, Vector3.left, Vector3.right })
|
||
{
|
||
if (Wall && w == Vector3.back)
|
||
{
|
||
continue;
|
||
}
|
||
float d = Vector3.Dot(w, new Vector3(_dir.x, 0f, _dir.z));
|
||
if (d > bestDot)
|
||
{
|
||
bestDot = d;
|
||
best = w;
|
||
}
|
||
}
|
||
return _home + best * CinerariumGenie.LogistExit;
|
||
}
|
||
|
||
Vector3 Home()
|
||
{
|
||
if (Small)
|
||
{
|
||
return Wall ? CinerariumGenie.LogistWallHome : CinerariumGenie.LogistHome;
|
||
}
|
||
float fwd = float.IsNaN(ForwardOverride) ? CinerariumGenie.Forward : ForwardOverride;
|
||
return new Vector3(0f, CinerariumGenie.OffsetY + Drop, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, fwd);
|
||
}
|
||
|
||
/// <summary>Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.</summary>
|
||
void Face(Vector3 _dir)
|
||
{
|
||
if (_dir.x * _dir.x + _dir.z * _dir.z > 0.01f)
|
||
{
|
||
_yaw = Mathf.MoveTowardsAngle(_yaw, Mathf.Atan2(_dir.x, _dir.z) * Mathf.Rad2Deg, 300f * Time.deltaTime) % 360f;
|
||
}
|
||
}
|
||
|
||
void Gesture(float _w)
|
||
{
|
||
if (_w > 0f)
|
||
{
|
||
float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave;
|
||
Pose(_w, CinerariumGenie.Elbow + 15f + wave, CinerariumGenie.Elbow + 15f - wave);
|
||
_gesturing = true;
|
||
}
|
||
else if (_gesturing)
|
||
{
|
||
Pose();
|
||
_gesturing = false;
|
||
}
|
||
}
|
||
|
||
public void SetStuck(bool _on)
|
||
{
|
||
if (_stuck == _on)
|
||
{
|
||
return;
|
||
}
|
||
_stuck = _on;
|
||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||
{
|
||
if (r.name.Contains("eye"))
|
||
{
|
||
continue;
|
||
}
|
||
if (!_glowing.ContainsKey(r))
|
||
{
|
||
_glowing[r] = r.sharedMaterials;
|
||
}
|
||
Material[] glow = _glowing[r];
|
||
if (_on)
|
||
{
|
||
Material[] dark = new Material[glow.Length];
|
||
for (int i = 0; i < glow.Length; i++)
|
||
{
|
||
dark[i] = CinerariumGenie.Stuck(CinerariumGenie.Source(glow[i]));
|
||
}
|
||
r.sharedMaterials = dark;
|
||
}
|
||
else
|
||
{
|
||
r.sharedMaterials = glow;
|
||
}
|
||
}
|
||
}
|
||
|
||
void OnEnable()
|
||
{
|
||
CinerariumGenie.Live.Add(this);
|
||
_phase = Random.value * 10f;
|
||
_yaw = Random.value * 360f;
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
CinerariumGenie.Live.Remove(this);
|
||
}
|
||
|
||
float _nextSleepCheck;
|
||
bool _asleep;
|
||
bool _hiddenApplied;
|
||
float _nextShaderCheck;
|
||
Renderer[] _renderers;
|
||
|
||
/// <summary>Раз в 5 с: не сломан ли шейдер у материалов духа (CinerariumGenie.Broken) - починить на месте.</summary>
|
||
void CheckShaders()
|
||
{
|
||
if (_renderers == null)
|
||
{
|
||
_renderers = GetComponentsInChildren<Renderer>(true);
|
||
}
|
||
foreach (Renderer r in _renderers)
|
||
{
|
||
if (r == null || r.name.Contains("eye"))
|
||
{
|
||
continue;
|
||
}
|
||
Material[] mats = r.sharedMaterials;
|
||
bool fixedAny = false;
|
||
for (int i = 0; i < mats.Length; i++)
|
||
{
|
||
if (CinerariumGenie.Broken(mats[i]))
|
||
{
|
||
mats[i] = CinerariumGenie.Repair(mats[i], _stuck);
|
||
fixedAny = true;
|
||
}
|
||
}
|
||
if (fixedAny)
|
||
{
|
||
r.sharedMaterials = mats;
|
||
_glowing.Clear(); // сохранённые светлые наборы могли быть со сломанными копиями
|
||
}
|
||
}
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (Time.time >= _nextShaderCheck)
|
||
{
|
||
_nextShaderCheck = Time.time + 5f;
|
||
CheckShaders();
|
||
}
|
||
if (Time.time >= _nextSleepCheck)
|
||
{
|
||
_nextSleepCheck = Time.time + 0.5f;
|
||
// Уснул - игрок далеко или дух не среди ближайших (CinerariumLoad.AnimateNearest): замирает дома, работа идёт.
|
||
bool asleep = CinerariumGenie.PlayerFar(transform.position) || !CinerariumLoad.Animated(this);
|
||
if (asleep && !_asleep && !Digging) // старатель под землёй - не возвращать домой
|
||
{
|
||
_t = -1f; // полёт прерван - дело всё равно выполнится
|
||
Gesture(0f);
|
||
transform.localPosition = Home();
|
||
}
|
||
_asleep = asleep;
|
||
}
|
||
// Виден ли: не скрыт командой; дух логиста - только в полёте (дома сидит в кадиле). Рендереры - только при смене
|
||
// (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
|
||
bool show = !CinerariumGenie.Hidden && (!Small || _t >= 0f) && !DigHidden;
|
||
if (_hiddenApplied != !show)
|
||
{
|
||
_hiddenApplied = !show;
|
||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||
{
|
||
r.enabled = show;
|
||
}
|
||
}
|
||
if (DigUpdate(_asleep))
|
||
{
|
||
return; // старатель: спуск, под землёй, подъём (CinerariumProspector.cs)
|
||
}
|
||
if (_stuck || _asleep)
|
||
{
|
||
return; // замер как был
|
||
}
|
||
float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
|
||
Vector3 home = Home();
|
||
if (_t >= 0f && _stops.Count > 0)
|
||
{
|
||
_t += Time.deltaTime;
|
||
float acc = 0f;
|
||
Vector3 from = home;
|
||
foreach (Stop st in _stops)
|
||
{
|
||
if (_t < acc + st.Fly)
|
||
{
|
||
transform.localPosition = Vector3.Lerp(from, st.Hover, Mathf.SmoothStep(0f, 1f, (_t - acc) / st.Fly));
|
||
Face(st.Hover - from);
|
||
Gesture(0f);
|
||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||
return;
|
||
}
|
||
acc += st.Fly;
|
||
if (!st.Pass && _t < acc + WorkSeconds)
|
||
{
|
||
transform.localPosition = st.Hover;
|
||
Face(st.Look - st.Hover); // за работой - лицом к месту
|
||
Gesture(Mathf.Sin((_t - acc) / WorkSeconds * Mathf.PI));
|
||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||
return;
|
||
}
|
||
if (!st.Pass)
|
||
{
|
||
acc += WorkSeconds;
|
||
}
|
||
from = st.Hover;
|
||
}
|
||
if (_t < acc + _backFly)
|
||
{
|
||
transform.localPosition = Vector3.Lerp(from, home, Mathf.SmoothStep(0f, 1f, (_t - acc) / _backFly));
|
||
Face(home - from); // назад - лицом к урне
|
||
Gesture(0f);
|
||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||
return;
|
||
}
|
||
_t = -1f;
|
||
Gesture(0f);
|
||
}
|
||
if (Small)
|
||
{
|
||
transform.localPosition = home; // в кадиле, не виден
|
||
return;
|
||
}
|
||
_yaw = yawTarget % 360f;
|
||
float bob = CinerariumGenie.BobPeriod > 0.01f
|
||
? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight
|
||
: 0f;
|
||
transform.localPosition = home + new Vector3(0f, bob, 0f);
|
||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||
}
|
||
}
|
||
|
||
/// <summary>Подгонка джинна в игре: высота над урной, скорость поворота, покачивание, прозрачность. Не сохраняется -
|
||
/// найденные значения переносятся в константы CinerariumGenie.</summary>
|
||
[Preserve]
|
||
public class ConsoleCmdNecroGenie : ConsoleCmdAbstract
|
||
{
|
||
public override bool IsExecuteOnClient => true;
|
||
|
||
public override bool AllowedInMainMenu => false;
|
||
|
||
public override string[] getCommands()
|
||
{
|
||
return new string[] { "necrogenie" };
|
||
}
|
||
|
||
public override string getDescription()
|
||
{
|
||
return "Necromancer's Tome: tune the cinerarium spirit (height over the urn, turning, bobbing, transparency).";
|
||
}
|
||
|
||
public override string getHelp()
|
||
{
|
||
return "necrogenie - show the current values and how many spirits are on screen\n" +
|
||
"necrogenie y <m> - model root height above the urn block's bottom (tail tip is 0.47 m above the root)\n" +
|
||
"necrogenie fwd <m> - shift along the spirit's own forward (keeps the curled tail inside the neck)\n" +
|
||
"necrogenie turn <deg/s> - idle turning speed\n" +
|
||
"necrogenie bob <m> <s> - bobbing height and period\n" +
|
||
"necrogenie alpha <0-1> - body brightness (the body adds light, it is not alpha-blended)\n" +
|
||
"necrogenie glow <0-1> - body's own glow (visible at night)\n" +
|
||
"necrogenie tint <r> <g> <b> - body colour\n" +
|
||
"necrogenie arms <out> [elbow] - gesture arms, degrees: upper arm from vertical sideways, elbow bend (rest pose 'attention' is the model's own)\n" +
|
||
"necrogenie solid <0|1> - TEMPORARY: opaque body (1) to see the pose, or the ghostly glow (0)\n" +
|
||
"necrogenie sleep <m> - spirits and threads freeze when the player is farther (work goes on)\n" +
|
||
"necrogenie hide|show - hide all spirits (weak PCs, video), work goes on";
|
||
}
|
||
|
||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||
{
|
||
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
|
||
if (_params.Count == 1 && (_params[0] == "hide" || _params[0] == "show"))
|
||
{
|
||
CinerariumGenie.Hidden = _params[0] == "hide";
|
||
}
|
||
if (_params.Count >= 2 && float.TryParse(_params[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float v))
|
||
{
|
||
switch (_params[0].ToLowerInvariant())
|
||
{
|
||
case "y":
|
||
CinerariumGenie.OffsetY = v;
|
||
break;
|
||
case "fwd":
|
||
CinerariumGenie.Forward = v;
|
||
break;
|
||
case "turn":
|
||
CinerariumGenie.TurnSpeed = v;
|
||
break;
|
||
case "bob":
|
||
CinerariumGenie.BobHeight = v;
|
||
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float per))
|
||
{
|
||
CinerariumGenie.BobPeriod = per;
|
||
}
|
||
break;
|
||
case "alpha":
|
||
CinerariumGenie.Alpha = Mathf.Clamp01(v);
|
||
CinerariumGenie.ApplyAlpha();
|
||
break;
|
||
case "glow":
|
||
CinerariumGenie.Glow = Mathf.Clamp01(v);
|
||
CinerariumGenie.ApplyAlpha();
|
||
break;
|
||
case "sleep":
|
||
CinerariumGenie.SleepDistance = Mathf.Max(5f, v);
|
||
break;
|
||
case "arms":
|
||
CinerariumGenie.ArmOut = v;
|
||
if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float ae))
|
||
{
|
||
CinerariumGenie.Elbow = ae;
|
||
}
|
||
break;
|
||
case "solid":
|
||
CinerariumGenie.Solid = v != 0f;
|
||
CinerariumGenie.ApplyAlpha();
|
||
break;
|
||
case "tint":
|
||
if (_params.Count >= 4 &&
|
||
float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float g) &&
|
||
float.TryParse(_params[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float b))
|
||
{
|
||
CinerariumGenie.Tint = new Color(v, g, b, 1f);
|
||
CinerariumGenie.ApplyAlpha();
|
||
}
|
||
break;
|
||
default:
|
||
con.Output("necrogenie: unknown '" + _params[0] + "' - see 'help necrogenie'");
|
||
return;
|
||
}
|
||
}
|
||
con.Output("necrogenie: y " + CinerariumGenie.OffsetY.ToString("F2") + ", fwd " + CinerariumGenie.Forward.ToString("F2") + ", turn " + CinerariumGenie.TurnSpeed.ToString("F1") +
|
||
", bob " + CinerariumGenie.BobHeight.ToString("F2") + " / " + CinerariumGenie.BobPeriod.ToString("F1") + " s, alpha " +
|
||
CinerariumGenie.Alpha.ToString("F2") + ", glow " + CinerariumGenie.Glow.ToString("F2") + ", tint " +
|
||
CinerariumGenie.Tint.r.ToString("F2") + " " + CinerariumGenie.Tint.g.ToString("F2") + " " + CinerariumGenie.Tint.b.ToString("F2") + ", arms " + CinerariumGenie.ArmOut.ToString("F0") + " " +
|
||
CinerariumGenie.Elbow.ToString("F0") + ", sleep " + CinerariumGenie.SleepDistance.ToString("F0") + " m" + (CinerariumGenie.Hidden ? ", HIDDEN" : "") + "; spirits on screen: " + CinerariumGenie.Live.Count);
|
||
}
|
||
}
|
||
}
|