Re-organize all projects (#4166)

This commit is contained in:
DrSmugleaf
2021-06-09 22:19:39 +02:00
committed by GitHub
parent 9f50e4061b
commit ff1a2d97ea
1773 changed files with 5258 additions and 5508 deletions

View File

@@ -0,0 +1,61 @@
using System;
using Content.Shared.Damage;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Weapon.Melee.Components
{
[RegisterComponent]
public class MeleeWeaponComponent : Component
{
public override string Name => "MeleeWeapon";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("hitSound")]
public string HitSound { get; set; } = "/Audio/Weapons/genhit1.ogg";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("missSound")]
public string MissSound { get; set; } = "/Audio/Weapons/punchmiss.ogg";
[ViewVariables]
[DataField("arcCooldownTime")]
public float ArcCooldownTime { get; } = 1f;
[ViewVariables]
[DataField("cooldownTime")]
public float CooldownTime { get; } = 1f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clickArc")]
public string ClickArc { get; set; } = "punch";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("arc")]
public string Arc { get; set; } = "default";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("arcwidth")]
public float ArcWidth { get; set; } = 90;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("range")]
public float Range { get; set; } = 1;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("damage")]
public int Damage { get; set; } = 5;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("damageType")]
public DamageType DamageType { get; set; } = DamageType.Blunt;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clickAttackEffect")]
public bool ClickAttackEffect { get; set; } = true;
public TimeSpan LastAttackTime;
public TimeSpan CooldownEnd;
}
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameObjects;
namespace Content.Server.Weapon.Melee.Components
{
// TODO: Remove this, just use MeleeWeapon...
[RegisterComponent]
[ComponentReference(typeof(MeleeWeaponComponent))]
public class UnarmedCombatComponent : MeleeWeaponComponent
{
public override string Name => "UnarmedCombat";
}
}

View File

