Одержимость: прицел-перекрестие с покраснением, кнопки 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>
This commit is contained in:
AlexCube
2026-10-09 13:31:55 +03:00
co-authored by Claude Opus 5.5
parent 31aa99860c
commit e94e967b8e
7 changed files with 572 additions and 55 deletions
+176 -16
View File
@@ -17,6 +17,11 @@ namespace NecromancerTome
/// переполнены»): звук ui_denied и xuiInventoryFullForPickup;
/// - окно не закрывается из-за дальности до ТЕЛА: XUiC_LootWindow.Update меряет её от персонажа.
/// Подсказку «E: обыскать» рисует управление игрока, а оно в одержимости выключено - выводим сами.
///
/// МЕШКИ (указание 09.10: «лутать жёлтые мешочки, и синие, и красные, падающие с зомби»). Мешок - сущность
/// EntityLootContainer, открывается той же цепочкой замка (LockRequestLocal -> Entity.OnLockResponseLocal), но
/// окно другое - XUiC_BagStorageWindowGroup; для него те же отличия: рюкзак закрыт, ячейки заперты, дальность
/// от крысы. Рюкзак погибшего игрока (EntityBackpack) - не мешок, его не трогаем.
/// </summary>
public static class RatLoot
{
@@ -29,6 +34,41 @@ namespace NecromancerTome
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)
@@ -76,21 +116,39 @@ namespace NecromancerTome
return st;
}
/// <summary>Каждый кадр одержимости крысой: подсказка и E - открыть.</summary>
public static void Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false)
/// <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;
}
TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv);
if (st == null)
{
return;
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))
{
@@ -98,7 +156,7 @@ namespace NecromancerTome
}
if (!_activate)
{
return;
return false;
}
bool locked = st.lockpickFeature != null && st.lockpickFeature.NeedsLockpicking() ||
st.lockFeature != null && st.lockFeature.IsLocked() && !st.lockFeature.IsUserAllowed(Platform.PlatformManager.InternalLocalUserIdentifier) ||
@@ -106,7 +164,7 @@ namespace NecromancerTome
if (locked)
{
Audio.Manager.BroadcastPlayByLocalPlayer(pos.ToVector3() + Vector3.one * 0.5f, "Misc/locked");
return;
return true;
}
Viewing = true;
Taking = _take;
@@ -116,7 +174,8 @@ namespace NecromancerTome
// Через отражение: у 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);
Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает " + bv.Block.GetBlockName() + " " + pos);
return true;
}
static System.Reflection.MethodInfo _lockRequest;
@@ -141,12 +200,17 @@ namespace NecromancerTome
LocalPlayerUI ui = LocalPlayerUI.GetUIForPrimaryPlayer();
ui?.windowManager.CloseAllOpenModalWindows();
Viewing = false;
_viewBag = null;
_spareUntil = Time.time + 5f;
}
static void LockSlots(XUiC_LootWindow _w, bool _lock)
{
XUiC_LootContainer grid = _w?.lootContainer;
LockSlots(_w?.lootContainer, _lock);
}
static void LockSlots(XUiC_ItemStackGrid grid, bool _lock)
{
if (grid?.itemControllers == null)
{
return;
@@ -263,6 +327,90 @@ namespace NecromancerTome
}
}
/// <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
{
@@ -277,6 +425,10 @@ namespace NecromancerTome
{
WolfBag.TakeAll(te);
}
else if (Taking && _viewBag != null)
{
WolfBag.TakeAll(_viewBag);
}
else
{
Deny();
@@ -300,21 +452,29 @@ namespace NecromancerTome
{
public static void Postfix(XUiC_ItemStack __instance)
{
if (!Viewing || !__instance.isOver || __instance.ItemStack == null || __instance.ItemStack.IsEmpty() ||
__instance.GetParentByType<XUiC_LootContainer>() == null)
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))
{
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;
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();