asd
This commit is contained in:
@@ -19,7 +19,7 @@ public class WeaponItemDefinition : BaseItemDefinition, IEquipable
|
||||
public float Damage { get; set; } = 10f;
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public float FireRate { get; set; } = 1f;
|
||||
public float FireRate { get; set; } = 10f; // Выстрелов в секунду
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public float Range { get; set; } = 1000f;
|
||||
@@ -30,6 +30,15 @@ public class WeaponItemDefinition : BaseItemDefinition, IEquipable
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public string AmmoType { get; set; } = "Pistol";
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public float ReloadTime { get; set; } = 2f;
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public float Spread { get; set; } = 0.02f; // Разброс при стрельбе
|
||||
|
||||
[Property, Category( "Weapon Properties" )]
|
||||
public bool IsAutomatic { get; set; } = true; // Всегда автоматический режим
|
||||
|
||||
public override Inventar.InventorySlot? GetSlot() => Slot;
|
||||
|
||||
public override ItemCategory Category => ItemCategory.Weapon;
|
||||
|
||||
@@ -65,36 +65,37 @@ public class Inventar : Component
|
||||
/// <summary>
|
||||
/// Добавляет предмет в инвентарь, распределяя по существующим и новым стекам. Возвращает остаток, который не удалось добавить (или 0, если всё добавлено).
|
||||
/// </summary>
|
||||
public int AddItem(InventoryItem item)
|
||||
public int AddItem( InventoryItem item )
|
||||
{
|
||||
if (item == null || item.Definition == null || item.Count <= 0)
|
||||
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))
|
||||
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)
|
||||
int canAdd = Math.Min( toAdd, stack.Definition.MaxCount - stack.Count );
|
||||
if ( canAdd > 0 )
|
||||
{
|
||||
stack.Count += canAdd;
|
||||
toAdd -= canAdd;
|
||||
OnChanged?.Invoke();
|
||||
OnItemAdded?.Invoke(stack);
|
||||
OnItemAdded?.Invoke( stack );
|
||||
}
|
||||
if (toAdd <= 0) return 0;
|
||||
|
||||
if ( toAdd <= 0 ) return 0;
|
||||
}
|
||||
|
||||
// 2. Добавляем новые стаки, если есть место
|
||||
while (toAdd > 0 && (UnlimitedSlots || Items.Count < MaxInventorySlots))
|
||||
while ( toAdd > 0 && (UnlimitedSlots || Items.Count < MaxInventorySlots) )
|
||||
{
|
||||
int stackCount = Math.Min(toAdd, item.Definition.MaxCount);
|
||||
int stackCount = Math.Min( toAdd, item.Definition.MaxCount );
|
||||
var newStack = new InventoryItem { Definition = item.Definition, Count = stackCount };
|
||||
Items.Add(newStack);
|
||||
Items.Add( newStack );
|
||||
toAdd -= stackCount;
|
||||
OnChanged?.Invoke();
|
||||
OnItemAdded?.Invoke(newStack);
|
||||
OnItemAdded?.Invoke( newStack );
|
||||
}
|
||||
|
||||
// 3. Возвращаем остаток, если не всё удалось добавить
|
||||
@@ -149,6 +150,7 @@ public class Inventar : Component
|
||||
// Экипировать новый предмет
|
||||
EquippedItems[slot.Value] = item;
|
||||
item.Equipped = true;
|
||||
item.OnEquipped();
|
||||
OnEquipped?.Invoke( item );
|
||||
OnChanged?.Invoke();
|
||||
return true;
|
||||
@@ -159,23 +161,23 @@ public class Inventar : Component
|
||||
if ( item == null || !Items.Contains( item ) )
|
||||
return;
|
||||
|
||||
// Создаем копию предмета для выбрасывания
|
||||
var droppedItem = new InventoryItem
|
||||
{
|
||||
Definition = item.Definition,
|
||||
Count = item.Count // Выбрасываем всю стопку
|
||||
};
|
||||
// // Создаем копию предмета для выбрасывания
|
||||
// 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 = droppedItem.Count;
|
||||
inventoryItem.Definition = droppedItem.Definition;
|
||||
inventoryItem.Count = item.Count;
|
||||
inventoryItem.Definition = item.Definition;
|
||||
}
|
||||
|
||||
gO.NetworkSpawn();
|
||||
|
||||
|
||||
// Удаляем весь предмет из инвентаря
|
||||
RemoveItem( item, item.Count );
|
||||
}
|
||||
@@ -191,6 +193,7 @@ public class Inventar : Component
|
||||
{
|
||||
EquippedItems.Remove( slotToRemove );
|
||||
item.Equipped = false;
|
||||
item.OnUnEquipped();
|
||||
OnUnEquipped?.Invoke( item );
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
@@ -240,7 +243,7 @@ public class Inventar : Component
|
||||
{
|
||||
if ( UnlimitedSlots )
|
||||
return int.MaxValue;
|
||||
|
||||
|
||||
return Math.Max( 0, MaxInventorySlots - Items.Count );
|
||||
}
|
||||
|
||||
@@ -248,7 +251,7 @@ public class Inventar : Component
|
||||
{
|
||||
if ( UnlimitedSlots )
|
||||
return false;
|
||||
|
||||
|
||||
return Items.Count >= MaxInventorySlots;
|
||||
}
|
||||
|
||||
@@ -256,7 +259,7 @@ public class Inventar : Component
|
||||
{
|
||||
if ( UnlimitedSlots )
|
||||
return 0f;
|
||||
|
||||
|
||||
return (float)Items.Count / MaxInventorySlots * 100f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,18 @@ public class InventoryItem : Component
|
||||
{
|
||||
if ( GameObject.Components.TryGet<PickupItem>( out var item ) )
|
||||
{
|
||||
item.Label = Definition?.Name ?? "Unknown Item";
|
||||
string itemName = Definition?.Name;
|
||||
|
||||
if ( string.IsNullOrEmpty( itemName ) )
|
||||
{
|
||||
item.Label = "Unknown Item";
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Label = Count > 1
|
||||
? $"{itemName} x{Count}"
|
||||
: itemName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +69,22 @@ public class InventoryItem : Component
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Виртуальный метод для экипировки предмета
|
||||
/// </summary>
|
||||
public virtual void OnEquipped()
|
||||
{
|
||||
Equipped = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Виртуальный метод для снятия предмета
|
||||
/// </summary>
|
||||
public virtual void OnUnEquipped()
|
||||
{
|
||||
Equipped = false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Definition?.Name ?? "Unknown"} x{Count}";
|
||||
|
||||
@@ -1,85 +1,14 @@
|
||||
namespace Sasalka;
|
||||
|
||||
/// <summary>
|
||||
/// Базовый класс для предметов, которые используют патроны
|
||||
/// Теперь используется только для совместимости со старой системой
|
||||
/// </summary>
|
||||
public abstract class AmmoUseableBase : UseableBase
|
||||
{
|
||||
private WeaponItemDefinition _cachedWeaponDef;
|
||||
private InventoryItem _cachedAmmoItem;
|
||||
|
||||
protected InventoryItem AmmoItem => FindAmmoItem();
|
||||
|
||||
private InventoryItem FindAmmoItem()
|
||||
protected override void OnUse()
|
||||
{
|
||||
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;
|
||||
var baseCanUse = base.CanUse();
|
||||
var hasAmmo = ammo != null && ammo.Count > 0;
|
||||
|
||||
return baseCanUse && hasAmmo;
|
||||
}
|
||||
|
||||
public override void Use()
|
||||
{
|
||||
if (!CanUse())
|
||||
return;
|
||||
|
||||
OnUse();
|
||||
|
||||
var ammo = AmmoItem;
|
||||
if (ammo != null)
|
||||
{
|
||||
// Уменьшаем количество патронов
|
||||
if (ammo.TryRemoveCount(1))
|
||||
{
|
||||
// Если патроны закончились, удаляем предмет из инвентаря
|
||||
if (ammo.Count <= 0)
|
||||
{
|
||||
Dedugan.Local.Inventory.RemoveItem(ammo);
|
||||
_cachedAmmoItem = null; // Очищаем кэш
|
||||
}
|
||||
}
|
||||
}
|
||||
// Базовая реализация - ничего не делает
|
||||
// Логика использования патронов теперь в BaseWeapon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ public sealed class PickupItem : InteractionButton
|
||||
// Устанавливаем правильную метку для предмета
|
||||
if ( GameObject.Components.TryGet<InventoryItem>( out var inventoryItem ) )
|
||||
{
|
||||
Label = inventoryItem.Definition?.Name ?? "Подобрать";
|
||||
Label = inventoryItem.Definition?.Name != ""
|
||||
? $"{inventoryItem.Definition.Name} x{inventoryItem.Count}"
|
||||
: "Подобрать";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,33 +10,17 @@ public sealed partial class Dedugan : Component
|
||||
[Sync] private bool InAds { get; set; } = false;
|
||||
private AttachmentSlotResolver _resolver;
|
||||
|
||||
// Автоматическая стрельба
|
||||
private bool _isShooting = false;
|
||||
private TimeSince _lastShootTime = 0f;
|
||||
|
||||
void InventoryStart()
|
||||
{
|
||||
if (!Network.IsOwner) return;
|
||||
if ( !Network.IsOwner ) return;
|
||||
|
||||
// Создаем инвентарь как компонент
|
||||
Inventory = GameObject.Components.GetOrCreate<Inventar>();
|
||||
_resolver = new AttachmentSlotResolver(Renderer.GetAttachmentObject);
|
||||
|
||||
// Добавляем тестовые предметы (раскомментируйте для тестирования)
|
||||
var clothingItem = new InventoryItem
|
||||
{
|
||||
Definition = ResourceLibrary.Get<ClothingItemDefinition>("Items/Cloth/cloth_pijama.clitem")
|
||||
};
|
||||
Inventory.AddItem(clothingItem);
|
||||
|
||||
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.inv")
|
||||
};
|
||||
ammoItem.Count = 30;
|
||||
Inventory.AddItem(ammoItem);
|
||||
_resolver = new AttachmentSlotResolver( Renderer.GetAttachmentObject );
|
||||
|
||||
Inventory.OnEquipped += OnItemEquipped;
|
||||
Inventory.OnUnEquipped += OnItemUnEquipped;
|
||||
@@ -44,29 +28,29 @@ public sealed partial class Dedugan : Component
|
||||
Inventory.OnItemRemoved += OnItemRemoved;
|
||||
}
|
||||
|
||||
private void OnItemEquipped(InventoryItem item)
|
||||
private void OnItemEquipped( InventoryItem item )
|
||||
{
|
||||
// Очищаем кэши при экипировке предмета
|
||||
_useableCache.Clear();
|
||||
_resolver?.ClearCache();
|
||||
|
||||
if (item?.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid())
|
||||
|
||||
if ( item?.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid() )
|
||||
{
|
||||
var go = weaponDef.Prefab.Clone();
|
||||
|
||||
AnimationHelper.HoldType = weaponDef.HoldType;
|
||||
|
||||
switch (weaponDef.Slot)
|
||||
switch ( weaponDef.Slot )
|
||||
{
|
||||
case Inventar.InventorySlot.LeftHand | Inventar.InventorySlot.RightHand:
|
||||
case Inventar.InventorySlot.RightHand:
|
||||
go.Parent = Renderer.GetAttachmentObject("hold_R");
|
||||
go.Parent = Renderer.GetAttachmentObject( "hold_R" );
|
||||
break;
|
||||
case Inventar.InventorySlot.LeftHand:
|
||||
go.Parent = Renderer.GetAttachmentObject("hold_L");
|
||||
go.Parent = Renderer.GetAttachmentObject( "hold_L" );
|
||||
break;
|
||||
default:
|
||||
go.Parent = Renderer.GetAttachmentObject("forward_reference_modelspace");
|
||||
go.Parent = Renderer.GetAttachmentObject( "forward_reference_modelspace" );
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -74,9 +58,11 @@ public sealed partial class Dedugan : Component
|
||||
go.LocalRotation = weaponDef.WeaponDefinition.Rotation;
|
||||
go.LocalScale = weaponDef.WeaponDefinition.Scale;
|
||||
|
||||
if (go.Components.TryGet<UseableBase>(out var useable))
|
||||
// Получаем компонент оружия и вызываем экипировку
|
||||
if ( go.Components.TryGet<BaseWeapon>( out var weapon ) )
|
||||
{
|
||||
useable.Equipped = true;
|
||||
weapon.OnEquipped();
|
||||
_useableCache[weaponDef.Slot] = (go, weapon);
|
||||
}
|
||||
|
||||
go.NetworkSpawn();
|
||||
@@ -90,65 +76,70 @@ public sealed partial class Dedugan : Component
|
||||
};
|
||||
|
||||
AnimationHelper.Handedness = hand;
|
||||
RpcSetHoldAnimation(weaponDef.HoldType, hand);
|
||||
RpcSetHoldAnimation( weaponDef.HoldType, hand );
|
||||
InAds = true;
|
||||
}
|
||||
else if (item?.Definition is ClothingItemDefinition clothingDef)
|
||||
else if ( item?.Definition is ClothingItemDefinition clothingDef )
|
||||
{
|
||||
WearWorkshop(new List<string>() { clothingDef.ClothUrl });
|
||||
WearWorkshop( new List<string>() { clothingDef.ClothUrl } );
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemUnEquipped(InventoryItem item)
|
||||
private void OnItemUnEquipped( InventoryItem item )
|
||||
{
|
||||
// Очищаем кэши при снятии предмета
|
||||
_useableCache.Clear();
|
||||
_resolver?.ClearCache();
|
||||
|
||||
if (item?.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid())
|
||||
|
||||
if ( item?.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid() )
|
||||
{
|
||||
switch (weaponDef.Slot)
|
||||
// Вызываем OnUnEquipped для оружия
|
||||
if ( _useableCache.TryGetValue( weaponDef.Slot, out var cached ) )
|
||||
{
|
||||
if ( cached.useable is BaseWeapon weapon )
|
||||
{
|
||||
weapon.OnUnEquipped();
|
||||
}
|
||||
}
|
||||
|
||||
switch ( weaponDef.Slot )
|
||||
{
|
||||
case Inventar.InventorySlot.LeftHand | Inventar.InventorySlot.RightHand:
|
||||
case Inventar.InventorySlot.RightHand:
|
||||
case Inventar.InventorySlot.LeftHand:
|
||||
var attachmentName = !weaponDef.Slot.HasFlag(Inventar.InventorySlot.RightHand)
|
||||
var attachmentName = !weaponDef.Slot.HasFlag( Inventar.InventorySlot.RightHand )
|
||||
? "hold_L"
|
||||
: "hold_R";
|
||||
|
||||
Renderer.GetAttachmentObject(attachmentName).Children.ForEach(child => child.Destroy());
|
||||
RpcSetHoldAnimation(CitizenAnimationHelper.HoldTypes.None, CitizenAnimationHelper.Hand.Both);
|
||||
Renderer.GetAttachmentObject( attachmentName ).Children.ForEach( child => child.Destroy() );
|
||||
RpcSetHoldAnimation( CitizenAnimationHelper.HoldTypes.None, CitizenAnimationHelper.Hand.Both );
|
||||
break;
|
||||
default:
|
||||
Renderer.GetAttachmentObject("forward_reference_modelspace").Children
|
||||
.ForEach(child => child.Destroy());
|
||||
Renderer.GetAttachmentObject( "forward_reference_modelspace" ).Children
|
||||
.ForEach( child => child.Destroy() );
|
||||
break;
|
||||
}
|
||||
|
||||
InAds = false;
|
||||
}
|
||||
else if (item?.Definition is ClothingItemDefinition clothingDef)
|
||||
else if ( item?.Definition is ClothingItemDefinition clothingDef )
|
||||
{
|
||||
StripByName(clothingDef.Description);
|
||||
StripByName( clothingDef.Description );
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemAdded(InventoryItem item)
|
||||
private void OnItemAdded( InventoryItem item )
|
||||
{
|
||||
// Очищаем кэши при добавлении предмета
|
||||
_useableCache.Clear();
|
||||
_resolver?.ClearCache();
|
||||
// Кэш не очищаем при добавлении предметов, чтобы не нарушить работу оружия
|
||||
}
|
||||
|
||||
private void OnItemRemoved(InventoryItem item)
|
||||
private void OnItemRemoved( InventoryItem item )
|
||||
{
|
||||
// Очищаем кэши при удалении предмета
|
||||
_useableCache.Clear();
|
||||
_resolver?.ClearCache();
|
||||
// Кэш не очищаем при удалении предметов, чтобы не нарушить работу оружия
|
||||
}
|
||||
|
||||
[Rpc.Broadcast]
|
||||
public void RpcSetHoldAnimation(CitizenAnimationHelper.HoldTypes HoldType, CitizenAnimationHelper.Hand hand)
|
||||
public void RpcSetHoldAnimation( CitizenAnimationHelper.HoldTypes HoldType, CitizenAnimationHelper.Hand hand )
|
||||
{
|
||||
AnimationHelper.HoldType = HoldType;
|
||||
AnimationHelper.Handedness = hand;
|
||||
@@ -156,59 +147,158 @@ public sealed partial class Dedugan : Component
|
||||
|
||||
void InventoryUpdate()
|
||||
{
|
||||
if (!Network.IsOwner) return;
|
||||
if ( !Network.IsOwner ) return;
|
||||
|
||||
if (Input.Pressed("Attack1"))
|
||||
// Обработка автоматической стрельбы
|
||||
HandleAutomaticShooting();
|
||||
|
||||
// Обработка перезарядки
|
||||
if ( Input.Pressed( "Reload" ) )
|
||||
{
|
||||
if (UseSystem.TryUse(this))
|
||||
{
|
||||
Attack();
|
||||
}
|
||||
TryReloadWeapon();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<IUseable> GetUsables()
|
||||
/// <summary>
|
||||
/// Обработка автоматической стрельбы
|
||||
/// </summary>
|
||||
private void HandleAutomaticShooting()
|
||||
{
|
||||
// Кэшируем слоты для избежания повторного создания массива
|
||||
var slots = new[] { Inventar.InventorySlot.LeftHand, Inventar.InventorySlot.RightHand };
|
||||
|
||||
foreach (var slot in slots)
|
||||
var weapon = GetEquippedWeapon();
|
||||
if ( weapon == null ) return;
|
||||
|
||||
var weaponDef = weapon.GetWeaponDefinition();
|
||||
if ( weaponDef == null ) return;
|
||||
|
||||
// Начало стрельбы
|
||||
if ( Input.Pressed( "Attack1" ) )
|
||||
{
|
||||
if (!Inventory.EquippedItems.TryGetValue(slot, out var item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var holder = _resolver.GetSlotObject(slot);
|
||||
if (holder == null) continue;
|
||||
|
||||
var heldObject = holder.Children.FirstOrDefault();
|
||||
if (heldObject == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
_isShooting = true;
|
||||
TryUseWeapon();
|
||||
}
|
||||
|
||||
// Проверяем кэш
|
||||
if (_useableCache.TryGetValue(slot, out var cached) && cached.obj == heldObject)
|
||||
{
|
||||
if (cached.useable != null)
|
||||
yield return cached.useable;
|
||||
}
|
||||
else
|
||||
{
|
||||
var useable = heldObject.Components.Get<IUseable>();
|
||||
_useableCache[slot] = (heldObject, useable);
|
||||
// Продолжение стрельбы (только если оружие автоматическое)
|
||||
if ( Input.Down( "Attack1" ) && _isShooting && weaponDef.IsAutomatic )
|
||||
{
|
||||
TryUseWeapon();
|
||||
}
|
||||
|
||||
if (useable != null)
|
||||
yield return useable;
|
||||
// Остановка стрельбы
|
||||
if ( Input.Released( "Attack1" ) )
|
||||
{
|
||||
_isShooting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Попытка использования экипированного предмета
|
||||
/// </summary>
|
||||
private void TryUseWeapon()
|
||||
{
|
||||
var useable = GetEquippedUseable();
|
||||
if ( useable != null && useable.CanUse() )
|
||||
{
|
||||
useable.Use();
|
||||
Attack(); // Анимация атаки
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Попытка перезарядки экипированного оружия
|
||||
/// </summary>
|
||||
private void TryReloadWeapon()
|
||||
{
|
||||
var weapon = GetEquippedWeapon();
|
||||
if ( weapon != null && !weapon.IsReloading )
|
||||
{
|
||||
weapon.StartReload();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить экипированный используемый предмет
|
||||
/// </summary>
|
||||
private IUseable GetEquippedUseable()
|
||||
{
|
||||
// Проверяем правую руку
|
||||
if ( Inventory.EquippedItems.TryGetValue( Inventar.InventorySlot.RightHand, out var rightHandItem ) )
|
||||
{
|
||||
if ( _useableCache.TryGetValue( Inventar.InventorySlot.RightHand, out var cached ) &&
|
||||
cached.useable != null )
|
||||
{
|
||||
return cached.useable;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем левую руку
|
||||
if ( Inventory.EquippedItems.TryGetValue( Inventar.InventorySlot.LeftHand, out var leftHandItem ) )
|
||||
{
|
||||
if ( _useableCache.TryGetValue( Inventar.InventorySlot.LeftHand, out var cached ) &&
|
||||
cached.useable != null )
|
||||
{
|
||||
return cached.useable;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем обе руки
|
||||
if ( Inventory.EquippedItems.TryGetValue( Inventar.InventorySlot.LeftHand | Inventar.InventorySlot.RightHand,
|
||||
out var bothHandsItem ) )
|
||||
{
|
||||
if ( _useableCache.TryGetValue( Inventar.InventorySlot.LeftHand | Inventar.InventorySlot.RightHand,
|
||||
out var cached ) && cached.useable != null )
|
||||
{
|
||||
return cached.useable;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить экипированное оружие
|
||||
/// </summary>
|
||||
private BaseWeapon GetEquippedWeapon()
|
||||
{
|
||||
var useable = GetEquippedUseable();
|
||||
return useable as BaseWeapon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить информацию о патронах для UI
|
||||
/// </summary>
|
||||
public (int current, int max, int totalInInventory) GetAmmoInfo()
|
||||
{
|
||||
var weapon = GetEquippedWeapon();
|
||||
if ( weapon == null )
|
||||
{
|
||||
return (0, 0, 0);
|
||||
}
|
||||
|
||||
return weapon.GetAmmoInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить общее количество патронов в инвентаре (для UI)
|
||||
/// </summary>
|
||||
public int GetTotalInInventory()
|
||||
{
|
||||
var ammoInfo = GetAmmoInfo();
|
||||
return ammoInfo.totalInInventory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить прогресс перезарядки
|
||||
/// </summary>
|
||||
public float GetReloadProgress()
|
||||
{
|
||||
var weapon = GetEquippedWeapon();
|
||||
return weapon?.GetReloadProgress() ?? 1f;
|
||||
}
|
||||
|
||||
[Rpc.Broadcast]
|
||||
void Attack()
|
||||
{
|
||||
Renderer.Set("b_attack", true);
|
||||
Renderer.Set( "b_attack", true );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ using Sandbox;
|
||||
using Sandbox.Citizen;
|
||||
using Sasalka;
|
||||
using ShrimpleCharacterController;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
public sealed partial class Dedugan : Component, IUseContext, Component.INetworkSpawn
|
||||
{
|
||||
@@ -121,4 +123,14 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork
|
||||
{
|
||||
DrawDebugGizmos();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Реализация интерфейса IUseContext
|
||||
/// Возвращает список используемых предметов
|
||||
/// </summary>
|
||||
public IEnumerable<IUseable> GetUsables()
|
||||
{
|
||||
// Возвращаем пустой список, так как теперь оружие управляется через новую систему
|
||||
return Enumerable.Empty<IUseable>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
|
||||
<root>
|
||||
<div class="crosshair"></div>
|
||||
<div class="@( HasWeapon ? "" : "hidden" )">
|
||||
<div class="ammo-counter">
|
||||
<div class="ammo-info">
|
||||
<div class="current-ammo">@CurrentAmmo</div>
|
||||
<div class="separator">/</div>
|
||||
<div class="max-ammo">@TotalInInventory</div>
|
||||
|
||||
@* <div class="inventory-ammo">(@MaxAmmo - вместимость магазина)</div> *@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ( IsReloading )
|
||||
{
|
||||
<div class="reload-progress">
|
||||
<div class="reload-bar" style="width: @( ReloadProgress * 100 )%"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</root>
|
||||
|
||||
<style>
|
||||
@@ -19,7 +37,107 @@
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ammo-counter {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
right: 50px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px;
|
||||
color: white;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
|
||||
.ammo-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.current-ammo {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.max-ammo {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.inventory-ammo {
|
||||
color: #4CAF50;
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reload-progress {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
right: 50px;
|
||||
height: 4px;
|
||||
width: 50px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.reload-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ff6b6b, #ff8e8e);
|
||||
transition: width 0.1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private int CurrentAmmo { get; set; } = 0;
|
||||
private int MaxAmmo { get; set; } = 0;
|
||||
private bool IsReloading { get; set; } = false;
|
||||
private float ReloadProgress { get; set; } = 0f;
|
||||
private int TotalInInventory { get; set; } = 0;
|
||||
private bool HasWeapon { get; set; } = false;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if ( Dedugan.Local == null ) return;
|
||||
|
||||
var ammoInfo = Dedugan.Local.GetAmmoInfo();
|
||||
CurrentAmmo = ammoInfo.current;
|
||||
MaxAmmo = ammoInfo.max;
|
||||
TotalInInventory = ammoInfo.totalInInventory;
|
||||
|
||||
ReloadProgress = Dedugan.Local.GetReloadProgress();
|
||||
IsReloading = ReloadProgress < 1f;
|
||||
|
||||
// Проверяем, есть ли оружие в руках (если MaxAmmo > 0, значит есть оружие)
|
||||
HasWeapon = MaxAmmo > 0;
|
||||
}
|
||||
|
||||
protected override int BuildHash()
|
||||
{
|
||||
if ( Dedugan.Local == null || !HasWeapon )
|
||||
return -1;
|
||||
|
||||
var hash = new System.HashCode();
|
||||
hash.Add( CurrentAmmo );
|
||||
hash.Add( MaxAmmo );
|
||||
hash.Add( TotalInInventory );
|
||||
hash.Add( IsReloading );
|
||||
hash.Add( ReloadProgress );
|
||||
hash.Add( HasWeapon );
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
532
Code/Weapons/BaseWeapon.cs
Normal file
532
Code/Weapons/BaseWeapon.cs
Normal 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 );
|
||||
}
|
||||
}
|
||||
72
Code/Weapons/Flashlight.cs
Normal file
72
Code/Weapons/Flashlight.cs
Normal 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
108
Code/Weapons/Pistol.cs
Normal 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
265
Code/Weapons/README.md
Normal 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. **Читаемость** - понятная структура и документация
|
||||
|
||||
## Примеры предметов
|
||||
|
||||
- **Оружие** - стрельба, перезарядка, патроны
|
||||
- **Фонарик** - включение/выключение света
|
||||
- **Аптечка** - лечение игрока
|
||||
- **Инструменты** - ремонт, строительство
|
||||
- **Еда** - восстановление здоровья/голода
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
114
Code/Weapons/WeaponFactory.cs
Normal file
114
Code/Weapons/WeaponFactory.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user