590 lines
15 KiB
C#
590 lines
15 KiB
C#
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 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;
|
||
private InventoryItem _inventoryItem;
|
||
|
||
// IUseable реализация - Cooldown вычисляется на основе FireRate
|
||
public float Cooldown
|
||
{
|
||
get
|
||
{
|
||
var weaponDef = GetWeaponDefinition();
|
||
return weaponDef != null ? 1f / weaponDef.FireRate : 0.1f;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Установить ссылку на InventoryItem
|
||
/// </summary>
|
||
public void SetInventoryItem( InventoryItem item )
|
||
{
|
||
_inventoryItem = item;
|
||
|
||
// Инициализируем оружие после установки InventoryItem
|
||
InitializeWeapon();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить количество патронов в магазине из InventoryItem
|
||
/// </summary>
|
||
public int CurrentAmmo
|
||
{
|
||
get => _inventoryItem?.MagazineAmmo ?? 0;
|
||
set
|
||
{
|
||
if ( _inventoryItem != null )
|
||
{
|
||
_inventoryItem.MagazineAmmo = value;
|
||
}
|
||
}
|
||
}
|
||
|
||
protected override void OnStart()
|
||
{
|
||
base.OnStart();
|
||
|
||
// Получаем компоненты
|
||
_sound = GameObject.GetComponent<SoundPointComponent>( true );
|
||
_pickupItem = GameObject.Components.Get<PickupItem>();
|
||
|
||
// Инициализация оружия будет вызвана после установки InventoryItem
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Инициализация завершена
|
||
_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;
|
||
|
||
var weaponDef = GetWeaponDefinition();
|
||
float reloadSpeed = weaponDef?.ReloadTime > 0 ? 1f / weaponDef.ReloadTime : 1f;
|
||
|
||
// Анимация перезарядки оружия
|
||
if ( GunRenderer != null )
|
||
{
|
||
GunRenderer.Set( "b_reload", true );
|
||
GunRenderer.Set( "speed_reload", reloadSpeed );
|
||
}
|
||
|
||
// Анимация перезарядки персонажа
|
||
var player = Dedugan.Local;
|
||
if ( player?.Renderer != null )
|
||
{
|
||
player.Renderer.Set( "b_reload", true );
|
||
player.Renderer.Set( "speed_reload", reloadSpeed );
|
||
}
|
||
|
||
// Эффекты перезарядки
|
||
PlayReloadEffects();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Завершить перезарядку
|
||
/// </summary>
|
||
protected virtual void FinishReload()
|
||
{
|
||
IsReloading = false;
|
||
|
||
// Сбрасываем анимацию перезарядки оружия
|
||
if ( GunRenderer != null )
|
||
{
|
||
GunRenderer.Set( "b_reload", false );
|
||
GunRenderer.Set( "speed_reload", 1f );
|
||
}
|
||
|
||
// Сбрасываем анимацию перезарядки персонажа
|
||
var player = Dedugan.Local;
|
||
if ( player?.Renderer != null )
|
||
{
|
||
player.Renderer.Set( "b_reload", false );
|
||
player.Renderer.Set( "speed_reload", 1f );
|
||
}
|
||
|
||
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 );
|
||
}
|
||
}
|