Files
necromants-tome-7d2d-3-2/HarmonySrc/CinerariumZone.cs
T

717 lines
23 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Урны с большой зоной - ремонтник и лутер (решения 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 «Принимаю всё»). Целей нет - проход окончен.
/// </summary>
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;
/// <summary>Сторона зоны, выбранная в меню (0 - по притязанию). Сохраняется (v6).</summary>
public int ZoneSide;
public bool ZoneRunning;
/// <summary>Обход ремонтника: номер следующего столба зоны (строка за строкой).</summary>
public int ZoneCursor;
/// <summary>Кто нажал «Запустить» - стадия добычи лутера.</summary>
public int ZonePressedBy = -1;
/// <summary>Не хватает материалов / ящик полон - кнопка мигает жёлтым.</summary>
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;
/// <summary>Блоки, взятые ремонтниками в очередь (зоны пересекаются - чинит взявший первым).</summary>
static readonly HashSet<Vector3i> Claimed = new HashSet<Vector3i>();
readonly List<Vector3i> _queue = new List<Vector3i>();
float _zoneNext;
bool _zonePending;
float _zoneActAt;
Vector3i _zoneTarget;
int _zoneTargetEntity = -1;
/// <summary>Стороны зоны: притязание сервера и меньшие нечётные (31, 21, 11), не больше притязания.</summary>
public static List<int> ZoneSides()
{
int claim = Mathf.Max(11, GamePrefs.GetInt(EnumGamePrefs.LandClaimSize));
List<int> l = new List<int> { claim };
foreach (int s in new[] { 31, 21, 11 })
{
if (s < claim)
{
l.Add(s);
}
}
return l;
}
public int ZoneSideNow
{
get
{
List<int> 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);
}
/// <summary>Пункты меню на E: запустить, остановить, размер зоны (по кругу).</summary>
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<int> 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();
}
/// <summary>Остановить проход (кнопка, урна убрана): очередь и взятые блоки отпускаются.</summary>
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<CinerariumGenieMotion>() : 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(ToWorldPos()))
{
return;
}
_zoneNext = Time.time + (IsRepairer ? RepairSeconds : LootSeconds);
if (IsRepairer)
{
RepairPick(_world, motion);
}
else
{
LootPick(_world, motion);
}
}
/// <summary>Слетать к месту и сделать дело по прилёте (как дух сборщика) или сразу, если духа нет.</summary>
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 + ")");
}
/// <summary>Хватает ли в ящиках с нитями к урне материалов на ремонт (RepairItems блока, доля по повреждению, не меньше
/// одного); _take - сразу и забрать.</summary>
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<TEFeatureStorage> 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;
}
/// <summary>Ящики урны по нитям: у ремонтника - «откуда» (материалы), у лутера - любая нить (куда класть).</summary>
List<TEFeatureStorage> ThreadBoxes(World _world)
{
List<TEFeatureStorage> l = new List<TEFeatureStorage>();
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<Entity> _ents = new List<Entity>();
/// <summary>Цели, уже обысканные за этот проход (пустой мешок, нечего взять) - второй раз не трогать.</summary>
readonly HashSet<int> _lootDone = new HashSet<int>();
/// <summary>Цель лутера в зоне: предмет на земле, потом мешок с зомби, потом красный труп. Ничего - проход окончен.</summary>
void LootPick(World _world, CinerariumGenieMotion _motion)
{
List<TEFeatureStorage> 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()));
}
/// <summary>Что собирать и в каком порядке (меньше - раньше); int.MaxValue - не трогать. Рюкзаки игроков (EntityBackpack) и
/// аэродроп (EntitySupplyCrate) - не трогать.</summary>
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<TEFeatureStorage> 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<CinerariumButtons>();
if (b == null)
{
b = t.gameObject.AddComponent<CinerariumButtons>();
}
b.State = Buttons;
}
}
/// <summary>Кнопки-звонки с черепом на урне ремонтника/лутера: рендереры модели с «button» в имени. Цвет свечения - по
/// состоянию урны (TEFeatureCinerarium.Buttons), жёлтая мигает - нехватка.</summary>
[Preserve]
public class CinerariumButtons : MonoBehaviour
{
public TEFeatureCinerarium.ButtonState State;
List<Renderer> _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<Renderer>();
foreach (Renderer r in GetComponentsInChildren<Renderer>(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);
}
}
}
}