@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Server.Body.Circulatory;
using Content.Server.Chemistry.Components;
using Content.Server.Cooldown;
using Content.Server.Weapon.Melee.Components;
using Content.Shared.Damage.Components;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Weapon.Melee
{
public sealed class MeleeWeaponSystem : EntitySystem
{
[Dependency] private IGameTiming _gameTiming = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MeleeWeaponComponent, HandSelectedEvent>(OnHandSelected);
SubscribeLocalEvent<MeleeWeaponComponent, ClickAttackEvent>(OnClickAttack);
SubscribeLocalEvent<MeleeWeaponComponent, WideAttackEvent>(OnWideAttack);
SubscribeLocalEvent<MeleeWeaponComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(OnChemicalInjectorHit);
}
private void OnHandSelected(EntityUid uid, MeleeWeaponComponent comp, HandSelectedEvent args)
{
var curTime = _gameTiming.CurTime;
var cool = TimeSpan.FromSeconds(comp.CooldownTime * 0.5f);
if (curTime < comp.CooldownEnd)
{
if (comp.CooldownEnd - curTime < cool)
{
comp.LastAttackTime = curTime;
comp.CooldownEnd += cool;
}
else
return;
}
else
{
comp.LastAttackTime = curTime;
comp.CooldownEnd = curTime + cool;
}
RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false);
}
private void OnClickAttack(EntityUid uid, MeleeWeaponComponent comp, ClickAttackEvent args)
{
args.Handled = true;
var curTime = _gameTiming.CurTime;
if (curTime < comp.CooldownEnd || !args.Target.IsValid())
return;
var owner = EntityManager.GetEntity(uid);
var target = args.TargetEntity;
var location = args.User.Transform.Coordinates;
var diff = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager);
var angle = Angle.FromWorldVec(diff);
if (target != null)
{
// Raise event before doing damage so we can cancel damage if the event is handled
var hitEvent = new MeleeHitEvent(new List<IEntity>() {target}, args.User);
RaiseLocalEvent(uid, hitEvent, false);
if (!hitEvent.Handled)
{
var targets = new[] {target};
SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false);
if (target.TryGetComponent(out IDamageableComponent? damageableComponent))
{
damageableComponent.ChangeDamage(comp.DamageType, comp.Damage, false, owner);
}
SoundSystem.Play(Filter.Pvs(owner), comp.HitSound, target);
}
}
else
{
SoundSystem.Play(Filter.Pvs(owner), comp.MissSound, args.User);
return;
}
comp.LastAttackTime = curTime;
comp.CooldownEnd = comp.LastAttackTime + TimeSpan.FromSeconds(comp.CooldownTime);
RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false);
}
private void OnWideAttack(EntityUid uid, MeleeWeaponComponent comp, WideAttackEvent args)
{
args.Handled = true;
var curTime = _gameTiming.CurTime;
if (curTime < comp.CooldownEnd)
{
return;
}
var owner = EntityManager.GetEntity(uid);
var location = args.User.Transform.Coordinates;
var diff = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager);
var angle = Angle.FromWorldVec(diff);
// This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
var entities = ArcRayCast(args.User.Transform.WorldPosition, angle, comp.ArcWidth, comp.Range, owner.Transform.MapID, args.User);
var hitEntities = new List<IEntity>();
foreach (var entity in entities)
{
if (!entity.Transform.IsMapTransform || entity == args.User)
continue;
if (ComponentManager.HasComponent<IDamageableComponent>(entity.Uid))
{
hitEntities.Add(entity);
}
}
// Raise event before doing damage so we can cancel damage if handled
var hitEvent = new MeleeHitEvent(hitEntities, args.User);
RaiseLocalEvent(uid, hitEvent, false);
SendAnimation(comp.Arc, angle, args.User, owner, hitEntities);
if (!hitEvent.Handled)
{
if (entities.Count != 0)
{
SoundSystem.Play(Filter.Pvs(owner), comp.HitSound, entities.First().Transform.Coordinates);
}
else
{
SoundSystem.Play(Filter.Pvs(owner), comp.MissSound, args.User.Transform.Coordinates);
}
foreach (var entity in hitEntities)
{
if (entity.TryGetComponent<IDamageableComponent>(out var damageComponent))
{
damageComponent.ChangeDamage(comp.DamageType, comp.Damage, false, owner);
}
}
}
comp.LastAttackTime = curTime;
comp.CooldownEnd = comp.LastAttackTime + TimeSpan.FromSeconds(comp.ArcCooldownTime);
RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false);
}
/// <summary>
/// Used for melee weapons that want some behavior on AfterInteract,
/// but also want the cooldown (stun batons, flashes)
/// </summary>
private void OnAfterInteract(EntityUid uid, MeleeWeaponComponent comp, AfterInteractEvent args)
{
if (!args.CanReach)
return;
var curTime = _gameTiming.CurTime;
if (curTime < comp.CooldownEnd)
{
return;
}
var owner = EntityManager.GetEntity(uid);
if (args.Target == null)
return;
var location = args.User.Transform.Coordinates;
var diff = args.ClickLocation.ToMapPos(owner.EntityManager) - location.ToMapPos(owner.EntityManager);
var angle = Angle.FromWorldVec(diff);
var hitEvent = new MeleeInteractEvent(args.Target, args.User);
RaiseLocalEvent(uid, hitEvent, false);
if (!hitEvent.CanInteract) return;
SendAnimation(comp.ClickArc, angle, args.User, owner, new List<IEntity>() { args.Target }, comp.ClickAttackEffect, false);
comp.LastAttackTime = curTime;
comp.CooldownEnd = comp.LastAttackTime + TimeSpan.FromSeconds(comp.CooldownTime);
RaiseLocalEvent(uid, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false);
}
private HashSet<IEntity> ArcRayCast(Vector2 position, Angle angle, float arcWidth, float range, MapId mapId, IEntity ignore)
{
var widthRad = Angle.FromDegrees(arcWidth);
var increments = 1 + 35 * (int) Math.Ceiling(widthRad / (2 * Math.PI));
var increment = widthRad / increments;
var baseAngle = angle - widthRad / 2;
var resSet = new HashSet<IEntity>();
for (var i = 0; i < increments; i++)
{
var castAngle = new Angle(baseAngle + increment * i);
var res = EntitySystem.Get<SharedBroadPhaseSystem>().IntersectRay(mapId,
new CollisionRay(position, castAngle.ToWorldVec(),
(int) (CollisionGroup.Impassable | CollisionGroup.MobImpassable)), range, ignore).ToList();
if (res.Count != 0)
{
resSet.Add(res[0].HitEntity);
}
}
return resSet;
}
private void OnChemicalInjectorHit(EntityUid uid, MeleeChemicalInjectorComponent comp, MeleeHitEvent args)
{
if (!ComponentManager.TryGetComponent<SolutionContainerComponent>(uid, out var solutionContainer))
return;
var hitBloodstreams = new List<BloodstreamComponent>();
foreach (var entity in args.HitEntities)
{
if (entity.Deleted)
continue;
if (entity.TryGetComponent<BloodstreamComponent>(out var bloodstream))
hitBloodstreams.Add(bloodstream);
}
if (hitBloodstreams.Count < 1)
return;
var removedSolution = solutionContainer.Solution.SplitSolution(comp.TransferAmount * hitBloodstreams.Count);
var removedVol = removedSolution.TotalVolume;
var solutionToInject = removedSolution.SplitSolution(removedVol * comp.TransferEfficiency);
var volPerBloodstream = solutionToInject.TotalVolume * (1 / hitBloodstreams.Count);
foreach (var bloodstream in hitBloodstreams)
{
var individualInjection = solutionToInject.SplitSolution(volPerBloodstream);
bloodstream.TryTransferSolution(individualInjection);
}
}
public void SendAnimation(string arc, Angle angle, IEntity attacker, IEntity source, IEnumerable<IEntity> hits, bool textureEffect = false, bool arcFollowAttacker = true)
{
RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, source.Uid,
hits.Select(e => e.Uid).ToList(), textureEffect, arcFollowAttacker), Filter.Pvs(source, 1f));
}
public void SendLunge(Angle angle, IEntity source)
{
RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayLungeAnimationMessage(angle, source.Uid), Filter.Pvs(source, 1f));
}
}
/// <summary>
/// Raised directed on the melee weapon entity used to attack something in combat mode,
/// whether through a click attack or wide attack.
/// </summary>
public class MeleeHitEvent : HandledEntityEventArgs
{
/// <summary>
/// A list containing every hit entity. Can be zero.
/// </summary>
public IEnumerable<IEntity> HitEntities { get; }
/// <summary>
/// The user who attacked with the melee wepaon.
/// </summary>
public IEntity User { get; }
public MeleeHitEvent(List<IEntity> hitEntities, IEntity user)
{
HitEntities = hitEntities;
User = user;
}
}
/// <summary>
/// Raised directed on the melee weapon entity used to attack something in combat mode,
/// whether through a click attack or wide attack.
/// </summary>
public class MeleeInteractEvent : EntityEventArgs
{
/// <summary>
/// The entity interacted with.
/// </summary>
public IEntity Entity { get; }
/// <summary>
/// The user who interacted using the melee weapon.
/// </summary>
public IEntity User { get; }
/// <summary>
/// Modified by the event handler to specify whether they could successfully interact with the entity.
/// Used to know whether to send the hit animation or not.
/// </summary>
public bool CanInteract { get; set; } = false;
public MeleeInteractEvent(IEntity entity, IEntity user)
{
Entity = entity;
User = user;
}
}
}

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
[RegisterComponent]
public sealed class AmmoBoxComponent : Component, IInteractUsing, IUse, IInteractHand, IMapInit, IExamine
{
public override string Name => "AmmoBox";
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
[DataField("capacity")]
public int Capacity
{
get => _capacity;
set
{
_capacity = value;
_spawnedAmmo = new Stack<IEntity>(value);
}
}
private int _capacity = 30;
public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount;
private Stack<IEntity> _spawnedAmmo = new();
private Container _ammoContainer = default!;
private int _unspawnedCount;
[DataField("fillPrototype")]
private string? _fillPrototype;
public override void Initialize()
{
base.Initialize();
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-container", 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 var ammo))
{
_ammoContainer.Remove(ammo);
return ammo;
}
if (_unspawnedCount > 0)
{
ammo = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.Coordinates);
_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;
}
async Task<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 (ammo == null)
{
continue;
}
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();
if (ammo == null)
{
return false;
}
if (ammo.TryGetComponent(out ItemComponent? item))
{
if (!handsComponent.CanPutInHand(item))
{
TryInsertAmmo(user, ammo);
return false;
}
handsComponent.PutInHand(item);
}
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)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Dump 10");
data.Visibility = component.AmmoLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled;
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
protected override void Activate(IEntity user, AmmoBoxComponent component)
{
component.EjectContents(10);
}
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
message.AddMarkup(Loc.GetString("\nIt's a [color=white]{0}[/color] ammo box.", _caliber));
message.AddMarkup(Loc.GetString("\nIt has [color=white]{0}[/color] out of [color=white]{1}[/color] ammo left.", AmmoLeft, _capacity));
}
}
}

View File

@@ -0,0 +1,181 @@
using System;
using Content.Shared.Examine;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
/// <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, IExamine, ISerializationHooks
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
public override string Name => "Ammo";
[DataField("caliber")]
public BallisticCaliber Caliber { get; } = BallisticCaliber.Unspecified;
public bool Spent
{
get
{
if (_ammoIsProjectile)
{
return false;
}
return _spent;
}
}
private bool _spent;
/// <summary>
/// Used for anything without a case that fires itself
/// </summary>
[DataField("isProjectile")]
private bool _ammoIsProjectile;
/// <summary>
/// Used for something that is deleted when the projectile is retrieved
/// </summary>
[DataField("caseless")]
public bool Caseless { get; }
// 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>
[DataField("projectilesFired")]
public int ProjectilesFired { get; } = 1;
[DataField("projectile")]
private string? _projectileId;
// How far apart each entity is if multiple are shot
[DataField("ammoSpread")]
public float EvenSpreadAngle { get; } = default;
/// <summary>
/// How fast the shot entities travel
/// </summary>
[DataField("ammoVelocity")]
public float Velocity { get; } = 20f;
[DataField("muzzleFlash")]
private string _muzzleFlashSprite = "Objects/Weapons/Guns/Projectiles/bullet_muzzle.png";
[DataField("soundCollectionEject")]
public string? SoundCollectionEject { get; } = "CasingEject";
void ISerializationHooks.AfterDeserialization()
{
// Being both caseless and shooting yourself doesn't make sense
DebugTools.Assert(!(_ammoIsProjectile == true && Caseless == true));
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(EntityCoordinates spawnAt)
{
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, spawnAt);
return entity;
}
public void MuzzleFlash(IEntity entity, Angle angle)
{
if (_muzzleFlashSprite == null)
{
return;
}
var time = _gameTiming.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,
AttachedEntityUid = entity.Uid,
AttachedOffset = 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 void Examine(FormattedMessage message, bool inDetailsRange)
{
var text = Loc.GetString("It's [color=white]{0}[/color] ammo.", Caliber);
message.AddMarkup(text);
}
}
public enum BallisticCaliber
{
Unspecified = 0,
A357, // Placeholder?
ClRifle,
SRifle,
Pistol,
A35, // Placeholder?
LRifle,
Magnum,
AntiMaterial,
Shotgun,
Cap,
Rocket,
Dart, // Placeholder
Grenade,
Energy,
CreamPie, // I can't wait for this enum to be a prototype type...
}
}

