Урны: череп-ретранслятор, прилив прячет духов, меры нагрузки; проверки без мыши

- Череп-ретранслятор: нить урны проходит через черепа-декор (цепочка), каждый пролёт до
  15 м; черепа хранятся у нити (запись урны v5, старые - без черепов). ПКМ ножом по черепу
  - провести нить, ЛКМ - снять нити через него; убрали череп - нить снимается.
- Прилив: ночью кровавой луны духи всех урн исчезают, работа стоит, утром - сама.
- Нагрузка: общий бюджет дел духов на кадр (3), анимируются только 16 ближайших духов;
  лимит урн сборщика смягчён - одна на 200 убийств (было 500), тексты на 13 языках.
- Описание логиста (кадило, два вида, маленький дух, мигание), черепа-декора (передаёт
  нить), подсказки ножа - на 13 языках.
- Разработка: necrogod, necrofps [uncap], necrotide, necroshot серией, necroclick по клетке,
  necrobox fill, necrourn с черепами на пути.

Проверено в игре (test8): мигание, нить 28 м через череп и её разрыв, прилив, 63 урны
сборщика: 13.0 мс на кадр против 11.8 мс без работы урн.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-18 14:42:52 +03:00
co-authored by Claude Opus 5.5
parent 6693d96d5e
commit 85d8036e81
10 changed files with 617 additions and 61 deletions
+231 -38
View File
@@ -68,8 +68,10 @@ namespace NecromancerTome
public const int HalfWidth = 2;
public const int Below = 1;
public const int Above = 4;
public const float KillsPerUrn = 500f;
const byte SaveVersion = 4; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10)
/// <summary>Убийств на одну урну сборщика. 17.10: «лимит урн - ослабить/убрать» - нагрузку теперь держит общий бюджет
/// шагов (CinerariumLoad), лимит остался мягким игровым порогом: было 500, стало 200.</summary>
public const float KillsPerUrn = 200f;
const byte SaveVersion = 5; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10), 5 - черепа-ретрансляторы (18.10)
public bool HasSkull;
public bool ZoneShown;
@@ -80,6 +82,13 @@ namespace NecromancerTome
public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0 || ThreadOut.Count > 0;
/// <summary>Нити рабочего «от урны»: станции (где изготавливать) и ящики (куда класть готовое), главные клетки блоков.</summary>
public readonly List<Vector3i> ThreadOut = new List<Vector3i>();
/// <summary>Черепа-ретрансляторы на пути каждой нити (18.10: «сам череп повысить из простого предмета, до промежуточного
/// ретранслятора... если понадобится протащить коммуникации дальше»; «Цепочка из нескольких»): клетки черепов-декора по
/// порядку ОТ УРНЫ к концу нити. Списки идут вровень с ThreadFrom / ThreadOut, ToVia - у нити «куда». Пусто - нить прямая.
/// Каждый пролёт - не длиннее CinerariumThreads.MaxLength. Убрали череп - нить через него снимается.</summary>
public readonly List<List<Vector3i>> FromVia = new List<List<Vector3i>>();
public readonly List<List<Vector3i>> OutVia = new List<List<Vector3i>>();
public readonly List<Vector3i> ToVia = new List<Vector3i>();
/// <summary>Образец рабочего: что изготавливать; число в стопке - сколько держать в запасе (ящики «куда» и выход станций).
/// Лежит в «Содержимом» урны второй ячейкой.</summary>
public ItemStack Sample = ItemStack.Empty.Clone();
@@ -138,6 +147,10 @@ namespace NecromancerTome
ThreadTo = other.ThreadTo;
ThreadOut.Clear();
ThreadOut.AddRange(other.ThreadOut);
CopyVia(other.FromVia, FromVia);
CopyVia(other.OutVia, OutVia);
ToVia.Clear();
ToVia.AddRange(other.ToVia);
Sample = other.Sample.Clone();
GroupMask = other.GroupMask;
_threadsDirty = true;
@@ -227,6 +240,61 @@ namespace NecromancerTome
}
CinerariumWriteHelper.WriteInts(_bw, o.ToArray());
(Sample ?? ItemStack.Empty).Clone().Write(_bw);
// v5: черепа на пути нитей - по нити: число черепов и их клетки; порядок нитей - как выше (откуда, куда, от урны).
List<int> via = new List<int>();
AlignVia();
foreach (List<Vector3i> l in FromVia)
{
AppendVia(via, l);
}
AppendVia(via, ToVia);
foreach (List<Vector3i> l in OutVia)
{
AppendVia(via, l);
}
CinerariumWriteHelper.WriteInts(_bw, via.ToArray());
}
static void AppendVia(List<int> _dst, List<Vector3i> _l)
{
_dst.Add(_l.Count);
foreach (Vector3i p in _l)
{
_dst.Add(p.x); _dst.Add(p.y); _dst.Add(p.z);
}
}
static List<Vector3i> TakeVia(int[] _v, ref int _k)
{
List<Vector3i> l = new List<Vector3i>();
int cnt = _k < _v.Length ? _v[_k++] : 0;
for (int i = 0; i < cnt && _k + 2 < _v.Length; i++, _k += 3)
{
l.Add(new Vector3i(_v[_k], _v[_k + 1], _v[_k + 2]));
}
return l;
}
static void CopyVia(List<List<Vector3i>> _src, List<List<Vector3i>> _dst)
{
_dst.Clear();
foreach (List<Vector3i> l in _src)
{
_dst.Add(new List<Vector3i>(l));
}
}
/// <summary>Списки черепов - вровень с нитями (старые записи без черепов, правки списков нитей).</summary>
void AlignVia()
{
while (FromVia.Count < ThreadFrom.Count) FromVia.Add(new List<Vector3i>());
while (FromVia.Count > ThreadFrom.Count) FromVia.RemoveAt(FromVia.Count - 1);
while (OutVia.Count < ThreadOut.Count) OutVia.Add(new List<Vector3i>());
while (OutVia.Count > ThreadOut.Count) OutVia.RemoveAt(OutVia.Count - 1);
if (!HasThreadTo)
{
ToVia.Clear();
}
}
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
@@ -298,6 +366,29 @@ namespace NecromancerTome
}
Sample = ItemStack.Empty.Clone().Read(_br);
}
FromVia.Clear();
OutVia.Clear();
ToVia.Clear();
if (version >= 5)
{
int n = _br.ReadInt32();
int[] v = new int[n];
for (int i = 0; i < n; i++)
{
v[i] = _br.ReadInt32();
}
int k = 0;
for (int i = 0; i < ThreadFrom.Count; i++)
{
FromVia.Add(TakeVia(v, ref k));
}
ToVia.AddRange(TakeVia(v, ref k));
for (int i = 0; i < ThreadOut.Count; i++)
{
OutVia.Add(TakeVia(v, ref k));
}
}
AlignVia();
_threadsDirty = true;
if (version > SaveVersion)
{
@@ -337,7 +428,10 @@ namespace NecromancerTome
}
SyncGenie(false);
SyncThreads(_world);
Work(_world);
if (!CinerariumLoad.Tide)
{
Work(_world);
}
}
// ------------------------------------------------------------------
@@ -348,39 +442,54 @@ namespace NecromancerTome
GameObject _threadView;
float _nextThreadCheck;
public bool AddThreadFrom(Vector3i _box)
/// <summary>_via - черепа-ретрансляторы по порядку от урны к ящику (null - прямая нить). Нить к тому же ящику -
/// заменяет прежнюю (новый путь).</summary>
public bool AddThreadFrom(Vector3i _box, List<Vector3i> _via = null)
{
if (ThreadFrom.Contains(_box))
AlignVia();
int i = ThreadFrom.IndexOf(_box);
if (i < 0)
{
return true;
if (ThreadFrom.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadFrom.Add(_box);
FromVia.Add(new List<Vector3i>());
i = ThreadFrom.Count - 1;
}
if (ThreadFrom.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadFrom.Add(_box);
FromVia[i] = _via != null ? new List<Vector3i>(_via) : new List<Vector3i>();
ThreadsChanged();
return true;
}
public void SetThreadTo(Vector3i _box)
public void SetThreadTo(Vector3i _box, List<Vector3i> _via = null)
{
HasThreadTo = true;
ThreadTo = _box;
ToVia.Clear();
if (_via != null)
{
ToVia.AddRange(_via);
}
ThreadsChanged();
}
public bool AddThreadOut(Vector3i _target)
public bool AddThreadOut(Vector3i _target, List<Vector3i> _via = null)
{
if (ThreadOut.Contains(_target))
AlignVia();
int i = ThreadOut.IndexOf(_target);
if (i < 0)
{
return true;
if (ThreadOut.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadOut.Add(_target);
OutVia.Add(new List<Vector3i>());
i = ThreadOut.Count - 1;
}
if (ThreadOut.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadOut.Add(_target);
OutVia[i] = _via != null ? new List<Vector3i>(_via) : new List<Vector3i>();
ThreadsChanged();
return true;
}
@@ -390,6 +499,9 @@ namespace NecromancerTome
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0) + ThreadOut.Count;
ThreadFrom.Clear();
ThreadOut.Clear();
FromVia.Clear();
OutVia.Clear();
ToVia.Clear();
HasThreadTo = false;
if (n > 0)
{
@@ -398,14 +510,41 @@ namespace NecromancerTome
return n;
}
public int RemoveThreadsTo(Vector3i _box)
/// <summary>Снять нити, у которых конец (_end) или череп на пути (_via) - в клетке _p.</summary>
int RemoveWhere(Vector3i _p, bool _end, bool _via)
{
int n = ThreadFrom.RemoveAll(p => p == _box) + ThreadOut.RemoveAll(p => p == _box);
if (HasThreadTo && ThreadTo == _box)
AlignVia();
int n = 0;
for (int i = ThreadFrom.Count - 1; i >= 0; i--)
{
if (_end && ThreadFrom[i] == _p || _via && FromVia[i].Contains(_p))
{
ThreadFrom.RemoveAt(i);
FromVia.RemoveAt(i);
n++;
}
}
for (int i = ThreadOut.Count - 1; i >= 0; i--)
{
if (_end && ThreadOut[i] == _p || _via && OutVia[i].Contains(_p))
{
ThreadOut.RemoveAt(i);
OutVia.RemoveAt(i);
n++;
}
}
if (HasThreadTo && (_end && ThreadTo == _p || _via && ToVia.Contains(_p)))
{
HasThreadTo = false;
ToVia.Clear();
n++;
}
return n;
}
public int RemoveThreadsTo(Vector3i _box)
{
int n = RemoveWhere(_box, true, false);
if (n > 0)
{
ThreadsChanged();
@@ -413,6 +552,53 @@ namespace NecromancerTome
return n;
}
/// <summary>Снять нити, идущие через череп _skull (ЛКМ ножом по черепу; череп убрали).</summary>
public int RemoveThreadsVia(Vector3i _skull)
{
int n = RemoveWhere(_skull, false, true);
if (n > 0)
{
ThreadsChanged();
}
return n;
}
/// <summary>Все черепы на нитях урны в клетках, где их больше нет (чанк загружен), - нити через них снимаются.</summary>
bool DropBrokenRelays(World _world)
{
AlignVia();
bool gone = false;
foreach (List<Vector3i> l in AllVia())
{
foreach (Vector3i p in l)
{
if (_world.GetChunkFromWorldPos(p) != null && !CinerariumThreads.IsRelay(_world.GetBlock(p).Block))
{
if (RemoveWhere(p, false, true) > 0)
{
Debug.Log("[NecromancerTome] Cinerarium: череп-ретранслятор " + p + " убран - нить урны " + ToWorldPos() + " порвалась");
gone = true;
}
return gone; // списки изменились - остальное в следующей проверке
}
}
}
return gone;
}
IEnumerable<List<Vector3i>> AllVia()
{
foreach (List<Vector3i> l in FromVia)
{
yield return l;
}
yield return ToVia;
foreach (List<Vector3i> l in OutVia)
{
yield return l;
}
}
void ThreadsChanged()
{
_threadsDirty = true;
@@ -427,22 +613,19 @@ namespace NecromancerTome
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsZoneless && _logistKnown)
{
Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у логиста и рабочего");
ThreadFrom.Clear();
ThreadOut.Clear();
HasThreadTo = false;
ThreadsChanged();
ClearThreads();
}
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && Time.time >= _nextThreadCheck)
{
_nextThreadCheck = Time.time + 2f;
bool gone = false;
bool gone = DropBrokenRelays(_world);
foreach (Vector3i p in ThreadFrom.ToArray())
{
if (!CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u) || u != null)
{
if (_world.GetChunkFromWorldPos(p) != null) // чанк не загружен - не снимать
{
ThreadFrom.Remove(p);
RemoveWhere(p, true, false);
gone = true;
}
}
@@ -451,6 +634,7 @@ namespace NecromancerTome
_world.GetChunkFromWorldPos(ThreadTo) != null)
{
HasThreadTo = false;
ToVia.Clear();
gone = true;
}
foreach (Vector3i p in ThreadOut.ToArray())
@@ -458,7 +642,7 @@ namespace NecromancerTome
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);
RemoveWhere(p, true, false);
gone = true;
}
}
@@ -490,18 +674,19 @@ namespace NecromancerTome
_threadView.AddComponent<CinerariumThreadView>();
}
Vector3i me = ToWorldPos();
AlignVia();
List<(Vector3 a, Vector3 b)> pairs = new List<(Vector3, Vector3)>();
foreach (Vector3i p in ThreadFrom)
for (int i = 0; i < ThreadFrom.Count; i++)
{
pairs.Add((CinerariumThreads.BoxPoint(p, me), CinerariumThreads.UrnPoint(me)));
CinerariumThreads.AddPath(pairs, me, FromVia[i], ThreadFrom[i], true);
}
if (HasThreadTo)
{
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me)));
CinerariumThreads.AddPath(pairs, me, ToVia, ThreadTo, false);
}
foreach (Vector3i p in ThreadOut)
for (int i = 0; i < ThreadOut.Count; i++)
{
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(p, me)));
CinerariumThreads.AddPath(pairs, me, OutVia[i], ThreadOut[i], false);
}
_threadView.GetComponent<CinerariumThreadView>().Build(pairs);
}
@@ -543,9 +728,9 @@ namespace NecromancerTome
_nextWork = Time.time + CinerariumWork.StepSeconds;
return;
}
if (Time.time < _nextWork)
if (Time.time < _nextWork || !CinerariumLoad.TryTakeStep())
{
return;
return; // не время или бюджет кадра на дела духов выбран - в следующем кадре
}
_nextWork = Time.time + CinerariumWork.StepSeconds;
Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck, true);
@@ -691,7 +876,15 @@ namespace NecromancerTome
public void SyncGenie(bool _sound)
{
SyncGlow();
if (HasSkull && _genie == null)
// Прилив (17.10: «думаю всех некротехнологов нужно прятать во время прилива... В прилив работа тоже стоит. Урны светятся
// как с останками, но духи исчезают»): плотность некроэнергии в воздухе сравнялась с плотностью духа - вне урны он не
// держит форму. Свечение урны - по черепу, как обычно; утром дух появляется сам.
bool tide = CinerariumLoad.Tide;
if (tide && _genie != null)
{
RemoveGenie(false);
}
if (HasSkull && _genie == null && !tide)
{
int logist = 0;
if (IsLogist)