Нити цинерария: нож «хватом для нитей» (R), нити урна-ящик как у стриппера, урна-логист перевозит по образцу, зелёные глазницы
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fad57a3756
commit
d8e7c8c66a
+13
-1
File diff suppressed because one or more lines are too long
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user