This commit is contained in:
Oscar
2025-06-28 18:13:47 +03:00
parent 875d594038
commit 23a35fe3cd
23 changed files with 2579 additions and 607 deletions

532
Code/Weapons/BaseWeapon.cs Normal file
View File

@@ -0,0 +1,532 @@
using System.Threading.Tasks;
using Sandbox;
using Sandbox.Citizen;
using Sasalka;
using System.Collections.Generic;
namespace Sandbox.Weapons;
/// <summary>
/// Базовый класс для всех видов оружия
/// Содержит всю логику стрельбы и управления патронами
/// </summary>
public class BaseWeapon : InventoryItem, IUseable
{
[Property] public SkinnedModelRenderer GunRenderer { get; private set; }
[Property] public GameObject MuzzleLight { get; private set; }
[Property] public GameObject ParticlePrefab { get; set; }
[Property] public GameObject BloodParticle { get; set; }
[Property] public GameObject MuzzleFlashPrefab { get; set; }
[Property] public GameObject EjectShellPrefab { get; set; }
[Property] public GameObject MuzzleTransform { get; set; }
[Property] public GameObject EjectTransform { get; set; }
// Состояние оружия
[Sync] public int CurrentAmmo { get; set; }
[Sync] public bool IsReloading { get; set; }
[Sync] public TimeSince TimeSinceLastShot { get; set; }
[Sync] public TimeSince TimeSinceReloadStart { get; set; }
// Компоненты
private SoundPointComponent _sound;
private Rigidbody _rigidbody;
private PickupItem _pickupItem;
// Кэш для производительности
private WeaponItemDefinition _weaponDefinition;
private bool _isInitialized;
// IUseable реализация - Cooldown вычисляется на основе FireRate
public float Cooldown
{
get
{
var weaponDef = GetWeaponDefinition();
return weaponDef != null ? 1f / weaponDef.FireRate : 0.1f;
}
}
protected override void OnStart()
{
base.OnStart();
// Получаем компоненты
_sound = GameObject.GetComponent<SoundPointComponent>( true );
_pickupItem = GameObject.Components.Get<PickupItem>();
// Инициализируем оружие
InitializeWeapon();
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( !IsValid || !Equipped ) return;
// Проверяем завершение перезарядки
if ( IsReloading && TimeSinceReloadStart >= GetWeaponDefinition()?.ReloadTime )
{
FinishReload();
}
// Обновляем эффекты
UpdateEffects();
}
/// <summary>
/// Инициализация оружия
/// </summary>
private void InitializeWeapon()
{
if ( _isInitialized ) return;
_weaponDefinition = Definition as WeaponItemDefinition;
if ( _weaponDefinition == null )
{
Log.Error( $"BaseWeapon: Definition is not WeaponItemDefinition for {GameObject.Name}" );
return;
}
// Оружие начинается с пустым магазином - патроны нужно зарядить
CurrentAmmo = 0;
_isInitialized = true;
}
/// <summary>
/// Получить определение оружия
/// </summary>
public WeaponItemDefinition GetWeaponDefinition()
{
return _weaponDefinition ??= Definition as WeaponItemDefinition;
}
/// <summary>
/// Экипировка оружия
/// </summary>
public override void OnEquipped()
{
base.OnEquipped();
// Получаем Rigidbody в момент экипировки
_rigidbody = GameObject.Components.Get<Rigidbody>();
if ( _rigidbody != null )
_rigidbody.Enabled = false;
if ( _pickupItem != null )
_pickupItem.Enabled = false;
}
/// <summary>
/// Снятие оружия
/// </summary>
public override void OnUnEquipped()
{
base.OnUnEquipped();
// Получаем Rigidbody в момент снятия
_rigidbody = GameObject.Components.Get<Rigidbody>();
if ( _rigidbody != null )
_rigidbody.Enabled = true;
if ( _pickupItem != null )
_pickupItem.Enabled = true;
}
// IUseable реализация
/// <summary>
/// Проверка возможности использования
/// </summary>
public virtual bool CanUse()
{
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return false;
return IsValid && Equipped && !IsReloading && TimeSinceLastShot >= 1f / weaponDef.FireRate;
}
/// <summary>
/// Использование оружия (выстрел)
/// </summary>
public virtual void Use()
{
if ( !CanUse() ) return;
TryShoot();
}
/// <summary>
/// Попытка выстрела
/// </summary>
public bool TryShoot()
{
if ( !CanShoot() ) return false;
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return false;
// Проверяем время между выстрелами из определения
if ( TimeSinceLastShot < 1f / weaponDef.FireRate ) return false;
// Проверяем патроны в магазине
if ( CurrentAmmo <= 0 )
{
return false;
}
// Выполняем выстрел
PerformShot();
return true;
}
/// <summary>
/// Проверка возможности выстрела
/// </summary>
public bool CanShoot()
{
if ( !IsValid || !Equipped || IsReloading ) return false;
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return false;
// Проверяем только патроны в магазине
return CurrentAmmo > 0 && TimeSinceLastShot >= 1f / weaponDef.FireRate;
}
/// <summary>
/// Проверка наличия патронов в инвентаре
/// </summary>
private bool HasAmmoInInventory()
{
var player = Dedugan.Local;
if ( player?.Inventory == null ) return false;
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return false;
foreach ( var item in player.Inventory.Items )
{
if ( item?.Definition is AmmoItemDefinition ammoDef && ammoDef.IsCompatibleWith( weaponDef.AmmoType ) )
{
if ( item.Count > 0 ) return true;
}
}
return false;
}
/// <summary>
/// Выполнение выстрела
/// </summary>
protected virtual void PerformShot()
{
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return;
// Уменьшаем патроны в магазине
CurrentAmmo--;
TimeSinceLastShot = 0;
// Эффекты выстрела
PlayShootEffects();
// Трассировка пули
PerformBulletTrace( weaponDef );
}
/// <summary>
/// Трассировка пули
/// </summary>
protected virtual void PerformBulletTrace( WeaponItemDefinition weaponDef )
{
var startPos = Scene.Camera.WorldPosition;
var direction = Scene.Camera.WorldRotation.Forward;
// Добавляем разброс
if ( weaponDef.Spread > 0 )
{
direction += Vector3.Random * weaponDef.Spread;
direction = direction.Normal;
}
var tr = Scene.Trace
.Ray( startPos, startPos + direction * weaponDef.Range )
.IgnoreGameObjectHierarchy( Dedugan.Local.GameObject )
.WithoutTags( "weapon" )
.UseHitboxes()
.Run();
if ( tr.Hit )
{
HandleHit( tr, weaponDef );
}
}
/// <summary>
/// Обработка попадания
/// </summary>
protected virtual void HandleHit( SceneTraceResult tr, WeaponItemDefinition weaponDef )
{
if ( tr.Hitbox != null )
{
var boneIndex = tr.Hitbox.Bone.Index;
var components = tr.GameObject.Components;
// Кэшируем компоненты для избежания повторных поисков
var dedugan = components.Get<Dedugan>();
var enemy = components.Get<Enemy>();
var hasTarget = dedugan.IsValid() || enemy.IsValid();
if ( hasTarget )
{
CreateHitEffects( tr.EndPosition, tr.Normal, true );
}
if ( dedugan.IsValid() )
{
dedugan.ReportHit( tr.Direction, boneIndex, (int)weaponDef.Damage );
}
if ( enemy.IsValid() )
{
enemy.ReportHit( tr.Direction, boneIndex, (int)weaponDef.Damage );
}
}
else
{
CreateHitEffects( tr.EndPosition, tr.Normal );
}
}
/// <summary>
/// Начать перезарядку
/// </summary>
public virtual void StartReload()
{
if ( IsReloading || CurrentAmmo >= GetWeaponDefinition()?.MagazineSize ) return;
IsReloading = true;
TimeSinceReloadStart = 0;
// Эффекты перезарядки
PlayReloadEffects();
}
/// <summary>
/// Завершить перезарядку
/// </summary>
protected virtual void FinishReload()
{
IsReloading = false;
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return;
// Загружаем патроны из инвентаря
LoadAmmoFromInventory( weaponDef.MagazineSize );
}
/// <summary>
/// Загрузить патроны из инвентаря
/// </summary>
private void LoadAmmoFromInventory( int maxAmmo )
{
var player = Dedugan.Local;
if ( player?.Inventory == null )
{
Log.Warning( "LoadAmmoFromInventory: Player or Inventory is null" );
return;
}
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null )
{
Log.Warning( "LoadAmmoFromInventory: Weapon definition is null" );
return;
}
// Проверяем, есть ли патроны в инвентаре
if ( !HasAmmoInInventory() )
{
return;
}
int ammoToLoad = maxAmmo - CurrentAmmo;
if ( ammoToLoad <= 0 )
{
return;
}
// Создаем копию списка для безопасной итерации
var itemsToProcess = player.Inventory.Items.ToList();
var itemsToRemove = new List<InventoryItem>();
int totalLoaded = 0;
foreach ( var item in itemsToProcess )
{
if ( item?.Definition is AmmoItemDefinition ammoDef && ammoDef.IsCompatibleWith( weaponDef.AmmoType ) )
{
if ( item.Count > 0 )
{
int canLoad = Math.Min( ammoToLoad, item.Count );
if ( item.TryRemoveCount( canLoad ) )
{
CurrentAmmo += canLoad;
ammoToLoad -= canLoad;
totalLoaded += canLoad;
// Если патроны закончились, помечаем для удаления
if ( item.Count <= 0 )
{
itemsToRemove.Add( item );
}
if ( ammoToLoad <= 0 )
{
break;
}
}
}
}
}
// Удаляем пустые предметы после итерации
foreach ( var item in itemsToRemove )
{
try
{
player.Inventory.RemoveItem( item );
}
catch ( System.Exception ex )
{
Log.Warning( $"LoadAmmoFromInventory: Failed to remove item {item.Definition.Name}: {ex.Message}" );
}
}
}
/// <summary>
/// Воспроизведение эффектов выстрела
/// </summary>
protected virtual void PlayShootEffects()
{
// Звук
_sound?.StartSound();
// Свет
if ( MuzzleLight != null )
{
MuzzleLight.Enabled = true;
GameTask.DelaySeconds( 0.05f ).ContinueWith( _ => MuzzleLight.Enabled = false );
}
// Анимация
if ( GunRenderer != null )
{
GunRenderer.Set( "Fire", true );
}
// Вспышка
if ( MuzzleFlashPrefab != null && MuzzleTransform != null )
{
var flash = MuzzleFlashPrefab.Clone( MuzzleTransform.WorldPosition, MuzzleTransform.WorldRotation );
DestroyAsync( flash, 0.1f );
}
// Гильза
if ( EjectShellPrefab != null && EjectTransform != null )
{
var shell = EjectShellPrefab.Clone( EjectTransform.WorldPosition, EjectTransform.WorldRotation );
DestroyAsync( shell, 3f );
}
}
/// <summary>
/// Воспроизведение эффектов перезарядки
/// </summary>
protected virtual void PlayReloadEffects()
{
// Можно добавить звук перезарядки
}
/// <summary>
/// Создание эффектов попадания
/// </summary>
[Rpc.Broadcast]
protected virtual void CreateHitEffects( Vector3 position, Vector3 normal, bool blood = false )
{
var rot = Rotation.LookAt( normal );
var effectPrefab = blood ? BloodParticle : ParticlePrefab;
if ( effectPrefab != null )
{
var effect = effectPrefab.Clone( position, rot );
DestroyAsync( effect, 0.5f );
}
}
/// <summary>
/// Обновление эффектов
/// </summary>
protected virtual void UpdateEffects()
{
// Можно добавить дополнительные эффекты
}
/// <summary>
/// Асинхронное уничтожение объекта
/// </summary>
protected async void DestroyAsync( GameObject go, float delay )
{
if ( go == null ) return;
await GameTask.DelaySeconds( delay );
if ( go.IsValid )
go.Destroy();
}
/// <summary>
/// Получить информацию о патронах для UI
/// </summary>
public (int current, int max, int totalInInventory) GetAmmoInfo()
{
var weaponDef = GetWeaponDefinition();
var totalInInventory = GetTotalAmmoInInventory();
return (CurrentAmmo, weaponDef?.MagazineSize ?? 0, totalInInventory);
}
/// <summary>
/// Получить общее количество патронов в инвентаре
/// </summary>
private int GetTotalAmmoInInventory()
{
var player = Dedugan.Local;
if ( player?.Inventory == null ) return 0;
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return 0;
int total = 0;
foreach ( var item in player.Inventory.Items )
{
if ( item?.Definition is AmmoItemDefinition ammoDef && ammoDef.IsCompatibleWith( weaponDef.AmmoType ) )
{
total += item.Count;
}
}
return total;
}
/// <summary>
/// Получить прогресс перезарядки (0-1)
/// </summary>
public float GetReloadProgress()
{
if ( !IsReloading ) return 1f;
var weaponDef = GetWeaponDefinition();
if ( weaponDef == null ) return 1f;
return MathX.Clamp( TimeSinceReloadStart / weaponDef.ReloadTime, 0f, 1f );
}
}

