Files
necromants-tome-7d2d-3-2/HarmonySrc/PossessionPowers.cs
T
AlexCubeandClaude Opus 5.5 85a7c2721e Одержимость: прицел мимо своего тела, плевок мутанта в прицел
- AimPoint пропускает тело одержимого: у крупных зомби луч камеры упирался в свою спину - прицел не краснел, удары и выстрелы шли в себя.
- Плевок мутанта (E): скорость снарядов пересчитывается навесом в цель под прицелом (до 15 м), вместо ванильного веера под ноги; подсказка на 13 языках.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:05:25 +03:00

380 lines
15 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 System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Сверхспособности одержимых особых зомби (указание 13.10: «проверь ванильные способности и переставь их на E»).
/// Чак (и Морозный коготь): E - замах, поднять валун над собой; ПКМ - метнуть его в точку под прицелом или
/// в существо у прицела (тест 12.10: «так и не бросил камень» - замах ждал позы готовности, а она не
/// наступала, и все следующие нажатия молча тонули).
/// Ранчер (и Чумной плевун): ПКМ - выплюнуть Рой фараона (necroInsectSwarm, наш рой из свитка, обращает
/// ужаленных зомби) вместо ванильных мух, откат SwarmCooldown; E - невидимость на InvisTime: зомби его
/// не видят и бросают погоню, модель полупрозрачна; удар или плевок её снимают.
/// Крикунья: E - крик зовёт орду разведчиков по стадии игры (как в игре, ScoutGSList), и вся орда сразу под
/// девиацией. Чужих зомби крик больше не стягивает (решение 13.10).
/// Мутант: E - его плевок веером вокруг себя (был на ПКМ). Подрыва у него нет: он EntityZombieCop, но в игре
/// не взрывается (ExplodeHealthThreshold 0), а E подрывал его по ошибке.
/// Откаты видны в PossessionHud (там, где полоска выносливости игрока).
///
/// ПАМЯТЬ. Только числа и очередь id классов орды; ссылок на сущности нет. Очередь чистится при выходе из мира.
/// </summary>
public static class PossessionPowers
{
public enum Kind { None, Thrower, Rancher, Screamer, Mutant }
public static Kind Current;
public const float InvisTime = 15f, InvisCooldown = 30f;
/// <summary>Прозрачность невидимого ранчера - родной _Fade шейдера персонажей (EntityAlive.renderFadeMax).</summary>
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";
/// <summary>Не больше стольких за крик (у игры на стадии 52 - 26).</summary>
public const int HordeMax = 40;
/// <summary>Орда появляется по одному зомби раз в столько секунд - без рывка кадра.</summary>
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<int> _hordeQueue = new List<int>();
static Vector3 _hordeAt;
static int _hordeFails;
/// <summary>Рой фараона уже вылетел в этом плевке - дальняя атака ранчера закончена.</summary>
public static bool SwarmSpawned;
/// <summary>Когда ранчер снова сможет плюнуть роем (Time.time); переживает перевселение.</summary>
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;
}
/// <summary>Выход из мира: всё в ноль (откаты тоже - Time.time в новом мире продолжается, но держать их незачем).</summary>
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 + " с");
}
/// <summary>Удар или плевок - невидимость снята, откат от этой минуты.</summary>
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;
}
/// <summary>Каждый кадр одержимости: пока невидим - кто гнался за ранчером, теряет его.</summary>
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 + ")");
}
/// <summary>Каждый кадр (и после выхода из одержимости): орда появляется по одному.</summary>
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;
/// <summary>Насколько луч прицела может пройти мимо груди существа, чтобы метнуть в него.</summary>
public const float ThrowAimRadius = 1.5f;
/// <summary>Куда лететь валуну: грудь существа у луча прицела (ближайшее по лучу), иначе точка под прицелом.</summary>
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;
}
}
/// <summary>Валун одержимого Чака - по дуге в точку под прицелом (или в существо): позиция вместо направления
/// и 1 - «баллистика», как у ИИ, когда цель есть. Без цели игра кидает прямо по взгляду - камень падал рядом.</summary>
[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;
}
}
/// <summary>Плевок одержимого ранчера - Рой фараона вместо ванильных мух (указание 13.10).</summary>
[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;
}
}
}
/// <summary>Плевок одержимого мутанта - в прицел (указание 13.10: «у мутанта атака не целится»). Ванильный
/// ItemActionVomit мутанта бросает снаряды веером под ноги (SpreadVerticalOffset -50, разброс 90° вбок) со
/// скоростью 4.5 м/с - ИИ так поливает того, кто вплотную. Здесь после вылета скорость каждого снаряда
/// пересчитывается навесом в Possession.SpitTarget, как ProjectileMoveScript.Fire делает для _isBallistic, с
/// небольшим разбросом между снарядами очереди.</summary>
[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;
}
}
}