Files
AlexCubeandClaude Opus 5.5 cf61d98bb7 Одержимый зомби: оружие на поясе по ремеслу (#37), карта по M (#36)
- Пояс - нижний ряд сумки зомби; оружие ближнего боя в руку, если этот тип зомби
  им владеет (ZombieWeapons: таблица умений, утверждена 25.10). Удар как у персонажа:
  ItemActionAttack.Hit, урон предмета без перков зомби, износ, темп оружия.
  Модель на кость RightWeapon, хват WeaponHoldType; иконки и износ на поясе HUD.
  Неумелое или сломанное - кулаки и подсказка (13 языков). Выключатель
  necroarms weapon on|off; dev-команда necropossess для проверок без мыши.
- M в зомби и волке открывал сумку вместо карты (перехват windowpaging) - исправлено;
  туман карты и карта на одержимом - у любого одержимого зомби, как у стервятника.
- Глава «Одержимость» (EN/RU): пояс - оружие, M - карта.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 14:26:55 +03:00

199 lines
8.3 KiB
C#
Raw Permalink 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 UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Сумка одержимого зомби (указание 13.10: «всем одержимым зомби с видом от первого лица сумку и возможность вызывать
/// её по Tab»). Решения пользователя 13.10:
/// - своя, одна на всех зомби, число ячеек - по виду зомби (SlotsFor), пока у всех 2;
/// - при выходе из одержимости зомби сбрасывает рюкзаком всё, что было в сумке (и на поясе - когда в поясе
/// появятся предметы);
/// - гибель одержимого - мешок на месте и метка на карте, как у сумки волка.
/// ПОЯС (#37, 25.10) - нижний ряд той же сумки: Possession.BeltSlots ячеек под верхним рядом, оружие в руке берётся
/// оттуда (ZombieWeapons). Хранение, выброс мешком и метка - общие с сумкой.
/// Лут (указание 13.10: «раз у них E свободно, подключай лутание») - как у волка: E на ящик или мешок - обыск с
/// переносом в сумку, E на предмет на земле - подобрать (WolfBag.Target - сумка зомби, пока она есть).
/// Только зомби из списка вида от первого лица (FpArms.Allows), не питомцы. Окно - то же, что у сумки волка
/// (XUiC_BagStorageWindowGroup, без рюкзака персонажа), Tab в зомби открывает его вместо рюкзака.
///
/// ХРАНЕНИЕ. Сумка живёт одно вселение, но пишется в отдельный файл сейва при каждом изменении (как сумка волка,
/// сейв игрока не трогается): вылет посреди вселения вещи не теряет - при следующем вселении то, что осталось в
/// файле, выпадает мешком у тела.
/// </summary>
public static class ZombieBag
{
const string FileName = "NecromancerTome_zombiebag.bin";
public const int DefaultSlots = 2;
/// <summary>Метка выпавшей сумки - ключ локализации (снимается вместе с меткой сумки волка, когда мешок исчез).</summary>
public const string WaypointName = "necroZombieBagWaypoint";
public static bool WindowOpen;
static Bag _bag;
/// <summary>Ячеек в ряду: верхний ряд - сумка, нижний - пояс.</summary>
static int _cols;
static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName);
public static bool Active => _bag != null;
public static Bag Current => _bag;
/// <summary>Ячейка пояса (0..BeltSlots-1) - нижний ряд сумки; нет сумки - null.</summary>
public static ItemStack Belt(int _i)
{
ItemStack[] items = _bag?.ItemGrid?.items;
int idx = _cols + _i;
return items != null && _i >= 0 && _i < Possession.BeltSlots && idx < items.Length && !items[idx].IsEmpty() ? items[idx] : null;
}
/// <summary>necropossess belt: положить предмет в ячейку пояса (проверка без мыши).</summary>
public static bool DevSetBelt(int _i, ItemStack _s)
{
if (_bag == null || _i < 0 || _i >= Possession.BeltSlots)
{
return false;
}
_bag.SetSlot(_cols + _i, _s);
Save();
return true;
}
/// <summary>Предмет пояса изменился на месте (износ) - окно и файл.</summary>
public static void BeltChanged()
{
_bag?.onBackpackChanged();
Save();
}
/// <summary>Записать после лута (WolfBag.SaveTarget).</summary>
public static void SaveNow() => Save();
/// <summary>У кого сумка: зомби с видом от первого лица, не питомцы.</summary>
public static bool Has(EntityAlive _z, bool _pet) => !_pet && FpArms.Allows(_z);
/// <summary>Ячеек у этого вида зомби (указание: «каким-то добавим, каким-то наоборот») - пока у всех DefaultSlots.</summary>
public static int SlotsFor(EntityAlive _z) => DefaultSlots;
/// <summary>Вселение началось: то, что осталось в файле после вылета, - мешком у тела; новая пустая сумка.</summary>
public static void Begin(EntityPlayerLocal _body, EntityAlive _z)
{
try
{
if (File.Exists(FilePath))
{
Bag old = SpatialVaultPersistence.ParsePayload(File.ReadAllBytes(FilePath));
List<ItemStack> left = Items(old);
if (left.Count > 0 && _body != null)
{
WolfBag.DropSack(_body.position + _body.GetForwardVector() * 0.8f, left);
Debug.Log("[NecromancerTome] ZombieBag: в файле осталась сумка прошлого вселения - мешком у тела (" + left.Count + ")");
}
File.Delete(FilePath);
}
}
catch (System.Exception e)
{
Debug.LogWarning("[NecromancerTome] ZombieBag: файл сумки не прочитан (" + e.Message + ") - оставлен как есть");
}
_cols = Mathf.Max(SlotsFor(_z), Possession.BeltSlots);
_bag = new Bag(new Vector2i(_cols, 2), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
Debug.Log("[NecromancerTome] ZombieBag: сумка " + _z.EntityName + " - ячеек " + _bag.SlotCount + ", из них пояс " + Possession.BeltSlots);
}
/// <summary>Вселение кончилось: всё из сумки - мешком на месте зомби; погиб - ещё и метка на карте.</summary>
public static void End(EntityAlive _z)
{
if (_bag == null)
{
return;
}
CloseWindow();
List<ItemStack> items = Items(_bag);
_bag = null;
Save();
if (items.Count == 0 || _z == null)
{
return;
}
bool died = _z.IsDead();
WolfBag.DropSack(_z.position, items);
if (died)
{
WolfBag.AddWaypoint(_z.position, WaypointName);
}
Debug.Log("[NecromancerTome] ZombieBag: " + (died ? "одержимый погиб" : "выход из одержимости") + " - сумка мешком (" + items.Count + ")");
}
static List<ItemStack> Items(Bag _b)
{
List<ItemStack> items = new List<ItemStack>();
if (_b?.ItemGrid?.items == null)
{
return items;
}
foreach (ItemStack s in _b.ItemGrid.items)
{
if (s != null && !s.IsEmpty())
{
items.Add(s.Clone());
}
}
return items;
}
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] ZombieBag: сумка не записана - " + e.Message);
}
}
/// <summary>Tab в зомби - окно сумки, как «багажник» волка (WolfBag.OpenWindow).</summary>
public static void OpenWindow(EntityPlayerLocal _player, EntityAlive _z)
{
LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player);
if (ui == null || _bag == null)
{
return;
}
WindowOpen = true;
XUiC_BagStorageWindowGroup.Open(ui.xui, _z, _bag, LootContainer.GetLootContainer("roboticDrone"),
Localization.Get("necroZombieBagWindowTitle"), Save, () =>
{
WindowOpen = false;
Save();
});
Debug.Log("[NecromancerTome] ZombieBag: окно сумки");
}
public static void CloseWindow()
{
if (WindowOpen)
{
LocalPlayerUI.GetUIForPrimaryPlayer()?.windowManager.CloseAllOpenModalWindows();
WindowOpen = false;
}
}
/// <summary>Выход из мира - забыть (файл остаётся: при следующем вселении выпадет мешком у тела).</summary>
public static void Forget()
{
_bag = null;
WindowOpen = false;
}
}
}