View File

@@ -0,0 +1,8 @@
using Robust.Shared.Serialization;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
public partial class AmmoComponentData : ISerializationHooks
{
}
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.Chemistry.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
[RegisterComponent]
public class ChemicalAmmoComponent : Component
{
public override string Name => "ChemicalAmmo";
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case BarrelFiredMessage barrelFired:
TransferSolution(barrelFired);
break;
}
}
private void TransferSolution(BarrelFiredMessage barrelFired)
{
if (!Owner.TryGetComponent<SolutionContainerComponent>(out var ammoSolutionContainer))
return;
var projectiles = barrelFired.FiredProjectiles;
var projectileSolutionContainers = new List<SolutionContainerComponent>();
foreach (var projectile in projectiles)
{
if (projectile.TryGetComponent<SolutionContainerComponent>(out var projectileSolutionContainer))
{
projectileSolutionContainers.Add(projectileSolutionContainer);
}
}
if (!projectileSolutionContainers.Any())
return;
var solutionPerProjectile = ammoSolutionContainer.CurrentVolume * (1 / projectileSolutionContainers.Count);
foreach (var projectileSolutionContainer in projectileSolutionContainers)
{
var solutionToTransfer = ammoSolutionContainer.SplitSolution(solutionPerProjectile);
projectileSolutionContainer.TryAddSolution(solutionToTransfer);
}
ammoSolutionContainer.RemoveAllSolution();
}
}
}

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
[RegisterComponent]
public class RangedMagazineComponent : Component, IMapInit, IInteractUsing, IUse, IExamine
{
public override string Name => "RangedMagazine";
private readonly Stack<IEntity> _spawnedAmmo = new();
private Container _ammoContainer = default!;
public int ShotsLeft => _spawnedAmmo.Count + _unspawnedCount;
public int Capacity => _capacity;
[DataField("capacity")]
private int _capacity = 20;
public MagazineType MagazineType => _magazineType;
[DataField("magazineType")]
private MagazineType _magazineType = MagazineType.Unspecified;
public BallisticCaliber Caliber => _caliber;
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
private AppearanceComponent? _appearanceComponent;
// If there's anything already in the magazine
[DataField("fillPrototype")]
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;
void IMapInit.MapInit()
{
if (_fillPrototype != null)
{
_unspawnedCount += Capacity;
}
UpdateAppearance();
}
public override void Initialize()
{
base.Initialize();
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-magazine", 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.Coordinates);
}
UpdateAppearance();
return ammo;
}
async Task<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.Coordinates = eventArgs.User.Transform.Coordinates;
ServerRangedBarrelComponent.EjectCasing(ammo);
}
else
{
handsComponent.PutInHand(itemComponent);
}
return true;
}
public void Examine(FormattedMessage message, bool inDetailsRange)
{
var text = Loc.GetString("It's a [color=white]{0}[/color] magazine of [color=white]{1}[/color] caliber.", MagazineType, Caliber);
message.AddMarkup(text);
}
}
}

View File

