using System.Threading.Tasks; using Sandbox; using Sandbox.Citizen; using Sasalka; using System.Collections.Generic; namespace Sandbox.Weapons; /// /// Базовый класс для всех видов оружия /// Содержит всю логику стрельбы и управления патронами /// 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; // Оптимизация обновлений private TimeSince _lastEffectsUpdate = 0f; private const float EFFECTS_UPDATE_INTERVAL = 0.1f; // Обновляем эффекты каждые 100мс // IUseable реализация - Cooldown вычисляется на основе FireRate public float Cooldown { get { var weaponDef = GetWeaponDefinition(); return weaponDef != null ? 1f / weaponDef.FireRate : 0.1f; } } /// /// Установить ссылку на InventoryItem /// public void SetInventoryItem( InventoryItem item ) { _inventoryItem = item; // Инициализируем оружие после установки InventoryItem InitializeWeapon(); } /// /// Получить количество патронов в магазине из InventoryItem /// public int CurrentAmmo { get => _inventoryItem?.MagazineAmmo ?? 0; set { if ( _inventoryItem != null ) { _inventoryItem.MagazineAmmo = value; } } } protected override void OnStart() { base.OnStart(); // Получаем компоненты _sound = GameObject.GetComponent( true ); _pickupItem = GameObject.Components.Get(); // Инициализация оружия будет вызвана после установки InventoryItem } protected override void OnUpdate() { base.OnUpdate(); if ( !IsValid || !Equipped ) return; // Проверяем завершение перезарядки if ( IsReloading && TimeSinceReloadStart >= GetWeaponDefinition()?.ReloadTime ) { FinishReload(); } // Обновляем эффекты с интервалом для оптимизации if ( _lastEffectsUpdate >= EFFECTS_UPDATE_INTERVAL ) { UpdateEffects(); _lastEffectsUpdate = 0f; } } /// /// Инициализация оружия /// 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; } /// /// Получить определение оружия /// public WeaponItemDefinition GetWeaponDefinition() { return _weaponDefinition ??= Definition as WeaponItemDefinition; } /// /// Экипировка оружия /// public override void OnEquipped() { base.OnEquipped(); // Кэшируем Rigidbody при экипировке if ( _rigidbody == null ) _rigidbody = GameObject.Components.Get(); if ( _rigidbody != null ) _rigidbody.Enabled = false; if ( _pickupItem == null ) _pickupItem = GameObject.Components.Get(); if ( _pickupItem != null ) _pickupItem.Enabled = false; } /// /// Снятие оружия /// public override void OnUnEquipped() { base.OnUnEquipped(); // Используем кэшированный Rigidbody if ( _rigidbody != null ) _rigidbody.Enabled = true; if ( _pickupItem != null ) _pickupItem.Enabled = true; } // IUseable реализация /// /// Проверка возможности использования /// public virtual bool CanUse() { var weaponDef = GetWeaponDefinition(); if ( weaponDef == null ) return false; return IsValid && Equipped && !IsReloading && TimeSinceLastShot >= 1f / weaponDef.FireRate; } /// /// Использование оружия (выстрел) /// public virtual void Use() { if ( !CanUse() ) return; TryShoot(); } /// /// Попытка выстрела /// 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; } /// /// Проверка возможности выстрела /// public bool CanShoot() { if ( !IsValid || !Equipped || IsReloading ) return false; var weaponDef = GetWeaponDefinition(); if ( weaponDef == null ) return false; // Проверяем только патроны в магазине return CurrentAmmo > 0 && TimeSinceLastShot >= 1f / weaponDef.FireRate; } /// /// Проверка наличия патронов в инвентаре /// 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; } /// /// Выполнение выстрела /// protected virtual void PerformShot() { var weaponDef = GetWeaponDefinition(); if ( weaponDef == null ) return; // Уменьшаем патроны в магазине CurrentAmmo--; TimeSinceLastShot = 0; // Эффекты выстрела PlayShootEffects(); // Трассировка пули PerformBulletTrace( weaponDef ); } /// /// Трассировка пули /// 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 ); } } /// /// Обработка попадания /// 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(); var enemy = components.Get(); 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 ); } } /// /// Начать перезарядку /// 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(); } /// /// Завершить перезарядку /// 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 ); } /// /// Загрузить патроны из инвентаря /// 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(); 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}" ); } } } /// /// Воспроизведение эффектов выстрела /// 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 ); } } /// /// Воспроизведение эффектов перезарядки /// protected virtual void PlayReloadEffects() { // Можно добавить звук перезарядки } /// /// Создание эффектов попадания /// [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 ); } } /// /// Обновление эффектов /// protected virtual void UpdateEffects() { // Можно добавить дополнительные эффекты } /// /// Асинхронное уничтожение объекта /// protected async void DestroyAsync( GameObject go, float delay ) { if ( go == null ) return; await GameTask.DelaySeconds( delay ); if ( go.IsValid ) go.Destroy(); } /// /// Получить информацию о патронах для UI /// public (int current, int max, int totalInInventory) GetAmmoInfo() { var weaponDef = GetWeaponDefinition(); var totalInInventory = GetTotalAmmoInInventory(); return (CurrentAmmo, weaponDef?.MagazineSize ?? 0, totalInInventory); } /// /// Получить общее количество патронов в инвентаре /// 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; } /// /// Получить прогресс перезарядки (0-1) /// public float GetReloadProgress() { if ( !IsReloading ) return 1f; var weaponDef = GetWeaponDefinition(); if ( weaponDef == null ) return 1f; return MathX.Clamp( TimeSinceReloadStart / weaponDef.ReloadTime, 0f, 1f ); } }