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