Нити цинерария: нож «хватом для нитей» (R), нити урна-ящик как у стриппера, урна-логист перевозит по образцу, зелёные глазницы

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-16 20:29:50 +03:00
co-authored by Claude Opus 5.5
parent fad57a3756
commit d8e7c8c66a
10 changed files with 885 additions and 63 deletions
File diff suppressed because one or more lines are too long
+15
View File
@@ -2018,4 +2018,19 @@
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necroblood?necroJuicePrefab.prefab"/>
</item>
</append>
<!-- Нож некроманта «хватом для нитей» (16.10.2026, HarmonySrc/CinerariumThreads.cs): «нож некроманта, но взятый особым
хватом... как кисточка или как тот же стриппер». Отдельный предмет - класс ножа в сейве менять нельзя; переключение -
меню по R (качество, износ и моды переносятся). Не бьёт: ПКМ - тянуть нить урна-ящик, ЛКМ - снять нити. Удары
перехвачены в ItemClass.ExecuteAction, поэтому действия ножа (Extends) остаются, но не срабатывают. HoldType 52 -
хват стриппера (meleeToolWireTool); посадку модели проверить в игре. effect_group через Extends не наследуется -
у этого предмета нет урона, лечения и слотов модов (моды в ItemValue сохраняются и вернутся с обычным хватом). -->
<append xpath="/items">
<item name="necroWpnBladeNecroKnifeThread">
<property name="Extends" value="necroWpnBladeNecroKnife"/>
<property name="CreativeMode" value="None"/>
<property name="HoldType" value="52"/>
<property name="DescriptionKey" value="necroWpnBladeNecroKnifeThreadDesc"/>
<property name="CustomIconTint" value="150, 205, 255"/>
</item>
</append>
</config>
+244 -6
View File
@@ -36,10 +36,15 @@ namespace NecromancerTome
public const int Below = 1;
public const int Above = 4;
public const float KillsPerUrn = 500f;
const byte SaveVersion = 1;
const byte SaveVersion = 2; // 2 - нити (16.10)
public bool HasSkull;
public bool ZoneShown;
/// <summary>Нити (CinerariumThreads.cs): ящики «откуда» и ящик «куда», главные клетки блоков. Есть нити - урна-логист.</summary>
public readonly List<Vector3i> ThreadFrom = new List<Vector3i>();
public bool HasThreadTo;
public Vector3i ThreadTo;
public bool IsLogist => HasThreadTo || ThreadFrom.Count > 0;
public override void CopyFromInternal(TileEntityComposite _other)
{
@@ -47,6 +52,11 @@ namespace NecromancerTome
{
HasSkull = other.HasSkull;
ZoneShown = other.ZoneShown;
ThreadFrom.Clear();
ThreadFrom.AddRange(other.ThreadFrom);
HasThreadTo = other.HasThreadTo;
ThreadTo = other.ThreadTo;
_threadsDirty = true;
}
}
@@ -69,6 +79,7 @@ namespace NecromancerTome
{
base.OnRemove(_world);
RemoveBounds();
RemoveThreadView();
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
if (!ConnectionManager.Instance.IsServer)
@@ -95,6 +106,8 @@ namespace NecromancerTome
{
base.OnUnload(_world);
RemoveBounds();
RemoveThreadView();
_threadsDirty = true; // чанк вернётся - нити построить заново
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
}
@@ -103,6 +116,14 @@ namespace NecromancerTome
{
base.Write(_bw, _eStreamMode);
CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown);
List<int> t = new List<int> { ThreadFrom.Count };
foreach (Vector3i p in ThreadFrom)
{
t.Add(p.x); t.Add(p.y); t.Add(p.z);
}
t.Add(HasThreadTo ? 1 : 0);
t.Add(ThreadTo.x); t.Add(ThreadTo.y); t.Add(ThreadTo.z);
CinerariumWriteHelper.WriteInts(_bw, t.ToArray());
}
public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode)
@@ -111,6 +132,29 @@ namespace NecromancerTome
byte version = _br.ReadByte();
HasSkull = _br.ReadBoolean();
ZoneShown = _br.ReadBoolean();
ThreadFrom.Clear();
HasThreadTo = false;
if (version >= 2)
{
int n = _br.ReadInt32();
int[] v = new int[n];
for (int i = 0; i < n; i++)
{
v[i] = _br.ReadInt32();
}
int k = 0;
int from = n > 0 ? v[k++] : 0;
for (int i = 0; i < from && k + 2 < n; i++, k += 3)
{
ThreadFrom.Add(new Vector3i(v[k], v[k + 1], v[k + 2]));
}
if (k + 3 < n)
{
HasThreadTo = v[k] != 0;
ThreadTo = new Vector3i(v[k + 1], v[k + 2], v[k + 3]);
}
}
_threadsDirty = true;
if (version > SaveVersion)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: урна из более новой версии мода (" + version + ") - прочитано только известное");
@@ -148,9 +192,148 @@ namespace NecromancerTome
SpawnBounds();
}
SyncGenie(false);
SyncThreads(_world);
Work(_world);
}
// ------------------------------------------------------------------
// Нити (CinerariumThreads.cs).
// ------------------------------------------------------------------
bool _threadsDirty = true;
GameObject _threadView;
float _nextThreadCheck;
public bool AddThreadFrom(Vector3i _box)
{
if (ThreadFrom.Contains(_box))
{
return true;
}
if (ThreadFrom.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadFrom.Add(_box);
ThreadsChanged();
return true;
}
public void SetThreadTo(Vector3i _box)
{
HasThreadTo = true;
ThreadTo = _box;
ThreadsChanged();
}
public int ClearThreads()
{
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0);
ThreadFrom.Clear();
HasThreadTo = false;
if (n > 0)
{
ThreadsChanged();
}
return n;
}
public int RemoveThreadsTo(Vector3i _box)
{
int n = ThreadFrom.RemoveAll(p => p == _box);
if (HasThreadTo && ThreadTo == _box)
{
HasThreadTo = false;
n++;
}
if (n > 0)
{
ThreadsChanged();
}
return n;
}
void ThreadsChanged()
{
_threadsDirty = true;
_glowState = -1; // логист - другие глаза
SetModified();
Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет"));
}
/// <summary>Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.</summary>
void SyncThreads(World _world)
{
if (_world != null && ConnectionManager.Instance.IsServer && IsLogist && Time.time >= _nextThreadCheck)
{
_nextThreadCheck = Time.time + 2f;
bool gone = false;
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);
gone = true;
}
}
}
if (HasThreadTo && (!CinerariumThreads.Resolve(_world, ThreadTo, out Vector3i _, out TEFeatureCinerarium u2) || u2 != null) &&
_world.GetChunkFromWorldPos(ThreadTo) != null)
{
HasThreadTo = false;
gone = true;
}
if (gone)
{
ThreadsChanged();
}
}
if (!_threadsDirty && (_threadView != null || !IsLogist))
{
return;
}
Transform t = ModelTransform();
if (t == null)
{
return;
}
_threadsDirty = false;
SyncGlow();
if (!IsLogist)
{
RemoveThreadView();
return;
}
if (_threadView == null)
{
_threadView = new GameObject("NecroCinerariumThreads");
_threadView.transform.SetParent(t, false);
_threadView.AddComponent<CinerariumThreadView>();
}
Vector3i me = ToWorldPos();
List<(Vector3 a, Vector3 b)> pairs = new List<(Vector3, Vector3)>();
foreach (Vector3i p in ThreadFrom)
{
pairs.Add((CinerariumThreads.BoxPoint(p, me), CinerariumThreads.UrnPoint(me)));
}
if (HasThreadTo)
{
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me)));
}
_threadView.GetComponent<CinerariumThreadView>().Build(pairs);
}
void RemoveThreadView()
{
if (_threadView != null)
{
UnityEngine.Object.Destroy(_threadView);
_threadView = null;
}
}
float _nextWork;
public bool Stuck;
/// <summary>Дело найдено пробным шагом, дух полетел; выполнить в _actAt (16.10: «делал действие не сразу»).</summary>
@@ -197,11 +380,17 @@ namespace NecromancerTome
if (at.HasValue)
{
_pending = true;
_actAt = Time.time + (motion != null ? CinerariumGenieMotion.ActDelay : 0f);
_actAt = Time.time;
if (motion != null)
{
Vector3 look = World.blockToTransformPos(at.Value) - Origin.position;
motion.Visit(Stand(_world, at.Value, out bool beside) - Origin.position, look, beside);
Vector3i? from = CinerariumWork.LastFrom;
List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)>();
if (from.HasValue)
{
stops.Add((Stand(_world, from.Value, out bool b0) - Origin.position, World.blockToTransformPos(from.Value) - Origin.position, b0));
}
stops.Add((Stand(_world, at.Value, out bool beside) - Origin.position, World.blockToTransformPos(at.Value) - Origin.position, beside));
_actAt = Time.time + motion.Visit(stops);
}
}
}
@@ -330,23 +519,67 @@ namespace NecromancerTome
void SyncGlow()
{
Transform t = ModelTransform();
int want = HasSkull ? 1 : 0;
int want = HasSkull ? (IsLogist ? 2 : 1) : 0;
Texture logist = want == 2 ? LogistEmission() : null;
if (t == null || (_glowState == want && _glowModel == t))
{
return;
}
foreach (Renderer r in t.GetComponentsInChildren<Renderer>(true))
{
if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform))
if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform) ||
_threadView != null && r.transform.IsChildOf(_threadView.transform))
{
continue;
}
r.GetPropertyBlock(_glowBlock);
_glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black);
if (logist != null)
{
_glowBlock.SetTexture(EmissionMapId, logist);
}
else if (_hadLogistTex)
{
_glowBlock.Clear(); // вернуть текстуру материала
_glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black);
}
r.SetPropertyBlock(_glowBlock);
}
_glowState = want;
_glowModel = t;
_hadLogistTex = logist != null;
}
bool _hadLogistTex;
static readonly int EmissionMapId = Shader.PropertyToID("_EmissionMap");
static Texture2D _logistTex;
static bool _logistTried;
public const string LogistTextureFile = "necroCinerarium_e_logist.png";
/// <summary>Свечение урны-логиста (16.10: «пусть глаза черепов светятся другим цветом. Зелёным»): копия запечённой
/// necroCinerarium_e с зелёными глазницами (руны - те же), из папки Resources мода - бандл не пересобирается.</summary>
static Texture2D LogistEmission()
{
if (!_logistTried)
{
_logistTried = true;
try
{
string path = Path.Combine(ModEntry.Instance.Path, "Resources", LogistTextureFile);
Texture2D tex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
if (tex.LoadImage(File.ReadAllBytes(path)))
{
tex.name = "necroCinerarium_e_logist";
tex.anisoLevel = 4;
_logistTex = tex;
}
}
catch (Exception e)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: свечение логиста не загружено (" + e.Message + ")");
}
}
return _logistTex;
}
Transform _glowModel;
@@ -563,6 +796,11 @@ namespace NecromancerTome
return n;
}
public static List<Vector3i> All()
{
return new List<Vector3i>(Urns().Keys);
}
/// <summary>Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям,
/// по высоте ближе 6 (зона от -1 до +4).</summary>
public static bool Overlaps(Vector3i _pos)
+91 -56
View File
@@ -227,15 +227,22 @@ namespace NecromancerTome
float _yaw;
float _phase;
/// <summary>Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. ActDelay),
/// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. Visit),
/// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома.</summary>
Vector3 _visit;
struct Stop
{
public Vector3 Hover, Look; // в осях модели урны
public float Fly;
}
readonly List<Stop> _stops = new List<Stop>();
float _backFly;
float _t = -1f;
/// <summary>Скорость полёта, м/с: время перелёта - не меньше OutSeconds/BackSeconds, дальше - по расстоянию (нити до 15 м).</summary>
public static float FlySpeed = 4f;
public static float OutSeconds = 1.3f;
public static float WorkSeconds = 1.6f;
public static float BackSeconds = 1.3f;
/// <summary>Через сколько после начала полёта дело выполняется - в середине жеста.</summary>
public static float ActDelay => OutSeconds + WorkSeconds * 0.5f;
/// <summary>Жест: плечи вперёд до GestureFwd градусов, локти ходят на ±GestureWave вразнобой.</summary>
public static float GestureFwd = 55f;
public static float GestureWave = 30f;
@@ -309,21 +316,63 @@ namespace NecromancerTome
/// <summary>Светящиеся материалы рендереров - вернуть после почернения.</summary>
readonly Dictionary<Renderer, Material[]> _glowing = new Dictionary<Renderer, Material[]>();
/// <summary>Куда смотреть за работой (в координатах модели урны); _beside - дух стоит сбоку от места, а не над ним.</summary>
Vector3 _look;
bool _beside;
/// <summary>Точки - в координатах сцены (без Origin): _stand - где встать (низ клетки), _look - низ клетки места работы.</summary>
public void Visit(Vector3 _stand, Vector3 _lookAt, bool _besideIt)
/// <summary>Облёт мест работы по порядку (у перевозки по нити - ящик «откуда», потом «куда»), у каждого - жест, затем
/// назад к урне. Точки - в координатах сцены (без Origin): stand - где встать (низ клетки), look - низ клетки места,
/// beside - встал сбоку, а не над местом. Возвращает, через сколько секунд дело (середина последнего жеста).</summary>
public float Visit(List<(Vector3 stand, Vector3 look, bool beside)> _list)
{
if (transform.parent == null)
if (transform.parent == null || _list == null || _list.Count == 0)
{
return;
return 0f;
}
_visit = transform.parent.InverseTransformPoint(_stand);
_look = transform.parent.InverseTransformPoint(_lookAt);
_beside = _besideIt;
_stops.Clear();
Vector3 home = Home();
Vector3 prev = home;
float delay = 0f;
foreach ((Vector3 stand, Vector3 look, bool beside) in _list)
{
Stop st = new Stop
{
Hover = transform.parent.InverseTransformPoint(stand) + new Vector3(0f, CinerariumGenie.OffsetY + (beside ? 0.15f : 0.8f), 0f),
Look = transform.parent.InverseTransformPoint(look)
};
st.Fly = Mathf.Max(OutSeconds, Vector3.Distance(prev, st.Hover) / FlySpeed);
delay += st.Fly + WorkSeconds;
prev = st.Hover;
_stops.Add(st);
}
_backFly = Mathf.Max(BackSeconds, Vector3.Distance(prev, home) / FlySpeed);
_t = 0f;
return delay - WorkSeconds * 0.5f;
}
Vector3 Home()
{
return new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
}
/// <summary>Повернуться лицом по направлению _dir (горизонтально), не быстрее 300°/с.</summary>
void Face(Vector3 _dir)
{
if (_dir.x * _dir.x + _dir.z * _dir.z > 0.01f)
{
_yaw = Mathf.MoveTowardsAngle(_yaw, Mathf.Atan2(_dir.x, _dir.z) * Mathf.Rad2Deg, 300f * Time.deltaTime) % 360f;
}
}
void Gesture(float _w)
{
if (_w > 0f)
{
float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * _w;
Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, _w), CinerariumGenie.Elbow + 15f * _w + wave, CinerariumGenie.Elbow + 15f * _w - wave);
_gesturing = true;
}
else if (_gesturing)
{
Pose();
_gesturing = false;
}
}
public void SetStuck(bool _on)
@@ -379,58 +428,44 @@ namespace NecromancerTome
return; // замер как был
}
float yawTarget = _yaw + CinerariumGenie.TurnSpeed * Time.deltaTime;
Vector3 home = new Vector3(0f, CinerariumGenie.OffsetY, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
if (_t >= 0f)
Vector3 home = Home();
if (_t >= 0f && _stops.Count > 0)
{
_t += Time.deltaTime;
if (_t < OutSeconds + WorkSeconds + BackSeconds)
float acc = 0f;
Vector3 from = home;
foreach (Stop st in _stops)
{
// Сбоку - на уровне места (тело вровень со станцией), над местом - выше него.
Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + (_beside ? 0.15f : 0.8f), 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward);
Vector3 d = _visit - new Vector3(0f, CinerariumGenie.OffsetY, 0f);
bool far = d.x * d.x + d.z * d.z > 0.01f;
float toPlace = far ? Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg : _yaw;
Vector3 dl = _look - _visit;
float toLook = dl.x * dl.x + dl.z * dl.z > 0.01f ? Mathf.Atan2(dl.x, dl.z) * Mathf.Rad2Deg : toPlace;
float k, face, w = 0f;
if (_t < OutSeconds)
if (_t < acc + st.Fly)
{
k = Mathf.SmoothStep(0f, 1f, _t / OutSeconds);
face = toPlace;
transform.localPosition = Vector3.Lerp(from, st.Hover, Mathf.SmoothStep(0f, 1f, (_t - acc) / st.Fly));
Face(st.Hover - from);
Gesture(0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return;
}
else if (_t < OutSeconds + WorkSeconds)
acc += st.Fly;
if (_t < acc + WorkSeconds)
{
k = 1f;
face = toLook; // за работой - лицом к станции
w = Mathf.Sin((_t - OutSeconds) / WorkSeconds * Mathf.PI);
transform.localPosition = st.Hover;
Face(st.Look - st.Hover); // за работой - лицом к месту
Gesture(Mathf.Sin((_t - acc) / WorkSeconds * Mathf.PI));
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return;
}
else
{
k = 1f - Mathf.SmoothStep(0f, 1f, (_t - OutSeconds - WorkSeconds) / BackSeconds);
face = far ? toPlace + 180f : _yaw; // назад - лицом к урне
}
_yaw = Mathf.MoveTowardsAngle(_yaw, face, 300f * Time.deltaTime) % 360f;
transform.localPosition = Vector3.Lerp(home, over, k);
acc += WorkSeconds;
from = st.Hover;
}
if (_t < acc + _backFly)
{
transform.localPosition = Vector3.Lerp(from, home, Mathf.SmoothStep(0f, 1f, (_t - acc) / _backFly));
Face(home - from); // назад - лицом к урне
Gesture(0f);
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
if (w > 0f)
{
float wave = Mathf.Sin(_t * Mathf.PI * 2f * 1.5f) * GestureWave * w;
Pose(Mathf.Lerp(CinerariumGenie.ArmFwd, GestureFwd, w), CinerariumGenie.Elbow + 15f * w + wave, CinerariumGenie.Elbow + 15f * w - wave);
_gesturing = true;
}
else if (_gesturing)
{
Pose();
_gesturing = false;
}
return;
}
_t = -1f;
if (_gesturing)
{
Pose();
_gesturing = false;
}
Gesture(0f);
}
_yaw = yawTarget % 360f;
float bob = CinerariumGenie.BobPeriod > 0.01f
+429
View File
@@ -0,0 +1,429 @@
using System.Collections.Generic;
using GUI_2;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Нити цинерария (16.10.2026, BACKLOG «Нити цинерария»): перевозка между ящиками, как провода электрики. «Ящик на
/// территории одного цинерария и присоединяю к нему другой цинерарий проводом. А затем от цинерария тяну провод до другого
/// ящика. Дух в цинерарии будет забирать всё что лежит в ящике из которого в него тянется провод, и тащить в ящик, в
/// который тянется провод.» Решения: у урны несколько нитей «откуда» и одна «куда»; везётся по образцу (только то, что уже
/// лежит в ящике «куда»; пустой - всё подряд); «куда» полон - дух ждёт; нить призрачная; длина - как у провода (15 м);
/// ящики - любые. Урна с нитями - «логист»: её дух только перевозит, глазницы черепов светятся зелёным.
///
/// Инструмент - нож некроманта «хватом для нитей»: отдельный предмет necroWpnBladeNecroKnifeThread (класс ножа в сейве
/// менять нельзя), переключение - меню по зажатому R, как у черепа (SkullDecorPlace); качество и моды переносятся
/// (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();
}
public static void OnMenu(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
{
EntityPlayerLocal pl = _sender?.xui?.playerUI?.entityPlayer;
if (_commandIndex == GripNormal)
{
Convert(pl, KnifeName);
}
else if (_commandIndex == GripThread)
{
Convert(pl, ThreadKnifeName);
}
}
public static void OpenMenu(PlayerMoveController _pmc, EntityPlayerLocal _pl)
{
XUiC_Radial r = _pmc.playerUI?.xui?.RadialWindow;
if (r == null)
{
return;
}
bool thread = IsThreadKnife(_pl.inventory.holdingItem);
r.Open();
r.ResetRadialEntries();
r.CreateRadialEntry(GripNormal, "NecromancersKnife", "ItemIconAtlas", "", Localization.Get("necroKnifeGripNormal"), !thread);
// 16.10: «Есть в игре пиктограмма ниток, катушки?» - набор для шитья (катушка с иглой).
r.CreateRadialEntry(GripThread, "resourceSewingKit", "ItemIconAtlas", "", Localization.Get("necroKnifeGripThread"), thread);
r.SetCommonData(UIUtils.GetButtonIconForAction(_pmc.playerInput.Reload), OnMenu,
new XUiC_Radial.RadialContextHoldingSlotIndex(_pl.inventory.holdingItemIdx), thread ? GripThread : GripNormal, false,
XUiC_Radial.RadialValidSameHoldingSlotIndex);
}
// ------------------------------------------------------------------
// Соединение.
// ------------------------------------------------------------------
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).</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);
}
}
void Update()
{
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;
}
}
/// <summary>R с ножом некроманта (любым хватом) - меню хвата. Игра ножу по R ничего не открывает.</summary>
[HarmonyPatch(typeof(PlayerMoveController), nameof(PlayerMoveController.Update))]
public static class Patch_PlayerMoveController_Update_KnifeGrip
{
public static void Postfix(PlayerMoveController __instance)
{
EntityPlayerLocal pl = __instance.entityPlayerLocal;
if (pl == null || pl.inventory == null || !CinerariumThreads.IsKnife(pl.inventory.holdingItem) || pl.IsDead())
{
return;
}
PlayerActionsLocal inp = __instance.playerInput;
if (inp == null || !(inp.Reload.WasPressed || inp.PermanentActions.Reload.WasPressed) || pl.inventory.IsHandSwitching())
{
return;
}
if (__instance.playerUI == null || __instance.playerUI.windowManager.IsModalWindowOpen())
{
return;
}
CinerariumThreads.OpenMenu(__instance, pl);
}
}
}
+83
View File
@@ -59,9 +59,18 @@ namespace NecromancerTome
}
}
/// <summary>У перевозки по нити - ящик «откуда» последнего шага (дух летит сначала к нему), иначе null.</summary>
public static Vector3i? LastFrom;
static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck)
{
_stuck = false;
LastFrom = null;
// Урна с нитями - логист (16.10: «занимается только перетаскиванием из ящиков в ящики»).
if (_urn.IsLogist)
{
return Ferry(_world, _urn);
}
Zone z = Scan(_world, _urn);
// 1. Расходники.
foreach (TileEntityCollector c in z.Collectors)
@@ -122,6 +131,80 @@ namespace NecromancerTome
return null;
}
/// <summary>Перевозка по нитям: одна стопка из ящика «откуда» в ящик «куда». По образцу - только то, что в «куда» уже
/// есть (пустой «куда» - что угодно); «куда» полон - ждём (дух не чернеет). Открытые у игрока ящики и закреплённые
/// ячейки не трогаются. Возвращает ящик «куда», LastFrom - откуда взято.</summary>
static Vector3i? Ferry(World _world, TEFeatureCinerarium _urn)
{
if (!_urn.HasThreadTo)
{
return null;
}
TEFeatureStorage dst = BoxAt(_world, _urn.ThreadTo);
if (dst == null)
{
return null;
}
bool any = dst.IsEmpty();
foreach (Vector3i f in _urn.ThreadFrom)
{
if (f == _urn.ThreadTo)
{
continue;
}
TEFeatureStorage src = BoxAt(_world, f);
if (src == null)
{
continue;
}
for (int k = 0; k < src.ItemGrid.Length; k++)
{
ItemStack s = src.ItemGrid[k];
if (s == null || s.IsEmpty() || Locked(src, k) || (!any && !Holds(dst, s.itemValue.type)) || !Fits(dst, s))
{
continue;
}
LastFrom = f;
if (_dry)
{
return _urn.ThreadTo;
}
ItemStack moving = s.Clone();
Place(moving, new[] { dst });
int moved = s.count - moving.count;
if (moved <= 0)
{
continue;
}
Take(src, k, moved);
dst.SetModified();
Debug.Log("[NecromancerTome] Cinerarium: дух перенёс " + moved + " " + s.itemValue.ItemClass?.GetItemName() + " из " + f + " в " + _urn.ThreadTo);
return _urn.ThreadTo;
}
}
return null;
}
static TEFeatureStorage BoxAt(World _world, Vector3i _p)
{
return _world.GetTileEntity(_p) is TileEntityComposite c && !c.IsUserAccessing() &&
!c.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium _) &&
c.TryGetSelfOrFeature<TEFeatureStorage>(out TEFeatureStorage s) ? s : null;
}
static bool Holds(TEFeatureStorage _t, int _type)
{
for (int i = 0; i < _t.ItemGrid.Length; i++)
{
ItemStack x = _t.ItemGrid[i];
if (x != null && !x.IsEmpty() && x.itemValue.type == _type)
{
return true;
}
}
return false;
}
static bool IsRipe(Block _b)
{
return _b != null && !(_b is BlockPlantGrowing) && _b.HasAnyFastTags(CropTag) &&
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+10
View File
@@ -14,5 +14,15 @@ namespace NecromancerTome
_bw.Write(_hasSkull);
_bw.Write(_zoneShown);
}
/// <summary>Версия 2 (16.10, нити): число, затем сами числа - координаты нитей (см. TEFeatureCinerarium.Write).</summary>
public static void WriteInts(PooledBinaryWriter _bw, int[] _values)
{
_bw.Write(_values.Length);
foreach (int v in _values)
{
_bw.Write(v);
}
}
}
}