Одержимость: приёмы Чака, ранчера и крикуньи на E, откаты в HUD
Чак: E - замах, ПКМ - бросок валуна в прицел или в существо. Ранчер: ПКМ - Рой фараона, E - невидимость. Крикунья: E - орда под девиацией. Здоровье одержимого и откаты - на месте полосок здоровья и выносливости игрока. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cb2a4f19b4
commit
37ad57f1bc
@@ -0,0 +1,349 @@
|
||||
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).
|
||||
/// Откаты видны в PossessionHud (там, где полоска выносливости игрока).
|
||||
///
|
||||
/// ПАМЯТЬ. Только числа и очередь id классов орды; ссылок на сущности нет. Очередь чистится при выходе из мира.
|
||||
/// </summary>
|
||||
public static class PossessionPowers
|
||||
{
|
||||
public enum Kind { None, Thrower, Rancher, Screamer }
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user