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:
metalgearsloth
2020-06-22 05:47:15 +10:00
committed by GitHub
parent ac19ad7eac
commit 95995b6232
1977 changed files with 13600 additions and 11229 deletions

View File

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

View File

@@ -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)

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {}
}
}

View File

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

View File

@@ -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,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

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

View File

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

View File

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

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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