@@ -0,0 +1,211 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
/// <summary>
/// Used to load certain ranged weapons quickly
/// </summary>
[RegisterComponent]
public class SpeedLoaderComponent : Component, IAfterInteract, IInteractUsing, IMapInit, IUse
{
public override string Name => "SpeedLoader";
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
public int Capacity => _capacity;
[DataField("capacity")]
private int _capacity = 6;
private Container _ammoContainer = default!;
private Stack<IEntity> _spawnedAmmo = new();
private int _unspawnedCount;
public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount;
[DataField("fillPrototype")]
private string? _fillPrototype = default;
public override void Initialize()
{
base.Initialize();
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-container", 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.Coordinates);
_unspawnedCount--;
}
return entity;
}
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
if (eventArgs.Target == null)
{
return false;
}
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
var 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();
}
return true;
}
async Task<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,393 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.NetIDs;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
/// <summary>
/// Shotguns mostly
/// </summary>
[RegisterComponent]
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IMapInit, IExamine
{
// 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 uint? NetID => ContentNetIDs.BOLTACTION_BARREL;
public override int ShotsLeft
{
get
{
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
}
}
public override int Capacity => _capacity;
[DataField("capacity")]
private int _capacity = 6;
private ContainerSlot _chamberContainer = default!;
private Stack<IEntity> _spawnedAmmo = default!;
private Container _ammoContainer = default!;
[ViewVariables]
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
[ViewVariables]
[DataField("fillPrototype")]
private string? _fillPrototype;
[ViewVariables]
private int _unspawnedCount;
public bool BoltOpen
{
get => _boltOpen;
set
{
if (_boltOpen == value)
{
return;
}
if (value)
{
TryEjectChamber();
if (_soundBoltOpen != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
}
else
{
TryFeedChamber();
if (_soundBoltClosed != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
}
_boltOpen = value;
UpdateAppearance();
Dirty();
}
}
private bool _boltOpen;
[DataField("autoCycle")]
private bool _autoCycle;
private AppearanceComponent? _appearanceComponent;
// Sounds
[DataField("soundCycle")]
private string _soundCycle = "/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg";
[DataField("soundBoltOpen")]
private string _soundBoltOpen = "/Audio/Weapons/Guns/Bolt/rifle_bolt_open.ogg";
[DataField("soundBoltClosed")]
private string _soundBoltClosed = "/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg";
[DataField("soundInsert")]
private string _soundInsert = "/Audio/Weapons/Guns/MagIn/bullet_insert.ogg";
void IMapInit.MapInit()
{
if (_fillPrototype != null)
{
_unspawnedCount += Capacity;
if (_unspawnedCount > 0)
{
_unspawnedCount--;
var chamberEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.Coordinates);
_chamberContainer.Insert(chamberEntity);
}
}
UpdateAppearance();
}
public override ComponentState GetComponentState(ICommonSession player)
{
(int, int)? count = (ShotsLeft, Capacity);
var chamberedExists = _chamberContainer.ContainedEntity != null;
// (Is one chambered?, is the bullet spend)
var chamber = (chamberedExists, false);
if (chamberedExists && _chamberContainer.ContainedEntity!.TryGetComponent<AmmoComponent>(out var ammo))
{
chamber.Item2 = ammo.Spent;
}
return new BoltActionBarrelComponentState(
chamber,
FireRateSelector,
count,
SoundGunshot);
}
public override void Initialize()
{
// TODO: Add existing ammo support on revolvers
base.Initialize();
_spawnedAmmo = new Stack<IEntity>(_capacity - 1);
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-ammo-container", out var existing);
if (existing)
{
foreach (var entity in _ammoContainer.ContainedEntities)
{
_spawnedAmmo.Push(entity);
_unspawnedCount--;
}
}
_chamberContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-chamber-container");
if (Owner.TryGetComponent(out AppearanceComponent? appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
Dirty();
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(EntityCoordinates spawnAt)
{
var chamberEntity = _chamberContainer.ContainedEntity;
if (_autoCycle)
{
Cycle();
}
else
{
Dirty();
}
return chamberEntity?.GetComponentOrNull<AmmoComponent>()?.TakeBullet(spawnAt);
}
protected override bool WeaponCanFire()
{
if (!base.WeaponCanFire())
{
return false;
}
return !BoltOpen && _chamberContainer.ContainedEntity != null;
}
private void Cycle(bool manual = false)
{
TryEjectChamber();
TryFeedChamber();
if (_chamberContainer.ContainedEntity == null && manual)
{
BoltOpen = true;
if (Owner.TryGetContainer(out var container))
{
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt opened"));
}
return;
}
else
{
if (!string.IsNullOrEmpty(_soundCycle))
{
SoundSystem.Play(Filter.Pvs(Owner), _soundCycle, Owner.Transform.Coordinates, 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)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
Dirty();
UpdateAppearance();
return true;
}
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
{
_ammoContainer.Insert(ammo);
_spawnedAmmo.Push(ammo);
if (_soundInsert != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert, Owner.Transform.Coordinates, 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"));
return true;
}
Cycle(true);
return true;
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs.User, eventArgs.Using);
}
private bool TryEjectChamber()
{
var chamberedEntity = _chamberContainer.ContainedEntity;
if (chamberedEntity != null)
{
if (!_chamberContainer.Remove(chamberedEntity))
{
return false;
}
if (!chamberedEntity.GetComponent<AmmoComponent>().Caseless)
{
EjectCasing(chamberedEntity);
}
return true;
}
return false;
}
private bool TryFeedChamber()
{
if (_chamberContainer.ContainedEntity != null)
{
return false;
}
if (_spawnedAmmo.TryPop(out var next))
{
_ammoContainer.Remove(next);
_chamberContainer.Insert(next);
return true;
}
else if (_unspawnedCount > 0)
{
_unspawnedCount--;
var ammoEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.Coordinates);
_chamberContainer.Insert(ammoEntity);
return true;
}
return false;
}
public override void Examine(FormattedMessage message, bool inDetailsRange)
{
base.Examine(message, inDetailsRange);
message.AddMarkup(Loc.GetString("\nIt uses [color=white]{0}[/color] ammo.", _caliber));
}
[Verb]
private sealed class OpenBoltVerb : Verb<BoltActionBarrelComponent>
{
protected override void GetData(IEntity user, BoltActionBarrelComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Open bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Invisible : 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)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Close bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Invisible;
}
protected override void Activate(IEntity user, BoltActionBarrelComponent component)
{
component.BoltOpen = false;
}
}
}
}

View File

@@ -0,0 +1,250 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Interaction;
using Content.Shared.NetIDs;
using Content.Shared.Notification;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
/// <summary>
/// Bolt-action rifles
/// </summary>
[RegisterComponent]
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IMapInit, ISerializationHooks
{
public override string Name => "PumpBarrel";
public override uint? NetID => ContentNetIDs.PUMP_BARREL;
public override int ShotsLeft
{
get
{
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
}
}
private const int DefaultCapacity = 6;
[DataField("capacity")]
public override int Capacity { get; } = DefaultCapacity;
// Even a point having a chamber? I guess it makes some of the below code cleaner
private ContainerSlot _chamberContainer = default!;
private Stack<IEntity> _spawnedAmmo = new (DefaultCapacity-1);
private Container _ammoContainer = default!;
[ViewVariables]
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
[ViewVariables]
[DataField("fillPrototype")]
private string? _fillPrototype;
[ViewVariables]
private int _unspawnedCount;
[DataField("manualCycle")]
private bool _manualCycle = true;
private AppearanceComponent? _appearanceComponent;
// Sounds
[DataField("soundCycle")]
private string _soundCycle = "/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg";
[DataField("soundInsert")]
private string _soundInsert = "/Audio/Weapons/Guns/MagIn/bullet_insert.ogg";
void IMapInit.MapInit()
{
if (_fillPrototype != null)
{
_unspawnedCount += Capacity - 1;
}
UpdateAppearance();
}
public override ComponentState GetComponentState(ICommonSession player)
{
(int, int)? count = (ShotsLeft, Capacity);
var chamberedExists = _chamberContainer.ContainedEntity != null;
// (Is one chambered?, is the bullet spend)
var chamber = (chamberedExists, false);
if (chamberedExists && _chamberContainer.ContainedEntity!.TryGetComponent<AmmoComponent>(out var ammo))
{
chamber.Item2 = ammo.Spent;
}
return new PumpBarrelComponentState(
chamber,
FireRateSelector,
count,
SoundGunshot);
}
void ISerializationHooks.AfterDeserialization()
{
_spawnedAmmo = new Stack<IEntity>(Capacity - 1);
}
public override void Initialize()
{
base.Initialize();
_ammoContainer =
ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-ammo-container", out var existing);
if (existing)
{
foreach (var entity in _ammoContainer.ContainedEntities)
{
_spawnedAmmo.Push(entity);
_unspawnedCount--;
}
}
_chamberContainer =
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-chamber-container", out existing);
if (existing)
{
_unspawnedCount--;
}
if (Owner.TryGetComponent(out AppearanceComponent? appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
Dirty();
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(EntityCoordinates spawnAt)
{
var chamberEntity = _chamberContainer.ContainedEntity;
if (!_manualCycle)
{
Cycle();
}
else
{
Dirty();
}
return chamberEntity?.GetComponentOrNull<AmmoComponent>()?.TakeBullet(spawnAt);
}
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.Coordinates);
_chamberContainer.Insert(ammoEntity);
}
if (manual)
{
if (!string.IsNullOrEmpty(_soundCycle))
{
SoundSystem.Play(Filter.Pvs(Owner), _soundCycle, Owner.Transform.Coordinates, 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)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert, Owner.Transform.Coordinates, 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 async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
return TryInsertBullet(eventArgs);
}
public override void Examine(FormattedMessage message, bool inDetailsRange)
{
base.Examine(message, inDetailsRange);
message.AddMarkup(Loc.GetString("\nIt uses [color=white]{0}[/color] ammo.", _caliber));
}
}
}

View File

@@ -0,0 +1,309 @@
using System;
using System.Threading.Tasks;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.NetIDs;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
[RegisterComponent]
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
{
[Dependency] private readonly IRobustRandom _random = default!;
public override string Name => "RevolverBarrel";
public override uint? NetID => ContentNetIDs.REVOLVER_BARREL;
[ViewVariables]
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
private Container _ammoContainer = default!;
[ViewVariables]
private int _currentSlot;
public override int Capacity => _ammoSlots.Length;
[DataField("capacity")]
private int _serializedCapacity = 6;
[DataField("ammoSlots", readOnly: true)]
private IEntity?[] _ammoSlots = Array.Empty<IEntity?>();
public override int ShotsLeft => _ammoContainer.ContainedEntities.Count;
[ViewVariables]
[DataField("fillPrototype")]
private string? _fillPrototype;
[ViewVariables]
private int _unspawnedCount;
// Sounds
[DataField("soundEject")]
private string _soundEject = "/Audio/Weapons/Guns/MagOut/revolver_magout.ogg";
[DataField("soundInsert")]
private string _soundInsert = "/Audio/Weapons/Guns/MagIn/revolver_magin.ogg";
[DataField("soundSpin")]
private string _soundSpin = "/Audio/Weapons/Guns/Misc/revolver_spin.ogg";
void ISerializationHooks.BeforeSerialization()
{
_serializedCapacity = _ammoSlots.Length;
}
void ISerializationHooks.AfterDeserialization()
{
_ammoSlots = new IEntity[_serializedCapacity];
}
public override ComponentState GetComponentState(ICommonSession player)
{
var slotsSpent = new bool?[Capacity];
for (var i = 0; i < Capacity; i++)
{
slotsSpent[i] = null;
var ammoEntity = _ammoSlots[i];
if (ammoEntity != null && ammoEntity.TryGetComponent(out AmmoComponent? ammo))
{
slotsSpent[i] = ammo.Spent;
}
}
//TODO: make yaml var to not sent currentSlot/UI? (for russian roulette)
return new RevolverBarrelComponentState(
_currentSlot,
FireRateSelector,
slotsSpent,
SoundGunshot);
}
public override void Initialize()
{
base.Initialize();
_unspawnedCount = Capacity;
int idx = 0;
_ammoContainer = ContainerHelpers.EnsureContainer<Container>(Owner, $"{Name}-ammoContainer", out var existing);
if (existing)
{
foreach (var entity in _ammoContainer.ContainedEntities)
{
_unspawnedCount--;
_ammoSlots[idx] = entity;
idx++;
}
}
for (var i = 0; i < _unspawnedCount; i++)
{
var entity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.Coordinates);
_ammoSlots[idx] = entity;
_ammoContainer.Insert(entity);
idx++;
}
UpdateAppearance();
Dirty();
}
private void UpdateAppearance()
{
if (!Owner.TryGetComponent(out AppearanceComponent? appearance))
{
return;
}
// Placeholder, at this stage it's just here for the RPG
appearance.SetData(MagazineBarrelVisuals.MagLoaded, ShotsLeft > 0);
appearance.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
appearance.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)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundInsert, Owner.Transform.Coordinates, 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 = _random.Next(_ammoSlots.Length - 1);
_currentSlot = random;
if (!string.IsNullOrEmpty(_soundSpin))
{
SoundSystem.Play(Filter.Pvs(Owner), _soundSpin, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
Dirty();
}
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(EntityCoordinates spawnAt)
{
var ammo = _ammoSlots[_currentSlot];
IEntity? bullet = null;
if (ammo != null)
{
var ammoComponent = ammo.GetComponent<AmmoComponent>();
bullet = ammoComponent.TakeBullet(spawnAt);
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)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundEject, Owner.Transform.Coordinates, 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 async Task<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)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Spin");
if (component.Capacity <= 1)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Visibility = component.ShotsLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled;
data.IconTexture = "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png";
}
protected override void Activate(IEntity user, RevolverBarrelComponent component)
{
component.Spin();
component.Owner.PopupMessage(user, Loc.GetString("Spun the cylinder"));
}
}
}
}