View File

@@ -0,0 +1,72 @@
using Sandbox;
using Sasalka;
namespace Sandbox.Weapons;
/// <summary>
/// Пример фонарика - используемого предмета с собственной логикой
/// Показывает универсальность системы IUseable
/// </summary>
public sealed class Flashlight : Component, IUseable
{
[Property] public GameObject LightSource { get; set; }
[Property] public SoundPointComponent ToggleSound { get; set; }
[Property] public float Cooldown { get; set; } = 0.5f;
private bool _isOn = false;
private TimeSince _lastUseTime;
protected override void OnStart()
{
base.OnStart();
// Изначально фонарик выключен
if (LightSource != null)
{
LightSource.Enabled = false;
}
}
/// <summary>
/// Проверка возможности использования
/// </summary>
public bool CanUse()
{
return IsValid && _lastUseTime >= Cooldown;
}
/// <summary>
/// Использование фонарика (включение/выключение)
/// </summary>
public void Use()
{
if (!CanUse()) return;
_lastUseTime = 0;
ToggleLight();
}
/// <summary>
/// Переключение света
/// </summary>
private void ToggleLight()
{
_isOn = !_isOn;
if (LightSource != null)
{
LightSource.Enabled = _isOn;
}
// Воспроизводим звук
ToggleSound?.StartSound();
// Можно добавить дополнительные эффекты
Log.Info($"Фонарик {( _isOn ? "включен" : "выключен" )}");
}
/// <summary>
/// Получить состояние фонарика
/// </summary>
public bool IsOn => _isOn;
}

