using System.Collections.Generic; using UnityEngine; namespace NecromancerTome { /// /// Работа духа урны - этап 4 некротехнолога (указание 16.10: дух - УНИВЕРСАЛЬНЫЙ работник зоны; BACKLOG «Некротехнолог v2»). /// Раз в StepSeconds - одно дело, по порядку: /// 1. пополнить расходники коллекторов из ящиков зоны: ячейки топлива (банки, цветы, корм, сферы - что коллектор /// принимает, GetFuelTypes) и катализатора (куры - GetCatalystTypes; по одной в пустую ячейку). Модификации /// коллекторов не трогаются; /// 2. забрать готовое из коллекторов (Items) и станций (TileEntityWorkstation.Output - верстак, кузня, химстанция, /// костёр, бетономешалка) и сложить в ящики зоны: сначала к таким же предметам, потом в свободное место; ящики полны - /// в сумку урны; некуда - дух чернеет и замирает над урной, оживает сам, когда место появится; /// 3. грядки - последними: собрать созревшее (блок с меткой crops и добычей Harvest) с «идеальными бонусами» /// (06.10: все бонусные доли урожая и семя - всегда), семя сразу посадить на то же место; затем засадить пустые /// грядки случайными семенами из ящиков и сумки (семя - предмет-блок стадии 1, BlockPlantGrowing; можно ли сажать, /// решает сам блок семени - CanPlaceBlockAt проверяет плодородие под ним, то есть грядку). /// Коллектор или станция подходят любые - будущий крольчатник обслуживается тем же кодом. Ящик - составной блок игрока /// с хранилищем (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; public readonly List Ripe = new List(); public readonly List Empty = new List(); } static readonly FastTags CropTag = FastTags.Parse("crops"); static readonly GameRandom _rnd = GameRandomManager.Instance.CreateGameRandom(); static readonly HashSet _seen = new HashSet(); /// Пробный шаг: найти дело и место, ничего не меняя. static bool _dry; /// Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда. /// _dryRun - только узнать, где будет дело (дух летит туда заранее), ничего не трогая. public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck, bool _dryRun) { _dry = _dryRun; try { return DoStep(_world, _urn, out _stuck); } finally { _dry = false; } } /// У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null. public static Vector3i? LastFrom; static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck) { _stuck = false; LastFrom = null; // Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»). if (_urn.IsLogist) { return Ferry(_world, _urn); } 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)) { if (!_dry) { 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)) { if (!_dry) { 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)) { if (!_dry) { w.Output = output; } return z.Where[w]; } } // 3. Грядки. foreach (Vector3i p in z.Ripe) { if (Harvest(_world, p, z, ref blocked)) { return p; } } if (z.Empty.Count > 0) { Vector3i? planted = PlantRandom(_world, z); if (planted.HasValue) { return planted; } } _stuck = blocked; return null; } /// Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». Только предметы включённых в урне групп /// (фильтр логиста, CinerariumGroups; 16.10 заменил прежнее правило «по образцу»); «куда» полон - ждём (дух не чернеет). Открытые у игрока ящики и закреплённые /// ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято. static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn) { if (!_urn.HasThreadTo) { return null; } TEFeatureStorage dst = BoxAt(_world, _urn.ThreadTo); if (dst == null) { return null; } foreach (Vector3i f in _urn.ThreadFrom) { if (f == _urn.ThreadTo) { continue; } TEFeatureStorage src = BoxAt(_world, f); if (src == null) { continue; } for (int k = 0; k < src.ItemGrid.Length; k++) { ItemStack s = src.ItemGrid[k]; if (s == null || s.IsEmpty() || Locked(src, k) || !_urn.GroupOn(CinerariumGroups.Of(s.itemValue)) || !Fits(dst, s)) { continue; } LastFrom = f; if (_dry) { return _urn.ThreadTo; } ItemStack moving = s.Clone(); Place(moving, new[] { dst }); int moved = s.count - moving.count; if (moved <= 0) { continue; } Take(src, k, moved); dst.SetModified(); Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + s.itemValue.ItemClass?.GetItemName() + " из " + f + " в " + _urn.ThreadTo); return _urn.ThreadTo; } } return null; } static TEFeatureStorage BoxAt(World _world, Vector3i _p) { return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() && !c.TryGetSelfOrFeature(out TEFeatureCinerarium _) && c.TryGetSelfOrFeature(out TEFeatureStorage s) ? s : null; } static bool IsRipe(Block _b) { return _b != null && !(_b is BlockPlantGrowing) && _b.HasAnyFastTags(CropTag) && _b.itemsToDrop.TryGetValue(EnumDropEvent.Harvest, out List l) && l != null && l.Count > 0; } static bool IsSeed(ItemValue _iv, out BlockValue _bv) { _bv = BlockValue.Air; if (_iv == null || _iv.type == 0 || !_iv.ItemClass.IsBlock()) { return false; } _bv = _iv.ToBlockValue(); return _bv.Block is BlockPlantGrowing; } /// Урожай: все доли добычи Harvest (бонусы - тоже, вероятность только у обычных долей), множители сервера на /// урожай и семена. Одно семя - сразу в землю на то же место, остальное - в ящики/сумку. Нет места ни для чего - /// грядку не трогаем, дух чернеет. static bool Harvest(World _world, Vector3i _p, Zone _z, ref bool _blocked) { Block crop = _world.GetBlock(_p).Block; List got = new List(); BlockValue replant = BlockValue.Air; foreach (Block.SItemDropProb d in crop.itemsToDrop[EnumDropEvent.Harvest]) { ItemValue iv = ItemClass.GetItem(d.name); if (iv == null || iv.type == 0) { continue; } bool seed = IsSeed(iv, out BlockValue seedBv); bool bonus = d.tag != null && (d.tag.Contains("Bonus") || d.tag.Contains("bonus")); if (!bonus && d.prob < 1f && _rnd.RandomFloat > d.prob) { continue; } int n = _rnd.RandomRange(d.minCount, d.maxCount + 1); n = Mathf.RoundToInt(n * (seed ? XUiM_Recipes.SeedDropOutputModifier : XUiM_Recipes.CropOutputModifier)); if (seed && replant.isair && n > 0) { replant = seedBv; n--; } if (n > 0) { got.Add(new ItemStack(iv, n)); } } if (got.Count > 0 && !HasRoom(got[0], _z)) { _blocked = true; return false; } if (_dry) { return true; } _world.SetBlockRPC(_p, BlockValue.Air); if (!replant.isair) { replant.rotation = (byte)_rnd.RandomRange(0, 4); _world.SetBlockRPC(_p, replant); } foreach (ItemStack s in got) { Place(s, _z.Boxes); if (s.count > 0 && _z.Bag != null) { Place(s, new[] { _z.Bag }); } if (s.count > 0) { // Влезло не всё (проверяли только первую долю) - остаток падает у урны, не пропадает. GameManager.Instance.ItemDropServer(s.Clone(), World.blockToTransformPos(_p) + Vector3.up * 0.5f, Vector3.zero); } } Debug.Log("[NecromancerTome] Cinerarium: дух собрал " + crop.GetBlockName() + " в " + _p + (replant.isair ? "" : ", посадил снова")); return true; } static bool HasRoom(ItemStack _s, Zone _z) { foreach (TEFeatureStorage t in _z.Boxes) { if (Fits(t, _s)) { return true; } } return _z.Bag != null && Fits(_z.Bag, _s); } static bool Fits(TEFeatureStorage _t, ItemStack _s) { for (int i = 0; i < _t.ItemGrid.Length; i++) { ItemStack x = _t.ItemGrid[i]; if (x == null || x.IsEmpty()) { return true; } int n = _s.count; if (x.itemValue.type == _s.itemValue.type && x.CanStackPartly(ref n)) { return true; } } return false; } /// Засадить одну пустую грядку случайным семенем из сумки и ящиков (06.10: «случайно сажает имеющиеся семена»). static Vector3i? PlantRandom(World _world, Zone _z) { List<(TEFeatureStorage src, int slot, BlockValue bv)> seeds = new List<(TEFeatureStorage, int, BlockValue)>(); foreach (TEFeatureStorage src in Sources(_z)) { for (int k = 0; k < src.ItemGrid.Length; k++) { ItemStack st = src.ItemGrid[k]; if (st != null && !st.IsEmpty() && !Locked(src, k) && IsSeed(st.itemValue, out BlockValue bv)) { seeds.Add((src, k, bv)); } } } if (seeds.Count == 0) { return null; } foreach (Vector3i p in _z.Empty) { var pick = seeds[_rnd.RandomRange(0, seeds.Count)]; BlockValue bv = pick.bv; bv.rotation = (byte)_rnd.RandomRange(0, 4); if (!bv.Block.CanPlaceBlockAt(_world, p, bv)) { continue; } if (_dry) { return p; } Take(pick.src, pick.slot, 1); _world.SetBlockRPC(p, bv); Debug.Log("[NecromancerTome] Cinerarium: дух посадил " + bv.Block.GetBlockName() + " в " + p); return p; } 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) { // Пустая клетка над плодородным блоком - кандидат в грядку (окончательно решит блок семени). if (_world.GetBlock(p - Vector3i.up).Block?.blockMaterial?.FertileLevel > 0) { z.Empty.Add(p); } continue; } if (!bv.ischild && IsRipe(bv.Block)) { z.Ripe.Add(p); 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 (_dry) { return true; } 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; } if (_dry) { if (HasRoom(s, _z)) { return true; } _blocked = true; 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(); } } }