View File

@@ -0,0 +1,322 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Battery.Components;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Projectiles.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Shared.NetIDs;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
[RegisterComponent]
public sealed class ServerBatteryBarrelComponent : ServerRangedBarrelComponent
{
public override string Name => "BatteryBarrel";
public override uint? NetID => ContentNetIDs.BATTERY_BARREL;
// The minimum change we need before we can fire
[DataField("lowerChargeLimit")]
[ViewVariables] private float _lowerChargeLimit = 10;
[DataField("fireCost")]
[ViewVariables] private int _baseFireCost = 300;
// What gets fired
[DataField("ammoPrototype")]
[ViewVariables] private string? _ammoPrototype;
[ViewVariables] public IEntity? PowerCellEntity => _powerCellContainer.ContainedEntity;
public BatteryComponent? PowerCell => _powerCellContainer.ContainedEntity?.GetComponentOrNull<BatteryComponent>();
private ContainerSlot _powerCellContainer = default!;
private ContainerSlot _ammoContainer = default!;
[DataField("powerCellPrototype")]
private string? _powerCellPrototype = default;
[DataField("powerCellRemovable")]
[ViewVariables] private bool _powerCellRemovable = default;
public override int ShotsLeft
{
get
{
var powerCell = _powerCellContainer.ContainedEntity;
if (powerCell == null)
{
return 0;
}
return (int) Math.Ceiling(powerCell.GetComponent<BatteryComponent>().CurrentCharge / _baseFireCost);
}
}
public override int Capacity
{
get
{
var powerCell = _powerCellContainer.ContainedEntity;
if (powerCell == null)
{
return 0;
}
return (int) Math.Ceiling((float) (powerCell.GetComponent<BatteryComponent>().MaxCharge / _baseFireCost));
}
}
private AppearanceComponent? _appearanceComponent;
// Sounds
[DataField("soundPowerCellInsert")]
private string? _soundPowerCellInsert = default;
[DataField("soundPowerCellEject")]
private string? _soundPowerCellEject = default;
public override ComponentState GetComponentState(ICommonSession player)
{
(int, int)? count = (ShotsLeft, Capacity);
return new BatteryBarrelComponentState(
FireRateSelector,
count);
}
public override void Initialize()
{
base.Initialize();
_powerCellContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-powercell-container", out var existing);
if (!existing && _powerCellPrototype != null)
{
var powerCellEntity = Owner.EntityManager.SpawnEntity(_powerCellPrototype, Owner.Transform.Coordinates);
_powerCellContainer.Insert(powerCellEntity);
}
if (_ammoPrototype != null)
{
_ammoContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-ammo-container");
}
if (Owner.TryGetComponent(out AppearanceComponent? appearanceComponent))
{
_appearanceComponent = appearanceComponent;
}
Dirty();
}
protected override void Startup()
{
UpdateAppearance();
}
public void UpdateAppearance()
{
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _powerCellContainer.ContainedEntity != null);
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
Dirty();
}
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.Coordinates);
_ammoContainer.Insert(ammo);
}
return ammo;
}
public override IEntity? TakeProjectile(EntityCoordinates spawnAt)
{
var powerCellEntity = _powerCellContainer.ContainedEntity;
if (powerCellEntity == null)
{
return null;
}
var capacitor = powerCellEntity.GetComponent<BatteryComponent>();
if (capacitor.CurrentCharge < _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.CurrentCharge, _baseFireCost);
if (capacitor.UseCharge(chargeChange) < _lowerChargeLimit)
{
// Handling of funny exploding cells.
return null;
}
var energyRatio = chargeChange / _baseFireCost;
if (_ammoContainer.ContainedEntity != null)
{
entity = _ammoContainer.ContainedEntity;
_ammoContainer.Remove(entity);
entity.Transform.Coordinates = spawnAt;
}
else
{
entity = Owner.EntityManager.SpawnEntity(_ammoPrototype, spawnAt);
}
if (entity.TryGetComponent(out ProjectileComponent? projectileComponent))
{
if (energyRatio < 1.0)
{
var newDamages = new Dictionary<DamageType, int>(projectileComponent.Damages.Count);
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?");
}
Dirty();
UpdateAppearance();
return entity;
}
public bool TryInsertPowerCell(IEntity entity)
{
if (_powerCellContainer.ContainedEntity != null)
{
return false;
}
if (!entity.HasComponent<BatteryComponent>())
{
return false;
}
if (_soundPowerCellInsert != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundPowerCellInsert, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
_powerCellContainer.Insert(entity);
Dirty();
UpdateAppearance();
return true;
}
public override bool UseEntity(UseEntityEventArgs eventArgs)
{
if (!_powerCellRemovable)
{
return false;
}
if (PowerCellEntity == null)
{
return false;
}
return TryEjectCell(eventArgs.User);
}
private bool TryEjectCell(IEntity user)
{
if (PowerCell == null || !_powerCellRemovable)
{
return false;
}
if (!user.TryGetComponent(out HandsComponent? hands))
{
return false;
}
var cell = PowerCell;
if (!_powerCellContainer.Remove(cell.Owner))
{
return false;
}
Dirty();
UpdateAppearance();
if (!hands.PutInHand(cell.Owner.GetComponent<ItemComponent>()))
{
cell.Owner.Transform.Coordinates = user.Transform.Coordinates;
}
if (_soundPowerCellEject != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundPowerCellEject, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
return true;
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.Using.HasComponent<BatteryComponent>())
{
return false;
}
return TryInsertPowerCell(eventArgs.Using);
}
[Verb]
public sealed class EjectCellVerb : Verb<ServerBatteryBarrelComponent>
{
protected override void GetData(IEntity user, ServerBatteryBarrelComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user) || !component._powerCellRemovable)
{
data.Visibility = VerbVisibility.Invisible;
return;
}
if (component.PowerCell == null)
{
data.Text = Loc.GetString("No cell");
data.Visibility = VerbVisibility.Disabled;
}
else
{
data.Text = Loc.GetString("Eject cell");
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
}
}
protected override void Activate(IEntity user, ServerBatteryBarrelComponent component)
{
component.TryEjectCell(user);
}
}
}
}

