Гашение было только в настоящем шаге, а он запускается, лишь когда пробный нашёл дело - пробный гашение делом не считал, огонь горел до конца топлива. Теперь погасить - дело: дух летит к станции и гасит. Проверено в игре: верстак (патроны), химстанция (клей), бетономешалка (песок), кузня (переплавка лома и глины -> дротики), костёр; огонь гаснет у костра, химстанции и кузни; разрушение урны - выпадают череп и образец; окно «Содержимое» - череп и образец. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
596 lines
18 KiB
C#
596 lines
18 KiB
C#
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 (st.Te.IsBurning)
|
|
{
|
|
// Погасить - тоже дело: дух летит к станции. Пробный шаг должен его найти - иначе настоящий шаг не
|
|
// запускается и огонь горел до конца топлива (проверка 18.10: костёр, химстанция, кузня).
|
|
if (_dry)
|
|
{
|
|
return st.Pos;
|
|
}
|
|
st.Te.IsBurning = false;
|
|
Debug.Log("[NecromancerTome] Cinerarium: рабочий погасил станцию " + st.Pos + (full ? " - выход полон" : " - делать нечего"));
|
|
if (full)
|
|
{
|
|
_stuck = true;
|
|
}
|
|
return st.Pos;
|
|
}
|
|
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;
|
|
}
|
|
string fuelName = g.itemValue.ItemClass?.GetItemName(); // до Take: опустевшая ячейка очищает стопку (было NRE, 18.10)
|
|
CinerariumWork.Take(s.box, k, take);
|
|
_te.Fuel = fuel;
|
|
Debug.Log("[NecromancerTome] Cinerarium: рабочий подбросил топливо " + take + " " + fuelName);
|
|
goto lit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
lit:
|
|
if (_te.BurnTotalTimeLeft > 0f && !_te.IsBurning)
|
|
{
|
|
_te.IsBurning = true;
|
|
}
|
|
}
|
|
}
|
|
}
|