using GUI_2;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
///
/// Хваты пустой руки (03.10, BACKLOG «Заклинания с руки и жесты»): на R - меню из трёх строк:
/// обычная рука, «отрицание», «пиф-паф». Пустая рука - предмет meleeHandPlayer с DynamicMelee,
/// меню на R игра открывает ей так же, как ножу (PlayerMoveController: HasRadial).
///
/// Выбор НЕ сохраняется (указание 03.10: «после перезахода будет снова обычная рука») - просто поле
/// в памяти. Ни cvar, ни предмет не трогаем. На выходе из мира (нет мира или сменился игрок) всё
/// сбрасывается, и ссылки на кости, снимок заморозки и цель отпускаются (HandPose.ResetAll) - чтобы
/// за сессию ничего не копилось (указание: «не допустить утечку памяти на сохранение состояний»).
/// В хвате рука стоит позой (HandPose: кости после аниматора + заморозка остальных), а кнопки мыши
/// не бьют: ExecuteAction у DynamicMelee пустой руки перехвачен.
/// ЛКМ в «отрицании» - зомби под прицелом в леденящем ужасе: бежит от игрока с ускорением и теряет
/// его как цель (NecroFleeSnakeTask.ScareFrom). Звук - как у празднующего убийства Twitch.
/// ЛКМ в «пиф-пафе» - одержимость зомби-гуманоида под прицелом (Possession.cs).
/// ПКМ в хватах - ничего.
///
public static class HandHolds
{
public const string HandItem = "meleeHandPlayer";
public const int None = 0, Deny = 1, PifPaf = 2, Thread = 3;
/// Хват для нитей - не сразу: снимок замороженных костей должен быть в покое хвата 68, после доставания.
const float ThreadDelay = 0.6f;
static float _threadSince = -1f;
/// Дальность и конус выбора зомби для «отрицания».
public const float DenyRange = 25f;
public const float DenyConeDeg = 12f;
/// Бежит, пока игрок не дальше этого: зрение зомби до 30 м, у Чака 40 (entityclasses.xml,
/// SightRange) - указание 03.10: «убегать он должен дальше 40 м, чтобы терять из прицела».
public const float DenyFarDistance = 40f;
/// Предел бегства на случай, если застрял и не может уйти на 40 м.
public const float DenyMaxSeconds = 25f;
/// Скорость бегства - выше бега зомби-людей (1.5), как у медведя (указание: «дать ему ускорение»).
public const float DenySpeed = 2.2f;
public const float Cooldown = 0.6f;
/// Звук «козы» (05.10: «нужен короткий звук» - вместо twitch_celebrate): бросок змеи - тот же,
/// что у рывка духа змеи (SnakeControl.AttackSound): «раз зомби боятся духа змеи, некромант может
/// пугать им и в телесной форме».
public const string DenySound = SnakeControl.AttackSound;
/// «Пиф-паф» (указание 03.10): не вселился - звон разбитого стекла, вселился - щелчок
/// удавшейся отмычки (sounds.xml: glassdestroy, lockpick_success).
public const string PifPafFailSound = "glassdestroy";
public const string PifPafPossessSound = "lockpick_success";
/// Выбранный хват - только на время сессии в мире.
public static int Hold;
static int _applied = -1;
static int _playerId = -1;
static float _nextCast;
/// Вселение - на пике толчка руки (05.10): иначе вид сразу уходит к зомби и толчка не видно.
/// Цель берётся в момент щелчка.
static EntityAlive _pifPafVictim;
static float _pifPafAt = -1f;
public static bool IsHand(ItemClass _item) => _item != null && _item.Name == HandItem;
public static int Current(EntityPlayerLocal _player)
{
// Только от первого лица (16.10: «В виде от третьего лица что-то страшное»): поза и перенос ножа - для рук первого
// лица; от третьего лица модель в руке - у тела, и нож улетал к невидимой руке первого лица.
if (_player != null && _player.inventory?.holdingItem?.Name == CinerariumThreads.ThreadKnifeName &&
(_player.emodel as EModelSDCS)?.IsFPV == true && !SnakeMode.Active && !MindPalace.Active && !Possession.Active && !_player.IsDead())
{
if (_threadSince < 0f)
{
_threadSince = Time.time;
}
bool ready = Time.time - _threadSince >= ThreadDelay && !(_player.Hand != null && _player.Hand.isSwitching);
return ready ? Thread : None;
}
_threadSince = -1f;
if (_player == null || _player.Buffs == null || !IsHand(_player.inventory?.holdingItem) ||
SnakeMode.Active || MindPalace.Active || Possession.Active || _player.IsDead())
{
return None;
}
return Hold;
}
/// Из HandPoseDriver.LateUpdate - до HandPose.Apply. Позу трогаем только при смене
/// хвата: ручная настройка командой necropose держится до следующей смены.
public static void Update()
{
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
int id = player != null ? player.entityId : -1;
if (id != _playerId)
{
// Вышли из мира, зашли в другой или перезашли - обычная рука и пустые ссылки.
_playerId = id;
Hold = None;
_applied = None;
_target = null;
_targetFrame = -1;
_pifPafVictim = null;
_pifPafAt = -1f;
HandPose.ResetAll();
return;
}
if (_pifPafAt >= 0f && Time.time >= _pifPafAt)
{
PifPafShot(player);
}
int want = Current(player);
if (want == _applied)
{
return;
}
_applied = want;
if (want == Deny)
{
HandPose.PresetDeny();
}
else if (want == PifPaf)
{
HandPose.PresetPifPaf();
}
else if (want == Thread)
{
HandPose.PresetThread();
}
else
{
HandPose.Enabled = false;
HandPose.KnifeToLeft = false;
}
}
static void PifPafShot(EntityPlayerLocal _player)
{
EntityAlive victim = _pifPafVictim;
_pifPafVictim = null;
_pifPafAt = -1f;
if (Current(_player) != PifPaf)
{
return; // за полтика сменил хват или руку - выстрела нет
}
if (victim != null && !victim.IsDead() && Possession.CanPossess(victim) && Possession.Enter(_player, victim))
{
_player.PlayOneShot(PifPafPossessSound);
}
else
{
_player.PlayOneShot(PifPafFailSound);
Debug.Log("[NecromancerTome] HandHolds: пиф-паф - " + (victim == null ? "в прицеле нет зомби" : victim.EntityName + " не гуманоид"));
}
}
public static void OnRadialCommand(XUiC_Radial _sender, int _commandIndex, XUiC_Radial.RadialContextAbs _context)
{
if (_commandIndex >= None && _commandIndex <= PifPaf)
{
Hold = _commandIndex;
}
}
/// ЛКМ в хвате. True - нажатие наше, удар не выполнять.
public static void Cast(EntityPlayerLocal _player, int _hold)
{
if (Time.time < _nextCast)
{
return;
}
_nextCast = Time.time + Cooldown;
if (_hold == PifPaf)
{
// Одержимость (решение 03.10): «пиф-паф» по зомби-гуманоиду - управление им.
HandPose.Jab();
_pifPafVictim = Target(_player);
_pifPafAt = Time.time + HandPose.JabTime * 0.5f;
return;
}
HandPose.Jab(); // толчок к прицелу, как у «пиф-пафа» (05.10)
_player.PlayOneShot(DenySound);
World world = _player.world;
if (SnakeMode.IsBloodTide())
{
// Кровавый прилив (10.10): леденящий ужас тонет в насыщенном воздухе - жест и звук есть, бегства нет.
SnakeMode.BloodTideHint(_player);
return;
}
if (world.IsRemote())
{
return; // ИИ живёт на сервере; клиент в мультиплеере - решать отдельно
}
EntityAlive target = Target(_player);
if (target != null)
{
if (target.HasAnyTags(FlyerTag))
{
NecroFleeSnakeTask.ForgetNow(target, _player); // летает - наземного бегства нет, только теряет цель
Debug.Log("[NecromancerTome] HandHolds: стервятник " + target.entityId + " отрицание - потерял цель");
}
else
{
NecroFleeSnakeTask.ScareFrom(target, _player, DenyMaxSeconds, DenySpeed, DenyFarDistance);
}
}
}
static readonly FastTags ZombieTag = FastTags.Parse("zombie");
static readonly FastTags FlyerTag = FastTags.Parse("vulture");
/// Только зомби (указание 03.10: «кабаны и змеи хоть и противник, но не зомби, кроме
/// зомбокабана»; «стервятники тоже зомби, обычных в игре нету») - метка zombie в entityclasses.xml:
/// люди-зомби, зомби-собаки, лютые волки, зомби-медведи, зомби-кабаны, стервятники.
public static bool IsZombie(EntityAlive _e)
{
return _e != null && _e.HasAnyTags(ZombieTag);
}
static int _targetFrame = -1;
static EntityAlive _target;
/// Зомби под прицелом (раз за кадр): и для заклинания, и для красного прицела.
public static EntityAlive Target(EntityPlayerLocal _player)
{
if (_targetFrame != Time.frameCount)
{
_targetFrame = Time.frameCount;
_target = FindTarget(_player.world, _player);
}
return _target;
}
/// Прямая видимость своим лучом: между глазом и точкой нет блоков и земли (сущности на луче
/// не мешают). Не EntityAlive.CanSee: у игрока нет ИИ, и его кэш видимости (seeCache) не обновляется -
/// тест 03.10: Арлин захватывалась, зомби-коп нет.
static bool Visible(Vector3 _eye, Vector3 _to)
{
Vector3 d = _to - _eye;
return !Physics.Raycast(_eye - Origin.position, d.normalized, d.magnitude - 0.3f, SnakeControl.WallMask, QueryTriggerInteraction.Ignore);
}
/// Зомби ближе всех к линии прицела в конусе DenyConeDeg, не дальше DenyRange,
/// с прямой видимостью.
static EntityAlive FindTarget(World _world, EntityPlayerLocal _player)
{
Transform cam = _player.cameraTransform;
if (cam == null)
{
return null;
}
Vector3 eye = cam.position + Origin.position;
Vector3 fwd = cam.forward;
EntityAlive best = null;
float bestAngle = DenyConeDeg;
for (int i = 0; i < _world.EntityAlives.Count; i++)
{
EntityAlive e = _world.EntityAlives[i];
if (e == null || e.IsDead() || !IsZombie(e))
{
continue;
}
Vector3 to = e.getChestPosition() - eye;
float d = to.magnitude;
if (d > DenyRange || d < 0.1f)
{
continue;
}
float angle = Vector3.Angle(fwd, to);
if (angle < bestAngle && Visible(eye, e.getChestPosition()))
{
bestAngle = angle;
best = e;
}
}
return best;
}
}
/// Меню на R у пустой руки - как у ножа (Patch_ItemAction_HasRadial_SnakeKnife).
[HarmonyPatch(typeof(ItemAction), nameof(ItemAction.HasRadial))]
public static class Patch_ItemAction_HasRadial_HandHolds
{
public static void Postfix(ItemAction __instance, ref bool __result)
{
if (!__result && !SnakeMode.Active && !MindPalace.Active && HandHolds.IsHand(__instance.item))
{
__result = true;
}
}
}
[HarmonyPatch(typeof(ItemAction), nameof(ItemAction.SetupRadial))]
public static class Patch_ItemAction_SetupRadial_HandHolds
{
public static bool Prefix(ItemAction __instance, XUiC_Radial _xuiRadialWindow, EntityPlayerLocal _epl)
{
if (!HandHolds.IsHand(__instance.item))
{
return true;
}
int cur = HandHolds.Hold;
_xuiRadialWindow.ResetRadialEntries();
_xuiRadialWindow.CreateRadialEntry(HandHolds.None, "ui_game_symbol_hand", "UIAtlas", "",
Localization.Get("necroHoldNone"), cur == HandHolds.None);
_xuiRadialWindow.CreateRadialEntry(HandHolds.Deny, "ui_game_symbol_run", "UIAtlas", "",
Localization.Get("necroHoldDeny"), cur == HandHolds.Deny);
_xuiRadialWindow.CreateRadialEntry(HandHolds.PifPaf, "ui_game_symbol_pistol", "UIAtlas", "",
Localization.Get("necroHoldPifPaf"), cur == HandHolds.PifPaf);
_xuiRadialWindow.SetCommonData(UIUtils.GetButtonIconForAction(_epl.playerInput.Reload),
HandHolds.OnRadialCommand, null, cur);
return false;
}
}
/// В хвате кнопки мыши не бьют: ЛКМ - заклинание хвата, ПКМ - ничего (03.10: «руки
/// деформируются, пытаясь совершить старую анимацию»).
[HarmonyPatch(typeof(ItemActionDynamicMelee), nameof(ItemActionDynamicMelee.ExecuteAction))]
public static class Patch_ItemActionDynamicMelee_ExecuteAction_HandHolds
{
public static bool Prefix(ItemActionData _actionData, bool _bReleased)
{
if (!(_actionData?.invData?.holdingEntity is EntityPlayerLocal player))
{
return true;
}
int hold = HandHolds.Current(player);
if (hold == HandHolds.None || !HandHolds.IsHand(_actionData.invData.item))
{
return true;
}
if (!_bReleased && _actionData.indexInEntityOfAction == 0)
{
HandHolds.Cast(player, hold);
}
return false;
}
}
/// Прицел краснеет, когда в нём зомби, в обоих хватах (указание 03.10). Цвет прицела игра
/// берёт из EntityPlayerLocal.crosshairColors[настройка] - на время рисования подменяем его красным.
[HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.guiDrawCrosshair))]
public static class Patch_EntityPlayerLocal_guiDrawCrosshair_HandHolds
{
static readonly Color Red = new Color(1f, 0.15f, 0.1f);
public static void Prefix(EntityPlayerLocal __instance, out Color? __state)
{
__state = null;
// В одержимости прицел красит Possession.AimTint по цели одержимого, а не хват тела.
if (Possession.Active)
{
return;
}
int hold = HandHolds.Current(__instance);
EntityAlive target = hold == HandHolds.None ? null : HandHolds.Target(__instance);
// «Пиф-паф» берёт только зомби-гуманоидов - на зверях-зомби прицел не краснеет.
if (target == null || hold == HandHolds.PifPaf && !Possession.CanPossess(target))
{
return;
}
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count)
{
i = 0;
}
__state = EntityPlayerLocal.crosshairColors[i];
EntityPlayerLocal.crosshairColors[i] = Red;
}
public static void Postfix(Color? __state)
{
if (__state == null)
{
return;
}
int i = GamePrefs.GetInt(EnumGamePrefs.OptionsCrosshairColor);
if (i < 0 || i >= EntityPlayerLocal.crosshairColors.Count)
{
i = 0;
}
EntityPlayerLocal.crosshairColors[i] = __state.Value;
}
}
}