ZombieBag: своя сумка на одно вселение у зомби с видом от первого лица (число ячеек по виду, пока 2), окно как у сумки волка; пишется в отдельный файл сейва, остаток после вылета выпадает у тела при следующем вселении. Мешок и метка - общие с сумкой волка (WolfBag.DropSack/AddWaypoint). Подсказка Tab - 13 языков. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
787 lines
29 KiB
C#
787 lines
29 KiB
C#
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;
|
||
}
|
||
|
||
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 = Get();
|
||
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(_key), Describe(), Get().GetUsedSlotCount(), Slots));
|
||
}
|
||
}
|
||
|
||
/// <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 = Get();
|
||
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;
|
||
Save();
|
||
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 = Get().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()
|
||
{
|
||
Get().ItemGrid.Sort();
|
||
Save();
|
||
}
|
||
|
||
/// <summary>Переложить ячейку сумки в массив ячеек контейнера (сами ячейки меняются, номера - в _changed).</summary>
|
||
static bool Give(int _idx, ItemStack[] _g, List<int> _changed)
|
||
{
|
||
Bag b = Get();
|
||
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());
|
||
Save();
|
||
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 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;
|
||
}
|
||
|
||
/// <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() && Get().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() && Get().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 && 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;
|
||
if (WolfBag.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.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]);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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.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
|
||
{
|
||
public static void Prefix(EntityAlive __instance)
|
||
{
|
||
if (!__instance.world.IsRemote() && WolfControl.Is(__instance))
|
||
{
|
||
WolfBag.DropFrom(__instance, "погиб");
|
||
}
|
||
}
|
||
}
|
||
}
|