Урна рабочего: изготовление по образцу на станциях по нитям (CinerariumCraft), образец во второй ячейке, нити к станциям, топливо и огонь
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a5f166c161
commit
eba17606d5
File diff suppressed because one or more lines are too long
+10
-3
@@ -582,13 +582,20 @@
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumLogistDesc"/>
|
||||
</block>
|
||||
<!-- Урна рабочего (17.10.2026), ПОКА ТОЛЬКО ПРОСМОТР МОДЕЛИ: чаша на трёх костях (BlenderWork/necroCinerariumWorker,
|
||||
necroCinerariumWorkerPrefab в бандле necrocinerarium). Код рабочего (изготовление по образцу) - не написан;
|
||||
до него блок ведёт себя как урна сборщика и виден только в режиме разработчика. -->
|
||||
<!-- Урна рабочего (17-18.10.2026, BACKLOG «Урна рабочего»): чаша на трёх костях (BlenderWork/necroCinerariumWorker,
|
||||
necroCinerariumWorkerPrefab в бандле necrocinerarium). Изготавливает предмет из второй ячейки «Содержимого» (образец)
|
||||
на станциях по нитям, пока запас меньше стопки образца (CinerariumCraft.cs). Без зоны и вне лимита, как логист.
|
||||
Рецепта пока нет - виден только в режиме разработчика; иконка - временно урны сборщика. -->
|
||||
<block name="necroCinerariumWorker">
|
||||
<property name="Extends" value="necroCinerarium"/>
|
||||
<property name="CreativeMode" value="Dev"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumWorkerDesc"/>
|
||||
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumWorkerPrefab.prefab"/>
|
||||
</block>
|
||||
<block name="necroCinerariumWorkerSkull">
|
||||
<property name="Extends" value="necroCinerariumWorker"/>
|
||||
<property name="CreativeMode" value="None"/>
|
||||
<property name="DescriptionKey" value="necroCinerariumWorkerDesc"/>
|
||||
</block>
|
||||
</append>
|
||||
</config>
|
||||
|
||||
+134
-20
@@ -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,14 +1217,18 @@ 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)
|
||||
: worker
|
||||
? (urn.HasSkull ? TEFeatureCinerarium.WorkerSkullBlockName : TEFeatureCinerarium.WorkerBlockName)
|
||||
: (urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName));
|
||||
if (iv == null || iv.type <= 0)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ namespace NecromancerTome
|
||||
/// <summary>Высота корня модели над низом блока урны, м. 16.10 в Blender по модели урны: -0.15 («духа чуть приподними»).
|
||||
/// Подгоняется командой necrogenie.</summary>
|
||||
public static float OffsetY = -0.15f;
|
||||
/// <summary>У урны рабочего горлышко ниже (земля в чаше на 0.45 м против 0.53 у урны сборщика) - дух ниже на столько же.</summary>
|
||||
public static float WorkerDrop = -0.08f;
|
||||
/// <summary>Сдвиг вперёд в системе самого духа, м (16.10: «чуть вперёд, чтобы хвост не просвечивал сквозь урну сзади»
|
||||
/// - загнутый назад кончик хвоста; в Blender 0.06 - ни одной вершины за стенкой, взято 0.07). Поворачивается вместе
|
||||
/// с духом, поэтому хвост всегда в горлышке. Знак проверить в игре: necrogenie fwd.</summary>
|
||||
@@ -79,7 +81,7 @@ namespace NecromancerTome
|
||||
return _prefab != null;
|
||||
}
|
||||
|
||||
public static GameObject Spawn(Transform _urnModel)
|
||||
public static GameObject Spawn(Transform _urnModel, float _drop)
|
||||
{
|
||||
if (_urnModel == null || !Load())
|
||||
{
|
||||
@@ -110,7 +112,9 @@ namespace NecromancerTome
|
||||
{
|
||||
Object.Destroy(c);
|
||||
}
|
||||
g.AddComponent<CinerariumGenieMotion>().Pose();
|
||||
CinerariumGenieMotion m = g.AddComponent<CinerariumGenieMotion>();
|
||||
m.Drop = _drop;
|
||||
m.Pose();
|
||||
return g;
|
||||
}
|
||||
|
||||
@@ -267,6 +271,8 @@ namespace NecromancerTome
|
||||
{
|
||||
float _yaw;
|
||||
float _phase;
|
||||
/// <summary>Своя поправка высоты урны (CinerariumGenie.WorkerDrop у рабочего).</summary>
|
||||
public float Drop;
|
||||
/// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
|
||||
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
|
||||
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary>
|
||||
@@ -439,7 +445,7 @@ namespace NecromancerTome
|
||||
{
|
||||
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)
|
||||
};
|
||||
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
|
||||
@@ -454,7 +460,7 @@ namespace NecromancerTome
|
||||
|
||||
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>
|
||||
|
||||
@@ -21,6 +21,10 @@ namespace NecromancerTome
|
||||
/// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
|
||||
/// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
|
||||
/// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
|
||||
///
|
||||
/// Урна рабочего (18.10): нити ящик -> урна - откуда брать ингредиенты и топливо, урна -> станция - где изготавливать,
|
||||
/// урна -> ящик - куда класть готовое; станций и ящиков сколько угодно (до MaxFrom в каждую сторону). Нить между станцией
|
||||
/// и урной рабочего - всегда «от урны», в каком бы порядке её ни тянули.
|
||||
/// </summary>
|
||||
public static class CinerariumThreads
|
||||
{
|
||||
@@ -66,6 +70,7 @@ namespace NecromancerTome
|
||||
|
||||
static Vector3i? _start;
|
||||
static bool _startUrn;
|
||||
static bool _startStation;
|
||||
static GameObject _preview;
|
||||
|
||||
public static Vector3i? Start => _start;
|
||||
@@ -98,6 +103,16 @@ namespace NecromancerTome
|
||||
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)
|
||||
{
|
||||
return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium u) ? u : null;
|
||||
@@ -113,6 +128,13 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
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 (_start.HasValue)
|
||||
@@ -135,13 +157,14 @@ namespace NecromancerTome
|
||||
}
|
||||
if (!_start.HasValue)
|
||||
{
|
||||
if (urn != null && !urn.IsLogist)
|
||||
if (urn != null && !urn.IsZoneless)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
|
||||
return;
|
||||
}
|
||||
_start = owner;
|
||||
_startUrn = urn != null;
|
||||
_startStation = station;
|
||||
_preview = new GameObject("NecroThreadPreview");
|
||||
_preview.AddComponent<CinerariumThreadPreview>();
|
||||
_pl.PlayOneShot("wiretool_grab");
|
||||
@@ -150,19 +173,44 @@ namespace NecromancerTome
|
||||
}
|
||||
Vector3i s = _start.Value;
|
||||
bool sUrn = _startUrn;
|
||||
bool sStation = _startStation;
|
||||
Cancel();
|
||||
if (s == owner)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
|
||||
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))
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
|
||||
return;
|
||||
}
|
||||
TEFeatureCinerarium end = sUrn ? UrnAt(w, s) : urn;
|
||||
if (end == null || !end.IsLogist)
|
||||
if (end == null || !end.IsZoneless)
|
||||
{
|
||||
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
|
||||
return;
|
||||
@@ -179,9 +227,22 @@ namespace NecromancerTome
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (u.IsWorker)
|
||||
{
|
||||
// У рабочего ящиков «куда» сколько угодно - туда готовое.
|
||||
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
|
||||
{
|
||||
if (!urn.AddThreadFrom(s))
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace NecromancerTome
|
||||
/// ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом). Своё здесь одно: после каждого изменения
|
||||
/// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли
|
||||
/// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя.
|
||||
/// У урны рабочего (18.10) ячеек две: вторая - образец (любой предмет, стопка целиком - сколько держать в запасе).
|
||||
/// </summary>
|
||||
public static class CinerariumContents
|
||||
{
|
||||
@@ -30,11 +31,15 @@ namespace NecromancerTome
|
||||
}
|
||||
_urn = _u;
|
||||
_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)
|
||||
{
|
||||
_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),
|
||||
Localization.Get("necroCinerariumTrunk"), Check, () =>
|
||||
{
|
||||
@@ -46,13 +51,27 @@ namespace NecromancerTome
|
||||
Debug.Log("[NecromancerTome] Cinerarium: «Содержимое» урны " + _u.ToWorldPos() + " открыто, череп " + (_u.HasSkull ? "есть" : "нет"));
|
||||
}
|
||||
|
||||
/// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне.</summary>
|
||||
/// <summary>Оставить в ячейке один череп, остальное - игроку; отметить в урне. У рабочего - запомнить образец; череп во
|
||||
/// второй ячейке - не образец, переложить в первую (или вернуть).</summary>
|
||||
static void Check()
|
||||
{
|
||||
if (_bag == null || _urn == null)
|
||||
{
|
||||
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];
|
||||
bool has = false;
|
||||
if (s != null && !s.IsEmpty())
|
||||
|
||||
@@ -71,6 +71,13 @@ namespace NecromancerTome
|
||||
{
|
||||
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);
|
||||
// 1. Расходники.
|
||||
foreach (TileEntityCollector c in z.Collectors)
|
||||
@@ -184,7 +191,7 @@ namespace NecromancerTome
|
||||
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() &&
|
||||
!c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
|
||||
@@ -285,7 +292,7 @@ namespace NecromancerTome
|
||||
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++)
|
||||
{
|
||||
@@ -519,7 +526,7 @@ namespace NecromancerTome
|
||||
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++)
|
||||
{
|
||||
@@ -548,13 +555,13 @@ namespace NecromancerTome
|
||||
}
|
||||
|
||||
/// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary>
|
||||
static bool Locked(TEFeatureStorage _src, int _slot)
|
||||
internal static bool Locked(TEFeatureStorage _src, int _slot)
|
||||
{
|
||||
PackedBoolArray locks = _src.ItemGrid.SlotLocks;
|
||||
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();
|
||||
s.count -= _count;
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user