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); } } }