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