108
Code/Weapons/Pistol.cs Normal file
View File

@@ -0,0 +1,108 @@
using Sandbox;
using Sasalka;
namespace Sandbox.Weapons;
/// <summary>
/// Конкретная реализация пистолета
/// Наследуется от BaseWeapon и может переопределять специфичное поведение
/// </summary>
public sealed class Pistol : BaseWeapon
{
[Property] public float RecoilForce { get; set; } = 5f;
[Property] public float RecoilRecoverySpeed { get; set; } = 2f;
private Vector3 _currentRecoil;
private Vector3 _targetRecoil;
protected override void OnStart()
{
base.OnStart();
// Специфичная инициализация для пистолета
_currentRecoil = Vector3.Zero;
_targetRecoil = Vector3.Zero;
}
protected override void OnUpdate()
{
base.OnUpdate();
// Восстановление отдачи
if (!IsReloading && Equipped)
{
RecoverRecoil();
}
}
/// <summary>
/// Переопределяем эффекты выстрела для пистолета
/// </summary>
protected override void PlayShootEffects()
{
base.PlayShootEffects();
// Добавляем отдачу
AddRecoil();
}
/// <summary>
/// Добавление отдачи
/// </summary>
private void AddRecoil()
{
var weaponDef = GetWeaponDefinition();
if (weaponDef == null) return;
// Вертикальная отдача
_targetRecoil += Vector3.Up * RecoilForce;
// Горизонтальная отдача (случайная)
_targetRecoil += Vector3.Random * RecoilForce * 0.3f;
// Применяем отдачу к камере
if (Dedugan.Local?.Camera != null)
{
var camera = Dedugan.Local.Camera;
camera.LocalRotation *= Rotation.From(new Angles(_targetRecoil.x, _targetRecoil.y, _targetRecoil.z));
}
}
/// <summary>
/// Восстановление отдачи
/// </summary>
private void RecoverRecoil()
{
if (_targetRecoil.Length < 0.1f) return;
_targetRecoil = Vector3.Lerp(_targetRecoil, Vector3.Zero, Time.Delta * RecoilRecoverySpeed);
// Применяем восстановление к камере
if (Dedugan.Local?.Camera != null)
{
var camera = Dedugan.Local.Camera;
camera.LocalRotation *= Rotation.From(new Angles(-_targetRecoil.x * Time.Delta * RecoilRecoverySpeed, -_targetRecoil.y * Time.Delta * RecoilRecoverySpeed, -_targetRecoil.z * Time.Delta * RecoilRecoverySpeed));
}
}
/// <summary>
/// Переопределяем трассировку для пистолета (можно добавить специфичную логику)
/// </summary>
protected override void PerformBulletTrace(WeaponItemDefinition weaponDef)
{
base.PerformBulletTrace(weaponDef);
// Дополнительная логика для пистолета может быть добавлена здесь
}
/// <summary>
/// Переопределяем эффекты перезарядки для пистолета
/// </summary>
protected override void PlayReloadEffects()
{
base.PlayReloadEffects();
// Специфичные эффекты перезарядки пистолета
// Например, звук перезарядки пистолета
}
}

