1125 lines
43 KiB
C#
1125 lines
43 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using HarmonyLib;
|
||
using Platform;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Урна-цинерарий (Harvest Cinerarium), этап 2 некротехнолога (BACKLOG «Некротехнолог v2», решения 06.10,
|
||
/// 15.10, 16.10). Составной блок игры, как пирамида и земельный камень: TEFeatureStorage игры - сумка духа
|
||
/// (обычное окно хранилища), эта деталь - череп, зона и меню на E. Решение 16.10 «урна как транспорт»: на E
|
||
/// круговое меню, как у велосипеда - «Багажник» (ячейка черепа, CinerariumTrunk.cs), сумка (хранилище игры),
|
||
/// зона. Есть ли в урне череп - поле детали с сохранением (CinerariumWriteHelper, версия первым байтом).
|
||
///
|
||
/// Поднятая урна держит череп, как транспорт - модификации: игра при подъёме данных блока в предмет не
|
||
/// переносит (Block.TakeItemWithTimerDone -> BlockValue.ToItemValue, только тип), поэтому два блока -
|
||
/// necroCinerarium (пустая) и necroCinerariumSkull (с черепом); подъём отдаёт тот, что соответствует черепу,
|
||
/// постановка второго кладёт череп сразу.
|
||
///
|
||
/// Зона работника - 5x5 по горизонтали с урной в центре, по высоте от -1 до +4 блока (06.10). Зоны урн не
|
||
/// пересекаются, урн у игрока не больше ceil(убийств / 500) - обе проверки при постановке
|
||
/// (CanPlaceBlockAt), по реестру урн сейва (CinerariumRegistry).
|
||
///
|
||
/// Имена команд не должны кончаться на zone_show/zone_hide/effect_on/effect_off: заплатка пирамиды на
|
||
/// BlockCompositeTileEntity.OnBlockActivated ловит их по EndsWith у ЛЮБОГО составного блока.
|
||
/// </summary>
|
||
public class TEFeatureCinerarium : TEFeatureAbs
|
||
{
|
||
public const string BlockName = "necroCinerarium";
|
||
public const string SkullBlockName = "necroCinerariumSkull";
|
||
/// <summary>Урна-логист (16.10: «Я не могу ставить урны рядом. Может нам для логиста вообще сделать отдельные урны?»):
|
||
/// без зоны - ставится вплотную к любым урнам, только нити и перевозка, глазницы всегда зелёные. Лимит - общий.</summary>
|
||
public const string LogistBlockName = "necroCinerariumLogist";
|
||
public const string LogistSkullBlockName = "necroCinerariumLogistSkull";
|
||
|
||
public static bool IsLogistBlock(Block _b)
|
||
{
|
||
string n = _b?.GetBlockName();
|
||
return n == LogistBlockName || n == LogistSkullBlockName;
|
||
}
|
||
public const string SkullItemName = "necroSkull";
|
||
public const int HalfWidth = 2;
|
||
public const int Below = 1;
|
||
public const int Above = 4;
|
||
public const float KillsPerUrn = 500f;
|
||
const byte SaveVersion = 3; // 2 - нити, 3 - фильтр логиста (16.10)
|
||
|
||
public bool HasSkull;
|
||
public bool ZoneShown;
|
||
/// <summary>Нити (CinerariumThreads.cs): ящики «откуда» и ящик «куда», главные клетки блоков. Есть нити - урна-логист.</summary>
|
||
public readonly List<Vector3i> ThreadFrom = new List<Vector3i>();
|
||
public bool HasThreadTo;
|
||
public Vector3i ThreadTo;
|
||
public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0;
|
||
/// <summary>Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.</summary>
|
||
public bool IsLogist
|
||
{
|
||
get
|
||
{
|
||
if (!_logistKnown)
|
||
{
|
||
World w = GameManager.Instance?.World;
|
||
BlockValue bv = w != null ? w.GetBlock(ToWorldPos()) : BlockValue.Air;
|
||
if (IsUrn(bv.Block))
|
||
{
|
||
_logist = IsLogistBlock(bv.Block);
|
||
_logistKnown = true;
|
||
}
|
||
}
|
||
return _logist;
|
||
}
|
||
}
|
||
bool _logist, _logistKnown;
|
||
/// <summary>Фильтр логиста: бит на группу CinerariumGroups; по умолчанию все включены.</summary>
|
||
public uint GroupMask = CinerariumGroups.All;
|
||
|
||
public bool GroupOn(int _g) => (GroupMask & (1u << _g)) != 0;
|
||
|
||
public void ToggleGroup(int _g)
|
||
{
|
||
GroupMask ^= 1u << _g;
|
||
SetModified();
|
||
Debug.Log("[NecromancerTome] Cinerarium: фильтр урны " + ToWorldPos() + " - группа " + _g + (GroupOn(_g) ? " включена" : " выключена"));
|
||
}
|
||
|
||
public override void CopyFromInternal(TileEntityComposite _other)
|
||
{
|
||
if (_other.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium other))
|
||
{
|
||
HasSkull = other.HasSkull;
|
||
ZoneShown = other.ZoneShown;
|
||
ThreadFrom.Clear();
|
||
ThreadFrom.AddRange(other.ThreadFrom);
|
||
HasThreadTo = other.HasThreadTo;
|
||
ThreadTo = other.ThreadTo;
|
||
GroupMask = other.GroupMask;
|
||
_threadsDirty = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>Блок поставлен (OnBlockAdded составного блока; на загрузке чанка не зовётся).</summary>
|
||
public override void OnAdded(Vector3i _blockPos, BlockValue _blockValue)
|
||
{
|
||
base.OnAdded(_blockPos, _blockValue);
|
||
_logist = IsLogistBlock(_blockValue.Block);
|
||
_logistKnown = true;
|
||
string placed = _blockValue.Block?.GetBlockName();
|
||
if (placed == SkullBlockName || placed == LogistSkullBlockName)
|
||
{
|
||
HasSkull = true; // поставили урну, поднятую с черепом
|
||
}
|
||
if (ConnectionManager.Instance.IsServer)
|
||
{
|
||
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist);
|
||
}
|
||
}
|
||
|
||
/// <summary>Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает.</summary>
|
||
public override void OnRemove(World _world)
|
||
{
|
||
base.OnRemove(_world);
|
||
RemoveBounds();
|
||
RemoveThreadView();
|
||
RemoveGenie(false);
|
||
CinerariumContents.CloseIfShowing(this);
|
||
if (!ConnectionManager.Instance.IsServer)
|
||
{
|
||
return;
|
||
}
|
||
Vector3i pos = ToWorldPos();
|
||
CinerariumRegistry.Remove(pos);
|
||
// Подняли целиком - череп уехал вместе с урной (necroCinerariumSkull), не выбрасывать.
|
||
bool pickedUp = Patch_Block_TakeItemWithTimerDone_Cinerarium.WasPickedUp(pos);
|
||
if (HasSkull && _world != null && !pickedUp)
|
||
{
|
||
HasSkull = false;
|
||
ItemValue skull = ItemClass.GetItem(SkullItemName);
|
||
if (skull != null && skull.type > 0)
|
||
{
|
||
_world.gameManager.ItemDropServer(new ItemStack(skull, 1), World.blockToTransformPos(pos) + Vector3.up * 0.7f, Vector3.zero);
|
||
Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - череп выпал");
|
||
}
|
||
}
|
||
}
|
||
|
||
public override void OnUnload(World _world)
|
||
{
|
||
base.OnUnload(_world);
|
||
RemoveBounds();
|
||
RemoveThreadView();
|
||
_threadsDirty = true; // чанк вернётся - нити построить заново
|
||
RemoveGenie(false);
|
||
CinerariumContents.CloseIfShowing(this);
|
||
}
|
||
|
||
public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode)
|
||
{
|
||
base.Write(_bw, _eStreamMode);
|
||
CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown);
|
||
List<int> t = new List<int> { ThreadFrom.Count };
|
||
foreach (Vector3i p in ThreadFrom)
|
||
{
|
||
t.Add(p.x); t.Add(p.y); t.Add(p.z);
|
||
}
|
||
t.Add(HasThreadTo ? 1 : 0);
|
||
t.Add(ThreadTo.x); t.Add(ThreadTo.y); t.Add(ThreadTo.z);
|
||
CinerariumWriteHelper.WriteInts(_bw, t.ToArray());
|
||
CinerariumWriteHelper.WriteInts(_bw, new[] { unchecked((int)GroupMask) });
|
||
}
|
||
|
||
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
|
||
{
|
||
base.Read(_br, _eStreamMode);
|
||
byte version = _br.ReadByte();
|
||
HasSkull = _br.ReadBoolean();
|
||
ZoneShown = _br.ReadBoolean();
|
||
ThreadFrom.Clear();
|
||
HasThreadTo = false;
|
||
if (version >= 2)
|
||
{
|
||
int n = _br.ReadInt32();
|
||
int[] v = new int[n];
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
v[i] = _br.ReadInt32();
|
||
}
|
||
int k = 0;
|
||
int from = n > 0 ? v[k++] : 0;
|
||
for (int i = 0; i < from && k + 2 < n; i++, k += 3)
|
||
{
|
||
ThreadFrom.Add(new Vector3i(v[k], v[k + 1], v[k + 2]));
|
||
}
|
||
if (k + 3 < n)
|
||
{
|
||
HasThreadTo = v[k] != 0;
|
||
ThreadTo = new Vector3i(v[k + 1], v[k + 2], v[k + 3]);
|
||
}
|
||
}
|
||
GroupMask = CinerariumGroups.All;
|
||
if (version >= 3)
|
||
{
|
||
int n = _br.ReadInt32();
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
int v = _br.ReadInt32();
|
||
if (i == 0)
|
||
{
|
||
GroupMask = unchecked((uint)v);
|
||
}
|
||
}
|
||
}
|
||
_threadsDirty = true;
|
||
if (version > SaveVersion)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] Cinerarium: урна из более новой версии мода (" + version + ") - прочитано только известное");
|
||
}
|
||
}
|
||
|
||
/// <summary>Меню как у велосипеда (EntityVehicle.InitLocalActivationCommands): «skull» - «Содержимое» (ячейка черепа; 16.10: «вместо пиктограммы
|
||
/// багажника... хотя бы череп» - ui_game_symbol_skull, иконка = ui_game_symbol_<имя>, XUiC_Radial), «wrench» - починить, «frames» - границы, как у земельного камня. Сумку даёт
|
||
/// TEFeatureStorage, подъём - TEFeaturePickup («Take»), обе детали игры. «Сумка» (16.10: «переименуй в Сумка») -
|
||
/// своя команда с иконкой мешка, как «Хранилище» у транспорта: открывает то же хранилище командой
|
||
/// TEFeatureStorage:Search (составной блок выполняет команду по имени, не глядя на enabled), а «Обыскать» игры у
|
||
/// урны спрятан. Стоит первой - короткое E открывает сумку.</summary>
|
||
public override void InitBlockActivationCommands(Action<BlockActivationCommand, TileEntityComposite.EBlockCommandOrder, TileEntityFeatureData> _addCallback)
|
||
{
|
||
base.InitBlockActivationCommands(_addCallback);
|
||
_addCallback(new BlockActivationCommand("bag", "loot_sack", _enabled: false), TileEntityComposite.EBlockCommandOrder.First, FeatureData);
|
||
_addCallback(new BlockActivationCommand("trunk", "skull", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
|
||
_addCallback(new BlockActivationCommand("urn_repair", "wrench", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
|
||
_addCallback(new BlockActivationCommand("bounds_show", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
|
||
_addCallback(new BlockActivationCommand("bounds_hide", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
|
||
}
|
||
|
||
public override string GetActivationText(WorldBase _world, Vector3i _blockPos, BlockValue _blockValue, EntityAlive _entityFocusing, string _activateHotkeyMarkup, string _focusedTileEntityName)
|
||
{
|
||
base.GetActivationText(_world, _blockPos, _blockValue, _entityFocusing, _activateHotkeyMarkup, _focusedTileEntityName);
|
||
return _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " +
|
||
Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty");
|
||
}
|
||
|
||
public override void UpdateTick(World _world)
|
||
{
|
||
base.UpdateTick(_world);
|
||
if (ZoneShown && _bounds == null)
|
||
{
|
||
SpawnBounds();
|
||
}
|
||
SyncGenie(false);
|
||
SyncThreads(_world);
|
||
Work(_world);
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Нити (CinerariumThreads.cs).
|
||
// ------------------------------------------------------------------
|
||
|
||
bool _threadsDirty = true;
|
||
GameObject _threadView;
|
||
float _nextThreadCheck;
|
||
|
||
public bool AddThreadFrom(Vector3i _box)
|
||
{
|
||
if (ThreadFrom.Contains(_box))
|
||
{
|
||
return true;
|
||
}
|
||
if (ThreadFrom.Count >= CinerariumThreads.MaxFrom)
|
||
{
|
||
return false;
|
||
}
|
||
ThreadFrom.Add(_box);
|
||
ThreadsChanged();
|
||
return true;
|
||
}
|
||
|
||
public void SetThreadTo(Vector3i _box)
|
||
{
|
||
HasThreadTo = true;
|
||
ThreadTo = _box;
|
||
ThreadsChanged();
|
||
}
|
||
|
||
public int ClearThreads()
|
||
{
|
||
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0);
|
||
ThreadFrom.Clear();
|
||
HasThreadTo = false;
|
||
if (n > 0)
|
||
{
|
||
ThreadsChanged();
|
||
}
|
||
return n;
|
||
}
|
||
|
||
public int RemoveThreadsTo(Vector3i _box)
|
||
{
|
||
int n = ThreadFrom.RemoveAll(p => p == _box);
|
||
if (HasThreadTo && ThreadTo == _box)
|
||
{
|
||
HasThreadTo = false;
|
||
n++;
|
||
}
|
||
if (n > 0)
|
||
{
|
||
ThreadsChanged();
|
||
}
|
||
return n;
|
||
}
|
||
|
||
void ThreadsChanged()
|
||
{
|
||
_threadsDirty = true;
|
||
_glowState = -1; // логист - другие глаза
|
||
SetModified();
|
||
Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет"));
|
||
}
|
||
|
||
/// <summary>Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.</summary>
|
||
void SyncThreads(World _world)
|
||
{
|
||
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsLogist && _logistKnown)
|
||
{
|
||
Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у урны-логиста");
|
||
ThreadFrom.Clear();
|
||
HasThreadTo = false;
|
||
ThreadsChanged();
|
||
}
|
||
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && Time.time >= _nextThreadCheck)
|
||
{
|
||
_nextThreadCheck = Time.time + 2f;
|
||
bool gone = false;
|
||
foreach (Vector3i p in ThreadFrom.ToArray())
|
||
{
|
||
if (!CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u) || u != null)
|
||
{
|
||
if (_world.GetChunkFromWorldPos(p) != null) // чанк не загружен - не снимать
|
||
{
|
||
ThreadFrom.Remove(p);
|
||
gone = true;
|
||
}
|
||
}
|
||
}
|
||
if (HasThreadTo && (!CinerariumThreads.Resolve(_world, ThreadTo, out Vector3i _, out TEFeatureCinerarium u2) || u2 != null) &&
|
||
_world.GetChunkFromWorldPos(ThreadTo) != null)
|
||
{
|
||
HasThreadTo = false;
|
||
gone = true;
|
||
}
|
||
if (gone)
|
||
{
|
||
ThreadsChanged();
|
||
}
|
||
}
|
||
if (!_threadsDirty && (_threadView != null || !HasThreads))
|
||
{
|
||
return;
|
||
}
|
||
Transform t = ModelTransform();
|
||
if (t == null)
|
||
{
|
||
return;
|
||
}
|
||
_threadsDirty = false;
|
||
SyncGlow();
|
||
if (!HasThreads)
|
||
{
|
||
RemoveThreadView();
|
||
return;
|
||
}
|
||
if (_threadView == null)
|
||
{
|
||
_threadView = new GameObject("NecroCinerariumThreads");
|
||
_threadView.transform.SetParent(t, false);
|
||
_threadView.AddComponent<CinerariumThreadView>();
|
||
}
|
||
Vector3i me = ToWorldPos();
|
||
List<(Vector3 a, Vector3 b)> pairs = new List<(Vector3, Vector3)>();
|
||
foreach (Vector3i p in ThreadFrom)
|
||
{
|
||
pairs.Add((CinerariumThreads.BoxPoint(p, me), CinerariumThreads.UrnPoint(me)));
|
||
}
|
||
if (HasThreadTo)
|
||
{
|
||
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me)));
|
||
}
|
||
_threadView.GetComponent<CinerariumThreadView>().Build(pairs);
|
||
}
|
||
|
||
void RemoveThreadView()
|
||
{
|
||
if (_threadView != null)
|
||
{
|
||
UnityEngine.Object.Destroy(_threadView);
|
||
_threadView = null;
|
||
}
|
||
}
|
||
|
||
float _nextWork;
|
||
public bool Stuck;
|
||
/// <summary>Дело найдено пробным шагом, дух полетел; выполнить в _actAt (16.10: «делал действие не сразу»).</summary>
|
||
bool _pending;
|
||
float _actAt;
|
||
|
||
/// <summary>Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере.
|
||
/// Сначала пробный шаг (ничего не меняет) - куда лететь; само дело - когда дух долетел и машет руками. Если за это
|
||
/// время что-то поменялось, настоящий шаг сделает то, что нужно на тот момент.</summary>
|
||
void Work(World _world)
|
||
{
|
||
if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer)
|
||
{
|
||
_pending = false;
|
||
return;
|
||
}
|
||
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
|
||
if (_pending)
|
||
{
|
||
if (Time.time < _actAt)
|
||
{
|
||
return;
|
||
}
|
||
_pending = false;
|
||
CinerariumWork.Step(_world, this, out bool _, false);
|
||
_nextWork = Time.time + CinerariumWork.StepSeconds;
|
||
return;
|
||
}
|
||
if (Time.time < _nextWork)
|
||
{
|
||
return;
|
||
}
|
||
_nextWork = Time.time + CinerariumWork.StepSeconds;
|
||
Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck, true);
|
||
if (stuck != Stuck)
|
||
{
|
||
Stuck = stuck;
|
||
Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? " почернел - готовое класть некуда" : " ожил"));
|
||
}
|
||
if (motion != null)
|
||
{
|
||
motion.SetStuck(Stuck && at == null);
|
||
}
|
||
if (at.HasValue)
|
||
{
|
||
_pending = true;
|
||
_actAt = Time.time;
|
||
if (motion != null)
|
||
{
|
||
Vector3i? from = CinerariumWork.LastFrom;
|
||
List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)>();
|
||
if (from.HasValue)
|
||
{
|
||
stops.Add((Stand(_world, from.Value, out bool b0) - Origin.position, World.blockToTransformPos(from.Value) - Origin.position, b0));
|
||
}
|
||
stops.Add((Stand(_world, at.Value, out bool beside) - Origin.position, World.blockToTransformPos(at.Value) - Origin.position, beside));
|
||
_actAt = Time.time + motion.Visit(stops);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Где духу встать у места работы (16.10: «влетает в станцию в самый центр... логичнее к краю станции с
|
||
/// доступной стороны»): из четырёх соседних клеток - свободная (воздух или растение), ближайшая к урне; дух встаёт у
|
||
/// её края со стороны места. Свободной нет - над местом. Возвращает низ точки в координатах мира (blockToTransformPos).</summary>
|
||
Vector3 Stand(World _world, Vector3i _at, out bool _beside)
|
||
{
|
||
Vector3i urn = ToWorldPos();
|
||
Vector3i best = _at;
|
||
int bestD = int.MaxValue;
|
||
foreach (Vector3i dir in new[] { Vector3i.forward, Vector3i.back, Vector3i.left, Vector3i.right })
|
||
{
|
||
Vector3i n = _at + dir;
|
||
BlockValue bv = _world.GetBlock(n);
|
||
if (!bv.isair && !(bv.Block is BlockPlant))
|
||
{
|
||
continue;
|
||
}
|
||
int d = (n.x - urn.x) * (n.x - urn.x) + (n.z - urn.z) * (n.z - urn.z);
|
||
if (d < bestD)
|
||
{
|
||
bestD = d;
|
||
best = n;
|
||
}
|
||
}
|
||
_beside = best != _at;
|
||
Vector3 at = World.blockToTransformPos(_at);
|
||
if (!_beside)
|
||
{
|
||
return at;
|
||
}
|
||
Vector3 n0 = World.blockToTransformPos(best);
|
||
return Vector3.Lerp(n0, at, 0.2f); // чуть ближе к месту - у самого края
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Череп.
|
||
// ------------------------------------------------------------------
|
||
|
||
/// <summary>Череп положен в ячейку багажника или вынут из неё.</summary>
|
||
public void SetSkull(bool _has)
|
||
{
|
||
if (HasSkull == _has)
|
||
{
|
||
return;
|
||
}
|
||
HasSkull = _has;
|
||
SetModified();
|
||
SyncGenie(true);
|
||
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут"));
|
||
}
|
||
|
||
/// <summary>«Починить», как у транспорта: целиком, за RepairBones костей из рюкзака (сколько берёт и молоток -
|
||
/// RepairItems в blocks.xml).</summary>
|
||
public const string RepairItemName = "resourceBone";
|
||
public const int RepairBones = 5;
|
||
|
||
public static int BonesCarried(EntityPlayerLocal _player)
|
||
{
|
||
ItemValue bone = ItemClass.GetItem(RepairItemName);
|
||
if (_player == null || bone == null || bone.type <= 0)
|
||
{
|
||
return 0;
|
||
}
|
||
return (_player.inventory?.GetItemCount(bone) ?? 0) + (_player.bag?.GetItemCount(bone) ?? 0);
|
||
}
|
||
|
||
public static bool Repair(World _world, Vector3i _pos, EntityPlayerLocal _player)
|
||
{
|
||
BlockValue bv = _world.GetBlock(_pos);
|
||
ItemValue bone = ItemClass.GetItem(RepairItemName);
|
||
if (bv.damage <= 0 || bone == null || bone.type <= 0 || BonesCarried(_player) < RepairBones)
|
||
{
|
||
return false;
|
||
}
|
||
int need = RepairBones;
|
||
if (_player.bag != null)
|
||
{
|
||
need -= _player.bag.DecItem(bone, need);
|
||
}
|
||
if (need > 0 && _player.inventory != null)
|
||
{
|
||
need -= _player.inventory.DecItem(bone, need);
|
||
}
|
||
bv.damage = 0;
|
||
_world.SetBlockRPC(_pos, bv);
|
||
_player.PlayOneShot("crafting/craft_repair_item", true);
|
||
Debug.Log("[NecromancerTome] Cinerarium: урна " + _pos + " починена");
|
||
return true;
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Джинн из урны (CinerariumGenie.cs): есть череп - есть дух.
|
||
// ------------------------------------------------------------------
|
||
|
||
GameObject _genie;
|
||
public const string GenieSound = "necroTechGust";
|
||
|
||
/// <summary>_sound - череп только что положили или вынули (порыв); при загрузке чанка дух появляется молча.</summary>
|
||
public void SyncGenie(bool _sound)
|
||
{
|
||
SyncGlow();
|
||
if (HasSkull && _genie == null)
|
||
{
|
||
_genie = CinerariumGenie.Spawn(ModelTransform());
|
||
if (_genie != null && _sound)
|
||
{
|
||
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
|
||
}
|
||
}
|
||
else if (!HasSkull && _genie != null)
|
||
{
|
||
RemoveGenie(_sound);
|
||
}
|
||
}
|
||
|
||
/// <summary>Руны кровью и глазницы черепов светятся, только когда в урне череп (16.10): _EmissionColor через
|
||
/// MaterialPropertyBlock на рендерерах модели урны (материал общий не трогается), духа не задевает.</summary>
|
||
public static Color GlowColor = new Color(2.2f, 2.2f, 2.2f, 1f);
|
||
static readonly MaterialPropertyBlock _glowBlock = new MaterialPropertyBlock();
|
||
static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
|
||
int _glowState = -1; // -1 - не ставили на эту модель
|
||
|
||
void SyncGlow()
|
||
{
|
||
Transform t = ModelTransform();
|
||
// Логист (16.10: «Так сразу будет видно что логистика, а что технолог») - всегда своя текстура свечения (руны и
|
||
// глазницы зелёные) и зеленоватая глазурь; светится, как и обычная урна, только с черепом.
|
||
int want = (HasSkull ? 1 : 0) + (IsLogist ? 2 : 0);
|
||
Texture logist = IsLogist ? LogistEmission() : null;
|
||
if (t == null || (_glowState == want && _glowModel == t))
|
||
{
|
||
return;
|
||
}
|
||
foreach (Renderer r in t.GetComponentsInChildren<Renderer>(true))
|
||
{
|
||
if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform) ||
|
||
_threadView != null && r.transform.IsChildOf(_threadView.transform))
|
||
{
|
||
continue;
|
||
}
|
||
r.GetPropertyBlock(_glowBlock);
|
||
_glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black);
|
||
if (logist != null)
|
||
{
|
||
_glowBlock.SetTexture(EmissionMapId, logist);
|
||
_glowBlock.SetColor(AlbedoColorId, LogistTint);
|
||
}
|
||
r.SetPropertyBlock(_glowBlock);
|
||
}
|
||
_glowState = want;
|
||
_glowModel = t;
|
||
}
|
||
|
||
/// <summary>Глазурь урны-логиста - множитель цвета материала (чёрная глазурь с зелёным отливом).</summary>
|
||
public static Color LogistTint = new Color(0.55f, 1f, 0.75f, 1f);
|
||
static readonly int AlbedoColorId = Shader.PropertyToID("_Color");
|
||
static readonly int EmissionMapId = Shader.PropertyToID("_EmissionMap");
|
||
static Texture2D _logistTex;
|
||
static bool _logistTried;
|
||
public const string LogistTextureFile = "necroCinerarium_e_logist.png";
|
||
|
||
/// <summary>Свечение урны-логиста (16.10: «пусть глаза черепов светятся другим цветом. Зелёным»): копия запечённой
|
||
/// necroCinerarium_e с зелёными глазницами (руны - те же), из папки Resources мода - бандл не пересобирается.</summary>
|
||
static Texture2D LogistEmission()
|
||
{
|
||
if (!_logistTried)
|
||
{
|
||
_logistTried = true;
|
||
try
|
||
{
|
||
string path = Path.Combine(ModEntry.Instance.Path, "Resources", LogistTextureFile);
|
||
Texture2D tex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
|
||
if (tex.LoadImage(File.ReadAllBytes(path)))
|
||
{
|
||
tex.name = "necroCinerarium_e_logist";
|
||
tex.anisoLevel = 4;
|
||
_logistTex = tex;
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] Cinerarium: свечение логиста не загружено (" + e.Message + ")");
|
||
}
|
||
}
|
||
return _logistTex;
|
||
}
|
||
|
||
Transform _glowModel;
|
||
|
||
void RemoveGenie(bool _sound)
|
||
{
|
||
if (_genie == null)
|
||
{
|
||
return;
|
||
}
|
||
UnityEngine.Object.Destroy(_genie);
|
||
_genie = null;
|
||
if (_sound)
|
||
{
|
||
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Границы зоны: каркас параллелепипеда, как у земельного камня. Дочерний объект модели блока - сдвиги
|
||
// Origin и выгрузка чанка его не теряют (как кольцо пирамиды); материал один на все урны.
|
||
// ------------------------------------------------------------------
|
||
|
||
GameObject _bounds;
|
||
static Material _boundsMat;
|
||
|
||
Transform ModelTransform()
|
||
{
|
||
World world = GameManager.Instance?.World;
|
||
if (world == null)
|
||
{
|
||
return null;
|
||
}
|
||
Vector3i pos = ToWorldPos();
|
||
Chunk chunk = world.GetChunkFromWorldPos(pos) as Chunk;
|
||
BlockEntityData be = chunk?.GetBlockEntity(pos);
|
||
return be != null && be.bHasTransform ? be.transform : null;
|
||
}
|
||
|
||
public void SpawnBounds()
|
||
{
|
||
if (_bounds != null)
|
||
{
|
||
return;
|
||
}
|
||
Transform t = ModelTransform();
|
||
if (t == null)
|
||
{
|
||
return;
|
||
}
|
||
_bounds = new GameObject("NecroCinerariumBounds");
|
||
_bounds.transform.SetParent(t, false);
|
||
LineRenderer lr = _bounds.AddComponent<LineRenderer>();
|
||
lr.useWorldSpace = false;
|
||
lr.widthMultiplier = 0.06f;
|
||
if (_boundsMat == null)
|
||
{
|
||
_boundsMat = new Material(Shader.Find("Sprites/Default")) { name = "NecroCinerariumBounds" };
|
||
}
|
||
lr.sharedMaterial = _boundsMat;
|
||
Color col = new Color(0.55f, 0.9f, 0.75f, 0.85f);
|
||
lr.startColor = col;
|
||
lr.endColor = col;
|
||
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
lr.receiveShadows = false;
|
||
|
||
// Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4.
|
||
Vector3 c = World.blockToTransformPos(ToWorldPos()) - Origin.position;
|
||
float x0 = c.x - HalfWidth - 0.5f, x1 = c.x + HalfWidth + 0.5f;
|
||
float z0 = c.z - HalfWidth - 0.5f, z1 = c.z + HalfWidth + 0.5f;
|
||
float y0 = c.y - Below, y1 = c.y + Above + 1f;
|
||
Vector3 b0 = new Vector3(x0, y0, z0), b1 = new Vector3(x1, y0, z0), b2 = new Vector3(x1, y0, z1), b3 = new Vector3(x0, y0, z1);
|
||
Vector3 t0 = new Vector3(x0, y1, z0), t1 = new Vector3(x1, y1, z0), t2 = new Vector3(x1, y1, z1), t3 = new Vector3(x0, y1, z1);
|
||
// Одна ломаная по всем 12 рёбрам (часть рёбер проходится дважды - на глаз не видно).
|
||
Vector3[] path = { b0, b1, b2, b3, b0, t0, t1, b1, t1, t2, b2, t2, t3, b3, t3, t0 };
|
||
lr.positionCount = path.Length;
|
||
for (int i = 0; i < path.Length; i++)
|
||
{
|
||
lr.SetPosition(i, t.InverseTransformPoint(path[i]));
|
||
}
|
||
}
|
||
|
||
public void RemoveBounds()
|
||
{
|
||
if (_bounds != null)
|
||
{
|
||
UnityEngine.Object.Destroy(_bounds);
|
||
_bounds = null;
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Постановка: лимит и пересечение зон.
|
||
// ------------------------------------------------------------------
|
||
|
||
public static bool IsUrn(Block _b)
|
||
{
|
||
string n = _b?.GetBlockName();
|
||
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName;
|
||
}
|
||
|
||
public static int UrnLimit(EntityPlayer _player)
|
||
{
|
||
float kills = _player?.Buffs != null ? _player.Buffs.GetCustomVar(Patch_EntityPlayer_AddKillXP_NecromancyCount.KillsCVarName) : 0f;
|
||
return Mathf.CeilToInt(kills / KillsPerUrn);
|
||
}
|
||
|
||
/// <summary>Ключ подсказки, почему урну сюда нельзя, или null.</summary>
|
||
public static string PlacementRefusal(Vector3i _pos, Block _placing)
|
||
{
|
||
if (!IsLogistBlock(_placing) && CinerariumRegistry.Overlaps(_pos))
|
||
{
|
||
return "necroCinerariumOverlap";
|
||
}
|
||
if (IsLogistBlock(_placing))
|
||
{
|
||
return null; // логисты в лимит не входят (16.10, пользователь)
|
||
}
|
||
World world = GameManager.Instance?.World;
|
||
EntityPlayerLocal player = world?.GetPrimaryPlayer();
|
||
// Реестр живёт на сервере; клиент в сети лимит не проверяет (мультиплеер поддержан не полностью).
|
||
if (player != null && world != null && !world.IsRemote())
|
||
{
|
||
PlatformUserIdentifierAbs me = PlatformManager.InternalLocalUserIdentifier;
|
||
if (CinerariumRegistry.CountFor(me) >= UrnLimit(player))
|
||
{
|
||
return "necroCinerariumLimit";
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>Все урны сейва: позиция и владелец, файл в папке сейва (как сумка волка). Сервер.</summary>
|
||
public static class CinerariumRegistry
|
||
{
|
||
const string FileName = "NecroCinerariums.txt";
|
||
static Dictionary<Vector3i, string> _urns;
|
||
/// <summary>Логисты (третье поле строки «L»): в лимит не входят.</summary>
|
||
static HashSet<Vector3i> _logists = new HashSet<Vector3i>();
|
||
static string _dir;
|
||
|
||
static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName);
|
||
|
||
static Dictionary<Vector3i, string> Urns()
|
||
{
|
||
string dir = GameIO.GetSaveGameDir();
|
||
if (_urns != null && _dir == dir)
|
||
{
|
||
return _urns;
|
||
}
|
||
_dir = dir;
|
||
_urns = new Dictionary<Vector3i, string>();
|
||
_logists = new HashSet<Vector3i>();
|
||
try
|
||
{
|
||
if (File.Exists(FilePath))
|
||
{
|
||
foreach (string line in File.ReadAllLines(FilePath))
|
||
{
|
||
string[] parts = line.Split('|');
|
||
string[] xyz = parts[0].Split(',');
|
||
if (xyz.Length == 3 && int.TryParse(xyz[0], out int x) && int.TryParse(xyz[1], out int y) && int.TryParse(xyz[2], out int z))
|
||
{
|
||
_urns[new Vector3i(x, y, z)] = parts.Length > 1 ? parts[1] : "";
|
||
if (parts.Length > 2 && parts[2] == "L")
|
||
{
|
||
_logists.Add(new Vector3i(x, y, z));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не прочитан (" + e.Message + ")");
|
||
}
|
||
return _urns;
|
||
}
|
||
|
||
static void Save()
|
||
{
|
||
try
|
||
{
|
||
StringBuilder sb = new StringBuilder();
|
||
foreach (KeyValuePair<Vector3i, string> kv in Urns())
|
||
{
|
||
sb.Append(kv.Key.x).Append(',').Append(kv.Key.y).Append(',').Append(kv.Key.z).Append('|').Append(kv.Value);
|
||
if (_logists.Contains(kv.Key))
|
||
{
|
||
sb.Append("|L");
|
||
}
|
||
sb.Append('\n');
|
||
}
|
||
File.WriteAllText(FilePath, sb.ToString());
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не записан (" + e.Message + ")");
|
||
}
|
||
}
|
||
|
||
public static void Add(Vector3i _pos, PlatformUserIdentifierAbs _owner, bool _logist)
|
||
{
|
||
Urns()[_pos] = _owner != null ? _owner.CombinedString : "";
|
||
if (_logist)
|
||
{
|
||
_logists.Add(_pos);
|
||
}
|
||
else
|
||
{
|
||
_logists.Remove(_pos);
|
||
}
|
||
Save();
|
||
Debug.Log("[NecromancerTome] Cinerarium: урна поставлена " + _pos + ", всего в сейве " + _urns.Count);
|
||
}
|
||
|
||
public static void Remove(Vector3i _pos)
|
||
{
|
||
if (Urns().Remove(_pos))
|
||
{
|
||
_logists.Remove(_pos);
|
||
Save();
|
||
Debug.Log("[NecromancerTome] Cinerarium: урна убрана " + _pos + ", всего в сейве " + _urns.Count);
|
||
}
|
||
}
|
||
|
||
public static int CountFor(PlatformUserIdentifierAbs _owner)
|
||
{
|
||
string id = _owner != null ? _owner.CombinedString : "";
|
||
int n = 0;
|
||
World world = GameManager.Instance?.World;
|
||
foreach (KeyValuePair<Vector3i, string> kv in Urns())
|
||
{
|
||
// Логисты не считаются; записанные до метки «L» узнаются по блоку в мире (чанк рядом загружен).
|
||
if (kv.Value == id && !_logists.Contains(kv.Key) &&
|
||
!(world != null && TEFeatureCinerarium.IsLogistBlock(world.GetBlock(kv.Key).Block)))
|
||
{
|
||
n++;
|
||
}
|
||
}
|
||
return n;
|
||
}
|
||
|
||
public static List<Vector3i> All()
|
||
{
|
||
return new List<Vector3i>(Urns().Keys);
|
||
}
|
||
|
||
/// <summary>Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям,
|
||
/// по высоте ближе 6 (зона от -1 до +4).</summary>
|
||
public static bool Overlaps(Vector3i _pos)
|
||
{
|
||
int w = TEFeatureCinerarium.HalfWidth * 2;
|
||
int h = TEFeatureCinerarium.Below + TEFeatureCinerarium.Above;
|
||
World world = GameManager.Instance?.World;
|
||
foreach (Vector3i p in Urns().Keys)
|
||
{
|
||
if (world != null && TEFeatureCinerarium.IsLogistBlock(world.GetBlock(p).Block))
|
||
{
|
||
continue; // у логиста зоны нет
|
||
}
|
||
if (p != _pos && Math.Abs(p.x - _pos.x) <= w && Math.Abs(p.z - _pos.z) <= w && Math.Abs(p.y - _pos.y) <= h)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Какие команды меню урны доступны сейчас (текст команд общий на тип блока - см. TEFeaturePyramidWard).</summary>
|
||
[HarmonyPatch(typeof(TileEntityComposite), "UpdateBlockActivationCommands")]
|
||
public static class Patch_TileEntityComposite_UpdateBlockActivationCommands_Cinerarium
|
||
{
|
||
public static void Postfix(TileEntityComposite __instance, BlockActivationCommand[] _commands, WorldBase _world, Vector3i _blockPos)
|
||
{
|
||
if (__instance == null || _commands == null || !__instance.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium urn))
|
||
{
|
||
return;
|
||
}
|
||
for (int i = 0; i < _commands.Length; i++)
|
||
{
|
||
string text = _commands[i].text;
|
||
if (string.IsNullOrEmpty(text))
|
||
{
|
||
continue;
|
||
}
|
||
if (text.EndsWith(":trunk") || text == "trunk" || text.EndsWith(":bag"))
|
||
{
|
||
_commands[i].enabled = true;
|
||
}
|
||
else if (text == "TEFeatureStorage:Search")
|
||
{
|
||
_commands[i].enabled = false; // вместо него - «Сумка»
|
||
}
|
||
else if (text.EndsWith("urn_repair"))
|
||
{
|
||
_commands[i].enabled = _world != null && _world.GetBlock(_blockPos).damage > 0;
|
||
}
|
||
else if (text.EndsWith(":Take"))
|
||
{
|
||
// Как у транспорта (16.10: «почему нету значка Забрать с рукой?»): игра даёт «Забрать» станциям только внутри
|
||
// своего земельного камня (TEFeaturePickup -> World.IsMyLandProtectedBlock), транспорт же поднимается где
|
||
// угодно. Урне - как транспорту: везде, кроме зоны торговца, только хозяину и только с пустой сумкой;
|
||
// череп (модификация) остаётся в урне.
|
||
World w = _world as World;
|
||
bool bagEmpty = !__instance.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage bag) || bag.IsEmpty();
|
||
bool mine = __instance.Owner == null || __instance.LocalPlayerIsOwner;
|
||
_commands[i].enabled = bagEmpty && mine && (w == null || !w.IsWithinTraderArea(_blockPos));
|
||
}
|
||
else if (text.EndsWith("bounds_show"))
|
||
{
|
||
_commands[i].enabled = !urn.ZoneShown && !urn.IsLogist;
|
||
}
|
||
else if (text.EndsWith("bounds_hide"))
|
||
{
|
||
_commands[i].enabled = urn.ZoneShown && !urn.IsLogist;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(BlockCompositeTileEntity), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })]
|
||
public static class Patch_BlockCompositeTileEntity_OnBlockActivated_Cinerarium
|
||
{
|
||
public static bool Prefix(string _commandName, Vector3i _blockPos, EntityPlayerLocal _player, ref bool __result)
|
||
{
|
||
if (string.IsNullOrEmpty(_commandName))
|
||
{
|
||
return true;
|
||
}
|
||
bool skull = _commandName.EndsWith(":trunk") || _commandName == "trunk";
|
||
bool repair = _commandName.EndsWith("urn_repair");
|
||
bool bag = _commandName.EndsWith("TEFeatureCinerarium:bag");
|
||
bool bounds = _commandName.EndsWith("bounds_show") || _commandName.EndsWith("bounds_hide");
|
||
if (!skull && !bounds && !repair && !bag)
|
||
{
|
||
return true;
|
||
}
|
||
World world = GameManager.Instance?.World;
|
||
if (!(world?.GetTileEntity(_blockPos) is TileEntityComposite composite) ||
|
||
!composite.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium urn))
|
||
{
|
||
return true;
|
||
}
|
||
if (bag)
|
||
{
|
||
__result = composite.OnBlockActivated(null, "TEFeatureStorage:Search", world, _blockPos, world.GetBlock(_blockPos), _player);
|
||
return false;
|
||
}
|
||
if (skull)
|
||
{
|
||
CinerariumContents.Open(_player, urn);
|
||
}
|
||
else if (repair)
|
||
{
|
||
if (!TEFeatureCinerarium.Repair(world, _blockPos, _player))
|
||
{
|
||
GameManager.ShowTooltip(_player, "necroCinerariumNoBones");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
urn.ZoneShown = !urn.ZoneShown;
|
||
if (urn.ZoneShown)
|
||
{
|
||
urn.SpawnBounds();
|
||
}
|
||
else
|
||
{
|
||
urn.RemoveBounds();
|
||
}
|
||
urn.SetModified();
|
||
}
|
||
__result = true;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Лимит урн и пересечение зон. Составной блок CanPlaceBlockAt не переопределяет - заплатка на Block,
|
||
/// с отбором по имени блока. Зовётся и каждый кадр для призрака постановки (RenderDisplacedCube) - он
|
||
/// краснеет; причину показывает подмена подсказки «нельзя поставить» при щелчке (ниже).</summary>
|
||
[HarmonyPatch(typeof(Block), nameof(Block.CanPlaceBlockAt))]
|
||
public static class Patch_Block_CanPlaceBlockAt_Cinerarium
|
||
{
|
||
public static string LastRefusal;
|
||
public static float LastRefusalTime;
|
||
|
||
public static void Postfix(Block __instance, Vector3i _blockPos, ref bool __result)
|
||
{
|
||
if (!__result || __instance == null || !TEFeatureCinerarium.IsUrn(__instance))
|
||
{
|
||
return;
|
||
}
|
||
string why = TEFeatureCinerarium.PlacementRefusal(_blockPos, __instance);
|
||
if (why != null)
|
||
{
|
||
__result = false;
|
||
LastRefusal = why;
|
||
LastRefusalTime = Time.time;
|
||
}
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(GameManager), nameof(GameManager.ShowTooltip), new Type[] { typeof(EntityPlayerLocal), typeof(string), typeof(bool), typeof(bool), typeof(float) })]
|
||
public static class Patch_GameManager_ShowTooltip_Cinerarium
|
||
{
|
||
public static void Prefix(ref string _text)
|
||
{
|
||
if (_text == "blockCantPlaced" && Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal != null &&
|
||
Time.time - Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusalTime < 0.1f)
|
||
{
|
||
_text = Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal;
|
||
Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Подъём урны («Take» детали TEFeaturePickup -> Block.TakeItemWithTimer): по окончании таймера игра отдаёт
|
||
/// предмет по типу блока (BlockValue.ToItemValue), OnBlockPickedUp не зовёт. Для урны - своя копия того же метода:
|
||
/// с черепом - предмет necroCinerariumSkull, без - necroCinerarium; сумка за время таймера не должна наполниться.
|
||
/// Позиция запоминается - OnRemove урны по ней понимает, что её подняли, а не разбили (череп не выбрасывать).</summary>
|
||
[HarmonyPatch(typeof(Block), nameof(Block.TakeItemWithTimerDone))]
|
||
public static class Patch_Block_TakeItemWithTimerDone_Cinerarium
|
||
{
|
||
static Vector3i _pos;
|
||
static float _time = -100f;
|
||
|
||
public static bool WasPickedUp(Vector3i _p) => _p == _pos && Time.time - _time < 5f;
|
||
|
||
public static bool Prefix(TimerEventData _timerData)
|
||
{
|
||
World world = GameManager.Instance?.World;
|
||
if (world == null || !(_timerData?.Data is object[] obj) || obj.Length < 3 || !(obj[1] is Vector3i pos) ||
|
||
!(obj[0] is BlockValue started) || !(obj[2] is EntityPlayerLocal player))
|
||
{
|
||
return true;
|
||
}
|
||
BlockValue bv = world.GetBlock(pos);
|
||
if (!TEFeatureCinerarium.IsUrn(bv.Block) || bv.type != started.type || bv.damage > 0 ||
|
||
!(world.GetTileEntity(pos) is TileEntityComposite composite) ||
|
||
!composite.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium urn))
|
||
{
|
||
return true; // чужой блок или отказ игры (повреждён, подменён) - пусть игра сама скажет почему
|
||
}
|
||
if (composite.IsUserAccessing())
|
||
{
|
||
GameManager.ShowTooltip(player, Localization.Get("ttCantPickupInUse"), string.Empty, "ui_denied");
|
||
return false;
|
||
}
|
||
if (composite.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage bag) && !bag.IsEmpty())
|
||
{
|
||
GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied");
|
||
return false;
|
||
}
|
||
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
|
||
ItemValue iv = ItemClass.GetItem(logist
|
||
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName)
|
||
: (urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName));
|
||
if (iv == null || iv.type <= 0)
|
||
{
|
||
return true;
|
||
}
|
||
_pos = pos;
|
||
_time = Time.time;
|
||
ItemStack stack = new ItemStack(iv, 1);
|
||
LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(player);
|
||
if (!ui.xui.PlayerInventory.AddItem(stack))
|
||
{
|
||
ui.xui.PlayerInventory.DropItem(stack);
|
||
}
|
||
world.SetBlockRPC(pos, BlockValue.Air);
|
||
Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " поднята" + (urn.HasSkull ? " с черепом" : ""));
|
||
return false;
|
||
}
|
||
}
|
||
}
|