Files
necromants-tome-7d2d-3-2/HarmonySrc/CinerariumThreads.cs
T

407 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using GUI_2;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Нити цинерария (16.10.2026, BACKLOG «Нити цинерария»): перевозка между ящиками, как провода электрики. «Ящик на
/// территории одного цинерария и присоединяю к нему другой цинерарий проводом. А затем от цинерария тяну провод до другого
/// ящика. Дух в цинерарии будет забирать всё что лежит в ящике из которого в него тянется провод, и тащить в ящик, в
/// который тянется провод.» Решения: у урны несколько нитей «откуда» и одна «куда»; везётся по образцу (только то, что уже
/// лежит в ящике «куда»; пустой - всё подряд); «куда» полон - дух ждёт; нить призрачная; длина - как у провода (15 м);
/// ящики - любые. Урна с нитями - «логист»: её дух только перевозит, глазницы черепов светятся зелёным.
///
/// Инструмент - нож некроманта «хватом для нитей»: отдельный предмет necroWpnBladeNecroKnifeThread (класс ножа в сейве
/// менять нельзя), переключение - пункт в меню ножа по R (то же меню, что «человек/змея/чертоги», SnakeMode.cs; 16.10:
/// своё меню по R перекрывало его - «ты убрал хваты чертогов памяти и духа змеи»); качество и моды переносятся
/// (ItemValue.Clone со сменой типа). Как у стриппера: ПКМ - начать нить и закончить её (порядок = направление: ящик, потом
/// урна - «откуда»; урна, потом ящик - «куда»), ЛКМ по урне или ящику - снять его нити.
/// Первая версия - одиночная игра и хост (связи меняются на месте, клиентам сети - позже).
/// </summary>
public static class CinerariumThreads
{
public const string KnifeName = Patch_ItemClass_ExecuteAction_KnifePowerCost.KnifeName;
public const string ThreadKnifeName = "necroWpnBladeNecroKnifeThread";
/// <summary>Как у провода электрики: ItemActionConnectPower.maxWireLength = 15.</summary>
public const float MaxLength = 15f;
public const int MaxFrom = 8;
public const int GripNormal = 0, GripThread = 1;
/// <summary>Цвет нити - в цвет духа (CinerariumGenie.Tint).</summary>
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 IsThreadKnife(ItemClass _i) => _i != null && _i.Name == ThreadKnifeName;
// ------------------------------------------------------------------
// Хват.
// ------------------------------------------------------------------
/// <summary>Нож в руке меняется на _name: тот же ItemValue (качество, износ, моды), другой тип.</summary>
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 GameObject _preview;
public static Vector3i? Start => _start;
public static bool StartIsUrn => _startUrn;
public static void Cancel()
{
_start = null;
if (_preview != null)
{
Object.Destroy(_preview);
_preview = null;
}
}
/// <summary>Урна или ящик (составной блок игрока с хранилищем) в клетке _p; _owner - главная клетка блока.</summary>
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<TEFeatureCinerarium>(out _urn))
{
return true;
}
return comp.PlayerPlaced && comp.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage _);
}
public static TEFeatureCinerarium UrnAt(World _w, Vector3i _p)
{
return _w.GetTileEntity(_p) is TileEntityComposite c && c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium u) ? u : null;
}
/// <summary>Отпущена кнопка с ножом-для-нитей: _connect - ПКМ (тянуть нить), иначе ЛКМ (снять нити).</summary>
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;
}
bool ok = Resolve(w, hi.hit.blockPos, out Vector3i owner, out TEFeatureCinerarium urn);
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)
{
_start = owner;
_startUrn = urn != null;
_preview = new GameObject("NecroThreadPreview");
_preview.AddComponent<CinerariumThreadPreview>();
_pl.PlayOneShot("wiretool_grab");
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadStart"));
return;
}
Vector3i s = _start.Value;
bool sUrn = _startUrn;
Cancel();
if (s == owner)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadCancel"));
return;
}
if (sUrn == (urn != null))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadNeedPair"));
return;
}
if (Vector3.Distance(s.ToVector3(), owner.ToVector3()) > MaxLength)
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTooFar"));
return;
}
if (sUrn)
{
TEFeatureCinerarium u = UrnAt(w, s);
if (u == null)
{
return;
}
u.SetThreadTo(owner);
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadTo"));
}
else
{
if (!urn.AddThreadFrom(s))
{
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadLimit"));
return;
}
GameManager.ShowTooltip(_pl, Localization.Get("necroThreadFrom"));
}
_pl.PlayOneShot("wiretool_place");
}
/// <summary>Снять все нити урны или все нити, идущие к ящику (у всех урн сейва). Сколько снято.</summary>
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;
}
/// <summary>Где нить крепится: у урны - горлышко, у ящика - край со стороны урны, чуть выше середины. Сцена (без Origin).</summary>
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;
}
/// <summary>Провисающая нить: точка по доле пути _u.</summary>
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<LineRenderer>();
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;
}
}
/// <summary>Нити одной урны: провисающие линии и бегущие по ним огоньки (направление). Видны, только пока в руках нож
/// хватом для нитей. Дочерний объект модели урны -
/// сдвиги Origin и выгрузка чанка его не теряют; точки - в осях модели.</summary>
[Preserve]
public class CinerariumThreadView : MonoBehaviour
{
struct Thread
{
public Vector3 A, B; // в осях модели урны, огоньки бегут от A к B
public LineRenderer[] Sparks;
}
readonly List<Thread> _threads = new List<Thread>();
const int Segments = 16;
const int SparksPerThread = 3;
public static float SparkSpeed = 1.2f; // м/с
/// <summary>_pairs - концы в координатах сцены (без Origin). Видимость - заново по ножу в руке (Update).</summary>
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;
/// <summary>16.10: «Нити должны быть видны, только когда нож в режиме нитей и в руках».</summary>
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<Renderer>(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));
}
}
}
}
/// <summary>Нить, которую тянут: от начала до руки игрока; дальше 15 м - красная. Нож убрали - нить брошена.</summary>
[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.Start.Value;
Vector3 a = 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));
}
}
}
/// <summary>Нож для нитей не бьёт: ЛКМ и ПКМ - нити (на отпускание кнопки).</summary>
[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;
}
}
}