diff --git a/HarmonySrc/Cinerarium.cs b/HarmonySrc/Cinerarium.cs
index a5cb373..7511c83 100644
--- a/HarmonySrc/Cinerarium.cs
+++ b/HarmonySrc/Cinerarium.cs
@@ -148,6 +148,35 @@ namespace NecromancerTome
SpawnBounds();
}
SyncGenie(false);
+ Work(_world);
+ }
+
+ float _nextWork;
+ public bool Stuck;
+
+ /// Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере.
+ void Work(World _world)
+ {
+ if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer || Time.time < _nextWork)
+ {
+ return;
+ }
+ _nextWork = Time.time + CinerariumWork.StepSeconds;
+ Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck);
+ if (stuck != Stuck)
+ {
+ Stuck = stuck;
+ Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? " почернел - готовое класть некуда" : " ожил"));
+ }
+ CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null;
+ if (motion != null)
+ {
+ motion.SetStuck(Stuck && at == null);
+ if (at.HasValue)
+ {
+ motion.Visit(World.blockToTransformPos(at.Value) - Origin.position);
+ }
+ }
}
// ------------------------------------------------------------------
diff --git a/HarmonySrc/CinerariumGenie.cs b/HarmonySrc/CinerariumGenie.cs
index 7e7c6d9..e8761d5 100644
--- a/HarmonySrc/CinerariumGenie.cs
+++ b/HarmonySrc/CinerariumGenie.cs
@@ -139,6 +139,51 @@ namespace NecromancerTome
{
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()
{
@@ -154,11 +199,33 @@ namespace NecromancerTome
void Update()
{
- _yaw = (_yaw + CinerariumGenie.TurnSpeed * Time.deltaTime) % 360f;
+ if (_stuck)
+ {
+ return; // замер как был
+ }
+ Vector3 home = new Vector3(0f, CinerariumGenie.OffsetY, 0f);
+ float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
+ 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);
+ 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 = new Vector3(0f, CinerariumGenie.OffsetY + bob, 0f);
+ transform.localPosition = home + new Vector3(0f, bob, 0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
}
}
diff --git a/HarmonySrc/CinerariumWork.cs b/HarmonySrc/CinerariumWork.cs
new file mode 100644
index 0000000..cf34972
--- /dev/null
+++ b/HarmonySrc/CinerariumWork.cs
@@ -0,0 +1,271 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NecromancerTome
+{
+ ///
+ /// Работа духа урны - этап 4 некротехнолога (указание 16.10: дух - УНИВЕРСАЛЬНЫЙ работник зоны; BACKLOG «Некротехнолог v2»).
+ /// Раз в StepSeconds - одно дело, по порядку:
+ /// 1. пополнить расходники коллекторов из ящиков зоны: ячейки топлива (банки, цветы, корм, сферы - что коллектор
+ /// принимает, GetFuelTypes) и катализатора (куры - GetCatalystTypes; по одной в пустую ячейку). Модификации
+ /// коллекторов не трогаются;
+ /// 2. забрать готовое из коллекторов (Items) и станций (TileEntityWorkstation.Output - верстак, кузня, химстанция,
+ /// костёр, бетономешалка) и сложить в ящики зоны: сначала к таким же предметам, потом в свободное место; ящики полны -
+ /// в сумку урны; некуда - дух чернеет и замирает над урной, оживает сам, когда место появится;
+ /// 3. грядки - последними (ещё не сделано).
+ /// Коллектор или станция подходят любые - будущий крольчатник обслуживается тем же кодом. Ящик - составной блок игрока
+ /// с хранилищем (TEFeatureStorage, PlayerPlaced), кроме урн. Открытое у игрока (IsUserAccessing) не трогается.
+ /// Работает на сервере: зовётся из TEFeatureCinerarium.UpdateTick.
+ ///
+ public static class CinerariumWork
+ {
+ public static float StepSeconds = 3f;
+
+ /// Что в зоне: собирается заново на каждом шаге (150 клеток - дёшево, а ящики могут переставить).
+ sealed class Zone
+ {
+ public readonly List Boxes = new List();
+ public readonly List Collectors = new List();
+ public readonly List Stations = new List();
+ public readonly Dictionary Where = new Dictionary();
+ public TEFeatureStorage Bag;
+ }
+
+ static readonly HashSet _seen = new HashSet();
+
+ /// Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда.
+ public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck)
+ {
+ _stuck = false;
+ Zone z = Scan(_world, _urn);
+ // 1. Расходники.
+ foreach (TileEntityCollector c in z.Collectors)
+ {
+ if (Refill(c, c.FuelSlots, c.GetFuelTypes(), false, z) || Refill(c, c.CatalystSlots, c.GetCatalystTypes(), true, z))
+ {
+ c.SetModified();
+ return z.Where[c];
+ }
+ }
+ // 2. Готовое.
+ bool blocked = false;
+ foreach (TileEntityCollector c in z.Collectors)
+ {
+ ItemStack[] items = c.Items;
+ if (Unload(items, z, ref blocked))
+ {
+ c.Items = items;
+ c.SetModified();
+ return z.Where[c];
+ }
+ }
+ foreach (TileEntityWorkstation w in z.Stations)
+ {
+ ItemStack[] output = w.Output;
+ if (Unload(output, z, ref blocked))
+ {
+ w.Output = output;
+ return z.Where[w];
+ }
+ }
+ _stuck = blocked;
+ return null;
+ }
+
+ static Zone Scan(World _world, TEFeatureCinerarium _urn)
+ {
+ Zone z = new Zone();
+ _seen.Clear();
+ Vector3i c = _urn.ToWorldPos();
+ if (_urn.Parent != null)
+ {
+ _urn.Parent.TryGetSelfOrFeature(out z.Bag);
+ _seen.Add(_urn.Parent);
+ }
+ int w = TEFeatureCinerarium.HalfWidth;
+ for (int dy = -TEFeatureCinerarium.Below; dy <= TEFeatureCinerarium.Above; dy++)
+ {
+ for (int dx = -w; dx <= w; dx++)
+ {
+ for (int dz = -w; dz <= w; dz++)
+ {
+ Vector3i p = new Vector3i(c.x + dx, c.y + dy, c.z + dz);
+ BlockValue bv = _world.GetBlock(p);
+ if (bv.isair)
+ {
+ continue;
+ }
+ Vector3i owner = bv.ischild ? p + bv.parent : p; // часть составного блока (верстак 2x1) - к главной клетке
+ TileEntity te = _world.GetTileEntity(owner);
+ if (te == null || !_seen.Add(te) || te.IsUserAccessing())
+ {
+ continue;
+ }
+ if (te is TileEntityCollector col)
+ {
+ z.Collectors.Add(col);
+ z.Where[col] = owner;
+ }
+ else if (te is TileEntityWorkstation ws)
+ {
+ z.Stations.Add(ws);
+ z.Where[ws] = owner;
+ }
+ else if (te is TileEntityComposite comp && comp.PlayerPlaced &&
+ !comp.TryGetSelfOrFeature(out TEFeatureCinerarium _) &&
+ comp.TryGetSelfOrFeature(out TEFeatureStorage box))
+ {
+ z.Boxes.Add(box);
+ z.Where[comp] = owner;
+ }
+ }
+ }
+ }
+ return z;
+ }
+
+ /// Дополнить ячейки коллектора тем, что он принимает, из ящиков и сумки. _single - по одному предмету в
+ /// пустую ячейку (куры), иначе до полного стака. true - что-то положено.
+ static bool Refill(TileEntityCollector _c, ItemStack[] _slots, string[] _allowed, bool _single, Zone _z)
+ {
+ if (_slots == null || _allowed == null || _allowed.Length == 0)
+ {
+ return false;
+ }
+ for (int i = 0; i < _slots.Length; i++)
+ {
+ ItemStack slot = _slots[i];
+ ItemClass want = null;
+ int room;
+ if (slot == null || slot.IsEmpty())
+ {
+ room = -1; // любой допустимый
+ }
+ else
+ {
+ if (_single)
+ {
+ continue;
+ }
+ want = slot.itemValue.ItemClass;
+ room = want != null ? want.MaxCount - slot.count : 0;
+ if (room <= 0)
+ {
+ continue;
+ }
+ }
+ foreach (TEFeatureStorage src in Sources(_z))
+ {
+ for (int k = 0; k < src.ItemGrid.Length; k++)
+ {
+ ItemStack s = src.ItemGrid[k];
+ if (s == null || s.IsEmpty() || Locked(src, k))
+ {
+ continue;
+ }
+ ItemClass ic = s.itemValue.ItemClass;
+ if (ic == null || (want != null ? ic != want : System.Array.IndexOf(_allowed, ic.GetItemName()) < 0))
+ {
+ continue;
+ }
+ int take = _single ? 1 : (room < 0 ? Mathf.Min(s.count, ic.MaxCount) : Mathf.Min(s.count, room));
+ if (take <= 0)
+ {
+ continue;
+ }
+ if (want == null)
+ {
+ _slots[i] = new ItemStack(s.itemValue.Clone(), take);
+ }
+ else
+ {
+ _slots[i].count += take;
+ }
+ Take(src, k, take);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /// Одна стопка готового - в ящики (к таким же, потом в пустое), затем в сумку урны. true - что-то перенесено;
+ /// _blocked - была стопка, которой не нашлось места.
+ static bool Unload(ItemStack[] _items, Zone _z, ref bool _blocked)
+ {
+ if (_items == null)
+ {
+ return false;
+ }
+ for (int i = 0; i < _items.Length; i++)
+ {
+ ItemStack s = _items[i];
+ if (s == null || s.IsEmpty())
+ {
+ continue;
+ }
+ int before = s.count;
+ // Сначала ящики зоны (к таким же, потом в пустое), они полны - тогда сумка урны (решение 06.10).
+ Place(s, _z.Boxes);
+ if (s.count > 0 && _z.Bag != null)
+ {
+ Place(s, new[] { _z.Bag });
+ }
+ if (s.count == 0)
+ {
+ _items[i] = ItemStack.Empty.Clone();
+ return true;
+ }
+ if (s.count != before)
+ {
+ return true;
+ }
+ _blocked = true;
+ }
+ return false;
+ }
+
+ static void Place(ItemStack _s, IList _dst)
+ {
+ for (int i = 0; i < _dst.Count && _s.count > 0; i++)
+ {
+ _dst[i].TryStackItem(0, _s);
+ }
+ for (int i = 0; i < _dst.Count && _s.count > 0; i++)
+ {
+ if (_dst[i].AddItem(_s.Clone()))
+ {
+ _s.count = 0;
+ }
+ }
+ }
+
+ /// Откуда брать расходники: сумка урны (там то, что дух накопил), потом ящики зоны.
+ static IEnumerable Sources(Zone _z)
+ {
+ if (_z.Bag != null)
+ {
+ yield return _z.Bag;
+ }
+ foreach (TEFeatureStorage b in _z.Boxes)
+ {
+ yield return b;
+ }
+ }
+
+ /// Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.
+ static bool Locked(TEFeatureStorage _src, int _slot)
+ {
+ PackedBoolArray locks = _src.ItemGrid.SlotLocks;
+ return locks != null && _slot < locks.Length && locks[_slot];
+ }
+
+ static void Take(TEFeatureStorage _src, int _slot, int _count)
+ {
+ ItemStack s = _src.ItemGrid[_slot].Clone();
+ s.count -= _count;
+ _src.UpdateSlot(_slot, s.count > 0 ? s : ItemStack.Empty.Clone());
+ _src.SetModified();
+ }
+ }
+}
diff --git a/NecromancerHarmony.dll b/NecromancerHarmony.dll
index 05b38ea..fe9cead 100644
Binary files a/NecromancerHarmony.dll and b/NecromancerHarmony.dll differ