Урна-цинерарий: блок, багажник с черепом, сумка духа, зона, лимит
Урна устроена как транспорт: на E круговое меню - «Багажник» (своё окно с одной ячейкой под череп), сумка духа 6x2 (хранилище игры), «Починить» (5 костей из рюкзака), показать/скрыть зону 5x5 от -1 до +4, «Поднять» (деталь игры TEFeaturePickup; только с пустой сумкой, череп остаётся внутри - поднятая урна с черепом становится necroCinerariumSkull). Постановка: зоны урн не пересекаются, урн не больше ceil(убийств / 500); реестр урн - файл в папке сейва. Разбитая урна роняет череп. Сохранение - CinerariumWriteHelper, версия первым байтом. Рецепт - конец Адепта (уровень 15). Модель и иконка временные (кастрюля). Тексты - 13 языков. Дух-работник - следующий этап. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
87f3aaf295
commit
bd76360eeb
File diff suppressed because one or more lines are too long
@@ -208,4 +208,21 @@
|
||||
<sprite depth="9" pos="0,20" height="8" color="[black]" sprite="menu_empty2px" globalopacitymod="0" type="sliced" fillcenter="false" />
|
||||
<sprite depth="4" pos="0,20" height="8" width="750" color="150,60,220,200" globalopacitymod="0" type="filled" fill="{% cvar('necroNecromancyProgressCVar') / 20 }" />
|
||||
</append>
|
||||
<!-- «Багажник» урны-цинерария (1.7.0, 16.10, HarmonySrc/CinerariumTrunk.cs): одна ячейка под череп - та же
|
||||
ячейка игры для обязательного предмета, что в панели модификаций коллектора росы (windowDewCollectorMods),
|
||||
не больше одного черепа (override_stack_count). -->
|
||||
<append xpath="/windows">
|
||||
<window name="windowNecroCinerariumTrunk" width="228" height="121" panel="Right" cursor_area="true">
|
||||
<rect style="header.panel">
|
||||
<headerbg />
|
||||
<sprite style="header.icon" sprite="ui_game_symbol_skull"/>
|
||||
<label style="header.name.shrink" text_key="necroCinerariumTrunk" />
|
||||
</rect>
|
||||
<rect name="content" depth="0" pos="0,-46" height="75" disablefallthrough="true">
|
||||
<grid name="inventory" rows="1" cols="1" pos="3,-3" depth="1" cell_width="75" cell_height="75" controller="NecromancerTome.XUiC_NecroCinerariumSkullGrid, NecromancerHarmony" repeat_content="true">
|
||||
<item_stack controller="RequiredItemStack" name="0" override_stack_count="1"/>
|
||||
</grid>
|
||||
</rect>
|
||||
</window>
|
||||
</append>
|
||||
</windows>
|
||||
|
||||
@@ -59,4 +59,10 @@
|
||||
<window name="windowNonPagingHeader" />
|
||||
</window_group>
|
||||
</append>
|
||||
<!-- «Багажник» урны-цинерария (1.7.0): ячейка черепа и рюкзак рядом, как у транспорта. -->
|
||||
<append xpath="/xui/ruleset">
|
||||
<window_group name="necroCinerariumTrunk" open_backpack_on_open="true" close_compass_on_open="true">
|
||||
<window name="windowNecroCinerariumTrunk" />
|
||||
</window_group>
|
||||
</append>
|
||||
</xui>
|
||||
|
||||
@@ -513,4 +513,49 @@
|
||||
<property name="ItemTypeIcon" value="all_blocks"/>
|
||||
</block>
|
||||
</append>
|
||||
|
||||
<!-- Урна-цинерарий (Harvest Cinerarium), 1.7.0, 16.10.2026: этап 2 некротехнолога (BACKLOG «Некротехнолог v2»).
|
||||
Составной блок, как земельный камень: TEFeatureStorage - сумка духа (окно хранилища 6x2, «Открыть»),
|
||||
TEFeatureCinerarium (HarmonySrc/Cinerarium.cs) - череп (положить/забрать), показ зоны 5x5, -1..+4,
|
||||
лимит ceil(убийств / 500) и запрет пересечения зон при постановке. Всё - в радиальном меню на E (решение 16.10).
|
||||
Модель и иконка ВРЕМЕННЫЕ - кастрюля игры; своя урна будет из Blender. -->
|
||||
<append xpath="/blocks">
|
||||
<block name="necroCinerarium">
|
||||
<property name="CreativeMode" value="Player"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumDesc"/>
|
||||
<property name="UnlockedBy" value="craftingNecroNecromancy"/>
|
||||
<property name="Class" value="CompositeTileEntity"/>
|
||||
<property class="CompositeFeatures">
|
||||
<property class="TEFeatureStorage">
|
||||
<property name="LootList" value="necroCinerariumBag"/>
|
||||
</property>
|
||||
<property class="TEFeatureCinerarium"/>
|
||||
<property class="TEFeaturePickup">
|
||||
<property name="TakeDelay" value="2"/>
|
||||
</property>
|
||||
</property>
|
||||
<property name="Material" value="Mstone"/>
|
||||
<property name="Shape" value="ModelEntity"/>
|
||||
<property name="Model" value="@:Other/Items/Crafting/cookingPot_01Prefab.prefab"/>
|
||||
<property name="CustomIcon" value="toolCookingPot"/>
|
||||
<property name="WaterFlow" value="permitted"/>
|
||||
<property name="OnlySimpleRotations" value="true"/>
|
||||
<property name="Collide" value="movement,melee,bullet,arrow,rocket"/>
|
||||
<property name="StabilitySupport" value="false"/>
|
||||
<property name="MaxDamage" value="500"/>
|
||||
<property name="EconomicValue" value="0"/>
|
||||
<property name="SellableToTrader" value="false"/>
|
||||
<property name="FilterTags" value="MC_playerBlocks,SC_decor"/>
|
||||
<property class="RepairItems">
|
||||
<property name="resourceBone" value="5"/>
|
||||
</property>
|
||||
</block>
|
||||
<!-- Урна, поднятая с черепом внутри (как транспорт держит модификации): тот же блок, при постановке череп
|
||||
уже в багажнике. Получается только подъёмом (Patch_Block_OnBlockPickedUp_Cinerarium). -->
|
||||
<block name="necroCinerariumSkull">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumDesc"/>
|
||||
</block>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -45,4 +45,11 @@
|
||||
<item group="groupZpackVictim"/>
|
||||
</lootcontainer>
|
||||
</append>
|
||||
|
||||
<!-- Сумка духа урны-цинерария (1.7.0): пустое хранилище игрока, 6x2. -->
|
||||
<append xpath="/lootcontainers">
|
||||
<lootcontainer name="necroCinerariumBag" count="0" size="6,2" sound_open="UseActions/open_backpack" sound_close="UseActions/close_backpack" open_time="0" loot_quality_template="qualBaseTemplate">
|
||||
<item name="cobweb" count="0"/>
|
||||
</lootcontainer>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
пользуется, вода сидит на necroNecromancyLvl20. -->
|
||||
<unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/>
|
||||
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
|
||||
<unlock_entry item="necroModKnifeDarkSense" unlock_tier="5"/>
|
||||
<unlock_entry item="necroModKnifeDarkSense,necroCinerarium" unlock_tier="5"/>
|
||||
</display_entry>
|
||||
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
|
||||
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
|
||||
|
||||
@@ -605,4 +605,13 @@
|
||||
<ingredient name="necroSkullDecor" count="1"/>
|
||||
</recipe>
|
||||
</append>
|
||||
|
||||
<!-- Урна-цинерарий (1.7.0, 16.10): конец Адепта - уровень 15, 300 убийств (решение 06.10). Состав - черновой. -->
|
||||
<append xpath="/recipes">
|
||||
<recipe name="necroCinerarium" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
|
||||
<ingredient name="resourceClayLump" count="30"/>
|
||||
<ingredient name="resourceZombieAsh" count="100"/>
|
||||
<ingredient name="resourceBone" count="10"/>
|
||||
</recipe>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
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();
|
||||
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();
|
||||
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 («Search»), подъём - TEFeaturePickup («Take»), обе детали игры.</summary>
|
||||
public override void InitBlockActivationCommands(Action<BlockActivationCommand, TileEntityComposite.EBlockCommandOrder, TileEntityFeatureData> _addCallback)
|
||||
{
|
||||
base.InitBlockActivationCommands(_addCallback);
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Череп.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Череп положен в ячейку багажника или вынут из неё.</summary>
|
||||
public void SetSkull(bool _has)
|
||||
{
|
||||
if (HasSkull == _has)
|
||||
{
|
||||
return;
|
||||
}
|
||||
HasSkull = _has;
|
||||
SetModified();
|
||||
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;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Границы зоны: каркас параллелепипеда, как у земельного камня. Дочерний объект модели блока - сдвиги
|
||||
// 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")
|
||||
{
|
||||
_commands[i].enabled = true;
|
||||
}
|
||||
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 bounds = _commandName.EndsWith("bounds_show") || _commandName.EndsWith("bounds_hide");
|
||||
if (!skull && !bounds && !repair)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
World world = GameManager.Instance?.World;
|
||||
if (!(world?.GetTileEntity(_blockPos) is TileEntityComposite composite) ||
|
||||
!composite.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium urn))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// «Багажник» урны-цинерария (решение 16.10: «урна как транспорт» - на E круговое меню, как у велосипеда:
|
||||
/// багажник с ячейкой черепа и сумка духа). Окно necroCinerariumTrunk (Config/XUi_InGame) - панель с одной
|
||||
/// ячейкой игры для обязательного предмета (XUiC_RequiredItemStack, как в панели модификаций коллектора
|
||||
/// росы), рядом рюкзак (open_backpack_on_open). Ячейка принимает только череп и не больше одного
|
||||
/// (override_stack_count="1" в XML). Череп - обычный предмет (ItemClass): в сейвах он уже есть, менять ему
|
||||
/// класс на модификацию нельзя (см. память про сейвы), поэтому ячейка, а не сборка как у робомолота.
|
||||
/// </summary>
|
||||
public class XUiC_NecroCinerariumSkullGrid : XUiC_ItemStackGrid
|
||||
{
|
||||
public const string WindowGroup = "necroCinerariumTrunk";
|
||||
|
||||
/// <summary>Урна, чей багажник открыт (ставится перед открытием окна).</summary>
|
||||
public static TEFeatureCinerarium Urn;
|
||||
|
||||
public static void Open(EntityPlayerLocal _player, TEFeatureCinerarium _urn)
|
||||
{
|
||||
LocalPlayerUI ui = _player?.PlayerUI;
|
||||
if (ui == null || _urn == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Urn = _urn;
|
||||
ui.windowManager.Open(WindowGroup, true);
|
||||
}
|
||||
|
||||
public override void OnOpen()
|
||||
{
|
||||
base.OnOpen();
|
||||
ItemValue skull = ItemClass.GetItem(TEFeatureCinerarium.SkullItemName);
|
||||
for (int i = 0; i < itemControllers.Length; i++)
|
||||
{
|
||||
if (itemControllers[i] is XUiC_RequiredItemStack slot)
|
||||
{
|
||||
slot.SetAllowedItemClassSingle(TEFeatureCinerarium.SkullItemName);
|
||||
slot.RequiredItemOnly = true;
|
||||
slot.StackLocation = XUiC_ItemStack.StackLocationTypes.Workstation;
|
||||
}
|
||||
}
|
||||
ItemStack[] stacks = new ItemStack[itemControllers.Length];
|
||||
for (int i = 0; i < stacks.Length; i++)
|
||||
{
|
||||
stacks[i] = ItemStack.Empty.Clone();
|
||||
}
|
||||
if (stacks.Length > 0 && Urn != null && Urn.HasSkull && skull != null && skull.type > 0)
|
||||
{
|
||||
stacks[0] = new ItemStack(skull, 1);
|
||||
}
|
||||
SetStacks(stacks);
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
base.OnClose();
|
||||
Urn = null;
|
||||
}
|
||||
|
||||
public override void UpdateBackend(ItemStack[] stackList)
|
||||
{
|
||||
base.UpdateBackend(stackList);
|
||||
bool has = stackList != null && stackList.Length > 0 && stackList[0] != null && !stackList[0].IsEmpty();
|
||||
Urn?.SetSkull(has);
|
||||
}
|
||||
|
||||
/// <summary>Урна убрана или выгружена, пока открыт её багажник, - закрыть окно (иначе череп в ячейке
|
||||
/// уже ни к чему не привязан).</summary>
|
||||
public static void CloseIfShowing(TEFeatureCinerarium _urn)
|
||||
{
|
||||
if (Urn == null || Urn != _urn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LocalPlayerUI ui = GameManager.Instance?.World?.GetPrimaryPlayer()?.PlayerUI;
|
||||
if (ui != null && ui.windowManager.IsWindowOpen(WindowGroup))
|
||||
{
|
||||
ui.windowManager.Close(WindowGroup);
|
||||
}
|
||||
Urn = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Запись состояния урны-цинерария (TEFeatureCinerarium, HarmonySrc/Cinerarium.cs). Здесь, а не в
|
||||
/// основном проекте, по той же причине, что и PyramidWardWriteHelper: PooledBinaryWriter.Write
|
||||
/// оттуда не компилируется (см. NecromancerTEPersistence.csproj). Первым байтом - версия, чтобы
|
||||
/// поля урны (дух, его работа) можно было дописывать, не ломая старые сейвы.
|
||||
/// </summary>
|
||||
public static class CinerariumWriteHelper
|
||||
{
|
||||
public static void Write(PooledBinaryWriter _bw, byte _version, bool _hasSkull, bool _zoneShown)
|
||||
{
|
||||
_bw.Write(_version);
|
||||
_bw.Write(_hasSkull);
|
||||
_bw.Write(_zoneShown);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user