265
Code/Weapons/README.md Normal file
View File

@@ -0,0 +1,265 @@
# Универсальная система используемых предметов
## Обзор
Система построена на принципах наследования и интерфейсов. Любой предмет в руках может реализовывать интерфейс `IUseable` и иметь свою уникальную логику использования.
## Архитектура
### IUseable
Интерфейс для всех используемых предметов:
- `CanUse()` - проверка возможности использования
- `Use()` - выполнение действия
- `Cooldown` - время перезарядки между использованиями
### BaseWeapon
Базовый класс для оружия, **наследуется от InventoryItem** и реализует `IUseable`. Содержит:
- Логику стрельбы и трассировки пуль
- Управление патронами (магазин + инвентарь)
- Систему перезарядки
- Эффекты выстрела и попадания
- Автоматическую стрельбу
- **Физику** - автоматическое отключение RigidBody при экипировке
### Flashlight
Пример другого используемого предмета - фонарика. Показывает:
- Как создавать предметы с собственной логикой
- Переключение состояний (вкл/выкл)
- Звуковые эффекты
### WeaponFactory
Универсальная фабрика для создания любых используемых предметов.
## Ключевые особенности
### 1. Наследование от InventoryItem
- **BaseWeapon наследуется от InventoryItem** - не нужно вешать 2 компонента на префаб
- Один компонент = один предмет в инвентаре
- Автоматическое управление состоянием экипировки
### 2. Автоматическая стрельба
- Оружие стреляет автоматически при удержании кнопки мыши **только если IsAutomatic = true**
- Для полуавтоматического оружия (IsAutomatic = false) - только одиночные выстрелы
- **Частота стрельбы берется из WeaponItemDefinition.FireRate**
### 3. Система патронов
- **Оружие начинается с пустым магазином** - патроны нужно зарядить
- **Сохранение патронов в магазине** - при выбрасывании оружия патроны остаются
- Патроны хранятся в инвентаре как отдельные предметы
- **Стрельба зависит ТОЛЬКО от патронов в магазине** - CurrentAmmo
- **HUD показывает патроны в магазине** - не зависит от инвентаря
- **Патроны тратятся только из магазина при выстреле** - CurrentAmmo уменьшается
- **Патроны тратятся из инвентаря только при перезарядке** - загружаются в магазин
- **Перезарядка только вручную** - кнопка R или автоматически при пустом магазине
- **Безопасная обработка неполных магазинов** - загружается столько патронов, сколько есть в инвентаре
### 4. HUD и UI
- **Отображение патронов в магазине** (текущие/максимум)
- **Отображение патронов в инвентаре** (общее количество)
- **Прогресс перезарядки** в реальном времени
- Формат: `15/30 (45)` - где 15 в магазине, 30 максимум, 45 всего в инвентаре
### 5. Физика
- **Автоматическое отключение RigidBody при экипировке**
- **Включение физики при снятии оружия**
- Предметы не падают когда в руках
### 6. Настройка в префабах
- Не нужно создавать отдельные файлы для каждого оружия
- Настройка происходит прямо в инстансе префаба
- Компоненты добавляются автоматически
## Практическое использование
### 1. Создание оружия в редакторе
#### Шаг 1: Создание префаба
1. Создайте новый GameObject в сцене
2. Добавьте модель оружия (SkinnedModelRenderer)
3. Добавьте компонент `BaseWeapon`
4. Настройте параметры в инспекторе:
- `GunRenderer` - ссылка на рендерер оружия
- `MuzzleLight` - свет вспышки
- `ParticlePrefab` - эффекты попадания
- `BloodParticle` - эффекты крови
- `Cooldown` - время между выстрелами
#### Шаг 2: Создание WeaponItemDefinition
1. В Project Settings создайте новый ресурс типа "Weapon Item Definition"
2. Настройте параметры:
```
Name: "Pistol"
Slot: RightHand
HoldType: Pistol
Damage: 25
FireRate: 8
Range: 1000
MagazineSize: 15
AmmoType: "Pistol"
ReloadTime: 2.0
Spread: 0.02
```
3. Укажите префаб в поле `Prefab`
#### Шаг 3: Добавление в игру
```csharp
// В коде игрока или менеджера
var weaponDef = ResourceLibrary.Get<WeaponItemDefinition>("Items/pistol.weapon");
var weaponItem = WeaponFactory.CreateWeaponItem(weaponDef);
player.Inventory.AddItem(weaponItem);
```
### 2. Создание фонарика
#### Шаг 1: Создание префаба фонарика
1. Создайте GameObject с моделью фонарика
2. Добавьте компонент `Flashlight`
3. Настройте параметры:
- `LightSource` - GameObject с источником света
- `ToggleSound` - звук включения/выключения
- `Cooldown` - время между переключениями
#### Шаг 2: Создание ItemDefinition
```csharp
// Создайте BaseItemDefinition или WeaponItemDefinition
// Name: "Flashlight"
// Slot: LeftHand
// Prefab: ссылка на префаб фонарика
```
#### Шаг 3: Использование
```csharp
var flashlightDef = ResourceLibrary.Get<BaseItemDefinition>("Items/flashlight.item");
var flashlightItem = WeaponFactory.CreateItem(flashlightDef);
player.Inventory.AddItem(flashlightItem);
```
### 3. Создание собственного предмета
```csharp
// Создайте новый класс
public sealed class Medkit : Component, IUseable
{
[Property] public float Cooldown { get; set; } = 5f;
[Property] public int HealAmount { get; set; } = 50;
private TimeSince _lastUseTime;
public bool CanUse()
{
return IsValid && _lastUseTime >= Cooldown;
}
public void Use()
{
if (!CanUse()) return;
_lastUseTime = 0;
// Логика лечения
var player = Dedugan.Local;
if (player != null)
{
player.Health = Math.Min(player.Health + HealAmount, 100);
Log.Info($"Игрок вылечен на {HealAmount} HP");
}
}
}
```
## WeaponFactory - подробное руководство
### Основные методы:
#### 1. `CreateUseableItem(BaseItemDefinition, InventoryItem)`
Создает любой используемый предмет:
```csharp
var itemDef = ResourceLibrary.Get<BaseItemDefinition>("path/to/item");
var item = WeaponFactory.CreateUseableItem(itemDef, inventoryItem);
```
#### 2. `CreateWeapon(WeaponItemDefinition, InventoryItem)`
Создает оружие (для обратной совместимости):
```csharp
var weaponDef = ResourceLibrary.Get<WeaponItemDefinition>("path/to/weapon");
var weapon = WeaponFactory.CreateWeapon(weaponDef, inventoryItem);
```
#### 3. `CreateItem(BaseItemDefinition)`
Создает предмет для инвентаря:
```csharp
var itemDef = ResourceLibrary.Get<BaseItemDefinition>("path/to/item");
var inventoryItem = WeaponFactory.CreateItem(itemDef);
```
### Автоматическое определение типа
WeaponFactory автоматически определяет тип предмета по имени:
- **Оружие**: содержит "pistol", "rifle", "gun" → создается `BaseWeapon`
- **Фонарик**: содержит "flashlight", "light" → создается `Flashlight`
- **По умолчанию**: создается `BaseWeapon`
### Примеры использования
#### Создание пистолета:
```csharp
// 1. Создаем предмет для инвентаря
var pistolDef = ResourceLibrary.Get<WeaponItemDefinition>("Items/pistol.weapon");
var pistolItem = WeaponFactory.CreateWeaponItem(pistolDef);
// 2. Добавляем в инвентарь
player.Inventory.AddItem(pistolItem);
// 3. Экипируем
player.Inventory.EquipItem(pistolItem);
```
#### Создание фонарика:
```csharp
// 1. Создаем предмет
var flashlightDef = ResourceLibrary.Get<BaseItemDefinition>("Items/flashlight.item");
var flashlightItem = WeaponFactory.CreateItem(flashlightDef);
// 2. Добавляем в инвентарь
player.Inventory.AddItem(flashlightItem);
```
#### Создание аптечки:
```csharp
// 1. Создаем предмет
var medkitDef = ResourceLibrary.Get<BaseItemDefinition>("Items/medkit.item");
var medkitItem = WeaponFactory.CreateItem(medkitDef);
// 2. Добавляем в инвентарь
player.Inventory.AddItem(medkitItem);
```
## Настройка в префабах
### Для оружия:
1. Добавьте компонент `BaseWeapon`
2. Настройте все необходимые ссылки (рендерер, звуки, эффекты)
3. Установите параметры стрельбы (Cooldown, FireRate и т.д.)
### Для других предметов:
1. Добавьте компонент, реализующий `IUseable`
2. Настройте специфичные параметры
3. Реализуйте логику в методах `CanUse()` и `Use()`
## Преимущества системы
1. **Универсальность** - один интерфейс для всех предметов
2. **Гибкость** - каждый предмет может иметь свою логику
3. **Простота** - настройка в префабах, без создания отдельных файлов
4. **Расширяемость** - легко добавлять новые типы предметов
5. **Производительность** - кэширование и оптимизация
6. **Читаемость** - понятная структура и документация
## Примеры предметов
- **Оружие** - стрельба, перезарядка, патроны
- **Фонарик** - включение/выключение света
- **Аптечка** - лечение игрока
- **Инструменты** - ремонт, строительство
- **Еда** - восстановление здоровья/голода

