using System.Collections.Generic; using GUI_2; using HarmonyLib; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Нити цинерария (16.10.2026, BACKLOG «Нити цинерария»): перевозка между ящиками, как провода электрики. «Ящик на /// территории одного цинерария и присоединяю к нему другой цинерарий проводом. А затем от цинерария тяну провод до другого /// ящика. Дух в цинерарии будет забирать всё что лежит в ящике из которого в него тянется провод, и тащить в ящик, в /// который тянется провод.» Решения: у урны несколько нитей «откуда» и одна «куда»; везётся по образцу (только то, что уже /// лежит в ящике «куда»; пустой - всё подряд); «куда» полон - дух ждёт; нить призрачная; длина - как у провода (15 м); /// ящики - любые. Нити - только у отдельного блока «урна-логист» (necroCinerariumLogist, 16.10): без зоны, дух только перевозит, /// глазницы всегда зелёные. /// /// Инструмент - нож некроманта «хватом для нитей»: отдельный предмет necroWpnBladeNecroKnifeThread (класс ножа в сейве /// менять нельзя), переключение - пункт в меню ножа по R (то же меню, что «человек/змея/чертоги», SnakeMode.cs; 16.10: /// своё меню по R перекрывало его - «ты убрал хваты чертогов памяти и духа змеи»); качество и моды переносятся /// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом /// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити. /// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже). /// /// Урна рабочего (18.10): нити ящик -> урна - откуда брать ингредиенты и топливо, урна -> станция - где изготавливать, /// урна -> ящик - куда класть готовое; станций и ящиков сколько угодно (до MaxFrom в каждую сторону). Нить между станцией /// и урной рабочего - всегда «от урны», в каком бы порядке её ни тянули. /// /// Череп-ретранслятор (18.10): череп-декор (блок necroSkullDecorCenter/Back) в середине нити. Начал нить у ящика или урны, /// ПКМ по черепу - нить проходит через него, дальше - следующий череп или конец; каждый пролёт - до MaxLength. Черепа /// хранятся у нити в урне (TEFeatureCinerarium.FromVia/ToVia/OutVia), ЛКМ по черепу - снять все нити через него. /// public static class CinerariumThreads { public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName; public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread"; /// Как у провода электрики: ItemActionConnectPower.maxWireLength = 15. public const float MaxLength = 15f; public const int MaxFrom = 8; public const int GripNormal = 0, GripThread = 1; /// Цвет нити - в цвет духа (CinerariumGenie.Tint). public static readonly Color ThreadColor = new Color(0.55f, 0.8f, 1f, 0.45f); 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); /// Череп-ретранслятор - череп-декор в мире (оба вида и старый блок-помощник). 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; // ------------------------------------------------------------------ // Хват. // ------------------------------------------------------------------ /// Нож в руке меняется на _name: тот же ItemValue (качество, износ, моды), другой тип. public static void Convert(EntityPlayerLocal _pl, string _name) { ItemStack cur = _pl?.inventory?.holdingItemStack; if (cur == null || cur.IsEmpty() || !IsKnife(cur.itemValue.ItemClass) || cur.itemValue.ItemClass.Name == _name) { return; } ItemValue target = ItemClass.GetItem(_name); if (target == null || target.IsEmpty()) { return; } ItemValue iv = cur.itemValue.Clone(); iv.type = target.type; _pl.inventory.SetItem(_pl.inventory.holdingItemIdx, new ItemStack(iv, cur.count)); Cancel(); } // ------------------------------------------------------------------ // Соединение. // ------------------------------------------------------------------ static Vector3i? _start; static bool _startUrn; static bool _startStation; static GameObject _preview; /// Черепа, через которые уже прошла тянущаяся нить, по порядку от начала. static readonly List _via = new List(); public static Vector3i? Start => _start; public static bool StartIsUrn => _startUrn; /// Откуда сейчас тянется нить: последний череп или начало. 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); _preview = null; } } /// Урна или ящик (составной блок игрока с хранилищем) в клетке _p; _owner - главная клетка блока. public static bool Resolve(World _w, Vector3i _p, out Vector3i _owner, out TEFeatureCinerarium _urn) { BlockValue bv = _w.GetBlock(_p); _owner = bv.ischild ? _p + bv.parent : _p; _urn = null; if (!(_w.GetTileEntity(_owner) is TileEntityComposite comp)) { return false; } if (comp.TryGetSelfOrFeature(out _urn)) { return true; } return comp.PlayerPlaced && comp.TryGetSelfOrFeature(out TEFeatureStorage _); } /// Станция игры (верстак, костёр, кузня, химстанция, бетономешалка, ступка) в клетке _p; _owner - главная клетка. 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(out TEFeatureCinerarium u) ? u : null; } /// Отпущена кнопка с ножом-для-нитей: _connect - ПКМ (тянуть нить), иначе ЛКМ (снять нити). public static void Click(EntityPlayerLocal _pl, bool _connect) { World w = GameManager.Instance?.World; WorldRayHitInfo hi = _pl?.HitInfo; if (w == null || hi == null || !hi.bHitValid || !GameUtils.IsBlockOrTerrain(hi.tag) || hi.hit.distanceSq > 36f) { return; } ClickAt(_pl, w, hi.hit.blockPos, _connect); } /// Щелчок ножом для нитей по клетке _pos (луч уже разобран; команда necroclick - напрямую). 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, _pos, out Vector3i stOwner) != null) { ok = true; station = true; owner = stOwner; } if (!_connect) { if (_start.HasValue) { Cancel(); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel")); return; } if (ok && RemoveAll(w, owner, urn) > 0) { _pl.PlayOneShot("wiretool_grab"); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadRemoved")); } return; } if (!ok) { GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair")); return; } if (!_start.HasValue) { 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(); _pl.PlayOneShot("wiretool_grab"); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStart")); return; } Vector3i s = _start.Value; bool sUrn = _startUrn; bool sStation = _startStation; Vector3i last = LastPoint.Value; // Черепа - по порядку от урны: начали у урны - как шли, начали у ящика или станции - в обратном порядке. List via = new List(_via); Cancel(); if (s == owner) { GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel")); return; } if (!sUrn) { via.Reverse(); } // Станция - только с урной рабочего, нить всегда «от урны». 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(last.ToVector3(), owner.ToVector3()) > MaxLength) { GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar")); return; } if (!wu.AddThreadOut(st, via)) { 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.IsZoneless) { GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedLogist")); return; } if (Vector3.Distance(last.ToVector3(), owner.ToVector3()) > MaxLength) { GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar")); return; } if (sUrn) { TEFeatureCinerarium u = UrnAt(w, s); if (u == null) { return; } if (u.IsWorker) { // У рабочего ящиков «куда» сколько угодно - туда готовое. if (!u.AddThreadOut(owner, via)) { GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit")); return; } GameManager.ShowTooltip(_pl, Localization.Get("necroThreadOut")); } else { u.SetThreadTo(owner, via); GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo")); } } else { if (!urn.AddThreadFrom(s, via)) { GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit")); return; } GameManager.ShowTooltip(_pl, Localization.Get("necroThreadFrom")); } _pl.PlayOneShot("wiretool_place"); } /// Щелчок ножом по черепу-ретранслятору: ПКМ при тянущейся нити - провести её через череп; ЛКМ - снять все нити /// через него (или бросить тянущуюся). 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")); } /// Нить урны _urn к _end через черепа _via (по порядку от урны) - пролётами в _pairs, огоньки бегут по направлению /// нити: _toUrn - от конца к урне (нить «откуда»), иначе от урны к концу. public static void AddPath(List<(Vector3 a, Vector3 b)> _pairs, Vector3i _urn, List _via, Vector3i _end, bool _toUrn) { List pts = new List { 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])); } } /// Точка нити у черепа - над черепом-декором (он лежит на дне клетки). Сцена (без Origin). public static Vector3 RelayPoint(Vector3i _skull) => World.blockToTransformPos(_skull) - Origin.position + Vector3.up * 0.3f; /// Снять все нити урны или все нити, идущие к ящику (у всех урн сейва). Сколько снято. static int RemoveAll(World _w, Vector3i _owner, TEFeatureCinerarium _urn) { if (_urn != null) { return _urn.ClearThreads(); } int n = 0; foreach (Vector3i p in CinerariumRegistry.All()) { TEFeatureCinerarium u = UrnAt(_w, p); if (u != null) { n += u.RemoveThreadsTo(_owner); } } return n; } // ------------------------------------------------------------------ // Вид нитей. // ------------------------------------------------------------------ static Material _mat; public static Material Mat() { if (_mat == null) { _mat = new Material(Shader.Find("Sprites/Default")) { name = "NecroThread" }; } return _mat; } /// Где нить крепится: у урны - горлышко, у ящика - край со стороны урны, чуть выше середины. Сцена (без Origin). public static Vector3 UrnPoint(Vector3i _urn) => World.blockToTransformPos(_urn) - Origin.position + Vector3.up * 0.5f; public static Vector3 BoxPoint(Vector3i _box, Vector3i _urn) { Vector3 c = World.blockToTransformPos(_box) - Origin.position + Vector3.up * 0.6f; Vector3 d = (World.blockToTransformPos(_urn) - World.blockToTransformPos(_box)); d.y = 0f; if (d.sqrMagnitude > 0.01f) { d.Normalize(); c += d * 0.5f; } return c; } /// Провисающая нить: точка по доле пути _u. public static Vector3 Curve(Vector3 _a, Vector3 _b, float _u) { float sag = Mathf.Min(0.6f, Vector3.Distance(_a, _b) * 0.05f); return Vector3.Lerp(_a, _b, _u) + Vector3.down * (sag * 4f * _u * (1f - _u)); } public static LineRenderer NewLine(Transform _parent, string _name, float _width, Color _c) { GameObject g = new GameObject(_name); g.transform.SetParent(_parent, false); LineRenderer lr = g.AddComponent(); lr.useWorldSpace = false; lr.widthMultiplier = _width; lr.sharedMaterial = Mat(); lr.startColor = _c; lr.endColor = _c; lr.numCapVertices = 2; lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; lr.receiveShadows = false; return lr; } } /// Нити одной урны: провисающие линии и бегущие по ним огоньки (направление). Видны, только пока в руках нож /// хватом для нитей. Дочерний объект модели урны - /// сдвиги Origin и выгрузка чанка его не теряют; точки - в осях модели. [Preserve] public class CinerariumThreadView : MonoBehaviour { struct Thread { public Vector3 A, B; // в осях модели урны, огоньки бегут от A к B public LineRenderer[] Sparks; } readonly List _threads = new List(); const int Segments = 16; const int SparksPerThread = 3; public static float SparkSpeed = 1.2f; // м/с /// _pairs - концы в координатах сцены (без Origin). Видимость - заново по ножу в руке (Update). public void Build(List<(Vector3 a, Vector3 b)> _pairs) { foreach (Transform c in transform) { Destroy(c.gameObject); } _threads.Clear(); Transform t = transform; foreach ((Vector3 a, Vector3 b) in _pairs) { Thread th = new Thread { A = t.InverseTransformPoint(a), B = t.InverseTransformPoint(b), Sparks = new LineRenderer[SparksPerThread] }; LineRenderer lr = CinerariumThreads.NewLine(t, "thread", 0.025f, CinerariumThreads.ThreadColor); lr.positionCount = Segments + 1; for (int i = 0; i <= Segments; i++) { lr.SetPosition(i, CinerariumThreads.Curve(th.A, th.B, i / (float)Segments)); } for (int k = 0; k < SparksPerThread; k++) { th.Sparks[k] = CinerariumThreads.NewLine(t, "spark", 0.06f, CinerariumThreads.SparkColor); th.Sparks[k].positionCount = 2; } _threads.Add(th); } _shown = -1; } float _nextSleepCheck; bool _asleep; int _shown = -1; /// 16.10: «Нити должны быть видны, только когда нож в режиме нитей и в руках». public static bool ToolInHand() { EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer(); return pl != null && CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem); } void Update() { if (Time.time >= _nextSleepCheck) { _nextSleepCheck = Time.time + 0.25f; _asleep = CinerariumGenie.PlayerFar(transform.position); // засыпание (16.10): огоньки стоят int show = ToolInHand() ? 1 : 0; if (show != _shown) { _shown = show; foreach (Renderer r in GetComponentsInChildren(true)) { r.enabled = show == 1; } } } if (_asleep || _shown != 1) { return; } foreach (Thread th in _threads) { float len = Mathf.Max(0.5f, Vector3.Distance(th.A, th.B)); float step = 0.2f / len; for (int k = 0; k < th.Sparks.Length; k++) { float u = Mathf.Repeat(Time.time * SparkSpeed / len + k / (float)th.Sparks.Length, 1f); th.Sparks[k].SetPosition(0, CinerariumThreads.Curve(th.A, th.B, Mathf.Max(0f, u - step))); th.Sparks[k].SetPosition(1, CinerariumThreads.Curve(th.A, th.B, u)); } } } } /// Нить, которую тянут: от начала до руки игрока; дальше 15 м - красная. Нож убрали - нить брошена. [Preserve] public class CinerariumThreadPreview : MonoBehaviour { LineRenderer _lr; void Start() { _lr = CinerariumThreads.NewLine(transform, "line", 0.025f, CinerariumThreads.ThreadColor); _lr.useWorldSpace = true; _lr.positionCount = 17; } void Update() { EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer(); if (pl == null || !CinerariumThreads.Start.HasValue || !CinerariumThreads.IsThreadKnife(pl.inventory?.holdingItem)) { CinerariumThreads.Cancel(); return; } if (_lr == null) { return; } 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; Color c = far ? new Color(1f, 0.3f, 0.3f, 0.7f) : CinerariumThreads.ThreadColor; _lr.startColor = c; _lr.endColor = c; for (int i = 0; i <= 16; i++) { _lr.SetPosition(i, CinerariumThreads.Curve(a, b, i / 16f)); } } } /// Нож для нитей не бьёт: ЛКМ и ПКМ - нити (на отпускание кнопки). [HarmonyPatch(typeof(ItemClass), nameof(ItemClass.ExecuteAction))] public static class Patch_ItemClass_ExecuteAction_ThreadKnife { public static bool Prefix(ItemClass __instance, int _actionIdx, ItemInventoryData _data, bool _bReleased) { if (!CinerariumThreads.IsThreadKnife(__instance)) { return true; } if (_bReleased && _data?.holdingEntity is EntityPlayerLocal pl) { CinerariumThreads.Click(pl, _actionIdx == 1); } return false; } } }