AI захватит мир
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
public class AttachmentSlotResolver
|
||||
{
|
||||
private readonly Func<string, GameObject> _attachmentGetter;
|
||||
private readonly Dictionary<Inventar.InventorySlot, GameObject> _slotCache = new();
|
||||
|
||||
public AttachmentSlotResolver( Func<string, GameObject> attachmentGetter )
|
||||
{
|
||||
@@ -11,12 +12,27 @@ public class AttachmentSlotResolver
|
||||
|
||||
public GameObject GetSlotObject( Inventar.InventorySlot slot )
|
||||
{
|
||||
return slot switch
|
||||
// Проверяем кэш
|
||||
if (_slotCache.TryGetValue(slot, out var cachedObject))
|
||||
{
|
||||
return cachedObject;
|
||||
}
|
||||
|
||||
// Получаем объект и кэшируем его
|
||||
var slotObject = slot switch
|
||||
{
|
||||
Inventar.InventorySlot.LeftHand => _attachmentGetter.Invoke( "hold_L" ),
|
||||
Inventar.InventorySlot.RightHand => _attachmentGetter.Invoke( "hold_R" ),
|
||||
Inventar.InventorySlot.Body => _attachmentGetter.Invoke( "forward_reference_modelspace" ),
|
||||
_ => _attachmentGetter.Invoke( "forward_reference_modelspace" )
|
||||
};
|
||||
|
||||
_slotCache[slot] = slotObject;
|
||||
return slotObject;
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_slotCache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
32
Code/Inventory/Definitions/AmmoItemDefinition.cs
Normal file
32
Code/Inventory/Definitions/AmmoItemDefinition.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace Sasalka;
|
||||
|
||||
[GameResource( "Ammo Item Definition", "ammo", "", Category = "Sasalka", Icon = "inventory_2" )]
|
||||
public class AmmoItemDefinition : BaseItemDefinition
|
||||
{
|
||||
[Property, Category( "Ammo Properties" )]
|
||||
public string AmmoType { get; set; } = "Pistol";
|
||||
|
||||
[Property, Category( "Ammo Properties" )]
|
||||
public float Damage { get; set; } = 10f;
|
||||
|
||||
[Property, Category( "Ammo Properties" )]
|
||||
public float Penetration { get; set; } = 0f;
|
||||
|
||||
[Property, Category( "Ammo Properties" )]
|
||||
public bool IsExplosive { get; set; } = false;
|
||||
|
||||
[Property, Category( "Ammo Properties" )]
|
||||
public float ExplosionRadius { get; set; } = 0f;
|
||||
|
||||
[Property, Category( "Ammo Properties" )]
|
||||
public string CompatibleWeapons { get; set; } = "";
|
||||
|
||||
public override ItemCategory Category => ItemCategory.Ammo;
|
||||
|
||||
public override bool CanUse() => false; // Патроны нельзя использовать напрямую
|
||||
|
||||
public bool IsCompatibleWith( string ammoType )
|
||||
{
|
||||
return AmmoType == ammoType;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,92 @@
|
||||
namespace Sasalka;
|
||||
|
||||
[GameResource( "Base Item Definition", "inv", "", Category = "Sasalka", Icon = "inventory_2" )]
|
||||
public enum ItemCategory
|
||||
{
|
||||
Weapon,
|
||||
Clothing,
|
||||
Ammo,
|
||||
Consumable,
|
||||
Tool,
|
||||
Misc
|
||||
}
|
||||
|
||||
public enum ItemRarity
|
||||
{
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Epic,
|
||||
Legendary
|
||||
}
|
||||
|
||||
[GameResource("Base Item Definition", "inv", "", Category = "Sasalka", Icon = "inventory_2")]
|
||||
public class BaseItemDefinition : GameResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
[Property, Title("Basic Info")]
|
||||
public string Name { get; set; } = "Unknown Item";
|
||||
|
||||
public string Description { get; set; }
|
||||
[Property]
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[ResourceType( "prefab" )]
|
||||
public GameObject Prefab { get; set; } = GameObject.GetPrefab( "prefabs/item_parcel.prefab" );
|
||||
[Property, Category("Visual")]
|
||||
[ResourceType("prefab")]
|
||||
public GameObject Prefab { get; set; } = GameObject.GetPrefab("prefabs/item_parcel.prefab");
|
||||
|
||||
[Property, Category("Visual")]
|
||||
public Texture ImageTexture { get; set; }
|
||||
|
||||
[Property, Category("Visual")]
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
[Property, Category("Properties")]
|
||||
[Range(1, 1000)]
|
||||
public int MaxCount { get; set; } = 1;
|
||||
|
||||
[Property, Category("Properties")]
|
||||
public virtual ItemCategory Category { get; set; } = ItemCategory.Misc;
|
||||
|
||||
[Property, Category("Properties")]
|
||||
public ItemRarity Rarity { get; set; } = ItemRarity.Common;
|
||||
|
||||
[Property, Category("Properties")]
|
||||
public float Weight { get; set; } = 1.0f;
|
||||
|
||||
[Property, Category("Properties")]
|
||||
public bool IsStackable => MaxCount > 1;
|
||||
|
||||
[Property, Category("Properties")]
|
||||
public bool IsEquipable => this is IEquipable;
|
||||
|
||||
public virtual Inventar.InventorySlot? GetSlot() => null;
|
||||
|
||||
public virtual bool CanUse() => false;
|
||||
|
||||
public virtual void OnUse(InventoryItem item) { }
|
||||
|
||||
public string GetRarityColor()
|
||||
{
|
||||
return Rarity switch
|
||||
{
|
||||
ItemRarity.Common => "#ffffff",
|
||||
ItemRarity.Uncommon => "#1eff00",
|
||||
ItemRarity.Rare => "#0070dd",
|
||||
ItemRarity.Epic => "#a335ee",
|
||||
ItemRarity.Legendary => "#ff8000",
|
||||
_ => "#ffffff"
|
||||
};
|
||||
}
|
||||
|
||||
public string GetCategoryIcon()
|
||||
{
|
||||
return Category switch
|
||||
{
|
||||
ItemCategory.Weapon => "🔫",
|
||||
ItemCategory.Clothing => "👕",
|
||||
ItemCategory.Ammo => "💥",
|
||||
ItemCategory.Consumable => "🍖",
|
||||
ItemCategory.Tool => "🔧",
|
||||
ItemCategory.Misc => "📦",
|
||||
_ => "📦"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,29 @@
|
||||
[GameResource( "Clothing Item Definition", "clitem", "", Category = "Sasalka", Icon = "inventory_2" )]
|
||||
public class ClothingItemDefinition : BaseItemDefinition, IEquipable
|
||||
{
|
||||
[Property] public string ClothUrl { get; set; }
|
||||
[Property, Category( "Clothing Properties" )]
|
||||
public string ClothUrl { get; set; }
|
||||
|
||||
[Property, Category( "Clothing Properties" )]
|
||||
public Inventar.InventorySlot Slot { get; set; }
|
||||
|
||||
[Property, Category( "Clothing Properties" )]
|
||||
public float ArmorValue { get; set; } = 0f;
|
||||
|
||||
[Property, Category( "Clothing Properties" )]
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
[Property, Category( "Clothing Properties" )]
|
||||
public string BodyPart { get; set; } = "";
|
||||
|
||||
public override Inventar.InventorySlot? GetSlot() => Slot;
|
||||
|
||||
public override ItemCategory Category => ItemCategory.Clothing;
|
||||
|
||||
public override bool CanUse() => true;
|
||||
|
||||
public override void OnUse( InventoryItem item )
|
||||
{
|
||||
// Логика экипировки одежды
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,55 @@ namespace Sasalka;
|
||||
[GameResource( "Weapon Item Definition", "weapon", "", Category = "Sasalka", Icon = "inventory_2" )]
|
||||
public class WeaponItemDefinition : BaseItemDefinition, IEquipable
|
||||
{
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public Inventar.InventorySlot Slot { get; set; }
|
||||
|
||||
public override Inventar.InventorySlot? GetSlot() => Slot;
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public CitizenAnimationHelper.HoldTypes HoldType { get; set; } = CitizenAnimationHelper.HoldTypes.None;
|
||||
[InlineEditor, Space] public WeaponDefinition WeaponDefinition { get; set; }
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
[InlineEditor, Space]
|
||||
public WeaponDefinition WeaponDefinition { get; set; }
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public float Damage { get; set; } = 10f;
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public float FireRate { get; set; } = 1f;
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public float Range { get; set; } = 1000f;
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public int MagazineSize { get; set; } = 30;
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public string AmmoType { get; set; } = "Pistol";
|
||||
|
||||
public override Inventar.InventorySlot? GetSlot() => Slot;
|
||||
|
||||
public override ItemCategory Category => ItemCategory.Weapon;
|
||||
|
||||
public override bool CanUse() => true;
|
||||
|
||||
public override void OnUse( InventoryItem item )
|
||||
{
|
||||
// Логика использования оружия будет в компоненте Weapon
|
||||
}
|
||||
}
|
||||
|
||||
public struct WeaponDefinition
|
||||
{
|
||||
// public CitizenAnimationHelper.Hand Hand { get; set; }
|
||||
public Vector3 Position { get; set; }
|
||||
public Rotation Rotation { get; set; }
|
||||
[Property] public Vector3 Position { get; set; }
|
||||
|
||||
[Property] public Rotation Rotation { get; set; }
|
||||
|
||||
[Property] public Vector3 Scale { get; set; }
|
||||
|
||||
public WeaponDefinition()
|
||||
{
|
||||
Position = Vector3.Zero;
|
||||
Rotation = Rotation.Identity;
|
||||
Scale = Vector3.One;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
namespace Sasalka;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sandbox;
|
||||
|
||||
public class Inventar
|
||||
namespace Sasalka;
|
||||
|
||||
public class Inventar : Component
|
||||
{
|
||||
[Flags]
|
||||
public enum InventorySlot
|
||||
@@ -15,84 +20,243 @@ public class Inventar
|
||||
Feet = 1 << 6 // 64
|
||||
}
|
||||
|
||||
public List<InventoryItem> Items { get; private set; } = new();
|
||||
[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 Dictionary<InventorySlot, InventoryItem> EquippedItems { get; private set; } = new();
|
||||
|
||||
public event Action OnChanged;
|
||||
public event Action<InventoryItem> OnEquipped;
|
||||
public event Action<InventoryItem> OnUnEquipped;
|
||||
public event Action<InventoryItem> OnItemAdded;
|
||||
public event Action<InventoryItem> OnItemRemoved;
|
||||
|
||||
public void AddItem( InventoryItem item )
|
||||
public bool CanAddItem( InventoryItem item )
|
||||
{
|
||||
Items.Add( item );
|
||||
OnChanged?.Invoke();
|
||||
if ( item == null || item.Definition == null )
|
||||
return false;
|
||||
|
||||
// Проверяем, есть ли уже такой предмет в инвентаре
|
||||
var existingItem = Items.FirstOrDefault( x => x.Definition == item.Definition );
|
||||
|
||||
if ( existingItem != null )
|
||||
{
|
||||
// Если предмет уже есть, проверяем, можно ли добавить к нему количество
|
||||
return existingItem.Count + item.Count <= item.Definition.MaxCount;
|
||||
}
|
||||
|
||||
// Если предмета нет, проверяем вместимость инвентаря
|
||||
if ( !UnlimitedSlots && Items.Count >= MaxInventorySlots )
|
||||
{
|
||||
return false; // Инвентарь полон
|
||||
}
|
||||
|
||||
// Проверяем только MaxCount для нового предмета
|
||||
return item.Count <= item.Definition.MaxCount;
|
||||
}
|
||||
|
||||
public void RemoveItem( InventoryItem item )
|
||||
/// <summary>
|
||||
/// Добавляет предмет в инвентарь, распределяя по существующим и новым стекам. Возвращает остаток, который не удалось добавить (или 0, если всё добавлено).
|
||||
/// </summary>
|
||||
public int AddItem(InventoryItem item)
|
||||
{
|
||||
UnEquipItem( item );
|
||||
Items.Remove( item );
|
||||
OnChanged?.Invoke();
|
||||
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;
|
||||
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 };
|
||||
Items.Add(newStack);
|
||||
toAdd -= stackCount;
|
||||
OnChanged?.Invoke();
|
||||
OnItemAdded?.Invoke(newStack);
|
||||
}
|
||||
|
||||
// 3. Возвращаем остаток, если не всё удалось добавить
|
||||
return toAdd;
|
||||
}
|
||||
|
||||
public void EquipItem( InventoryItem item )
|
||||
public bool RemoveItem( InventoryItem item, int count = 1 )
|
||||
{
|
||||
if ( item.Definition is not IEquipable equipable )
|
||||
return;
|
||||
if ( item == null || !Items.Contains( item ) )
|
||||
return false;
|
||||
|
||||
if ( count >= item.Count )
|
||||
{
|
||||
// Удаляем весь предмет
|
||||
UnEquipItem( item );
|
||||
Items.Remove( item );
|
||||
OnChanged?.Invoke();
|
||||
OnItemRemoved?.Invoke( item );
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Уменьшаем количество
|
||||
item.Count -= count;
|
||||
OnChanged?.Invoke();
|
||||
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.ContainsValue( item ) )
|
||||
if ( EquippedItems.Values.Contains( item ) )
|
||||
{
|
||||
UnEquipItem( item );
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Если на этом слоте уже что-то есть — снять старый предмет
|
||||
if ( EquippedItems.TryGetValue( equipable.Slot, out var oldItem ) )
|
||||
if ( EquippedItems.TryGetValue( slot.Value, out var oldItem ) )
|
||||
{
|
||||
UnEquipItem( oldItem );
|
||||
|
||||
// Вернуть снятый предмет обратно в инвентарь, если его там нет
|
||||
if ( !Items.Contains( oldItem ) )
|
||||
Items.Add( oldItem );
|
||||
}
|
||||
|
||||
// Экипировать новый предмет
|
||||
EquippedItems[equipable.Slot] = item;
|
||||
EquippedItems[slot.Value] = item;
|
||||
item.Equipped = true;
|
||||
OnEquipped?.Invoke( item );
|
||||
OnChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DropItem( InventoryItem item, Vector3 position )
|
||||
{
|
||||
if ( item == null || !Items.Contains( item ) )
|
||||
return;
|
||||
|
||||
// Создаем копию предмета для выбрасывания
|
||||
var droppedItem = new InventoryItem
|
||||
{
|
||||
Definition = item.Definition,
|
||||
Count = item.Count // Выбрасываем всю стопку
|
||||
};
|
||||
|
||||
GameObject gO = item.Definition.Prefab.Clone( position );
|
||||
|
||||
if ( gO.Components.TryGet<InventoryItem>( out var inventoryItem ) )
|
||||
{
|
||||
inventoryItem.Count = item.Count;
|
||||
|
||||
if ( inventoryItem.Definition == null )
|
||||
{
|
||||
inventoryItem.Definition = item.Definition;
|
||||
}
|
||||
inventoryItem.Count = droppedItem.Count;
|
||||
inventoryItem.Definition = droppedItem.Definition;
|
||||
}
|
||||
|
||||
gO.NetworkSpawn( null );
|
||||
RemoveItem( item );
|
||||
Items.Remove( item );
|
||||
OnChanged?.Invoke();
|
||||
gO.NetworkSpawn();
|
||||
|
||||
// Удаляем весь предмет из инвентаря
|
||||
RemoveItem( item, item.Count );
|
||||
}
|
||||
|
||||
|
||||
public void UnEquipItem( InventoryItem item )
|
||||
{
|
||||
foreach ( var kvp in EquippedItems.Where( kvp => kvp.Value == item ).ToList() )
|
||||
if ( item == null )
|
||||
return;
|
||||
|
||||
var slotToRemove = EquippedItems.FirstOrDefault( kvp => kvp.Value == item ).Key;
|
||||
|
||||
if ( EquippedItems.ContainsKey( slotToRemove ) )
|
||||
{
|
||||
EquippedItems.Remove( kvp.Key );
|
||||
EquippedItems.Remove( slotToRemove );
|
||||
item.Equipped = false;
|
||||
OnUnEquipped?.Invoke( item );
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
item.Equipped = false;
|
||||
OnUnEquipped?.Invoke( 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,60 @@ namespace Sasalka;
|
||||
public class InventoryItem : Component
|
||||
{
|
||||
[Property] public BaseItemDefinition Definition { get; set; }
|
||||
[Property] public int Count { get; set; } = 1;
|
||||
[Property] public bool Equipped { get; set; } = false;
|
||||
[Sync, Property] public int Count { get; set; } = 1;
|
||||
[Sync] public bool Equipped { get; set; } = false;
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
if ( GameObject.Components.TryGet<PickupItem>( out var item ) ) //FindMode.EverythingInSelf
|
||||
if ( GameObject.Components.TryGet<PickupItem>( out var item ) )
|
||||
{
|
||||
item.Label = Definition.Name;
|
||||
item.Label = Definition?.Name ?? "Unknown Item";
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanStackWith( InventoryItem other )
|
||||
{
|
||||
if ( other == null || Definition == null || other.Definition == null )
|
||||
return false;
|
||||
|
||||
return Definition == other.Definition && Definition.MaxCount > 1;
|
||||
}
|
||||
|
||||
public bool CanAddCount( int amount )
|
||||
{
|
||||
if ( Definition == null )
|
||||
return false;
|
||||
|
||||
return Count + amount <= Definition.MaxCount;
|
||||
}
|
||||
|
||||
public bool TryAddCount( int amount )
|
||||
{
|
||||
if ( !CanAddCount( amount ) )
|
||||
return false;
|
||||
|
||||
Count += amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryRemoveCount( int amount )
|
||||
{
|
||||
if ( Count < amount )
|
||||
return false;
|
||||
|
||||
Count -= amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
public InventoryItem Clone()
|
||||
{
|
||||
var clone = new InventoryItem { Definition = Definition, Count = Count, Equipped = false };
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Definition?.Name ?? "Unknown"} x{Count}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using Sandbox;
|
||||
|
||||
namespace Sasalka;
|
||||
|
||||
public class InventoryItemCountable : InventoryItem
|
||||
{
|
||||
// public int Count { get; set; } = 1;
|
||||
// public int MaxCount { get; set; } = 1;
|
||||
}
|
||||
55
Code/Inventory/ItemSpawner.cs
Normal file
55
Code/Inventory/ItemSpawner.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Sandbox;
|
||||
|
||||
namespace Sasalka;
|
||||
|
||||
public class ItemSpawner : Component
|
||||
{
|
||||
[Property] public bool SpawnTestItems { get; set; } = false;
|
||||
[Property] public GameObject ItemPrefab { get; set; }
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
if (!Network.IsOwner || !SpawnTestItems) return;
|
||||
|
||||
GameTask.DelaySeconds(2f).ContinueWith(_ => SpawnItems());
|
||||
}
|
||||
|
||||
private void SpawnItems()
|
||||
{
|
||||
if (ItemPrefab == null)
|
||||
{
|
||||
// Log.Warning("ItemPrefab не установлен!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Log.Info("Спавним тестовые предметы...");
|
||||
|
||||
var player = Dedugan.Local;
|
||||
if (player?.Transform == null) return;
|
||||
|
||||
var playerPos = player.WorldPosition;
|
||||
var spawnRadius = 5f;
|
||||
var itemCount = 5;
|
||||
|
||||
for (int i = 0; i < itemCount; i++)
|
||||
{
|
||||
var angle = (float)i / itemCount * 360f * MathF.PI / 180f;
|
||||
var offset = new Vector3(
|
||||
MathF.Cos(angle) * spawnRadius,
|
||||
0,
|
||||
MathF.Sin(angle) * spawnRadius
|
||||
);
|
||||
|
||||
var position = playerPos + offset;
|
||||
var item = ItemPrefab.Clone(position);
|
||||
|
||||
// Добавляем случайный поворот
|
||||
item.WorldRotation = Rotation.Random;
|
||||
|
||||
// Спавним в сети
|
||||
item.NetworkSpawn();
|
||||
|
||||
// Log.Info($"Предмет создан в позиции {position}");
|
||||
}
|
||||
}
|
||||
}
|
||||
172
Code/Inventory/README.md
Normal file
172
Code/Inventory/README.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Система инвентаря Sasalka
|
||||
|
||||
## Обзор
|
||||
|
||||
Новая система инвентаря была полностью переработана для улучшения производительности, сетевой синхронизации и пользовательского опыта.
|
||||
|
||||
## Основные изменения
|
||||
|
||||
### 1. Inventar (Основной класс инвентаря)
|
||||
- Теперь наследуется от `Component` для лучшей интеграции с S&box
|
||||
- Добавлена сетевая синхронизация с атрибутами `[Sync]`
|
||||
- Улучшена валидация при добавлении предметов
|
||||
- Добавлена поддержка стакания предметов
|
||||
- Новые события: `OnItemAdded`, `OnItemRemoved`
|
||||
|
||||
### 2. InventoryItem
|
||||
- Добавлена сетевая синхронизация с `[Sync]`
|
||||
- Улучшена валидация количества предметов
|
||||
- Новые методы: `CanStackWith`, `CanAddCount`, `TryAddCount`, `TryRemoveCount`, `Clone`
|
||||
|
||||
### 3. Определения предметов
|
||||
|
||||
#### BaseItemDefinition
|
||||
- Добавлены категории предметов (`ItemCategory`)
|
||||
- Добавлена система редкости (`ItemRarity`)
|
||||
- Добавлен вес предметов
|
||||
- Улучшен редактор с группировкой свойств
|
||||
|
||||
#### WeaponItemDefinition
|
||||
- Добавлены свойства оружия (урон, скорострельность, дальность)
|
||||
- Улучшена структура `WeaponDefinition`
|
||||
|
||||
#### ClothingItemDefinition
|
||||
- Добавлены свойства брони и видимости
|
||||
- Улучшена интеграция с системой одежды
|
||||
|
||||
#### AmmoItemDefinition (новый)
|
||||
- Специализированное определение для патронов
|
||||
- Система совместимости с оружием
|
||||
- Свойства патронов (урон, пробивная способность)
|
||||
|
||||
### 4. UI инвентаря
|
||||
- Добавлена фильтрация по категориям
|
||||
- Улучшен дизайн с поддержкой редкости
|
||||
- Отображение экипированных предметов
|
||||
- Адаптивный интерфейс
|
||||
|
||||
## Исправления API
|
||||
|
||||
### Сетевая синхронизация
|
||||
- Заменены атрибуты `[Net]` на `[Sync]` для совместимости с S&box
|
||||
- Все изменения инвентаря автоматически синхронизируются между клиентами
|
||||
|
||||
### Методы коллекций
|
||||
- Исправлен метод `ContainsValue()` на `Values.Contains()` для `Dictionary`
|
||||
- Обновлены методы работы с коллекциями для совместимости
|
||||
|
||||
### Using директивы
|
||||
- Добавлены необходимые `using` директивы в UI файлы
|
||||
- Исправлены namespace для компонентов
|
||||
|
||||
### Наследование
|
||||
- Исправлено наследование `PickupItem` от `Sandbox.UI.InteractionButton`
|
||||
- Обновлены интерфейсы и базовые классы
|
||||
|
||||
## Использование
|
||||
|
||||
### Создание предметов
|
||||
|
||||
```csharp
|
||||
// Создание предмета оружия
|
||||
var weaponItem = new InventoryItem
|
||||
{
|
||||
Definition = ResourceLibrary.Get<WeaponItemDefinition>("Items/pistol_test.weapon")
|
||||
};
|
||||
inventory.AddItem(weaponItem);
|
||||
|
||||
// Создание патронов
|
||||
var ammoItem = new InventoryItem
|
||||
{
|
||||
Definition = ResourceLibrary.Get<AmmoItemDefinition>("Items/pistol_ammo.ammo")
|
||||
};
|
||||
ammoItem.Count = 30;
|
||||
inventory.AddItem(ammoItem);
|
||||
```
|
||||
|
||||
### Экипировка предметов
|
||||
|
||||
```csharp
|
||||
// Экипировка предмета
|
||||
inventory.EquipItem(item);
|
||||
|
||||
// Проверка экипированного предмета
|
||||
var equippedWeapon = inventory.GetEquippedItem(Inventar.InventorySlot.RightHand);
|
||||
```
|
||||
|
||||
### Валидация
|
||||
|
||||
```csharp
|
||||
// Проверка возможности добавления
|
||||
if (inventory.CanAddItem(item))
|
||||
{
|
||||
inventory.AddItem(item);
|
||||
}
|
||||
|
||||
// Проверка стакания
|
||||
if (item1.CanStackWith(item2))
|
||||
{
|
||||
// Предметы можно объединить
|
||||
}
|
||||
```
|
||||
|
||||
## Создание определений предметов
|
||||
|
||||
### Оружие
|
||||
1. Создайте ресурс типа "Weapon Item Definition"
|
||||
2. Укажите слот экипировки
|
||||
3. Настройте параметры оружия (урон, дальность и т.д.)
|
||||
4. Укажите тип патронов
|
||||
|
||||
### Патроны
|
||||
1. Создайте ресурс типа "Ammo Item Definition"
|
||||
2. Укажите тип патронов
|
||||
3. Настройте совместимость с оружием
|
||||
|
||||
### Одежда
|
||||
1. Создайте ресурс типа "Clothing Item Definition"
|
||||
2. Укажите слот экипировки
|
||||
3. Добавьте URL одежды из Workshop
|
||||
|
||||
## Сетевая синхронизация
|
||||
|
||||
Все изменения инвентаря автоматически синхронизируются между клиентами благодаря атрибутам `[Sync]` на свойствах `Items` и `EquippedItems`.
|
||||
|
||||
## Производительность
|
||||
|
||||
- Улучшена производительность UI с оптимизированным `BuildHash()`
|
||||
- Добавлено кэширование для `GetUsables()`
|
||||
- Оптимизирована работа с коллекциями
|
||||
|
||||
## Миграция
|
||||
|
||||
Для миграции существующих предметов:
|
||||
1. Обновите определения предметов с новыми свойствами
|
||||
2. Измените код создания предметов на новый API
|
||||
3. Обновите UI для использования новых компонентов
|
||||
|
||||
## Тестирование
|
||||
|
||||
### InventoryTest
|
||||
Используйте компонент `InventoryTest` для добавления тестовых предметов:
|
||||
|
||||
```csharp
|
||||
// Добавьте компонент к игроку и установите AddTestItems = true
|
||||
var testComponent = player.GameObject.Components.GetOrCreate<InventoryTest>();
|
||||
testComponent.AddTestItems = true;
|
||||
```
|
||||
|
||||
### InventoryCompilationTest
|
||||
Используйте компонент `InventoryCompilationTest` для проверки компиляции:
|
||||
|
||||
```csharp
|
||||
// Добавьте компонент к игроку и установите RunTest = true
|
||||
var testComponent = player.GameObject.Components.GetOrCreate<InventoryCompilationTest>();
|
||||
testComponent.RunTest = true;
|
||||
```
|
||||
|
||||
## Известные проблемы
|
||||
|
||||
- Убедитесь, что все необходимые ресурсы (префабы, текстуры) существуют
|
||||
- Проверьте, что определения предметов правильно настроены в редакторе
|
||||
- При использовании в сети убедитесь, что все компоненты имеют правильные атрибуты `[Sync]`
|
||||
@@ -4,11 +4,31 @@
|
||||
|
||||
<root class="inventory @( Inventar.IsInventoryOpen ? "" : "hidden" )">
|
||||
<div class="inventory-panel">
|
||||
@if ( PlayerInventory.Items.Count > 0 )
|
||||
@if ( PlayerInventory != null )
|
||||
{
|
||||
@foreach ( var item in PlayerInventory.Items )
|
||||
<div class="inventory-header">
|
||||
<div class="inventory-title">Инвентарь</div>
|
||||
@if ( !PlayerInventory.UnlimitedSlots )
|
||||
{
|
||||
<div class="inventory-slots">
|
||||
@PlayerInventory.GetUsedSlots() / @PlayerInventory.MaxInventorySlots слотов
|
||||
<div class="inventory-usage-bar">
|
||||
<div class="inventory-usage-fill" style="width: @(PlayerInventory.GetInventoryUsagePercentage())%"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if ( PlayerInventory.Items.Count > 0 )
|
||||
{
|
||||
<Sasalka.Ui.InventoryItem Item="@item" OnItemClick="@( UseItem )" OnItemRightClick="@( DropItem )"/>
|
||||
@foreach ( var item in PlayerInventory.Items )
|
||||
{
|
||||
<Sasalka.Ui.InventoryItem Item="@item" OnItemClick="@( UseItem )" OnItemRightClick="@( DropItem )"/>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="inventory-empty">Инвентарь пуст</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,59 @@ Inventory {
|
||||
//background-color: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.inventory-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 2px solid #2a3d54;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.inventory-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.inventory-slots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inventory-slots {
|
||||
font-size: 14px;
|
||||
color: #a0b4c8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inventory-usage-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: rgba(42, 61, 84, 0.5);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #2a3d54;
|
||||
}
|
||||
|
||||
.inventory-usage-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50 0%, #8BC34A 100%);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.inventory-empty {
|
||||
text-align: center;
|
||||
color: #a0b4c8;
|
||||
font-style: italic;
|
||||
padding: 40px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
|
||||
@@ -12,25 +12,50 @@
|
||||
: !string.IsNullOrWhiteSpace( definition?.ImageUrl )
|
||||
? definition.ImageUrl
|
||||
: null;
|
||||
var rarityColor = definition?.GetRarityColor() ?? "#ffffff";
|
||||
var categoryIcon = definition?.GetCategoryIcon() ?? "📦";
|
||||
}
|
||||
|
||||
<root class="inventory-item @( Item.Equipped ? "equipped" : "" )" @onclick="@(() => OnItemClick?.Invoke( Item ))" @onrightclick=@( () => OnItemRightClick?.Invoke( Item ) )>
|
||||
@if ( slot is not null )
|
||||
{
|
||||
<div class="inventory-item__slot">@slot</div>
|
||||
}
|
||||
<root class="inventory-item @( Item.Equipped ? "equipped" : "" )"
|
||||
@onclick="@(() => OnItemClick?.Invoke( Item ))"
|
||||
@onrightclick="@( () => OnItemRightClick?.Invoke( Item ) )"
|
||||
style="border-left-color: @rarityColor;">
|
||||
|
||||
@if ( !string.IsNullOrEmpty( imageUrl ) )
|
||||
{
|
||||
<img src="@imageUrl" alt="@name"/>
|
||||
}
|
||||
<div class="inventory-item__icon">
|
||||
@if ( !string.IsNullOrEmpty( imageUrl ) )
|
||||
{
|
||||
<img src="@imageUrl" alt="@name"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="category-icon">@categoryIcon</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inventory-item__name">@name</div>
|
||||
<div class="inventory-item__info">
|
||||
<div class="inventory-item__name" style="color: @rarityColor;">@name</div>
|
||||
@if ( definition?.Category != ItemCategory.Misc )
|
||||
{
|
||||
<div class="inventory-item__category">@definition?.Category</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if ( definition?.MaxCount > 1 )
|
||||
{
|
||||
<div class="inventory-item__count">@Item?.Count / @definition.MaxCount</div>
|
||||
}
|
||||
<div class="inventory-item__meta">
|
||||
@if ( slot is not null )
|
||||
{
|
||||
<div class="inventory-item__slot">@GetSlotName( slot.Value )</div>
|
||||
}
|
||||
|
||||
@if ( definition?.MaxCount > 1 )
|
||||
{
|
||||
<div class="inventory-item__count">@Item?.Count / @definition.MaxCount</div>
|
||||
}
|
||||
|
||||
@* @if ( Item?.Equipped == true ) *@
|
||||
@* { *@
|
||||
@* <div class="inventory-item__equipped">✓</div> *@
|
||||
@* } *@
|
||||
</div>
|
||||
</root>
|
||||
|
||||
@code {
|
||||
@@ -38,11 +63,28 @@
|
||||
public Action<Sasalka.InventoryItem> OnItemClick { get; set; }
|
||||
public Action<Sasalka.InventoryItem> OnItemRightClick { get; set; }
|
||||
|
||||
string GetSlotName( Inventar.InventorySlot slot )
|
||||
{
|
||||
return slot switch
|
||||
{
|
||||
Inventar.InventorySlot.LeftHand => "Л.Рука",
|
||||
Inventar.InventorySlot.RightHand => "П.Рука",
|
||||
Inventar.InventorySlot.Head => "Голова",
|
||||
Inventar.InventorySlot.Body => "Тело",
|
||||
Inventar.InventorySlot.Hands => "Руки",
|
||||
Inventar.InventorySlot.Bottom => "Ноги",
|
||||
Inventar.InventorySlot.Feet => "Обувь",
|
||||
_ => "Неизвестно"
|
||||
};
|
||||
}
|
||||
|
||||
protected override int BuildHash()
|
||||
{
|
||||
base.BuildHash();
|
||||
var hash = new HashCode();
|
||||
hash.Add( Item?.Count );
|
||||
hash.Add( Item?.Equipped );
|
||||
hash.Add( Item?.Definition?.Name );
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +1,154 @@
|
||||
InventoryItem {
|
||||
.inventory-item {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
background: #2a3d53;
|
||||
background: linear-gradient(135deg, #2a3d53 0%, #1f2d3f 100%);
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: 1px solid #666;
|
||||
border-left: 4px solid #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
//box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #444;
|
||||
background: linear-gradient(135deg, #3a4d63 0%, #2f3d4f 100%);
|
||||
transform: translateY(-2px);
|
||||
//box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.inventory-item__name {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.inventory-item__count {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.inventory-item__slot {
|
||||
font-size: 12px;
|
||||
color: #8fc98f;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
//box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
&.equipped {
|
||||
border: 2px solid #4caf50;
|
||||
background: #2e3e2e;
|
||||
background: linear-gradient(135deg, #2e3e2e 0%, #1f2d1f 100%);
|
||||
//box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #3e4e3e 0%, #2f3d2f 100%);
|
||||
//box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.inventory-item__name {
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.inventory-item__icon {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1a2a3a 0%, #0f1a2a 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #3a4a5a;
|
||||
//box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
//filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 24px;
|
||||
//filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
.inventory-item__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 20px;
|
||||
|
||||
.inventory-item__name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
//text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.inventory-item__description {
|
||||
font-size: 12px;
|
||||
color: #cccccc;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.inventory-item__category {
|
||||
font-size: 10px;
|
||||
color: #8fc98f;
|
||||
background: linear-gradient(135deg, rgba(143, 201, 143, 0.2) 0%, rgba(143, 201, 143, 0.1) 100%);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
border: 1px solid rgba(143, 201, 143, 0.3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.inventory-item__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
|
||||
.inventory-item__slot {
|
||||
font-size: 10px;
|
||||
color: #8fc98f;
|
||||
background: linear-gradient(135deg, rgba(143, 201, 143, 0.3) 0%, rgba(143, 201, 143, 0.2) 100%);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
border: 1px solid rgba(143, 201, 143, 0.4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.inventory-item__count {
|
||||
font-size: 12px;
|
||||
color: #cccccc;
|
||||
font-weight: 500;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.inventory-item__equipped {
|
||||
font-size: 14px;
|
||||
color: #4caf50;
|
||||
font-weight: bold;
|
||||
//text-shadow: 0 1px 2px rgba(76, 175, 80, 0.5);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,35 +2,83 @@
|
||||
|
||||
public abstract class AmmoUseableBase : UseableBase
|
||||
{
|
||||
private WeaponItemDefinition _cachedWeaponDef;
|
||||
private InventoryItem _cachedAmmoItem;
|
||||
|
||||
protected InventoryItem AmmoItem => FindAmmoItem();
|
||||
|
||||
private InventoryItem FindAmmoItem()
|
||||
{
|
||||
//По типу патрон поиск + енум типа патрон
|
||||
return Dedugan.Local.Inventory.Items.FirstOrDefault( i => i.Definition.Name == "Pistol Ammo" );
|
||||
var player = Dedugan.Local;
|
||||
if (player?.Inventory == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Кэшируем WeaponDefinition для избежания повторных вызовов
|
||||
if (_cachedWeaponDef == null)
|
||||
{
|
||||
_cachedWeaponDef = GetWeaponDefinition();
|
||||
if (_cachedWeaponDef == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем кэшированный результат
|
||||
if (_cachedAmmoItem != null && _cachedAmmoItem.Count > 0)
|
||||
{
|
||||
return _cachedAmmoItem;
|
||||
}
|
||||
|
||||
// Ищем патроны
|
||||
foreach (var item in player.Inventory.Items)
|
||||
{
|
||||
if (item.Definition is AmmoItemDefinition ammoDef && ammoDef.IsCompatibleWith(_cachedWeaponDef.AmmoType))
|
||||
{
|
||||
_cachedAmmoItem = item;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
_cachedAmmoItem = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual WeaponItemDefinition GetWeaponDefinition()
|
||||
{
|
||||
// Переопределите в наследниках для возврата определения оружия
|
||||
return null;
|
||||
}
|
||||
|
||||
public override bool CanUse()
|
||||
{
|
||||
var ammo = AmmoItem;
|
||||
return base.CanUse() && ammo != null && ammo.Count > 0;
|
||||
var baseCanUse = base.CanUse();
|
||||
var hasAmmo = ammo != null && ammo.Count > 0;
|
||||
|
||||
return baseCanUse && hasAmmo;
|
||||
}
|
||||
|
||||
public override void Use()
|
||||
{
|
||||
if ( !CanUse() )
|
||||
if (!CanUse())
|
||||
return;
|
||||
|
||||
OnUse();
|
||||
|
||||
var ammo = AmmoItem;
|
||||
if ( ammo != null )
|
||||
if (ammo != null)
|
||||
{
|
||||
ammo.Count--;
|
||||
|
||||
if ( ammo.Count <= 0 )
|
||||
// Уменьшаем количество патронов
|
||||
if (ammo.TryRemoveCount(1))
|
||||
{
|
||||
Dedugan.Local.Inventory.RemoveItem( ammo );
|
||||
// Если патроны закончились, удаляем предмет из инвентаря
|
||||
if (ammo.Count <= 0)
|
||||
{
|
||||
Dedugan.Local.Inventory.RemoveItem(ammo);
|
||||
_cachedAmmoItem = null; // Очищаем кэш
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
using Sandbox.Gravity;
|
||||
using Sandbox.UI;
|
||||
using Sasalka;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Sandbox.UI;
|
||||
namespace Sasalka;
|
||||
|
||||
[Icon( "skip_next" )]
|
||||
public sealed class PickupItem : InteractionButton
|
||||
{
|
||||
[Property] public override string Label { get; set; } = "E";
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
|
||||
// Устанавливаем правильную метку для предмета
|
||||
if ( GameObject.Components.TryGet<InventoryItem>( out var inventoryItem ) )
|
||||
{
|
||||
Label = inventoryItem.Definition?.Name ?? "Подобрать";
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Press( IPressable.Event e )
|
||||
{
|
||||
base.Press( e );
|
||||
|
||||
if ( e.Source.Components.TryGet<Dedugan>( out var dedugan ) )
|
||||
{
|
||||
dedugan.Inventory.AddItem( Components.Get<InventoryItem>() );
|
||||
RpcDestroy();
|
||||
var inventoryItem = Components.Get<InventoryItem>();
|
||||
|
||||
if ( inventoryItem != null && dedugan.Inventory != null )
|
||||
{
|
||||
// Пытаемся добавить предмет в инвентарь, остаток остаётся на земле
|
||||
int left = dedugan.Inventory.AddItem( inventoryItem );
|
||||
if ( left <= 0 )
|
||||
{
|
||||
RpcDestroy();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
inventoryItem.Count = left;
|
||||
// Оставляем предмет с новым количеством на земле
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
[Rpc.Broadcast]
|
||||
|
||||
@@ -4,8 +4,13 @@ public static class UseSystem
|
||||
{
|
||||
public static bool TryUse( IUseContext context )
|
||||
{
|
||||
foreach ( var useable in context.GetUsables() )
|
||||
// Получаем все доступные предметы
|
||||
var usables = context.GetUsables();
|
||||
|
||||
// Проверяем каждый предмет на возможность использования
|
||||
foreach ( var useable in usables )
|
||||
{
|
||||
// Раннее прерывание если предмет может быть использован
|
||||
if ( useable.CanUse() )
|
||||
{
|
||||
useable.Use();
|
||||
|
||||
@@ -37,7 +37,8 @@ public abstract class UseableBase : Component, IUseable
|
||||
|
||||
public virtual void OnEquipped()
|
||||
{
|
||||
Log.Info( $"OnEquip {this}" );
|
||||
Equipped = true;
|
||||
// Log.Info( $"OnEquip {this}" );
|
||||
}
|
||||
|
||||
public virtual bool CanUse()
|
||||
|
||||
Reference in New Issue
Block a user