sasalka/Code/Weapons/BaseWeapon.cs
2025-06-28 19:17:59 +03:00

590 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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