Урна рабочего: изготовление по образцу на станциях по нитям (CinerariumCraft), образец во второй ячейке, нити к станциям, топливо и огонь

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-18 00:11:56 +03:00
co-authored by Claude Opus 5.5
parent a5f166c161
commit eba17606d5
9 changed files with 843 additions and 40 deletions
+135 -21
View File
@@ -41,12 +41,26 @@ namespace NecromancerTome
string n = _b?.GetBlockName();
return n == LogistBlockName || n == LogistSkullBlockName;
}
/// <summary>Урна рабочего (17.10, BACKLOG «Урна рабочего»): изготавливает предмет из ячейки образца на станциях, к которым
/// от неё тянутся нити; ингредиенты - из ящиков с нитями к урне, готовое - в ящики с нитями от урны. Без зоны и вне лимита,
/// как логист.</summary>
public const string WorkerBlockName = "necroCinerariumWorker";
public const string WorkerSkullBlockName = "necroCinerariumWorkerSkull";
public static bool IsWorkerBlock(Block _b)
{
string n = _b?.GetBlockName();
return n == WorkerBlockName || n == WorkerSkullBlockName;
}
/// <summary>Урна без зоны (логист, рабочий): ставится вплотную к любым урнам, в лимит не входит, работает по нитям.</summary>
public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b);
public const string SkullItemName = "necroSkull";
public const int HalfWidth = 2;
public const int Below = 1;
public const int Above = 4;
public const float KillsPerUrn = 500f;
const byte SaveVersion = 3; // 2 - нити, 3 - фильтр логиста (16.10)
const byte SaveVersion = 4; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10)
public bool HasSkull;
public bool ZoneShown;
@@ -54,7 +68,23 @@ namespace NecromancerTome
public readonly List<Vector3i> ThreadFrom = new List<Vector3i>();
public bool HasThreadTo;
public Vector3i ThreadTo;
public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0;
public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0 || ThreadOut.Count > 0;
/// <summary>Нити рабочего «от урны»: станции (где изготавливать) и ящики (куда класть готовое), главные клетки блоков.</summary>
public readonly List<Vector3i> ThreadOut = new List<Vector3i>();
/// <summary>Образец рабочего: что изготавливать; число в стопке - сколько держать в запасе (ящики «куда» и выход станций).
/// Лежит в «Содержимом» урны второй ячейкой.</summary>
public ItemStack Sample = ItemStack.Empty.Clone();
/// <summary>Урна рабочего - по блоку, как логист.</summary>
public bool IsWorker
{
get
{
_ = IsLogist; // узнаёт тип блока
return _worker;
}
}
bool _worker;
public bool IsZoneless => IsLogist || IsWorker;
/// <summary>Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.</summary>
public bool IsLogist
{
@@ -67,6 +97,7 @@ namespace NecromancerTome
if (IsUrn(bv.Block))
{
_logist = IsLogistBlock(bv.Block);
_worker = IsWorkerBlock(bv.Block);
_logistKnown = true;
}
}
@@ -96,6 +127,9 @@ namespace NecromancerTome
ThreadFrom.AddRange(other.ThreadFrom);
HasThreadTo = other.HasThreadTo;
ThreadTo = other.ThreadTo;
ThreadOut.Clear();
ThreadOut.AddRange(other.ThreadOut);
Sample = other.Sample.Clone();
GroupMask = other.GroupMask;
_threadsDirty = true;
}
@@ -106,15 +140,16 @@ namespace NecromancerTome
{
base.OnAdded(_blockPos, _blockValue);
_logist = IsLogistBlock(_blockValue.Block);
_worker = IsWorkerBlock(_blockValue.Block);
_logistKnown = true;
string placed = _blockValue.Block?.GetBlockName();
if (placed == SkullBlockName || placed == LogistSkullBlockName)
if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == WorkerSkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
if (ConnectionManager.Instance.IsServer)
{
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist);
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker);
}
}
@@ -144,6 +179,12 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - череп выпал");
}
}
if (Sample != null && !Sample.IsEmpty() && _world != null)
{
_world.gameManager.ItemDropServer(Sample.Clone(), World.blockToTransformPos(pos) + Vector3.up * 0.7f, Vector3.zero);
Sample = ItemStack.Empty.Clone();
Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - образец выпал");
}
}
public override void OnUnload(World _world)
@@ -170,6 +211,13 @@ namespace NecromancerTome
CinerariumWriteHelper.WriteInts(_bw, t.ToArray());
// Второе число - сколько групп было при записи: новые группы (в конец) у старых урн включаются по правилу ниже.
CinerariumWriteHelper.WriteInts(_bw, new[] { unchecked((int)GroupMask), CinerariumGroups.Count });
List<int> o = new List<int> { ThreadOut.Count };
foreach (Vector3i p in ThreadOut)
{
o.Add(p.x); o.Add(p.y); o.Add(p.z);
}
CinerariumWriteHelper.WriteInts(_bw, o.ToArray());
(Sample ?? ItemStack.Empty).Clone().Write(_bw);
}
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
@@ -224,6 +272,23 @@ namespace NecromancerTome
GroupMask = on ? GroupMask | (1u << g) : GroupMask & ~(1u << g);
}
}
ThreadOut.Clear();
Sample = ItemStack.Empty.Clone();
if (version >= 4)
{
int n = _br.ReadInt32();
int[] v = new int[n];
for (int i = 0; i < n; i++)
{
v[i] = _br.ReadInt32();
}
int cnt = n > 0 ? v[0] : 0;
for (int i = 0, k = 1; i < cnt && k + 2 < n; i++, k += 3)
{
ThreadOut.Add(new Vector3i(v[k], v[k + 1], v[k + 2]));
}
Sample = ItemStack.Empty.Clone().Read(_br);
}
_threadsDirty = true;
if (version > SaveVersion)
{
@@ -296,10 +361,26 @@ namespace NecromancerTome
ThreadsChanged();
}
public bool AddThreadOut(Vector3i _target)
{
if (ThreadOut.Contains(_target))
{
return true;
}
if (ThreadOut.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadOut.Add(_target);
ThreadsChanged();
return true;
}
public int ClearThreads()
{
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0);
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0) + ThreadOut.Count;
ThreadFrom.Clear();
ThreadOut.Clear();
HasThreadTo = false;
if (n > 0)
{
@@ -310,7 +391,7 @@ namespace NecromancerTome
public int RemoveThreadsTo(Vector3i _box)
{
int n = ThreadFrom.RemoveAll(p => p == _box);
int n = ThreadFrom.RemoveAll(p => p == _box) + ThreadOut.RemoveAll(p => p == _box);
if (HasThreadTo && ThreadTo == _box)
{
HasThreadTo = false;
@@ -328,16 +409,17 @@ namespace NecromancerTome
_threadsDirty = true;
_glowState = -1; // логист - другие глаза
SetModified();
Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет"));
Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет") + (ThreadOut.Count > 0 ? ", рабочему " + ThreadOut.Count : ""));
}
/// <summary>Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.</summary>
void SyncThreads(World _world)
{
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsLogist && _logistKnown)
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsZoneless && _logistKnown)
{
Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у урны-логиста");
Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у логиста и рабочего");
ThreadFrom.Clear();
ThreadOut.Clear();
HasThreadTo = false;
ThreadsChanged();
}
@@ -362,6 +444,15 @@ namespace NecromancerTome
HasThreadTo = false;
gone = true;
}
foreach (Vector3i p in ThreadOut.ToArray())
{
bool box = CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u3) && u3 == null;
if (!box && CinerariumThreads.StationAt(_world, p) == null && _world.GetChunkFromWorldPos(p) != null)
{
ThreadOut.Remove(p);
gone = true;
}
}
if (gone)
{
ThreadsChanged();
@@ -399,6 +490,10 @@ namespace NecromancerTome
{
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me)));
}
foreach (Vector3i p in ThreadOut)
{
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(p, me)));
}
_threadView.GetComponent<CinerariumThreadView>().Build(pairs);
}
@@ -522,6 +617,20 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут"));
}
/// <summary>Образец рабочего положен, изменён или вынут.</summary>
public void SetSample(ItemStack _s)
{
ItemStack was = Sample ?? ItemStack.Empty;
if (was.itemValue.type == _s.itemValue.type && was.count == _s.count)
{
return;
}
Sample = _s;
SetModified();
Debug.Log("[NecromancerTome] Cinerarium: образец урны " + ToWorldPos() + " - " +
(_s.IsEmpty() ? "вынут" : _s.itemValue.ItemClass?.GetItemName() + " x" + _s.count));
}
/// <summary>«Починить», как у транспорта: целиком, за RepairBones костей из рюкзака (сколько берёт и молоток -
/// RepairItems в blocks.xml).</summary>
public const string RepairItemName = "resourceBone";
@@ -574,7 +683,7 @@ namespace NecromancerTome
SyncGlow();
if (HasSkull && _genie == null)
{
_genie = CinerariumGenie.Spawn(ModelTransform());
_genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f);
if (_genie != null && _sound)
{
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
@@ -754,7 +863,8 @@ namespace NecromancerTome
public static bool IsUrn(Block _b)
{
string n = _b?.GetBlockName();
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName;
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName ||
n == WorkerBlockName || n == WorkerSkullBlockName;
}
public static int UrnLimit(EntityPlayer _player)
@@ -766,13 +876,13 @@ namespace NecromancerTome
/// <summary>Ключ подсказки, почему урну сюда нельзя, или null.</summary>
public static string PlacementRefusal(Vector3i _pos, Block _placing)
{
if (!IsLogistBlock(_placing) && CinerariumRegistry.Overlaps(_pos))
if (!IsZonelessBlock(_placing) && CinerariumRegistry.Overlaps(_pos))
{
return "necroCinerariumOverlap";
}
if (IsLogistBlock(_placing))
if (IsZonelessBlock(_placing))
{
return null; // логисты в лимит не входят (16.10, пользователь)
return null; // логисты и рабочие в лимит не входят (16.10, 17.10 - пользователь)
}
World world = GameManager.Instance?.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
@@ -892,7 +1002,7 @@ namespace NecromancerTome
{
// Логисты не считаются; записанные до метки «L» узнаются по блоку в мире (чанк рядом загружен).
if (kv.Value == id && !_logists.Contains(kv.Key) &&
!(world != null && TEFeatureCinerarium.IsLogistBlock(world.GetBlock(kv.Key).Block)))
!(world != null && TEFeatureCinerarium.IsZonelessBlock(world.GetBlock(kv.Key).Block)))
{
n++;
}
@@ -914,9 +1024,9 @@ namespace NecromancerTome
World world = GameManager.Instance?.World;
foreach (Vector3i p in Urns().Keys)
{
if (world != null && TEFeatureCinerarium.IsLogistBlock(world.GetBlock(p).Block))
if (world != null && TEFeatureCinerarium.IsZonelessBlock(world.GetBlock(p).Block))
{
continue; // у логиста зоны нет
continue; // у логиста и рабочего зоны нет
}
if (p != _pos && Math.Abs(p.x - _pos.x) <= w && Math.Abs(p.z - _pos.z) <= w && Math.Abs(p.y - _pos.y) <= h)
{
@@ -969,11 +1079,11 @@ namespace NecromancerTome
}
else if (text.EndsWith("bounds_show"))
{
_commands[i].enabled = !urn.ZoneShown && !urn.IsLogist;
_commands[i].enabled = !urn.ZoneShown && !urn.IsZoneless;
}
else if (text.EndsWith("bounds_hide"))
{
_commands[i].enabled = urn.ZoneShown && !urn.IsLogist;
_commands[i].enabled = urn.ZoneShown && !urn.IsZoneless;
}
}
}
@@ -1107,15 +1217,19 @@ namespace NecromancerTome
GameManager.ShowTooltip(player, Localization.Get("ttCantPickupInUse"), string.Empty, "ui_denied");
return false;
}
if (composite.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage bag) && !bag.IsEmpty())
if (composite.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage bag) && !bag.IsEmpty() ||
urn.Sample != null && !urn.Sample.IsEmpty())
{
GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied");
return false;
}
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(logist
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName)
: (urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName));
: worker
? (urn.HasSkull ? TEFeatureCinerarium.WorkerSkullBlockName : TEFeatureCinerarium.WorkerBlockName)
: (urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName));
if (iv == null || iv.type <= 0)
{
return true;