View File

@@ -1,118 +0,0 @@
using System.Threading.Tasks;
using Sandbox.UI;
using Sasalka;
namespace Sandbox.Weapons;
public sealed class Weapon : AmmoUseableBase
{
[Property] public SkinnedModelRenderer GunRenderer { get; private set; }
[Property] public GameObject MuzzleLight { get; private set; }
[Property] public GameObject particlePrefab { get; set; }
[Property] public GameObject bloodParticle { get; set; }
[Property] public WeaponItemDefinition WeaponDefinition { get; set; }
private SoundPointComponent _sound;
private Rigidbody _rigidbody;
protected override void OnStart()
{
base.OnStart();
_sound = GameObject.GetComponent<SoundPointComponent>(true);
}
public override void OnEquipped()
{
_rigidbody = GameObject.Components.Get<Rigidbody>();
_rigidbody.Enabled = false;
GameObject.Components.Get<PickupItem>().Enabled = false;
}
protected override WeaponItemDefinition GetWeaponDefinition()
{
return WeaponDefinition;
}
public void Attack()
{
AttackEffects();
Vector3 startPos = Scene.Camera.WorldPosition;
Vector3 dir = Scene.Camera.WorldRotation.Forward;
float maxDistance = WeaponDefinition?.Range ?? 1000f;
var tr = Scene.Trace
.Ray(startPos, startPos + dir * maxDistance)
.IgnoreGameObjectHierarchy(Dedugan.Local.GameObject)
.WithoutTags("weapon")
.UseHitboxes()
.Run();
if (tr.Hit)
{
if (tr.Hitbox != null)
{
var boneIndex = tr.Hitbox.Bone.Index;
var components = tr.GameObject.Components;
// Кэшируем компоненты для избежания повторных поисков
var dedugan = components.Get<Dedugan>();
var enemy = components.Get<Enemy>();
var hasTarget = dedugan.IsValid() || enemy.IsValid();
if (hasTarget)
{
CreateHitEffects(tr.EndPosition, tr.Normal, true);
}
if (dedugan.IsValid())
{
dedugan.ReportHit(dir, boneIndex);
}
if (enemy.IsValid())
{
enemy.ReportHit(dir, boneIndex);
}
}
else
{
CreateHitEffects(tr.EndPosition, tr.Normal);
}
}
}
[Rpc.Broadcast]
private void CreateHitEffects(Vector3 position, Vector3 normal, bool blood = false)
{
var rot = Rotation.LookAt(normal);
DestroyAsync(
blood
? bloodParticle.Clone(position, rot)
: particlePrefab.Clone(position, rot), 0.5f);
}
async void DestroyAsync(GameObject go, float delay)
{
await GameTask.DelaySeconds(delay);
go.Destroy();
}
[Rpc.Broadcast]
public void AttackEffects()
{
_sound?.StartSound();
MuzzleLight.Enabled = true;
GunRenderer.Set("Fire", true);
GameTask.DelaySeconds(0.05f).ContinueWith((_) =>
{
MuzzleLight.Enabled = false;
});
}
protected override void OnUse()
{
Attack();
}
}

