Files
necromants-tome-7d2d-3-2/HarmonySrc/RatLoot.cs
T
AlexCubeandClaude Opus 5.5 e94e967b8e Одержимость: прицел-перекрестие с покраснением, кнопки E/F, лут мешков и багажник волка, медведь на дыбы
- Прицел всех одержимых - всегда перекрестие (свой 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>
2026-10-09 13:31:55 +03:00

487 lines
19 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 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();
}
}
}
}
}
}