Files
necromants-tome-7d2d-3-2/HarmonySrc/RatModel.cs
T
AlexCubeandClaude Opus 5.5 31aa99860c Одержимость питомцев: роли медведя, волка, собаки и крысы; ноша стервятника; череп при обыске
- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы);
  E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь
  заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная.
- Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса.
- Зомбособака: нюх - все существа рядом на компасе и карте.
- Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам.
- Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки
  (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва).
- Череп (останки): 2% при обычном обыске красного трупа вместо браслета.
- Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках.
- Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:58:40 +03:00

251 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Своя модель Могильной крысы (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.
/// </summary>
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<Mesh>("necroRatMesh");
_mats = new Material[MatNames.Length];
Shader std = Shader.Find("Standard");
for (int i = 0; i < MatNames.Length; i++)
{
_mats[i] = b.LoadAsset<Material>(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;
}
/// <summary>Прозрачность крысы во время прохода сквозь постройки (указание 09.10: «пока зажата ПКМ, делать
/// крысу полупрозрачной»).</summary>
public const float GhostAlpha = 0.35f;
static Material[] _ghostMats;
/// <summary>Полупрозрачные копии материалов (Standard в режиме Fade) - один раз на игру; глаза
/// (necroRat_eye) остаются непрозрачными и светятся. Переключение - подмена sharedMaterials у рендерера
/// DireWolf, без пересоздания материалов.</summary>
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<SkinnedMeshRenderer>(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<SkinnedMeshRenderer>(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);
}
}
/// <summary>Крыса бегала, не касаясь земли (тест 08.10). Entity.SetCCScale уменьшает высоту капсулы движения
/// в SizeScale раз, а радиус - только увеличивает (FastMax(scale, 1)). У крысы (0.28 от волка) высота
/// стала меньше двух радиусов, капсула выродилась в шар волчьего радиуса, и его низ ушёл под ноги -
/// контроллер поднимал крысу над землёй. Радиус ужимаем так же, как высоту.</summary>
[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"));
}
}
/// <summary>ЗАМЕР (08.10, временный): крыса всё ещё «не достаёт до земли» после правки капсулы - раз в 3 с пишем,
/// где сущность, где её видимая модель, физическое тело, низ костей и земля под ней. По логу видно, что
/// именно приподнято. Убрать после починки.</summary>
[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<SkinnedMeshRenderer>(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);
}
}
}
}