Files
necromants-tome-7d2d-3-2/HarmonySrc/Cinerarium.cs
T
AlexCubeandClaude Opus 5.5 0890ffe1e9 Некротехнолог: работа духа - расходники коллекторов и готовое из станций
Раз в 3 с одно дело: (1) пополнить из ящиков зоны и сумки урны ячейки
расходников любого коллектора (банки, цветы, корм, сферы) и катализатора
(куры, по одной в пустую ячейку); (2) забрать готовое из коллекторов и
станций (верстак, кузня, химстанция, костёр...) в ящики зоны - сначала
к таким же, потом в пустое, ящики полны - в сумку урны; некуда - дух
чернеет и замирает, оживает сам. Модификации, инструменты, топливо и
сырьё станций не трогаются; открытое у игрока и закреплённые ячейки -
тоже. Модель духа плывёт к месту работы и обратно. Грядки - позже.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:43:36 +03:00

692 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";
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 = 1;
public bool HasSkull;
public bool ZoneShown;
public override void CopyFromInternal(TileEntityComposite _other)
{
if (_other.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium other))
{
HasSkull = other.HasSkull;
ZoneShown = other.ZoneShown;
}
}
/// <summary>Блок поставлен (OnBlockAdded составного блока; на загрузке чанка не зовётся).</summary>
public override void OnAdded(Vector3i _blockPos, BlockValue _blockValue)
{
base.OnAdded(_blockPos, _blockValue);
if (_blockValue.Block?.GetBlockName() == SkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
if (ConnectionManager.Instance.IsServer)
{
CinerariumRegistry.Add(_blockPos, Parent?.Owner);
}
}
/// <summary>Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает.</summary>
public override void OnRemove(World _world)
{
base.OnRemove(_world);
RemoveBounds();
RemoveGenie(false);
XUiC_NecroCinerariumSkullGrid.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();
RemoveGenie(false);
XUiC_NecroCinerariumSkullGrid.CloseIfShowing(this);
}
public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode)
{
base.Write(_bw, _eStreamMode);
CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown);
}
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
{
base.Read(_br, _eStreamMode);
byte version = _br.ReadByte();
HasSkull = _br.ReadBoolean();
ZoneShown = _br.ReadBoolean();
if (version > SaveVersion)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: урна из более новой версии мода (" + version + ") - прочитано только известное");
}
}
/// <summary>Меню как у велосипеда (EntityVehicle.InitLocalActivationCommands): «service» - багажник (у транспорта
/// «Обслуживание», окно модификаций), «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", "service", _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);
Work(_world);
}
float _nextWork;
public bool Stuck;
/// <summary>Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере.</summary>
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<CinerariumGenieMotion>() : null;
if (motion != null)
{
motion.SetStuck(Stuck && at == null);
if (at.HasValue)
{
motion.Visit(World.blockToTransformPos(at.Value) - Origin.position);
}
}
}
// ------------------------------------------------------------------
// Череп.
// ------------------------------------------------------------------
/// <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)
{
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);
}
}
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;
}
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)
{
if (CinerariumRegistry.Overlaps(_pos))
{
return "necroCinerariumOverlap";
}
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;
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>();
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] : "";
}
}
}
}
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).Append('\n');
}
File.WriteAllText(FilePath, sb.ToString());
}
catch (Exception e)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не записан (" + e.Message + ")");
}
}
public static void Add(Vector3i _pos, PlatformUserIdentifierAbs _owner)
{
Urns()[_pos] = _owner != null ? _owner.CombinedString : "";
Save();
Debug.Log("[NecromancerTome] Cinerarium: урна поставлена " + _pos + ", всего в сейве " + _urns.Count);
}
public static void Remove(Vector3i _pos)
{
if (Urns().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;
foreach (string v in Urns().Values)
{
if (v == id)
{
n++;
}
}
return n;
}
/// <summary>Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям,
/// по высоте ближе 6 (зона от -1 до +4).</summary>
public static bool Overlaps(Vector3i _pos)
{
int w = TEFeatureCinerarium.HalfWidth * 2;
int h = TEFeatureCinerarium.Below + TEFeatureCinerarium.Above;
foreach (Vector3i p in Urns().Keys)
{
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"))
{
// Как у транспорта: поднять можно, только вынув вещи из сумки; череп (модификация) остаётся в урне.
if (__instance.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage bag) && !bag.IsEmpty())
{
_commands[i].enabled = false;
}
}
else if (text.EndsWith("bounds_show"))
{
_commands[i].enabled = !urn.ZoneShown;
}
else if (text.EndsWith("bounds_hide"))
{
_commands[i].enabled = urn.ZoneShown;
}
}
}
}
[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)
{
XUiC_NecroCinerariumSkullGrid.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);
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;
}
ItemValue iv = ItemClass.GetItem(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;
}
}
}