using System.Collections.Generic; using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Сверхспособности одержимых особых зомби (указание 13.10: «проверь ванильные способности и переставь их на E»). /// Чак (и Морозный коготь): E - замах, поднять валун над собой; ПКМ - метнуть его в точку под прицелом или /// в существо у прицела (тест 12.10: «так и не бросил камень» - замах ждал позы готовности, а она не /// наступала, и все следующие нажатия молча тонули). /// Ранчер (и Чумной плевун): ПКМ - выплюнуть Рой фараона (necroInsectSwarm, наш рой из свитка, обращает /// ужаленных зомби) вместо ванильных мух, откат SwarmCooldown; E - невидимость на InvisTime: зомби его /// не видят и бросают погоню, модель полупрозрачна; удар или плевок её снимают. /// Крикунья: E - крик зовёт орду разведчиков по стадии игры (как в игре, ScoutGSList), и вся орда сразу под /// девиацией. Чужих зомби крик больше не стягивает (решение 13.10). /// Мутант: E - его плевок веером вокруг себя (был на ПКМ). Подрыва у него нет: он EntityZombieCop, но в игре /// не взрывается (ExplodeHealthThreshold 0), а E подрывал его по ошибке. /// Откаты видны в PossessionHud (там, где полоска выносливости игрока). /// /// ПАМЯТЬ. Только числа и очередь id классов орды; ссылок на сущности нет. Очередь чистится при выходе из мира. /// public static class PossessionPowers { public enum Kind { None, Thrower, Rancher, Screamer, Mutant } public static Kind Current; public const float InvisTime = 15f, InvisCooldown = 30f; /// Прозрачность невидимого ранчера - родной _Fade шейдера персонажей (EntityAlive.renderFadeMax). public const float InvisFade = 0.25f; public const float InvisClearPeriod = 0.25f; public const float SwarmCooldown = 30f; public const string SwarmEntity = "necroInsectSwarm"; public const float HordeCooldown = 60f; public const string HordeSpawner = "ScoutGSList"; /// Не больше стольких за крик (у игры на стадии 52 - 26). public const int HordeMax = 40; /// Орда появляется по одному зомби раз в столько секунд - без рывка кадра. public const float HordeSpawnStep = 0.2f; public const int HordeMinRange = 4, HordeMaxRange = 14; static float _invisUntil, _invisNext, _invisClear; static float _hordeNext, _hordeStep; static readonly List _hordeQueue = new List(); static Vector3 _hordeAt; static int _hordeFails; /// Рой фараона уже вылетел в этом плевке - дальняя атака ранчера закончена. public static bool SwarmSpawned; /// Когда ранчер снова сможет плюнуть роем (Time.time); переживает перевселение. public static float SwarmReadyAt; public static bool Invisible => Current == Kind.Rancher && Time.time < _invisUntil; public static Kind KindOf(EntityAlive _z) { if (_z == null || !EntityClass.list.TryGetValue(_z.entityClass, out EntityClass c)) { return Kind.None; } string n = c.entityClassName; if (n.StartsWith("zombieChuck") || n.StartsWith("zombieFrostclaw")) { return Kind.Thrower; } if (n.StartsWith("zombieRancher") || n.StartsWith("zombiePlagueSpitter")) { return Kind.Rancher; } if (n.StartsWith("zombieScreamer")) { return Kind.Screamer; } if (n.StartsWith("zombieMutated")) { return Kind.Mutant; } return Kind.None; } public static void Begin(EntityAlive _z, bool _pet) { Current = _pet ? Kind.None : KindOf(_z); _invisUntil = 0f; SwarmSpawned = false; } public static void End(EntityAlive _z) { if (Invisible) { SetFade(_z, 1f); _invisNext = Time.time + InvisCooldown; } _invisUntil = 0f; Current = Kind.None; } /// Выход из мира: всё в ноль (откаты тоже - Time.time в новом мире продолжается, но держать их незачем). public static void Clear() { Current = Kind.None; _invisUntil = _invisNext = _hordeNext = SwarmReadyAt = 0f; _hordeQueue.Clear(); SwarmSpawned = false; } // ---------- Невидимость ранчера ---------- public static void StartInvis(EntityAlive _z) { if (Invisible) { return; } if (Time.time < _invisNext) { Debug.Log("[NecromancerTome] Possession: невидимость не готова - откат " + (_invisNext - Time.time).ToString("F1") + " с"); return; } _invisUntil = Time.time + InvisTime; _invisNext = _invisUntil + InvisCooldown; _invisClear = 0f; SetFade(_z, InvisFade); Audio.Manager.BroadcastPlay(_z, "plaguesense"); Debug.Log("[NecromancerTome] Possession: невидимость ранчера на " + InvisTime + " с"); } /// Удар или плевок - невидимость снята, откат от этой минуты. public static void BreakInvis(EntityAlive _z, string _why) { if (!Invisible) { return; } _invisUntil = 0f; _invisNext = Time.time + InvisCooldown; SetFade(_z, 1f); Debug.Log("[NecromancerTome] Possession: невидимость снята - " + _why); } static void SetFade(EntityAlive _z, float _fade) { if (_z == null) { return; } _z.renderFadeMax = _fade; _z.renderFadeTarget = _fade; } /// Каждый кадр одержимости: пока невидим - кто гнался за ранчером, теряет его. public static void Tick(World _world, EntityAlive _z) { if (Current != Kind.Rancher || _invisUntil <= 0f) { return; } if (Time.time >= _invisUntil) { _invisUntil = 0f; SetFade(_z, 1f); Debug.Log("[NecromancerTome] Possession: невидимость кончилась"); return; } _z.renderFadeMax = InvisFade; // игра поднимает его только на смерти, но держим явно if (Time.time < _invisClear) { return; } _invisClear = Time.time + InvisClearPeriod; for (int i = 0; i < _world.EntityAlives.Count; i++) { EntityAlive e = _world.EntityAlives[i]; if (e == null || e == _z || e.IsDead()) { continue; } if (e.GetAttackTarget() == _z) { e.SetAttackTarget(null, 0); } if (e.GetRevengeTarget() == _z) { e.SetRevengeTarget(null); } } } public static float InvisLeft => Invisible ? _invisUntil - Time.time : 0f; public static float InvisCooldownLeft => Invisible ? 0f : Mathf.Max(0f, _invisNext - Time.time); // ---------- Орда крикуньи ---------- public static float HordeCooldownLeft => Mathf.Max(0f, _hordeNext - Time.time); public static void Scream(EntityPlayerLocal _player, EntityAlive _z) { if (Time.time < _hordeNext) { Debug.Log("[NecromancerTome] Possession: крик не готов - откат " + (_hordeNext - Time.time).ToString("F1") + " с"); return; } _hordeNext = Time.time + HordeCooldown; string snd = _z.GetSoundAlert(); if (!string.IsNullOrEmpty(snd)) { Audio.Manager.BroadcastPlay(_z, snd); } if (!GameStageDefinition.TryGetGameStage(HordeSpawner, out GameStageDefinition def) || def.stages.Count == 0) { Debug.Log("[NecromancerTome] Possession: орды нет - нет спавнера " + HordeSpawner); return; } int gs = GameStageDefinition.CalcGameStageAround(_player); GameStageDefinition.Stage stage = def.GetStage(gs) ?? def.stages[0]; int last = -1; int added = 0; for (int g = 0; g < stage.Count; g++) { GameStageDefinition.SpawnGroup grp = stage.GetSpawnGroup(g); // У игры на первых стадиях num не задан (1) - даём хотя бы двоих. int n = Mathf.Max(2, (int)grp.spawnCount); for (int k = 0; k < n && _hordeQueue.Count < HordeMax; k++) { int id = EntityGroups.GetRandomFromGroup(grp.groupName, ref last); if (id != -1 && id != 0) { _hordeQueue.Add(id); added++; } } } _hordeAt = _z.position; _hordeStep = 0f; _hordeFails = 0; Debug.Log("[NecromancerTome] Possession: крик крикуньи - орда " + added + " (стадия " + gs + ", ступень " + stage.stageNum + ")"); } /// Каждый кадр (и после выхода из одержимости): орда появляется по одному. public static void SpawnTick(World _world) { if (_hordeQueue.Count == 0 || Time.time < _hordeStep) { return; } _hordeStep = Time.time + HordeSpawnStep; if (!_world.GetMobRandomSpawnPosWithWater(_hordeAt, HordeMinRange, HordeMaxRange, 0, false, out Vector3 pos)) { if (++_hordeFails > 20) { Debug.Log("[NecromancerTome] Possession: орде негде появиться, осталось " + _hordeQueue.Count + " - отбой"); _hordeQueue.Clear(); } return; } int id = _hordeQueue[_hordeQueue.Count - 1]; _hordeQueue.RemoveAt(_hordeQueue.Count - 1); Entity e = EntityFactory.CreateEntity(id, pos, new Vector3(0f, Random.Range(0f, 360f), 0f)); e.SetSpawnerSource(EnumSpawnerSource.Dynamic); _world.SpawnEntityInWorld(e); if (e is EntityAlive a && a.Buffs != null) { a.Buffs.AddBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName); } if (_hordeQueue.Count == 0) { Debug.Log("[NecromancerTome] Possession: орда крикуньи вся на месте"); } } // ---------- Бросок Чака ---------- public const float ThrowAimRange = 100f; /// Насколько луч прицела может пройти мимо груди существа, чтобы метнуть в него. public const float ThrowAimRadius = 1.5f; /// Куда лететь валуну: грудь существа у луча прицела (ближайшее по лучу), иначе точка под прицелом. public static Vector3 ThrowTarget(World _world, EntityAlive _z, Vector3 _aimPoint) { Transform cam = Possession.Player?.cameraTransform; if (cam == null) { return _aimPoint; } Vector3 from = cam.position + Origin.position; Vector3 dir = cam.forward; EntityAlive best = null; float bestAlong = ThrowAimRange; for (int i = 0; i < _world.EntityAlives.Count; i++) { EntityAlive e = _world.EntityAlives[i]; if (e == null || e == _z || e.IsDead() || e == Possession.Player) { continue; } Vector3 to = e.getChestPosition() - from; float along = Vector3.Dot(to, dir); if (along < Possession.CamBack + 0.5f || along > bestAlong) { continue; } if ((to - dir * along).sqrMagnitude > ThrowAimRadius * ThrowAimRadius) { continue; } bestAlong = along; best = e; } return best != null ? best.getChestPosition() : _aimPoint; } } /// Валун одержимого Чака - по дуге в точку под прицелом (или в существо): позиция вместо направления /// и 1 - «баллистика», как у ИИ, когда цель есть. Без цели игра кидает прямо по взгляду - камень падал рядом. [HarmonyPatch(typeof(ItemActionVomit), "GetActionEffectsValues")] public static class Patch_ItemActionVomit_GetActionEffectsValues_Possession { public static void Postfix(ItemActionData _actionData, ref Vector3 _direction, ref int __result) { EntityAlive holder = _actionData?.invData?.holdingEntity; if (!Possession.IsPossessed(holder) || PossessionPowers.Current != PossessionPowers.Kind.Thrower) { return; } _direction = Possession.ThrowAim; __result = 1; } } /// Плевок одержимого ранчера - Рой фараона вместо ванильных мух (указание 13.10). [HarmonyPatch(typeof(ItemActionSpawnEntity), "Spawn")] public static class Patch_ItemActionSpawnEntity_Spawn_Possession { public static void Prefix(ItemActionSpawnEntity __instance, ItemActionData _actionData, ref string __state) { EntityAlive holder = _actionData?.invData?.holdingEntity; if (!Possession.IsPossessed(holder) || PossessionPowers.Current != PossessionPowers.Kind.Rancher) { return; } __state = __instance.entityToSpawn; __instance.entityToSpawn = PossessionPowers.SwarmEntity; PossessionPowers.SwarmSpawned = true; Debug.Log("[NecromancerTome] Possession: ранчер выплюнул Рой фараона"); } public static void Finalizer(ItemActionSpawnEntity __instance, string __state) { if (__state != null) { __instance.entityToSpawn = __state; } } } /// Плевок одержимого мутанта - в прицел (указание 13.10: «у мутанта атака не целится»). Ванильный /// ItemActionVomit мутанта бросает снаряды веером под ноги (SpreadVerticalOffset -50, разброс 90° вбок) со /// скоростью 4.5 м/с - ИИ так поливает того, кто вплотную. Здесь после вылета скорость каждого снаряда /// пересчитывается навесом в Possession.SpitTarget, как ProjectileMoveScript.Fire делает для _isBallistic, с /// небольшим разбросом между снарядами очереди. [HarmonyPatch(typeof(ProjectileMoveScript), nameof(ProjectileMoveScript.Fire))] public static class Patch_ProjectileMoveScript_Fire_PossessedSpit { public static void Postfix(ProjectileMoveScript __instance, Vector3 _idealStartPos, Entity _firingEntity) { if (!Possession.IsPossessed(_firingEntity) || PossessionPowers.Current != PossessionPowers.Kind.Mutant || __instance.itemActionProjectile == null || __instance.itemActionProjectile.FlyTime < 0f) { return; } Vector3 from = _idealStartPos.y != 0f ? _idealStartPos : __instance.transform.position + Origin.position; Vector3 d = Possession.SpitTarget(from) + Random.insideUnitSphere * 0.4f - from; float t = Mathf.Clamp(d.magnitude / 12f, 0.2f, 1.2f); __instance.velocity = d / t; __instance.velocity.y -= __instance.gravity * 0.5f * t; __instance.flyDirection = __instance.velocity.normalized; } } }