Урны ремонтника и лутера: большая зона, запуск кнопкой, кнопки-состояния

- necroCinerariumRepair/+Skull и necroCinerariumLoot/+Skull (пока на модели урны сборщика),
  рецепты (урна + ремкомплект / отмычка, на крови и на соке), открываются с урнами.
- Зона - сторона притязания сервера или 31/21/11 (пункт «Размер зоны»), весь столб мира,
  каркас по «Показать границы»; работа - по «Запустить», один проход, «Остановить».
- Ремонтник: обход столбов из общего бюджета клеток, очередь повреждённых блоков с
  RepairItems (зоны пересекаются - блок берёт первый), ремонт раз в 2.5 с материалами из
  ящиков по нитям (доля по повреждению), не хватает - ждёт с мигающей кнопкой.
- Лутер: предметы на земле, мешки с зомби, красные трупы (тем же путём, что игрок, с
  находкой черепа) - в ящик с любой нитью; рюкзаки игроков и аэродроп не трогает.
- Кнопки на модели (рендереры «button»): жёлтая - ждёт, мигает - нехватка/ящик полон,
  зелёная - работа, красная - прилив. Запись урны v6 (зона, проход, курсор, кто нажал).
- Тексты 13 языков; команды разработки necrodamage, necrodrop, necrobag, necrocorpse,
  necrourn run|stop|size.

