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