- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
251 lines
11 KiB
C#
251 lines
11 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|
||
}
|