View File

@@ -0,0 +1,539 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.NetIDs;
using Content.Shared.Notification;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
[RegisterComponent]
public sealed class ServerMagazineBarrelComponent : ServerRangedBarrelComponent, IExamine
{
public override string Name => "MagazineBarrel";
public override uint? NetID => ContentNetIDs.MAGAZINE_BARREL;
[ViewVariables]
private ContainerSlot _chamberContainer = default!;
[ViewVariables] public bool HasMagazine => _magazineContainer.ContainedEntity != null;
private ContainerSlot _magazineContainer = default!;
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
[DataField("magazineTypes")]
private MagazineType _magazineTypes = default;
[ViewVariables] public BallisticCaliber Caliber => _caliber;
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
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;
}
}
[DataField("magFillPrototype")]
private string? _magFillPrototype;
public bool BoltOpen
{
get => _boltOpen;
set
{
if (_boltOpen == value)
{
return;
}
if (value)
{
TryEjectChamber();
if (_soundBoltOpen != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
}
else
{
TryFeedChamber();
if (_soundBoltClosed != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
}
_boltOpen = value;
UpdateAppearance();
Dirty();
}
}
private bool _boltOpen = true;
[DataField("autoEjectMag")]
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;
[DataField("magNeedsOpenBolt")]
private bool _magNeedsOpenBolt = default;
private AppearanceComponent? _appearanceComponent;
// Sounds
[DataField("soundBoltOpen")]
private string? _soundBoltOpen = default;
[DataField("soundBoltClosed")]
private string? _soundBoltClosed = default;
[DataField("soundRack")]
private string? _soundRack = default;
[DataField("soundMagInsert")]
private string? _soundMagInsert = default;
[DataField("soundMagEject")]
private string? _soundMagEject = default;
[DataField("soundAutoEject")]
private string _soundAutoEject = "/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg";
private List<MagazineType> GetMagazineTypes()
{
var types = new List<MagazineType>();
foreach (MagazineType mag in Enum.GetValues(typeof(MagazineType)))
{
if ((_magazineTypes & mag) != 0)
{
types.Add(mag);
}
}
return types;
}
public override ComponentState GetComponentState(ICommonSession player)
{
(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 = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-chamber");
_magazineContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-magazine", out var existing);
if (!existing && _magFillPrototype != null)
{
var magEntity = Owner.EntityManager.SpawnEntity(_magFillPrototype, Owner.Transform.Coordinates);
_magazineContainer.Insert(magEntity);
}
Dirty();
}
protected override void Startup()
{
UpdateAppearance();
}
public override IEntity? PeekAmmo()
{
return BoltOpen ? null : _chamberContainer.ContainedEntity;
}
public override IEntity? TakeProjectile(EntityCoordinates spawnAt)
{
if (BoltOpen)
{
return null;
}
var entity = _chamberContainer.ContainedEntity;
Cycle();
return entity?.GetComponent<AmmoComponent>().TakeBullet(spawnAt);
}
private void Cycle(bool manual = false)
{
if (BoltOpen)
{
return;
}
TryEjectChamber();
TryFeedChamber();
if (_chamberContainer.ContainedEntity == null && !BoltOpen)
{
if (_soundBoltOpen != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-5));
}
if (Owner.TryGetContainer(out var container))
{
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt open"));
}
BoltOpen = true;
return;
}
if (manual)
{
if (_soundRack != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundRack, Owner.Transform.Coordinates, 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)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-5));
}
Owner.PopupMessage(eventArgs.User, Loc.GetString("Bolt closed"));
BoltOpen = false;
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 bool TryEjectChamber()
{
var chamberEntity = _chamberContainer.ContainedEntity;
if (chamberEntity != null)
{
if (!_chamberContainer.Remove(chamberEntity))
{
return false;
}
var ammoComponent = chamberEntity.GetComponent<AmmoComponent>();
if (!ammoComponent.Caseless)
{
EjectCasing(chamberEntity);
}
return true;
}
return false;
}
public bool TryFeedChamber()
{
if (_chamberContainer.ContainedEntity != null)
{
return false;
}
// 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)
{
return false;
}
_chamberContainer.Insert(nextRound);
if (_autoEjectMag && magazine != null && magazine.GetComponent<RangedMagazineComponent>().ShotsLeft == 0)
{
if (_soundAutoEject != null)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundAutoEject, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
_magazineContainer.Remove(magazine);
SendNetworkMessage(new MagazineAutoEjectMessage());
}
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)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundMagEject, Owner.Transform.Coordinates, AudioParams.Default.WithVolume(-2));
}
if (user.TryGetComponent(out HandsComponent? handsComponent))
{
handsComponent.PutInHandOrDrop(mag.GetComponent<ItemComponent>());
}
Dirty();
UpdateAppearance();
}
public override async Task<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)
{
SoundSystem.Play(Filter.Pvs(Owner), _soundMagInsert, Owner.Transform.Coordinates, 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;
}
public override void Examine(FormattedMessage message, bool inDetailsRange)
{
base.Examine(message, inDetailsRange);
message.AddMarkup(Loc.GetString("\nIt uses [color=white]{0}[/color] ammo.", Caliber));
foreach (var magazineType in GetMagazineTypes())
{
message.AddMarkup(Loc.GetString("\nIt accepts [color=white]{0}[/color] magazines.", magazineType));
}
}
[Verb]
private sealed class EjectMagazineVerb : Verb<ServerMagazineBarrelComponent>
{
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Eject magazine");
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
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)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Open bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Invisible : VerbVisibility.Visible;
}
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
{
component.BoltOpen = true;
}
}
[Verb]
private sealed class CloseBoltVerb : Verb<ServerMagazineBarrelComponent>
{
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
{
if (!ActionBlockerSystem.CanInteract(user))
{
data.Visibility = VerbVisibility.Invisible;
return;
}
data.Text = Loc.GetString("Close bolt");
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Invisible;
}
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
{
component.BoltOpen = false;
}
}
}
[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
CalicoTopMounted = 1 << 10,
}
}

