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; /// Сдвиг вперёд в системе самого духа, м (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: «Т-поза бесит»): плечо от вертикали вбок и вперёд, локоть согнут вперёд, градусы. /// Модель в позе покоя, аниматора нет - кости ставятся один раз при появлении. public static float ArmOut = 12f; public static float ArmFwd = 15f; public static float Elbow = 35f; 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; } public static GameObject Spawn(Transform _urnModel) { 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 = true; // скелет свой и неподвижный - границы не пересчитываются, не пропадать с краю экрана } 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); } g.AddComponent().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; } m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent; SetLook(m); _ghostMats[_src] = m; return m; } /// Холодный бледный оттенок духа (умножается на серую текстуру). public static Color Tint = new Color(0.55f, 0.75f, 1f, 1f); /// Своё свечение тела, доля от Tint. public static float Glow = 0.35f; static void SetLook(Material _m) { if (_m == null) { 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 class CinerariumGenieMotion : MonoBehaviour { float _yaw; float _phase; /// Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками /// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. ActDelay), /// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома. Vector3 _visit; float _t = -1f; public static float OutSeconds = 1.3f; public static float WorkSeconds = 1.6f; public static float BackSeconds = 1.3f; /// Через сколько после начала полёта дело выполняется - в середине жеста. public static float ActDelay => OutSeconds + WorkSeconds * 0.5f; /// Жест: плечи вперёд до 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; /// Опустить руки из позы покоя: направление плеча и предплечья задаётся в осях модели (вперёд - +Z), /// поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют. public void Pose() { Pose(CinerariumGenie.ArmFwd, CinerariumGenie.Elbow, CinerariumGenie.Elbow); } void Pose(float _fwd, 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]; } } 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 = _fwd * 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.FromToRotation(fore.position - up.position, transform.TransformDirection(upDir)) * up.rotation; fore.rotation = Quaternion.FromToRotation(hand.position - fore.position, transform.TransformDirection(foreDir)) * fore.rotation; } } /// Светящиеся материалы рендереров - вернуть после почернения. readonly Dictionary _glowing = new Dictionary(); /// Куда смотреть за работой (в координатах модели урны); _beside - дух стоит сбоку от места, а не над ним. Vector3 _look; bool _beside; /// Точки - в координатах сцены (без Origin): _stand - где встать (низ клетки), _look - низ клетки места работы. public void Visit(Vector3 _stand, Vector3 _lookAt, bool _besideIt) { if (transform.parent == null) { return; } _visit = transform.parent.InverseTransformPoint(_stand); _look = transform.parent.InverseTransformPoint(_lookAt); _beside = _besideIt; _t = 0f; } 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); } void Update() { if (_stuck) { return; // замер как был } float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime; Vector3 home = new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward); if (_t >= 0f) { _t += Time.deltaTime; if (_t < OutSeconds + WorkSeconds + BackSeconds) { // Сбоку - на уровне места (тело вровень со станцией), над местом - выше него. Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + (_beside ? 0.15f : 0.8f), 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward); Vector3 d = _visit - new Vector3(0f, CinerariumGenie.OffsetY, 0f); bool far = d.x * d.x + d.z * d.z > 0.01f; float toPlace = far ? Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg : _yaw; Vector3 dl = _look - _visit; float toLook = dl.x * dl.x + dl.z * dl.z > 0.01f ? Mathf.Atan2(dl.x, dl.z) * Mathf.Rad2Deg : toPlace; float k, face, w = 0f; if (_t < OutSeconds) { k = Mathf.SmoothStep(0f, 1f, _t / OutSeconds); face = toPlace; } else if (_t < OutSeconds + WorkSeconds) { k = 1f; face = toLook; // за работой - лицом к станции w = Mathf.Sin((_t - OutSeconds) / WorkSeconds * Mathf.PI); } else { k = 1f - Mathf.SmoothStep(0f, 1f, (_t - OutSeconds - WorkSeconds) / BackSeconds); face = far ? toPlace + 180f : _yaw; // назад - лицом к урне } _yaw = Mathf.MoveTowardsAngle(_yaw, face, 300f * Time.deltaTime) % 360f; transform.localPosition = Vector3.Lerp(home, over, k); transform.localRotation = Quaternion.Euler(0f, _yaw, 0f); if (w > 0f) { float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * w; Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, w), CinerariumGenie.Elbow + 15f * w + wave, CinerariumGenie.Elbow + 15f * w - wave); _gesturing = true; } else if (_gesturing) { Pose(); _gesturing = false; } return; } _t = -1f; if (_gesturing) { Pose(); _gesturing = false; } } _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 - arm pose, degrees: upper arm from vertical sideways and forward, elbow bend"; } public override void Execute(List _params, CommandSenderInfo _senderInfo) { SdtdConsole con = SingletonMonoBehaviour.Instance; 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 "arms": CinerariumGenie.ArmOut = v; if (_params.Count >= 3 && float.TryParse(_params[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float af)) { CinerariumGenie.ArmFwd = af; } if (_params.Count >= 4 && float.TryParse(_params[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float ae)) { CinerariumGenie.Elbow = ae; } foreach (CinerariumGenieMotion m in CinerariumGenie.Live) { m.Pose(); } 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.ArmFwd.ToString("F0") + " " + CinerariumGenie.Elbow.ToString("F0") + "; spirits on screen: " + CinerariumGenie.Live.Count); } } }