using System.Collections.Generic; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Урны с большой зоной - ремонтник и лутер (решения 17.10 и 18.10, BACKLOG «Урна ремонтника», «Урна лутера»). /// /// Общее. Зона - квадрат со стороной блока притязания (LandClaimSize сервера, по умолчанию 41) или меньше (31/21/11, пункт /// меню «Размер зоны»), по высоте - весь столб мира; зоны таких урн могут пересекаться. Работа - только по кнопке (17.10: «А /// если сделать рубильник или кнопку. Включил, ремонтник начал работу»): «Запустить» - один проход по зоне, конец прохода - /// снова ожидание; до нажатия урна ничего не считает. Кнопки - на самой урне с четырёх сторон («как у дверного звонка, но с /// черепом»), их цвет - состояние (CinerariumButtons): не горят - нет черепа; жёлтая ровно - ожидание; жёлтая мигает - не /// хватает материалов (ремонтник) или ящик полон (лутер); зелёная - работа; красная - прилив (работа стоит, дух в урне). /// /// Ремонтник. Обход - по столбам зоны, порциями из общего бюджета клеток на кадр (CellsPerFrame на все урны); в столбе - от /// низа до высоты чанка, земля мира (форма terrain) и воздух пропускаются сразу. Найденный повреждённый блок с RepairItems /// встаёт в очередь (не длиннее QueueMax - обход ждёт), блок берёт тот, кто нашёл первым (Claimed). Ремонт - неспешно: одно /// действие в RepairSeconds, дух летит к блоку и машет руками; материалы - RepairItems блока, доля по повреждению (как молотком), /// из ящиков с нитями к урне. Не хватает - кнопка мигает, урна ждёт (положил в ящик - продолжит сама). Разрушенное не /// восстанавливает - только повреждённое (17.10: «Восстанавливаю порушенное, затем включаю ремонтника и ухожу по делам»). /// /// Лутер. За действие - одна цель в зоне: предмет на земле, затем мешок с зомби, затем красный труп (тем же путём, что игрок: /// LootBagOpened + NecroSkull.Roll, стадия добычи - того, кто нажал кнопку); добыча - в ящик с любой нитью к урне. Ящики /// зданий, аэродроп и рюкзаки погибших игроков не трогает (18.10 «Принимаю всё»). Целей нет - проход окончен. /// public partial class TEFeatureCinerarium { public const string RepairBlockName = "necroCinerariumRepair"; public const string RepairSkullBlockName = "necroCinerariumRepairSkull"; public const string LootBlockName = "necroCinerariumLoot"; public const string LootSkullBlockName = "necroCinerariumLootSkull"; public static bool IsRepairBlock(Block _b) { string n = _b?.GetBlockName(); return n == RepairBlockName || n == RepairSkullBlockName; } public static bool IsLootBlock(Block _b) { string n = _b?.GetBlockName(); return n == LootBlockName || n == LootSkullBlockName; } bool _repair, _loot; public bool IsRepairer { get { _ = IsLogist; return _repair; } } public bool IsLooter { get { _ = IsLogist; return _loot; } } public bool IsBigZone => IsRepairer || IsLooter; /// Сторона зоны, выбранная в меню (0 - по притязанию). Сохраняется (v6). public int ZoneSide; public bool ZoneRunning; /// Обход ремонтника: номер следующего столба зоны (строка за строкой). public int ZoneCursor; /// Кто нажал «Запустить» - стадия добычи лутера. public int ZonePressedBy = -1; /// Не хватает материалов / ящик полон - кнопка мигает жёлтым. public bool ZoneLacking; public static float RepairSeconds = 2.5f; public static float LootSeconds = 1.5f; public static int CellsPerFrame = 4000; public static int QueueMax = 32; static int _cellsFrame = -1, _cellsUsed; /// Блоки, взятые ремонтниками в очередь (зоны пересекаются - чинит взявший первым). static readonly HashSet Claimed = new HashSet(); readonly List _queue = new List(); float _zoneNext; bool _zonePending; float _zoneActAt; Vector3i _zoneTarget; int _zoneTargetEntity = -1; /// Стороны зоны: притязание сервера и меньшие нечётные (31, 21, 11), не больше притязания. public static List ZoneSides() { int claim = Mathf.Max(11, GamePrefs.GetInt(EnumGamePrefs.LandClaimSize)); List l = new List { claim }; foreach (int s in new[] { 31, 21, 11 }) { if (s < claim) { l.Add(s); } } return l; } public int ZoneSideNow { get { List l = ZoneSides(); return ZoneSide > 0 && l.Contains(ZoneSide) ? ZoneSide : l[0]; } } public enum ButtonState { Off, Wait, Lacking, Busy, Tide } public ButtonState Buttons { get { if (!HasSkull) { return ButtonState.Off; } if (CinerariumLoad.Tide) { return ButtonState.Tide; } if (!ZoneRunning) { return ButtonState.Wait; } return ZoneLacking ? ButtonState.Lacking : ButtonState.Busy; } } string ZoneStateText() { string key = Buttons switch { ButtonState.Tide => "necroZoneUrnTide", ButtonState.Lacking => IsLooter ? "necroZoneUrnFull" : "necroZoneUrnNoMaterials", ButtonState.Busy => "necroZoneUrnBusy", ButtonState.Wait => "necroZoneUrnWait", _ => null }; string side = ZoneSideNow.ToString(); return (key != null ? ", " + Localization.Get(key) : "") + ", " + string.Format(Localization.Get("necroZoneUrnSize"), side); } /// Пункты меню на E: запустить, остановить, размер зоны (по кругу). public void ZoneCommand(string _cmd, EntityPlayerLocal _player) { if (_cmd.EndsWith("zurn_run")) { if (!HasSkull) { return; } ZoneRunning = true; ZoneCursor = 0; ZoneLacking = false; ZonePressedBy = _player != null ? _player.entityId : -1; _queue.Clear(); _lootDone.Clear(); Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4"); Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " запущен, зона " + ZoneSideNow); } else if (_cmd.EndsWith("zurn_stop")) { ZoneStop(true); Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4"); } else if (_cmd.EndsWith("zurn_size")) { List l = ZoneSides(); int i = l.IndexOf(ZoneSideNow); ZoneSide = l[(i + 1) % l.Count]; if (_player != null) { GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZoneUrnSize"), ZoneSide.ToString())); } if (ZoneShown) { RemoveBounds(); SpawnBounds(); } } SetModified(); } /// Остановить проход (кнопка, урна убрана): очередь и взятые блоки отпускаются. public void ZoneStop(bool _log) { foreach (Vector3i p in _queue) { Claimed.Remove(p); } _queue.Clear(); _zonePending = false; if (ZoneRunning && _log) { Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " остановлен"); } ZoneRunning = false; ZoneLacking = false; } void ZoneWork(World _world) { SyncButtons(); if (!ZoneRunning) { return; } CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null; if (_zonePending) { if (Time.time < _zoneActAt) { return; } _zonePending = false; if (IsRepairer) { RepairNow(_world, _zoneTarget); } else { LootNow(_world, _zoneTargetEntity); } return; } if (IsRepairer) { ScanColumns(_world); } if (Time.time < _zoneNext || !CinerariumLoad.TryTakeStep()) { return; } _zoneNext = Time.time + (IsRepairer ? RepairSeconds : LootSeconds); if (IsRepairer) { RepairPick(_world, motion); } else { LootPick(_world, motion); } } /// Слетать к месту и сделать дело по прилёте (как дух сборщика) или сразу, если духа нет. void FlyThenAct(World _world, CinerariumGenieMotion _motion, Vector3i _at) { _zonePending = true; _zoneActAt = Time.time; if (_motion != null) { List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)> { (Stand(_world, _at, out bool beside) - Origin.position, World.blockToTransformPos(_at) - Origin.position, beside) }; _zoneActAt = Time.time + _motion.Visit(stops); } } // ------------------------------------------------------------------ // Ремонтник. // ------------------------------------------------------------------ void ScanColumns(World _world) { if (_queue.Count >= QueueMax) { return; } if (Time.frameCount != _cellsFrame) { _cellsFrame = Time.frameCount; _cellsUsed = 0; } int side = ZoneSideNow, half = side / 2, total = side * side; Vector3i c = ToWorldPos(); while (ZoneCursor < total && _cellsUsed < CellsPerFrame && _queue.Count < QueueMax) { int x = c.x - half + ZoneCursor % side, z = c.z - half + ZoneCursor / side; ZoneCursor++; if (!(_world.GetChunkFromWorldPos(x, 0, z) is Chunk ch)) { continue; // не загружен - пропустить столб } int lx = World.toBlockXZ(x), lz = World.toBlockXZ(z); int top = Mathf.Min(254, ch.GetHeight(lx, lz) + 1); _cellsUsed += top; for (int y = 1; y <= top; y++) { BlockValue bv = ch.GetBlock(lx, y, lz); if (bv.isair || bv.ischild || bv.damage <= 0) { continue; } Block b = bv.Block; if (b == null || b.shape.IsTerrain() || b.RepairItems == null || b.RepairItems.Count == 0) { continue; } Vector3i p = new Vector3i(x, y, z); if (Claimed.Add(p)) { _queue.Add(p); } } } if (ZoneCursor >= total && _queue.Count == 0 && !_zonePending) { Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + c + " обошёл зону - ожидание"); ZoneStop(false); SetModified(); } } void RepairPick(World _world, CinerariumGenieMotion _motion) { while (_queue.Count > 0) { Vector3i p = _queue[0]; BlockValue bv = _world.GetBlock(p); if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null) { _queue.RemoveAt(0); Claimed.Remove(p); continue; // починили до нас или блок убран } if (!HasMaterials(_world, bv, false)) { if (!ZoneLacking) { Debug.Log("[NecromancerTome] Cinerarium: ремонтнику " + ToWorldPos() + " не хватает материалов для " + bv.Block.GetBlockName() + " в " + p); } ZoneLacking = true; return; // ждём: положат в ящик - продолжим с того же блока } ZoneLacking = false; _zoneTarget = p; FlyThenAct(_world, _motion, p); return; } } void RepairNow(World _world, Vector3i _p) { _queue.Remove(_p); Claimed.Remove(_p); BlockValue bv = _world.GetBlock(_p); if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null) { return; } if (!HasMaterials(_world, bv, true)) { ZoneLacking = true; _queue.Insert(0, _p); Claimed.Add(_p); return; } int was = bv.damage; bv.damage = 0; _world.SetBlockRPC(_p, bv); Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + ToWorldPos() + " починил " + bv.Block.GetBlockName() + " в " + _p + " (было повреждение " + was + ")"); } /// Хватает ли в ящиках с нитями к урне материалов на ремонт (RepairItems блока, доля по повреждению, не меньше /// одного); _take - сразу и забрать. bool HasMaterials(World _world, BlockValue _bv, bool _take) { Block b = _bv.Block; float share = Mathf.Clamp01(_bv.damage / (float)Mathf.Max(1, b.MaxDamage)); List boxes = ThreadBoxes(_world); foreach (Block.SItemNameCount need in b.RepairItems) { ItemValue iv = ItemClass.GetItem(need.ItemName); if (iv == null || iv.type == 0) { continue; } int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share)); int have = 0; foreach (TEFeatureStorage s in boxes) { have += Count(s, iv); } if (have < n) { return false; } } if (!_take) { return true; } foreach (Block.SItemNameCount need in b.RepairItems) { ItemValue iv = ItemClass.GetItem(need.ItemName); if (iv == null || iv.type == 0) { continue; } int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share)); foreach (TEFeatureStorage s in boxes) { n -= TakeFrom(s, iv, n); if (n <= 0) { break; } } } return true; } static int Count(TEFeatureStorage _s, ItemValue _iv) { int n = 0; for (int i = 0; i < _s.ItemGrid.Length; i++) { ItemStack st = _s.ItemGrid[i]; if (st != null && !st.IsEmpty() && st.itemValue.type == _iv.type && !CinerariumWork.Locked(_s, i)) { n += st.count; } } return n; } static int TakeFrom(TEFeatureStorage _s, ItemValue _iv, int _n) { int took = 0; for (int i = 0; i < _s.ItemGrid.Length && took < _n; i++) { ItemStack st = _s.ItemGrid[i]; if (st == null || st.IsEmpty() || st.itemValue.type != _iv.type || CinerariumWork.Locked(_s, i)) { continue; } int k = Mathf.Min(st.count, _n - took); CinerariumWork.Take(_s, i, k); took += k; } return took; } /// Ящики урны по нитям: у ремонтника - «откуда» (материалы), у лутера - любая нить (куда класть). List ThreadBoxes(World _world) { List l = new List(); void Add(Vector3i _p) { TEFeatureStorage s = CinerariumWork.BoxAt(_world, _p); if (s != null && !l.Contains(s)) { l.Add(s); } } foreach (Vector3i p in ThreadFrom) { Add(p); } if (IsLooter) { if (HasThreadTo) { Add(ThreadTo); } foreach (Vector3i p in ThreadOut) { Add(p); } } return l; } // ------------------------------------------------------------------ // Лутер. // ------------------------------------------------------------------ static readonly List _ents = new List(); /// Цели, уже обысканные за этот проход (пустой мешок, нечего взять) - второй раз не трогать. readonly HashSet _lootDone = new HashSet(); /// Цель лутера в зоне: предмет на земле, потом мешок с зомби, потом красный труп. Ничего - проход окончен. void LootPick(World _world, CinerariumGenieMotion _motion) { List boxes = ThreadBoxes(_world); if (boxes.Count == 0) { ZoneLacking = true; // некуда класть - как полный ящик return; } int side = ZoneSideNow; Vector3 c = World.blockToTransformPos(ToWorldPos()); Bounds bb = new Bounds(new Vector3(c.x, 128f, c.z), new Vector3(side, 256f, side)); Entity best = null; int bestRank = int.MaxValue; _ents.Clear(); _world.GetEntitiesInBounds(typeof(Entity), bb, _ents); foreach (Entity e in _ents) { int rank = LootRank(e); if (rank < bestRank && !_lootDone.Contains(e.entityId) && !LockManager.Instance.IsLockedServer(e, 0)) { best = e; bestRank = rank; } } _ents.Clear(); if (best == null) { Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " собрал всё в зоне - ожидание"); ZoneStop(false); SetModified(); return; } ZoneLacking = false; _zoneTargetEntity = best.entityId; FlyThenAct(_world, _motion, World.worldToBlockPos(best.GetPosition())); } /// Что собирать и в каком порядке (меньше - раньше); int.MaxValue - не трогать. Рюкзаки игроков (EntityBackpack) и /// аэродроп (EntitySupplyCrate) - не трогать. static int LootRank(Entity _e) { if (_e == null || _e.IsDespawned) { return int.MaxValue; } if (_e is EntityBackpack || _e is EntitySupplyCrate) { return int.MaxValue; } if (_e is EntityLootContainer lc) { return lc.bag != null ? 1 : int.MaxValue; } if (_e is EntityItem it && !(_e is EntityContainerAbs)) { return it.itemStack != null && !it.itemStack.IsEmpty() ? 0 : int.MaxValue; } if (_e is EntityAlive a && a.IsDead() && CorpseLoot.IsSearchable(a) && a.bag != null) { return 2; } return int.MaxValue; } void LootNow(World _world, int _id) { Entity e = _world.GetEntity(_id); if (e == null || LootRank(e) == int.MaxValue || LockManager.Instance.IsLockedServer(e, 0)) { return; } List boxes = ThreadBoxes(_world); if (e is EntityItem it && !(e is EntityContainerAbs)) { ItemStack s = it.itemStack.Clone(); int before = s.count; CinerariumWork.Place(s, boxes); if (s.count == before) { ZoneLacking = true; return; } if (s.count <= 0) { _world.RemoveEntity(e.entityId, EnumRemoveEntityReason.Despawned); } else { it.itemStack = s; } Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " подобрал " + s.itemValue.ItemClass?.GetItemName() + " x" + (before - s.count)); return; } Bag bag = e is EntityAlive a ? a.bag : (e as EntityLootContainer)?.bag; if (bag == null) { return; } if (!bag.Touched) { // Как игрок: первое открытие наполняет сумку по списку добычи (стадия - нажавшего кнопку); у красного трупа - и // находка черепа (CorpseLootPatch: OnLockRequestServer). int who = ZonePressedBy >= 0 ? ZonePressedBy : (_world.GetPrimaryPlayer()?.entityId ?? -1); GameManager.Instance.lootManager.LootBagOpened(bag, e, who); if (e is EntityAlive corpse) { NecroSkull.Roll(corpse, bag); } } bool moved = false, left = false; ItemStackGrid slots = bag.ItemGrid; for (int i = 0; i < slots.Length; i++) { if (slots[i] == null || slots[i].IsEmpty()) { continue; } ItemStack s = slots[i].Clone(); int before = s.count; CinerariumWork.Place(s, boxes); if (s.count != before) { moved = true; } if (s.count > 0) { left = true; } bag.SetSlot(i, s.count > 0 ? s : ItemStack.Empty.Clone()); } ZoneLacking = left; // не всё влезло - ящик полон, кнопка мигает if (!left) { _lootDone.Add(e.entityId); // опустошён (или был пуст) - больше не цель; пустой убирает игра } Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " обыскал " + e.GetType().Name + " " + e.entityId + (left ? " - не всё влезло" : "")); } // ------------------------------------------------------------------ // Кнопки на модели. // ------------------------------------------------------------------ void SyncButtons() { Transform t = ModelTransform(); if (t == null) { return; } CinerariumButtons b = t.GetComponent(); if (b == null) { b = t.gameObject.AddComponent(); } b.State = Buttons; } } /// Кнопки-звонки с черепом на урне ремонтника/лутера: рендереры модели с «button» в имени. Цвет свечения - по /// состоянию урны (TEFeatureCinerarium.Buttons), жёлтая мигает - нехватка. [Preserve] public class CinerariumButtons : MonoBehaviour { public TEFeatureCinerarium.ButtonState State; List _buttons; static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock(); static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor"); public static Color Yellow = new Color(2.2f, 1.6f, 0.15f); public static Color Green = new Color(0.3f, 2.2f, 0.4f); public static Color Red = new Color(2.4f, 0.15f, 0.1f); Color _last = new Color(-1f, 0f, 0f); public static bool IsButton(Renderer _r) => _r != null && _r.name.ToLowerInvariant().Contains("button"); void Update() { if (_buttons == null) { _buttons = new List(); foreach (Renderer r in GetComponentsInChildren(true)) { if (IsButton(r)) { _buttons.Add(r); } } } Color c; switch (State) { case TEFeatureCinerarium.ButtonState.Wait: c = Yellow; break; case TEFeatureCinerarium.ButtonState.Lacking: c = Mathf.Repeat(Time.time, 0.8f) < 0.4f ? Yellow : Color.black; break; case TEFeatureCinerarium.ButtonState.Busy: c = Green; break; case TEFeatureCinerarium.ButtonState.Tide: c = Red; break; default: c = Color.black; break; } if (c == _last) { return; } _last = c; foreach (Renderer r in _buttons) { if (r == null) { continue; } r.GetPropertyBlock(_block); _block.SetColor(EmissionColorId, c); r.SetPropertyBlock(_block); } } } }