- Прицел всех одержимых - всегда перекрестие (свой guiDrawCrosshair: точка бралась из проверки камеры от 3-го лица у тела); краснеет на враге: у стервятника - цель рывка, у наземных - враг на луче прицела до 30 м, не за стеной - Падаль: отхил вдвое меньше (EatHealFrac 0.125); ест на F стервятник, медведь, собака, волк и одержимые зомби - E - действие: стервятник - клич (был F), крыса/волк - обыск, медведь - на дыбы (враги до 3.5 м сбиты с ног и отброшены, откат 6 с) - Рёв медведя: надписи «на медведя идут N» и откат, мимо очереди подсказок - Крыса и волк обыскивают мешки с зомби (EntityLootContainer), волк берёт в сумку - Багажник волка: окно сумки как у браслета, без рюкзака; выброс - у волка - Отзыв волка: мешок на месте и метка «Сумка волка» на карте, снимается с мешком - Тексты на 13 языках Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
487 lines
19 KiB
C#
487 lines
19 KiB
C#
using HarmonyLib;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Обыск одержимой крысой (указание 08.10). Крыса - разведчик: «она может осмотреть ящики, и если там
|
||
/// что-то интересное, персонаж придёт и сам залутает». Поэтому - только смотреть.
|
||
///
|
||
/// Окно ванильное, ровно как у персонажа (таймер первого обыска, сетка, звуки): контейнер открывается
|
||
/// той же цепочкой LockManager.LockRequestLocal -> OnLockResponseLocal -> XUiC_LootWindowGroup.OpenLooting.
|
||
/// Отличия на время такого просмотра (Viewing):
|
||
/// - рюкзак персонажа не показывается («сумку персонажа вообще не показывать») - openContainer
|
||
/// открывает его сам, закрываем сразу же;
|
||
/// - ячейки контейнера заперты (XUiC_ItemStack.IsLocked), «Взять всё» (R) и сортировка не работают;
|
||
/// попытка взять - отказ, как при переполненном рюкзаке (указание: «делал бы вид, что сумки
|
||
/// переполнены»): звук ui_denied и xuiInventoryFullForPickup;
|
||
/// - окно не закрывается из-за дальности до ТЕЛА: XUiC_LootWindow.Update меряет её от персонажа.
|
||
/// Подсказку «E: обыскать» рисует управление игрока, а оно в одержимости выключено - выводим сами.
|
||
///
|
||
/// МЕШКИ (указание 09.10: «лутать жёлтые мешочки, и синие, и красные, падающие с зомби»). Мешок - сущность
|
||
/// EntityLootContainer, открывается той же цепочкой замка (LockRequestLocal -> Entity.OnLockResponseLocal), но
|
||
/// окно другое - XUiC_BagStorageWindowGroup; для него те же отличия: рюкзак закрыт, ячейки заперты, дальность
|
||
/// от крысы. Рюкзак погибшего игрока (EntityBackpack) - не мешок, его не трогаем.
|
||
/// </summary>
|
||
public static class RatLoot
|
||
{
|
||
/// <summary>Открыто (или открывается) окно просмотра крысой.</summary>
|
||
public static bool Viewing;
|
||
/// <summary>Окно открыл волк: щелчок и «Взять всё» кладут в его сумку (WolfBag), а не отказ.</summary>
|
||
public static bool Taking;
|
||
/// <summary>Контейнер, который осматривает крыса, и до какого времени после закрытия его не разрушать.</summary>
|
||
static Vector3i _sparePos;
|
||
static float _spareUntil;
|
||
/// <summary>Досягаемость контейнера от крысы, м.</summary>
|
||
const float Reach = 2.5f;
|
||
/// <summary>Мешок - не дальше стольких градусов от луча камеры (мешок мал, точно навести трудно).</summary>
|
||
const float BagAimDeg = 25f;
|
||
/// <summary>Открытый на просмотр мешок - для щелчков по ячейкам и «Взять всё».</summary>
|
||
static EntityLootContainer _viewBag;
|
||
|
||
/// <summary>Кто осматривает - для лога (у волка тот же обыск).</summary>
|
||
static string Who => Possession.Zombie != null && WolfControl.Is(Possession.Zombie) ? "волк" : "крыса";
|
||
|
||
/// <summary>Мешок перед крысой: ближе Reach к ней и ближе всех к лучу камеры, не дальше BagAimDeg.</summary>
|
||
public static EntityLootContainer FindBag(World _world, EntityPlayerLocal _player, EntityAlive _rat)
|
||
{
|
||
Transform cam = _player.cameraTransform;
|
||
if (cam == null)
|
||
{
|
||
return null;
|
||
}
|
||
Vector3 from = cam.position + Origin.position;
|
||
EntityLootContainer best = null;
|
||
float bestDeg = BagAimDeg;
|
||
for (int i = 0; i < _world.Entities.list.Count; i++)
|
||
{
|
||
if (!(_world.Entities.list[i] is EntityLootContainer bag) || bag.IsMarkedForUnload() || bag.bag == null ||
|
||
(bag.position - _rat.position).sqrMagnitude > Reach * Reach)
|
||
{
|
||
continue;
|
||
}
|
||
float deg = Vector3.Angle(cam.forward, bag.position + Vector3.up * 0.2f - from);
|
||
if (deg < bestDeg)
|
||
{
|
||
bestDeg = deg;
|
||
best = bag;
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
/// <summary>Контейнер перед крысой по лучу камеры: блок с хранилищем ближе Reach к крысе; иначе null.</summary>
|
||
public static TEFeatureStorage Find(World _world, EntityPlayerLocal _player, EntityAlive _rat, out Vector3i _pos, out BlockValue _bv)
|
||
{
|
||
_pos = Vector3i.zero;
|
||
_bv = BlockValue.Air;
|
||
Camera cam = _player.playerCamera;
|
||
if (cam == null)
|
||
{
|
||
return null;
|
||
}
|
||
Vector3 eye = _rat.getHeadPosition();
|
||
Vector3 from = cam.transform.position + Origin.position;
|
||
float dist = Vector3.Distance(from, eye) + Reach + 1f;
|
||
Ray ray = new Ray(from, cam.transform.forward);
|
||
if (!Voxel.Raycast(_world, ray, dist, -555266077, 69, 0f))
|
||
{
|
||
return null;
|
||
}
|
||
WorldRayHitInfo hit = Voxel.voxelRayHitInfo;
|
||
if ((hit.hit.pos - eye).sqrMagnitude > Reach * Reach)
|
||
{
|
||
return null;
|
||
}
|
||
Vector3i pos = hit.hit.blockPos;
|
||
BlockValue bv = hit.hit.blockValue;
|
||
Block block = bv.Block;
|
||
if (block == null || bv.isair)
|
||
{
|
||
return null;
|
||
}
|
||
if (bv.ischild && block.multiBlockPos != null)
|
||
{
|
||
pos = block.multiBlockPos.GetParentPos(pos, bv);
|
||
bv = _world.GetBlock(pos);
|
||
}
|
||
TileEntity te = _world.GetTileEntity(pos);
|
||
TEFeatureStorage st = te != null ? te.GetSelfOrFeature<TEFeatureStorage>() : null;
|
||
if (st == null)
|
||
{
|
||
return null;
|
||
}
|
||
_pos = pos;
|
||
_bv = bv;
|
||
return st;
|
||
}
|
||
|
||
/// <summary>Каждый кадр одержимости крысой: подсказка и E - открыть. true - нажатие E ушло на контейнер или
|
||
/// мешок (волку тогда не открывать свою сумку).</summary>
|
||
public static bool Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false)
|
||
{
|
||
LocalPlayerUI ui = _player.PlayerUI;
|
||
if (ui == null || Viewing || ui.windowManager.IsModalWindowOpen())
|
||
{
|
||
return false;
|
||
}
|
||
PlayerActionsLocal input = _player.playerInput;
|
||
string key = input.Activate.GetBindingXuiMarkupString() + input.PermanentActions.Activate.GetBindingXuiMarkupString();
|
||
TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv);
|
||
if (st == null)
|
||
{
|
||
EntityLootContainer bag = FindBag(_world, _player, _rat);
|
||
if (bag == null)
|
||
{
|
||
return false;
|
||
}
|
||
XUiC_InteractionPrompt.SetText(ui, string.Format(Localization.Get("necroPossessSearchBag"), key, bag.LocalizedEntityName));
|
||
if (!_activate)
|
||
{
|
||
return false;
|
||
}
|
||
Viewing = true;
|
||
Taking = _take;
|
||
_viewBag = bag;
|
||
XUiC_InteractionPrompt.SetText(ui, null);
|
||
_lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) });
|
||
_lockRequest.Invoke(LockManager.Instance, new object[] { bag, (ushort)0 });
|
||
Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает мешок " + EntityClass.GetEntityClassName(bag.entityClass) + " (" + bag.entityId + ")");
|
||
return true;
|
||
}
|
||
string text = st.GetActivationText(_world, pos, bv, _rat, key, bv.Block.GetLocalizedBlockName());
|
||
if (!string.IsNullOrEmpty(text))
|
||
{
|
||
XUiC_InteractionPrompt.SetText(ui, text);
|
||
}
|
||
if (!_activate)
|
||
{
|
||
return false;
|
||
}
|
||
bool locked = st.lockpickFeature != null && st.lockpickFeature.NeedsLockpicking() ||
|
||
st.lockFeature != null && st.lockFeature.IsLocked() && !st.lockFeature.IsUserAllowed(Platform.PlatformManager.InternalLocalUserIdentifier) ||
|
||
st.isJammed;
|
||
if (locked)
|
||
{
|
||
Audio.Manager.BroadcastPlayByLocalPlayer(pos.ToVector3() + Vector3.one * 0.5f, "Misc/locked");
|
||
return true;
|
||
}
|
||
Viewing = true;
|
||
Taking = _take;
|
||
_sparePos = pos;
|
||
_spareUntil = float.MaxValue;
|
||
XUiC_InteractionPrompt.SetText(ui, null);
|
||
// Через отражение: у LockRequestLocal есть перегрузка с ReadOnlySpan, которой нет в справочных сборках проекта.
|
||
_lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) });
|
||
_lockRequest.Invoke(LockManager.Instance, new object[] { st, (ushort)0 });
|
||
Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает " + bv.Block.GetBlockName() + " " + pos);
|
||
return true;
|
||
}
|
||
|
||
static System.Reflection.MethodInfo _lockRequest;
|
||
|
||
/// <summary>Отказ «рюкзак полон».</summary>
|
||
public static void Deny()
|
||
{
|
||
LocalPlayerUI ui = LocalPlayerUI.GetUIForPrimaryPlayer();
|
||
if (ui?.entityPlayer != null)
|
||
{
|
||
GameManager.ShowTooltip(ui.entityPlayer, Localization.Get("xuiInventoryFullForPickup"), string.Empty, "ui_denied");
|
||
}
|
||
}
|
||
|
||
/// <summary>Выход из одержимости при открытом окне - закрыть его.</summary>
|
||
public static void CloseIfViewing()
|
||
{
|
||
if (!Viewing)
|
||
{
|
||
return;
|
||
}
|
||
LocalPlayerUI ui = LocalPlayerUI.GetUIForPrimaryPlayer();
|
||
ui?.windowManager.CloseAllOpenModalWindows();
|
||
Viewing = false;
|
||
_viewBag = null;
|
||
_spareUntil = Time.time + 5f;
|
||
}
|
||
|
||
static void LockSlots(XUiC_LootWindow _w, bool _lock)
|
||
{
|
||
LockSlots(_w?.lootContainer, _lock);
|
||
}
|
||
|
||
static void LockSlots(XUiC_ItemStackGrid grid, bool _lock)
|
||
{
|
||
if (grid?.itemControllers == null)
|
||
{
|
||
return;
|
||
}
|
||
foreach (XUiC_ItemStack s in grid.itemControllers)
|
||
{
|
||
if (s != null)
|
||
{
|
||
s.IsLocked = _lock;
|
||
}
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(XUiC_LootWindowGroup), nameof(XUiC_LootWindowGroup.openContainer))]
|
||
public static class Patch_openContainer
|
||
{
|
||
public static void Postfix(XUiC_LootWindowGroup __instance)
|
||
{
|
||
if (!Viewing)
|
||
{
|
||
return;
|
||
}
|
||
__instance.xui.playerUI.windowManager.Close("backpack");
|
||
LockSlots(__instance.lootWindow, true);
|
||
}
|
||
}
|
||
|
||
/// <summary>Контейнеры с destroy_on_close (loot.xml: «empty» - 94 вида, мусор, кучи, холодильники, рюкзаки...;
|
||
/// «true» - аэродроп и коробки Twitch, это сущности, крыса их не открывает) разрушаются при снятии замка
|
||
/// после закрытия окна. Осмотр крысой их не разрушает (указание 08.10).</summary>
|
||
[HarmonyPatch(typeof(GameManager), nameof(GameManager.CheckDestroyTileEntity))]
|
||
public static class Patch_CheckDestroyTileEntity
|
||
{
|
||
public static bool Prefix(Vector3i _blockPos)
|
||
{
|
||
if (_blockPos != _sparePos || Time.time > _spareUntil)
|
||
{
|
||
return true;
|
||
}
|
||
_spareUntil = 0f;
|
||
Debug.Log("[NecromancerTome] RatLoot: " + _blockPos + " после осмотра крысой не разрушен");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Таймер первого обыска прерван - просмотра нет.</summary>
|
||
[HarmonyPatch(typeof(XUiC_LootWindowGroup), nameof(XUiC_LootWindowGroup.openTimerClosedManually))]
|
||
public static class Patch_openTimerClosedManually
|
||
{
|
||
public static void Postfix()
|
||
{
|
||
if (Viewing)
|
||
{
|
||
Viewing = false;
|
||
_spareUntil = Time.time + 5f;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Отказ замка (контейнер занят другим) - просмотра нет.</summary>
|
||
[HarmonyPatch(typeof(TEFeatureStorage), nameof(TEFeatureStorage.OnLockResponseLocal))]
|
||
public static class Patch_OnLockResponseLocal
|
||
{
|
||
public static void Postfix(bool _success)
|
||
{
|
||
if (!_success && Viewing)
|
||
{
|
||
Viewing = false;
|
||
_spareUntil = 0f;
|
||
}
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(XUiC_LootWindowGroup), nameof(XUiC_LootWindowGroup.OnClose))]
|
||
public static class Patch_OnClose
|
||
{
|
||
public static void Postfix(XUiC_LootWindowGroup __instance)
|
||
{
|
||
LockSlots(__instance.lootWindow, false); // всегда: запертые ячейки не должны достаться персонажу
|
||
if (Viewing)
|
||
{
|
||
Viewing = false;
|
||
_spareUntil = Time.time + 5f; // снятие замка (и проверка разрушения) - следом, может прийти пакетом
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Дальность окна - от крысы, не от тела: XUiC_LootWindow.Update меряет её от entityPlayer.position,
|
||
/// поэтому на время его Update персонаж «стоит» на месте крысы (поле, ничего больше не трогается), а
|
||
/// Finalizer возвращает позицию при любом исходе. (Тест 08.10: прежний вариант обнулял te - и закрытие окна
|
||
/// по E/ESC падало в XUiC_LootWindow.OnClose, окно застревало и после выхода из одержимости.)</summary>
|
||
[HarmonyPatch(typeof(XUiC_LootWindow), nameof(XUiC_LootWindow.Update))]
|
||
public static class Patch_LootWindow_Update
|
||
{
|
||
public static void Prefix(XUiC_LootWindow __instance, out Vector3? __state)
|
||
{
|
||
__state = null;
|
||
EntityPlayerLocal p = __instance.xui.playerUI.entityPlayer;
|
||
EntityAlive rat = Possession.Zombie;
|
||
if (!Viewing || p == null || rat == null)
|
||
{
|
||
return;
|
||
}
|
||
__state = p.position;
|
||
p.position = rat.position;
|
||
}
|
||
|
||
public static void Finalizer(XUiC_LootWindow __instance, Vector3? __state)
|
||
{
|
||
if (__state.HasValue)
|
||
{
|
||
__instance.xui.playerUI.entityPlayer.position = __state.Value;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Окно мешка открылось на просмотр - рюкзак закрыть, ячейки запереть (как у ящика).</summary>
|
||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))]
|
||
public static class Patch_Bag_OnOpen
|
||
{
|
||
public static void Postfix(XUiC_BagStorageWindowGroup __instance)
|
||
{
|
||
if (!Viewing || _viewBag == null)
|
||
{
|
||
return;
|
||
}
|
||
__instance.xui.playerUI.windowManager.Close("backpack");
|
||
LockSlots(__instance.containerWindow, true);
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnClose))]
|
||
public static class Patch_Bag_OnClose
|
||
{
|
||
public static void Prefix(XUiC_BagStorageWindowGroup __instance)
|
||
{
|
||
LockSlots(__instance.containerWindow, false); // всегда: запертые ячейки не должны достаться персонажу
|
||
if (Viewing && _viewBag != null)
|
||
{
|
||
Viewing = false;
|
||
_viewBag = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Таймер обыска мешка прерван - просмотра нет.</summary>
|
||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.CancelOpen))]
|
||
public static class Patch_Bag_CancelOpen
|
||
{
|
||
public static void Postfix()
|
||
{
|
||
if (Viewing && _viewBag != null)
|
||
{
|
||
Viewing = false;
|
||
_viewBag = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Замок мешка не дан (занят другим) - просмотра нет.</summary>
|
||
[HarmonyPatch(typeof(Entity), nameof(Entity.OnLockResponseLocal))]
|
||
public static class Patch_Entity_OnLockResponseLocal
|
||
{
|
||
public static void Postfix(Entity __instance, bool __result)
|
||
{
|
||
if (!__result && Viewing && _viewBag != null && __instance == _viewBag)
|
||
{
|
||
Viewing = false;
|
||
_viewBag = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Дальность окна мешка - от крысы: проверка окна меряет её до entityPlayer, поэтому на время Update
|
||
/// персонаж «стоит» на месте крысы, как у окна ящика.</summary>
|
||
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.Update))]
|
||
public static class Patch_Bag_Update
|
||
{
|
||
public static void Prefix(XUiC_BagStorageWindowGroup __instance, out Vector3? __state)
|
||
{
|
||
__state = null;
|
||
EntityPlayerLocal p = __instance.xui.playerUI.entityPlayer;
|
||
EntityAlive rat = Possession.Zombie;
|
||
if (!Viewing || _viewBag == null || p == null || rat == null)
|
||
{
|
||
return;
|
||
}
|
||
__state = p.position;
|
||
p.position = rat.position;
|
||
}
|
||
|
||
public static void Finalizer(XUiC_BagStorageWindowGroup __instance, Vector3? __state)
|
||
{
|
||
if (__state.HasValue)
|
||
{
|
||
__instance.xui.playerUI.entityPlayer.position = __state.Value;
|
||
}
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveAll))]
|
||
public static class Patch_MoveAll
|
||
{
|
||
public static bool Prefix(XUiC_ContainerStandardControls __instance)
|
||
{
|
||
if (!Viewing)
|
||
{
|
||
return true;
|
||
}
|
||
TEFeatureStorage te = __instance.GetParentByType<XUiC_LootWindow>()?.te;
|
||
if (Taking && te != null)
|
||
{
|
||
WolfBag.TakeAll(te);
|
||
}
|
||
else if (Taking && _viewBag != null)
|
||
{
|
||
WolfBag.TakeAll(_viewBag);
|
||
}
|
||
else
|
||
{
|
||
Deny();
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
[HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.Sort))]
|
||
public static class Patch_Sort
|
||
{
|
||
public static bool Prefix()
|
||
{
|
||
return !Viewing;
|
||
}
|
||
}
|
||
|
||
/// <summary>Щелчок по запертой ячейке контейнера - отказ «рюкзак полон».</summary>
|
||
[HarmonyPatch(typeof(XUiC_ItemStack), nameof(XUiC_ItemStack.Update))]
|
||
public static class Patch_ItemStack_Update
|
||
{
|
||
public static void Postfix(XUiC_ItemStack __instance)
|
||
{
|
||
if (!Viewing || !__instance.isOver || __instance.ItemStack == null || __instance.ItemStack.IsEmpty())
|
||
{
|
||
return;
|
||
}
|
||
// Ячейка контейнера-блока (XUiC_LootContainer) или мешка (XUiC_BagContainer), не рюкзака.
|
||
XUiC_ItemStackGrid grid = (XUiC_ItemStackGrid)__instance.GetParentByType<XUiC_LootContainer>() ?? __instance.GetParentByType<XUiC_BagContainer>();
|
||
if (grid == null)
|
||
{
|
||
return;
|
||
}
|
||
CursorControllerAbs cur = __instance.xui.playerUI.CursorController;
|
||
if (cur.GetMouseButtonDown(UICamera.MouseButton.LeftButton) || cur.GetMouseButtonDown(UICamera.MouseButton.RightButton))
|
||
{
|
||
TEFeatureStorage te = __instance.GetParentByType<XUiC_LootWindow>()?.te;
|
||
int idx = grid.itemControllers != null ? System.Array.IndexOf(grid.itemControllers, __instance) : -1;
|
||
if (Taking && te != null && idx >= 0)
|
||
{
|
||
WolfBag.TakeFrom(te, idx);
|
||
}
|
||
else if (Taking && _viewBag != null && grid is XUiC_BagContainer && idx >= 0)
|
||
{
|
||
WolfBag.TakeFrom(_viewBag, idx);
|
||
}
|
||
else
|
||
{
|
||
Deny();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|