Некротехнолог: работа духа - расходники коллекторов и готовое из станций
Раз в 3 с одно дело: (1) пополнить из ящиков зоны и сумки урны ячейки расходников любого коллектора (банки, цветы, корм, сферы) и катализатора (куры, по одной в пустую ячейку); (2) забрать готовое из коллекторов и станций (верстак, кузня, химстанция, костёр...) в ящики зоны - сначала к таким же, потом в пустое, ящики полны - в сумку урны; некуда - дух чернеет и замирает, оживает сам. Модификации, инструменты, топливо и сырьё станций не трогаются; открытое у игрока и закреплённые ячейки - тоже. Модель духа плывёт к месту работы и обратно. Грядки - позже. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0558f46069
commit
0890ffe1e9
@@ -148,6 +148,35 @@ namespace NecromancerTome
|
||||
SpawnBounds();
|
||||
}
|
||||
SyncGenie(false);
|
||||
Work(_world);
|
||||
}
|
||||
|
||||
float _nextWork;
|
||||
public bool Stuck;
|
||||
|
||||
/// <summary>Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере.</summary>
|
||||
void Work(World _world)
|
||||
{
|
||||
if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer || Time.time < _nextWork)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nextWork = Time.time + CinerariumWork.StepSeconds;
|
||||
Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck);
|
||||
if (stuck != Stuck)
|
||||
{
|
||||
Stuck = stuck;
|
||||
Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? " почернел - готовое класть некуда" : " ожил"));
|
||||
}
|
||||
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
|
||||
if (motion != null)
|
||||
{
|
||||
motion.SetStuck(Stuck && at == null);
|
||||
if (at.HasValue)
|
||||
{
|
||||
motion.Visit(World.blockToTransformPos(at.Value) - Origin.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -139,6 +139,51 @@ namespace NecromancerTome
|
||||
{
|
||||
float _yaw;
|
||||
float _phase;
|
||||
/// <summary>Полёт к месту работы и обратно: точка в координатах модели урны и сколько осталось.</summary>
|
||||
Vector3 _visit;
|
||||
float _visitLeft;
|
||||
public const float VisitSeconds = 2.2f;
|
||||
/// <summary>Почернел и замер (готовое класть некуда, решение 06.10).</summary>
|
||||
bool _stuck;
|
||||
static readonly MaterialPropertyBlock _mpb = new MaterialPropertyBlock();
|
||||
static readonly int ColorId = Shader.PropertyToID("_Color");
|
||||
|
||||
/// <summary>_worldUnity - точка в координатах сцены (без Origin): низ блока, где дух работал.</summary>
|
||||
public void Visit(Vector3 _worldUnity)
|
||||
{
|
||||
if (transform.parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_visit = transform.parent.InverseTransformPoint(_worldUnity);
|
||||
_visitLeft = VisitSeconds;
|
||||
}
|
||||
|
||||
public void SetStuck(bool _on)
|
||||
{
|
||||
if (_stuck == _on)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_stuck = _on;
|
||||
foreach (Renderer r in GetComponentsInChildren<Renderer>(true))
|
||||
{
|
||||
if (r.name.Contains("eye"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_on)
|
||||
{
|
||||
r.GetPropertyBlock(_mpb);
|
||||
_mpb.SetColor(ColorId, new Color(0.04f, 0.035f, 0.045f, Mathf.Max(CinerariumGenie.Alpha, 0.6f)));
|
||||
r.SetPropertyBlock(_mpb);
|
||||
}
|
||||
else
|
||||
{
|
||||
r.SetPropertyBlock(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
@@ -154,11 +199,33 @@ namespace NecromancerTome
|
||||
|
||||
void Update()
|
||||
{
|
||||
_yaw = (_yaw + CinerariumGenie.TurnSpeed * Time.deltaTime) % 360f;
|
||||
if (_stuck)
|
||||
{
|
||||
return; // замер как был
|
||||
}
|
||||
Vector3 home = new Vector3(0f, CinerariumGenie.OffsetY, 0f);
|
||||
float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
|
||||
if (_visitLeft > 0f)
|
||||
{
|
||||
// Туда и обратно по синусу: на полпути - над местом работы, лицом к нему.
|
||||
_visitLeft -= Time.deltaTime;
|
||||
float k = Mathf.Sin(Mathf.Clamp01(1f - _visitLeft / VisitSeconds) * Mathf.PI);
|
||||
Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + 0.6f, 0f);
|
||||
transform.localPosition = Vector3.Lerp(home, over, k);
|
||||
Vector3 d = _visit - home;
|
||||
if (d.x * d.x + d.z * d.z > 0.01f)
|
||||
{
|
||||
yawTarget = Mathf.LerpAngle(_yaw, Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg, Time.deltaTime * 4f);
|
||||
}
|
||||
_yaw = yawTarget % 360f;
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
return;
|
||||
}
|
||||
_yaw = yawTarget % 360f;
|
||||
float bob = CinerariumGenie.BobPeriod > 0.01f
|
||||
? Mathf.Sin((Time.time + _phase) * Mathf.PI * 2f / CinerariumGenie.BobPeriod) * CinerariumGenie.BobHeight
|
||||
: 0f;
|
||||
transform.localPosition = new Vector3(0f, CinerariumGenie.OffsetY + bob, 0f);
|
||||
transform.localPosition = home + new Vector3(0f, bob, 0f);
|
||||
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Работа духа урны - этап 4 некротехнолога (указание 16.10: дух - УНИВЕРСАЛЬНЫЙ работник зоны; BACKLOG «Некротехнолог v2»).
|
||||
/// Раз в StepSeconds - одно дело, по порядку:
|
||||
/// 1. пополнить расходники коллекторов из ящиков зоны: ячейки топлива (банки, цветы, корм, сферы - что коллектор
|
||||
/// принимает, GetFuelTypes) и катализатора (куры - GetCatalystTypes; по одной в пустую ячейку). Модификации
|
||||
/// коллекторов не трогаются;
|
||||
/// 2. забрать готовое из коллекторов (Items) и станций (TileEntityWorkstation.Output - верстак, кузня, химстанция,
|
||||
/// костёр, бетономешалка) и сложить в ящики зоны: сначала к таким же предметам, потом в свободное место; ящики полны -
|
||||
/// в сумку урны; некуда - дух чернеет и замирает над урной, оживает сам, когда место появится;
|
||||
/// 3. грядки - последними (ещё не сделано).
|
||||
/// Коллектор или станция подходят любые - будущий крольчатник обслуживается тем же кодом. Ящик - составной блок игрока
|
||||
/// с хранилищем (TEFeatureStorage, PlayerPlaced), кроме урн. Открытое у игрока (IsUserAccessing) не трогается.
|
||||
/// Работает на сервере: зовётся из TEFeatureCinerarium.UpdateTick.
|
||||
/// </summary>
|
||||
public static class CinerariumWork
|
||||
{
|
||||
public static float StepSeconds = 3f;
|
||||
|
||||
/// <summary>Что в зоне: собирается заново на каждом шаге (150 клеток - дёшево, а ящики могут переставить).</summary>
|
||||
sealed class Zone
|
||||
{
|
||||
public readonly List<TEFeatureStorage> Boxes = new List<TEFeatureStorage>();
|
||||
public readonly List<TileEntityCollector> Collectors = new List<TileEntityCollector>();
|
||||
public readonly List<TileEntityWorkstation> Stations = new List<TileEntityWorkstation>();
|
||||
public readonly Dictionary<TileEntity, Vector3i> Where = new Dictionary<TileEntity, Vector3i>();
|
||||
public TEFeatureStorage Bag;
|
||||
}
|
||||
|
||||
static readonly HashSet<TileEntity> _seen = new HashSet<TileEntity>();
|
||||
|
||||
/// <summary>Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда.</summary>
|
||||
public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck)
|
||||
{
|
||||
_stuck = false;
|
||||
Zone z = Scan(_world, _urn);
|
||||
// 1. Расходники.
|
||||
foreach (TileEntityCollector c in z.Collectors)
|
||||
{
|
||||
if (Refill(c, c.FuelSlots, c.GetFuelTypes(), false, z) || Refill(c, c.CatalystSlots, c.GetCatalystTypes(), true, z))
|
||||
{
|
||||
c.SetModified();
|
||||
return z.Where[c];
|
||||
}
|
||||
}
|
||||
// 2. Готовое.
|
||||
bool blocked = false;
|
||||
foreach (TileEntityCollector c in z.Collectors)
|
||||
{
|
||||
ItemStack[] items = c.Items;
|
||||
if (Unload(items, z, ref blocked))
|
||||
{
|
||||
c.Items = items;
|
||||
c.SetModified();
|
||||
return z.Where[c];
|
||||
}
|
||||
}
|
||||
foreach (TileEntityWorkstation w in z.Stations)
|
||||
{
|
||||
ItemStack[] output = w.Output;
|
||||
if (Unload(output, z, ref blocked))
|
||||
{
|
||||
w.Output = output;
|
||||
return z.Where[w];
|
||||
}
|
||||
}
|
||||
_stuck = blocked;
|
||||
return null;
|
||||
}
|
||||
|
||||
static Zone Scan(World _world, TEFeatureCinerarium _urn)
|
||||
{
|
||||
Zone z = new Zone();
|
||||
_seen.Clear();
|
||||
Vector3i c = _urn.ToWorldPos();
|
||||
if (_urn.Parent != null)
|
||||
{
|
||||
_urn.Parent.TryGetSelfOrFeature<TEFeatureStorage>(out z.Bag);
|
||||
_seen.Add(_urn.Parent);
|
||||
}
|
||||
int w = TEFeatureCinerarium.HalfWidth;
|
||||
for (int dy = -TEFeatureCinerarium.Below; dy <= TEFeatureCinerarium.Above; dy++)
|
||||
{
|
||||
for (int dx = -w; dx <= w; dx++)
|
||||
{
|
||||
for (int dz = -w; dz <= w; dz++)
|
||||
{
|
||||
Vector3i p = new Vector3i(c.x + dx, c.y + dy, c.z + dz);
|
||||
BlockValue bv = _world.GetBlock(p);
|
||||
if (bv.isair)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Vector3i owner = bv.ischild ? p + bv.parent : p; // часть составного блока (верстак 2x1) - к главной клетке
|
||||
TileEntity te = _world.GetTileEntity(owner);
|
||||
if (te == null || !_seen.Add(te) || te.IsUserAccessing())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (te is TileEntityCollector col)
|
||||
{
|
||||
z.Collectors.Add(col);
|
||||
z.Where[col] = owner;
|
||||
}
|
||||
else if (te is TileEntityWorkstation ws)
|
||||
{
|
||||
z.Stations.Add(ws);
|
||||
z.Where[ws] = owner;
|
||||
}
|
||||
else if (te is TileEntityComposite comp && comp.PlayerPlaced &&
|
||||
!comp.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
|
||||
comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage box))
|
||||
{
|
||||
z.Boxes.Add(box);
|
||||
z.Where[comp] = owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return z;
|
||||
}
|
||||
|
||||
/// <summary>Дополнить ячейки коллектора тем, что он принимает, из ящиков и сумки. _single - по одному предмету в
|
||||
/// пустую ячейку (куры), иначе до полного стака. true - что-то положено.</summary>
|
||||
static bool Refill(TileEntityCollector _c, ItemStack[] _slots, string[] _allowed, bool _single, Zone _z)
|
||||
{
|
||||
if (_slots == null || _allowed == null || _allowed.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
ItemStack slot = _slots[i];
|
||||
ItemClass want = null;
|
||||
int room;
|
||||
if (slot == null || slot.IsEmpty())
|
||||
{
|
||||
room = -1; // любой допустимый
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_single)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
want = slot.itemValue.ItemClass;
|
||||
room = want != null ? want.MaxCount - slot.count : 0;
|
||||
if (room <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
foreach (TEFeatureStorage src in Sources(_z))
|
||||
{
|
||||
for (int k = 0; k < src.ItemGrid.Length; k++)
|
||||
{
|
||||
ItemStack s = src.ItemGrid[k];
|
||||
if (s == null || s.IsEmpty() || Locked(src, k))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ItemClass ic = s.itemValue.ItemClass;
|
||||
if (ic == null || (want != null ? ic != want : System.Array.IndexOf(_allowed, ic.GetItemName()) < 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int take = _single ? 1 : (room < 0 ? Mathf.Min(s.count, ic.MaxCount) : Mathf.Min(s.count, room));
|
||||
if (take <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (want == null)
|
||||
{
|
||||
_slots[i] = new ItemStack(s.itemValue.Clone(), take);
|
||||
}
|
||||
else
|
||||
{
|
||||
_slots[i].count += take;
|
||||
}
|
||||
Take(src, k, take);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Одна стопка готового - в ящики (к таким же, потом в пустое), затем в сумку урны. true - что-то перенесено;
|
||||
/// _blocked - была стопка, которой не нашлось места.</summary>
|
||||
static bool Unload(ItemStack[] _items, Zone _z, ref bool _blocked)
|
||||
{
|
||||
if (_items == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < _items.Length; i++)
|
||||
{
|
||||
ItemStack s = _items[i];
|
||||
if (s == null || s.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int before = s.count;
|
||||
// Сначала ящики зоны (к таким же, потом в пустое), они полны - тогда сумка урны (решение 06.10).
|
||||
Place(s, _z.Boxes);
|
||||
if (s.count > 0 && _z.Bag != null)
|
||||
{
|
||||
Place(s, new[] { _z.Bag });
|
||||
}
|
||||
if (s.count == 0)
|
||||
{
|
||||
_items[i] = ItemStack.Empty.Clone();
|
||||
return true;
|
||||
}
|
||||
if (s.count != before)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_blocked = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void Place(ItemStack _s, IList<TEFeatureStorage> _dst)
|
||||
{
|
||||
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
|
||||
{
|
||||
_dst[i].TryStackItem(0, _s);
|
||||
}
|
||||
for (int i = 0; i < _dst.Count && _s.count > 0; i++)
|
||||
{
|
||||
if (_dst[i].AddItem(_s.Clone()))
|
||||
{
|
||||
_s.count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Откуда брать расходники: сумка урны (там то, что дух накопил), потом ящики зоны.</summary>
|
||||
static IEnumerable<TEFeatureStorage> Sources(Zone _z)
|
||||
{
|
||||
if (_z.Bag != null)
|
||||
{
|
||||
yield return _z.Bag;
|
||||
}
|
||||
foreach (TEFeatureStorage b in _z.Boxes)
|
||||
{
|
||||
yield return b;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ячейка, закреплённая игроком (замок в окне ящика), - не трогать.</summary>
|
||||
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)
|
||||
{
|
||||
ItemStack s = _src.ItemGrid[_slot].Clone();
|
||||
s.count -= _count;
|
||||
_src.UpdateSlot(_slot, s.count > 0 ? s : ItemStack.Empty.Clone());
|
||||
_src.SetModified();
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user