Одержимость питомцев: роли медведя, волка, собаки и крысы; ноша стервятника; череп при обыске
- Могильная крыса: своя модель без волчьей шерсти; стоит на земле (центр капсулы); E - осмотр контейнеров без права взять (окно игры, рюкзак скрыт, отказ «инвентарь заполнен», контейнер не разрушается); ПКМ - проход сквозь постройки, полупрозрачная. - Стервятник: курица в когтях - слепок с обвисшими крыльями и шеей, без хитбокса. - Зомбособака: нюх - все существа рядом на компасе и карте. - Медведь: толстая шкура (вдвое меньше урона), рёв-провокация, удар лапой по блокам. - Лютоволк: бросок на цель с опрокидыванием, след крови раненых, сумка на 3 ячейки (лут, выгрузка у тела, выпадает мешком при гибели и отзыве; отдельный файл сейва). - Череп (останки): 2% при обычном обыске красного трупа вместо браслета. - Прицел книги не рисуется в одержимости; подсказки вселения на 13 языках. - Замер EntityLeakProbe: удалённые, но живые в памяти сущности - в лог. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
beab1640b0
commit
31aa99860c
@@ -0,0 +1,258 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Сумка одержимого лютоволка на 3 ячейки (решено 09.10, BACKLOG «Медведь и лютоволк в одержимости»):
|
||||
/// - волк лутает: окно контейнера то же, что у крысы (RatLoot, рюкзак персонажа не показывается),
|
||||
/// но щелчок по предмету (и «Взять всё») кладёт его в сумку волка; места нет - «Сумка полна»;
|
||||
/// - выгрузка - принести к телу: рядом с телом E перекладывает сумку в рюкзак персонажа, что не
|
||||
/// влезло - мешком на землю рядом;
|
||||
/// - волк погиб или отозван - сумка выпадает мешком на месте.
|
||||
///
|
||||
/// ХРАНЕНИЕ. Сумку сущности игра не сохраняет (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;
|
||||
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()
|
||||
{
|
||||
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 src = _te.ItemGrid.GetItem(_idx);
|
||||
if (src == null || src.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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 false;
|
||||
}
|
||||
ItemStack rest = src.Clone();
|
||||
rest.count = moving.count;
|
||||
_te.UpdateSlot(_idx, rest.count > 0 ? rest : ItemStack.Empty.Clone());
|
||||
_te.SetModified();
|
||||
_te.Parent?.NotifyListeners(); // окно контейнера перерисует ячейку (как ванильная сортировка)
|
||||
Save();
|
||||
Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1");
|
||||
Show("necroWolfBagTook");
|
||||
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>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);
|
||||
Debug.Log("[NecromancerTome] WolfBag: волк " + _why + " - сумка выпала мешком (" + items.Count + ")");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
[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, "погиб");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user