Проверено в игре: ремонтник (нехватка -> материалы -> починил 2 блока), лутер (предметы
с земли). Мешки и красные трупы - не проверены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-18 14:55:33 +03:00
co-authored by Claude Opus 5.5
parent 85d8036e81
commit a352a408a4
9 changed files with 953 additions and 21 deletions
File diff suppressed because one or more lines are too long
+26
View File
@@ -613,5 +613,31 @@
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumWorkerDesc"/>
</block>
<!-- Урна ремонтника и урна лутера (18.10.2026, BACKLOG «Урна ремонтника», «Урна лутера»; CinerariumZone.cs): большая зона -
сторона притязания или меньше, весь столб мира; работа - по кнопке «Запустить» (один проход), кнопки-звонки с черепом
на модели показывают состояние. Ремонтник чинит повреждённые блоки материалами из ящиков по нитям; лутер собирает
предметы на земле, мешки с зомби и красные трупы в ящик по нити. Без зоны 5x5 и вне лимита, как логист. -->
<block name="necroCinerariumRepair">
<property name="Extends" value="necroCinerarium"/>
<property name="CreativeMode" value="Player"/>
<property name="DescriptionKey" value="necroCinerariumRepairDesc"/>
<property name="CustomIcon" value="necroCinerarium"/> <!-- своя иконка - после модели -->
</block>
<block name="necroCinerariumRepairSkull">
<property name="Extends" value="necroCinerariumRepair"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumRepairDesc"/>
</block>
<block name="necroCinerariumLoot">
<property name="Extends" value="necroCinerarium"/>
<property name="CreativeMode" value="Player"/>
<property name="DescriptionKey" value="necroCinerariumLootDesc"/>
<property name="CustomIcon" value="necroCinerarium"/> <!-- своя иконка - после модели -->
</block>
<block name="necroCinerariumLootSkull">
<property name="Extends" value="necroCinerariumLoot"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumLootDesc"/>
</block>
</append>
</config>
+1 -1
View File
@@ -181,7 +181,7 @@
пользуется, вода сидит на necroNecromancyLvl20. -->
<unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/>
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
<unlock_entry item="necroModKnifeDarkSense,necroCinerarium,necroCinerariumLogist,necroCinerariumWorker" unlock_tier="5"/>
<unlock_entry item="necroModKnifeDarkSense,necroCinerarium,necroCinerariumLogist,necroCinerariumWorker,necroCinerariumRepair,necroCinerariumLoot" unlock_tier="5"/>
</display_entry>
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
+30
View File
@@ -680,5 +680,35 @@
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="meleeToolRepairT1ClawHammer" count="1"/>
</recipe>
<!-- Урна ремонтника (18.10) - рецепт урны и ремкомплект; копия на соке чёрной розы. -->
<recipe name="necroCinerariumRepair" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceRepairKit" count="1"/>
</recipe>
<recipe name="necroCinerariumRepair" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceRepairKit" count="1"/>
</recipe>
<!-- Урна лутера (18.10) - рецепт урны и отмычка; копия на соке чёрной розы. -->
<recipe name="necroCinerariumLoot" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceLockPick" count="1"/>
</recipe>
<recipe name="necroCinerariumLoot" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceClayLump" count="30"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceLockPick" count="1"/>
</recipe>
</append>
</config>
+83 -19
View File
@@ -27,7 +27,7 @@ namespace NecromancerTome
/// Имена команд не должны кончаться на zone_show/zone_hide/effect_on/effect_off: заплатка пирамиды на
/// BlockCompositeTileEntity.OnBlockActivated ловит их по EndsWith у ЛЮБОГО составного блока.
/// </summary>
public class TEFeatureCinerarium : TEFeatureAbs
public partial class TEFeatureCinerarium : TEFeatureAbs
{
public const string BlockName = "necroCinerarium";
public const string SkullBlockName = "necroCinerariumSkull";
@@ -62,8 +62,9 @@ namespace NecromancerTome
return n == WorkerBlockName || n == WorkerSkullBlockName;
}
/// <summary>Урна без зоны (логист, рабочий): ставится вплотную к любым урнам, в лимит не входит, работает по нитям.</summary>
public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b);
/// <summary>Урна без зоны 5x5 (логист, рабочий, ремонтник, лутер): ставится вплотную к любым урнам, в лимит не входит,
/// работает по нитям. У ремонтника и лутера своя большая зона (CinerariumZone.cs) - зоны таких урн могут пересекаться.</summary>
public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b) || IsRepairBlock(_b) || IsLootBlock(_b);
public const string SkullItemName = "necroSkull";
public const int HalfWidth = 2;
public const int Below = 1;
@@ -71,7 +72,7 @@ namespace NecromancerTome
/// <summary>Убийств на одну урну сборщика. 17.10: «лимит урн - ослабить/убрать» - нагрузку теперь держит общий бюджет
/// шагов (CinerariumLoad), лимит остался мягким игровым порогом: было 500, стало 200.</summary>
public const float KillsPerUrn = 200f;
const byte SaveVersion = 5; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10), 5 - черепа-ретрансляторы (18.10)
const byte SaveVersion = 6; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10), 5 - черепа-ретрансляторы (18.10), 6 - зона и запуск ремонтника/лутера (18.10)
public bool HasSkull;
public bool ZoneShown;
@@ -102,7 +103,7 @@ namespace NecromancerTome
}
}
bool _worker;
public bool IsZoneless => IsLogist || IsWorker;
public bool IsZoneless => IsLogist || IsWorker || IsBigZone;
/// <summary>Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.</summary>
public bool IsLogist
{
@@ -116,6 +117,8 @@ namespace NecromancerTome
{
_logist = IsLogistBlock(bv.Block);
_worker = IsWorkerBlock(bv.Block);
_repair = IsRepairBlock(bv.Block);
_loot = IsLootBlock(bv.Block);
_logistKnown = true;
}
}
@@ -163,15 +166,18 @@ namespace NecromancerTome
base.OnAdded(_blockPos, _blockValue);
_logist = IsLogistBlock(_blockValue.Block);
_worker = IsWorkerBlock(_blockValue.Block);
_repair = IsRepairBlock(_blockValue.Block);
_loot = IsLootBlock(_blockValue.Block);
_logistKnown = true;
string placed = _blockValue.Block?.GetBlockName();
if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName)
if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName ||
placed == RepairSkullBlockName || placed == LootSkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
if (ConnectionManager.Instance.IsServer)
{
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker);
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker || _repair || _loot);
}
}
@@ -183,6 +189,7 @@ namespace NecromancerTome
RemoveThreadView();
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
ZoneStop(false);
if (!ConnectionManager.Instance.IsServer)
{
return;
@@ -253,6 +260,8 @@ namespace NecromancerTome
AppendVia(via, l);
}
CinerariumWriteHelper.WriteInts(_bw, via.ToArray());
// v6: ремонтник/лутер - сторона зоны, идёт ли проход, где курсор обхода, кто нажал кнопку.
CinerariumWriteHelper.WriteInts(_bw, new[] { ZoneSide, ZoneRunning ? 1 : 0, ZoneCursor, ZonePressedBy });
}
static void AppendVia(List<int> _dst, List<Vector3i> _l)
@@ -389,6 +398,23 @@ namespace NecromancerTome
}
}
AlignVia();
ZoneSide = 0;
ZoneRunning = false;
ZoneCursor = 0;
ZonePressedBy = -1;
if (version >= 6)
{
int n = _br.ReadInt32();
int[] v = new int[n];
for (int i = 0; i < n; i++)
{
v[i] = _br.ReadInt32();
}
ZoneSide = n > 0 ? v[0] : 0;
ZoneRunning = n > 1 && v[1] != 0;
ZoneCursor = n > 2 ? v[2] : 0;
ZonePressedBy = n > 3 ? v[3] : -1;
}
_threadsDirty = true;
if (version > SaveVersion)
{
@@ -410,13 +436,18 @@ namespace NecromancerTome
_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);
// Ремонтник, лутер (CinerariumZone.cs): запуск прохода кнопкой, остановка, размер зоны.
_addCallback(new BlockActivationCommand("zurn_run", "electric_switch", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
_addCallback(new BlockActivationCommand("zurn_stop", "electric_switch", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
_addCallback(new BlockActivationCommand("zurn_size", "map_cursor", _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");
string s = _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " +
Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty");
return IsBigZone ? s + ZoneStateText() : s;
}
public override void UpdateTick(World _world)
@@ -716,6 +747,11 @@ namespace NecromancerTome
_pending = false;
return;
}
if (IsBigZone)
{
ZoneWork(_world); // ремонтник, лутер - CinerariumZone.cs
return;
}
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
if (_pending)
{
@@ -926,7 +962,7 @@ namespace NecromancerTome
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))
_threadView != null && r.transform.IsChildOf(_threadView.transform) || CinerariumButtons.IsButton(r))
{
continue;
}
@@ -1013,11 +1049,13 @@ namespace NecromancerTome
lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lr.receiveShadows = false;
// Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4.
// Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4. Большая зона
// (ремонтник, лутер) - сторона ZoneSideNow, по высоте весь столб мира.
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;
int hw = IsBigZone ? ZoneSideNow / 2 : HalfWidth;
float x0 = c.x - hw - 0.5f, x1 = c.x + hw + 0.5f;
float z0 = c.z - hw - 0.5f, z1 = c.z + hw + 0.5f;
float y0 = IsBigZone ? -Origin.position.y + 1f : c.y - Below, y1 = IsBigZone ? -Origin.position.y + 254f : 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 рёбрам (часть рёбер проходится дважды - на глаз не видно).
@@ -1046,7 +1084,8 @@ namespace NecromancerTome
{
string n = _b?.GetBlockName();
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName ||
n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName;
n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName ||
IsRepairBlock(_b) || IsLootBlock(_b);
}
public static int UrnLimit(EntityPlayer _player)
@@ -1261,11 +1300,23 @@ namespace NecromancerTome
}
else if (text.EndsWith("bounds_show"))
{
_commands[i].enabled = !urn.ZoneShown && !urn.IsZoneless;
_commands[i].enabled = !urn.ZoneShown && (!urn.IsZoneless || urn.IsBigZone);
}
else if (text.EndsWith("bounds_hide"))
{
_commands[i].enabled = urn.ZoneShown && !urn.IsZoneless;
_commands[i].enabled = urn.ZoneShown && (!urn.IsZoneless || urn.IsBigZone);
}
else if (text.EndsWith("zurn_run"))
{
_commands[i].enabled = urn.IsBigZone && urn.HasSkull && !urn.ZoneRunning;
}
else if (text.EndsWith("zurn_stop"))
{
_commands[i].enabled = urn.IsBigZone && urn.ZoneRunning;
}
else if (text.EndsWith("zurn_size"))
{
_commands[i].enabled = urn.IsBigZone;
}
}
}
@@ -1284,7 +1335,8 @@ namespace NecromancerTome
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)
bool zone = _commandName.EndsWith("zurn_run") || _commandName.EndsWith("zurn_stop") || _commandName.EndsWith("zurn_size");
if (!skull && !bounds && !repair && !bag && !zone)
{
return true;
}
@@ -1299,6 +1351,12 @@ namespace NecromancerTome
__result = composite.OnBlockActivated(null, "TEFeatureStorage:Search", world, _blockPos, world.GetBlock(_blockPos), _player);
return false;
}
if (zone)
{
urn.ZoneCommand(_commandName, _player);
__result = true;
return false;
}
if (skull)
{
CinerariumContents.Open(_player, urn);
@@ -1408,7 +1466,13 @@ namespace NecromancerTome
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
bool wall = TEFeatureCinerarium.IsLogistWallBlock(bv.Block);
bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(wall
bool repairUrn = TEFeatureCinerarium.IsRepairBlock(bv.Block);
bool lootUrn = TEFeatureCinerarium.IsLootBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(repairUrn
? (urn.HasSkull ? TEFeatureCinerarium.RepairSkullBlockName : TEFeatureCinerarium.RepairBlockName)
: lootUrn
? (urn.HasSkull ? TEFeatureCinerarium.LootSkullBlockName : TEFeatureCinerarium.LootBlockName)
: wall
? (urn.HasSkull ? TEFeatureCinerarium.LogistWallSkullBlockName : TEFeatureCinerarium.LogistWallBlockName)
: logist
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName)
+709
View File
@@ -0,0 +1,709 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Урны с большой зоной - ремонтник и лутер (решения 17.10 и 18.10, BACKLOG «Урна ремонтника», «Урна лутера»).
///
/// Общее. Зона - квадрат со стороной блока притязания (LandClaimSize сервера, по умолчанию 41) или меньше (31/21/11, пункт
/// меню «Размер зоны»), по высоте - весь столб мира; зоны таких урн могут пересекаться. Работа - только по кнопке (17.10: «А
/// если сделать рубильник или кнопку. Включил, ремонтник начал работу»): «Запустить» - один проход по зоне, конец прохода -
/// снова ожидание; до нажатия урна ничего не считает. Кнопки - на самой урне с четырёх сторон («как у дверного звонка, но с
/// черепом»), их цвет - состояние (CinerariumButtons): не горят - нет черепа; жёлтая ровно - ожидание; жёлтая мигает - не
/// хватает материалов (ремонтник) или ящик полон (лутер); зелёная - работа; красная - прилив (работа стоит, дух в урне).
///
/// Ремонтник. Обход - по столбам зоны, порциями из общего бюджета клеток на кадр (CellsPerFrame на все урны); в столбе - от
/// низа до высоты чанка, земля мира (форма terrain) и воздух пропускаются сразу. Найденный повреждённый блок с RepairItems
/// встаёт в очередь (не длиннее QueueMax - обход ждёт), блок берёт тот, кто нашёл первым (Claimed). Ремонт - неспешно: одно
/// действие в RepairSeconds, дух летит к блоку и машет руками; материалы - RepairItems блока, доля по повреждению (как молотком),
/// из ящиков с нитями к урне. Не хватает - кнопка мигает, урна ждёт (положил в ящик - продолжит сама). Разрушенное не
/// восстанавливает - только повреждённое (17.10: «Восстанавливаю порушенное, затем включаю ремонтника и ухожу по делам»).
///
/// Лутер. За действие - одна цель в зоне: предмет на земле, затем мешок с зомби, затем красный труп (тем же путём, что игрок:
/// LootBagOpened + NecroSkull.Roll, стадия добычи - того, кто нажал кнопку); добыча - в ящик с любой нитью к урне. Ящики
/// зданий, аэродроп и рюкзаки погибших игроков не трогает (18.10 «Принимаю всё»). Целей нет - проход окончен.
/// </summary>
public partial class TEFeatureCinerarium
{
public const string RepairBlockName = "necroCinerariumRepair";
public const string RepairSkullBlockName = "necroCinerariumRepairSkull";
public const string LootBlockName = "necroCinerariumLoot";
public const string LootSkullBlockName = "necroCinerariumLootSkull";
public static bool IsRepairBlock(Block _b)
{
string n = _b?.GetBlockName();
return n == RepairBlockName || n == RepairSkullBlockName;
}
public static bool IsLootBlock(Block _b)
{
string n = _b?.GetBlockName();
return n == LootBlockName || n == LootSkullBlockName;
}
bool _repair, _loot;
public bool IsRepairer
{
get
{
_ = IsLogist;
return _repair;
}
}
public bool IsLooter
{
get
{
_ = IsLogist;
return _loot;
}
}
public bool IsBigZone => IsRepairer || IsLooter;
/// <summary>Сторона зоны, выбранная в меню (0 - по притязанию). Сохраняется (v6).</summary>
public int ZoneSide;
public bool ZoneRunning;
/// <summary>Обход ремонтника: номер следующего столба зоны (строка за строкой).</summary>
public int ZoneCursor;
/// <summary>Кто нажал «Запустить» - стадия добычи лутера.</summary>
public int ZonePressedBy = -1;
/// <summary>Не хватает материалов / ящик полон - кнопка мигает жёлтым.</summary>
public bool ZoneLacking;
public static float RepairSeconds = 2.5f;
public static float LootSeconds = 1.5f;
public static int CellsPerFrame = 4000;
public static int QueueMax = 32;
static int _cellsFrame = -1, _cellsUsed;
/// <summary>Блоки, взятые ремонтниками в очередь (зоны пересекаются - чинит взявший первым).</summary>
static readonly HashSet<Vector3i> Claimed = new HashSet<Vector3i>();
readonly List<Vector3i> _queue = new List<Vector3i>();
float _zoneNext;
bool _zonePending;
float _zoneActAt;
Vector3i _zoneTarget;
int _zoneTargetEntity = -1;
/// <summary>Стороны зоны: притязание сервера и меньшие нечётные (31, 21, 11), не больше притязания.</summary>
public static List<int> ZoneSides()
{
int claim = Mathf.Max(11, GamePrefs.GetInt(EnumGamePrefs.LandClaimSize));
List<int> l = new List<int> { claim };
foreach (int s in new[] { 31, 21, 11 })
{
if (s < claim)
{
l.Add(s);
}
}
return l;
}
public int ZoneSideNow
{
get
{
List<int> l = ZoneSides();
return ZoneSide > 0 && l.Contains(ZoneSide) ? ZoneSide : l[0];
}
}
public enum ButtonState
{
Off,
Wait,
Lacking,
Busy,
Tide
}
public ButtonState Buttons
{
get
{
if (!HasSkull)
{
return ButtonState.Off;
}
if (CinerariumLoad.Tide)
{
return ButtonState.Tide;
}
if (!ZoneRunning)
{
return ButtonState.Wait;
}
return ZoneLacking ? ButtonState.Lacking : ButtonState.Busy;
}
}
string ZoneStateText()
{
string key = Buttons switch
{
ButtonState.Tide => "necroZoneUrnTide",
ButtonState.Lacking => IsLooter ? "necroZoneUrnFull" : "necroZoneUrnNoMaterials",
ButtonState.Busy => "necroZoneUrnBusy",
ButtonState.Wait => "necroZoneUrnWait",
_ => null
};
string side = ZoneSideNow.ToString();
return (key != null ? ", " + Localization.Get(key) : "") + ", " + string.Format(Localization.Get("necroZoneUrnSize"), side);
}
/// <summary>Пункты меню на E: запустить, остановить, размер зоны (по кругу).</summary>
public void ZoneCommand(string _cmd, EntityPlayerLocal _player)
{
if (_cmd.EndsWith("zurn_run"))
{
if (!HasSkull)
{
return;
}
ZoneRunning = true;
ZoneCursor = 0;
ZoneLacking = false;
ZonePressedBy = _player != null ? _player.entityId : -1;
_queue.Clear();
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4");
Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " запущен, зона " + ZoneSideNow);
}
else if (_cmd.EndsWith("zurn_stop"))
{
ZoneStop(true);
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), "button4");
}
else if (_cmd.EndsWith("zurn_size"))
{
List<int> l = ZoneSides();
int i = l.IndexOf(ZoneSideNow);
ZoneSide = l[(i + 1) % l.Count];
if (_player != null)
{
GameManager.ShowTooltip(_player, string.Format(Localization.Get("necroZoneUrnSize"), ZoneSide.ToString()));
}
if (ZoneShown)
{
RemoveBounds();
SpawnBounds();
}
}
SetModified();
}
/// <summary>Остановить проход (кнопка, урна убрана): очередь и взятые блоки отпускаются.</summary>
public void ZoneStop(bool _log)
{
foreach (Vector3i p in _queue)
{
Claimed.Remove(p);
}
_queue.Clear();
_zonePending = false;
if (ZoneRunning && _log)
{
Debug.Log("[NecromancerTome] Cinerarium: " + (IsLooter ? "лутер" : "ремонтник") + " " + ToWorldPos() + " остановлен");
}
ZoneRunning = false;
ZoneLacking = false;
}
void ZoneWork(World _world)
{
SyncButtons();
if (!ZoneRunning)
{
return;
}
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
if (_zonePending)
{
if (Time.time < _zoneActAt)
{
return;
}
_zonePending = false;
if (IsRepairer)
{
RepairNow(_world, _zoneTarget);
}
else
{
LootNow(_world, _zoneTargetEntity);
}
return;
}
if (IsRepairer)
{
ScanColumns(_world);
}
if (Time.time < _zoneNext || !CinerariumLoad.TryTakeStep())
{
return;
}
_zoneNext = Time.time + (IsRepairer ? RepairSeconds : LootSeconds);
if (IsRepairer)
{
RepairPick(_world, motion);
}
else
{
LootPick(_world, motion);
}
}
/// <summary>Слетать к месту и сделать дело по прилёте (как дух сборщика) или сразу, если духа нет.</summary>
void FlyThenAct(World _world, CinerariumGenieMotion _motion, Vector3i _at)
{
_zonePending = true;
_zoneActAt = Time.time;
if (_motion != null)
{
List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)>
{
(Stand(_world, _at, out bool beside) - Origin.position, World.blockToTransformPos(_at) - Origin.position, beside)
};
_zoneActAt = Time.time + _motion.Visit(stops);
}
}
// ------------------------------------------------------------------
// Ремонтник.
// ------------------------------------------------------------------
void ScanColumns(World _world)
{
if (_queue.Count >= QueueMax)
{
return;
}
if (Time.frameCount != _cellsFrame)
{
_cellsFrame = Time.frameCount;
_cellsUsed = 0;
}
int side = ZoneSideNow, half = side / 2, total = side * side;
Vector3i c = ToWorldPos();
while (ZoneCursor < total && _cellsUsed < CellsPerFrame && _queue.Count < QueueMax)
{
int x = c.x - half + ZoneCursor % side, z = c.z - half + ZoneCursor / side;
ZoneCursor++;
if (!(_world.GetChunkFromWorldPos(x, 0, z) is Chunk ch))
{
continue; // не загружен - пропустить столб
}
int lx = World.toBlockXZ(x), lz = World.toBlockXZ(z);
int top = Mathf.Min(254, ch.GetHeight(lx, lz) + 1);
_cellsUsed += top;
for (int y = 1; y <= top; y++)
{
BlockValue bv = ch.GetBlock(lx, y, lz);
if (bv.isair || bv.ischild || bv.damage <= 0)
{
continue;
}
Block b = bv.Block;
if (b == null || b.shape.IsTerrain() || b.RepairItems == null || b.RepairItems.Count == 0)
{
continue;
}
Vector3i p = new Vector3i(x, y, z);
if (Claimed.Add(p))
{
_queue.Add(p);
}
}
}
if (ZoneCursor >= total && _queue.Count == 0 && !_zonePending)
{
Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + c + " обошёл зону - ожидание");
ZoneStop(false);
SetModified();
}
}
void RepairPick(World _world, CinerariumGenieMotion _motion)
{
while (_queue.Count > 0)
{
Vector3i p = _queue[0];
BlockValue bv = _world.GetBlock(p);
if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
{
_queue.RemoveAt(0);
Claimed.Remove(p);
continue; // починили до нас или блок убран
}
if (!HasMaterials(_world, bv, false))
{
if (!ZoneLacking)
{
Debug.Log("[NecromancerTome] Cinerarium: ремонтнику " + ToWorldPos() + " не хватает материалов для " + bv.Block.GetBlockName() + " в " + p);
}
ZoneLacking = true;
return; // ждём: положат в ящик - продолжим с того же блока
}
ZoneLacking = false;
_zoneTarget = p;
FlyThenAct(_world, _motion, p);
return;
}
}
void RepairNow(World _world, Vector3i _p)
{
_queue.Remove(_p);
Claimed.Remove(_p);
BlockValue bv = _world.GetBlock(_p);
if (bv.isair || bv.damage <= 0 || bv.Block?.RepairItems == null)
{
return;
}
if (!HasMaterials(_world, bv, true))
{
ZoneLacking = true;
_queue.Insert(0, _p);
Claimed.Add(_p);
return;
}
int was = bv.damage;
bv.damage = 0;
_world.SetBlockRPC(_p, bv);
Debug.Log("[NecromancerTome] Cinerarium: ремонтник " + ToWorldPos() + " починил " + bv.Block.GetBlockName() + " в " + _p + " (было повреждение " + was + ")");
}
/// <summary>Хватает ли в ящиках с нитями к урне материалов на ремонт (RepairItems блока, доля по повреждению, не меньше
/// одного); _take - сразу и забрать.</summary>
bool HasMaterials(World _world, BlockValue _bv, bool _take)
{
Block b = _bv.Block;
float share = Mathf.Clamp01(_bv.damage / (float)Mathf.Max(1, b.MaxDamage));
List<TEFeatureStorage> boxes = ThreadBoxes(_world);
foreach (Block.SItemNameCount need in b.RepairItems)
{
ItemValue iv = ItemClass.GetItem(need.ItemName);
if (iv == null || iv.type == 0)
{
continue;
}
int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share));
int have = 0;
foreach (TEFeatureStorage s in boxes)
{
have += Count(s, iv);
}
if (have < n)
{
return false;
}
}
if (!_take)
{
return true;
}
foreach (Block.SItemNameCount need in b.RepairItems)
{
ItemValue iv = ItemClass.GetItem(need.ItemName);
if (iv == null || iv.type == 0)
{
continue;
}
int n = Mathf.Max(1, Mathf.CeilToInt(need.Count * share));
foreach (TEFeatureStorage s in boxes)
{
n -= TakeFrom(s, iv, n);
if (n <= 0)
{
break;
}
}
}
return true;
}
static int Count(TEFeatureStorage _s, ItemValue _iv)
{
int n = 0;
for (int i = 0; i < _s.ItemGrid.Length; i++)
{
ItemStack st = _s.ItemGrid[i];
if (st != null && !st.IsEmpty() && st.itemValue.type == _iv.type && !CinerariumWork.Locked(_s, i))
{
n += st.count;
}
}
return n;
}
static int TakeFrom(TEFeatureStorage _s, ItemValue _iv, int _n)
{
int took = 0;
for (int i = 0; i < _s.ItemGrid.Length && took < _n; i++)
{
ItemStack st = _s.ItemGrid[i];
if (st == null || st.IsEmpty() || st.itemValue.type != _iv.type || CinerariumWork.Locked(_s, i))
{
continue;
}
int k = Mathf.Min(st.count, _n - took);
CinerariumWork.Take(_s, i, k);
took += k;
}
return took;
}
/// <summary>Ящики урны по нитям: у ремонтника - «откуда» (материалы), у лутера - любая нить (куда класть).</summary>
List<TEFeatureStorage> ThreadBoxes(World _world)
{
List<TEFeatureStorage> l = new List<TEFeatureStorage>();
void Add(Vector3i _p)
{
TEFeatureStorage s = CinerariumWork.BoxAt(_world, _p);
if (s != null && !l.Contains(s))
{
l.Add(s);
}
}
foreach (Vector3i p in ThreadFrom)
{
Add(p);
}
if (IsLooter)
{
if (HasThreadTo)
{
Add(ThreadTo);
}
foreach (Vector3i p in ThreadOut)
{
Add(p);
}
}
return l;
}
// ------------------------------------------------------------------
// Лутер.
// ------------------------------------------------------------------
static readonly List<Entity> _ents = new List<Entity>();
/// <summary>Цель лутера в зоне: предмет на земле, потом мешок с зомби, потом красный труп. Ничего - проход окончен.</summary>
void LootPick(World _world, CinerariumGenieMotion _motion)
{
List<TEFeatureStorage> boxes = ThreadBoxes(_world);
if (boxes.Count == 0)
{
ZoneLacking = true; // некуда класть - как полный ящик
return;
}
int side = ZoneSideNow;
Vector3 c = World.blockToTransformPos(ToWorldPos());
Bounds bb = new Bounds(new Vector3(c.x, 128f, c.z), new Vector3(side, 256f, side));
Entity best = null;
int bestRank = int.MaxValue;
_ents.Clear();
_world.GetEntitiesInBounds(typeof(Entity), bb, _ents);
foreach (Entity e in _ents)
{
int rank = LootRank(e);
if (rank < bestRank && !LockManager.Instance.IsLockedServer(e, 0))
{
best = e;
bestRank = rank;
}
}
_ents.Clear();
if (best == null)
{
Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " собрал всё в зоне - ожидание");
ZoneStop(false);
SetModified();
return;
}
ZoneLacking = false;
_zoneTargetEntity = best.entityId;
FlyThenAct(_world, _motion, World.worldToBlockPos(best.GetPosition()));
}
/// <summary>Что собирать и в каком порядке (меньше - раньше); int.MaxValue - не трогать. Рюкзаки игроков (EntityBackpack) и
/// аэродроп (EntitySupplyCrate) - не трогать.</summary>
static int LootRank(Entity _e)
{
if (_e == null || _e.IsDespawned)
{
return int.MaxValue;
}
if (_e is EntityBackpack || _e is EntitySupplyCrate)
{
return int.MaxValue;
}
if (_e is EntityLootContainer lc)
{
return lc.bag != null ? 1 : int.MaxValue;
}
if (_e is EntityItem it && !(_e is EntityContainerAbs))
{
return it.itemStack != null && !it.itemStack.IsEmpty() ? 0 : int.MaxValue;
}
if (_e is EntityAlive a && a.IsDead() && CorpseLoot.IsSearchable(a) && a.bag != null)
{
return 2;
}
return int.MaxValue;
}
void LootNow(World _world, int _id)
{
Entity e = _world.GetEntity(_id);
if (e == null || LootRank(e) == int.MaxValue || LockManager.Instance.IsLockedServer(e, 0))
{
return;
}
List<TEFeatureStorage> boxes = ThreadBoxes(_world);
if (e is EntityItem it && !(e is EntityContainerAbs))
{
ItemStack s = it.itemStack.Clone();
int before = s.count;
CinerariumWork.Place(s, boxes);
if (s.count == before)
{
ZoneLacking = true;
return;
}
if (s.count <= 0)
{
_world.RemoveEntity(e.entityId, EnumRemoveEntityReason.Despawned);
}
else
{
it.itemStack = s;
}
Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " подобрал " + s.itemValue.ItemClass?.GetItemName() + " x" + (before - s.count));
return;
}
Bag bag = e is EntityAlive a ? a.bag : (e as EntityLootContainer)?.bag;
if (bag == null)
{
return;
}
if (!bag.Touched)
{
// Как игрок: первое открытие наполняет сумку по списку добычи (стадия - нажавшего кнопку); у красного трупа - и
// находка черепа (CorpseLootPatch: OnLockRequestServer).
int who = ZonePressedBy >= 0 ? ZonePressedBy : (_world.GetPrimaryPlayer()?.entityId ?? -1);
GameManager.Instance.lootManager.LootBagOpened(bag, e, who);
if (e is EntityAlive corpse)
{
NecroSkull.Roll(corpse, bag);
}
}
bool moved = false, left = false;
ItemStackGrid slots = bag.ItemGrid;
for (int i = 0; i < slots.Length; i++)
{
if (slots[i] == null || slots[i].IsEmpty())
{
continue;
}
ItemStack s = slots[i].Clone();
int before = s.count;
CinerariumWork.Place(s, boxes);
if (s.count != before)
{
moved = true;
}
if (s.count > 0)
{
left = true;
}
bag.SetSlot(i, s.count > 0 ? s : ItemStack.Empty.Clone());
}
ZoneLacking = left; // не всё влезло - ящик полон, кнопка мигает
Debug.Log("[NecromancerTome] Cinerarium: лутер " + ToWorldPos() + " обыскал " + e.GetType().Name + " " + e.entityId + (left ? " - не всё влезло" : ""));
}
// ------------------------------------------------------------------
// Кнопки на модели.
// ------------------------------------------------------------------
void SyncButtons()
{
Transform t = ModelTransform();
if (t == null)
{
return;
}
CinerariumButtons b = t.GetComponent<CinerariumButtons>();
if (b == null)
{
b = t.gameObject.AddComponent<CinerariumButtons>();
}
b.State = Buttons;
}
}
/// <summary>Кнопки-звонки с черепом на урне ремонтника/лутера: рендереры модели с «button» в имени. Цвет свечения - по
/// состоянию урны (TEFeatureCinerarium.Buttons), жёлтая мигает - нехватка.</summary>
[Preserve]
public class CinerariumButtons : MonoBehaviour
{
public TEFeatureCinerarium.ButtonState State;
List<Renderer> _buttons;
static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock();
static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor");
public static Color Yellow = new Color(2.2f, 1.6f, 0.15f);
public static Color Green = new Color(0.3f, 2.2f, 0.4f);
public static Color Red = new Color(2.4f, 0.15f, 0.1f);
Color _last = new Color(-1f, 0f, 0f);
public static bool IsButton(Renderer _r) => _r != null && _r.name.ToLowerInvariant().Contains("button");
void Update()
{
if (_buttons == null)
{
_buttons = new List<Renderer>();
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
{
if (IsButton(r))
{
_buttons.Add(r);
}
}
}
Color c;
switch (State)
{
case TEFeatureCinerarium.ButtonState.Wait: c = Yellow; break;
case TEFeatureCinerarium.ButtonState.Lacking: c = Mathf.Repeat(Time.time, 0.8f) < 0.4f ? Yellow : Color.black; break;
case TEFeatureCinerarium.ButtonState.Busy: c = Green; break;
case TEFeatureCinerarium.ButtonState.Tide: c = Red; break;
default: c = Color.black; break;
}
if (c == _last)
{
return;
}
_last = c;
foreach (Renderer r in _buttons)
{
if (r == null)
{
continue;
}
r.GetPropertyBlock(_block);
_block.SetColor(EmissionColorId, c);
r.SetPropertyBlock(_block);
}
}
}
}
+89 -1
View File
@@ -46,6 +46,43 @@ namespace NecromancerTome
case "necrohold": Hold(pl, _params); break;
case "necrobox": Box(w, _params); break;
case "necrourn": Urn(w, _params); break;
case "necrodamage":
{
// necrodamage x y z урон - повредить блок (проверка ремонтника)
Vector3i dp = P(_params, 0);
BlockValue dbv = w.GetBlock(dp);
dbv.damage = Mathf.Min(dbv.Block.MaxDamage - 1, dbv.damage + int.Parse(_params[3]));
w.SetBlockRPC(dp, dbv);
Out("necrodamage: " + dbv.Block.GetBlockName() + " в " + dp + " повреждение " + dbv.damage + "/" + dbv.Block.MaxDamage);
break;
}
case "necrodrop":
{
// necrodrop предмет число x y z - бросить на землю (проверка лутера)
ItemValue div = ItemClass.GetItem(_params[0], true);
GameManager.Instance.ItemDropServer(new ItemStack(div, int.Parse(_params[1])), P(_params, 2).ToVector3() + new Vector3(0.5f, 0.5f, 0.5f), Vector3.zero);
Out("necrodrop: " + _params[0] + " x" + _params[1]);
break;
}
case "necrobag":
{
// necrobag x y z - мешок с зомби (EntityLootContainer со списком cntDropBag), нетронутый
Entity bag = EntityFactory.CreateEntity(EntityClass.FromString("DroppedLootContainer"), P(_params, 0).ToVector3() + new Vector3(0.5f, 0.3f, 0.5f));
bag.lootList = "cntDropBag";
w.SpawnEntityInWorld(bag);
Out("necrobag: " + bag.entityId);
break;
}
case "necrocorpse":
{
// necrocorpse x y z [класс] - зомби, убитый и помеченный красным трупом (как VictimPatch)
string cls = _params.Count > 3 ? _params[3] : "zombieArlene";
EntityAlive z = EntityFactory.CreateEntity(EntityClass.FromString(cls), P(_params, 0).ToVector3() + new Vector3(0.5f, 0.1f, 0.5f)) as EntityAlive;
w.SpawnEntityInWorld(z);
ThreadManager.StartCoroutine(KillAndMark(z));
Out("necrocorpse: " + z.entityId + " " + cls);
break;
}
case "necrogod": God(pl, _params.Count == 0 || _params[0] != "off"); break;
case "necroclick":
// necroclick 0|1 [x y z] - по блоку под прицелом или по указанной клетке (луч не всегда попадает в низкий череп).
@@ -99,6 +136,18 @@ namespace NecromancerTome
Out("necrogod: " + (_on ? "бог, ИИ не видит" : "обычный"));
}
static System.Collections.IEnumerator KillAndMark(EntityAlive _z)
{
yield return new WaitForSeconds(1f);
_z.DamageEntity(new DamageSource(EnumDamageSource.Internal, EnumDamageTypes.Suicide), 99999, false);
yield return new WaitForSeconds(1f);
if (_z.IsDead())
{
CorpseLoot.MarkCorpse(_z);
}
UnityEngine.Debug.Log("[NecromancerTome] mailbox: necrocorpse: " + _z.entityId + (_z.IsDead() ? " убит и помечен" : " НЕ умер"));
}
static void Aim(World _w, EntityPlayerLocal _pl)
{
StringBuilder sb = new StringBuilder("necroaim: игрок ").Append(_pl.position.ToString("F1")).Append(", взгляд ").Append(_pl.rotation.ToString("F0"));
@@ -247,6 +296,9 @@ namespace NecromancerTome
case "sample":
u.SetSample(_p.Count > 5 ? new ItemStack(ItemClass.GetItem(_p[4], true), int.Parse(_p[5])) : ItemStack.Empty.Clone());
break;
case "run": u.ZoneCommand("zurn_run", GameManager.Instance.World.GetPrimaryPlayer()); break;
case "stop": u.ZoneCommand("zurn_stop", null); break;
case "size": u.ZoneSide = int.Parse(_p[4]); u.SetModified(); break;
case "group":
u.GroupMask = _p[4] == "нет" || _p[4] == "none" ? 0u : CinerariumGroups.All;
u.SetModified();
@@ -262,7 +314,7 @@ namespace NecromancerTome
}
Out("necrourn: " + pos + " " + _w.GetBlock(pos).Block?.GetBlockName() + ", череп " + u.HasSkull + ", застрял " + u.Stuck + ", мигание " + blink +
", откуда " + u.ThreadFrom.Count + ", куда " + (u.HasThreadTo ? u.ThreadTo.ToString() : "нет") + ", от урны " + u.ThreadOut.Count +
", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count));
", зона " + (u.IsBigZone ? u.ZoneSideNow + (u.ZoneRunning ? " идёт" : " ждёт") + (u.ZoneLacking ? " нехватка" : "") + " курсор " + u.ZoneCursor : "-") + ", образец " + (u.Sample == null || u.Sample.IsEmpty() ? "нет" : u.Sample.itemValue.ItemClass?.GetItemName() + "x" + u.Sample.count));
}
}
@@ -337,4 +389,40 @@ namespace NecromancerTome
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrogod", _params);
}
[Preserve]
public class ConsoleCmdNecroDamage : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrodamage" };
public override string getDescription() => "NecromancerTome (разработка): повредить блок x y z урон";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrodamage", _params);
}
[Preserve]
public class ConsoleCmdNecroDrop : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrodrop" };
public override string getDescription() => "NecromancerTome (разработка): бросить предмет: имя число x y z";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrodrop", _params);
}
[Preserve]
public class ConsoleCmdNecroBag : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrobag" };
public override string getDescription() => "NecromancerTome (разработка): мешок с зомби x y z";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrobag", _params);
}
[Preserve]
public class ConsoleCmdNecroCorpse : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrocorpse" };
public override string getDescription() => "NecromancerTome (разработка): красный труп x y z [класс]";
public override bool AllowedInMainMenu => false;
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo) => NecroDev.Run("necrocorpse", _params);
}
}
Binary file not shown.
Binary file not shown.