Add a LOT more dakka (#1033)
* Start adding flashy flash * Change slop Might give a smoother decline * flashy flash * Add flashbang and flash projectiles Bang bang bang pull my flash trigger * Add collision check to area flash * Flash cleanupo * flash.ogg mixed to mono * Adjusted flash curve again * Enhancing flashes with unshaded and lights and shit Still a WIP * Add the other ballistic gun types Re-organised some of the gun stuff so the powercell guns share the shooting code with the ballistic guns. * Re-merging branch with master Also fixed some visualizer bugs * Last cleanup Fixed some crashes Fixed Deckard sprite Fixed Hitscan effects Re-applied master changes Re-factor to using soundsystem Add some more audio effects * Cleanup flashes for merge Can put flashbangs in lockers so you don't get blinded Fix some bugs * Fix shotties Also removed some redundant code * Bulldoze some legacycode brrrrrrrrt * Fix clientignore warnings * Add the other Stunnable types to StunnableProjectile * Some gun refactoring * Removed extra visualizers * All casing ejections use the same code * Speed loaders can have their ammo pulled out * Bolt sound less loud * Stop ThrowController from throwing * Fix speed loader visuals * Update hitscan collision mask and fix typo * Cleanup * Fit hitscan and flashbang collisions * Use the new flags support * Update taser placeholder description * Update protonames per style guide * Add yaml flag support for gun firerates * Cleanup crew * Fix Audio up (components, audio file, + remove global sounds) * Add server-side recoil back-in (forgot that I was testing this client-side) * Add Flag support for fire-rate selectors * Wrong int you dolt * Fix AI conflicts Haha ranged bulldozer go BRR (I'll rewrite it after the other AI systems are done). * Mix bang.ogg from stereo to mono * Make sure serializer's reading for guns Fixes integration test * Change EntitySystem calls to use the static function Also removed the Pumpbarrel commented-out code * Change StunnableProjectile defaults to 0 * Fix taser paralyse Apparently removing defaults means you have to specify the values, whodathunkit * Add slowdown to stunnableprojectiles and fix tasers * Remove FlagsFor from gun components Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com> Co-authored-by: Víctor Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using Content.Server.GameObjects.Components.Weapon;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Explosion
|
||||
{
|
||||
/// <summary>
|
||||
/// When triggered will flash in an area around the object and destroy itself
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class FlashExplosiveComponent : Component, ITimerTrigger, IDestroyAct
|
||||
{
|
||||
public override string Name => "FlashExplosive";
|
||||
|
||||
private float _range;
|
||||
private double _duration;
|
||||
private string _sound;
|
||||
private bool _deleteOnFlash;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _range, "range", 7.0f);
|
||||
serializer.DataField(ref _duration, "duration", 8.0);
|
||||
serializer.DataField(ref _sound, "sound", "/Audio/effects/flash_bang.ogg");
|
||||
serializer.DataField(ref _deleteOnFlash, "deleteOnFlash", true);
|
||||
}
|
||||
|
||||
public bool Explode()
|
||||
{
|
||||
// If we're in a locker or whatever then can't flash anything
|
||||
ContainerHelpers.TryGetContainer(Owner, out var container);
|
||||
if (container == null || !container.Owner.HasComponent<EntityStorageComponent>())
|
||||
{
|
||||
ServerFlashableComponent.FlashAreaHelper(Owner, _range, _duration);
|
||||
}
|
||||
|
||||
if (_sound != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_sound, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
if (_deleteOnFlash && !Owner.Deleted)
|
||||
{
|
||||
Owner.Delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ITimerTrigger.Trigger(TimerTriggerEventArgs eventArgs)
|
||||
{
|
||||
return Explode();
|
||||
}
|
||||
|
||||
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
|
||||
{
|
||||
Explode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
@@ -13,7 +14,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
public abstract class BaseCharger : Component
|
||||
{
|
||||
|
||||
public IEntity HeldItem { get; protected set; }
|
||||
protected IEntity _heldItem;
|
||||
protected ContainerSlot _container;
|
||||
protected PowerDeviceComponent _powerDevice;
|
||||
public CellChargerStatus Status => _status;
|
||||
@@ -58,37 +59,28 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will remove the item directly into the user's hand rather than the floor
|
||||
/// This will remove the item directly into the user's hand / floor
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
public void RemoveItemToHand(IEntity user)
|
||||
public void RemoveItem(IEntity user)
|
||||
{
|
||||
var heldItem = _container.ContainedEntity;
|
||||
if (heldItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
RemoveItem();
|
||||
|
||||
if (user.TryGetComponent(out HandsComponent handsComponent) &&
|
||||
heldItem.TryGetComponent(out ItemComponent itemComponent))
|
||||
_container.Remove(heldItem);
|
||||
if (user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will put the charger's item on the floor if available
|
||||
/// </summary>
|
||||
public void RemoveItem()
|
||||
{
|
||||
if (_container.ContainedEntity == null)
|
||||
{
|
||||
return;
|
||||
handsComponent.PutInHandOrDrop(heldItem.GetComponent<ItemComponent>());
|
||||
}
|
||||
|
||||
_container.Remove(HeldItem);
|
||||
HeldItem = null;
|
||||
if (heldItem.TryGetComponent(out ServerBatteryBarrelComponent batteryBarrelComponent))
|
||||
{
|
||||
batteryBarrelComponent.UpdateAppearance();
|
||||
}
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
@@ -135,8 +127,6 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(CellVisual.Occupied, _container.ContainedEntity != null);
|
||||
|
||||
_status = status;
|
||||
}
|
||||
|
||||
public void OnUpdate(float frameTime)
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
RemoveItemToHand(eventArgs.User);
|
||||
RemoveItem(eventArgs.User);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
@@ -111,7 +111,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
protected override void Activate(IEntity user, PowerCellChargerComponent component)
|
||||
{
|
||||
component.RemoveItem();
|
||||
component.RemoveItem(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HeldItem = entity;
|
||||
if (!_container.Insert(HeldItem))
|
||||
|
||||
if (!_container.Insert(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -157,7 +156,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
// Two numbers: One for how much power actually goes into the device (chargeAmount) and
|
||||
// chargeLoss which is how much is drawn from the powernet
|
||||
_container.ContainedEntity.TryGetComponent(out PowerCellComponent cellComponent);
|
||||
var cellComponent = _container.ContainedEntity.GetComponent<PowerCellComponent>();
|
||||
var chargeLoss = cellComponent.RequestCharge(frameTime) * _transferRatio;
|
||||
_powerDevice.Load = chargeLoss;
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
@@ -26,8 +21,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
public override string Name => "WeaponCapacitorCharger";
|
||||
public override double CellChargePercent => _container.ContainedEntity != null ?
|
||||
_container.ContainedEntity.GetComponent<HitscanWeaponCapacitorComponent>().Charge /
|
||||
_container.ContainedEntity.GetComponent<HitscanWeaponCapacitorComponent>().Capacity * 100 : 0.0f;
|
||||
_container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell.Charge /
|
||||
_container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell.Capacity * 100 : 0.0f;
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
@@ -43,7 +38,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
RemoveItemToHand(eventArgs.User);
|
||||
RemoveItem(eventArgs.User);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
@@ -106,21 +101,19 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
protected override void Activate(IEntity user, WeaponCapacitorChargerComponent component)
|
||||
{
|
||||
component.RemoveItem();
|
||||
component.RemoveItem(user);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryInsertItem(IEntity entity)
|
||||
{
|
||||
if (!entity.HasComponent<HitscanWeaponCapacitorComponent>() ||
|
||||
if (!entity.HasComponent<ServerBatteryBarrelComponent>() ||
|
||||
_container.ContainedEntity != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HeldItem = entity;
|
||||
|
||||
if (!_container.Insert(HeldItem))
|
||||
if (!_container.Insert(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -140,8 +133,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
return CellChargerStatus.Empty;
|
||||
}
|
||||
|
||||
if (_container.ContainedEntity.TryGetComponent(out HitscanWeaponCapacitorComponent component) &&
|
||||
Math.Abs(component.Capacity - component.Charge) < 0.01)
|
||||
if (_container.ContainedEntity.TryGetComponent(out ServerBatteryBarrelComponent component) &&
|
||||
Math.Abs(component.PowerCell.Capacity - component.PowerCell.Charge) < 0.01)
|
||||
{
|
||||
return CellChargerStatus.Charged;
|
||||
}
|
||||
@@ -153,8 +146,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
// Two numbers: One for how much power actually goes into the device (chargeAmount) and
|
||||
// chargeLoss which is how much is drawn from the powernet
|
||||
_container.ContainedEntity.TryGetComponent(out HitscanWeaponCapacitorComponent weaponCapacitorComponent);
|
||||
var chargeLoss = weaponCapacitorComponent.RequestCharge(frameTime) * _transferRatio;
|
||||
var powerCell = _container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell;
|
||||
var chargeLoss = powerCell.RequestCharge(frameTime) * _transferRatio;
|
||||
_powerDevice.Load = chargeLoss;
|
||||
|
||||
if (!_powerDevice.Powered)
|
||||
@@ -165,14 +158,13 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
var chargeAmount = chargeLoss * _transferEfficiency;
|
||||
|
||||
weaponCapacitorComponent.AddCharge(chargeAmount);
|
||||
powerCell.AddCharge(chargeAmount);
|
||||
// Just so the sprite won't be set to 99.99999% visibility
|
||||
if (weaponCapacitorComponent.Capacity - weaponCapacitorComponent.Charge < 0.01)
|
||||
if (powerCell.Capacity - powerCell.Charge < 0.01)
|
||||
{
|
||||
weaponCapacitorComponent.Charge = weaponCapacitorComponent.Capacity;
|
||||
powerCell.Charge = powerCell.Capacity;
|
||||
}
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Explosion;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ExplosiveProjectileComponent : Component, ICollideBehavior
|
||||
{
|
||||
public override string Name => "ExplosiveProjectile";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (!Owner.HasComponent<ExplosiveComponent>())
|
||||
{
|
||||
Logger.Error("ExplosiveProjectiles need an ExplosiveComponent");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
var explosiveComponent = Owner.GetComponent<ExplosiveComponent>();
|
||||
explosiveComponent.Explosion();
|
||||
}
|
||||
|
||||
// Projectile should handle the deleting
|
||||
void ICollideBehavior.PostCollide(int collisionCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon colliding with an object this will flash in an area around it
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class FlashProjectileComponent : Component, ICollideBehavior
|
||||
{
|
||||
public override string Name => "FlashProjectile";
|
||||
|
||||
private double _range;
|
||||
private double _duration;
|
||||
|
||||
private bool _flashed;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _range, "range", 1.0);
|
||||
serializer.DataField(ref _duration, "duration", 8.0);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
// Shouldn't be using this without a ProjectileComponent because it will just immediately collide with thrower
|
||||
if (!Owner.HasComponent<ProjectileComponent>())
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
if (_flashed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ServerFlashableComponent.FlashAreaHelper(Owner, _range, _duration);
|
||||
_flashed = true;
|
||||
}
|
||||
|
||||
// Projectile should handle the deleting
|
||||
void ICollideBehavior.PostCollide(int collisionCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Timer = Robust.Shared.Timers.Timer;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Lasers etc.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class HitscanComponent : Component
|
||||
{
|
||||
public override string Name => "Hitscan";
|
||||
public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask;
|
||||
private int _collisionMask;
|
||||
|
||||
public float Damage
|
||||
{
|
||||
get => _damage;
|
||||
set => _damage = value;
|
||||
}
|
||||
private float _damage;
|
||||
public DamageType DamageType => _damageType;
|
||||
private DamageType _damageType;
|
||||
public float MaxLength => 20.0f;
|
||||
|
||||
private TimeSpan _startTime;
|
||||
private TimeSpan _deathTime;
|
||||
|
||||
public float ColorModifier { get; set; } = 1.0f;
|
||||
private string _spriteName;
|
||||
private string _muzzleFlash;
|
||||
private string _impactFlash;
|
||||
private string _soundHitWall;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _collisionMask, "layers", (int) CollisionGroup.Opaque, WithFormat.Flags<CollisionLayer>());
|
||||
serializer.DataField(ref _damage, "damage", 10.0f);
|
||||
serializer.DataField(ref _damageType, "damageType", DamageType.Heat);
|
||||
serializer.DataField(ref _spriteName, "spriteName", "Objects/Guns/Projectiles/laser.png");
|
||||
serializer.DataField(ref _muzzleFlash, "muzzleFlash", null);
|
||||
serializer.DataField(ref _impactFlash, "impactFlash", null);
|
||||
serializer.DataField(ref _soundHitWall, "soundHitWall", "/Audio/Guns/Hits/laser_sear_wall.ogg");
|
||||
}
|
||||
|
||||
public void FireEffects(IEntity user, float distance, Angle angle, IEntity hitEntity = null)
|
||||
{
|
||||
var effectSystem = EntitySystem.Get<EffectSystem>();
|
||||
_startTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
_deathTime = _startTime + TimeSpan.FromSeconds(1);
|
||||
|
||||
var afterEffect = AfterEffects(user.Transform.GridPosition, angle, distance, 1.0f);
|
||||
if (afterEffect != null)
|
||||
{
|
||||
effectSystem.CreateParticle(afterEffect);
|
||||
}
|
||||
|
||||
// if we're too close we'll stop the impact and muzzle / impact sprites from clipping
|
||||
if (distance > 1.0f)
|
||||
{
|
||||
var impactEffect = ImpactFlash(distance, angle);
|
||||
if (impactEffect != null)
|
||||
{
|
||||
effectSystem.CreateParticle(impactEffect);
|
||||
}
|
||||
|
||||
var muzzleEffect = MuzzleFlash(user.Transform.GridPosition, angle);
|
||||
if (muzzleEffect != null)
|
||||
{
|
||||
effectSystem.CreateParticle(muzzleEffect);
|
||||
}
|
||||
}
|
||||
|
||||
if (hitEntity != null && _soundHitWall != null)
|
||||
{
|
||||
// TODO: No wall component so ?
|
||||
var offset = angle.ToVec().Normalized / 2;
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundHitWall, user.Transform.GridPosition.Translated(offset));
|
||||
}
|
||||
|
||||
Timer.Spawn((int) _deathTime.TotalMilliseconds, () =>
|
||||
{
|
||||
if (!Owner.Deleted)
|
||||
{
|
||||
Owner.Delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private EffectSystemMessage MuzzleFlash(GridCoordinates grid, Angle angle)
|
||||
{
|
||||
if (_muzzleFlash == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var offset = angle.ToVec().Normalized / 2;
|
||||
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = _muzzleFlash,
|
||||
Born = _startTime,
|
||||
DeathTime = _deathTime,
|
||||
Coordinates = grid.Translated(offset),
|
||||
//Rotated from east facing
|
||||
Rotation = (float) angle.Theta,
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
Shaded = false
|
||||
};
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private EffectSystemMessage AfterEffects(GridCoordinates origin, Angle angle, float distance, float offset = 0.0f)
|
||||
{
|
||||
var midPointOffset = angle.ToVec() * distance / 2;
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = _spriteName,
|
||||
Born = _startTime,
|
||||
DeathTime = _deathTime,
|
||||
Size = new Vector2(distance - offset, 1f),
|
||||
Coordinates = origin.Translated(midPointOffset),
|
||||
//Rotated from east facing
|
||||
Rotation = (float) angle.Theta,
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
|
||||
Shaded = false
|
||||
};
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private EffectSystemMessage ImpactFlash(float distance, Angle angle)
|
||||
{
|
||||
if (_impactFlash == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = _impactFlash,
|
||||
Born = _startTime,
|
||||
DeathTime = _deathTime,
|
||||
Coordinates = Owner.Transform.GridPosition.Translated(angle.ToVec() * distance),
|
||||
//Rotated from east facing
|
||||
Rotation = (float) angle.FlipPositive(),
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
Shaded = false
|
||||
};
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,13 @@
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -18,24 +22,32 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
|
||||
public bool IgnoreShooter = true;
|
||||
|
||||
private EntityUid Shooter = EntityUid.Invalid;
|
||||
private EntityUid _shooter = EntityUid.Invalid;
|
||||
|
||||
private Dictionary<DamageType, int> _damages;
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<DamageType, int> Damages => _damages;
|
||||
private float _velocity;
|
||||
public float Velocity
|
||||
public Dictionary<DamageType, int> Damages
|
||||
{
|
||||
get => _velocity;
|
||||
set => _velocity = value;
|
||||
get => _damages;
|
||||
set => _damages = value;
|
||||
}
|
||||
|
||||
public bool DeleteOnCollide => _deleteOnCollide;
|
||||
private bool _deleteOnCollide;
|
||||
|
||||
// Get that juicy FPS hit sound
|
||||
private string _soundHit;
|
||||
private string _soundHitSpecies;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _deleteOnCollide, "delete_on_collide", true);
|
||||
// If not specified 0 damage
|
||||
serializer.DataField(ref _damages, "damages", new Dictionary<DamageType, int>());
|
||||
serializer.DataField(ref _velocity, "velocity", 20f);
|
||||
serializer.DataField(ref _soundHit, "soundHit", null);
|
||||
serializer.DataField(ref _soundHitSpecies, "soundHitSpecies", null);
|
||||
}
|
||||
|
||||
public float TimeLeft { get; set; } = 10;
|
||||
@@ -46,7 +58,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
/// <param name="shooter"></param>
|
||||
public void IgnoreEntity(IEntity shooter)
|
||||
{
|
||||
Shooter = shooter.Uid;
|
||||
_shooter = shooter.Uid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,7 +68,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
/// <returns></returns>
|
||||
bool ICollideSpecial.PreventCollide(IPhysBody collidedwith)
|
||||
{
|
||||
if (IgnoreShooter && collidedwith.Owner.Uid == Shooter)
|
||||
if (IgnoreShooter && collidedwith.Owner.Uid == _shooter)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@@ -67,9 +79,17 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
/// <param name="entity"></param>
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
if (_soundHitSpecies != null && entity.HasComponent<SpeciesComponent>())
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundHitSpecies, entity.Transform.GridPosition);
|
||||
} else if (_soundHit != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundHit, entity.Transform.GridPosition);
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out DamageableComponent damage))
|
||||
{
|
||||
Owner.EntityManager.TryGetEntity(Shooter, out var shooter);
|
||||
Owner.EntityManager.TryGetEntity(_shooter, out var shooter);
|
||||
|
||||
foreach (var (damageType, amount) in _damages)
|
||||
{
|
||||
@@ -87,7 +107,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
|
||||
void ICollideBehavior.PostCollide(int collideCount)
|
||||
{
|
||||
if (collideCount > 0) Owner.Delete();
|
||||
if (collideCount > 0 && DeleteOnCollide) Owner.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds stun when it collides with an entity
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class StunnableProjectileComponent : Component, ICollideBehavior
|
||||
{
|
||||
public override string Name => "StunnableProjectile";
|
||||
|
||||
// See stunnable for what these do
|
||||
private int _stunAmount;
|
||||
private int _knockdownAmount;
|
||||
private int _slowdownAmount;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _stunAmount, "stunAmount", 0);
|
||||
serializer.DataField(ref _knockdownAmount, "knockdownAmount", 0);
|
||||
serializer.DataField(ref _slowdownAmount, "slowdownAmount", 0);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (!Owner.HasComponent<ProjectileComponent>())
|
||||
{
|
||||
Logger.Error("StunProjectile entity must have a ProjectileComponent");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
if (entity.TryGetComponent(out StunnableComponent stunnableComponent))
|
||||
{
|
||||
stunnableComponent.Stun(_stunAmount);
|
||||
stunnableComponent.Knockdown(_knockdownAmount);
|
||||
stunnableComponent.Slowdown(_slowdownAmount);
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.PostCollide(int collidedCount) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class AmmoBoxComponent : Component, IInteractUsing, IUse, IInteractHand, IMapInit
|
||||
{
|
||||
public override string Name => "AmmoBox";
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
public int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private Container _ammoContainer;
|
||||
private int _unspawnedCount;
|
||||
|
||||
private string _fillPrototype;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 30);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
_spawnedAmmo.Push(entity);
|
||||
_ammoContainer.Insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
_unspawnedCount += _capacity;
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, AmmoLeft);
|
||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, _capacity);
|
||||
}
|
||||
}
|
||||
|
||||
public IEntity TakeAmmo()
|
||||
{
|
||||
if (_spawnedAmmo.TryPop(out IEntity ammo))
|
||||
{
|
||||
_ammoContainer.Remove(ammo);
|
||||
return ammo;
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
ammo = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
|
||||
return ammo;
|
||||
}
|
||||
|
||||
public bool TryInsertAmmo(IEntity user, IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AmmoLeft >= Capacity)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("No room"));
|
||||
return false;
|
||||
}
|
||||
|
||||
_spawnedAmmo.Push(entity);
|
||||
_ammoContainer.Insert(entity);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Using.HasComponent<AmmoComponent>())
|
||||
{
|
||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
if (eventArgs.Using.TryGetComponent(out RangedMagazineComponent rangedMagazine))
|
||||
{
|
||||
for (var i = 0; i < Math.Max(10, rangedMagazine.ShotsLeft); i++)
|
||||
{
|
||||
var ammo = rangedMagazine.TakeAmmo();
|
||||
|
||||
if (!TryInsertAmmo(eventArgs.User, ammo))
|
||||
{
|
||||
rangedMagazine.TryInsertAmmo(eventArgs.User, ammo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryUse(IEntity user)
|
||||
{
|
||||
if (!user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammo = TakeAmmo();
|
||||
var itemComponent = ammo.GetComponent<ItemComponent>();
|
||||
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
TryInsertAmmo(user, ammo);
|
||||
return false;
|
||||
}
|
||||
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EjectContents(int count)
|
||||
{
|
||||
var ejectCount = Math.Min(count, Capacity);
|
||||
var ejectAmmo = new List<IEntity>(ejectCount);
|
||||
|
||||
for (var i = 0; i < Math.Min(count, Capacity); i++)
|
||||
{
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ejectAmmo.Add(ammo);
|
||||
}
|
||||
|
||||
ServerRangedBarrelComponent.EjectCasings(ejectAmmo);
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
return TryUse(eventArgs.User);
|
||||
}
|
||||
|
||||
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
|
||||
{
|
||||
return TryUse(eventArgs.User);
|
||||
}
|
||||
|
||||
// So if you have 200 rounds in a box and that suddenly creates 200 entities you're not having a fun time
|
||||
[Verb]
|
||||
private sealed class DumpVerb : Verb<AmmoBoxComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, AmmoBoxComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Dump 10");
|
||||
data.Visibility = component.AmmoLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, AmmoBoxComponent component)
|
||||
{
|
||||
component.EjectContents(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Timers;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Logger = Robust.Shared.Log.Logger;
|
||||
using Timer = Robust.Shared.Timers.Timer;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows this entity to be loaded into a ranged weapon (if the caliber matches)
|
||||
/// Generally used for bullets but can be used for other things like bananas
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class AmmoComponent : Component
|
||||
{
|
||||
public override string Name => "Ammo";
|
||||
public BallisticCaliber Caliber => _caliber;
|
||||
private BallisticCaliber _caliber;
|
||||
public bool Spent
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ammoIsProjectile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _spent;
|
||||
}
|
||||
}
|
||||
private bool _spent;
|
||||
|
||||
/// <summary>
|
||||
/// Used for anything without a case that fires itself
|
||||
/// </summary>
|
||||
private bool _ammoIsProjectile;
|
||||
|
||||
/// <summary>
|
||||
/// Used for something that is deleted when the projectile is retrieved
|
||||
/// </summary>
|
||||
public bool Caseless => _caseless;
|
||||
private bool _caseless;
|
||||
// Rather than managing bullet / case state seemed easier to just have 2 toggles
|
||||
// ammoIsProjectile being for a beanbag for example and caseless being for ClRifle rounds
|
||||
|
||||
/// <summary>
|
||||
/// For shotguns where they might shoot multiple entities
|
||||
/// </summary>
|
||||
public int ProjectilesFired => _projectilesFired;
|
||||
private int _projectilesFired;
|
||||
private string _projectileId;
|
||||
// How far apart each entity is if multiple are shot
|
||||
public float EvenSpreadAngle => _evenSpreadAngle;
|
||||
private float _evenSpreadAngle;
|
||||
/// <summary>
|
||||
/// How fast the shot entities travel
|
||||
/// </summary>
|
||||
public float Velocity => _velocity;
|
||||
private float _velocity;
|
||||
|
||||
private string _muzzleFlashSprite;
|
||||
|
||||
public string SoundCollectionEject => _soundCollectionEject;
|
||||
private string _soundCollectionEject;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
// For shotty of whatever as well
|
||||
serializer.DataField(ref _projectileId, "projectile", null);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _projectilesFired, "projectilesFired", 1);
|
||||
// Used for shotty to determine overall pellet spread
|
||||
serializer.DataField(ref _evenSpreadAngle, "ammoSpread", 0);
|
||||
serializer.DataField(ref _velocity, "ammoVelocity", 20.0f);
|
||||
serializer.DataField(ref _ammoIsProjectile, "isProjectile", false);
|
||||
serializer.DataField(ref _caseless, "caseless", false);
|
||||
// Being both caseless and shooting yourself doesn't make sense
|
||||
DebugTools.Assert(!(_ammoIsProjectile && _caseless));
|
||||
serializer.DataField(ref _muzzleFlashSprite, "muzzleFlash", "Objects/Guns/Projectiles/bullet_muzzle.png");
|
||||
serializer.DataField(ref _soundCollectionEject, "soundCollectionEject", "CasingEject");
|
||||
|
||||
if (_projectilesFired < 1)
|
||||
{
|
||||
Logger.Error("Ammo can't have less than 1 projectile");
|
||||
}
|
||||
|
||||
if (_evenSpreadAngle > 0 && _projectilesFired == 1)
|
||||
{
|
||||
Logger.Error("Can't have an even spread if only 1 projectile is fired");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public IEntity TakeBullet()
|
||||
{
|
||||
if (_ammoIsProjectile)
|
||||
{
|
||||
return Owner;
|
||||
}
|
||||
|
||||
if (_spent)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_spent = true;
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
appearanceComponent.SetData(AmmoVisuals.Spent, true);
|
||||
}
|
||||
|
||||
var entity = Owner.EntityManager.SpawnEntity(_projectileId, Owner.Transform.GridPosition);
|
||||
DebugTools.AssertNotNull(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public void MuzzleFlash(GridCoordinates grid, Angle angle)
|
||||
{
|
||||
if (_muzzleFlashSprite == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var time = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
var deathTime = time + TimeSpan.FromMilliseconds(200);
|
||||
// Offset the sprite so it actually looks like it's coming from the gun
|
||||
var offset = angle.ToVec().Normalized / 2;
|
||||
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = _muzzleFlashSprite,
|
||||
Born = time,
|
||||
DeathTime = deathTime,
|
||||
Coordinates = grid.Translated(offset),
|
||||
//Rotated from east facing
|
||||
Rotation = (float) angle.Theta,
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
Shaded = false
|
||||
};
|
||||
EntitySystem.Get<EffectSystem>().CreateParticle(message);
|
||||
}
|
||||
}
|
||||
|
||||
public enum BallisticCaliber
|
||||
{
|
||||
Unspecified = 0,
|
||||
A357, // Placeholder?
|
||||
ClRifle,
|
||||
SRifle,
|
||||
Pistol,
|
||||
A35, // Placeholder?
|
||||
LRifle,
|
||||
Magnum,
|
||||
AntiMaterial,
|
||||
Shotgun,
|
||||
Cap, // Placeholder
|
||||
Rocket,
|
||||
Dart, // Placeholder
|
||||
Grenade,
|
||||
Energy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class RangedMagazineComponent : Component, IMapInit, IInteractUsing, IUse
|
||||
{
|
||||
public override string Name => "RangedMagazine";
|
||||
|
||||
private Stack<IEntity> _spawnedAmmo = new Stack<IEntity>();
|
||||
private Container _ammoContainer;
|
||||
|
||||
public int ShotsLeft => _spawnedAmmo.Count + _unspawnedCount;
|
||||
public int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
public MagazineType MagazineType => _magazineType;
|
||||
private MagazineType _magazineType;
|
||||
public BallisticCaliber Caliber => _caliber;
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// If there's anything already in the magazine
|
||||
private string _fillPrototype;
|
||||
// By default the magazine won't spawn the entity until needed so we need to keep track of how many left we can spawn
|
||||
// Generally you probablt don't want to use this
|
||||
private int _unspawnedCount;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _magazineType, "magazineType", MagazineType.Unspecified);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
serializer.DataField(ref _capacity, "capacity", 20);
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_fillPrototype != null)
|
||||
{
|
||||
_unspawnedCount += Capacity;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-magazine", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
if (_ammoContainer.ContainedEntities.Count > Capacity)
|
||||
{
|
||||
throw new InvalidOperationException("Initialized capacity of magazine higher than its actual capacity");
|
||||
}
|
||||
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_spawnedAmmo.Push(entity);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public bool TryInsertAmmo(IEntity user, IEntity ammo)
|
||||
{
|
||||
if (!ammo.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ShotsLeft >= Capacity)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Magazine is full"));
|
||||
return false;
|
||||
}
|
||||
|
||||
_ammoContainer.Insert(ammo);
|
||||
_spawnedAmmo.Push(ammo);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEntity TakeAmmo()
|
||||
{
|
||||
IEntity ammo = null;
|
||||
// If anything's spawned use that first, otherwise use the fill prototype as a fallback (if we have spawn count left)
|
||||
if (_spawnedAmmo.TryPop(out var entity))
|
||||
{
|
||||
ammo = entity;
|
||||
_ammoContainer.Remove(entity);
|
||||
}
|
||||
else if (_unspawnedCount > 0)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
ammo = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
return ammo;
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.User.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemComponent = ammo.GetComponent<ItemComponent>();
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
ammo.Transform.GridPosition = eventArgs.User.Transform.GridPosition;
|
||||
ServerRangedBarrelComponent.EjectCasing(ammo);
|
||||
}
|
||||
else
|
||||
{
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to load certain ranged weapons quickly
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class SpeedLoaderComponent : Component, IAfterInteract, IInteractUsing, IMapInit, IUse
|
||||
{
|
||||
public override string Name => "SpeedLoader";
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
public int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
private Container _ammoContainer;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private int _unspawnedCount;
|
||||
|
||||
public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount;
|
||||
|
||||
private string _fillPrototype;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 6);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var ammo in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
_spawnedAmmo.Push(ammo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
_unspawnedCount += _capacity;
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
appearanceComponent?.SetData(AmmoVisuals.AmmoCount, AmmoLeft);
|
||||
appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryInsertAmmo(IEntity user, IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AmmoLeft >= Capacity)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("No room"));
|
||||
return false;
|
||||
}
|
||||
|
||||
_spawnedAmmo.Push(entity);
|
||||
_ammoContainer.Insert(entity);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private bool UseEntity(IEntity user)
|
||||
{
|
||||
if (!user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemComponent = ammo.GetComponent<ItemComponent>();
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
ServerRangedBarrelComponent.EjectCasing(ammo);
|
||||
}
|
||||
else
|
||||
{
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEntity TakeAmmo()
|
||||
{
|
||||
if (_spawnedAmmo.TryPop(out var entity))
|
||||
{
|
||||
_ammoContainer.Remove(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
entity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
|
||||
bool changed = false;
|
||||
|
||||
if (eventArgs.Target.TryGetComponent(out RevolverBarrelComponent revolverBarrel))
|
||||
{
|
||||
for (var i = 0; i < Capacity; i++)
|
||||
{
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (revolverBarrel.TryInsertBullet(eventArgs.User, ammo))
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the ammo back
|
||||
TryInsertAmmo(eventArgs.User, ammo);
|
||||
break;
|
||||
}
|
||||
} else if (eventArgs.Target.TryGetComponent(out BoltActionBarrelComponent boltActionBarrel))
|
||||
{
|
||||
for (var i = 0; i < Capacity; i++)
|
||||
{
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (boltActionBarrel.TryInsertBullet(eventArgs.User, ammo))
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the ammo back
|
||||
TryInsertAmmo(eventArgs.User, ammo);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
UpdateAppearance();
|
||||
}
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
return UseEntity(eventArgs.User);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
/// <summary>
|
||||
/// Shotguns mostly
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IMapInit
|
||||
{
|
||||
// Originally I had this logic shared with PumpBarrel and used a couple of variables to control things
|
||||
// but it felt a lot messier to play around with, especially when adding verbs
|
||||
|
||||
public override string Name => "BoltActionBarrel";
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
|
||||
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
|
||||
}
|
||||
}
|
||||
public override int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
private ContainerSlot _chamberContainer;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private Container _ammoContainer;
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
private string _fillPrototype;
|
||||
private int _unspawnedCount;
|
||||
|
||||
public bool BoltOpen
|
||||
{
|
||||
get => _boltOpen;
|
||||
set
|
||||
{
|
||||
if (_boltOpen == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
|
||||
if (value)
|
||||
{
|
||||
if (_soundBoltOpen != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_soundBoltClosed != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
_boltOpen = value;
|
||||
UpdateAppearance();
|
||||
}
|
||||
}
|
||||
private bool _boltOpen;
|
||||
private bool _autoCycle;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
// Sounds
|
||||
private string _soundCycle;
|
||||
private string _soundBoltOpen;
|
||||
private string _soundBoltClosed;
|
||||
private string _soundInsert;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 6);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
serializer.DataField(ref _autoCycle, "autoCycle", false);
|
||||
|
||||
serializer.DataField(ref _soundCycle, "soundCycle", "/Audio/Guns/Cock/sf_rifle_cock.ogg");
|
||||
serializer.DataField(ref _soundBoltOpen, "soundBoltOpen", "/Audio/Guns/Bolt/rifle_bolt_open.ogg");
|
||||
serializer.DataField(ref _soundBoltClosed, "soundBoltClosed", "/Audio/Guns/Bolt/rifle_bolt_closed.ogg");
|
||||
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/bullet_insert.ogg");
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_fillPrototype != null)
|
||||
{
|
||||
_unspawnedCount += Capacity - 1;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
// TODO: Add existing ammo support on revolvers
|
||||
base.Initialize();
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity - 1);
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-ammo-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_spawnedAmmo.Push(entity);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
_chamberContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber-container", Owner);
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(BarrelBoltVisuals.BoltOpen, BoltOpen);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return _chamberContainer.ContainedEntity;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var chamberEntity = _chamberContainer.ContainedEntity;
|
||||
if (_autoCycle)
|
||||
{
|
||||
Cycle();
|
||||
}
|
||||
return chamberEntity?.GetComponent<AmmoComponent>().TakeBullet();
|
||||
}
|
||||
|
||||
protected override bool WeaponCanFire()
|
||||
{
|
||||
if (!base.WeaponCanFire())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !BoltOpen && _chamberContainer.ContainedEntity != null;
|
||||
}
|
||||
|
||||
private void Cycle(bool manual = false)
|
||||
{
|
||||
var chamberedEntity = _chamberContainer.ContainedEntity;
|
||||
if (chamberedEntity != null)
|
||||
{
|
||||
_chamberContainer.Remove(chamberedEntity);
|
||||
var ammoComponent = chamberedEntity.GetComponent<AmmoComponent>();
|
||||
if (!ammoComponent.Caseless)
|
||||
{
|
||||
EjectCasing(chamberedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (_spawnedAmmo.TryPop(out var next))
|
||||
{
|
||||
_ammoContainer.Remove(next);
|
||||
_chamberContainer.Insert(next);
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
var ammoEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_chamberContainer.Insert(ammoEntity);
|
||||
}
|
||||
|
||||
if (_chamberContainer.ContainedEntity == null && manual)
|
||||
{
|
||||
BoltOpen = true;
|
||||
if (ContainerHelpers.TryGetContainer(Owner, out var container))
|
||||
{
|
||||
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt opened"));
|
||||
}
|
||||
}
|
||||
|
||||
if (manual)
|
||||
{
|
||||
if (_soundCycle != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundCycle, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public bool TryInsertBullet(IEntity user, IEntity ammo)
|
||||
{
|
||||
if (!ammo.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Bolt isn't open"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_chamberContainer.ContainedEntity == null)
|
||||
{
|
||||
_chamberContainer.Insert(ammo);
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
|
||||
{
|
||||
_ammoContainer.Insert(ammo);
|
||||
_spawnedAmmo.Push(ammo);
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user, Loc.GetString("No room"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (BoltOpen)
|
||||
{
|
||||
BoltOpen = false;
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Bolt closed"));
|
||||
// Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
Cycle(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertBullet(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class OpenBoltVerb : Verb<BoltActionBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BoltActionBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Open bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Disabled : VerbVisibility.Visible;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BoltActionBarrelComponent component)
|
||||
{
|
||||
component.BoltOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class CloseBoltVerb : Verb<BoltActionBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BoltActionBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Close bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BoltActionBarrelComponent component)
|
||||
{
|
||||
component.BoltOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
/// <summary>
|
||||
/// Bolt-action rifles
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IMapInit
|
||||
{
|
||||
public override string Name => "PumpBarrel";
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
|
||||
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
|
||||
}
|
||||
}
|
||||
|
||||
public override int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
// Even a point having a chamber? I guess it makes some of the below code cleaner
|
||||
private ContainerSlot _chamberContainer;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private Container _ammoContainer;
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
private string _fillPrototype;
|
||||
private int _unspawnedCount;
|
||||
|
||||
private bool _manualCycle;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundCycle;
|
||||
private string _soundInsert;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 6);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
serializer.DataField(ref _manualCycle, "manualCycle", true);
|
||||
|
||||
serializer.DataField(ref _soundCycle, "soundCycle", "/Audio/Guns/Cock/sf_rifle_cock.ogg");
|
||||
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/bullet_insert.ogg");
|
||||
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity - 1);
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_fillPrototype != null)
|
||||
{
|
||||
_unspawnedCount += Capacity - 1;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_ammoContainer =
|
||||
ContainerManagerComponent.Ensure<Container>($"{Name}-ammo-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_spawnedAmmo.Push(entity);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
_chamberContainer =
|
||||
ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber-container", Owner, out existing);
|
||||
if (existing)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return _chamberContainer.ContainedEntity;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var chamberEntity = _chamberContainer.ContainedEntity;
|
||||
if (!_manualCycle)
|
||||
{
|
||||
Cycle();
|
||||
}
|
||||
return chamberEntity?.GetComponent<AmmoComponent>().TakeBullet();
|
||||
}
|
||||
|
||||
private void Cycle(bool manual = false)
|
||||
{
|
||||
var chamberedEntity = _chamberContainer.ContainedEntity;
|
||||
if (chamberedEntity != null)
|
||||
{
|
||||
_chamberContainer.Remove(chamberedEntity);
|
||||
var ammoComponent = chamberedEntity.GetComponent<AmmoComponent>();
|
||||
if (!ammoComponent.Caseless)
|
||||
{
|
||||
EjectCasing(chamberedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (_spawnedAmmo.TryPop(out var next))
|
||||
{
|
||||
_ammoContainer.Remove(next);
|
||||
_chamberContainer.Insert(next);
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
var ammoEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_chamberContainer.Insert(ammoEntity);
|
||||
}
|
||||
|
||||
if (manual)
|
||||
{
|
||||
if (_soundCycle != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundCycle, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public bool TryInsertBullet(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
|
||||
{
|
||||
_ammoContainer.Insert(eventArgs.Using);
|
||||
_spawnedAmmo.Push(eventArgs.Using);
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("No room"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
Cycle(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertBullet(eventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent
|
||||
{
|
||||
public override string Name => "RevolverBarrel";
|
||||
private BallisticCaliber _caliber;
|
||||
private Container _ammoContainer;
|
||||
private int _currentSlot = 0;
|
||||
public override int Capacity => _ammoSlots.Length;
|
||||
private IEntity[] _ammoSlots;
|
||||
|
||||
public override int ShotsLeft => _ammoContainer.ContainedEntities.Count;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundEject;
|
||||
private string _soundInsert;
|
||||
private string _soundSpin;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
var capacity = serializer.ReadDataField("capacity", 6);
|
||||
_ammoSlots = new IEntity[capacity];
|
||||
|
||||
// Sounds
|
||||
serializer.DataField(ref _soundEject, "soundEject", "/Audio/Guns/MagOut/revolver_magout.ogg");
|
||||
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/revolver_magin.ogg");
|
||||
serializer.DataField(ref _soundSpin, "soundSpin", "/Audio/Guns/Misc/revolver_spin.ogg");
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-ammoContainer", Owner);
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
// Placeholder, at this stage it's just here for the RPG
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, ShotsLeft > 0);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public bool TryInsertBullet(IEntity user, IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Functions like a stack
|
||||
// These are inserted in reverse order but then when fired Cycle will go through in order
|
||||
// The reason we don't just use an actual stack is because spin can select a random slot to point at
|
||||
for (var i = _ammoSlots.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var slot = _ammoSlots[i];
|
||||
if (slot == null)
|
||||
{
|
||||
_currentSlot = i;
|
||||
_ammoSlots[i] = entity;
|
||||
_ammoContainer.Insert(entity);
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user, Loc.GetString("Ammo full"));
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Cycle()
|
||||
{
|
||||
// Move up a slot
|
||||
_currentSlot = (_currentSlot + 1) % _ammoSlots.Length;
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Russian Roulette
|
||||
/// </summary>
|
||||
public void Spin()
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>().Next(_ammoSlots.Length - 1);
|
||||
_currentSlot = random;
|
||||
if (_soundSpin != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundSpin, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return _ammoSlots[_currentSlot];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a projectile out if possible
|
||||
/// IEnumerable just to make supporting shotguns saner
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var ammo = _ammoSlots[_currentSlot];
|
||||
IEntity bullet = null;
|
||||
if (ammo != null)
|
||||
{
|
||||
var ammoComponent = ammo.GetComponent<AmmoComponent>();
|
||||
bullet = ammoComponent.TakeBullet();
|
||||
if (ammoComponent.Caseless)
|
||||
{
|
||||
_ammoSlots[_currentSlot] = null;
|
||||
_ammoContainer.Remove(ammo);
|
||||
}
|
||||
}
|
||||
Cycle();
|
||||
UpdateAppearance();
|
||||
return bullet;
|
||||
}
|
||||
|
||||
private void EjectAllSlots()
|
||||
{
|
||||
for (var i = 0; i < _ammoSlots.Length; i++)
|
||||
{
|
||||
var entity = _ammoSlots[i];
|
||||
if (entity == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ammoContainer.Remove(entity);
|
||||
EjectCasing(entity);
|
||||
_ammoSlots[i] = null;
|
||||
}
|
||||
|
||||
if (_ammoContainer.ContainedEntities.Count > 0)
|
||||
{
|
||||
if (_soundEject != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
|
||||
}
|
||||
}
|
||||
|
||||
// May as well point back at the end?
|
||||
_currentSlot = _ammoSlots.Length - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eject all casings
|
||||
/// </summary>
|
||||
/// <param name="eventArgs"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
EjectAllSlots();
|
||||
//Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertBullet(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class SpinRevolverVerb : Verb<RevolverBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, RevolverBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Spin");
|
||||
if (component.Capacity <= 1)
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Visibility = component.ShotsLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, RevolverBarrelComponent component)
|
||||
{
|
||||
component.Spin();
|
||||
component.Owner.PopupMessage(user, Loc.GetString("Spun the cylinder"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Server.GameObjects.Components.Projectiles;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Logger = Robust.Shared.Log.Logger;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ServerBatteryBarrelComponent : ServerRangedBarrelComponent
|
||||
{
|
||||
public override string Name => "BatteryBarrel";
|
||||
|
||||
// The minimum change we need before we can fire
|
||||
[ViewVariables] private float _lowerChargeLimit;
|
||||
[ViewVariables] private int _baseFireCost;
|
||||
// What gets fired
|
||||
[ViewVariables] private string _ammoPrototype;
|
||||
|
||||
[ViewVariables] public IEntity PowerCellEntity => _powerCellContainer.ContainedEntity;
|
||||
public PowerCellComponent PowerCell => _powerCellContainer.ContainedEntity.GetComponent<PowerCellComponent>();
|
||||
private ContainerSlot _powerCellContainer;
|
||||
private ContainerSlot _ammoContainer;
|
||||
private string _powerCellPrototype;
|
||||
[ViewVariables] private bool _powerCellRemovable;
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var powerCell = _powerCellContainer.ContainedEntity;
|
||||
|
||||
if (powerCell == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) Math.Ceiling(powerCell.GetComponent<PowerCellComponent>().Charge / _baseFireCost);
|
||||
}
|
||||
}
|
||||
|
||||
public override int Capacity
|
||||
{
|
||||
get
|
||||
{
|
||||
var powerCell = _powerCellContainer.ContainedEntity;
|
||||
|
||||
if (powerCell == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) Math.Ceiling(powerCell.GetComponent<PowerCellComponent>().Capacity / _baseFireCost);
|
||||
}
|
||||
}
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundPowerCellInsert;
|
||||
private string _soundPowerCellEject;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
if (serializer.Reading)
|
||||
{
|
||||
_powerCellPrototype = serializer.ReadDataField<string>("powerCellPrototype", null);
|
||||
}
|
||||
|
||||
serializer.DataField(ref _powerCellRemovable, "powerCellRemovable", false);
|
||||
serializer.DataField(ref _baseFireCost, "fireCost", 300);
|
||||
serializer.DataField(ref _ammoPrototype, "ammoPrototype", null);
|
||||
serializer.DataField(ref _lowerChargeLimit, "lowerChargeLimit", 10);
|
||||
serializer.DataField(ref _soundPowerCellInsert, "soundPowerCellInsert", null);
|
||||
serializer.DataField(ref _soundPowerCellEject, "soundPowerCellEject", null);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_powerCellContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-powercell-container", Owner, out var existing);
|
||||
if (!existing && _powerCellPrototype != null)
|
||||
{
|
||||
var powerCellEntity = Owner.EntityManager.SpawnEntity(_powerCellPrototype, Owner.Transform.GridPosition);
|
||||
_powerCellContainer.Insert(powerCellEntity);
|
||||
}
|
||||
|
||||
if (_ammoPrototype != null)
|
||||
{
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-ammo-container", Owner);
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _powerCellContainer.ContainedEntity != null);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
// Spawn a dummy entity because it's easier to work with I guess
|
||||
// This will get re-used for the projectile
|
||||
var ammo = _ammoContainer.ContainedEntity;
|
||||
if (ammo == null)
|
||||
{
|
||||
ammo = Owner.EntityManager.SpawnEntity(_ammoPrototype, Owner.Transform.GridPosition);
|
||||
_ammoContainer.Insert(ammo);
|
||||
}
|
||||
|
||||
return ammo;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var powerCellEntity = _powerCellContainer.ContainedEntity;
|
||||
|
||||
if (powerCellEntity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var capacitor = powerCellEntity.GetComponent<PowerCellComponent>();
|
||||
if (capacitor.Charge < _lowerChargeLimit)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Can fire confirmed
|
||||
// Multiply the entity's damage / whatever by the percentage of charge the shot has.
|
||||
IEntity entity;
|
||||
var chargeChange = Math.Min(capacitor.Charge, _baseFireCost);
|
||||
capacitor.DeductCharge(chargeChange);
|
||||
var energyRatio = chargeChange / _baseFireCost;
|
||||
|
||||
if (_ammoContainer.ContainedEntity != null)
|
||||
{
|
||||
entity = _ammoContainer.ContainedEntity;
|
||||
_ammoContainer.Remove(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = Owner.EntityManager.SpawnEntity(_ammoPrototype, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out ProjectileComponent projectileComponent))
|
||||
{
|
||||
if (energyRatio < 1.0)
|
||||
{
|
||||
var newDamages = new Dictionary<DamageType, int>(projectileComponent.Damages);
|
||||
foreach (var (damageType, damage) in projectileComponent.Damages)
|
||||
{
|
||||
newDamages.Add(damageType, (int) (damage * energyRatio));
|
||||
}
|
||||
|
||||
projectileComponent.Damages = newDamages;
|
||||
}
|
||||
} else if (entity.TryGetComponent(out HitscanComponent hitscanComponent))
|
||||
{
|
||||
hitscanComponent.Damage *= energyRatio;
|
||||
hitscanComponent.ColorModifier = energyRatio;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
//Dirty();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public bool TryInsertPowerCell(IEntity entity)
|
||||
{
|
||||
if (_powerCellContainer.ContainedEntity != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!entity.HasComponent<PowerCellComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_soundPowerCellInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundPowerCellInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
_powerCellContainer.Insert(entity);
|
||||
UpdateAppearance();
|
||||
//Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEntity RemovePowerCell()
|
||||
{
|
||||
if (!_powerCellRemovable || _powerCellContainer.ContainedEntity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _powerCellContainer.ContainedEntity;
|
||||
_powerCellContainer.Remove(entity);
|
||||
if (_soundPowerCellEject != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundPowerCellEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
//Dirty();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (!_powerCellRemovable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!eventArgs.User.TryGetComponent(out HandsComponent handsComponent) ||
|
||||
PowerCellEntity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemComponent = PowerCellEntity.GetComponent<ItemComponent>();
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var powerCell = RemovePowerCell();
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
powerCell.Transform.GridPosition = eventArgs.User.Transform.GridPosition;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.HasComponent<PowerStorageComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryInsertPowerCell(eventArgs.Using);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ServerMagazineBarrelComponent : ServerRangedBarrelComponent
|
||||
{
|
||||
public override string Name => "MagazineBarrel";
|
||||
public override uint? NetID => ContentNetIDs.MAGAZINE_BARREL;
|
||||
|
||||
private ContainerSlot _chamberContainer;
|
||||
[ViewVariables] public bool HasMagazine => _magazineContainer.ContainedEntity != null;
|
||||
private ContainerSlot _magazineContainer;
|
||||
|
||||
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
|
||||
private MagazineType _magazineTypes;
|
||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var count = 0;
|
||||
if (_chamberContainer.ContainedEntity != null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
if (magazine != null)
|
||||
{
|
||||
count += magazine.GetComponent<RangedMagazineComponent>().ShotsLeft;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public override int Capacity
|
||||
{
|
||||
get
|
||||
{
|
||||
// Chamber
|
||||
var count = 1;
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
if (magazine != null)
|
||||
{
|
||||
count += magazine.GetComponent<RangedMagazineComponent>().Capacity;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public bool BoltOpen { get; private set; } = true;
|
||||
private bool _autoEjectMag;
|
||||
// If the bolt needs to be open before we can insert / remove the mag (i.e. for LMGs)
|
||||
public bool MagNeedsOpenBolt => _magNeedsOpenBolt;
|
||||
private bool _magNeedsOpenBolt;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundBoltOpen;
|
||||
private string _soundBoltClosed;
|
||||
private string _soundRack;
|
||||
private string _soundMagInsert;
|
||||
private string _soundMagEject;
|
||||
private string _soundAutoEject;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
if (serializer.Reading)
|
||||
{
|
||||
var magTypes = serializer.ReadDataField("magazineTypes", new List<MagazineType>());
|
||||
foreach (var mag in magTypes)
|
||||
{
|
||||
_magazineTypes |= mag;
|
||||
}
|
||||
}
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _autoEjectMag, "autoEjectMag", false);
|
||||
serializer.DataField(ref _magNeedsOpenBolt, "magNeedsOpenBolt", false);
|
||||
serializer.DataField(ref _soundBoltOpen, "soundBoltOpen", null);
|
||||
serializer.DataField(ref _soundBoltClosed, "soundBoltClosed", null);
|
||||
serializer.DataField(ref _soundRack, "soundRack", null);
|
||||
serializer.DataField(ref _soundMagInsert, "soundMagInsert", null);
|
||||
serializer.DataField(ref _soundMagEject, "soundMagEject", null);
|
||||
serializer.DataField(ref _soundAutoEject, "soundAutoEject", "/Audio/Guns/EmptyAlarm/smg_empty_alarm.ogg");
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
(int, int)? count = null;
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
if (magazine != null && magazine.TryGetComponent(out RangedMagazineComponent rangedMagazineComponent))
|
||||
{
|
||||
count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
|
||||
}
|
||||
|
||||
return new MagazineBarrelComponentState(
|
||||
_chamberContainer.ContainedEntity != null,
|
||||
FireRateSelector,
|
||||
count,
|
||||
SoundGunshot);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_chamberContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber", Owner);
|
||||
_magazineContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-magazine", Owner);
|
||||
}
|
||||
|
||||
public void ToggleBolt()
|
||||
{
|
||||
// For magazines only when we normally set BoltOpen we'll defer the UpdateAppearance until everything is done
|
||||
// Whereas this will just call it straight up.
|
||||
BoltOpen = !BoltOpen;
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
if (BoltOpen)
|
||||
{
|
||||
if (_soundBoltOpen != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_soundBoltClosed != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
}
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return BoltOpen ? null : _chamberContainer.ContainedEntity;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
if (BoltOpen)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var entity = _chamberContainer.ContainedEntity;
|
||||
|
||||
Cycle();
|
||||
return entity?.GetComponent<AmmoComponent>().TakeBullet();
|
||||
}
|
||||
|
||||
private void Cycle(bool manual = false)
|
||||
{
|
||||
if (BoltOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var chamberEntity = _chamberContainer.ContainedEntity;
|
||||
if (chamberEntity != null)
|
||||
{
|
||||
_chamberContainer.Remove(chamberEntity);
|
||||
var ammoComponent = chamberEntity.GetComponent<AmmoComponent>();
|
||||
if (!ammoComponent.Caseless)
|
||||
{
|
||||
EjectCasing(chamberEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// Try and pull a round from the magazine to replace the chamber if possible
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
var nextRound = magazine?.GetComponent<RangedMagazineComponent>().TakeAmmo();
|
||||
|
||||
if (nextRound != null)
|
||||
{
|
||||
// If you're really into gunporn you could put a sound here
|
||||
_chamberContainer.Insert(nextRound);
|
||||
}
|
||||
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
|
||||
if (_autoEjectMag && magazine != null && magazine.GetComponent<RangedMagazineComponent>().ShotsLeft == 0)
|
||||
{
|
||||
if (_soundAutoEject != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundAutoEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
_magazineContainer.Remove(magazine);
|
||||
}
|
||||
|
||||
if (nextRound == null && !BoltOpen)
|
||||
{
|
||||
if (_soundBoltOpen != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
|
||||
if (ContainerHelpers.TryGetContainer(Owner, out var container))
|
||||
{
|
||||
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt open"));
|
||||
}
|
||||
BoltOpen = true;
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return;
|
||||
}
|
||||
|
||||
if (manual)
|
||||
{
|
||||
if (_soundRack != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundRack, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(BarrelBoltVisuals.BoltOpen, BoltOpen);
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _magazineContainer.ContainedEntity != null);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
// Behavior:
|
||||
// If bolt open just close it
|
||||
// If bolt closed then cycle
|
||||
// If we cycle then get next round
|
||||
// If no more round then open bolt
|
||||
|
||||
if (BoltOpen)
|
||||
{
|
||||
if (_soundBoltClosed != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Bolt closed"));
|
||||
BoltOpen = false;
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Could play a rack-slide specific sound here if you're so inclined (if the chamber is empty but rounds are available)
|
||||
|
||||
Cycle(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveMagazine(IEntity user)
|
||||
{
|
||||
var mag = _magazineContainer.ContainedEntity;
|
||||
|
||||
if (mag == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (MagNeedsOpenBolt && !BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Bolt needs to be open"));
|
||||
return;
|
||||
}
|
||||
|
||||
_magazineContainer.Remove(mag);
|
||||
if (_soundMagEject != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundMagEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
if (user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
handsComponent.PutInHandOrDrop(mag.GetComponent<ItemComponent>());
|
||||
}
|
||||
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
// Insert magazine
|
||||
if (eventArgs.Using.TryGetComponent(out RangedMagazineComponent magazineComponent))
|
||||
{
|
||||
if ((MagazineTypes & magazineComponent.MagazineType) == 0)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong magazine type"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (magazineComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_magNeedsOpenBolt && !BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Need to open bolt first"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_magazineContainer.ContainedEntity == null)
|
||||
{
|
||||
if (_soundMagInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundMagInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Magazine inserted"));
|
||||
_magazineContainer.Insert(eventArgs.Using);
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Already holding a magazine"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Insert 1 ammo
|
||||
if (eventArgs.Using.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
if (!BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Cannot insert ammo while bolt is closed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_chamberContainer.ContainedEntity == null)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Ammo inserted"));
|
||||
_chamberContainer.Insert(eventArgs.Using);
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Chamber full"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class EjectMagazineVerb : Verb<ServerMagazineBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Eject magazine");
|
||||
if (component.MagNeedsOpenBolt)
|
||||
{
|
||||
data.Visibility = component.HasMagazine && component.BoltOpen
|
||||
? VerbVisibility.Visible
|
||||
: VerbVisibility.Disabled;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Visibility = component.HasMagazine ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
|
||||
{
|
||||
component.RemoveMagazine(user);
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class OpenBoltVerb : Verb<ServerMagazineBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Open bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Disabled : VerbVisibility.Visible;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
|
||||
{
|
||||
component.ToggleBolt();
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class CloseBoltVerb : Verb<ServerMagazineBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Close bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
|
||||
{
|
||||
component.ToggleBolt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum MagazineType
|
||||
{
|
||||
|
||||
Unspecified = 0,
|
||||
LPistol = 1 << 0, // Placeholder?
|
||||
Pistol = 1 << 1,
|
||||
HCPistol = 1 << 2,
|
||||
Smg = 1 << 3,
|
||||
SmgTopMounted = 1 << 4,
|
||||
Rifle = 1 << 5,
|
||||
IH = 1 << 6, // Placeholder?
|
||||
Box = 1 << 7,
|
||||
Pan = 1 << 8,
|
||||
Dart = 1 << 9, // Placeholder
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Projectiles;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Physics;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
/// <summary>
|
||||
/// All of the ranged weapon components inherit from this to share mechanics like shooting etc.
|
||||
/// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.)
|
||||
/// </summary>
|
||||
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IUse, IInteractUsing
|
||||
{
|
||||
// There's still some of py01 and PJB's work left over, especially in underlying shooting logic,
|
||||
// it's just when I re-organised it changed me as the contributor
|
||||
#pragma warning disable 649
|
||||
[Dependency] private IGameTiming _gameTiming;
|
||||
[Dependency] private IRobustRandom _robustRandom;
|
||||
#pragma warning restore 649
|
||||
|
||||
public override FireRateSelector FireRateSelector => _fireRateSelector;
|
||||
private FireRateSelector _fireRateSelector;
|
||||
public override FireRateSelector AllRateSelectors => _fireRateSelector;
|
||||
private FireRateSelector _allRateSelectors;
|
||||
public override float FireRate => _fireRate;
|
||||
private float _fireRate;
|
||||
|
||||
// _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
|
||||
private TimeSpan _lastFire;
|
||||
|
||||
public abstract IEntity PeekAmmo();
|
||||
public abstract IEntity TakeProjectile();
|
||||
|
||||
// Recoil / spray control
|
||||
private Angle _minAngle;
|
||||
private Angle _maxAngle;
|
||||
private Angle _currentAngle = Angle.Zero;
|
||||
/// <summary>
|
||||
/// How slowly the angle's theta decays per second in radians
|
||||
/// </summary>
|
||||
private float _angleDecay;
|
||||
/// <summary>
|
||||
/// How quickly the angle's theta builds for every shot fired in radians
|
||||
/// </summary>
|
||||
private float _angleIncrease;
|
||||
// Multiplies the ammo spread to get the final spread of each pellet
|
||||
private float _spreadRatio;
|
||||
|
||||
public bool CanMuzzleFlash => _canMuzzleFlash;
|
||||
private bool _canMuzzleFlash = true;
|
||||
|
||||
// Sounds
|
||||
public string SoundGunshot
|
||||
{
|
||||
get => _soundGunshot;
|
||||
set => _soundGunshot = value;
|
||||
}
|
||||
private string _soundGunshot;
|
||||
public string SoundEmpty => _soundEmpty;
|
||||
private string _soundEmpty;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _fireRateSelector, "currentSelector", FireRateSelector.Safety);
|
||||
serializer.DataField(ref _fireRate, "fireRate", 2.0f);
|
||||
|
||||
// This hard-to-read area's dealing with recoil
|
||||
// Use degrees in yaml as it's easier to read compared to "0.0125f"
|
||||
if (serializer.Reading)
|
||||
{
|
||||
var minAngle = serializer.ReadDataField("minAngle", 0) / 2;
|
||||
_minAngle = Angle.FromDegrees(minAngle);
|
||||
// Random doubles it as it's +/- so uhh we'll just half it here for readability
|
||||
var maxAngle = serializer.ReadDataField("maxAngle", 45) / 2;
|
||||
_maxAngle = Angle.FromDegrees(maxAngle);
|
||||
var angleIncrease = serializer.ReadDataField("angleIncrease", (40 / _fireRate));
|
||||
_angleIncrease = angleIncrease * (float) Math.PI / 180;
|
||||
var angleDecay = serializer.ReadDataField("angleDecay", (float) 20);
|
||||
_angleDecay = angleDecay * (float) Math.PI / 180;
|
||||
serializer.DataField(ref _spreadRatio, "ammoSpreadRatio", 1.0f);
|
||||
|
||||
// FireRate options
|
||||
var allFireRates = serializer.ReadDataField("allSelectors", new List<FireRateSelector>());
|
||||
foreach (var fireRate in allFireRates)
|
||||
{
|
||||
_allRateSelectors |= fireRate;
|
||||
}
|
||||
}
|
||||
|
||||
// For simplicity we'll enforce it this way; ammo determines max spread
|
||||
if (_spreadRatio > 1.0f)
|
||||
{
|
||||
Logger.Error("SpreadRatio must be <= 1.0f for guns");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
serializer.DataField(ref _canMuzzleFlash, "canMuzzleFlash", true);
|
||||
// Sounds
|
||||
serializer.DataField(ref _soundGunshot, "soundGunshot", null);
|
||||
serializer.DataField(ref _soundEmpty, "soundEmpty", "/Audio/Guns/Empty/empty.ogg");
|
||||
}
|
||||
|
||||
public override void OnAdd()
|
||||
{
|
||||
base.OnAdd();
|
||||
var rangedWeapon = Owner.GetComponent<ServerRangedWeaponComponent>();
|
||||
rangedWeapon.Barrel = this;
|
||||
rangedWeapon.FireHandler += Fire;
|
||||
rangedWeapon.WeaponCanFireHandler += WeaponCanFire;
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
var rangedWeapon = Owner.GetComponent<ServerRangedWeaponComponent>();
|
||||
rangedWeapon.Barrel = null;
|
||||
rangedWeapon.FireHandler -= Fire;
|
||||
rangedWeapon.WeaponCanFireHandler -= WeaponCanFire;
|
||||
}
|
||||
|
||||
private Angle GetRecoilAngle(Angle direction)
|
||||
{
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
var timeSinceLastFire = (currentTime - _lastFire).TotalSeconds;
|
||||
var newTheta = Math.Clamp(_currentAngle.Theta + _angleIncrease - _angleDecay * timeSinceLastFire, _minAngle.Theta, _maxAngle.Theta);
|
||||
_currentAngle = new Angle(newTheta);
|
||||
|
||||
var random = (_robustRandom.NextDouble() - 0.5) * 2;
|
||||
var angle = Angle.FromDegrees(direction.Degrees + _currentAngle.Degrees * random);
|
||||
return angle;
|
||||
}
|
||||
|
||||
public abstract bool UseEntity(UseEntityEventArgs eventArgs);
|
||||
public abstract bool InteractUsing(InteractUsingEventArgs eventArgs);
|
||||
|
||||
public void ChangeFireSelector(FireRateSelector rateSelector)
|
||||
{
|
||||
if ((rateSelector & AllRateSelectors) != 0)
|
||||
{
|
||||
_fireRateSelector = rateSelector;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
protected virtual bool WeaponCanFire()
|
||||
{
|
||||
// If the ServerRangedWeaponComponent gets re-done probably need to add the checks here
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Fire(IEntity shooter, GridCoordinates target)
|
||||
{
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
if (ShotsLeft == 0)
|
||||
{
|
||||
if (_soundEmpty != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var ammo = PeekAmmo();
|
||||
var projectile = TakeProjectile();
|
||||
if (projectile == null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point firing is confirmed
|
||||
var worldPosition = IoCManager.Resolve<IMapManager>().GetGrid(target.GridID).LocalToWorld(target).Position;
|
||||
var direction = (worldPosition - shooter.Transform.WorldPosition).ToAngle();
|
||||
var angle = GetRecoilAngle(direction);
|
||||
// This should really be client-side but for now we'll just leave it here
|
||||
if (shooter.TryGetComponent(out CameraRecoilComponent recoilComponent))
|
||||
{
|
||||
recoilComponent.Kick(-angle.ToVec() * 0.15f);
|
||||
}
|
||||
|
||||
// This section probably needs tweaking so there can be caseless hitscan etc.
|
||||
if (projectile.TryGetComponent(out HitscanComponent hitscan))
|
||||
{
|
||||
FireHitscan(shooter, hitscan, angle);
|
||||
}
|
||||
else if (projectile.HasComponent<ProjectileComponent>())
|
||||
{
|
||||
var ammoComponent = ammo.GetComponent<AmmoComponent>();
|
||||
|
||||
FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity);
|
||||
|
||||
if (CanMuzzleFlash)
|
||||
{
|
||||
ammoComponent.MuzzleFlash(Owner.Transform.GridPosition, angle);
|
||||
}
|
||||
|
||||
if (ammoComponent.Caseless)
|
||||
{
|
||||
ammo.Delete();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid types
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
soundSystem.PlayAtCoords(_soundGunshot, Owner.Transform.GridPosition);
|
||||
_lastFire = _gameTiming.CurTime;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops a single cartridge / shell
|
||||
/// Made as a static function just because multiple places need it
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="playSound"></param>
|
||||
/// <param name="robustRandom"></param>
|
||||
/// <param name="prototypeManager"></param>
|
||||
/// <param name="ejectDirections"></param>
|
||||
public static void EjectCasing(
|
||||
IEntity entity,
|
||||
bool playSound = true,
|
||||
IRobustRandom robustRandom = null,
|
||||
IPrototypeManager prototypeManager = null,
|
||||
Direction[] ejectDirections = null)
|
||||
{
|
||||
if (robustRandom == null)
|
||||
{
|
||||
robustRandom = IoCManager.Resolve<IRobustRandom>();
|
||||
}
|
||||
|
||||
if (ejectDirections == null)
|
||||
{
|
||||
ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
|
||||
}
|
||||
|
||||
const float ejectOffset = 0.2f;
|
||||
var ammo = entity.GetComponent<AmmoComponent>();
|
||||
var offsetPos = (robustRandom.NextFloat() * ejectOffset, robustRandom.NextFloat() * ejectOffset);
|
||||
entity.Transform.GridPosition = entity.Transform.GridPosition.Offset(offsetPos);
|
||||
entity.Transform.LocalRotation = robustRandom.Pick(ejectDirections).ToAngle();
|
||||
|
||||
if (ammo.SoundCollectionEject == null || !playSound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (prototypeManager == null)
|
||||
{
|
||||
prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
}
|
||||
|
||||
var soundCollection = prototypeManager.Index<SoundCollectionPrototype>(ammo.SoundCollectionEject);
|
||||
var randomFile = robustRandom.Pick(soundCollection.PickFiles);
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(randomFile, entity.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops multiple cartridges / shells on the floor
|
||||
/// Wraps EjectCasing to make it less toxic for bulk ejections
|
||||
/// </summary>
|
||||
/// <param name="entities"></param>
|
||||
public static void EjectCasings(IEnumerable<IEntity> entities)
|
||||
{
|
||||
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
var ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
|
||||
var soundPlayCount = 0;
|
||||
var playSound = true;
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
EjectCasing(entity, playSound, robustRandom, prototypeManager, ejectDirections);
|
||||
soundPlayCount++;
|
||||
if (soundPlayCount > 3)
|
||||
{
|
||||
playSound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Firing
|
||||
/// <summary>
|
||||
/// Handles firing one or many projectiles
|
||||
/// </summary>
|
||||
private void FireProjectiles(IEntity shooter, IEntity baseProjectile, int count, float evenSpreadAngle, Angle angle, float velocity)
|
||||
{
|
||||
List<Angle> sprayAngleChange = null;
|
||||
if (count > 1)
|
||||
{
|
||||
evenSpreadAngle *= _spreadRatio;
|
||||
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
|
||||
}
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
IEntity projectile;
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
projectile = baseProjectile;
|
||||
}
|
||||
else
|
||||
{
|
||||
projectile =
|
||||
Owner.EntityManager.SpawnEntity(baseProjectile.Prototype.ID, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
Angle projectileAngle;
|
||||
|
||||
if (sprayAngleChange != null)
|
||||
{
|
||||
projectileAngle = angle + sprayAngleChange[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
projectileAngle = angle;
|
||||
}
|
||||
|
||||
var physicsComponent = projectile.GetComponent<PhysicsComponent>();
|
||||
physicsComponent.Status = BodyStatus.InAir;
|
||||
projectile.Transform.GridPosition = Owner.Transform.GridPosition;
|
||||
|
||||
var projectileComponent = projectile.GetComponent<ProjectileComponent>();
|
||||
projectileComponent.IgnoreEntity(shooter);
|
||||
projectile.GetComponent<PhysicsComponent>().LinearVelocity = projectileAngle.ToVec() * velocity;
|
||||
projectile.Transform.LocalRotation = projectileAngle.Theta;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
|
||||
/// </summary>
|
||||
private List<Angle> Linspace(double start, double end, int intervals)
|
||||
{
|
||||
DebugTools.Assert(intervals > 1);
|
||||
|
||||
var linspace = new List<Angle>(intervals);
|
||||
|
||||
for (var i = 0; i <= intervals - 1; i++)
|
||||
{
|
||||
linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
|
||||
}
|
||||
return linspace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires hitscan entities and then displays their effects
|
||||
/// </summary>
|
||||
private void FireHitscan(IEntity shooter, HitscanComponent hitscan, Angle angle)
|
||||
{
|
||||
var ray = new CollisionRay(Owner.Transform.GridPosition.Position, angle.ToVec(), (int) hitscan.CollisionMask);
|
||||
var physicsManager = IoCManager.Resolve<IPhysicsManager>();
|
||||
var rayCastResults = physicsManager.IntersectRay(Owner.Transform.MapID, ray, hitscan.MaxLength, shooter, false).ToList();
|
||||
|
||||
if (rayCastResults.Count >= 1)
|
||||
{
|
||||
var result = rayCastResults[0];
|
||||
var distance = result.HitEntity != null ? result.Distance : hitscan.MaxLength;
|
||||
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
|
||||
|
||||
if (result.HitEntity == null || !result.HitEntity.TryGetComponent(out DamageableComponent damageable))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
damageable.TakeDamage(
|
||||
hitscan.DamageType,
|
||||
(int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero),
|
||||
Owner,
|
||||
shooter);
|
||||
//I used Math.Round over Convert.toInt32, as toInt32 always rounds to
|
||||
//even numbers if halfway between two numbers, rather than rounding to nearest
|
||||
}
|
||||
else
|
||||
{
|
||||
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class HitscanWeaponCapacitorComponent : PowerCellComponent
|
||||
{
|
||||
private AppearanceComponent _appearance;
|
||||
|
||||
public override string Name => "HitscanWeaponCapacitor";
|
||||
|
||||
public override float Charge
|
||||
{
|
||||
get => base.Charge;
|
||||
set
|
||||
{
|
||||
base.Charge = value;
|
||||
_updateAppearance();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Charge = Capacity;
|
||||
Owner.TryGetComponent(out _appearance);
|
||||
|
||||
}
|
||||
|
||||
public float GetChargeFrom(float toDeduct)
|
||||
{
|
||||
//Use this function when you want to shoot even though you don't have enough energy for basecost
|
||||
ChargeChanged();
|
||||
var chargeChangedBy = Math.Min(this.Charge, toDeduct);
|
||||
this.DeductCharge(chargeChangedBy);
|
||||
_updateAppearance();
|
||||
return chargeChangedBy;
|
||||
}
|
||||
|
||||
public void FillFrom(PowerStorageComponent battery)
|
||||
{
|
||||
var capacitorPowerDeficit = this.Capacity - this.Charge;
|
||||
if (battery.CanDeductCharge(capacitorPowerDeficit))
|
||||
{
|
||||
battery.DeductCharge(capacitorPowerDeficit);
|
||||
this.AddCharge(capacitorPowerDeficit);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddCharge(battery.Charge);
|
||||
battery.DeductCharge(battery.Charge);
|
||||
}
|
||||
_updateAppearance();
|
||||
}
|
||||
|
||||
private void _updateAppearance()
|
||||
{
|
||||
_appearance?.SetData(PowerCellVisuals.ChargeLevel, Charge / Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Physics;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class HitscanWeaponComponent : Component, IInteractUsing
|
||||
{
|
||||
private const float MaxLength = 20;
|
||||
public override string Name => "HitscanWeapon";
|
||||
|
||||
string _spritename;
|
||||
private int _damage;
|
||||
private int _baseFireCost;
|
||||
private float _lowerChargeLimit;
|
||||
private string _fireSound;
|
||||
|
||||
//As this is a component that sits on the weapon rather than a static value
|
||||
//we just declare the field and then use GetComponent later to actually get it.
|
||||
//Do remember to add it in both the .yaml prototype and the factory in EntryPoint.cs
|
||||
//Otherwise you will get errors
|
||||
private HitscanWeaponCapacitorComponent capacitorComponent;
|
||||
|
||||
public int Damage => _damage;
|
||||
|
||||
public int BaseFireCost => _baseFireCost;
|
||||
|
||||
public HitscanWeaponCapacitorComponent CapacitorComponent => capacitorComponent;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _spritename, "fireSprite", "Objects/laser.png");
|
||||
serializer.DataField(ref _damage, "damage", 10);
|
||||
serializer.DataField(ref _baseFireCost, "baseFireCost", 300);
|
||||
serializer.DataField(ref _lowerChargeLimit, "lowerChargeLimit", 10);
|
||||
serializer.DataField(ref _fireSound, "fireSound", "/Audio/laser.ogg");
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var rangedWeapon = Owner.GetComponent<RangedWeaponComponent>();
|
||||
capacitorComponent = Owner.GetComponent<HitscanWeaponCapacitorComponent>();
|
||||
rangedWeapon.FireHandler = Fire;
|
||||
|
||||
}
|
||||
|
||||
public bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.TryGetComponent(out PowerStorageComponent component))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (capacitorComponent.Full)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Capacitor at max charge");
|
||||
return false;
|
||||
}
|
||||
capacitorComponent.FillFrom(component);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Fire(IEntity user, GridCoordinates clickLocation)
|
||||
{
|
||||
if (capacitorComponent.Charge < _lowerChargeLimit)
|
||||
{//If capacitor has less energy than the lower limit, do nothing
|
||||
return;
|
||||
}
|
||||
float energyModifier = capacitorComponent.GetChargeFrom(_baseFireCost) / _baseFireCost;
|
||||
var userPosition = user.Transform.WorldPosition; //Remember world positions are ephemeral and can only be used instantaneously
|
||||
var angle = new Angle(clickLocation.Position - userPosition);
|
||||
|
||||
var ray = new CollisionRay(userPosition, angle.ToVec(), (int)(CollisionGroup.Opaque));
|
||||
var rayCastResults = IoCManager.Resolve<IPhysicsManager>().IntersectRay(user.Transform.MapID, ray, MaxLength, user, returnOnFirstHit: false).ToList();
|
||||
|
||||
//The first result is guaranteed to be the closest one
|
||||
if (rayCastResults.Count >= 1)
|
||||
{
|
||||
Hit(rayCastResults[0], energyModifier, user);
|
||||
AfterEffects(user, rayCastResults[0].Distance, angle, energyModifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
AfterEffects(user, MaxLength, angle, energyModifier);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Hit(RayCastResults ray, float damageModifier, IEntity user = null)
|
||||
{
|
||||
if (ray.HitEntity != null && ray.HitEntity.TryGetComponent(out DamageableComponent damage))
|
||||
{
|
||||
damage.TakeDamage(DamageType.Heat, (int)Math.Round(_damage * damageModifier, MidpointRounding.AwayFromZero), Owner, user);
|
||||
//I used Math.Round over Convert.toInt32, as toInt32 always rounds to
|
||||
//even numbers if halfway between two numbers, rather than rounding to nearest
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void AfterEffects(IEntity user, float distance, Angle angle, float energyModifier)
|
||||
{
|
||||
var time = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
var offset = angle.ToVec() * distance / 2;
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = _spritename,
|
||||
Born = time,
|
||||
DeathTime = time + TimeSpan.FromSeconds(1),
|
||||
Size = new Vector2(distance, 1f),
|
||||
Coordinates = user.Transform.GridPosition.Translated(offset),
|
||||
//Rotated from east facing
|
||||
Rotation = (float) angle.Theta,
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), energyModifier),
|
||||
|
||||
Shaded = false
|
||||
};
|
||||
EntitySystem.Get<EffectSystem>().CreateParticle(message);
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_fireSound, Owner, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class AmmoBoxComponent : Component, IInteractUsing, IMapInit
|
||||
// TODO: Potential improvements:
|
||||
// Add verbs for stack splitting
|
||||
// Behaviour is largely the same as BallisticMagazine except you can't insert it into a gun.
|
||||
{
|
||||
public override string Name => "AmmoBox";
|
||||
private BallisticCaliber _caliber;
|
||||
private int _capacity;
|
||||
[ViewVariables] private int _availableSpawnCount;
|
||||
|
||||
[ViewVariables] private readonly Stack<IEntity> _loadedBullets = new Stack<IEntity>();
|
||||
|
||||
[ViewVariables]
|
||||
public string FillType => _fillType;
|
||||
private string _fillType;
|
||||
|
||||
[ViewVariables] private Container _bulletContainer;
|
||||
[ViewVariables] private AppearanceComponent _appearance;
|
||||
|
||||
[ViewVariables] public int Capacity => _capacity;
|
||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
||||
[ViewVariables] public int CountLeft => _loadedBullets.Count + _availableSpawnCount;
|
||||
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _fillType, "fill", null);
|
||||
serializer.DataField(ref _capacity, "capacity", 30);
|
||||
serializer.DataField(ref _availableSpawnCount, "availableSpawnCount", Capacity);
|
||||
}
|
||||
|
||||
private void _updateAppearance()
|
||||
{
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoLeft, CountLeft);
|
||||
}
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
_availableSpawnCount = Capacity;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_appearance = Owner.GetComponent<AppearanceComponent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
_bulletContainer =
|
||||
ContainerManagerComponent.Ensure<Container>("box_bullet_container", Owner, out var existed);
|
||||
|
||||
if (existed)
|
||||
{
|
||||
foreach (var entity in _bulletContainer.ContainedEntities)
|
||||
{
|
||||
_loadedBullets.Push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
_updateAppearance();
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoCapacity, Capacity);
|
||||
}
|
||||
|
||||
AmmoBoxTransferPopupMessage CanTransferFrom(IEntity source)
|
||||
{
|
||||
// Currently the below duplicates mags but at some stage these will likely differ
|
||||
if (source.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
if (magazineComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLeft == Capacity)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (magazineComponent.CountLoaded == 0)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoBoxTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
if (source.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
if (boxComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLeft == Capacity)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (boxComponent.CountLeft == 0)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoBoxTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "");
|
||||
}
|
||||
|
||||
// TODO: Potentially abstract out to reduce duplicate structs
|
||||
private struct AmmoBoxTransferPopupMessage
|
||||
{
|
||||
public readonly bool Result;
|
||||
public readonly string Message;
|
||||
|
||||
public AmmoBoxTransferPopupMessage(bool result, string message)
|
||||
{
|
||||
Result = result;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
var ammoBoxTransfer = CanTransferFrom(eventArgs.Using);
|
||||
if (ammoBoxTransfer.Result) {
|
||||
IEntity bullet;
|
||||
if (eventArgs.Using.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
int fillCount = Math.Min(magazineComponent.CountLoaded, Capacity - CountLeft);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = magazineComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
if (eventArgs.Using.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
int fillCount = Math.Min(boxComponent.CountLeft, Capacity - CountLeft);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = boxComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User, ammoBoxTransfer.Message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void AddBullet(IEntity bullet)
|
||||
{
|
||||
if (Owner.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
magazineComponent.AddBullet(bullet);
|
||||
return;
|
||||
}
|
||||
if (!bullet.TryGetComponent(out BallisticBulletComponent component))
|
||||
{
|
||||
throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (component.Caliber != Caliber)
|
||||
{
|
||||
throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (CountLeft >= Capacity)
|
||||
{
|
||||
throw new InvalidOperationException("Box is full.");
|
||||
}
|
||||
|
||||
_bulletContainer.Insert(bullet);
|
||||
_loadedBullets.Push(bullet);
|
||||
_updateAppearance();
|
||||
}
|
||||
|
||||
public IEntity TakeBullet()
|
||||
{
|
||||
IEntity bullet;
|
||||
if (Owner.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
bullet = magazineComponent.TakeBullet();
|
||||
return bullet;
|
||||
}
|
||||
if (_loadedBullets.Count == 0)
|
||||
{
|
||||
if (_availableSpawnCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_availableSpawnCount -= 1;
|
||||
bullet = Owner.EntityManager.SpawnEntity(FillType, Owner.Transform.GridPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
bullet = _loadedBullets.Pop();
|
||||
_bulletContainer.Remove(bullet);
|
||||
}
|
||||
|
||||
_updateAppearance();
|
||||
return bullet;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Passes information about the projectiles to be fired by AmmoWeapons
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class BallisticBulletComponent : Component
|
||||
{
|
||||
public override string Name => "BallisticBullet";
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
/// <summary>
|
||||
/// Cartridge calibre, restricts what AmmoWeapons this ammo can be fired from.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public BallisticCaliber Caliber { get => _caliber; set => _caliber = value; }
|
||||
|
||||
private string _projectileID;
|
||||
/// <summary>
|
||||
/// YAML ID of the projectiles to be created when firing this ammo.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string ProjectileID { get => _projectileID; set => _projectileID = value; }
|
||||
|
||||
private int _projectilesFired;
|
||||
/// <summary>
|
||||
/// How many copies of the projectile are shot.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int ProjectilesFired { get => _projectilesFired; set => _projectilesFired = value; }
|
||||
|
||||
private float _spreadStdDev_Ammo;
|
||||
/// <summary>
|
||||
/// Weapons that fire projectiles from ammo types.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SpreadStdDev_Ammo { get => _spreadStdDev_Ammo; set => _spreadStdDev_Ammo = value; }
|
||||
|
||||
private float _evenSpreadAngle_Ammo;
|
||||
/// <summary>
|
||||
/// Arc angle of shotgun pellet spreads, only used if multiple projectiles are being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float EvenSpreadAngle_Ammo { get => _evenSpreadAngle_Ammo; set => _evenSpreadAngle_Ammo = value; }
|
||||
|
||||
private float _velocity_Ammo;
|
||||
/// <summary>
|
||||
/// Adds additional velocity to the projectile, on top of what it already has.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Velocity_Ammo { get => _velocity_Ammo; set => _velocity_Ammo = value; }
|
||||
|
||||
private bool _spent;
|
||||
/// <summary>
|
||||
/// If the ammo cartridge has been shot already.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Spent { get => _spent; set => _spent = value; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _projectileID, "projectile", null);
|
||||
serializer.DataField(ref _spent, "spent", false);
|
||||
serializer.DataField(ref _projectilesFired, "projectilesfired", 1);
|
||||
serializer.DataField(ref _spreadStdDev_Ammo, "ammostddev", 0);
|
||||
serializer.DataField(ref _evenSpreadAngle_Ammo, "ammospread", 0);
|
||||
serializer.DataField(ref _velocity_Ammo, "ammovelocity", 0);
|
||||
}
|
||||
}
|
||||
public enum BallisticCaliber
|
||||
{
|
||||
Unspecified = 0,
|
||||
// .32
|
||||
A32,
|
||||
// .357
|
||||
A357,
|
||||
// .44
|
||||
A44,
|
||||
// .45mm
|
||||
A45mm,
|
||||
// .50 cal
|
||||
A50,
|
||||
// 5.56mm
|
||||
A556mm,
|
||||
// 6.5mm
|
||||
A65mm,
|
||||
// 7.62mm
|
||||
A762mm,
|
||||
// 9mm
|
||||
A9mm,
|
||||
// 10mm
|
||||
A10mm,
|
||||
// 20mm
|
||||
A20mm,
|
||||
// 24mm
|
||||
A24mm,
|
||||
// 12g
|
||||
A12g,
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class BallisticMagazineComponent : Component, IMapInit, IInteractUsing
|
||||
{
|
||||
public override string Name => "BallisticMagazine";
|
||||
|
||||
// Stack of loaded bullets.
|
||||
[ViewVariables] private readonly Stack<IEntity> _loadedBullets = new Stack<IEntity>();
|
||||
private string _fillType;
|
||||
|
||||
[ViewVariables] private Container _bulletContainer;
|
||||
[ViewVariables] private AppearanceComponent _appearance;
|
||||
|
||||
private BallisticMagazineType _magazineType;
|
||||
private BallisticCaliber _caliber;
|
||||
private int _capacity;
|
||||
|
||||
[ViewVariables] public string FillType => _fillType;
|
||||
[ViewVariables] public BallisticMagazineType MagazineType => _magazineType;
|
||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
||||
[ViewVariables] public int Capacity => _capacity;
|
||||
|
||||
[ViewVariables] public int CountLoaded => _loadedBullets.Count + _availableSpawnCount;
|
||||
|
||||
[ViewVariables] private int _availableSpawnCount;
|
||||
|
||||
public event Action OnAmmoCountChanged;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _magazineType, "magazine", BallisticMagazineType.Unspecified);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _fillType, "fill", null);
|
||||
serializer.DataField(ref _capacity, "capacity", 20);
|
||||
serializer.DataField(ref _availableSpawnCount, "availableSpawnCount", Capacity);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_appearance = Owner.GetComponent<AppearanceComponent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
_bulletContainer =
|
||||
ContainerManagerComponent.Ensure<Container>("magazine_bullet_container", Owner, out var existed);
|
||||
|
||||
if (existed)
|
||||
{
|
||||
foreach (var entity in _bulletContainer.ContainedEntities)
|
||||
{
|
||||
_loadedBullets.Push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
|
||||
OnAmmoCountChanged?.Invoke();
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoCapacity, Capacity);
|
||||
}
|
||||
|
||||
public void AddBullet(IEntity bullet)
|
||||
{
|
||||
if (!bullet.TryGetComponent(out BallisticBulletComponent component))
|
||||
{
|
||||
throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (component.Caliber != Caliber)
|
||||
{
|
||||
throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (CountLoaded >= Capacity)
|
||||
{
|
||||
throw new InvalidOperationException("Magazine is full.");
|
||||
}
|
||||
|
||||
_bulletContainer.Insert(bullet);
|
||||
_loadedBullets.Push(bullet);
|
||||
UpdateAppearance();
|
||||
OnAmmoCountChanged?.Invoke();
|
||||
}
|
||||
|
||||
public IEntity TakeBullet()
|
||||
{
|
||||
IEntity bullet;
|
||||
if (_loadedBullets.Count == 0)
|
||||
{
|
||||
if (_availableSpawnCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_availableSpawnCount -= 1;
|
||||
bullet = Owner.EntityManager.SpawnEntity(FillType, Owner.Transform.GridPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
bullet = _loadedBullets.Pop();
|
||||
_bulletContainer.Remove(bullet);
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
OnAmmoCountChanged?.Invoke();
|
||||
return bullet;
|
||||
}
|
||||
|
||||
// TODO: Allow putting individual casings into mag (also box)
|
||||
AmmoMagTransferPopupMessage CanTransferFrom(IEntity source)
|
||||
{
|
||||
// Currently the below duplicates box but at some stage these will likely differ
|
||||
if (source.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
if (magazineComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLoaded == Capacity)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (magazineComponent.CountLoaded == 0)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoMagTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
// If box
|
||||
if (source.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
if (boxComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLoaded == Capacity)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (boxComponent.CountLeft == 0)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoMagTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "");
|
||||
}
|
||||
|
||||
// TODO: Potentially abstract out to reduce duplicate structs
|
||||
private struct AmmoMagTransferPopupMessage
|
||||
{
|
||||
public readonly bool Result;
|
||||
public readonly string Message;
|
||||
|
||||
public AmmoMagTransferPopupMessage(bool result, string message)
|
||||
{
|
||||
Result = result;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
var ammoMagTransfer = CanTransferFrom(eventArgs.Using);
|
||||
if (ammoMagTransfer.Result) {
|
||||
IEntity bullet;
|
||||
if (eventArgs.Using.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
int fillCount = Math.Min(magazineComponent.CountLoaded, Capacity - CountLoaded);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = magazineComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
if (eventArgs.Using.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
int fillCount = Math.Min(boxComponent.CountLeft, Capacity - CountLoaded);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = boxComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User, ammoMagTransfer.Message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoLeft, CountLoaded);
|
||||
}
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
_availableSpawnCount = Capacity;
|
||||
}
|
||||
}
|
||||
|
||||
public enum BallisticMagazineType
|
||||
{
|
||||
Unspecified = 0,
|
||||
|
||||
// .32
|
||||
A32,
|
||||
|
||||
// .357
|
||||
A357,
|
||||
|
||||
// .44
|
||||
A44,
|
||||
|
||||
// .45mm
|
||||
A45mm,
|
||||
|
||||
// .50 cal
|
||||
A50,
|
||||
|
||||
// 5.56mm
|
||||
A556mm,
|
||||
|
||||
// 6.5mm
|
||||
A65mm,
|
||||
|
||||
// 7.62mm
|
||||
A762mm,
|
||||
Maxim,
|
||||
|
||||
// 9mm
|
||||
A9mm,
|
||||
A9mmSMG,
|
||||
A9mmTopMounted,
|
||||
|
||||
// 10mm
|
||||
A10mm,
|
||||
A10mmSMG,
|
||||
|
||||
// 20mm
|
||||
A20mm,
|
||||
|
||||
// 24mm
|
||||
A24mm,
|
||||
|
||||
// 12g
|
||||
A12g,
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Interfaces;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Guns that have a magazine.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class BallisticMagazineWeaponComponent : BallisticWeaponComponent, IUse, IInteractUsing, IMapInit
|
||||
{
|
||||
private const float BulletOffset = 0.2f;
|
||||
|
||||
public override string Name => "BallisticMagazineWeapon";
|
||||
public override uint? NetID => ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON;
|
||||
|
||||
[ViewVariables] private string _defaultMagazine;
|
||||
|
||||
public ContainerSlot MagazineSlot => _magazineSlot;
|
||||
[ViewVariables] private ContainerSlot _magazineSlot;
|
||||
private List<BallisticMagazineType> _magazineTypes;
|
||||
|
||||
[ViewVariables] public List<BallisticMagazineType> MagazineTypes => _magazineTypes;
|
||||
[ViewVariables] private IEntity Magazine => _magazineSlot.ContainedEntity;
|
||||
|
||||
#pragma warning disable 649
|
||||
[Dependency] private readonly IRobustRandom _bulletDropRandom;
|
||||
#pragma warning restore 649
|
||||
[ViewVariables] private string _magInSound;
|
||||
[ViewVariables] private string _magOutSound;
|
||||
[ViewVariables] private string _autoEjectSound;
|
||||
[ViewVariables] private bool _autoEjectMagazine;
|
||||
[ViewVariables] private AppearanceComponent _appearance;
|
||||
|
||||
private static readonly Direction[] RandomBulletDirs =
|
||||
{
|
||||
Direction.North,
|
||||
Direction.East,
|
||||
Direction.South,
|
||||
Direction.West
|
||||
};
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _magazineTypes, "magazines",
|
||||
new List<BallisticMagazineType> {BallisticMagazineType.Unspecified});
|
||||
serializer.DataField(ref _defaultMagazine, "default_magazine", null);
|
||||
serializer.DataField(ref _autoEjectMagazine, "auto_eject_magazine", false);
|
||||
serializer.DataField(ref _autoEjectSound, "sound_auto_eject", null);
|
||||
serializer.DataField(ref _magInSound, "sound_magazine_in", null);
|
||||
serializer.DataField(ref _magOutSound, "sound_magazine_out", null);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_appearance = Owner.GetComponent<AppearanceComponent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
_magazineSlot = ContainerManagerComponent.Ensure<ContainerSlot>("ballistic_gun_magazine", Owner);
|
||||
if (Magazine != null)
|
||||
{
|
||||
// Already got magazine from loading a container.
|
||||
Magazine.GetComponent<BallisticMagazineComponent>().OnAmmoCountChanged += MagazineAmmoCountChanged;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public bool InsertMagazine(IEntity magazine, bool playSound = true)
|
||||
{
|
||||
if (!magazine.TryGetComponent(out BallisticMagazineComponent magazinetype))
|
||||
{
|
||||
throw new ArgumentException("Not a magazine", nameof(magazine));
|
||||
}
|
||||
if (!MagazineTypes.Contains(magazinetype.MagazineType))
|
||||
{
|
||||
throw new ArgumentException("Wrong magazine type", nameof(magazine));
|
||||
}
|
||||
if (!_magazineSlot.Insert(magazine))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_magInSound != null && playSound)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_magInSound, Owner);
|
||||
}
|
||||
magazinetype.OnAmmoCountChanged += MagazineAmmoCountChanged;
|
||||
if (GetChambered(0) == null)
|
||||
{
|
||||
// No bullet in chamber, load one from magazine.
|
||||
var bullet = magazinetype.TakeBullet();
|
||||
if (bullet != null)
|
||||
{
|
||||
LoadIntoChamber(0, bullet);
|
||||
}
|
||||
}
|
||||
UpdateAppearance();
|
||||
Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool EjectMagazine(bool playSound = true)
|
||||
{
|
||||
var entity = Magazine;
|
||||
if (entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_magazineSlot.Remove(entity))
|
||||
{
|
||||
entity.Transform.GridPosition = Owner.Transform.GridPosition;
|
||||
if (_magOutSound != null && playSound)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_magOutSound, Owner, AudioParams.Default.WithVolume(20));
|
||||
}
|
||||
UpdateAppearance();
|
||||
Dirty();
|
||||
entity.GetComponent<BallisticMagazineComponent>().OnAmmoCountChanged -= MagazineAmmoCountChanged;
|
||||
return true;
|
||||
}
|
||||
UpdateAppearance();
|
||||
Dirty();
|
||||
return false;
|
||||
}
|
||||
|
||||
// these are complete strings for the sake of the shared string dict
|
||||
[UsedImplicitly]
|
||||
private static readonly string[] _bulletDropSounds =
|
||||
{
|
||||
"/Audio/Guns/Casings/casingfall1.ogg",
|
||||
"/Audio/Guns/Casings/casingfall2.ogg",
|
||||
"/Audio/Guns/Casings/casingfall3.ogg"
|
||||
};
|
||||
|
||||
protected override void CycleChamberedBullet(int chamber)
|
||||
{
|
||||
DebugTools.Assert(chamber == 0);
|
||||
|
||||
// Eject chambered bullet.
|
||||
var entity = RemoveFromChamber(chamber);
|
||||
if (entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var offsetPos = (CalcBulletOffset(), CalcBulletOffset());
|
||||
entity.Transform.GridPosition = Owner.Transform.GridPosition.Offset(offsetPos);
|
||||
entity.Transform.LocalRotation = _bulletDropRandom.Pick(RandomBulletDirs).ToAngle();
|
||||
var bulletDropNext = _bulletDropRandom.Next(1, 3);
|
||||
var effect = _bulletDropSounds[bulletDropNext];
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(effect, Owner, AudioParams.Default.WithVolume(-3));
|
||||
|
||||
if (Magazine != null)
|
||||
{
|
||||
var magComponent = Magazine.GetComponent<BallisticMagazineComponent>();
|
||||
var bullet = magComponent.TakeBullet();
|
||||
if (bullet != null)
|
||||
{
|
||||
LoadIntoChamber(0, bullet);
|
||||
}
|
||||
|
||||
if (magComponent.CountLoaded == 0 && _autoEjectMagazine)
|
||||
{
|
||||
DoAutoEject();
|
||||
}
|
||||
}
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private float CalcBulletOffset()
|
||||
{
|
||||
return _bulletDropRandom.NextFloat() * (BulletOffset * 2) - BulletOffset;
|
||||
}
|
||||
|
||||
private void DoAutoEject()
|
||||
{
|
||||
SendNetworkMessage(new BmwComponentAutoEjectedMessage());
|
||||
EjectMagazine();
|
||||
if (_autoEjectSound != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_autoEjectSound, Owner, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
var ret = EjectMagazine();
|
||||
if (ret)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Magazine ejected");
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "No magazine");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.TryGetComponent(out BallisticMagazineComponent component))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Magazine != null)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Already got a magazine.");
|
||||
return false;
|
||||
}
|
||||
if (!MagazineTypes.Contains(component.MagazineType))
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Magazine doesn't fit.");
|
||||
return false;
|
||||
}
|
||||
return InsertMagazine(eventArgs.Using);
|
||||
}
|
||||
|
||||
private void MagazineAmmoCountChanged()
|
||||
{
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (Magazine != null)
|
||||
{
|
||||
var comp = Magazine.GetComponent<BallisticMagazineComponent>();
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, comp.CountLoaded);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoCapacity, comp.Capacity);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.MagazineLoaded, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, 0);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, 0);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.MagazineLoaded, false);
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
var chambered = GetChambered(0) != null;
|
||||
(int, int)? count = null;
|
||||
if (Magazine != null)
|
||||
{
|
||||
var magComponent = Magazine.GetComponent<BallisticMagazineComponent>();
|
||||
count = (magComponent.CountLoaded, magComponent.Capacity);
|
||||
}
|
||||
return new BallisticMagazineWeaponComponentState(chambered, count);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
public sealed class EjectMagazineVerb : Verb<BallisticMagazineWeaponComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BallisticMagazineWeaponComponent component, VerbData data)
|
||||
{
|
||||
if (component.Magazine == null)
|
||||
{
|
||||
data.Text = "Eject magazine (magazine missing)";
|
||||
data.Visibility = VerbVisibility.Disabled;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = "Eject magazine";
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BallisticMagazineWeaponComponent component)
|
||||
{
|
||||
component.EjectMagazine();
|
||||
}
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_defaultMagazine != null)
|
||||
{
|
||||
var magazine = Owner.EntityManager.SpawnEntity(_defaultMagazine, Owner.Transform.GridPosition);
|
||||
InsertMagazine(magazine, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles firing projectiles from a contained <see cref="BallisticBulletComponent" />.
|
||||
/// </summary>
|
||||
public abstract class BallisticWeaponComponent : BaseProjectileWeaponComponent
|
||||
{
|
||||
private Chamber[] _chambers;
|
||||
|
||||
/// <summary>
|
||||
/// Number of chambers created during initialization.
|
||||
/// </summary>
|
||||
private int _chamberCount;
|
||||
|
||||
[ViewVariables]
|
||||
private BallisticCaliber _caliber ;
|
||||
/// <summary>
|
||||
/// What type of ammo this gun can fire.
|
||||
/// </summary>
|
||||
|
||||
private string _soundGunEmpty;
|
||||
/// <summary>
|
||||
/// Sound played when trying to shoot if there is no ammo available.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SoundGunEmpty { get => _soundGunEmpty; set => _soundGunEmpty = value; }
|
||||
|
||||
private float _spreadStdDevGun;
|
||||
/// <summary>
|
||||
/// Increases the standard deviation of the ammo being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SpreadStdDevGun { get => _spreadStdDevGun; set => _spreadStdDevGun = value; }
|
||||
|
||||
private float _evenSpreadAngleGun;
|
||||
/// <summary>
|
||||
/// Increases the evenspread of the ammo being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float EvenSpreadAngleGun { get => _evenSpreadAngleGun; set => _evenSpreadAngleGun = value; }
|
||||
|
||||
private float _velocityGun;
|
||||
/// <summary>
|
||||
/// Increases the velocity of the ammo being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float VelocityGun { get => _velocityGun; set => _velocityGun = value; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _soundGunEmpty, "sound_empty", "/Audio/Guns/Empty/empty.ogg");
|
||||
serializer.DataField(ref _spreadStdDevGun, "spreadstddev", 0);
|
||||
serializer.DataField(ref _evenSpreadAngleGun, "evenspread", 0);
|
||||
serializer.DataField(ref _velocityGun, "gunvelocity", 0);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _chamberCount, "chambers", 1);
|
||||
}
|
||||
|
||||
// for shared string dict, since we don't define these anywhere in content
|
||||
[UsedImplicitly]
|
||||
private static readonly string[] _ballisticsChambersStrings =
|
||||
{
|
||||
"ballistics_chamber_0",
|
||||
"ballistics_chamber_1",
|
||||
"ballistics_chamber_2",
|
||||
"ballistics_chamber_3",
|
||||
"ballistics_chamber_4",
|
||||
"ballistics_chamber_5",
|
||||
"ballistics_chamber_6",
|
||||
"ballistics_chamber_7",
|
||||
"ballistics_chamber_8",
|
||||
"ballistics_chamber_9",
|
||||
};
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
Owner.GetComponent<RangedWeaponComponent>().FireHandler = TryShoot;
|
||||
_chambers = new Chamber[_chamberCount];
|
||||
for (var i = 0; i < _chambers.Length; i++)
|
||||
{
|
||||
var container = ContainerManagerComponent.Ensure<ContainerSlot>($"ballistics_chamber_{i}", Owner);
|
||||
_chambers[i] = new Chamber(container);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires projectiles based on loaded ammo from entity to a coordinate.
|
||||
/// </summary>
|
||||
protected void TryShoot(IEntity user, GridCoordinates clickLocation)
|
||||
{
|
||||
var ammo = GetChambered(FirstChamber)?.GetComponent<BallisticBulletComponent>();
|
||||
CycleChamberedBullet(FirstChamber);
|
||||
if (ammo == null || ammo?.Spent == true || ammo?.Caliber != _caliber)
|
||||
{
|
||||
PlayEmptySound();
|
||||
return;
|
||||
}
|
||||
ammo.Spent = true;
|
||||
var total_stdev = _spreadStdDevGun + ammo.SpreadStdDev_Ammo;
|
||||
var final_evenspread = _evenSpreadAngleGun + ammo.EvenSpreadAngle_Ammo;
|
||||
var final_velocity = _velocityGun + ammo.Velocity_Ammo;
|
||||
FireAtCoord(user, clickLocation, ammo.ProjectileID, total_stdev, ammo.ProjectilesFired, final_evenspread, final_velocity);
|
||||
}
|
||||
|
||||
public IEntity GetChambered(int chamber) => _chambers[chamber].Slot.ContainedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the next ammo casing into the chamber.
|
||||
/// </summary>
|
||||
protected virtual void CycleChamberedBullet(int chamber) { }
|
||||
|
||||
public IEntity RemoveFromChamber(int chamber)
|
||||
{
|
||||
var c = _chambers[chamber];
|
||||
var loaded = c.Slot.ContainedEntity;
|
||||
if (loaded != null)
|
||||
{
|
||||
c.Slot.Remove(loaded);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
protected bool LoadIntoChamber(int chamber, IEntity bullet)
|
||||
{
|
||||
if (!bullet.TryGetComponent(out BallisticBulletComponent component))
|
||||
{
|
||||
throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
|
||||
}
|
||||
if (component.Caliber != _caliber)
|
||||
{
|
||||
throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
|
||||
}
|
||||
if (GetChambered(chamber) != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_chambers[chamber].Slot.Insert(bullet);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void PlayEmptySound() => EntitySystem.Get<AudioSystem>().PlayFromEntity(_soundGunEmpty, Owner);
|
||||
|
||||
protected sealed class Chamber
|
||||
{
|
||||
public Chamber(ContainerSlot slot)
|
||||
{
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
public ContainerSlot Slot { get; }
|
||||
}
|
||||
|
||||
private const int FirstChamber = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Projectiles;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods to shoot projectiles.
|
||||
/// </summary>
|
||||
public abstract class BaseProjectileWeaponComponent : Component
|
||||
{
|
||||
private string _soundGunshot;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SoundGunshot
|
||||
{ get => _soundGunshot; set => _soundGunshot = value; }
|
||||
|
||||
#pragma warning disable 649
|
||||
[Dependency] private IRobustRandom _spreadRandom;
|
||||
#pragma warning restore 649
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _soundGunshot, "sound_gunshot", "/Audio/Guns/Gunshots/smg.ogg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires projectile from an entity at a coordinate.
|
||||
/// </summary>
|
||||
protected void FireAtCoord(IEntity source, GridCoordinates coord, string projectileType, double spreadStdDev, int projectilesFired = 1, double evenSpreadAngle = 0, float velocity = 0)
|
||||
{
|
||||
var angle = GetAngleFromClickLocation(source, coord);
|
||||
FireAtAngle(source, angle, projectileType, spreadStdDev, projectilesFired, evenSpreadAngle, velocity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires projectile in the direction of an angle.
|
||||
/// </summary>
|
||||
protected void FireAtAngle(IEntity source, Angle angle, string projectileType = null, double spreadStdDev = 0, int projectilesFired = 1, double evenSpreadAngle = 0, float velocity = 0)
|
||||
{
|
||||
List<Angle> sprayanglechange = null;
|
||||
if (evenSpreadAngle != 0 & projectilesFired > 1)
|
||||
{
|
||||
sprayanglechange = Linspace(-evenSpreadAngle/2, evenSpreadAngle/2, projectilesFired);
|
||||
}
|
||||
for (var i = 1; i <= projectilesFired; i++)
|
||||
{
|
||||
Angle finalangle = angle + Angle.FromDegrees(_spreadRandom.NextGaussian(0, spreadStdDev)) + (sprayanglechange != null ? sprayanglechange[i - 1] : 0);
|
||||
var projectile = Owner.EntityManager.SpawnEntity(projectileType, Owner.Transform.GridPosition);
|
||||
projectile.Transform.GridPosition = source.Transform.GridPosition; //move projectile to entity it is being fired from
|
||||
projectile.GetComponent<ProjectileComponent>().IgnoreEntity(source);//make sure it doesn't hit the source entity
|
||||
var finalvelocity = projectile.GetComponent<ProjectileComponent>().Velocity + velocity;//add velocity
|
||||
var physicsComponent = projectile.GetComponent<PhysicsComponent>();
|
||||
physicsComponent.Status = BodyStatus.InAir;
|
||||
physicsComponent.LinearVelocity = finalangle.ToVec() * finalvelocity;//Rotate the bullets sprite to the correct direction
|
||||
projectile.Transform.LocalRotation = finalangle.Theta;
|
||||
}
|
||||
PlayFireSound();
|
||||
if (source.TryGetComponent(out CameraRecoilComponent recoil))
|
||||
{
|
||||
var recoilVec = angle.ToVec() * -0.15f;
|
||||
recoil.Kick(recoilVec);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayFireSound() => EntitySystem.Get<AudioSystem>().PlayFromEntity(_soundGunshot, Owner);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the angle from an entity to a coordinate.
|
||||
/// </summary>
|
||||
protected Angle GetAngleFromClickLocation(IEntity source, GridCoordinates clickLocation) => new Angle(clickLocation.Position - source.Transform.GridPosition.Position);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
|
||||
/// </summary>
|
||||
protected List<Angle> Linspace(double start, double end, int intervals)
|
||||
{
|
||||
var linspace = new List<Angle> { };
|
||||
for (var i = 0; i <= intervals - 1; i++)
|
||||
{
|
||||
linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
|
||||
}
|
||||
return linspace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,46 @@
|
||||
using System;
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class RangedWeaponComponent : SharedRangedWeaponComponent
|
||||
public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent, IHandSelected
|
||||
{
|
||||
private TimeSpan _lastFireTime;
|
||||
|
||||
|
||||
public Func<bool> WeaponCanFireHandler;
|
||||
public Func<IEntity, bool> UserCanFireHandler;
|
||||
public Action<IEntity, GridCoordinates> FireHandler;
|
||||
|
||||
public ServerRangedBarrelComponent Barrel
|
||||
{
|
||||
get => _barrel;
|
||||
set
|
||||
{
|
||||
if (_barrel != null && value != null)
|
||||
{
|
||||
Logger.Error("Tried setting Barrel on RangedWeapon that already has one");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_barrel = value;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
private ServerRangedBarrelComponent _barrel;
|
||||
|
||||
private FireRateSelector FireRateSelector => _barrel?.FireRateSelector ?? FireRateSelector.Safety;
|
||||
|
||||
private bool WeaponCanFire()
|
||||
{
|
||||
@@ -32,12 +52,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
return (UserCanFireHandler == null || UserCanFireHandler(user)) && ActionBlockerSystem.CanAttack(user);
|
||||
}
|
||||
|
||||
private void Fire(IEntity user, GridCoordinates clickLocation)
|
||||
{
|
||||
_lastFireTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
FireHandler?.Invoke(user, clickLocation);
|
||||
}
|
||||
|
||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession session = null)
|
||||
{
|
||||
base.HandleNetworkMessage(message, channel, session);
|
||||
@@ -49,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case SyncFirePosMessage msg:
|
||||
case FirePosComponentMessage msg:
|
||||
var user = session.AttachedEntity;
|
||||
if (user == null)
|
||||
{
|
||||
@@ -61,16 +75,9 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
}
|
||||
}
|
||||
|
||||
// Probably shouldn't be a separate method but don't want anything except NPCs calling this,
|
||||
// and currently ranged combat is handled via player only messages
|
||||
public void AiFire(IEntity entity, GridCoordinates coordinates)
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
if (!entity.HasComponent<AiControllerComponent>())
|
||||
{
|
||||
throw new InvalidOperationException("Only AIs should call AiFire");
|
||||
}
|
||||
|
||||
_tryFire(entity, coordinates);
|
||||
return new RangedWeaponComponentState(FireRateSelector);
|
||||
}
|
||||
|
||||
private void _tryFire(IEntity user, GridCoordinates coordinates)
|
||||
@@ -91,12 +98,19 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
|
||||
var curTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
var span = curTime - _lastFireTime;
|
||||
if (span.TotalSeconds < 1 / FireRate)
|
||||
if (span.TotalSeconds < 1 / _barrel.FireRate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Fire(user, coordinates);
|
||||
_lastFireTime = curTime;
|
||||
FireHandler?.Invoke(user, coordinates);
|
||||
}
|
||||
|
||||
// Probably a better way to do this.
|
||||
void IHandSelected.HandSelected(HandSelectedEventArgs eventArgs)
|
||||
{
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Weapons;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Physics;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ServerFlashableComponent : SharedFlashableComponent
|
||||
{
|
||||
private double _duration;
|
||||
private TimeSpan _lastFlash;
|
||||
|
||||
public void Flash(double duration)
|
||||
{
|
||||
var timing = IoCManager.Resolve<IGameTiming>();
|
||||
_lastFlash = timing.CurTime;
|
||||
_duration = duration;
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new FlashComponentState(_duration, _lastFlash);
|
||||
}
|
||||
|
||||
public static void FlashAreaHelper(IEntity source, double range, double duration, string sound = null)
|
||||
{
|
||||
var physicsManager = IoCManager.Resolve<IPhysicsManager>();
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
foreach (var entity in entityManager.GetEntities(new TypeEntityQuery(typeof(ServerFlashableComponent))))
|
||||
{
|
||||
if (source.Transform.MapID != entity.Transform.MapID ||
|
||||
entity == source)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var direction = entity.Transform.WorldPosition - source.Transform.WorldPosition;
|
||||
|
||||
if (direction.Length > range)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Direction will be zero if they're hit with the source only I think
|
||||
if (direction == Vector2.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var ray = new CollisionRay(source.Transform.WorldPosition, direction.Normalized, (int) CollisionGroup.Opaque);
|
||||
var rayCastResults = physicsManager.IntersectRay(source.Transform.MapID, ray, direction.Length, source, false).ToList();
|
||||
if (rayCastResults.Count == 0 ||
|
||||
rayCastResults[0].HitEntity != entity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var flashable = entity.GetComponent<ServerFlashableComponent>();
|
||||
flashable.Flash(duration);
|
||||
}
|
||||
|
||||
if (sound != null)
|
||||
{
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayAtCoords(sound, source.Transform.GridPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user