using System.Collections.Generic; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Дух-работник урны - «джинн из урны» (решение 16.10): кончик хвоста уходит в горлышко, дух вырастает над урной, /// без работы висит, чуть покачиваясь и медленно поворачиваясь. Этап 3 некротехнолога. /// /// Не сущность игры, а видимая модель - дочерний объект модели блока урны, как каркас зоны (решение 16.10 вместо /// плана 15.10 «сущность на префабе медсестры»): хитбокса, ИИ и сохранения у неё нет, поэтому «убить нельзя», /// «зомби не замечают», «сквозь стены» выходят сами, а сейвы дух не трогает - он появляется по черепу в урне /// (TEFeatureCinerarium.HasSkull) и пропадает без него. Сдвиги Origin и выгрузку чанка переживает с моделью урны. /// /// Модель - бандл necrotechnologist (NecroTechnologistSetup.cs): своя сетка на своём скелете NT_rig в позе покоя, /// глаза - отдельные рендереры (в имени «eye»). Тело - светящаяся дымка (16.10: Fade давал пятна; сложение One/One, /// холодный оттенок Tint, своё слабое свечение Glow); текстура уже серая (сделана под обесцвечивание). Глаза - свой /// непрозрачный светящийся материал из бандла, очередь отрисовки 3100 - после тела. /// public static class CinerariumGenie { public const string ModelPath = "#@modfolder(NecromancerTome):Resources/necrotechnologist?necroTechnologistPrefab.prefab"; /// Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»). /// Подгоняется командой necrogenie. public static float OffsetY = -0.15f; /// У урны рабочего горлышко ниже (земля в чаше на 0.45 м против 0.53 у урны сборщика) - дух ниже на столько же. public static float WorkerDrop = -0.08f; /// Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади» /// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе /// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd. public static float Forward = 0.07f; /// Поворот без работы, градусов в секунду. public static float TurnSpeed = 6f; /// Покачивание: размах (м) и период (с). public static float BobHeight = 0.04f; public static float BobPeriod = 4f; public static float Alpha = 0.3f; /// Засыпание (16.10): игрок дальше SleepDistance - дух замирает дома, огоньки нитей стоят. Работа идёт. public static float SleepDistance = 40f; public static bool Hidden; /// Далеко ли игрок от точки сцены (без Origin). Для засыпания духа и нитей. 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; } /// Руки в жесте: плечо от вертикали вбок, локоть согнут вперёд, градусы. Покой - «смирно» (18.10: «Всё Смирно и /// будет позой для бездельничающих некротехов») запечён в позу покоя модели (pose_attention.json при выгрузке FBX), /// аниматора нет - без жеста кости не трогаются. public static float ArmOut = 3f; public static float Elbow = 0f; static GameObject _prefab; static bool _tried; static readonly Dictionary _ghostMats = new Dictionary(); /// Все джинны на сцене - команда necrogenie правит их сразу. public static readonly List Live = new List(); static bool Load() { if (!_tried) { _tried = true; _prefab = DataLoader.LoadAsset(ModelPath); Debug.Log("[NecromancerTome] CinerariumGenie: модель " + (_prefab != null ? "загружена" : "НЕ загружена: " + ModelPath)); } return _prefab != null; } /// Дух логиста (18.10: «Пусть дух вылетает когда нужно что-то нести. Но уменьшим его в три раза»): размер. public static float LogistScale = 1f / 3f; /// Дом духа логиста в осях модели урны - корень модели духа внутри кадила, середина тела на высоте окон крышки /// (по центру - окна на 0.63 м, у стены - 0.73 м, кадило в 0.22 м от стены, стена - -z). public static Vector3 LogistHome = new Vector3(0f, 0.25f, 0f); public static Vector3 LogistWallHome = new Vector3(0f, 0.35f, -0.22f); /// Окна крышки - на расстоянии LogistExit от оси кадила (радиус крышки 0.21 м), по осям модели. public static float LogistExit = 0.3f; /// Лутер: сдвиг духа вперёд (подобран в Blender 18.10). public static float LootForward = 0.08f; /// _logist: 0 - обычный дух над горлышком, 1 - дух логиста в кадиле по центру, 2 - в кадиле у стены. 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(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(true)) { Object.Destroy(c); } CinerariumGenieMotion m = g.AddComponent(); m.Drop = _drop; if (_logist > 0) { m.Small = true; m.Wall = _logist == 2; g.transform.localScale = Vector3.one * LogistScale; } m.Pose(); return g; } /// Полупрозрачная копия материала тела - одна на исходный материал на всю игру. 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; } /// Холодный бледный оттенок духа (умножается на серую текстуру). public static Color Tint = new Color(0.55f, 0.75f, 1f, 1f); /// Своё свечение тела, доля от Tint. public static float Glow = 0.35f; /// ВРЕМЕННО (17.10: «Временно убери им прозрачность. А то плохо видно» - пока подбирается поза рук): дух /// непрозрачный, обычная заливка с записью глубины. Перед релизом вернуть false (BACKLOG). Консоль: necrogenie solid 0|1. 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 _stuckMats = new Dictionary(); /// Почерневший дух (класть некуда): свечением чёрное не нарисовать, поэтому обычная Fade-копия, тёмная. 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; } /// Исходный материал тела по его светящейся копии. public static Material Source(Material _ghost) { foreach (KeyValuePair p in _ghostMats) { if (p.Value == _ghost) { return p.Key; } } return _ghost; } } /// Покачивание и поворот джинна над урной. Только вид - никакой логики. [Preserve] public partial class CinerariumGenieMotion : MonoBehaviour { float _yaw; float _phase; /// Своя поправка высоты урны (CinerariumGenie.WorkerDrop у рабочего). public float Drop; /// Свой сдвиг вперёд (NaN - общий CinerariumGenie.Forward). Лутер (18.10, банка с крышкой-кадилом): 0.08 - хвост /// по центру отверстия на макушке («Вот сейчас было идеально»). public float ForwardOverride = float.NaN; /// Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками /// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit), /// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома. struct Stop { public Vector3 Hover, Look; // в осях модели урны public float Fly; public bool Pass; // точка пути (окно кадила) - без жеста } /// Дух логиста (18.10): втрое меньше, дома (в кадиле) не виден, вылетает и влетает через окно крышки. public bool Small; /// Кадило у стены: дом сдвинут к кронштейну, окно в стену (-z) не используется. public bool Wall; readonly List _stops = new List(); float _backFly; float _t = -1f; /// Скорость полёта, м/с: время перелёта - не меньше OutSeconds/BackSeconds, дальше - по расстоянию (нити до 15 м). public static float FlySpeed = 4f; public static float OutSeconds = 1.3f; public static float WorkSeconds = 1.6f; public static float BackSeconds = 1.3f; /// Жест: плечи вперёд до GestureFwd градусов, локти ходят на ±GestureWave вразнобой. public static float GestureFwd = 55f; public static float GestureWave = 30f; bool _gesturing; /// Почернел и замер (готовое класть некуда, решение 06.10). bool _stuck; /// Кости рук (плечо, предплечье, кисть по сторонам) и их поворот в позе покоя. Transform[] _arm; Quaternion[] _armRest; static bool _armLogged; /// Покой - руки в позе модели («смирно»). public void Pose() { Pose(0f, CinerariumGenie.Elbow, CinerariumGenie.Elbow); } /// Жест долей _w от покоя: плечо вперёд на GestureFwd, направление плеча и предплечья задаётся в осях модели /// (вперёд - +Z), поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют. 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(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; } } /// Светящиеся материалы рендереров - вернуть после почернения. readonly Dictionary _glowing = new Dictionary(); /// Облёт мест работы по порядку (у перевозки по нити - ящик «откуда», потом «куда»), у каждого - жест, затем /// назад к урне. Точки - в координатах сцены (без Origin): stand - где встать (низ клетки), look - низ клетки места, /// beside - встал сбоку, а не над местом. Возвращает, через сколько секунд дело (середина последнего жеста). 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; } /// Окно крышки кадила по направлению _dir (оси модели): одно из четырёх по осям x/z, у стены - не в стену. 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); } /// Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с. 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(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; void Update() { 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(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); } } /// Подгонка джинна в игре: высота над урной, скорость поворота, покачивание, прозрачность. Не сохраняется - /// найденные значения переносятся в константы CinerariumGenie. [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 - model root height above the urn block's bottom (tail tip is 0.47 m above the root)\n" + "necrogenie fwd - shift along the spirit's own forward (keeps the curled tail inside the neck)\n" + "necrogenie turn - idle turning speed\n" + "necrogenie bob - 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 - body colour\n" + "necrogenie arms [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 - 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 _params, CommandSenderInfo _senderInfo) { SdtdConsole con = SingletonMonoBehaviour.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); } } }