diff --git a/HarmonySrc/Cinerarium.cs b/HarmonySrc/Cinerarium.cs index 6f74096..a5cb373 100644 --- a/HarmonySrc/Cinerarium.cs +++ b/HarmonySrc/Cinerarium.cs @@ -69,6 +69,7 @@ namespace NecromancerTome { base.OnRemove(_world); RemoveBounds(); + RemoveGenie(false); XUiC_NecroCinerariumSkullGrid.CloseIfShowing(this); if (!ConnectionManager.Instance.IsServer) { @@ -94,6 +95,7 @@ namespace NecromancerTome { base.OnUnload(_world); RemoveBounds(); + RemoveGenie(false); XUiC_NecroCinerariumSkullGrid.CloseIfShowing(this); } @@ -145,6 +147,7 @@ namespace NecromancerTome { SpawnBounds(); } + SyncGenie(false); } // ------------------------------------------------------------------ @@ -160,6 +163,7 @@ namespace NecromancerTome } HasSkull = _has; SetModified(); + SyncGenie(true); Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут")); } @@ -202,6 +206,44 @@ namespace NecromancerTome return true; } + // ------------------------------------------------------------------ + // Джинн из урны (CinerariumGenie.cs): есть череп - есть дух. + // ------------------------------------------------------------------ + + GameObject _genie; + public const string GenieSound = "necroTechGust"; + + /// _sound - череп только что положили или вынули (порыв); при загрузке чанка дух появляется молча. + public void SyncGenie(bool _sound) + { + if (HasSkull && _genie == null) + { + _genie = CinerariumGenie.Spawn(ModelTransform()); + if (_genie != null && _sound) + { + Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound); + } + } + else if (!HasSkull && _genie != null) + { + RemoveGenie(_sound); + } + } + + void RemoveGenie(bool _sound) + { + if (_genie == null) + { + return; + } + UnityEngine.Object.Destroy(_genie); + _genie = null; + if (_sound) + { + Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound); + } + } + // ------------------------------------------------------------------ // Границы зоны: каркас параллелепипеда, как у земельного камня. Дочерний объект модели блока - сдвиги // Origin и выгрузка чанка его не теряют (как кольцо пирамиды); материал один на все урны. diff --git a/HarmonySrc/CinerariumGenie.cs b/HarmonySrc/CinerariumGenie.cs new file mode 100644 index 0000000..7e7c6d9 --- /dev/null +++ b/HarmonySrc/CinerariumGenie.cs @@ -0,0 +1,228 @@ +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"; + + /// Высота корня модели над низом блока урны, м (кончик хвоста в модели - на 0.45 м). Подгоняется командой necrogenie. + public static float OffsetY = -0.25f; + /// Поворот без работы, градусов в секунду. + 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; + + void OnEnable() + { + CinerariumGenie.Live.Add(this); + _phase = Random.value * 10f; + _yaw = Random.value * 360f; + } + + void OnDisable() + { + CinerariumGenie.Live.Remove(this); + } + + void Update() + { + _yaw = (_yaw + CinerariumGenie.TurnSpeed * Time.deltaTime) % 360f; + float bob = CinerariumGenie.BobPeriod > 0.01f + ? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight + : 0f; + transform.localPosition = new Vector3(0f, CinerariumGenie.OffsetY + 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.45 m above the root)\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 "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") + ", 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); + } + } +} diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll index 8c6ca6b..05b38ea 100644 Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