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

- Череп-ретранслятор: нить урны проходит через черепа-декор (цепочка), каждый пролёт до
  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
+116 -10
View File
@@ -25,6 +25,10 @@ namespace NecromancerTome
/// Урна рабочего (18.10): нити ящик -> урна - откуда брать ингредиенты и топливо, урна -> станция - где изготавливать,
/// урна -> ящик - куда класть готовое; станций и ящиков сколько угодно (до MaxFrom в каждую сторону). Нить между станцией
/// и урной рабочего - всегда «от урны», в каком бы порядке её ни тянули.
///
/// Череп-ретранслятор (18.10): череп-декор (блок necroSkullDecorCenter/Back) в середине нити. Начал нить у ящика или урны,
/// ПКМ по черепу - нить проходит через него, дальше - следующий череп или конец; каждый пролёт - до MaxLength. Черепа
/// хранятся у нити в урне (TEFeatureCinerarium.FromVia/ToVia/OutVia), ЛКМ по черепу - снять все нити через него.
/// </summary>
public static class CinerariumThreads
{
@@ -39,6 +43,13 @@ namespace NecromancerTome
public static readonly Color SparkColor = new Color(0.8f, 0.95f, 1f, 1f);
public static bool IsKnife(ItemClass _i) => _i != null && (_i.Name == KnifeName || _i.Name == ThreadKnifeName);
/// <summary>Череп-ретранслятор - череп-декор в мире (оба вида и старый блок-помощник).</summary>
public static bool IsRelay(Block _b)
{
string n = _b?.GetBlockName();
return n == SkullDecorPlace.CenterBlock || n == SkullDecorPlace.BackBlock || n == "necroSkullDecor";
}
public static bool IsThreadKnife(ItemClass _i) => _i != null && _i.Name == ThreadKnifeName;
// ------------------------------------------------------------------
@@ -72,13 +83,19 @@ namespace NecromancerTome
static bool _startUrn;
static bool _startStation;
static GameObject _preview;
/// <summary>Черепа, через которые уже прошла тянущаяся нить, по порядку от начала.</summary>
static readonly List<Vector3i> _via = new List<Vector3i>();
public static Vector3i? Start => _start;
public static bool StartIsUrn => _startUrn;
/// <summary>Откуда сейчас тянется нить: последний череп или начало.</summary>
public static Vector3i? LastPoint => _via.Count > 0 ? _via[_via.Count - 1] : _start;
public static bool LastIsRelay => _via.Count > 0;
public static void Cancel()
{
_start = null;
_via.Clear();
if (_preview != null)
{
Object.Destroy(_preview);
@@ -127,9 +144,20 @@ namespace NecromancerTome
{
return;
}
bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
ClickAt(_pl, w, hi.hit.blockPos, _connect);
}
/// <summary>Щелчок ножом для нитей по клетке _pos (луч уже разобран; команда necroclick - напрямую).</summary>
public static void ClickAt(EntityPlayerLocal _pl, World w, Vector3i _pos, bool _connect)
{
if (IsRelay(w.GetBlock(_pos).Block))
{
ClickRelay(_pl, w, _pos, _connect);
return;
}
bool ok = Resolve(w, _pos, out Vector3i owner, out TEFeatureCinerarium urn);
bool station = false;
if (!ok && StationAt(w, hi.hit.blockPos, out Vector3i stOwner) != null)
if (!ok && StationAt(w, _pos, out Vector3i stOwner) != null)
{
ok = true;
station = true;
@@ -174,12 +202,19 @@ namespace NecromancerTome
Vector3i s = _start.Value;
bool sUrn = _startUrn;
bool sStation = _startStation;
Vector3i last = LastPoint.Value;
// Черепа - по порядку от урны: начали у урны - как шли, начали у ящика или станции - в обратном порядке.
List<Vector3i> via = new List<Vector3i>(_via);
Cancel();
if (s == owner)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
return;
}
if (!sUrn)
{
via.Reverse();
}
// Станция - только с урной рабочего, нить всегда «от урны».
if (sStation || station)
{
@@ -190,12 +225,12 @@ namespace NecromancerTome
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedWorker"));
return;
}
if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
if (Vector3.Distance(last.ToVector3(), owner.ToVector3()) > MaxLength)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
return;
}
if (!wu.AddThreadOut(st))
if (!wu.AddThreadOut(st, via))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
return;
@@ -215,7 +250,7 @@ namespace NecromancerTome
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist"));
return;
}
if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
if (Vector3.Distance(last.ToVector3(), owner.ToVector3()) > MaxLength)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
return;
@@ -230,7 +265,7 @@ namespace NecromancerTome
if (u.IsWorker)
{
// У рабочего ящиков «куда» сколько угодно - туда готовое.
if (!u.AddThreadOut(owner))
if (!u.AddThreadOut(owner, via))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
return;
@@ -239,13 +274,13 @@ namespace NecromancerTome
}
else
{
u.SetThreadTo(owner);
u.SetThreadTo(owner, via);
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
}
}
else
{
if (!urn.AddThreadFrom(s))
if (!urn.AddThreadFrom(s, via))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
return;
@@ -255,6 +290,76 @@ namespace NecromancerTome
_pl.PlayOneShot("wiretool_place");
}
/// <summary>Щелчок ножом по черепу-ретранслятору: ПКМ при тянущейся нити - провести её через череп; ЛКМ - снять все нити
/// через него (или бросить тянущуюся).</summary>
static void ClickRelay(EntityPlayerLocal _pl, World _w, Vector3i _skull, bool _connect)
{
if (!_connect)
{
if (_start.HasValue)
{
Cancel();
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
return;
}
int n = 0;
foreach (Vector3i p in CinerariumRegistry.All())
{
TEFeatureCinerarium u = UrnAt(_w, p);
if (u != null)
{
n += u.RemoveThreadsVia(_skull);
}
}
if (n > 0)
{
_pl.PlayOneShot("wiretool_grab");
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadRemoved"));
}
return;
}
if (!_start.HasValue)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadRelayFirst"));
return;
}
if (_via.Contains(_skull))
{
return;
}
if (Vector3.Distance(LastPoint.Value.ToVector3(), _skull.ToVector3()) > MaxLength)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
return;
}
_via.Add(_skull);
_pl.PlayOneShot("wiretool_place");
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadRelay"));
}
/// <summary>Нить урны _urn к _end через черепа _via (по порядку от урны) - пролётами в _pairs, огоньки бегут по направлению
/// нити: _toUrn - от конца к урне (нить «откуда»), иначе от урны к концу.</summary>
public static void AddPath(List<(Vector3 a, Vector3 b)> _pairs, Vector3i _urn, List<Vector3i> _via, Vector3i _end, bool _toUrn)
{
List<Vector3> pts = new List<Vector3> { UrnPoint(_urn) };
if (_via != null)
{
foreach (Vector3i v in _via)
{
pts.Add(RelayPoint(v));
}
}
Vector3i before = _via != null && _via.Count > 0 ? _via[_via.Count - 1] : _urn;
pts.Add(BoxPoint(_end, before));
for (int i = 0; i + 1 < pts.Count; i++)
{
_pairs.Add(_toUrn ? (pts[i + 1], pts[i]) : (pts[i], pts[i + 1]));
}
}
/// <summary>Точка нити у черепа - над черепом-декором (он лежит на дне клетки). Сцена (без Origin).</summary>
public static Vector3 RelayPoint(Vector3i _skull) => World.blockToTransformPos(_skull) - Origin.position + Vector3.up * 0.3f;
/// <summary>Снять все нити урны или все нити, идущие к ящику (у всех урн сейва). Сколько снято.</summary>
static int RemoveAll(World _w, Vector3i _owner, TEFeatureCinerarium _urn)
{
@@ -444,8 +549,9 @@ namespace NecromancerTome
{
return;
}
Vector3i s = CinerariumThreads.Start.Value;
Vector3 a = World.blockToTransformPos(s) - Origin.position + Vector3.up * (CinerariumThreads.StartIsUrn ? 0.5f : 0.6f);
Vector3i s = CinerariumThreads.LastPoint.Value;
Vector3 a = CinerariumThreads.LastIsRelay ? CinerariumThreads.RelayPoint(s)
: World.blockToTransformPos(s) - Origin.position + Vector3.up * (CinerariumThreads.StartIsUrn ? 0.5f : 0.6f);
Transform cam = pl.playerCamera != null ? pl.playerCamera.transform : null;
Vector3 b = cam != null ? cam.position + cam.forward * 0.6f + cam.right * 0.2f - cam.up * 0.25f : pl.GetPosition() - Origin.position + Vector3.up * 1.2f;
bool far = Vector3.Distance(s.ToVector3(), pl.GetPosition()) > CinerariumThreads.MaxLength;