diff --git a/HarmonySrc/Possession.cs b/HarmonySrc/Possession.cs index 84c7fc2..832b11e 100644 --- a/HarmonySrc/Possession.cs +++ b/HarmonySrc/Possession.cs @@ -667,7 +667,8 @@ namespace NecromancerTome // E: на контейнер или мешок с зомби - лутать в сумку (3 ячейки); у тела - выгрузить сумку в рюкзак; // иначе - «багажник», окно сумки (указание 09.10). bool looted = RatLoot.Tick(world, _player, z, detonate, true); - if (detonate && !looted && !RatLoot.Viewing && !WolfBag.TryUnload(_player, z)) + // E на предмет на земле - в сумку (тест 10.10: «не могу подбирать выброшенные предметы»). + if (detonate && !looted && !RatLoot.Viewing && !WolfBag.TryPickup(world, _player, z) && !WolfBag.TryUnload(_player, z)) { WolfBag.OpenWindow(_player, z); } diff --git a/HarmonySrc/RatLoot.cs b/HarmonySrc/RatLoot.cs index 12c6b33..2f22183 100644 --- a/HarmonySrc/RatLoot.cs +++ b/HarmonySrc/RatLoot.cs @@ -233,7 +233,10 @@ namespace NecromancerTome { return; } - __instance.xui.playerUI.windowManager.Close("backpack"); + if (!Taking) + { + __instance.xui.playerUI.windowManager.Close("backpack"); // у волка рюкзак показывает его сумку (WolfBagBackpack) + } LockSlots(__instance.lootWindow, true); } } @@ -337,7 +340,10 @@ namespace NecromancerTome { return; } - __instance.xui.playerUI.windowManager.Close("backpack"); + if (!Taking) + { + __instance.xui.playerUI.windowManager.Close("backpack"); + } LockSlots(__instance.containerWindow, true); } } diff --git a/HarmonySrc/WolfBag.cs b/HarmonySrc/WolfBag.cs index 9c009c8..06eecd8 100644 --- a/HarmonySrc/WolfBag.cs +++ b/HarmonySrc/WolfBag.cs @@ -16,7 +16,11 @@ namespace NecromancerTome /// - «багажник» (указание 09.10: «почти как у пространственного хранилища»): E не на контейнер и не у тела - /// окно сумки, то же ванильное окно, что у браслета (XUiC_BagStorageWindowGroup, шаблон roboticDrone), но /// без рюкзака персонажа: внутри перекладывать, вытащить за окно - выбросить у волка (Patch_GetDropPosition); - /// - мешки с зомби волк лутает так же, как ящики (RatLoot, TakeFrom(EntityLootContainer)). + /// - мешки с зомби волк лутает так же, как ящики (RatLoot, TakeFrom(EntityLootContainer)); + /// - тест 10.10: «волк - это ещё одно пространственное хранилище: берёт игрок, но положить может только в эти + /// 3 ячейки» - при обыске рядом с ящиком окно рюкзака показывает сумку волка (WolfBagBackpack: первые 3 + /// ячейки, остальные скрыты, ячейки заперты - перекладка щелчком по ящику), а E на предмет на земле + /// кладёт его в сумку (TryPickup). /// /// ХРАНЕНИЕ. Сумку сущности игра не сохраняет (Entity.bag нет в Entity.Write), поэтому она своя. /// Упаковка - та же, что у пространственного хранилища (SpatialVaultPersistence.BuildPayload/ParsePayload, @@ -35,6 +39,11 @@ namespace NecromancerTome public const string WaypointIcon = "ui_game_symbol_backpack"; /// Открыто окно сумки («багажник»). public static bool WindowOpen; + /// Счётчик изменений сумки - окно рюкзака перерисовывает её по нему. + public static int Version; + /// Предмет на земле - не дальше стольких метров от волка и градусов от луча камеры. + const float PickupReach = 2.5f; + const float PickupAimDeg = 25f; static Bag _bag; static string _dir; @@ -77,6 +86,7 @@ namespace NecromancerTome static void Save() { + Version++; try { if (_bag == null || _bag.IsEmpty()) @@ -181,6 +191,52 @@ namespace NecromancerTome return rest.count > 0 ? rest : ItemStack.Empty.Clone(); } + /// E на предмет, лежащий на земле, - в сумку (как игрок подбирает: что не влезло, остаётся лежать). + /// True - предмет перед волком был (взят или «сумка полна»). + public static bool TryPickup(World _world, EntityPlayerLocal _player, EntityAlive _wolf) + { + Transform cam = _player.cameraTransform; + if (cam == null || _world.IsRemote()) + { + return false; + } + Vector3 from = cam.position + Origin.position; + EntityItem best = null; + float bestDeg = PickupAimDeg; + for (int i = 0; i < _world.Entities.list.Count; i++) + { + if (!(_world.Entities.list[i] is EntityItem it) || it.IsDead() || it.IsMarkedForUnload() || it.itemStack == null || + it.itemStack.IsEmpty() || !it.CanCollect() || (it.position - _wolf.position).sqrMagnitude > PickupReach * PickupReach) + { + continue; + } + float deg = Vector3.Angle(cam.forward, it.position - from); + if (deg < bestDeg) + { + bestDeg = deg; + best = it; + } + } + if (best == null) + { + return false; + } + ItemStack rest = Take(best.itemStack); + if (rest == null) + { + return true; + } + if (rest.IsEmpty()) + { + _world.RemoveEntity(best.entityId, EnumRemoveEntityReason.Killed); + } + else + { + best.itemStack.count = rest.count; + } + return true; + } + /// «Взять всё» - по ячейкам, пока есть место. public static void TakeAll(TEFeatureStorage _te) { @@ -423,6 +479,124 @@ namespace NecromancerTome } } + /// Окно рюкзака при обыске волком показывает сумку волка (указание 10.10). Рюкзак персонажа при этом + /// не читается и не пишется: UpdateBackend пропущен - в сейв игрока сумка не попадёт. + public static class WolfBagBackpack + { + public static bool On => RatLoot.Viewing && RatLoot.Taking && Possession.Active; + static int _seen = -1; + static readonly List _hidden = new List(); + + public static ItemStack[] Slots(int _n) + { + ItemStack[] a = new ItemStack[_n]; + ItemStack[] b = WolfBag.Get().ItemGrid.items; + for (int i = 0; i < _n; i++) + { + a[i] = i < b.Length && b[i] != null ? b[i].Clone() : ItemStack.Empty.Clone(); + } + return a; + } + + public static void Tick(XUiC_Backpack _bp) + { + if (_seen != WolfBag.Version || _hidden.Count == 0) + { + _seen = WolfBag.Version; + _bp.RefreshBackpackSlots(); + } + XUiC_ItemStack[] c = _bp.itemControllers; + for (int i = 0; c != null && i < c.Length; i++) + { + c[i].IsLocked = true; + if (i >= WolfBag.Slots && c[i].ViewComponent.IsVisible) + { + c[i].ViewComponent.IsVisible = false; + _hidden.Add(c[i]); + } + } + } + + /// Обыск закончен: ячейки рюкзака снова видны и отперты, рюкзак - снова свой. + public static void Unlock(XUiC_Backpack _bp) + { + foreach (XUiC_ItemStack s in _hidden) + { + if (s != null) + { + s.ViewComponent.IsVisible = true; + } + } + bool was = _hidden.Count > 0 || _seen != -1; + _hidden.Clear(); + _seen = -1; + if (_bp?.itemControllers == null || !was) + { + return; + } + foreach (XUiC_ItemStack s in _bp.itemControllers) + { + if (s != null) + { + s.IsLocked = false; + } + } + _bp.RefreshBackpackSlots(); + } + } + + [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.GetSlots))] + public static class Patch_Backpack_GetSlots_WolfBag + { + public static bool Prefix(XUiC_Backpack __instance, ref ItemStack[] __result) + { + if (!WolfBagBackpack.On) + { + return true; + } + __result = WolfBagBackpack.Slots(__instance.itemControllers?.Length ?? WolfBag.Slots); + return false; + } + } + + [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.UpdateBackend))] + public static class Patch_Backpack_UpdateBackend_WolfBag + { + public static bool Prefix() + { + return !WolfBagBackpack.On; + } + } + + [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.Update))] + public static class Patch_Backpack_Update_WolfBag + { + static bool _was; + + public static void Postfix(XUiC_Backpack __instance) + { + bool on = WolfBagBackpack.On; + if (on) + { + WolfBagBackpack.Tick(__instance); + } + else if (_was) + { + WolfBagBackpack.Unlock(__instance); + } + _was = on; + } + } + + [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.OnClose))] + public static class Patch_Backpack_OnClose_WolfBag + { + public static void Postfix(XUiC_Backpack __instance) + { + WolfBagBackpack.Unlock(__instance); + } + } + [HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.OnEntityDeath))] public static class Patch_EntityAlive_OnEntityDeath_WolfBag {