From 7e93e19c5ff1dac14a1e4e7465b24a7aa58b7940 Mon Sep 17 00:00:00 2001 From: AlexCube Date: Fri, 16 Oct 2026 20:03:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D1=80=D0=BD=D0=B0:=20=D0=B4=D1=83=D1=85?= =?UTF-8?q?=20=D0=BB=D0=B5=D1=82=D0=B8=D1=82=20=D0=BA=20=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=83=20=D0=BB=D0=B8=D1=86=D0=BE=D0=BC,=20=D0=BD=D0=B0=D0=B4?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=81=D1=82=D0=BE=D0=BC=20=D0=B6=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D1=83=D0=BB=D0=B8=D1=80=D1=83=D0=B5=D1=82=20?= =?UTF-8?q?=D1=80=D1=83=D0=BA=D0=B0=D0=BC=D0=B8,=20=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=BE=20-=20=D0=B2=20=D1=81=D0=B5=D1=80=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B6=D0=B5=D1=81=D1=82=D0=B0,=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=20=D0=BB=D0=B8=D1=86=D0=BE=D0=BC=20=D0=BA=20?= =?UTF-8?q?=D1=83=D1=80=D0=BD=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- HarmonySrc/Cinerarium.cs | 39 ++++++++++++---- HarmonySrc/CinerariumGenie.cs | 84 +++++++++++++++++++++++++++-------- HarmonySrc/CinerariumWork.cs | 59 +++++++++++++++++++++--- 3 files changed, 150 insertions(+), 32 deletions(-) diff --git a/HarmonySrc/Cinerarium.cs b/HarmonySrc/Cinerarium.cs index ab2cf98..1a2f7af 100644 --- a/HarmonySrc/Cinerarium.cs +++ b/HarmonySrc/Cinerarium.cs @@ -153,29 +153,52 @@ namespace NecromancerTome float _nextWork; public bool Stuck; + /// Дело найдено пробным шагом, дух полетел; выполнить в _actAt (16.10: «делал действие не сразу»). + bool _pending; + float _actAt; - /// Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере. + /// Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере. + /// Сначала пробный шаг (ничего не меняет) - куда лететь; само дело - когда дух долетел и машет руками. Если за это + /// время что-то поменялось, настоящий шаг сделает то, что нужно на тот момент. void Work(World _world) { - if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer || Time.time < _nextWork) + if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer) + { + _pending = false; + return; + } + CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null; + if (_pending) + { + if (Time.time < _actAt) + { + return; + } + _pending = false; + CinerariumWork.Step(_world, this, out bool _, false); + _nextWork = Time.time + CinerariumWork.StepSeconds; + return; + } + if (Time.time < _nextWork) { return; } _nextWork = Time.time + CinerariumWork.StepSeconds; - Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck); + Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck, true); if (stuck != Stuck) { Stuck = stuck; Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? " почернел - готовое класть некуда" : " ожил")); } - CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null; if (motion != null) { motion.SetStuck(Stuck && at == null); - if (at.HasValue) - { - motion.Visit(World.blockToTransformPos(at.Value) - Origin.position); - } + } + if (at.HasValue) + { + _pending = true; + _actAt = Time.time + (motion != null ? CinerariumGenieMotion.ActDelay : 0f); + motion?.Visit(World.blockToTransformPos(at.Value) - Origin.position); } } diff --git a/HarmonySrc/CinerariumGenie.cs b/HarmonySrc/CinerariumGenie.cs index 9f3c447..e7cb347 100644 --- a/HarmonySrc/CinerariumGenie.cs +++ b/HarmonySrc/CinerariumGenie.cs @@ -226,10 +226,20 @@ namespace NecromancerTome { float _yaw; float _phase; - /// Полёт к месту работы и обратно: точка в координатах модели урны и сколько осталось. + /// Полёт к месту работы и обратно (16.10: «возвращается задом», «делал действие не сразу, а хоть руками + /// пошевелив»): туда - лицом к месту, над местом - жест руками (дело делается в его середине, см. ActDelay), + /// обратно - развернувшись лицом к урне. Точка - в координатах модели урны; _t - время с начала полёта, -1 - дома. Vector3 _visit; - float _visitLeft; - public const float VisitSeconds = 2.2f; + float _t = -1f; + public static float OutSeconds = 1.3f; + public static float WorkSeconds = 1.6f; + public static float BackSeconds = 1.3f; + /// Через сколько после начала полёта дело выполняется - в середине жеста. + public static float ActDelay => OutSeconds + WorkSeconds * 0.5f; + /// Жест: плечи вперёд до GestureFwd градусов, локти ходят на ±GestureWave вразнобой. + public static float GestureFwd = 55f; + public static float GestureWave = 30f; + bool _gesturing; /// Почернел и замер (готовое класть некуда, решение 06.10). bool _stuck; /// Кости рук (плечо, предплечье, кисть по сторонам) и их поворот в позе покоя. @@ -240,6 +250,11 @@ namespace NecromancerTome /// Опустить руки из позы покоя: направление плеча и предплечья задаётся в осях модели (вперёд - +Z), /// поворот - от текущего направления кости к нужному, так что оси самих костей значения не имеют. public void Pose() + { + Pose(CinerariumGenie.ArmFwd, CinerariumGenie.Elbow, CinerariumGenie.Elbow); + } + + void Pose(float _fwd, float _elbowL, float _elbowR) { if (_arm == null) { @@ -283,9 +298,9 @@ namespace NecromancerTome continue; } float sign = Mathf.Sign(transform.InverseTransformPoint(up.position).x); - float o = CinerariumGenie.ArmOut * Mathf.Deg2Rad, f = CinerariumGenie.ArmFwd * Mathf.Deg2Rad; + float o = CinerariumGenie.ArmOut * Mathf.Deg2Rad, f = _fwd * Mathf.Deg2Rad; Vector3 upDir = new Vector3(sign * Mathf.Sin(o), -Mathf.Cos(o) * Mathf.Cos(f), Mathf.Sin(f)).normalized; - Vector3 foreDir = Vector3.RotateTowards(upDir, Vector3.forward, CinerariumGenie.Elbow * Mathf.Deg2Rad, 0f); + Vector3 foreDir = Vector3.RotateTowards(upDir, Vector3.forward, (side == 0 ? _elbowL : _elbowR) * Mathf.Deg2Rad, 0f); up.rotation = Quaternion.FromToRotation(fore.position - up.position, transform.TransformDirection(upDir)) * up.rotation; fore.rotation = Quaternion.FromToRotation(hand.position - fore.position, transform.TransformDirection(foreDir)) * fore.rotation; } @@ -302,7 +317,7 @@ namespace NecromancerTome return; } _visit = transform.parent.InverseTransformPoint(_worldUnity); - _visitLeft = VisitSeconds; + _t = 0f; } public void SetStuck(bool _on) @@ -359,21 +374,54 @@ namespace NecromancerTome } 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 (_visitLeft > 0f) + if (_t >= 0f) { - // Туда и обратно по синусу: на полпути - над местом работы, лицом к нему. - _visitLeft -= Time.deltaTime; - float k = Mathf.Sin(Mathf.Clamp01(1f - _visitLeft / VisitSeconds) * Mathf.PI); - Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + 0.6f, 0f) + Quaternion.Euler(0f, _yaw, 0f) * new Vector3(0f, 0f, CinerariumGenie.Forward); - transform.localPosition = Vector3.Lerp(home, over, k); - Vector3 d = _visit - home; - if (d.x * d.x + d.z * d.z > 0.01f) + _t += Time.deltaTime; + if (_t < OutSeconds + WorkSeconds + BackSeconds) { - yawTarget = Mathf.LerpAngle(_yaw, Mathf.Atan2(d.x, d.z) * Mathf.Rad2Deg, Time.deltaTime * 4f); + Vector3 over = _visit + new Vector3(0f, CinerariumGenie.OffsetY + 0.6f, 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; + float k, face, w = 0f; + if (_t < OutSeconds) + { + k = Mathf.SmoothStep(0f, 1f, _t / OutSeconds); + face = toPlace; + } + else if (_t < OutSeconds + WorkSeconds) + { + k = 1f; + face = toPlace; + w = Mathf.Sin((_t - OutSeconds) / WorkSeconds * Mathf.PI); + } + 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); + 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; } - _yaw = yawTarget % 360f; - transform.localRotation = Quaternion.Euler(0f, _yaw, 0f); - return; } _yaw = yawTarget % 360f; float bob = CinerariumGenie.BobPeriod > 0.01f diff --git a/HarmonySrc/CinerariumWork.cs b/HarmonySrc/CinerariumWork.cs index 2fab8db..d659e4c 100644 --- a/HarmonySrc/CinerariumWork.cs +++ b/HarmonySrc/CinerariumWork.cs @@ -41,8 +41,25 @@ namespace NecromancerTome static readonly HashSet _seen = new HashSet(); - /// Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда. - public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck) + /// Пробный шаг: найти дело и место, ничего не меняя. + static bool _dry; + + /// Одно дело. Возвращает, где дух работал (для полёта модели), или null; _stuck - есть что забрать, но некуда. + /// _dryRun - только узнать, где будет дело (дух летит туда заранее), ничего не трогая. + public static Vector3i? Step(World _world, TEFeatureCinerarium _urn, out bool _stuck, bool _dryRun) + { + _dry = _dryRun; + try + { + return DoStep(_world, _urn, out _stuck); + } + finally + { + _dry = false; + } + } + + static Vector3i? DoStep(World _world, TEFeatureCinerarium _urn, out bool _stuck) { _stuck = false; Zone z = Scan(_world, _urn); @@ -51,7 +68,10 @@ namespace NecromancerTome { if (Refill(c, c.FuelSlots, c.GetFuelTypes(), false, z) || Refill(c, c.CatalystSlots, c.GetCatalystTypes(), true, z)) { - c.SetModified(); + if (!_dry) + { + c.SetModified(); + } return z.Where[c]; } } @@ -62,8 +82,11 @@ namespace NecromancerTome ItemStack[] items = c.Items; if (Unload(items, z, ref blocked)) { - c.Items = items; - c.SetModified(); + if (!_dry) + { + c.Items = items; + c.SetModified(); + } return z.Where[c]; } } @@ -72,7 +95,10 @@ namespace NecromancerTome ItemStack[] output = w.Output; if (Unload(output, z, ref blocked)) { - w.Output = output; + if (!_dry) + { + w.Output = output; + } return z.Where[w]; } } @@ -151,6 +177,10 @@ namespace NecromancerTome _blocked = true; return false; } + if (_dry) + { + return true; + } _world.SetBlockRPC(_p, BlockValue.Air); if (!replant.isair) { @@ -232,6 +262,10 @@ namespace NecromancerTome { continue; } + if (_dry) + { + return p; + } Take(pick.src, pick.slot, 1); _world.SetBlockRPC(p, bv); Debug.Log("[NecromancerTome] Cinerarium: дух посадил " + bv.Block.GetBlockName() + " в " + p); @@ -351,6 +385,10 @@ namespace NecromancerTome { continue; } + if (_dry) + { + return true; + } if (want == null) { _slots[i] = new ItemStack(s.itemValue.Clone(), take); @@ -382,6 +420,15 @@ namespace NecromancerTome { continue; } + if (_dry) + { + if (HasRoom(s, _z)) + { + return true; + } + _blocked = true; + continue; + } int before = s.count; // Сначала ящики зоны (к таким же, потом в пустое), они полны - тогда сумка урны (решение 06.10). Place(s, _z.Boxes);