Files
necromants-tome-7d2d-3-2/HarmonySrc/EraserPatch.cs
T
AlexCubeandClaude Opus 5.5 8541c7abdf Кровавый прилив: в ночь кровавой луны дух не выходит из тела
Нет змеи, чертогов, одержимости зомби и питомцев; кто вне тела - выброшен
в тело; «коза» без эффекта. Подмешивание Стирателей в орду отключено.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:56:52 +03:00

288 lines
13 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>
/// «Стиратель» (указание 02.10) - античит к режиму змеи: если игрок в кровавую луну стал змеёй,
/// в его орде с большой вероятностью приходят Стиратели - маленькие быстрые зомби с
/// нитроглицериновыми вздутиями. Взрываются, когда пытаются сломать блок и когда их разрывает
/// змея; взрыв ломает блоки, ранит тело игрока и соседних зомби, змею не ранит (она бессмертна),
/// а отбрасывает.
///
/// Сущность - zombieEraser (entityclasses.xml): имя НЕ на necro, иначе IsNecroEntity сочтёт её
/// своей и змея перестанет видеть в ней врага. Параметры взрыва - блок Explosion там же,
/// подрыв - как у ванильного EntityZombieCop: ExplosionServer + SetDead.
/// </summary>
public static class EraserPatch
{
public const string EntityClassName = "zombieEraser";
/// <summary>Доля Стирателей в орде игрока-змеи («с большой вероятностью»).</summary>
public const float HordeChance = 0.5f;
/// <summary>Отброс змеи: дальше этого от взрыва змею не задевает.</summary>
public const float KnockRange = 8f;
static int _classId = -1;
public static bool IsEraser(Entity _e)
{
if (_e == null)
{
return false;
}
if (_classId == -1)
{
_classId = EntityClass.FromString(EntityClassName);
}
return _e.entityClass == _classId;
}
static System.Collections.IEnumerator BurstNextFrame(EntityAlive _e)
{
yield return null;
if (_e != null && _e.world != null && _e.world.GetEntity(_e.entityId) != null)
{
NecroBurst.Burst(_e, _e);
}
}
/// <summary>Подрыв на сервере. Возвращает false, если подрывать нечего или не здесь.</summary>
public static bool Explode(EntityAlive _e)
{
if (_e == null || _e.IsDead() || _e.world == null || _e.world.IsRemote())
{
return false;
}
Vector3 pos = _e.GetPosition();
_e.NotifySleeperDeath();
_e.SetModelLayer(2);
GameManager.Instance.ExplosionServer(pos, World.worldToBlockPos(pos), _e.transform.rotation,
EntityClass.list[_e.entityClass].explosionData, _e.entityId, 0f, false);
_e.timeStayAfterDeath = 0;
_e.SetDead();
SnakeControl.Knockback(pos, KnockRange);
// Тела не остаётся (указание 02.10: «после взрыва остаются трупы туристов»): чёрный
// разрыв, как от змеи. Не сразу, а в следующем кадре: подрыв идёт изнутри хода ИИ
// (Attack из задачи), и удаление посреди него роняло EntityAlive.updateTasks (NRE, тест 02.10).
GameManager.Instance.StartCoroutine(BurstNextFrame(_e));
Debug.Log("[NecromancerTome] EraserPatch: Стиратель " + _e.entityId + " взорвался в " + pos);
return true;
}
}
/// <summary>Стиратель упёрся и начинает ломать блок - вместо удара взрыв.</summary>
[HarmonyPatch(typeof(EAIBreakBlock), nameof(EAIBreakBlock.Start))]
public static class Patch_EAIBreakBlock_Start_Eraser
{
public static bool Prefix(EAIBreakBlock __instance)
{
return !(EraserPatch.IsEraser(__instance.theEntity) && EraserPatch.Explode(__instance.theEntity));
}
}
/// <summary>Любая атака Стирателя - по блоку (BreakBlock, DestroyArea) или по цели
/// (ApproachAndAttackTarget) - идёт через EntityAlive.Attack: взрыв вместо удара (тест 02.10:
/// «взрываются не всегда, хотя атакуют»).</summary>
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.Attack))]
public static class Patch_EntityAlive_Attack_Eraser
{
public static bool Prefix(EntityAlive __instance, ref bool __result)
{
if (EraserPatch.IsEraser(__instance) && EraserPatch.Explode(__instance))
{
__result = false;
return false;
}
return true;
}
}
// ОТКЛЮЧЕНО 10.10 (новая концепция кровавой луны - «кровавый прилив», SnakeMode.IsBloodTide): в эту ночь
// дух не выходит из тела, а кто был змеёй - выброшен в тело. Игрока-змеи в орде больше не бывает, и
// античит не нужен. Сущность и её вид (выше и ниже) оставлены - вернуть подмешивание можно, сняв комментарий.
/*
/// <summary>
/// Подмешивание в орду. Ванильный образец - AIDirectorBloodMoonParty.SpawnZombie: игрок в
/// транспорте получает в орду стервятников с шансом 0.5. Класс зомби там берётся из
/// EntityGroups.GetRandomEntityFromGroupMaxTier; на время SpawnZombie запоминаем цель и, если
/// это игрок в режиме змеи, с шансом подменяем результат на Стирателя.
/// Режим змеи - состояние локального игрока (SnakeControl.Controlling), поэтому работает в
/// одиночной игре и у хоста; змея подключённого клиента орду не меняет.
/// </summary>
[HarmonyPatch(typeof(AIDirectorBloodMoonParty), "SpawnZombie")]
public static class Patch_BloodMoonParty_SpawnZombie_Eraser
{
[System.ThreadStatic] public static EntityPlayer Target;
public static void Prefix(EntityPlayer _target)
{
Target = _target;
}
public static void Finalizer()
{
Target = null;
}
}
[HarmonyPatch(typeof(EntityGroups), nameof(EntityGroups.GetRandomEntityFromGroupMaxTier))]
public static class Patch_EntityGroups_GetRandom_Eraser
{
public static void Postfix(ref int __result)
{
EntityPlayer target = Patch_BloodMoonParty_SpawnZombie_Eraser.Target;
if (target == null || __result == -1 || !SnakeControl.Controlling ||
!(target is EntityPlayerLocal) || Random.value >= EraserPatch.HordeChance)
{
return;
}
int id = EntityClass.FromString(EraserPatch.EntityClassName);
if (id != -1 && EntityClass.list.ContainsKey(id))
{
__result = id;
}
}
}
*/
/// <summary>
/// Вид Стирателя (02.10): тело - ванильная модель толстого гавайца (скелет, физика, теги,
/// анимации игры), а раскраска своя. Игра в EModelBase.createModel сама копирует материал тела
/// (ветка MatColor/AltMats); после неё подставляем в материал LOD-рендереров свои текстуры
/// (Resources/Eraser/*.png, нарисованы скриптом по развёртке гавайца): _Albedo, _Normal и _RMOE
/// (шероховатость, металл, затенение, в альфе - маска свечения), свечение - _EmissiveColor.
/// Глаза - отдельные рендереры *Eye* - прячем: на их месте в текстуре чёрные провалы.
/// Ступни босые - пальцы и ногти нарисованы в текстуре (слоновья нога масштабом костей не
/// получилась, 02.10).
/// </summary>
[HarmonyPatch(typeof(EModelBase), nameof(EModelBase.createModel))]
public static class Patch_EModelBase_createModel_EraserLook
{
public static readonly Color Emission = new Color(1f, 0.1f, 0.65f) * 2.5f;
static Texture2D _albedo, _normal, _rmoe;
/// <summary>Своя раскраска на материал тела (только у материалов с _Albedo - тело и куски).</summary>
public static void Paint(Renderer _r)
{
// Сначала смотрим общий материал: обращение к .material создаёт копию, и делать её
// у глаз, огня и прочих рендереров без _Albedo незачем. Если копия уже наша - .material
// вернёт её же, повторной копии не будет. Брошенные копии убирает сама игра
// (GameManager: Resources.UnloadUnusedAssets по таймеру).
Material sm = _r.sharedMaterial;
if (sm == null || !sm.HasProperty("_Albedo") || sm.GetTexture("_Albedo") == _albedo && sm.GetColor("_EmissiveColor") == Emission)
{
return;
}
Material m = _r.material;
if (_albedo != null) m.SetTexture("_Albedo", _albedo);
if (_normal != null) m.SetTexture("_Normal", _normal);
if (_rmoe != null) m.SetTexture("_RMOE", _rmoe);
m.SetColor("_EmissiveColor", Emission);
}
static bool _loaded;
static Texture2D Load(string _file, bool _linear)
{
string path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(typeof(EraserPatch).Assembly.Location), "Resources", "Eraser", _file);
if (!System.IO.File.Exists(path))
{
Debug.LogWarning("[NecromancerTome] EraserLook: нет файла " + path);
return null;
}
Texture2D t = new Texture2D(2, 2, TextureFormat.RGBA32, true, _linear);
if (!ImageConversion.LoadImage(t, System.IO.File.ReadAllBytes(path)))
{
return null;
}
t.name = "Eraser_" + _file;
t.Apply(true, true);
return t;
}
public static void Postfix(EModelBase __instance)
{
Entity e = __instance.entity;
if (!EraserPatch.IsEraser(e))
{
return;
}
if (!_loaded)
{
_loaded = true;
_albedo = Load("eraser_albedo.png", false);
_normal = Load("eraser_normal.png", true);
_rmoe = Load("eraser_rmoe.png", true);
}
foreach (SkinnedMeshRenderer r in __instance.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
if (r.name.IndexOf("Eye", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
r.enabled = false;
continue;
}
if (!r.CompareTag("LOD"))
{
continue;
}
Paint(r);
}
Debug.Log("[NecromancerTome] EraserLook: вид Стирателя на " + e.entityId);
}
}
/// <summary>
/// Отрубленная конечность (тест 02.10: «если у трупа отрубить руку, он становится туристом»):
/// игра при расчленении подменяет материалы тела и создаёт отлетающий кусок с ванильным
/// материалом гавайца. После RemoveLimb заново красим тело и новые куски.
/// </summary>
[HarmonyPatch(typeof(AvatarZombieController), nameof(AvatarZombieController.RemoveLimb))]
public static class Patch_AvatarZombieController_RemoveLimb_EraserLook
{
public static void Prefix(out int __state)
{
__state = DismembermentManager.Instance?.parts.Count ?? 0;
}
public static void Postfix(AvatarZombieController __instance, int __state)
{
EntityAlive e = Traverse.Create(__instance).Field("entity").GetValue<EntityAlive>();
if (!EraserPatch.IsEraser(e) || e.emodel == null)
{
return;
}
foreach (Renderer r in e.emodel.GetComponentsInChildren<Renderer>(true))
{
Patch_EModelBase_createModel_EraserLook.Paint(r);
}
var parts = DismembermentManager.Instance?.parts;
for (int i = __state; parts != null && i < parts.Count; i++)
{
foreach (Transform t in new[] { parts[i].prefabT, parts[i].detachT })
{
if (t == null) continue;
foreach (Renderer r in t.GetComponentsInChildren<Renderer>(true))
{
Patch_EModelBase_createModel_EraserLook.Paint(r);
}
}
}
}
}
/// <summary>Стиратель убит чем угодно (игроком, взрывом соседа, под бафом жертвы) - чёрный
/// разрыв, трупа не остаётся (указание 02.10). Тот же путь, что у змеи: постфикс ClientKill.</summary>
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.ClientKill))]
public static class Patch_EntityAlive_ClientKill_EraserBurst
{
public static void Postfix(EntityAlive __instance)
{
if (EraserPatch.IsEraser(__instance) && __instance.IsDead() && __instance.world != null &&
__instance.world.GetEntity(__instance.entityId) != null)
{
NecroBurst.Burst(__instance, __instance.entityThatKilledMe != null ? (Entity)__instance.entityThatKilledMe : __instance);
}
}
}
}