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»). Тело - полупрозрачное, как у торговцев-призраков (alpha 0.3, Standard /// в режиме Fade, как у крысы при проходе); текстура уже серая (сделана под обесцвечивание). Глаза - свой /// непрозрачный светящийся материал из бандла, очередь отрисовки 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; 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(); 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; } 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", 0); m.DisableKeyword("_ALPHATEST_ON"); m.EnableKeyword("_ALPHABLEND_ON"); m.DisableKeyword("_ALPHAPREMULTIPLY_ON"); m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent; SetAlpha(m, Alpha); _ghostMats[_src] = m; return m; } static void SetAlpha(Material _m, float _a) { if (_m != null && _m.HasProperty("_Color")) { Color c = _m.color; c.a = _a; _m.color = c; } } public static void ApplyAlpha() { foreach (Material m in _ghostMats.Values) { SetAlpha(m, Alpha); } } } /// Покачивание и поворот джинна над урной. Только вид - никакой логики. [Preserve] public class CinerariumGenieMotion : MonoBehaviour { float _yaw; float _phase; /// Полёт к месту работы и обратно: точка в координатах модели урны и сколько осталось. Vector3 _visit; float _visitLeft; public const float VisitSeconds = 2.2f; /// Почернел и замер (готовое класть некуда, решение 06.10). bool _stuck; static readonly MaterialPropertyBlock _mpb = new MaterialPropertyBlock(); static readonly int ColorId = Shader.PropertyToID("_Color"); /// _worldUnity - точка в координатах сцены (без Origin): низ блока, где дух работал. public void Visit(Vector3 _worldUnity) { if (transform.parent == null) { return; } _visit = transform.parent.InverseTransformPoint(_worldUnity); _visitLeft = VisitSeconds; } public void SetStuck(bool _on) { if (_stuck == _on) { return; } _stuck = _on; foreach (Renderer r in GetComponentsInChildren(true)) { if (r.name.Contains("eye")) { continue; } if (_on) { r.GetPropertyBlock(_mpb); _mpb.SetColor(ColorId, new Color(0.04f, 0.035f, 0.045f, Mathf.Max(CinerariumGenie.Alpha, 0.6f))); r.SetPropertyBlock(_mpb); } else { r.SetPropertyBlock(null); } } } 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 (_visitLeft > 0f) { // Туда и обратно по синусу: на полпути - над местом работы, лицом к нему. _visitLeft -= Time.deltaTime; float k = Mathf.Sin(Mathf.Clamp01(1f - _visitLeft / VisitSeconds) * Mathf.PI); Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + 0.6f, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward); transform.localPosition = Vector3.Lerp(home, over, k); Vector3 d = _visit - home; if (d.x * d.x + d.z * d.z > 0.01f) { yawTarget = Mathf.LerpAngle(_yaw, Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg, Time.deltaTime * 4f); } _yaw = yawTarget % 360f; transform.localRotation = Quaternion.Euler(0f, _yaw, 0f); 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 opacity"; } 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; 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") + "; spirits on screen: " + CinerariumGenie.Live.Count); } } }