using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Своя модель Могильной крысы (08.10.2026). Сущность necroRatSpirit по-прежнему стоит на префабе /// лютоволка (Prefab в entityclasses.xml): его кости, аниматор, рэгдолл и коллайдеры остаются /// ванильными. Меняется только то, что видно: в рендерер DireWolf кладём нашу сетку из бандла /// Resources/necrorat, шерсть (DireWolfHair) прячем. /// /// Сетка сделана с нуля в Blender, веса - по костям волка; привязку (bindposes) и номера костей /// посчитал редактор Unity по самому префабу волка (NecroRatSkin.cs в DonorUnityProject), /// поэтому smr.bones менять не нужно - порядок костей тот же. Проверка ниже на число костей /// ловит случай, если игра однажды поменяет скелет волка: тогда остаётся волчья шкура, а не /// изломанная крыса. /// /// Шейдер - Standard (тот же, что у тела зомбопса). Свечение глаз - _EmissionMap и /// _EmissionColor в материале necroRat_eye, фиолетовое, как у PetEyeGlow. /// public static class RatModel { public const string ClassName = "necroRatSpirit"; const string BundleFile = "Resources/necrorat"; static readonly string[] MatNames = { "necroRat_body", "necroRat_jaw", "necroRat_eye", "necroRat_incisor", "necroRat_claw" }; static bool _tried; static Mesh _mesh; static Material[] _mats; public static bool Is(Entity _e) { return _e != null && EntityClass.list.TryGetValue(_e.entityClass, out EntityClass cls) && cls != null && cls.entityClassName == ClassName; } static bool Load() { if (_tried) { return _mesh != null; } _tried = true; if (ModEntry.Instance == null) { return false; } string path = System.IO.Path.Combine(ModEntry.Instance.Path, BundleFile); AssetBundle b = System.IO.File.Exists(path) ? AssetBundle.LoadFromFile(path) : null; if (b == null) { Debug.LogWarning("[NecromancerTome] RatModel: бандл не загружен: " + path); return false; } _mesh = b.LoadAsset("necroRatMesh"); _mats = new Material[MatNames.Length]; Shader std = Shader.Find("Standard"); for (int i = 0; i < MatNames.Length; i++) { _mats[i] = b.LoadAsset(MatNames[i]); if (_mats[i] != null && std != null && _mats[i].shader != std) { _mats[i].shader = std; } } Debug.Log("[NecromancerTome] RatModel: сетка " + (_mesh != null ? _mesh.vertexCount + " вершин, костей " + _mesh.bindposes.Length : "НЕТ") + ", шейдер " + (std != null ? std.name : "нет Standard")); return _mesh != null; } /// Прозрачность крысы во время прохода сквозь постройки (указание 09.10: «пока зажата ПКМ, делать /// крысу полупрозрачной»). public const float GhostAlpha = 0.35f; static Material[] _ghostMats; /// Полупрозрачные копии материалов (Standard в режиме Fade) - один раз на игру; глаза /// (necroRat_eye) остаются непрозрачными и светятся. Переключение - подмена sharedMaterials у рендерера /// DireWolf, без пересоздания материалов. public static void SetGhost(EntityAlive _rat, bool _ghost) { if (_rat?.emodel == null || _mats == null) { return; } if (_ghost && _ghostMats == null) { _ghostMats = new Material[_mats.Length]; for (int i = 0; i < _mats.Length; i++) { Material src = _mats[i]; if (src == null || MatNames[i] == "necroRat_eye") { _ghostMats[i] = src; continue; } Material m = new Material(src) { name = src.name + "_ghost" }; m.SetFloat("_Mode", 2f); m.SetOverrideTag("RenderType", "Transparent"); m.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); m.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); m.SetInt("_ZWrite", 0); m.DisableKeyword("_ALPHATEST_ON"); m.EnableKeyword("_ALPHABLEND_ON"); m.DisableKeyword("_ALPHAPREMULTIPLY_ON"); m.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent; if (m.HasProperty("_Color")) { Color c = m.color; c.a = GhostAlpha; m.color = c; } _ghostMats[i] = m; } } foreach (SkinnedMeshRenderer r in _rat.emodel.GetComponentsInChildren(true)) { if (r.name == "DireWolf" && r.sharedMesh == _mesh) { r.sharedMaterials = _ghost ? _ghostMats : _mats; } } } public static void Apply(EModelBase _model) { if (!Load()) { return; } bool done = false; foreach (SkinnedMeshRenderer r in _model.GetComponentsInChildren(true)) { if (r.name == "DireWolf") { if (r.bones == null || r.bones.Length != _mesh.bindposes.Length) { Debug.LogWarning("[NecromancerTome] RatModel: у волка " + (r.bones?.Length ?? 0) + " костей, у крысы " + _mesh.bindposes.Length + " - модель не поставлена"); return; } r.sharedMesh = _mesh; r.sharedMaterials = _mats; r.localBounds = _mesh.bounds; done = true; } else if (r.name == "DireWolfHair") { // Не enabled = false: при появлении питомца (renderFade) игра через avatarController.SetVisible // включает все рендереры заново, и шерсть ложилась поверх крысы (тест 08.10: «модель крысы // применилась, но почему-то в шерсти»). forceRenderingOff игра не трогает. r.enabled = false; r.forceRenderingOff = true; } } Debug.Log("[NecromancerTome] RatModel: " + (done ? "модель крысы поставлена на " : "рендерер DireWolf не найден у ") + _model.entity?.entityId); } } /// Крыса бегала, не касаясь земли (тест 08.10). Entity.SetCCScale уменьшает высоту капсулы движения /// в SizeScale раз, а радиус - только увеличивает (FastMax(scale, 1)). У крысы (0.28 от волка) высота /// стала меньше двух радиусов, капсула выродилась в шар волчьего радиуса, и его низ ушёл под ноги - /// контроллер поднимал крысу над землёй. Радиус ужимаем так же, как высоту. [HarmonyPatch(typeof(Entity), nameof(Entity.SetScale))] public static class Patch_Entity_SetScale_RatModel { public static void Postfix(Entity __instance, float scale) { CharacterControllerAbstract cc = __instance.m_characterController; if (cc == null || scale >= 1f || !RatModel.Is(__instance)) { return; } float h = cc.GetHeight(); float r = Mathf.Min(cc.GetRadius() * scale, h * 0.5f); // Центр - ровно половина высоты, иначе низ капсулы уходит под ноги. Замер 08.10 (RatProbe): центр 0.16 // при высоте 0.48 - низ на 8 см ниже точки сущности, земля ровно на -0.084, крыса висит. Волку игра // центр выравнивает сама (SetCCScale: высота 1.89-2.2 -> 1.89, центр = половина), крысе - нет. Vector3 c = cc.GetCenter(); c.y = h * 0.5f; cc.SetSize(c, h, r); Debug.Log("[NecromancerTome] RatModel: капсула крысы " + __instance.entityId + " - высота " + h.ToString("F2") + ", радиус " + r.ToString("F2") + ", центр " + c.y.ToString("F2")); } } /// ЗАМЕР (08.10, временный): крыса всё ещё «не достаёт до земли» после правки капсулы - раз в 3 с пишем, /// где сущность, где её видимая модель, физическое тело, низ костей и земля под ней. По логу видно, что /// именно приподнято. Убрать после починки. [HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.LateUpdate))] public static class Patch_EntityAlive_LateUpdate_RatProbe { static int _ratClass = int.MinValue; static float _next; public static void Postfix(EntityAlive __instance) { if (_ratClass == int.MinValue) { _ratClass = EntityClass.FromString(RatModel.ClassName); } if (__instance.entityClass != _ratClass || Time.time < _next || __instance.IsDead()) { return; } _next = Time.time + 3f; Vector3 p = __instance.position - Origin.position; string ground = Physics.Raycast(p + Vector3.up, Vector3.down, out RaycastHit hit, 4f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore) ? (hit.point.y - p.y).ToString("F3") : "нет"; float low = float.MaxValue, rmin = float.MaxValue; string lowName = ""; foreach (SkinnedMeshRenderer r in __instance.GetComponentsInChildren(false)) { if (r.forceRenderingOff || r.bones == null) { continue; } rmin = Mathf.Min(rmin, r.bounds.min.y - p.y); foreach (Transform b in r.bones) { if (b != null && b.position.y - p.y < low) { low = b.position.y - p.y; lowName = b.name; } } } CharacterControllerAbstract cc = __instance.m_characterController; Transform mt = __instance.emodel != null ? __instance.emodel.GetModelTransform() : null; Debug.Log("[NecromancerTome] RatProbe " + __instance.entityId + ": земля " + ground + ", transform " + (__instance.transform.position.y - p.y).ToString("F3") + ", model " + (mt != null ? (mt.position.y - p.y).ToString("F3") + " scale " + mt.lossyScale.y.ToString("F2") : "нет") + ", низ кости " + low.ToString("F3") + " (" + lowName + "), низ сетки " + rmin.ToString("F3") + ", physicsRB " + (__instance.physicsRBT != null ? (__instance.physicsRBT.position.y - __instance.physicsBasePos.y - p.y).ToString("F3") : "нет") + ", onGround " + __instance.onGround + (cc != null ? ", cc c" + cc.GetCenter().y.ToString("F2") + " h" + cc.GetHeight().ToString("F2") + " r" + cc.GetRadius().ToString("F2") : "")); } } [HarmonyPatch(typeof(EModelBase), nameof(EModelBase.createModel))] public static class Patch_EModelBase_createModel_RatModel { public static void Postfix(EModelBase __instance) { if (RatModel.Is(__instance.entity)) { RatModel.Apply(__instance); } } } }