View File

@@ -0,0 +1,114 @@
using Sandbox;
using Sasalka;
namespace Sandbox.Weapons;
/// <summary>
/// Фабрика для создания используемых предметов с правильной настройкой компонентов
/// </summary>
public static class WeaponFactory
{
/// <summary>
/// Создать используемый предмет с правильной настройкой
/// </summary>
public static GameObject CreateUseableItem(BaseItemDefinition itemDef, InventoryItem inventoryItem = null)
{
if (itemDef?.Prefab == null) return null;
var itemGo = itemDef.Prefab.Clone();
// Проверяем, есть ли уже компонент оружия
if (!itemGo.Components.TryGet<BaseWeapon>(out var weapon))
{
// Определяем тип предмета по имени или другим параметрам
if (itemDef.Name.ToLower().Contains("pistol") || itemDef.Name.ToLower().Contains("rifle") || itemDef.Name.ToLower().Contains("gun"))
{
// Создаем оружие как InventoryItem
weapon = itemGo.Components.Create<BaseWeapon>();
weapon.Definition = itemDef;
}
else if (itemDef.Name.ToLower().Contains("flashlight") || itemDef.Name.ToLower().Contains("light"))
{
// Создаем фонарик
var flashlight = itemGo.Components.Create<Flashlight>();
}
else
{
// По умолчанию создаем базовое оружие
weapon = itemGo.Components.Create<BaseWeapon>();
weapon.Definition = itemDef;
}
}
// Настраиваем компоненты
SetupItemComponents(itemGo, itemDef);
return itemGo;
}
/// <summary>
/// Создать оружие с правильной настройкой (для обратной совместимости)
/// </summary>
public static GameObject CreateWeapon(WeaponItemDefinition weaponDef, InventoryItem inventoryItem = null)
{
return CreateUseableItem(weaponDef, inventoryItem);
}
/// <summary>
/// Настройка компонентов предмета
/// </summary>
private static void SetupItemComponents(GameObject itemGo, BaseItemDefinition itemDef)
{
// Настраиваем рендерер
if (itemGo.Components.TryGet<SkinnedModelRenderer>(out var renderer))
{
// Можно добавить специфичную настройку рендерера
}
// Настраиваем звук
if (itemGo.Components.TryGet<SoundPointComponent>(out var sound))
{
// Можно добавить специфичную настройку звука
}
// Настраиваем физику
if (itemGo.Components.TryGet<Rigidbody>(out var rigidbody))
{
// Можно добавить специфичную настройку физики
}
}
/// <summary>
/// Создать предмет для инвентаря
/// </summary>
public static InventoryItem CreateItem(BaseItemDefinition itemDef)
{
// Для оружия создаем BaseWeapon, для остального - обычный InventoryItem
if (itemDef is WeaponItemDefinition)
{
return new BaseWeapon
{
Definition = itemDef,
Count = 1
};
}
return new InventoryItem
{
Definition = itemDef,
Count = 1
};
}
/// <summary>
/// Создать предмет оружия для инвентаря (для обратной совместимости)
/// </summary>
public static InventoryItem CreateWeaponItem(WeaponItemDefinition weaponDef)
{
return new BaseWeapon
{
Definition = weaponDef,
Count = 1
};
}
}