View File

@@ -0,0 +1,451 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Camera;
using Content.Server.Projectiles.Components;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Audio;
using Content.Shared.Damage.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
/// <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, IExamine, ISerializationHooks
{
// 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
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
public override FireRateSelector FireRateSelector => _fireRateSelector;
[DataField("currentSelector")]
private FireRateSelector _fireRateSelector = FireRateSelector.Safety;
public override FireRateSelector AllRateSelectors => _fireRateSelector;
[DataField("allSelectors")]
private FireRateSelector _allRateSelectors;
[DataField("fireRate")]
public override float FireRate { get; } = 2f;
// _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(EntityCoordinates spawnAt);
// Recoil / spray control
[DataField("minAngle")]
private float _minAngleDegrees;
public Angle MinAngle { get; private set; }
[DataField("maxAngle")]
private float _maxAngleDegrees = 45;
public Angle MaxAngle { get; private set; }
private Angle _currentAngle = Angle.Zero;
[DataField("angleDecay")]
private float _angleDecayDegrees = 20;
/// <summary>
/// How slowly the angle's theta decays per second in radians
/// </summary>
public float AngleDecay { get; private set; }
[DataField("angleIncrease")]
private float? _angleIncreaseDegrees;
/// <summary>
/// How quickly the angle's theta builds for every shot fired in radians
/// </summary>
public float AngleIncrease { get; private set; }
// Multiplies the ammo spread to get the final spread of each pellet
[DataField("ammoSpreadRatio")]
public float SpreadRatio { get; private set; }
[DataField("canMuzzleFlash")]
public bool CanMuzzleFlash { get; } = true;
// Sounds
[DataField("soundGunshot")]
public string? SoundGunshot { get; set; }
[DataField("soundEmpty")]
public string SoundEmpty { get; } = "/Audio/Weapons/Guns/Empty/empty.ogg";
void ISerializationHooks.BeforeSerialization()
{
_minAngleDegrees = (float) (MinAngle.Degrees * 2);
_maxAngleDegrees = (float) (MaxAngle.Degrees * 2);
_angleIncreaseDegrees = MathF.Round(AngleIncrease / ((float) Math.PI / 180f), 2);
AngleDecay = MathF.Round(AngleDecay / ((float) Math.PI / 180f), 2);
}
void ISerializationHooks.AfterDeserialization()
{
// This hard-to-read area's dealing with recoil
// Use degrees in yaml as it's easier to read compared to "0.0125f"
MinAngle = Angle.FromDegrees(_minAngleDegrees / 2f);
// Random doubles it as it's +/- so uhh we'll just half it here for readability
MaxAngle = Angle.FromDegrees(_maxAngleDegrees / 2f);
_angleIncreaseDegrees ??= 40 / FireRate;
AngleIncrease = _angleIncreaseDegrees.Value * (float) Math.PI / 180f;
AngleDecay = _angleDecayDegrees * (float) Math.PI / 180f;
// 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();
}
}
public override void OnAdd()
{
base.OnAdd();
Owner.EnsureComponentWarn(out ServerRangedWeaponComponent rangedWeaponComponent);
rangedWeaponComponent.Barrel ??= this;
rangedWeaponComponent.FireHandler += Fire;
rangedWeaponComponent.WeaponCanFireHandler += WeaponCanFire;
}
public override void OnRemove()
{
base.OnRemove();
if (Owner.TryGetComponent(out ServerRangedWeaponComponent? rangedWeaponComponent))
{
rangedWeaponComponent.Barrel = null;
rangedWeaponComponent.FireHandler -= Fire;
rangedWeaponComponent.WeaponCanFireHandler -= WeaponCanFire;
}
}
private Angle GetRecoilAngle(Angle direction)
{
var currentTime = _gameTiming.CurTime;
var timeSinceLastFire = (currentTime - _lastFire).TotalSeconds;
var newTheta = MathHelper.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 Task<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;
}
/// <summary>
/// Fires a round of ammo out of the weapon.
/// </summary>
/// <param name="shooter">Entity that is operating the weapon, usually the player.</param>
/// <param name="targetPos">Target position on the map to shoot at.</param>
private void Fire(IEntity shooter, Vector2 targetPos)
{
if (ShotsLeft == 0)
{
if (SoundEmpty != null)
{
SoundSystem.Play(Filter.Broadcast(), SoundEmpty, Owner.Transform.Coordinates);
}
return;
}
var ammo = PeekAmmo();
var projectile = TakeProjectile(shooter.Transform.Coordinates);
if (projectile == null)
{
SoundSystem.Play(Filter.Broadcast(), SoundEmpty, Owner.Transform.Coordinates);
return;
}
// At this point firing is confirmed
var direction = (targetPos - 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>() &&
ammo != null &&
ammo.TryGetComponent(out AmmoComponent? ammoComponent))
{
FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity, ammo);
if (CanMuzzleFlash)
{
ammoComponent.MuzzleFlash(Owner, angle);
}
if (ammoComponent.Caseless)
{
ammo.Delete();
}
}
else
{
// Invalid types
throw new InvalidOperationException();
}
if (!string.IsNullOrEmpty(SoundGunshot))
{
SoundSystem.Play(Filter.Broadcast(), SoundGunshot, Owner.Transform.Coordinates);
}
_lastFire = _gameTiming.CurTime;
}
/// <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)
{
robustRandom ??= IoCManager.Resolve<IRobustRandom>();
ejectDirections ??= new[]
{Direction.East, Direction.North, Direction.NorthWest, Direction.South, Direction.SouthEast, Direction.West};
const float ejectOffset = 1.8f;
var ammo = entity.GetComponent<AmmoComponent>();
var offsetPos = ((robustRandom.NextFloat() - 0.5f) * ejectOffset, (robustRandom.NextFloat() - 0.5f) * ejectOffset);
entity.Transform.Coordinates = entity.Transform.Coordinates.Offset(offsetPos);
entity.Transform.LocalRotation = robustRandom.Pick(ejectDirections).ToAngle();
if (ammo.SoundCollectionEject == null || !playSound)
{
return;
}
prototypeManager ??= IoCManager.Resolve<IPrototypeManager>();
var soundCollection = prototypeManager.Index<SoundCollectionPrototype>(ammo.SoundCollectionEject);
var randomFile = robustRandom.Pick(soundCollection.PickFiles);
SoundSystem.Play(Filter.Broadcast(), randomFile, entity.Transform.Coordinates, 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.NorthWest, Direction.South, Direction.SouthEast, 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, IEntity ammo)
{
List<Angle>? sprayAngleChange = null;
if (count > 1)
{
evenSpreadAngle *= SpreadRatio;
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
}
var firedProjectiles = new List<IEntity>();
for (var i = 0; i < count; i++)
{
IEntity projectile;
if (i == 0)
{
projectile = baseProjectile;
}
else
{
projectile =
Owner.EntityManager.SpawnEntity(baseProjectile.Prototype?.ID, baseProjectile.Transform.Coordinates);
}
firedProjectiles.Add(projectile);
Angle projectileAngle;
if (sprayAngleChange != null)
{
projectileAngle = angle + sprayAngleChange[i];
}
else
{
projectileAngle = angle;
}
var physics = projectile.GetComponent<IPhysBody>();
physics.BodyStatus = BodyStatus.InAir;
var projectileComponent = projectile.GetComponent<ProjectileComponent>();
projectileComponent.IgnoreEntity(shooter);
// FIXME: Work around issue where inserting and removing an entity from a container,
// then setting its linear velocity in the same tick resets velocity back to zero.
// See SharedBroadPhaseSystem.HandleContainerInsert()... It sets Awake to false, which causes this.
projectile.SpawnTimer(TimeSpan.FromMilliseconds(25), () =>
{
projectile
.GetComponent<IPhysBody>()
.LinearVelocity = projectileAngle.ToVec() * velocity;
});
projectile.Transform.LocalRotation = projectileAngle + MathHelper.PiOver2;
}
ammo.SendMessage(this, new BarrelFiredMessage(firedProjectiles));
}
/// <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.Coordinates.ToMapPos(Owner.EntityManager), angle.ToVec(), (int) hitscan.CollisionMask);
var physicsManager = EntitySystem.Get<SharedBroadPhaseSystem>();
var rayCastResults = physicsManager.IntersectRay(Owner.Transform.MapID, ray, hitscan.MaxLength, shooter, false).ToList();
if (rayCastResults.Count >= 1)
{
var result = rayCastResults[0];
var distance = result.Distance;
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
if (!result.HitEntity.TryGetComponent(out IDamageableComponent? damageable))
return;
damageable.ChangeDamage(hitscan.DamageType, (int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero), false, Owner);
//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
public virtual void Examine(FormattedMessage message, bool inDetailsRange)
{
var fireRateMessage = Loc.GetString(FireRateSelector switch
{
FireRateSelector.Safety => "Its safety is enabled.",
FireRateSelector.Single => "It's in single fire mode.",
FireRateSelector.Automatic => "It's in automatic fire mode.",
_ => throw new IndexOutOfRangeException()
});
message.AddText(fireRateMessage);
}
}
public class BarrelFiredMessage : ComponentMessage
{
public readonly List<IEntity> FiredProjectiles;
public BarrelFiredMessage(List<IEntity> firedProjectiles)
{
FiredProjectiles = firedProjectiles;
}
}
}

