Files
necromants-tome-7d2d-3-2/HarmonySrc/WolfBag.cs
T

862 lines
32 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 System.Collections.Generic;
using System.IO;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Сумка одержимого лютоволка на 3 ячейки (решено 09.10, BACKLOG «Медведь и лютоволк в одержимости»):
/// - волк лутает: окно контейнера то же, что у крысы (RatLoot, рюкзак персонажа не показывается),
/// но щелчок по предмету (и «Взять всё») кладёт его в сумку волка; места нет - «Сумка полна»;
/// - выгрузка - принести к телу: рядом с телом E перекладывает сумку в рюкзак персонажа, что не
/// влезло - мешком на землю рядом;
/// - волк погиб или отозван - сумка выпадает мешком на месте; при отзыве (указание 09.10) на карте метка
/// «Сумка волка», чтобы за ним прийти; мешок подобран или истлел - метка снята (Patch_removeBackpack);
/// - «багажник» (указание 09.10: «почти как у пространственного хранилища»): E не на контейнер и не у тела -
/// окно сумки, то же ванильное окно, что у браслета (XUiC_BagStorageWindowGroup, шаблон roboticDrone), но
/// без рюкзака персонажа: внутри перекладывать, вытащить за окно - выбросить у волка (Patch_GetDropPosition);
/// - мешки с зомби волк лутает так же, как ящики (RatLoot, TakeFrom(EntityLootContainer));
/// - тест 10.10: «волк - это ещё одно пространственное хранилище: берёт игрок, но положить может только в эти
/// 3 ячейки» - при обыске рядом с ящиком окно рюкзака показывает сумку волка (WolfBagBackpack: первые 3
/// ячейки, остальные скрыты, ячейки заперты - перекладка щелчком: по ящику - в сумку, по сумке - в ящик,
/// PutTo), E на предмет на земле кладёт его в сумку (TryPickup), Tab в волке - окно сумки, не рюкзак
/// персонажа (Possession: windowpaging закрывается).
///
/// ХРАНЕНИЕ. Сумку сущности игра не сохраняет (Entity.bag нет в Entity.Write), поэтому она своя.
/// Упаковка - та же, что у пространственного хранилища (SpatialVaultPersistence.BuildPayload/ParsePayload,
/// проверена на сейвах 3.2 и 3.3), но в ОТДЕЛЬНОМ файле папки сейва, как у Чертогов разума
/// (NecromancerTome_palaces.txt): файл игрока не меняется (сейвы - главный риск мода). Пишется сразу
/// при каждом изменении - переживает и сохранение, и вылет. Волк у игрока один (один спутник за раз),
/// поэтому сумка одна, без привязки к номеру сущности.
/// </summary>
public static class WolfBag
{
public const int Slots = 3;
const string FileName = "NecromancerTome_wolfbag.bin";
public const float UnloadRange = 3f;
/// <summary>Метка выпавшей сумки: имя - ключ локализации, по нему и находим свою метку среди меток игрока.</summary>
public const string WaypointName = "necroWolfBagWaypoint";
public const string WaypointIcon = "ui_game_symbol_backpack";
/// <summary>Открыто окно сумки («багажник»).</summary>
public static bool WindowOpen;
/// <summary>Счётчик изменений сумки - окно рюкзака перерисовывает её по нему.</summary>
public static int Version;
/// <summary>Предмет на земле - не дальше стольких метров от волка и градусов от луча камеры.</summary>
const float PickupReach = 2.5f;
const float PickupAimDeg = 25f;
static Bag _bag;
static string _dir;
static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName);
/// <summary>Сумка текущего сейва (из файла при первом обращении в этом сейве).</summary>
public static Bag Get()
{
string dir = GameIO.GetSaveGameDir();
if (_bag != null && _dir == dir)
{
return _bag;
}
_dir = dir;
_bag = null;
try
{
if (File.Exists(FilePath))
{
_bag = SpatialVaultPersistence.ParsePayload(File.ReadAllBytes(FilePath));
}
}
catch (System.Exception e)
{
Debug.LogWarning("[NecromancerTome] WolfBag: файл сумки не прочитан (" + e.Message + ") - оставлен как есть, сумка пустая");
_bag = new Bag(new Vector2i(Slots, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
return _bag;
}
if (_bag == null)
{
_bag = new Bag(new Vector2i(Slots, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
}
else if (_bag.SlotCount < Slots)
{
_bag.Resize(new Vector2i(Slots, 1));
}
Debug.Log("[NecromancerTome] WolfBag: сумка загружена - " + Describe());
return _bag;
}
/// <summary>Сумка, куда идёт лут сейчас (13.10): одержимого зомби (ZombieBag), если он с сумкой, иначе волка.
/// Обыск, подбор с земли, окно рюкзака при обыске и подсказки - общие, разница только здесь.</summary>
public static Bag Target => ZombieBag.Active ? ZombieBag.Current : Get();
public static int TargetSlots => ZombieBag.Active ? ZombieBag.Current.SlotCount : Slots;
static void SaveTarget()
{
if (ZombieBag.Active)
{
Version++;
ZombieBag.SaveNow();
return;
}
Save();
}
static void Save()
{
Version++;
try
{
if (_bag == null || _bag.IsEmpty())
{
if (File.Exists(FilePath))
{
File.Delete(FilePath);
}
return;
}
File.WriteAllBytes(FilePath, SpatialVaultPersistence.BuildPayload(_bag));
}
catch (System.Exception e)
{
Debug.LogWarning("[NecromancerTome] WolfBag: сумка не записана - " + e.Message);
}
}
public static string Describe()
{
Bag b = Target;
List<string> parts = new List<string>();
foreach (ItemStack s in b.ItemGrid.items)
{
if (s != null && !s.IsEmpty())
{
parts.Add(s.itemValue.ItemClass.GetLocalizedItemName() + (s.count > 1 ? " x" + s.count : ""));
}
}
return parts.Count == 0 ? Localization.Get("necroWolfBagEmpty") : string.Join(", ", parts);
}
static void Show(string _key)
{
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
if (p != null)
{
GameManager.ShowTooltip(p, string.Format(Localization.Get(ZombieBag.Active ? _key.Replace("necroWolfBag", "necroZombieBag") : _key), Describe(), Target.GetUsedSlotCount(), TargetSlots));
}
}
/// <summary>Переложить ячейку контейнера в сумку. Без раздвоения: сперва в неполные стопки, остаток -
/// в пустую ячейку; в контейнере остаётся ровно то, что не влезло.</summary>
public static bool TakeFrom(TEFeatureStorage _te, int _idx)
{
ItemStack rest = Take(_te.ItemGrid.GetItem(_idx));
if (rest == null)
{
return false;
}
_te.UpdateSlot(_idx, rest);
_te.SetModified();
_te.Parent?.NotifyListeners(); // окно контейнера перерисует ячейку (как ванильная сортировка)
return true;
}
/// <summary>То же из мешка, упавшего с зомби. Окно мешка перерисуется по событию сумки (SetSlot).</summary>
public static bool TakeFrom(EntityLootContainer _src, int _idx)
{
if (_idx >= _src.bag.SlotCount)
{
return false;
}
ItemStack rest = Take(_src.bag.ItemGrid.items[_idx]);
if (rest == null)
{
return false;
}
_src.bag.SetSlot(_idx, rest);
_src.OnBagModified(); // на клиенте - отправить сумку мешка серверу, как делает окно
return true;
}
/// <summary>Переложить стопку в сумку; что осталось в источнике (пустая стопка, если всё), или null - не влезло.</summary>
static ItemStack Take(ItemStack src)
{
if (src == null || src.IsEmpty())
{
return null;
}
Bag b = Target;
ItemStack moving = src.Clone();
int before = moving.count;
b.TryStackItem(0, moving);
if (moving.count > 0 && b.AddItem(moving.Clone()))
{
moving.count = 0;
}
int moved = before - moving.count;
if (moved <= 0)
{
Audio.Manager.PlayInsidePlayerHead("ui_denied");
Show("necroWolfBagFull");
return null;
}
ItemStack rest = src.Clone();
rest.count = moving.count;
SaveTarget();
Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1");
Show("necroWolfBagTook");
Debug.Log("[NecromancerTome] WolfBag: в сумку " + src.itemValue.ItemClass.GetItemName() + " x" + moved + " - " + Describe());
return rest.count > 0 ? rest : ItemStack.Empty.Clone();
}
/// <summary>Ячейку сумки - в ящик (щелчок по сумке в окне рюкзака при обыске, тест 10.10: «выложить в ящик не
/// получается - это неправильно»). Сперва в неполные стопки, остаток - в пустые ячейки; что не влезло -
/// остаётся в сумке.</summary>
public static bool PutTo(TEFeatureStorage _te, int _idx)
{
ItemStack[] g = _te.ItemGrid.items;
List<int> changed = new List<int>();
if (!Give(_idx, g, changed))
{
return false;
}
foreach (int i in changed)
{
_te.UpdateSlot(i, g[i].Clone());
}
_te.SetModified();
_te.Parent?.NotifyListeners();
return true;
}
/// <summary>То же в мешок с зомби.</summary>
public static bool PutTo(EntityLootContainer _dst, int _idx)
{
ItemStack[] g = _dst.bag.ItemGrid.items;
List<int> changed = new List<int>();
if (!Give(_idx, g, changed))
{
return false;
}
foreach (int i in changed)
{
_dst.bag.SetSlot(i, g[i].Clone());
}
_dst.OnBagModified();
return true;
}
/// <summary>Кнопки окна сумки при обыске (указание 12.10): «Положить всё» и «Положить похожие» - в открытый
/// ящик или мешок; похожие - только то, что в контейнере уже лежит.</summary>
public static void PutAll(TEFeatureStorage _te, EntityLootContainer _bag, bool _similarOnly)
{
bool any = false;
ItemStack[] slots = Target.ItemGrid.items;
for (int i = 0; i < slots.Length; i++)
{
if (slots[i] == null || slots[i].IsEmpty())
{
continue;
}
ItemStack[] dst = _te != null ? _te.ItemGrid.items : _bag?.bag.ItemGrid.items;
if (dst == null || (_similarOnly && System.Array.FindIndex(dst, s => s != null && !s.IsEmpty() && s.itemValue.type == slots[i].itemValue.type) < 0))
{
continue;
}
any |= _te != null ? PutTo(_te, i) : PutTo(_bag, i);
}
if (!any)
{
Audio.Manager.PlayInsidePlayerHead("ui_denied");
}
}
/// <summary>«Сортировать» - по самой сумке.</summary>
public static void Sort()
{
Target.ItemGrid.Sort();
SaveTarget();
}
/// <summary>Переложить ячейку сумки в массив ячеек контейнера (сами ячейки меняются, номера - в _changed).</summary>
static bool Give(int _idx, ItemStack[] _g, List<int> _changed)
{
Bag b = Target;
ItemStack src = _idx < b.SlotCount ? b.ItemGrid.items[_idx] : null;
if (src == null || src.IsEmpty())
{
return false;
}
int left = src.count;
int max = src.itemValue.ItemClass?.Stacknumber.Value ?? 1;
for (int i = 0; i < _g.Length && left > 0 && max > 1; i++)
{
if (_g[i] != null && !_g[i].IsEmpty() && _g[i].itemValue.type == src.itemValue.type && _g[i].count < max)
{
int n = Mathf.Min(left, max - _g[i].count);
_g[i] = _g[i].Clone();
_g[i].count += n;
left -= n;
_changed.Add(i);
}
}
for (int i = 0; i < _g.Length && left > 0; i++)
{
if (_g[i] == null || _g[i].IsEmpty())
{
_g[i] = new ItemStack(src.itemValue.Clone(), left);
left = 0;
_changed.Add(i);
}
}
int moved = src.count - left;
if (moved <= 0)
{
Audio.Manager.PlayInsidePlayerHead("ui_denied");
return false;
}
string name = src.itemValue.ItemClass?.GetItemName(); // до SetSlot: src - сама ячейка сумки, очистится
ItemStack rest = src.Clone();
rest.count = left;
b.SetSlot(_idx, left > 0 ? rest : ItemStack.Empty.Clone());
SaveTarget();
Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1");
Debug.Log("[NecromancerTome] WolfBag: из сумки в контейнер " + name + " x" + moved + " - " + Describe());
return true;
}
/// <summary>E на предмет, лежащий на земле, - в сумку (как игрок подбирает: что не влезло, остаётся лежать).
/// True - предмет перед волком был (взят или «сумка полна»).</summary>
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 TryPickupBlock(_world, _player, cam, from, _wolf);
}
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;
}
/// <summary>E на подбираемую «природную мелочь» мира - камешек rock01 и т. п. (тест 13.10: «лутающие одержимые и
/// питомцы не могут подбирать предметы, например камни»): блок с CanPickup, что персонаж поднимает по E, - в
/// сумку (что выдаёт - Block.OnBlockPickedUp, как у игры), блок убрать. Только IsTerrainDecoration: CanPickup
/// есть и у построек игрока (каркасы, фонари, котелки) - их по E не утаскиваем. Чужой участок - как у игры
/// (CanPickupBlockAt). Ближайший к лучу камеры в PickupReach от того, кто подбирает.</summary>
static bool TryPickupBlock(World _world, EntityPlayerLocal _player, Transform _cam, Vector3 _from, EntityAlive _who)
{
Vector3i c = World.worldToBlockPos(_who.position);
int r = Mathf.CeilToInt(PickupReach);
Vector3i best = Vector3i.zero;
BlockValue bestBv = BlockValue.Air;
float bestDeg = PickupAimDeg;
for (int x = -r; x <= r; x++)
for (int y = -1; y <= 2; y++)
for (int z = -r; z <= r; z++)
{
Vector3i p = new Vector3i(c.x + x, c.y + y, c.z + z);
BlockValue bv = _world.GetBlock(p);
if (bv.isair || bv.ischild)
{
continue;
}
Block b = bv.Block;
if (b == null || !b.CanPickup || !b.IsTerrainDecoration)
{
continue;
}
Vector3 at = p.ToVector3() + new Vector3(0.5f, 0.3f, 0.5f);
if ((at - _who.position).sqrMagnitude > PickupReach * PickupReach)
{
continue;
}
float deg = Vector3.Angle(_cam.forward, at - _from);
if (deg < bestDeg)
{
bestDeg = deg;
best = p;
bestBv = bv;
}
}
if (bestBv.isair || !_world.CanPickupBlockAt(best, GameManager.Instance.GetPersistentLocalPlayer()))
{
return false;
}
ItemStack st = bestBv.Block.OnBlockPickedUp(_world, best, bestBv, _player.entityId);
ItemStack rest = Take(st);
if (rest == null)
{
return true;
}
if (rest.IsEmpty())
{
_world.SetBlockRPC(best, BlockValue.Air);
Debug.Log("[NecromancerTome] WolfBag: поднят блок " + bestBv.Block.GetBlockName() + " " + best);
}
return true;
}
/// <summary>«Взять всё» - по ячейкам, пока есть место.</summary>
public static void TakeAll(TEFeatureStorage _te)
{
bool any = false;
for (int i = 0; i < _te.ItemGrid.items.Length; i++)
{
if (!_te.ItemGrid.items[i].IsEmpty() && Target.CanTakeItem(_te.ItemGrid.items[i]))
{
any |= TakeFrom(_te, i);
}
}
if (!any)
{
Audio.Manager.PlayInsidePlayerHead("ui_denied");
Show("necroWolfBagFull");
}
}
/// <summary>«Взять всё» из мешка - по ячейкам, пока есть место.</summary>
public static void TakeAll(EntityLootContainer _src)
{
bool any = false;
ItemStack[] slots = _src.bag.ItemGrid.items;
for (int i = 0; i < slots.Length; i++)
{
if (!slots[i].IsEmpty() && Target.CanTakeItem(slots[i]))
{
any |= TakeFrom(_src, i);
}
}
if (!any)
{
Audio.Manager.PlayInsidePlayerHead("ui_denied");
Show("necroWolfBagFull");
}
}
/// <summary>«Багажник»: окно сумки, как у пространственного браслета (шаблон дрона), без рюкзака персонажа.
/// Перекладка внутри пишется сразу (Save на каждое изменение и на закрытие).</summary>
public static void OpenWindow(EntityPlayerLocal _player, EntityAlive _wolf)
{
LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player);
if (ui == null)
{
return;
}
WindowOpen = true;
XUiC_BagStorageWindowGroup.Open(ui.xui, _wolf, Get(), LootContainer.GetLootContainer("roboticDrone"),
Localization.Get("necroWolfBagWindowTitle"), Save, () =>
{
WindowOpen = false;
Save();
});
Debug.Log("[NecromancerTome] WolfBag: окно сумки - " + Describe());
}
/// <summary>Выход из одержимости при открытом окне сумки - закрыть его.</summary>
public static void CloseWindow()
{
if (WindowOpen)
{
LocalPlayerUI.GetUIForPrimaryPlayer()?.windowManager.CloseAllOpenModalWindows();
WindowOpen = false;
}
}
/// <summary>E рядом с телом: сумка - в рюкзак, что не влезло - мешком у тела.</summary>
public static bool TryUnload(EntityPlayerLocal _body, EntityAlive _wolf)
{
if (_body == null || (_body.position - _wolf.position).sqrMagnitude > UnloadRange * UnloadRange)
{
return false;
}
Bag b = Get();
if (b.IsEmpty())
{
Show("necroWolfBagShow");
return true;
}
List<ItemStack> left = new List<ItemStack>();
ItemStack[] slots = b.ItemGrid.items;
for (int i = 0; i < slots.Length; i++)
{
ItemStack s = slots[i]?.Clone();
if (s == null || s.IsEmpty())
{
continue;
}
_body.bag.TryStackItem(0, s);
if (s.count > 0 && _body.bag.AddItem(s.Clone()))
{
s.count = 0;
}
if (s.count > 0)
{
left.Add(s);
}
}
b.Clear();
Save();
if (left.Count > 0)
{
Drop(_body.position + _body.GetForwardVector() * 0.8f, left);
}
Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1");
GameManager.ShowTooltip(_body, Localization.Get(left.Count > 0 ? "necroWolfBagUnloadedPart" : "necroWolfBagUnloaded"));
Debug.Log("[NecromancerTome] WolfBag: выгружена у тела" + (left.Count > 0 ? ", не влезло " + left.Count + " - мешком" : ""));
return true;
}
/// <summary>Волк погиб или отозван - сумка мешком на месте.</summary>
public static void DropFrom(EntityAlive _wolf, string _why)
{
if (_wolf == null || !WolfControl.Is(_wolf))
{
return;
}
Bag b = Get();
if (b.IsEmpty())
{
return;
}
List<ItemStack> items = new List<ItemStack>();
foreach (ItemStack s in b.ItemGrid.items)
{
if (s != null && !s.IsEmpty())
{
items.Add(s.Clone());
}
}
b.Clear();
Save();
Drop(_wolf.position, items);
if (_why == "отозван")
{
AddWaypoint(_wolf.position);
}
Debug.Log("[NecromancerTome] WolfBag: волк " + _why + " - сумка выпала мешком (" + items.Count + ")");
}
/// <summary>Метка на карте и компасе - как игра ставит метку транспорта (XUiC_MapArea.RefreshVehiclePositionWaypoint),
/// но обычная, не «последнее место сущности»: номер мешка после перезахода другой. Метка - в списке меток игрока,
/// сохраняется игрой вместе с ним; удалить можно и вручную.</summary>
public static void AddWaypoint(Vector3 _at, string _name = WaypointName)
{
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
if (p?.Waypoints == null)
{
return;
}
Waypoint w = new Waypoint();
w.pos = World.worldToBlockPos(_at);
w.icon = WaypointIcon;
w.ownerId = Platform.PlatformManager.InternalLocalUserIdentifier;
w.name.Update(_name, Platform.PlatformManager.MultiPlatform.User.PlatformUserId);
w.bUsingLocalizationId = true;
p.Waypoints.Collection.Add(w);
w.navObject = NavObjectManager.Instance.RegisterNavObject("waypoint", w.pos.ToVector3(), w.icon);
w.navObject.IsActive = false;
w.navObject.name = w.name.Text;
w.navObject.usingLocalizationId = true;
Debug.Log("[NecromancerTome] WolfBag: метка сумки на карте " + w.pos);
}
/// <summary>Мешок исчез (опустел или истлел) - снять метку сумки, стоящую рядом с ним.</summary>
public static void RemoveWaypointAt(Vector3 _at)
{
EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer();
if (p?.Waypoints == null)
{
return;
}
List<Waypoint> list = p.Waypoints.Collection.list;
for (int i = list.Count - 1; i >= 0; i--)
{
Waypoint w = list[i];
if ((w.name.Text == WaypointName || w.name.Text == ZombieBag.WaypointName) && (w.pos.ToVector3() - _at).sqrMagnitude < 3f * 3f)
{
p.Waypoints.Collection.Remove(w);
if (w.navObject != null)
{
NavObjectManager.Instance.UnRegisterNavObject(w.navObject);
}
Debug.Log("[NecromancerTome] WolfBag: мешок сумки подобран - метка снята " + w.pos);
}
}
}
/// <summary>Мешок с предметами на земле (и для сумки одержимого зомби, ZombieBag).</summary>
public static void DropSack(Vector3 _at, List<ItemStack> _items) => Drop(_at, _items);
static void Drop(Vector3 _at, List<ItemStack> _items)
{
GameManager.Instance.DropContentInLootContainerServer(-1, "DroppedLootContainer", _at + Vector3.up * 0.5f, _items.ToArray());
}
/// <summary>Выход из мира - забыть (следующий сейв прочитает свой файл).</summary>
public static void Forget()
{
_bag = null;
_dir = null;
}
}
/// <summary>Мешок исчез (пустой после обыска - «empty», или истлел - «old») - снять метку сумки волка рядом.</summary>
[HarmonyPatch(typeof(EntityContainerAbs), nameof(EntityContainerAbs.removeBackpack))]
public static class Patch_EntityContainerAbs_removeBackpack_WolfBag
{
public static void Postfix(EntityContainerAbs __instance)
{
if (__instance is EntityLootContainer)
{
WolfBag.RemoveWaypointAt(__instance.position);
}
}
}
/// <summary>«Багажник» без рюкзака персонажа: окно мешка открывает его само (OnOpen), закрываем сразу же.</summary>
[HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))]
public static class Patch_XUiC_BagStorageWindowGroup_OnOpen_WolfBag
{
public static void Postfix(XUiC_BagStorageWindowGroup __instance)
{
bool wolf = (WolfBag.WindowOpen || ZombieBag.WindowOpen) && Possession.Active;
if (wolf)
{
__instance.xui.playerUI.windowManager.Close("backpack");
}
// «Взять всё» / «Взять похожие» кладут в рюкзак тела, а оно далеко - у волка кнопок нет; у дрона и браслета - вернуть
XUiC_ContainerStandardControls c = __instance.GetChildByType<XUiC_BagContainer>()?.standardControls;
foreach (string id in WolfBagControls.Hidden)
{
XUiController b = c?.GetChildById(id);
if (b?.ViewComponent != null)
{
b.ViewComponent.IsVisible = !wolf;
}
}
}
}
/// <summary>Окно сумки волка: работает только «Сортировать» (по самой сумке, пишется через onModified).
/// Перенос в рюкзак (кнопки и клавиша R) и замки ячеек (нужны лишь для переноса) - выключены.</summary>
public static class WolfBagControls
{
public static readonly string[] Hidden = { "btnMoveAll", "btnMoveFillAndSmart", "btnMoveFillStacks", "btnMoveSmart" };
public static bool Blocked(XUiController _c)
{
return (WolfBag.WindowOpen || ZombieBag.WindowOpen) && Possession.Active && _c?.GetParentByType<XUiC_BagContainer>() != null;
}
}
[HarmonyPatch(typeof(XUiC_ContainerStandardControls))]
public static class Patch_ContainerStandardControls_Move_WolfBag
{
static IEnumerable<System.Reflection.MethodBase> TargetMethods()
{
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveAll));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillAndSmart));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillStacks));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveSmart));
yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.ToggleLockMode));
}
public static bool Prefix(XUiC_ContainerStandardControls __instance)
{
return !WolfBagControls.Blocked(__instance);
}
}
/// <summary>Кнопка замка видна по привязке {hasslotlocks} - у волка её нет.</summary>
[HarmonyPatch(typeof(XUiC_BagContainer), nameof(XUiC_BagContainer.GetBindingValueInternal))]
public static class Patch_BagContainer_Binding_WolfBag
{
public static void Postfix(XUiC_BagContainer __instance, ref string _value, string _bindingName, ref bool __result)
{
if (_bindingName == "hasslotlocks" && WolfBagControls.Blocked(__instance))
{
_value = "false";
__result = true;
}
}
}
/// <summary>Предмет, вытащенный за окно сумки волка, падает у волка, а не у тела (XUiM_PlayerInventory.DropItem
/// берёт место у игрока - GetDropPosition).</summary>
[HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.GetDropPosition))]
public static class Patch_EntityPlayerLocal_GetDropPosition_WolfBag
{
public static void Postfix(EntityPlayerLocal __instance, ref Vector3 __result)
{
EntityAlive wolf = Possession.Zombie;
// И из сумки одержимого зомби (тест 13.10: «выкидываю камень - на землю не падает» - падал у тела).
if ((WolfBag.WindowOpen || ZombieBag.WindowOpen) && Possession.Active && __instance == Possession.Player && wolf != null)
{
__result = wolf.position + wolf.GetForwardVector() * 0.8f + Vector3.up * 0.5f;
}
}
}
/// <summary>Окно рюкзака при обыске волком показывает сумку волка (указание 10.10). Рюкзак персонажа при этом
/// не читается и не пишется: UpdateBackend пропущен - в сейв игрока сумка не попадёт.</summary>
public static class WolfBagBackpack
{
public static bool On => RatLoot.Viewing && RatLoot.Taking && Possession.Active;
static int _seen = -1;
static readonly List<XUiC_ItemStack> _hidden = new List<XUiC_ItemStack>();
public static ItemStack[] Slots(int _n)
{
ItemStack[] a = new ItemStack[_n];
ItemStack[] b = WolfBag.Target.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.TargetSlots && c[i].ViewComponent.IsVisible)
{
c[i].ViewComponent.IsVisible = false;
_hidden.Add(c[i]);
}
}
}
/// <summary>Обыск закончен: ячейки рюкзака снова видны и отперты, рюкзак - снова свой.</summary>
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.TargetSlots);
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
{
public static void Prefix(EntityAlive __instance)
{
if (!__instance.world.IsRemote() && WolfControl.Is(__instance))
{
WolfBag.DropFrom(__instance, "погиб");
}
}
}
}