Урна рабочего: изготовление по образцу на станциях по нитям (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
File diff suppressed because one or more lines are too long
+10 -3
View File
@@ -582,13 +582,20 @@
<property name="CreativeMode" value="None"/> <property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/> <property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
</block> </block>
<!-- Урна рабочего (17.10.2026), ПОКА ТОЛЬКО ПРОСМОТР МОДЕЛИ: чаша на трёх костях (BlenderWork/necroCinerariumWorker, <!-- Урна рабочего (17-18.10.2026, BACKLOG «Урна рабочего»): чаша на трёх костях (BlenderWork/necroCinerariumWorker,
necroCinerariumWorkerPrefab в бандле necrocinerarium). Код рабочего (изготовление по образцу) - не написан; necroCinerariumWorkerPrefab в бандле necrocinerarium). Изготавливает предмет из второй ячейки «Содержимого» (образец)
до него блок ведёт себя как урна сборщика и виден только в режиме разработчика. --> на станциях по нитям, пока запас меньше стопки образца (CinerariumCraft.cs). Без зоны и вне лимита, как логист.
Рецепта пока нет - виден только в режиме разработчика; иконка - временно урны сборщика. -->
<block name="necroCinerariumWorker"> <block name="necroCinerariumWorker">
<property name="Extends" value="necroCinerarium"/> <property name="Extends" value="necroCinerarium"/>
<property name="CreativeMode" value="Dev"/> <property name="CreativeMode" value="Dev"/>
<property name="DescriptionKey" value="necroCinerariumWorkerDesc"/>
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumWorkerPrefab.prefab"/> <property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumWorkerPrefab.prefab"/>
</block> </block>
<block name="necroCinerariumWorkerSkull">
<property name="Extends" value="necroCinerariumWorker"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumWorkerDesc"/>
</block>
</append> </append>
</config> </config>
+135 -21
View File
@@ -41,12 +41,26 @@ namespace NecromancerTome
string n = _b?.GetBlockName(); string n = _b?.GetBlockName();
return n == LogistBlockName || n == LogistSkullBlockName; 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 string SkullItemName = "necroSkull";
public const int HalfWidth = 2; public const int HalfWidth = 2;
public const int Below = 1; public const int Below = 1;
public const int Above = 4; public const int Above = 4;
public const float KillsPerUrn = 500f; 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 HasSkull;
public bool ZoneShown; public bool ZoneShown;
@@ -54,7 +68,23 @@ namespace NecromancerTome
public readonly List<Vector3i> ThreadFrom = new List<Vector3i>(); public readonly List<Vector3i> ThreadFrom = new List<Vector3i>();
public bool HasThreadTo; public bool HasThreadTo;
public Vector3i ThreadTo; 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> /// <summary>Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.</summary>
public bool IsLogist public bool IsLogist
{ {
@@ -67,6 +97,7 @@ namespace NecromancerTome
if (IsUrn(bv.Block)) if (IsUrn(bv.Block))
{ {
_logist = IsLogistBlock(bv.Block); _logist = IsLogistBlock(bv.Block);
_worker = IsWorkerBlock(bv.Block);
_logistKnown = true; _logistKnown = true;
} }
} }
@@ -96,6 +127,9 @@ namespace NecromancerTome
ThreadFrom.AddRange(other.ThreadFrom); ThreadFrom.AddRange(other.ThreadFrom);
HasThreadTo = other.HasThreadTo; HasThreadTo = other.HasThreadTo;
ThreadTo = other.ThreadTo; ThreadTo = other.ThreadTo;
ThreadOut.Clear();
ThreadOut.AddRange(other.ThreadOut);
Sample = other.Sample.Clone();
GroupMask = other.GroupMask; GroupMask = other.GroupMask;
_threadsDirty = true; _threadsDirty = true;
} }
@@ -106,15 +140,16 @@ namespace NecromancerTome
{ {
base.OnAdded(_blockPos, _blockValue); base.OnAdded(_blockPos, _blockValue);
_logist = IsLogistBlock(_blockValue.Block); _logist = IsLogistBlock(_blockValue.Block);
_worker = IsWorkerBlock(_blockValue.Block);
_logistKnown = true; _logistKnown = true;
string placed = _blockValue.Block?.GetBlockName(); string placed = _blockValue.Block?.GetBlockName();
if (placed == SkullBlockName || placed == LogistSkullBlockName) if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == WorkerSkullBlockName)
{ {
HasSkull = true; // поставили урну, поднятую с черепом HasSkull = true; // поставили урну, поднятую с черепом
} }
if (ConnectionManager.Instance.IsServer) 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 + " убрана - череп выпал"); 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) public override void OnUnload(World _world)
@@ -170,6 +211,13 @@ namespace NecromancerTome
CinerariumWriteHelper.WriteInts(_bw, t.ToArray()); CinerariumWriteHelper.WriteInts(_bw, t.ToArray());
// Второе число - сколько групп было при записи: новые группы (в конец) у старых урн включаются по правилу ниже. // Второе число - сколько групп было при записи: новые группы (в конец) у старых урн включаются по правилу ниже.
CinerariumWriteHelper.WriteInts(_bw, new[] { unchecked((int)GroupMask), CinerariumGroups.Count }); 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) public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
@@ -224,6 +272,23 @@ namespace NecromancerTome
GroupMask = on ? GroupMask | (1u << g) : GroupMask & ~(1u << g); 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; _threadsDirty = true;
if (version > SaveVersion) if (version > SaveVersion)
{ {
@@ -296,10 +361,26 @@ namespace NecromancerTome
ThreadsChanged(); 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() public int ClearThreads()
{ {
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0); int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0) + ThreadOut.Count;
ThreadFrom.Clear(); ThreadFrom.Clear();
ThreadOut.Clear();
HasThreadTo = false; HasThreadTo = false;
if (n > 0) if (n > 0)
{ {
@@ -310,7 +391,7 @@ namespace NecromancerTome
public int RemoveThreadsTo(Vector3i _box) 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) if (HasThreadTo && ThreadTo == _box)
{ {
HasThreadTo = false; HasThreadTo = false;
@@ -328,16 +409,17 @@ namespace NecromancerTome
_threadsDirty = true; _threadsDirty = true;
_glowState = -1; // логист - другие глаза _glowState = -1; // логист - другие глаза
SetModified(); 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> /// <summary>Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.</summary>
void SyncThreads(World _world) 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(); ThreadFrom.Clear();
ThreadOut.Clear();
HasThreadTo = false; HasThreadTo = false;
ThreadsChanged(); ThreadsChanged();
} }
@@ -362,6 +444,15 @@ namespace NecromancerTome
HasThreadTo = false; HasThreadTo = false;
gone = true; 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) if (gone)
{ {
ThreadsChanged(); ThreadsChanged();
@@ -399,6 +490,10 @@ namespace NecromancerTome
{ {
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me))); 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); _threadView.GetComponent<CinerariumThreadView>().Build(pairs);
} }
@@ -522,6 +617,20 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут")); 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 костей из рюкзака (сколько берёт и молоток - /// <summary>«Починить», как у транспорта: целиком, за RepairBones костей из рюкзака (сколько берёт и молоток -
/// RepairItems в blocks.xml).</summary> /// RepairItems в blocks.xml).</summary>
public const string RepairItemName = "resourceBone"; public const string RepairItemName = "resourceBone";
@@ -574,7 +683,7 @@ namespace NecromancerTome
SyncGlow(); SyncGlow();
if (HasSkull && _genie == null) if (HasSkull && _genie == null)
{ {
_genie = CinerariumGenie.Spawn(ModelTransform()); _genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f);
if (_genie != null && _sound) if (_genie != null && _sound)
{ {
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound); Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
@@ -754,7 +863,8 @@ namespace NecromancerTome
public static bool IsUrn(Block _b) public static bool IsUrn(Block _b)
{ {
string n = _b?.GetBlockName(); 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) public static int UrnLimit(EntityPlayer _player)
@@ -766,13 +876,13 @@ namespace NecromancerTome
/// <summary>Ключ подсказки, почему урну сюда нельзя, или null.</summary> /// <summary>Ключ подсказки, почему урну сюда нельзя, или null.</summary>
public static string PlacementRefusal(Vector3i _pos, Block _placing) public static string PlacementRefusal(Vector3i _pos, Block _placing)
{ {
if (!IsLogistBlock(_placing) && CinerariumRegistry.Overlaps(_pos)) if (!IsZonelessBlock(_placing) && CinerariumRegistry.Overlaps(_pos))
{ {
return "necroCinerariumOverlap"; return "necroCinerariumOverlap";
} }
if (IsLogistBlock(_placing)) if (IsZonelessBlock(_placing))
{ {
return null; // логисты в лимит не входят (16.10, пользователь) return null; // логисты и рабочие в лимит не входят (16.10, 17.10 - пользователь)
} }
World world = GameManager.Instance?.World; World world = GameManager.Instance?.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer(); EntityPlayerLocal player = world?.GetPrimaryPlayer();
@@ -892,7 +1002,7 @@ namespace NecromancerTome
{ {
// Логисты не считаются; записанные до метки «L» узнаются по блоку в мире (чанк рядом загружен). // Логисты не считаются; записанные до метки «L» узнаются по блоку в мире (чанк рядом загружен).
if (kv.Value == id && !_logists.Contains(kv.Key) && 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++; n++;
} }
@@ -914,9 +1024,9 @@ namespace NecromancerTome
World world = GameManager.Instance?.World; World world = GameManager.Instance?.World;
foreach (Vector3i p in Urns().Keys) 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) 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")) 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")) 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"); GameManager.ShowTooltip(player, Localization.Get("ttCantPickupInUse"), string.Empty, "ui_denied");
return false; 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"); GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied");
return false; return false;
} }
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block); bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(logist ItemValue iv = ItemClass.GetItem(logist
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName) ? (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) if (iv == null || iv.type <= 0)
{ {
return true; return true;
+583
View File
@@ -0,0 +1,583 @@
using System.Collections.Generic;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Работа духа урны рабочего (18.10.2026, BACKLOG «Урна рабочего»): изготовить предмет из ячейки образца, пока его в запасе
/// меньше, чем в стопке образца. Решения 17.10: нити ящик -> урна - откуда ингредиенты и топливо, урна -> станция - где
/// изготавливать, урна -> ящик - куда готовое (нет такой нити - готовое остаётся в выходе станции, выход полон - станция
/// гаснет); только прямые рецепты (цепочки - несколько урн); качество 1; топливо - костру, химстанции, кузне, и станция
/// гасится, когда делать нечего (верстаку, ступке, бетономешалке топливо не нужно). Изготавливает сама станция игры: дух
/// только кладёт рецепт в её очередь, забрав ингредиенты, - время, огонь и выход считает игра.
/// Запас = такие же предметы в ящиках «куда», в выходах станций и в их очередях.
/// Одно дело за шаг, как у прочих духов (CinerariumWork.Step), _dry - пробный шаг без изменений.
/// </summary>
public static class CinerariumCraft
{
const int FuelModule = 3, MaterialModule = 4;
sealed class Station
{
public Vector3i Pos;
public TileEntityWorkstation Te;
public string[] Areas;
}
public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, bool _dry, out bool _stuck, out Vector3i? _from)
{
_stuck = false;
_from = null;
ItemStack sample = _urn.Sample;
List<(Vector3i p, TEFeatureStorage box)> src = new List<(Vector3i, TEFeatureStorage)>();
foreach (Vector3i p in _urn.ThreadFrom)
{
TEFeatureStorage b = CinerariumWork.BoxAt(_world, p);
if (b != null)
{
src.Add((p, b));
}
}
List<Station> stations = new List<Station>();
List<(Vector3i p, TEFeatureStorage box)> outs = new List<(Vector3i, TEFeatureStorage)>();
foreach (Vector3i p in _urn.ThreadOut)
{
TileEntityWorkstation te = CinerariumThreads.StationAt(_world, p);
if (te != null)
{
if (!te.IsUserAccessing())
{
stations.Add(new Station { Pos = p, Te = te, Areas = AreasOf(_world.GetBlock(p).Block) });
}
continue;
}
TEFeatureStorage b = CinerariumWork.BoxAt(_world, p);
if (b != null)
{
outs.Add((p, b));
}
}
bool hasSample = sample != null && !sample.IsEmpty();
// 1. Готовое (только предмет образца - чужое изготовленное игроком не трогаем) - из выхода станции в ящики «куда».
if (hasSample && outs.Count > 0)
{
foreach (Station st in stations)
{
ItemStack[] output = st.Te.Output;
for (int i = 0; i < output.Length; i++)
{
ItemStack s = output[i];
if (s == null || s.IsEmpty() || s.itemValue.type != sample.itemValue.type)
{
continue;
}
Vector3i? dst = null;
foreach ((Vector3i p, TEFeatureStorage box) o in outs)
{
if (CinerariumWork.Fits(o.box, s))
{
dst = o.p;
break;
}
}
if (!dst.HasValue)
{
_stuck = true; // ящики «куда» полны
continue;
}
_from = st.Pos;
if (_dry)
{
return dst;
}
ItemStack moving = s.Clone();
List<TEFeatureStorage> boxes = new List<TEFeatureStorage>();
foreach ((Vector3i p, TEFeatureStorage box) o in outs)
{
boxes.Add(o.box);
}
CinerariumWork.Place(moving, boxes);
output[i] = moving.count > 0 ? moving : ItemStack.Empty.Clone();
st.Te.Output = output;
Debug.Log("[NecromancerTome] Cinerarium: рабочий перенёс " + (s.count - moving.count) + " " + s.itemValue.ItemClass?.GetItemName() + " из станции " + st.Pos);
return dst;
}
}
}
// 2. Изготовить, если запаса мало.
EntityPlayer owner = OwnerPlayer(_world, _urn);
if (hasSample && Stock(sample, stations, outs) < sample.count)
{
string name = sample.itemValue.ItemClass?.GetItemName();
foreach (Station st in stations)
{
int slot = FreeQueueSlot(st.Te);
if (slot < 0)
{
continue;
}
if (!OutputHasRoom(st.Te, sample) && outs.Count == 0)
{
continue; // выход полон, класть некуда - ниже станция погаснет
}
foreach (Recipe r in CraftingManager.GetRecipes(name))
{
if (r.IsScrap || !AreaFits(r, st.Areas) || !ToolFits(r, st.Te) || owner != null && !r.IsUnlocked(owner))
{
continue;
}
if (!Ingredients(r, st.Te, src, out Vector3i? srcAt, out string shortUnit))
{
// Кузня: не хватает переплавленного материала - положить сырьё во вход (плавит сама, с огнём).
if (shortUnit != null && FeedMaterial(st, shortUnit, src, _dry, out Vector3i? rawAt))
{
_from = rawAt;
if (!_dry)
{
Ignite(st.Te, src, false);
}
return st.Pos;
}
continue;
}
_from = srcAt;
if (_dry)
{
return st.Pos;
}
TakeIngredients(r, st.Te, src);
Enqueue(st.Te, slot, r);
Ignite(st.Te, src, false);
Debug.Log("[NecromancerTome] Cinerarium: рабочий поставил " + name + " на станцию " + st.Pos);
return st.Pos;
}
}
}
// 3. Огонь: работа есть - топливо и огонь; нет - погасить (экономия топлива).
foreach (Station st in stations)
{
if (!st.Te.isModuleUsed[FuelModule])
{
continue;
}
bool busy = st.Te.hasRecipeInQueue() || RawInInput(st.Te);
bool full = hasSample && !OutputHasRoom(st.Te, sample) && outs.Count == 0;
if (busy && !full)
{
if (st.Te.BurnTotalTimeLeft <= 0f)
{
Vector3i? fuelAt = FuelSource(src);
if (fuelAt.HasValue)
{
_from = fuelAt;
if (_dry)
{
return st.Pos;
}
Ignite(st.Te, src, true);
return st.Pos;
}
}
else if (!_dry && !st.Te.IsBurning)
{
st.Te.IsBurning = true;
}
}
else if (!_dry && st.Te.IsBurning)
{
st.Te.IsBurning = false;
Debug.Log("[NecromancerTome] Cinerarium: рабочий погасил станцию " + st.Pos + (full ? " - выход полон" : " - делать нечего"));
}
if (full)
{
_stuck = true;
}
}
return null;
}
/// <summary>Чьи рецепты изучены: хозяин урны, если он в игре; в одиночной - игрок. Нет никого - не проверяем.</summary>
static EntityPlayer OwnerPlayer(World _world, TEFeatureCinerarium _urn)
{
PlatformUserIdentifierAbs id = _urn.Parent?.Owner;
if (id != null)
{
PersistentPlayerData ppd = GameManager.Instance.persistentPlayers?.GetPlayerData(id);
if (ppd != null && _world.GetEntity(ppd.EntityId) is EntityPlayer p)
{
return p;
}
}
return _world.GetPrimaryPlayer();
}
/// <summary>Какие рецепты станция делает: Workstation.CraftingAreaRecipes или имя блока («player» - рецепты без станции).</summary>
static string[] AreasOf(Block _b)
{
if (_b == null)
{
return new string[0];
}
string a = _b.Properties.Contains("Workstation", "CraftingAreaRecipes") ? _b.Properties.GetString("Workstation", "CraftingAreaRecipes") : _b.GetBlockName();
return a.Replace(" ", "").Split(',');
}
static bool AreaFits(Recipe _r, string[] _areas)
{
string area = string.IsNullOrEmpty(_r.craftingArea) ? "player" : _r.craftingArea;
foreach (string a in _areas)
{
if (a.EqualsCaseInsensitive(area))
{
return true;
}
}
return false;
}
static bool ToolFits(Recipe _r, TileEntityWorkstation _te)
{
if (_r.craftingToolType == 0)
{
return true;
}
foreach (ItemStack t in _te.Tools)
{
if (t != null && !t.IsEmpty() && t.itemValue.type == _r.craftingToolType)
{
return true;
}
}
return false;
}
/// <summary>Запас: в ящиках «куда», в выходе станций и в их очередях.</summary>
static int Stock(ItemStack _sample, List<Station> _stations, List<(Vector3i p, TEFeatureStorage box)> _outs)
{
int type = _sample.itemValue.type;
int n = 0;
foreach ((Vector3i p, TEFeatureStorage box) o in _outs)
{
foreach (ItemStack s in o.box.ItemGrid.items)
{
if (s != null && !s.IsEmpty() && s.itemValue.type == type)
{
n += s.count;
}
}
}
foreach (Station st in _stations)
{
foreach (ItemStack s in st.Te.Output)
{
if (s != null && !s.IsEmpty() && s.itemValue.type == type)
{
n += s.count;
}
}
foreach (RecipeQueueItem q in st.Te.Queue)
{
if (q != null && q.Recipe != null && q.Multiplier > 0 && q.Recipe.itemValueType == type)
{
n += q.Multiplier * q.Recipe.count;
}
}
}
return n;
}
/// <summary>Свободная ячейка очереди: самая близкая к работающей (последней).</summary>
static int FreeQueueSlot(TileEntityWorkstation _te)
{
RecipeQueueItem[] q = _te.Queue;
for (int i = q.Length - 1; i >= 0; i--)
{
if (q[i] == null || q[i].Recipe == null || q[i].Multiplier <= 0)
{
return i;
}
}
return -1;
}
static bool OutputHasRoom(TileEntityWorkstation _te, ItemStack _sample)
{
foreach (ItemStack s in _te.Output)
{
if (s == null || s.IsEmpty())
{
return true;
}
int n = 1;
if (s.itemValue.type == _sample.itemValue.type && s.CanStackPartly(ref n))
{
return true;
}
}
return false;
}
/// <summary>Индекс ячейки переплавленного материала кузни для unit_X (вход 3 + номер материала), или -1.</summary>
static int UnitSlot(TileEntityWorkstation _te, string _itemName)
{
if (!_te.isModuleUsed[MaterialModule] || !_itemName.StartsWith("unit_"))
{
return -1;
}
string mat = _itemName.Substring(5);
for (int j = 0; j < _te.materialNames.Length; j++)
{
if (_te.materialNames[j].EqualsCaseInsensitive(mat) && 3 + j < _te.Input.Length)
{
return 3 + j;
}
}
return -1;
}
/// <summary>Хватает ли всего: переплавленное - в кузне, прочее - в ящиках «откуда». _srcAt - ящик, откуда первое;
/// _shortUnit - какого переплавленного не хватило (кузне можно подложить сырья).</summary>
static bool Ingredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, out Vector3i? _srcAt, out string _shortUnit)
{
_srcAt = null;
_shortUnit = null;
foreach (ItemStack ing in _r.ingredients)
{
if (ing == null || ing.IsEmpty() || ing.count <= 0)
{
continue;
}
string iname = ing.itemValue.ItemClass?.GetItemName() ?? "";
int us = UnitSlot(_te, iname);
if (us >= 0)
{
if (_te.Input[us].count < ing.count)
{
_shortUnit = iname;
return false;
}
continue;
}
int have = 0;
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
{
ItemStack[] grid = s.box.ItemGrid.items;
for (int k = 0; k < grid.Length; k++)
{
if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k))
{
have += grid[k].count;
if (!_srcAt.HasValue)
{
_srcAt = s.p;
}
}
}
}
if (have < ing.count)
{
return false;
}
}
return true;
}
static void TakeIngredients(Recipe _r, TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src)
{
ItemStack[] input = _te.Input;
bool inputChanged = false;
foreach (ItemStack ing in _r.ingredients)
{
if (ing == null || ing.IsEmpty() || ing.count <= 0)
{
continue;
}
int us = UnitSlot(_te, ing.itemValue.ItemClass?.GetItemName() ?? "");
if (us >= 0)
{
input[us].count -= ing.count;
inputChanged = true;
continue;
}
int need = ing.count;
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
{
ItemStack[] grid = s.box.ItemGrid.items;
for (int k = 0; k < grid.Length && need > 0; k++)
{
if (grid[k] != null && !grid[k].IsEmpty() && grid[k].itemValue.type == ing.itemValue.type && !CinerariumWork.Locked(s.box, k))
{
int take = Mathf.Min(need, grid[k].count);
CinerariumWork.Take(s.box, k, take);
need -= take;
}
}
if (need <= 0)
{
break;
}
}
}
if (inputChanged)
{
_te.Input = input;
}
}
static void Enqueue(TileEntityWorkstation _te, int _slot, Recipe _r)
{
RecipeQueueItem[] q = _te.Queue;
float t = _r.craftingTime > 0f ? _r.craftingTime : 1f;
ItemClass outClass = ItemClass.GetForId(_r.itemValueType);
q[_slot] = new RecipeQueueItem
{
Recipe = _r,
Multiplier = 1,
CraftingTimeLeft = t,
OneItemCraftTime = t,
IsCrafting = _slot == q.Length - 1,
Quality = (byte)(outClass != null && outClass.HasQuality ? 1 : 0),
StartingEntityId = -1
};
_te.Queue = q;
}
static bool RawInInput(TileEntityWorkstation _te)
{
if (!_te.isModuleUsed[MaterialModule])
{
return false;
}
for (int i = 0; i < 3 && i < _te.Input.Length; i++)
{
if (_te.Input[i] != null && !_te.Input[i].IsEmpty())
{
return true;
}
}
return false;
}
/// <summary>Сырьё для переплавки в unit_X: предмет из ящика «откуда», сделанный из этого материала, - во вход кузни.</summary>
static bool FeedMaterial(Station _st, string _unit, List<(Vector3i p, TEFeatureStorage box)> _src, bool _dry, out Vector3i? _at)
{
_at = null;
string mat = _unit.Substring(5);
ItemStack[] input = _st.Te.Input;
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
{
ItemStack[] grid = s.box.ItemGrid.items;
for (int k = 0; k < grid.Length; k++)
{
ItemStack g = grid[k];
ItemClass ic = g != null && !g.IsEmpty() ? g.itemValue.ItemClass : null;
if (ic == null || ic.MadeOfMaterial?.ForgeCategory == null || !ic.MadeOfMaterial.ForgeCategory.EqualsCaseInsensitive(mat) ||
ic.GetItemName().StartsWith("unit_") || CinerariumWork.Locked(s.box, k))
{
continue;
}
for (int i = 0; i < 3 && i < input.Length; i++)
{
bool empty = input[i] == null || input[i].IsEmpty();
int room = empty ? ic.MaxCount : input[i].itemValue.type == g.itemValue.type ? ic.MaxCount - input[i].count : 0;
if (room <= 0)
{
continue;
}
_at = s.p;
if (_dry)
{
return true;
}
int take = Mathf.Min(room, g.count);
if (empty)
{
input[i] = new ItemStack(g.itemValue.Clone(), take);
}
else
{
input[i].count += take;
}
CinerariumWork.Take(s.box, k, take);
_st.Te.Input = input;
Debug.Log("[NecromancerTome] Cinerarium: рабочий положил в кузню " + _st.Pos + " " + take + " " + ic.GetItemName());
return true;
}
}
}
return false;
}
static Vector3i? FuelSource(List<(Vector3i p, TEFeatureStorage box)> _src)
{
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
{
ItemStack[] grid = s.box.ItemGrid.items;
for (int k = 0; k < grid.Length; k++)
{
if (grid[k] != null && !grid[k].IsEmpty() && ItemClass.GetFuelValue(grid[k].itemValue) > 0 && !CinerariumWork.Locked(s.box, k))
{
return s.p;
}
}
}
return null;
}
/// <summary>Огонь станции с топливом: нет топлива - стопку горючего из ящиков «откуда» (_addFuel), затем зажечь.</summary>
static void Ignite(TileEntityWorkstation _te, List<(Vector3i p, TEFeatureStorage box)> _src, bool _addFuel)
{
if (!_te.isModuleUsed[FuelModule])
{
return;
}
if (_addFuel || _te.BurnTotalTimeLeft <= 0f)
{
ItemStack[] fuel = _te.Fuel;
foreach ((Vector3i p, TEFeatureStorage box) s in _src)
{
ItemStack[] grid = s.box.ItemGrid.items;
for (int k = 0; k < grid.Length; k++)
{
ItemStack g = grid[k];
if (g == null || g.IsEmpty() || ItemClass.GetFuelValue(g.itemValue) <= 0 || CinerariumWork.Locked(s.box, k))
{
continue;
}
for (int i = 0; i < fuel.Length; i++)
{
bool empty = fuel[i] == null || fuel[i].IsEmpty();
int max = g.itemValue.ItemClass.MaxCount;
int room = empty ? max : fuel[i].itemValue.type == g.itemValue.type ? max - fuel[i].count : 0;
if (room <= 0)
{
continue;
}
int take = Mathf.Min(room, g.count);
if (empty)
{
fuel[i] = new ItemStack(g.itemValue.Clone(), take);
}
else
{
fuel[i].count += take;
}
CinerariumWork.Take(s.box, k, take);
_te.Fuel = fuel;
Debug.Log("[NecromancerTome] Cinerarium: рабочий подбросил топливо " + take + " " + g.itemValue.ItemClass.GetItemName());
goto lit;
}
}
}
}
lit:
if (_te.BurnTotalTimeLeft > 0f && !_te.IsBurning)
{
_te.IsBurning = true;
}
}
}
}
+10 -4
View File
@@ -25,6 +25,8 @@ namespace NecromancerTome
/// <summary>Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»). /// <summary>Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»).
/// Подгоняется командой necrogenie.</summary> /// Подгоняется командой necrogenie.</summary>
public static float OffsetY = -0.15f; public static float OffsetY = -0.15f;
/// <summary>У урны рабочего горлышко ниже (земля в чаше на 0.45 м против 0.53 у урны сборщика) - дух ниже на столько же.</summary>
public static float WorkerDrop = -0.08f;
/// <summary>Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади» /// <summary>Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади»
/// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе /// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе
/// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.</summary> /// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.</summary>
@@ -79,7 +81,7 @@ namespace NecromancerTome
return _prefab != null; return _prefab != null;
} }
public static GameObject Spawn(Transform _urnModel) public static GameObject Spawn(Transform _urnModel, float _drop)
{ {
if (_urnModel == null || !Load()) if (_urnModel == null || !Load())
{ {
@@ -110,7 +112,9 @@ namespace NecromancerTome
{ {
Object.Destroy(c); Object.Destroy(c);
} }
g.AddComponent<CinerariumGenieMotion>().Pose(); CinerariumGenieMotion m = g.AddComponent<CinerariumGenieMotion>();
m.Drop = _drop;
m.Pose();
return g; return g;
} }
@@ -267,6 +271,8 @@ namespace NecromancerTome
{ {
float _yaw; float _yaw;
float _phase; float _phase;
/// <summary>Своя поправка высоты урны (CinerariumGenie.WorkerDrop у рабочего).</summary>
public float Drop;
/// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками /// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit), /// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary> /// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary>
@@ -439,7 +445,7 @@ namespace NecromancerTome
{ {
Stop st = new Stop Stop st = new Stop
{ {
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + (beside ? 0.15f : 0.8f), 0f), Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + Drop + (beside ? 0.15f : 0.8f), 0f),
Look = transform.parent.InverseTransformPoint(look) Look = transform.parent.InverseTransformPoint(look)
}; };
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed); st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
@@ -454,7 +460,7 @@ namespace NecromancerTome
Vector3 Home() Vector3 Home()
{ {
return new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward); return new Vector3(0f, CinerariumGenie.OffsetY + Drop, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
} }
/// <summary>Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.</summary> /// <summary>Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.</summary>
+65 -4
View File
@@ -21,6 +21,10 @@ namespace NecromancerTome
/// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом /// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
/// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити. /// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
/// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже). /// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
///
/// Урна рабочего (18.10): нити ящик -> урна - откуда брать ингредиенты и топливо, урна -> станция - где изготавливать,
/// урна -> ящик - куда класть готовое; станций и ящиков сколько угодно (до MaxFrom в каждую сторону). Нить между станцией
/// и урной рабочего - всегда «от урны», в каком бы порядке её ни тянули.
/// </summary> /// </summary>
public static class CinerariumThreads public static class CinerariumThreads
{ {
@@ -66,6 +70,7 @@ namespace NecromancerTome
static Vector3i? _start; static Vector3i? _start;
static bool _startUrn; static bool _startUrn;
static bool _startStation;
static GameObject _preview; static GameObject _preview;
public static Vector3i? Start => _start; public static Vector3i? Start => _start;
@@ -98,6 +103,16 @@ namespace NecromancerTome
return comp.PlayerPlaced && comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage _); return comp.PlayerPlaced && comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage _);
} }
/// <summary>Станция игры (верстак, костёр, кузня, химстанция, бетономешалка, ступка) в клетке _p; _owner - главная клетка.</summary>
public static TileEntityWorkstation StationAt(World _w, Vector3i _p, out Vector3i _owner)
{
BlockValue bv = _w.GetBlock(_p);
_owner = bv.ischild ? _p + bv.parent : _p;
return _w.GetTileEntity(_owner) as TileEntityWorkstation;
}
public static TileEntityWorkstation StationAt(World _w, Vector3i _p) => StationAt(_w, _p, out Vector3i _);
public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p) public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p)
{ {
return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium u) ? u : null; return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium u) ? u : null;
@@ -113,6 +128,13 @@ namespace NecromancerTome
return; return;
} }
bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn); bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
bool station = false;
if (!ok && StationAt(w, hi.hit.blockPos, out Vector3i stOwner) != null)
{
ok = true;
station = true;
owner = stOwner;
}
if (!_connect) if (!_connect)
{ {
if (_start.HasValue) if (_start.HasValue)
@@ -135,13 +157,14 @@ namespace NecromancerTome
} }
if (!_start.HasValue) if (!_start.HasValue)
{ {
if (urn != null && !urn.IsLogist) if (urn != null && !urn.IsZoneless)
{ {
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist")); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
return; return;
} }
_start = owner; _start = owner;
_startUrn = urn != null; _startUrn = urn != null;
_startStation = station;
_preview = new GameObject("NecroThreadPreview"); _preview = new GameObject("NecroThreadPreview");
_preview.AddComponent<CinerariumThreadPreview>(); _preview.AddComponent<CinerariumThreadPreview>();
_pl.PlayOneShot("wiretool_grab"); _pl.PlayOneShot("wiretool_grab");
@@ -150,19 +173,44 @@ namespace NecromancerTome
} }
Vector3i s = _start.Value; Vector3i s = _start.Value;
bool sUrn = _startUrn; bool sUrn = _startUrn;
bool sStation = _startStation;
Cancel(); Cancel();
if (s == owner) if (s == owner)
{ {
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel")); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
return; return;
} }
// Станция - только с урной рабочего, нить всегда «от урны».
if (sStation || station)
{
TEFeatureCinerarium wu = sStation ? urn : UrnAt(w, s);
Vector3i st = sStation ? s : owner;
if (wu == null || !wu.IsWorker)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedWorker"));
return;
}
if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
return;
}
if (!wu.AddThreadOut(st))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
return;
}
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStation"));
_pl.PlayOneShot("wiretool_place");
return;
}
if (sUrn == (urn != null)) if (sUrn == (urn != null))
{ {
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair")); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
return; return;
} }
TEFeatureCinerarium end = sUrn ? UrnAt(w, s) : urn; TEFeatureCinerarium end = sUrn ? UrnAt(w, s) : urn;
if (end == null || !end.IsLogist) if (end == null || !end.IsZoneless)
{ {
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist")); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
return; return;
@@ -179,8 +227,21 @@ namespace NecromancerTome
{ {
return; return;
} }
u.SetThreadTo(owner); if (u.IsWorker)
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo")); {
// У рабочего ящиков «куда» сколько угодно - туда готовое.
if (!u.AddThreadOut(owner))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
return;
}
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadOut"));
}
else
{
u.SetThreadTo(owner);
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
}
} }
else else
{ {
+21 -2
View File
@@ -9,6 +9,7 @@ namespace NecromancerTome
/// ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом). Своё здесь одно: после каждого изменения /// ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом). Своё здесь одно: после каждого изменения
/// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли /// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли
/// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя. /// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя.
/// У урны рабочего (18.10) ячеек две: вторая - образец (любой предмет, стопка целиком - сколько держать в запасе).
/// </summary> /// </summary>
public static class CinerariumContents public static class CinerariumContents
{ {
@@ -30,11 +31,15 @@ namespace NecromancerTome
} }
_urn = _u; _urn = _u;
_player = _p; _player = _p;
_bag = new Bag(new Vector2i(1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null); _bag = new Bag(new Vector2i(_u.IsWorker ? 2 : 1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
if (_u.HasSkull && skull != null && skull.type > 0) if (_u.HasSkull && skull != null && skull.type > 0)
{ {
_bag.SetSlot(0, new ItemStack(skull, 1)); _bag.SetSlot(0, new ItemStack(skull, 1));
} }
if (_u.IsWorker && _u.Sample != null && !_u.Sample.IsEmpty())
{
_bag.SetSlot(1, _u.Sample.Clone());
}
XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate), XUiC_BagStorageWindowGroup.Open(ui.xui, _p, _bag, LootContainer.GetLootContainer(LootTemplate),
Localization.Get("necroCinerariumTrunk"), Check, () => Localization.Get("necroCinerariumTrunk"), Check, () =>
{ {
@@ -46,13 +51,27 @@ namespace NecromancerTome
Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет")); Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет"));
} }
/// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне.</summary> /// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне. У рабочего - запомнить образец; череп во
/// второй ячейке - не образец, переложить в первую (или вернуть).</summary>
static void Check() static void Check()
{ {
if (_bag == null || _urn == null) if (_bag == null || _urn == null)
{ {
return; return;
} }
if (_urn.IsWorker && _bag.ItemGrid.items.Length > 1)
{
ItemStack smp = _bag.ItemGrid.items[1];
ItemStack first = _bag.ItemGrid.items[0];
if (smp != null && !smp.IsEmpty() && smp.itemValue.ItemClass?.GetItemName() == TEFeatureCinerarium.SkullItemName &&
(first == null || first.IsEmpty()))
{
_bag.SetSlot(0, smp.Clone());
_bag.SetSlot(1, ItemStack.Empty.Clone());
smp = _bag.ItemGrid.items[1];
}
_urn.SetSample(smp != null && !smp.IsEmpty() ? smp.Clone() : ItemStack.Empty.Clone());
}
ItemStack s = _bag.ItemGrid.items[0]; ItemStack s = _bag.ItemGrid.items[0];
bool has = false; bool has = false;
if (s != null && !s.IsEmpty()) if (s != null && !s.IsEmpty())
+12 -5
View File
@@ -71,6 +71,13 @@ namespace NecromancerTome
{ {
return Ferry(_world, _urn); return Ferry(_world, _urn);
} }
// Урна рабочего - изготовление по образцу на станциях по нитям (CinerariumCraft.cs).
if (_urn.IsWorker)
{
Vector3i? at = CinerariumCraft.Step(_world, _urn, _dry, out _stuck, out Vector3i? from);
LastFrom = from;
return at;
}
Zone z = Scan(_world, _urn); Zone z = Scan(_world, _urn);
// 1. Расходники. // 1. Расходники.
foreach (TileEntityCollector c in z.Collectors) foreach (TileEntityCollector c in z.Collectors)
@@ -184,7 +191,7 @@ namespace NecromancerTome
return null; return null;
} }
static TEFeatureStorage BoxAt(World _world, Vector3i _p) internal static TEFeatureStorage BoxAt(World _world, Vector3i _p)
{ {
return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() && return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
!c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) && !c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
@@ -285,7 +292,7 @@ namespace NecromancerTome
return _z.Bag != null && Fits(_z.Bag, _s); return _z.Bag != null && Fits(_z.Bag, _s);
} }
static bool Fits(TEFeatureStorage _t, ItemStack _s) internal static bool Fits(TEFeatureStorage _t, ItemStack _s)
{ {
for (int i = 0; i < _t.ItemGrid.Length; i++) for (int i = 0; i < _t.ItemGrid.Length; i++)
{ {
@@ -519,7 +526,7 @@ namespace NecromancerTome
return false; return false;
} }
static void Place(ItemStack _s, IList<TEFeatureStorage> _dst) internal static void Place(ItemStack _s, IList<TEFeatureStorage> _dst)
{ {
for (int i = 0; i < _dst.Count && _s.count > 0; i++) for (int i = 0; i < _dst.Count && _s.count > 0; i++)
{ {
@@ -548,13 +555,13 @@ namespace NecromancerTome
} }
/// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary> /// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary>
static bool Locked(TEFeatureStorage _src, int _slot) internal static bool Locked(TEFeatureStorage _src, int _slot)
{ {
PackedBoolArray locks = _src.ItemGrid.SlotLocks; PackedBoolArray locks = _src.ItemGrid.SlotLocks;
return locks != null && _slot < locks.Length && locks[_slot]; return locks != null && _slot < locks.Length && locks[_slot];
} }
static void Take(TEFeatureStorage _src, int _slot, int _count) internal static void Take(TEFeatureStorage _src, int _slot, int _count)
{ {
ItemStack s = _src.ItemGrid[_slot].Clone(); ItemStack s = _src.ItemGrid[_slot].Clone();
s.count -= _count; s.count -= _count;
Binary file not shown.