View File

@@ -0,0 +1,197 @@
using System;
using Content.Server.Atmos;
using Content.Server.CombatMode;
using Content.Server.Hands.Components;
using Content.Server.Interaction.Components;
using Content.Server.Stunnable.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Hands;
using Content.Shared.Notification;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Server.Weapon.Ranged
{
[RegisterComponent]
public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent, IHandSelected
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private TimeSpan _lastFireTime;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clumsyCheck")]
public bool ClumsyCheck { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clumsyExplodeChance")]
public float ClumsyExplodeChance { get; set; } = 0.5f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canHotspot")]
private bool _canHotspot = true;
public Func<bool>? WeaponCanFireHandler;
public Func<IEntity, bool>? UserCanFireHandler;
public Action<IEntity, Vector2>? 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()
{
return WeaponCanFireHandler == null || WeaponCanFireHandler();
}
private bool UserCanFire(IEntity user)
{
return (UserCanFireHandler == null || UserCanFireHandler(user)) && ActionBlockerSystem.CanAttack(user);
}
/// <inheritdoc />
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null)
{
base.HandleNetworkMessage(message, channel, session);
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
switch (message)
{
case FirePosComponentMessage msg:
var user = session.AttachedEntity;
if (user == null)
{
return;
}
if (msg.TargetGrid != GridId.Invalid)
{
// grid pos
if (!_mapManager.TryGetGrid(msg.TargetGrid, out var grid))
{
// Client sent us a message with an invalid grid.
break;
}
var targetPos = grid.LocalToWorld(msg.TargetPosition);
TryFire(user, targetPos);
}
else
{
// map pos
TryFire(user, msg.TargetPosition);
}
break;
}
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new RangedWeaponComponentState(FireRateSelector);
}
/// <summary>
/// Tries to fire a round of ammo out of the weapon.
/// </summary>
/// <param name="user">Entity that is operating the weapon, usually the player.</param>
/// <param name="targetPos">Target position on the map to shoot at.</param>
private void TryFire(IEntity user, Vector2 targetPos)
{
if (!user.TryGetComponent(out HandsComponent? hands) || hands.GetActiveHand?.Owner != Owner)
{
return;
}
if(!user.TryGetComponent(out CombatModeComponent? combat) || !combat.IsInCombatMode) {
return;
}
if (!UserCanFire(user) || !WeaponCanFire())
{
return;
}
var curTime = _gameTiming.CurTime;
var span = curTime - _lastFireTime;
if (span.TotalSeconds < 1 / _barrel?.FireRate)
{
return;
}
_lastFireTime = curTime;
if (ClumsyCheck && ClumsyComponent.TryRollClumsy(user, ClumsyExplodeChance))
{
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Items/bikehorn.ogg",
Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5));
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Weapons/Guns/Gunshots/bang.ogg",
Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5));
if (user.TryGetComponent(out IDamageableComponent? health))
{
health.ChangeDamage(DamageType.Blunt, 10, false, user);
health.ChangeDamage(DamageType.Heat, 5, false, user);
}
if (user.TryGetComponent(out StunnableComponent? stun))
{
stun.Paralyze(3f);
}
user.PopupMessage(Loc.GetString("The gun blows up in your face!"));
Owner.Delete();
return;
}
if (_canHotspot && user.Transform.Coordinates.TryGetTileAtmosphere(out var tile))
{
tile.HotspotExpose(700, 50);
}
FireHandler?.Invoke(user, targetPos);
}
// Probably a better way to do this.
void IHandSelected.HandSelected(HandSelectedEventArgs eventArgs)
{
Dirty();
}
}
}

View File

@@ -0,0 +1,48 @@
#nullable enable
using Content.Server.Battery.Components;
using Content.Server.Power.Components;
using Content.Server.PowerCell.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
namespace Content.Server.Weapon
{
/// <summary>
/// Recharges the battery in a <see cref="ServerBatteryBarrelComponent"/>.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(BaseCharger))]
public sealed class WeaponCapacitorChargerComponent : BaseCharger
{
public override string Name => "WeaponCapacitorCharger";
protected override bool IsEntityCompatible(IEntity entity)
{
return entity.TryGetComponent(out ServerBatteryBarrelComponent? battery) && battery.PowerCell != null ||
entity.TryGetComponent(out PowerCellSlotComponent? slot) && slot.HasCell;
}
protected override BatteryComponent? GetBatteryFrom(IEntity entity)
{
if (entity.TryGetComponent(out PowerCellSlotComponent? slot))
{
if (slot.Cell != null)
{
return slot.Cell;
}
}
if (entity.TryGetComponent(out ServerBatteryBarrelComponent? battery))
{
if (battery.PowerCell != null)
{
return battery.PowerCell;
}
}
return null;
}
}
}