Files
necromants-tome-7d2d-3-2/HarmonySrc/RatLoot.cs
T
AlexCubeandClaude Opus 5.5 31aa99860c Одержимость питомцев: роли медведя, волка, собаки и крысы; ноша стервятника; череп при обыске
- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы);
  E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь
  заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная.
- Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса.
- Зомбособака: нюх - все существа рядом на компасе и карте.
- Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам.
- Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки
  (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва).
- Череп (останки): 2% при обычном обыске красного трупа вместо браслета.
- Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках.
- Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:58:40 +03:00

327 lines
12 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: обыскать» рисует управление игрока, а оно в одержимости выключено - выводим сами.
/// </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>Контейнер перед крысой по лучу камеры: блок с хранилищем ближе 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 - открыть.</summary>
public static void Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false)
{
LocalPlayerUI ui = _player.PlayerUI;
if (ui == null || Viewing || ui.windowManager.IsModalWindowOpen())
{
return;
}
TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv);
if (st == null)
{
return;
}
PlayerActionsLocal input = _player.playerInput;
string key = input.Activate.GetBindingXuiMarkupString() + input.PermanentActions.Activate.GetBindingXuiMarkupString();
string text = st.GetActivationText(_world, pos, bv, _rat, key, bv.Block.GetLocalizedBlockName());
if (!string.IsNullOrEmpty(text))
{
XUiC_InteractionPrompt.SetText(ui, text);
}
if (!_activate)
{
return;
}
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;
}
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: крыса осматривает " + bv.Block.GetBlockName() + " " + pos);
}
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;
_spareUntil = Time.time + 5f;
}
static void LockSlots(XUiC_LootWindow _w, bool _lock)
{
XUiC_LootContainer grid = _w?.lootContainer;
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;
}
}
}
[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
{
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() ||
__instance.GetParentByType<XUiC_LootContainer>() == null)
{
return;
}
CursorControllerAbs cur = __instance.xui.playerUI.CursorController;
if (cur.GetMouseButtonDown(UICamera.MouseButton.LeftButton) || cur.GetMouseButtonDown(UICamera.MouseButton.RightButton))
{
XUiC_LootContainer grid = __instance.GetParentByType<XUiC_LootContainer>();
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
{
Deny();
}
}
}
}
}
}