using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// «Стиратель» (указание 02.10) - античит к режиму змеи: если игрок в кровавую луну стал змеёй, /// в его орде с большой вероятностью приходят Стиратели - маленькие быстрые зомби с /// нитроглицериновыми вздутиями. Взрываются, когда пытаются сломать блок и когда их разрывает /// змея; взрыв ломает блоки, ранит тело игрока и соседних зомби, змею не ранит (она бессмертна), /// а отбрасывает. /// /// Сущность - zombieEraser (entityclasses.xml): имя НЕ на necro, иначе IsNecroEntity сочтёт её /// своей и змея перестанет видеть в ней врага. Параметры взрыва - блок Explosion там же, /// подрыв - как у ванильного EntityZombieCop: ExplosionServer + SetDead. /// public static class EraserPatch { public const string EntityClassName = "zombieEraser"; /// Доля Стирателей в орде игрока-змеи («с большой вероятностью»). public const float HordeChance = 0.5f; /// Отброс змеи: дальше этого от взрыва змею не задевает. 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); } } /// Подрыв на сервере. Возвращает false, если подрывать нечего или не здесь. 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; } } /// Стиратель упёрся и начинает ломать блок - вместо удара взрыв. [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)); } } /// Любая атака Стирателя - по блоку (BreakBlock, DestroyArea) или по цели /// (ApproachAndAttackTarget) - идёт через EntityAlive.Attack: взрыв вместо удара (тест 02.10: /// «взрываются не всегда, хотя атакуют»). [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; } } /// /// Подмешивание в орду. Ванильный образец - AIDirectorBloodMoonParty.SpawnZombie: игрок в /// транспорте получает в орду стервятников с шансом 0.5. Класс зомби там берётся из /// EntityGroups.GetRandomEntityFromGroupMaxTier; на время SpawnZombie запоминаем цель и, если /// это игрок в режиме змеи, с шансом подменяем результат на Стирателя. /// Режим змеи - состояние локального игрока (SnakeControl.Controlling), поэтому работает в /// одиночной игре и у хоста; змея подключённого клиента орду не меняет. /// [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; } } } /// /// Вид Стирателя (02.10): тело - ванильная модель толстого гавайца (скелет, физика, теги, /// анимации игры), а раскраска своя. Игра в EModelBase.createModel сама копирует материал тела /// (ветка MatColor/AltMats); после неё подставляем в материал LOD-рендереров свои текстуры /// (Resources/Eraser/*.png, нарисованы скриптом по развёртке гавайца): _Albedo, _Normal и _RMOE /// (шероховатость, металл, затенение, в альфе - маска свечения), свечение - _EmissiveColor. /// Глаза - отдельные рендереры *Eye* - прячем: на их месте в текстуре чёрные провалы. /// Ступни босые - пальцы и ногти нарисованы в текстуре (слоновья нога масштабом костей не /// получилась, 02.10). /// [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; /// Своя раскраска на материал тела (только у материалов с _Albedo - тело и куски). 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(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); } } /// /// Отрубленная конечность (тест 02.10: «если у трупа отрубить руку, он становится туристом»): /// игра при расчленении подменяет материалы тела и создаёт отлетающий кусок с ванильным /// материалом гавайца. После RemoveLimb заново красим тело и новые куски. /// [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(); if (!EraserPatch.IsEraser(e) || e.emodel == null) { return; } foreach (Renderer r in e.emodel.GetComponentsInChildren(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(true)) { Patch_EModelBase_createModel_EraserLook.Paint(r); } } } } } /// Стиратель убит чем угодно (игроком, взрывом соседа, под бафом жертвы) - чёрный /// разрыв, трупа не остаётся (указание 02.10). Тот же путь, что у змеи: постфикс ClientKill. [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); } } } }