460 lines
13 KiB
C#
460 lines
13 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Sandbox;
|
||
|
||
namespace Sasalka;
|
||
|
||
public class Inventar : Component
|
||
{
|
||
[Flags]
|
||
public enum InventorySlot
|
||
{
|
||
None = 0,
|
||
LeftHand = 1 << 0, // 1
|
||
RightHand = 1 << 1, // 2
|
||
Head = 1 << 2, // 4
|
||
Body = 1 << 3, // 8
|
||
Hands = 1 << 4, // 16
|
||
Bottom = 1 << 5, // 32
|
||
Feet = 1 << 6 // 64
|
||
}
|
||
|
||
[Sync] public IList<InventoryItem> Items { get; set; } = new List<InventoryItem>();
|
||
|
||
[Sync]
|
||
public IDictionary<InventorySlot, InventoryItem> EquippedItems { get; set; } =
|
||
new Dictionary<InventorySlot, InventoryItem>();
|
||
|
||
// Настройка вместимости инвентаря
|
||
[Property] public int MaxInventorySlots { get; set; } = 20; // Максимальное количество слотов
|
||
[Property] public bool UnlimitedSlots { get; set; } = false; // Безлимитный инвентарь
|
||
|
||
public static bool IsInventoryOpen = false;
|
||
|
||
public event Action OnChanged;
|
||
public event Action<InventoryItem> OnEquipped;
|
||
public event Action<InventoryItem> OnUnEquipped;
|
||
public event Action<InventoryItem> OnItemAdded;
|
||
public event Action<InventoryItem> OnItemRemoved;
|
||
|
||
// Кэш для оптимизации поиска предметов
|
||
private Dictionary<BaseItemDefinition, InventoryItem> _itemCache = new();
|
||
private bool _cacheDirty = true;
|
||
|
||
public bool CanAddItem( InventoryItem item )
|
||
{
|
||
if ( item == null || item.Definition == null )
|
||
return false;
|
||
|
||
// Обновляем кэш при необходимости
|
||
if ( _cacheDirty )
|
||
{
|
||
UpdateItemCache();
|
||
}
|
||
|
||
// Проверяем, есть ли уже такой предмет в инвентаре
|
||
if ( _itemCache.TryGetValue( item.Definition, out var existingItem ) )
|
||
{
|
||
// Если предмет уже есть, проверяем, можно ли добавить к нему количество
|
||
return existingItem.Count + item.Count <= item.Definition.MaxCount;
|
||
}
|
||
|
||
// Если предмета нет, проверяем вместимость инвентаря
|
||
if ( !UnlimitedSlots && Items.Count >= MaxInventorySlots )
|
||
{
|
||
return false; // Инвентарь полон
|
||
}
|
||
|
||
// Проверяем только MaxCount для нового предмета
|
||
return item.Count <= item.Definition.MaxCount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновляет кэш предметов
|
||
/// </summary>
|
||
private void UpdateItemCache()
|
||
{
|
||
_itemCache.Clear();
|
||
foreach ( var item in Items )
|
||
{
|
||
if ( item.Definition != null )
|
||
{
|
||
_itemCache[item.Definition] = item;
|
||
}
|
||
}
|
||
|
||
_cacheDirty = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Добавляет предмет в инвентарь, распределяя по существующим и новым стекам. Возвращает остаток, который не удалось добавить (или 0, если всё добавлено).
|
||
/// </summary>
|
||
public int AddItem( InventoryItem item )
|
||
{
|
||
if ( item == null || item.Definition == null || item.Count <= 0 )
|
||
return item.Count;
|
||
|
||
int toAdd = item.Count;
|
||
|
||
// 1. Заполняем существующие стаки
|
||
foreach ( var stack in Items.Where( x => x.Definition == item.Definition && x.Count < x.Definition.MaxCount ) )
|
||
{
|
||
int canAdd = Math.Min( toAdd, stack.Definition.MaxCount - stack.Count );
|
||
if ( canAdd > 0 )
|
||
{
|
||
stack.Count += canAdd;
|
||
toAdd -= canAdd;
|
||
_cacheDirty = true; // Помечаем кэш как устаревший
|
||
OnChanged?.Invoke();
|
||
OnItemAdded?.Invoke( stack );
|
||
}
|
||
|
||
if ( toAdd <= 0 ) return 0;
|
||
}
|
||
|
||
// 2. Добавляем новые стаки, если есть место
|
||
while ( toAdd > 0 && (UnlimitedSlots || Items.Count < MaxInventorySlots) )
|
||
{
|
||
int stackCount = Math.Min( toAdd, item.Definition.MaxCount );
|
||
var newStack = new InventoryItem
|
||
{
|
||
Definition = item.Definition, Count = stackCount, MagazineAmmo = item.MagazineAmmo
|
||
};
|
||
Items.Add( newStack );
|
||
toAdd -= stackCount;
|
||
_cacheDirty = true; // Помечаем кэш как устаревший
|
||
OnChanged?.Invoke();
|
||
OnItemAdded?.Invoke( newStack );
|
||
}
|
||
|
||
// 3. Возвращаем остаток, если не всё удалось добавить
|
||
AutoSave(); // Автоматическое сохранение
|
||
return toAdd;
|
||
}
|
||
|
||
public bool RemoveItem( InventoryItem item, int count = 1 )
|
||
{
|
||
if ( item == null || !Items.Contains( item ) )
|
||
return false;
|
||
|
||
if ( count >= item.Count )
|
||
{
|
||
// Удаляем весь предмет
|
||
UnEquipItem( item );
|
||
Items.Remove( item );
|
||
_cacheDirty = true; // Помечаем кэш как устаревший
|
||
OnChanged?.Invoke();
|
||
OnItemRemoved?.Invoke( item );
|
||
AutoSave(); // Автоматическое сохранение
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
// Уменьшаем количество
|
||
item.Count -= count;
|
||
_cacheDirty = true; // Помечаем кэш как устаревший
|
||
OnChanged?.Invoke();
|
||
AutoSave(); // Автоматическое сохранение
|
||
return true;
|
||
}
|
||
}
|
||
|
||
public bool EquipItem( InventoryItem item )
|
||
{
|
||
if ( item?.Definition is not IEquipable equipable )
|
||
return false;
|
||
|
||
var slot = item.Definition.GetSlot();
|
||
if ( slot == null )
|
||
return false;
|
||
|
||
// Если уже экипирован этот же предмет — снять его
|
||
if ( EquippedItems.Values.Contains( item ) )
|
||
{
|
||
UnEquipItem( item );
|
||
return true;
|
||
}
|
||
|
||
// Если на этом слоте уже что-то есть — снять старый предмет
|
||
if ( EquippedItems.TryGetValue( slot.Value, out var oldItem ) )
|
||
{
|
||
UnEquipItem( oldItem );
|
||
}
|
||
|
||
// Экипировать новый предмет
|
||
EquippedItems[slot.Value] = item;
|
||
item.Equipped = true;
|
||
item.OnEquipped();
|
||
OnEquipped?.Invoke( item );
|
||
OnChanged?.Invoke();
|
||
AutoSave(); // Автоматическое сохранение
|
||
return true;
|
||
}
|
||
|
||
public void DropItem( InventoryItem item, Vector3 position )
|
||
{
|
||
if ( item == null || !Items.Contains( item ) )
|
||
return;
|
||
|
||
// Проверяем, является ли предмет одеждой
|
||
if ( item.Definition is ClothingItemDefinition clothingDef )
|
||
{
|
||
// Для одежды создаем специальный физический объект
|
||
DropClothingItem( item, position, clothingDef );
|
||
}
|
||
else
|
||
{
|
||
// Для остальных предметов используем стандартный префаб
|
||
if ( item.Definition.Prefab != null )
|
||
{
|
||
GameObject gO = item.Definition.Prefab.Clone( position );
|
||
|
||
if ( gO.Components.TryGet<InventoryItem>( out var inventoryItem ) )
|
||
{
|
||
inventoryItem.Count = item.Count;
|
||
inventoryItem.Definition = item.Definition;
|
||
// Копируем патроны из оригинального предмета
|
||
inventoryItem.MagazineAmmo = item.MagazineAmmo;
|
||
}
|
||
|
||
gO.NetworkSpawn();
|
||
}
|
||
else
|
||
{
|
||
Log.Warning( $"Префаб не найден для предмета: {item.Definition.Name}" );
|
||
}
|
||
}
|
||
|
||
// Удаляем весь предмет из инвентаря
|
||
RemoveItem( item, item.Count );
|
||
}
|
||
|
||
/// <summary>
|
||
/// Выбрасывает предмет одежды как физический объект
|
||
/// </summary>
|
||
private void DropClothingItem( InventoryItem item, Vector3 position, ClothingItemDefinition clothingDef )
|
||
{
|
||
// Пытаемся найти подходящий префаб для одежды
|
||
GameObject clothingPrefab = GameObject.GetPrefab( "prefabs/item_parcel.prefab" );
|
||
GameObject clothingObject = null;
|
||
|
||
clothingObject = clothingPrefab.Clone( position );
|
||
|
||
// Добавляем компонент InventoryItem
|
||
if ( clothingObject.Components.TryGet<InventoryItem>( out var inventoryItem ) )
|
||
{
|
||
inventoryItem.Count = item.Count;
|
||
inventoryItem.Definition = item.Definition;
|
||
}
|
||
|
||
// Добавляем компонент PickupItem для подбора
|
||
if ( clothingObject.Components.TryGet<PickupItem>( out var pickupItem ) )
|
||
{
|
||
// Устанавливаем правильную метку для одежды
|
||
var slotName = GetSlotDisplayName( clothingDef.Slot );
|
||
pickupItem.Label = $"{clothingDef.Name} ({slotName})";
|
||
}
|
||
|
||
clothingObject.NetworkSpawn();
|
||
|
||
Log.Info( $"Выброшена одежда: {clothingDef.Name} ({clothingDef.Slot})" );
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Получает отображаемое название слота
|
||
/// </summary>
|
||
private string GetSlotDisplayName( InventorySlot slot )
|
||
{
|
||
return slot switch
|
||
{
|
||
InventorySlot.LeftHand => "Л.Рука",
|
||
InventorySlot.RightHand => "П.Рука",
|
||
InventorySlot.Head => "Голова",
|
||
InventorySlot.Body => "Тело",
|
||
InventorySlot.Hands => "Руки",
|
||
InventorySlot.Bottom => "Ноги",
|
||
InventorySlot.Feet => "Обувь",
|
||
_ => "Неизвестно"
|
||
};
|
||
}
|
||
|
||
public void UnEquipItem( InventoryItem item )
|
||
{
|
||
if ( item == null )
|
||
return;
|
||
|
||
var slotToRemove = EquippedItems.FirstOrDefault( kvp => kvp.Value == item ).Key;
|
||
|
||
if ( EquippedItems.ContainsKey( slotToRemove ) )
|
||
{
|
||
EquippedItems.Remove( slotToRemove );
|
||
item.Equipped = false;
|
||
item.OnUnEquipped();
|
||
OnUnEquipped?.Invoke( item );
|
||
OnChanged?.Invoke();
|
||
AutoSave(); // Автоматическое сохранение
|
||
}
|
||
}
|
||
|
||
public InventoryItem GetEquippedItem( InventorySlot slot )
|
||
{
|
||
return EquippedItems.TryGetValue( slot, out var item ) ? item : null;
|
||
}
|
||
|
||
public bool IsSlotOccupied( InventorySlot slot )
|
||
{
|
||
return EquippedItems.ContainsKey( slot );
|
||
}
|
||
|
||
public void ClearInventory()
|
||
{
|
||
// Снимаем все экипированные предметы
|
||
foreach ( var item in EquippedItems.Values.ToList() )
|
||
{
|
||
UnEquipItem( item );
|
||
}
|
||
|
||
Items.Clear();
|
||
OnChanged?.Invoke();
|
||
}
|
||
|
||
// Публичный метод для уведомления об изменениях извне класса
|
||
public void NotifyChanged()
|
||
{
|
||
OnChanged?.Invoke();
|
||
}
|
||
|
||
// Публичный метод для уведомления о добавлении предмета извне класса
|
||
public void NotifyItemAdded( InventoryItem item )
|
||
{
|
||
OnItemAdded?.Invoke( item );
|
||
}
|
||
|
||
// Методы для получения информации о вместимости
|
||
public int GetUsedSlots()
|
||
{
|
||
return Items.Count;
|
||
}
|
||
|
||
public int GetAvailableSlots()
|
||
{
|
||
if ( UnlimitedSlots )
|
||
return int.MaxValue;
|
||
|
||
return Math.Max( 0, MaxInventorySlots - Items.Count );
|
||
}
|
||
|
||
public bool IsInventoryFull()
|
||
{
|
||
if ( UnlimitedSlots )
|
||
return false;
|
||
|
||
return Items.Count >= MaxInventorySlots;
|
||
}
|
||
|
||
public float GetInventoryUsagePercentage()
|
||
{
|
||
if ( UnlimitedSlots )
|
||
return 0f;
|
||
|
||
return (float)Items.Count / MaxInventorySlots * 100f;
|
||
}
|
||
|
||
#region Сохранение и загрузка
|
||
|
||
/// <summary>
|
||
/// Автоматически сохраняет инвентарь при изменениях
|
||
/// </summary>
|
||
public void AutoSave()
|
||
{
|
||
if ( Network.IsOwner )
|
||
{
|
||
InventorySaveSystem.SaveInventory( this );
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загружает инвентарь из файла сохранения
|
||
/// </summary>
|
||
public void LoadFromSave()
|
||
{
|
||
if ( Network.IsOwner )
|
||
{
|
||
InventorySaveSystem.LoadInventory( this );
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Проверяет, существует ли файл сохранения
|
||
/// </summary>
|
||
public bool HasSaveFile()
|
||
{
|
||
return InventorySaveSystem.HasSaveFile();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Удаляет файл сохранения
|
||
/// </summary>
|
||
public void DeleteSaveFile()
|
||
{
|
||
if ( Network.IsOwner )
|
||
{
|
||
InventorySaveSystem.DeleteSaveFile();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Выбрасывает все предметы из инвентаря
|
||
/// </summary>
|
||
/// <param name="dropPosition">Базовая позиция для выбрасывания</param>
|
||
/// <param name="scatterRadius">Радиус разброса предметов</param>
|
||
/// <returns>Количество выброшенных предметов</returns>
|
||
public int DropAllItems( Vector3 dropPosition, float scatterRadius = 50f )
|
||
{
|
||
if ( !Network.IsOwner ) return 0;
|
||
|
||
Log.Info( "Выбрасываем все предметы из инвентаря..." );
|
||
|
||
// Создаем копию списка предметов для безопасной итерации
|
||
var itemsToDrop = Items.ToList();
|
||
var equippedItemsToDrop = EquippedItems.Values.ToList();
|
||
|
||
int droppedCount = 0;
|
||
|
||
// Сначала снимаем все экипированные предметы и добавляем их в список для выбрасывания
|
||
foreach ( var equippedItem in equippedItemsToDrop )
|
||
{
|
||
if ( equippedItem != null )
|
||
{
|
||
// Снимаем предмет
|
||
UnEquipItem( equippedItem );
|
||
|
||
// Добавляем в список для выбрасывания, если его там еще нет
|
||
if ( !itemsToDrop.Contains( equippedItem ) )
|
||
{
|
||
itemsToDrop.Add( equippedItem );
|
||
}
|
||
}
|
||
}
|
||
|
||
// Затем выбрасываем все предметы
|
||
foreach ( var item in itemsToDrop )
|
||
{
|
||
if ( item != null && item.Count > 0 )
|
||
{
|
||
// Выбираем случайную позицию в радиусе разброса
|
||
var scatteredPosition = dropPosition + Vector3.Random * scatterRadius + Vector3.Up * 10f;
|
||
DropItem( item, scatteredPosition );
|
||
droppedCount++;
|
||
}
|
||
}
|
||
|
||
Log.Info( $"Выброшено {droppedCount} предметов из инвентаря" );
|
||
return droppedCount;
|
||
}
|
||
|
||
#endregion
|
||
}
|