diff --git a/HarmonySrc/SkullDecorTweak.cs b/HarmonySrc/SkullDecorTweak.cs new file mode 100644 index 0000000..34255f6 --- /dev/null +++ b/HarmonySrc/SkullDecorTweak.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using System.Globalization; +using UnityEngine; +using UnityEngine.Scripting; + +namespace NecromancerTome +{ + /// + /// Подбор посадки черепа-декорации прямо в игре (15.10.2026, по образцу necroheld): «чтобы если нужно будет + /// спозиционировать, не перезапускать игру». Правка ставится узлу skull во всех экземплярах вида (поставленные + /// блоки) и в загруженном префабе этого вида - новые блоки ставятся уже с ней. Коллайдер корня (хитбокс) + /// пересчитывается по черепу. Итог - строкой в лог, затем переносится в NecroSkullDecorSetup (Unity) и бандл + /// necroskull пересобирается при закрытой игре. + /// + public static class SkullDecorTweak + { + public const string CenterPrefab = "necroSkullDecorCenterPrefab"; + public const string BackPrefab = "necroSkullDecorBackPrefab"; + + public class State + { + public Vector3 Pos; // метры, добавка к исходному положению, в осях блока + public Vector3 Rot; // градусы, добавка к исходному повороту + public float Scale = 1f; + } + + public static readonly Dictionary States = new Dictionary + { + { CenterPrefab, new State() }, { BackPrefab, new State() } + }; + + /// Исходные значения узла skull (по экземпляру узла). + static readonly Dictionary _base = new Dictionary(); + + static string KindOf(Transform _root) + { + string n = _root.name; + if (n.StartsWith(CenterPrefab)) return CenterPrefab; + if (n.StartsWith(BackPrefab)) return BackPrefab; + return null; + } + + /// Применяет правку вида _kind ко всем найденным черепам; возвращает их число. + public static int Apply(string _kind) + { + State st = States[_kind]; + int n = 0; + foreach (Transform t in Resources.FindObjectsOfTypeAll()) + { + if (t == null || t.name != "skull" || t.parent == null || KindOf(t.parent) != _kind) + { + continue; + } + int id = t.GetInstanceID(); + if (!_base.TryGetValue(id, out var b)) + { + b = (t.localPosition, t.localRotation, t.localScale); + _base[id] = b; + } + t.localPosition = b.p + st.Pos; + t.localRotation = Quaternion.Euler(st.Rot) * b.r; + t.localScale = b.s * st.Scale; + FitCollider(t); + n++; + } + return n; + } + + /// Хитбокс корня - по габариту черепа в осях корня. + static void FitCollider(Transform _skull) + { + BoxCollider bc = _skull.parent.GetComponent(); + MeshFilter mf = _skull.GetComponent(); + if (bc == null || mf == null || mf.sharedMesh == null) + { + return; + } + Bounds mb = mf.sharedMesh.bounds; + Matrix4x4 m = Matrix4x4.TRS(_skull.localPosition, _skull.localRotation, _skull.localScale); + Bounds r = new Bounds(m.MultiplyPoint3x4(mb.center), Vector3.zero); + for (int sx = -1; sx <= 1; sx += 2) for (int sy = -1; sy <= 1; sy += 2) for (int sz = -1; sz <= 1; sz += 2) + r.Encapsulate(m.MultiplyPoint3x4(mb.center + Vector3.Scale(mb.extents, new Vector3(sx, sy, sz)))); + bc.center = r.center; + bc.size = r.size; + } + + /// Габарит черепа вида в осях блока (по первому найденному экземпляру): «влезает ли в клетку». + public static string Box(string _kind) + { + foreach (Transform t in Resources.FindObjectsOfTypeAll()) + { + if (t != null && t.name == "skull" && t.parent != null && KindOf(t.parent) == _kind) + { + BoxCollider bc = t.parent.GetComponent(); + if (bc != null) + { + Vector3 a = bc.center - bc.size / 2f, z = bc.center + bc.size / 2f; + bool inside = a.x >= -0.5001f && z.x <= 0.5001f && a.y >= -0.0001f && z.y <= 1.0001f && a.z >= -0.5001f && z.z <= 0.5001f; + return "габарит (м) от " + F(a) + " до " + F(z) + (inside ? " - в клетке" : " - ВЫЛЕЗАЕТ ИЗ КЛЕТКИ"); + } + } + } + return "черепов этого вида не найдено"; + } + + public static string F(float _v) => _v.ToString("0.###", CultureInfo.InvariantCulture); + public static string F(Vector3 _v) => F(_v.x) + " " + F(_v.y) + " " + F(_v.z); + } + + /// + /// necroskulldecor center|back - посадка черепа-декорации: + /// pos x y z (см) | add x y z (см) | rot x y z (градусы) | scale s | off | show + /// + [Preserve] + public class ConsoleCmdNecroSkullDecor : ConsoleCmdAbstract + { + public override string[] getCommands() => new[] { "necroskulldecor" }; + public override string getDescription() => + "NecromancerTome: посадка черепа-декорации - center|back, затем pos x y z (см)|add x y z (см)|rot x y z|scale s|off|show"; + + static bool Num(List _p, int _i, out float _v) + { + _v = 0f; + return _p.Count > _i && float.TryParse(_p[_i].Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out _v); + } + + static bool Vec(List _p, out Vector3 _v) + { + bool ok = Num(_p, 2, out float x) & Num(_p, 3, out float y) & Num(_p, 4, out float z); + _v = new Vector3(x, y, z); + return ok; + } + + public override void Execute(List _params, CommandSenderInfo _senderInfo) + { + string which = _params.Count > 0 ? _params[0] : ""; + string kind = which == "center" ? SkullDecorTweak.CenterPrefab : which == "back" ? SkullDecorTweak.BackPrefab : null; + if (kind == null) + { + Out(getDescription()); + return; + } + SkullDecorTweak.State st = SkullDecorTweak.States[kind]; + string cmd = _params.Count > 1 ? _params[1] : "show"; + Vector3 v; + switch (cmd) + { + case "pos" when Vec(_params, out v): + st.Pos = v / 100f; + break; + case "add" when Vec(_params, out v): + st.Pos += v / 100f; + break; + case "rot" when Vec(_params, out v): + st.Rot = v; + break; + case "scale" when Num(_params, 2, out float s) && s > 0f: + st.Scale = s; + break; + case "off": + st.Pos = Vector3.zero; + st.Rot = Vector3.zero; + st.Scale = 1f; + break; + case "show": + break; + default: + Out(getDescription()); + return; + } + int n = SkullDecorTweak.Apply(kind); + Out(which + ": черепов " + n + ", сдвиг (см) " + SkullDecorTweak.F(st.Pos * 100f) + ", поворот " + SkullDecorTweak.F(st.Rot) + + ", масштаб " + SkullDecorTweak.F(st.Scale) + "; " + SkullDecorTweak.Box(kind)); + } + + static void Out(string _s) + { + SdtdConsole.Instance.Output(_s); + Debug.Log("[NecromancerTome] necroskulldecor: " + _s); + } + } +}