AI захватит мир

This commit is contained in:
Oscar
2025-06-26 23:24:52 +03:00
parent 793165bb03
commit 875d594038
28 changed files with 1263 additions and 266 deletions

View File

@@ -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();
}
}

View 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;
}
}

View File

@@ -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 => "📦",
_ => "📦"
};
}
}

View File

@@ -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 )
{
// Логика экипировки одежды
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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}";
}
}

View File

@@ -1,9 +0,0 @@
using Sandbox;
namespace Sasalka;
public class InventoryItemCountable : InventoryItem
{
// public int Count { get; set; } = 1;
// public int MaxCount { get; set; } = 1;
}

View 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
View 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]`

View File

@@ -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>

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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; // Очищаем кэш
}
}
}
}

View File

@@ -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]

View File

@@ -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();

View File

@@ -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()