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