Proto-kinetic crusher (#16277)

Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>
This commit is contained in:
metalgearsloth
2023-05-14 13:15:18 +10:00
committed by GitHub
parent 356bf96039
commit 6417bb4fa0
68 changed files with 926 additions and 312 deletions

View File

@@ -1,16 +0,0 @@
namespace Content.Shared.Interaction.Events;
/// <summary>
/// Raised on directed a weapon when being used in a melee attack.
/// </summary>
[ByRefEvent]
public struct MeleeAttackAttemptEvent
{
public bool Cancelled = false;
public readonly EntityUid User;
public MeleeAttackAttemptEvent(EntityUid user)
{
User = user;
}
}

View File

@@ -0,0 +1,53 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.PowerCell;
/// <summary>
/// Indicates that the entity's ActivatableUI requires power or else it closes.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class PowerCellDrawComponent : Component
{
#region Prediction
/// <summary>
/// Whether there is any charge available to draw.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("canDraw"), AutoNetworkedField]
public bool CanDraw;
/// <summary>
/// Whether there is sufficient charge to use.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("canUse"), AutoNetworkedField]
public bool CanUse;
#endregion
/// <summary>
/// Is this power cell currently drawing power every tick.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("enabled")]
public bool Drawing;
/// <summary>
/// How much the entity draws while the UI is open.
/// Set to 0 if you just wish to check for power upon opening the UI.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("drawRate")]
public float DrawRate = 1f;
/// <summary>
/// How much power is used whenever the entity is "used".
/// This is used to ensure the UI won't open again without a minimum use power.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("useRate")]
public float UseRate;
/// <summary>
/// When the next automatic power draw will occur
/// </summary>
[DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdateTime;
}

View File

@@ -21,7 +21,7 @@ public abstract class SharedPowerCellSystem : EntitySystem
private void OnRejuventate(EntityUid uid, PowerCellSlotComponent component, RejuvenateEvent args)
{
if (!_itemSlots.TryGetSlot(uid, component.CellSlotId, out ItemSlot? itemSlot) || !itemSlot.Item.HasValue)
if (!_itemSlots.TryGetSlot(uid, component.CellSlotId, out var itemSlot) || !itemSlot.Item.HasValue)
return;
// charge entity batteries and remove booby traps.

View File

@@ -4,34 +4,42 @@ using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Projectiles
namespace Content.Shared.Projectiles;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ProjectileComponent : Component
{
[RegisterComponent, NetworkedComponent]
public sealed class ProjectileComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("impactEffect", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? ImpactEffect;
[ViewVariables(VVAccess.ReadWrite), DataField("impactEffect", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? ImpactEffect;
public EntityUid Shooter { get; set; }
/// <summary>
/// User that shot this projectile.
/// </summary>
[DataField("shooter"), AutoNetworkedField] public EntityUid Shooter;
public bool IgnoreShooter = true;
/// <summary>
/// Weapon used to shoot.
/// </summary>
[DataField("weapon"), AutoNetworkedField]
public EntityUid Weapon;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
[DataField("ignoreShooter"), AutoNetworkedField]
public bool IgnoreShooter = true;
[DataField("deleteOnCollide")]
public bool DeleteOnCollide { get; } = true;
[DataField("damage", required: true)] [ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = new();
[DataField("ignoreResistances")]
public bool IgnoreResistances { get; } = false;
[DataField("deleteOnCollide")]
public bool DeleteOnCollide = true;
// Get that juicy FPS hit sound
[DataField("soundHit")] public SoundSpecifier? SoundHit;
[DataField("ignoreResistances")]
public bool IgnoreResistances = false;
[DataField("soundForce")]
public bool ForceSound = false;
// Get that juicy FPS hit sound
[DataField("soundHit")] public SoundSpecifier? SoundHit;
public bool DamagedEntity;
}
[DataField("soundForce")]
public bool ForceSound = false;
public bool DamagedEntity;
}

View File

@@ -30,31 +30,18 @@ namespace Content.Shared.Projectiles
component.Shooter = uid;
Dirty(component);
}
}
[NetSerializable, Serializable]
public sealed class ProjectileComponentState : ComponentState
[Serializable, NetSerializable]
public sealed class ImpactEffectEvent : EntityEventArgs
{
public string Prototype;
public EntityCoordinates Coordinates;
public ImpactEffectEvent(string prototype, EntityCoordinates coordinates)
{
public ProjectileComponentState(EntityUid shooter, bool ignoreShooter)
{
Shooter = shooter;
IgnoreShooter = ignoreShooter;
}
public EntityUid Shooter { get; }
public bool IgnoreShooter { get; }
}
[Serializable, NetSerializable]
public sealed class ImpactEffectEvent : EntityEventArgs
{
public string Prototype;
public EntityCoordinates Coordinates;
public ImpactEffectEvent(string prototype, EntityCoordinates coordinates)
{
Prototype = prototype;
Coordinates = coordinates;
}
Prototype = prototype;
Coordinates = coordinates;
}
}
}

View File

@@ -0,0 +1,38 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
namespace Content.Shared.Weapons.Marker;
/// <summary>
/// Marks an entity to take additional damage
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedDamageMarkerSystem))]
public sealed partial class DamageMarkerComponent : Component
{
/// <summary>
/// Sprite to apply to the entity while damagemarker is applied.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("effect")]
public SpriteSpecifier.Rsi? Effect = new(new ResPath("/Textures/Objects/Weapons/Effects"), "shield2");
/// <summary>
/// Sound to play when the damage marker is procced.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("sound")]
public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/kinetic_accel.ogg");
[ViewVariables(VVAccess.ReadWrite), DataField("damage")]
public DamageSpecifier Damage = new();
/// <summary>
/// Entity that marked this entity for a damage surplus.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("marker"), AutoNetworkedField]
public EntityUid Marker;
[ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
public TimeSpan EndTime;
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Damage;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Weapons.Marker;
/// <summary>
/// Applies <see cref="DamageMarkerComponent"/> when colliding with an entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedDamageMarkerSystem))]
public sealed partial class DamageMarkerOnCollideComponent : Component
{
[DataField("whitelist"), AutoNetworkedField]
public EntityWhitelist? Whitelist = new();
[ViewVariables(VVAccess.ReadWrite), DataField("duration"), AutoNetworkedField]
public TimeSpan Duration = TimeSpan.FromSeconds(5);
/// <summary>
/// Additional damage to be applied.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("damage")]
public DamageSpecifier Damage = new();
/// <summary>
/// How many more times we can apply it.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("amount"), AutoNetworkedField]
public int Amount = 1;
}

View File

@@ -0,0 +1,81 @@
using Content.Shared.Damage;
using Content.Shared.Projectiles;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Physics.Events;
using Robust.Shared.Timing;
namespace Content.Shared.Weapons.Marker;
public abstract class SharedDamageMarkerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DamageMarkerOnCollideComponent, StartCollideEvent>(OnMarkerCollide);
SubscribeLocalEvent<DamageMarkerComponent, EntityUnpausedEvent>(OnMarkerUnpaused);
SubscribeLocalEvent<DamageMarkerComponent, AttackedEvent>(OnMarkerAttacked);
}
private void OnMarkerAttacked(EntityUid uid, DamageMarkerComponent component, AttackedEvent args)
{
if (component.Marker != args.Used)
return;
args.BonusDamage += component.Damage;
RemCompDeferred<DamageMarkerComponent>(uid);
_audio.PlayPredicted(component.Sound, uid, args.User);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<DamageMarkerComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.EndTime > _timing.CurTime)
continue;
RemCompDeferred<DamageMarkerComponent>(uid);
}
}
private void OnMarkerUnpaused(EntityUid uid, DamageMarkerComponent component, ref EntityUnpausedEvent args)
{
component.EndTime += args.PausedTime;
}
private void OnMarkerCollide(EntityUid uid, DamageMarkerOnCollideComponent component, ref StartCollideEvent args)
{
if (!args.OtherFixture.Hard ||
args.OurFixture.ID != SharedProjectileSystem.ProjectileFixture ||
component.Amount <= 0 ||
component.Whitelist?.IsValid(args.OtherEntity, EntityManager) == false ||
!TryComp<ProjectileComponent>(uid, out var projectile) ||
!projectile.Weapon.IsValid())
{
return;
}
// Markers are exclusive, deal with it.
var marker = EnsureComp<DamageMarkerComponent>(args.OtherEntity);
marker.Damage = new DamageSpecifier(component.Damage);
marker.Marker = projectile.Weapon;
marker.EndTime = _timing.CurTime + component.Duration;
component.Amount--;
Dirty(marker);
if (component.Amount <= 0)
{
QueueDel(uid);
}
else
{
Dirty(component);
}
}
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Weapons.Melee.Components;
/// <summary>
/// Indicates that this meleeweapon requires wielding to be useable.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class MeleeRequiresWieldComponent : Component
{
}

View File

@@ -1,3 +1,4 @@
using Content.Shared.Damage;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
@@ -37,6 +38,8 @@ namespace Content.Shared.Weapons.Melee.Events
/// </summary>
public EntityCoordinates ClickLocation { get; }
public DamageSpecifier BonusDamage = new();
public AttackedEvent(EntityUid used, EntityUid user, EntityCoordinates clickLocation)
{
Used = used;

View File

@@ -0,0 +1,7 @@
namespace Content.Shared.Weapons.Melee.Events;
/// <summary>
/// Raised directed on a weapon when attempt a melee attack.
/// </summary>
[ByRefEvent]
public record struct AttemptMeleeEvent(bool Cancelled, string? Message);

View File

@@ -15,6 +15,8 @@ using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Weapons.Melee.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
@@ -70,6 +72,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
SubscribeLocalEvent<MeleeWeaponComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<MeleeWeaponComponent, HandDeselectedEvent>(OnMeleeDropped);
SubscribeLocalEvent<MeleeWeaponComponent, HandSelectedEvent>(OnMeleeSelected);
SubscribeLocalEvent<MeleeWeaponComponent, GunShotEvent>(OnMeleeShot);
SubscribeAllEvent<HeavyAttackEvent>(OnHeavyAttack);
SubscribeAllEvent<LightAttackEvent>(OnLightAttack);
@@ -89,6 +92,18 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
#endif
}
private void OnMeleeShot(EntityUid uid, MeleeWeaponComponent component, ref GunShotEvent args)
{
if (!TryComp<GunComponent>(uid, out var gun))
return;
if (gun.NextFire > component.NextAttack)
{
component.NextAttack = gun.NextFire;
Dirty(component);
}
}
private void OnMeleeUnpaused(EntityUid uid, MeleeWeaponComponent component, ref EntityUnpausedEvent args)
{
component.NextAttack += args.PausedTime;
@@ -356,38 +371,64 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
}
// Windup time checked elsewhere.
var fireRate = TimeSpan.FromSeconds(1f / weapon.AttackRate);
var swings = 0;
// TODO: If we get autoattacks then probably need a shotcounter like guns so we can do timing properly.
if (weapon.NextAttack < curTime)
weapon.NextAttack = curTime;
weapon.NextAttack += TimeSpan.FromSeconds(1f / weapon.AttackRate);
// Attack confirmed
string animation;
switch (attack)
while (weapon.NextAttack <= curTime)
{
case LightAttackEvent light:
DoLightAttack(user, light, weaponUid, weapon, session);
animation = weapon.ClickAnimation;
break;
case DisarmAttackEvent disarm:
if (!DoDisarm(user, disarm, weaponUid, weapon, session))
return;
animation = weapon.ClickAnimation;
break;
case HeavyAttackEvent heavy:
DoHeavyAttack(user, heavy, weaponUid, weapon, session);
animation = weapon.WideAnimation;
break;
default:
throw new NotImplementedException();
weapon.NextAttack += fireRate;
swings++;
}
DoLungeAnimation(user, weapon.Angle, attack.Coordinates.ToMap(EntityManager, TransformSystem), weapon.Range, animation);
weapon.Attacking = true;
Dirty(weapon);
// Do this AFTER attack so it doesn't spam every tick
var ev = new AttemptMeleeEvent();
RaiseLocalEvent(weaponUid, ref ev);
if (ev.Cancelled)
{
if (ev.Message != null)
{
PopupSystem.PopupClient(ev.Message, weaponUid, user);
}
return;
}
// Attack confirmed
for (var i = 0; i < swings; i++)
{
string animation;
switch (attack)
{
case LightAttackEvent light:
DoLightAttack(user, light, weaponUid, weapon, session);
animation = weapon.ClickAnimation;
break;
case DisarmAttackEvent disarm:
if (!DoDisarm(user, disarm, weaponUid, weapon, session))
return;
animation = weapon.ClickAnimation;
break;
case HeavyAttackEvent heavy:
DoHeavyAttack(user, heavy, weaponUid, weapon, session);
animation = weapon.WideAnimation;
break;
default:
throw new NotImplementedException();
}
DoLungeAnimation(user, weapon.Angle, attack.Coordinates.ToMap(EntityManager, TransformSystem), weapon.Range, animation);
}
weapon.Attacking = true;
}
/// <summary>
@@ -469,9 +510,10 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
Interaction.DoContactInteraction(user, ev.Target);
// For stuff that cares about it being attacked.
RaiseLocalEvent(ev.Target.Value, new AttackedEvent(meleeUid, user, targetXform.Coordinates));
var attackedEvent = new AttackedEvent(meleeUid, user, targetXform.Coordinates);
RaiseLocalEvent(ev.Target.Value, attackedEvent);
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage, hitEvent.ModifiersList);
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
var damageResult = Damageable.TryChangeDamage(ev.Target, modifiedDamage, origin:user);
if (damageResult != null && damageResult.Total > FixedPoint2.Zero)
@@ -596,16 +638,15 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
// If the user is using a long-range weapon, this probably shouldn't be happening? But I'll interpret melee as a
// somewhat messy scuffle. See also, light attacks.
Interaction.DoContactInteraction(user, target);
RaiseLocalEvent(target, new AttackedEvent(meleeUid, user, Transform(target).Coordinates));
}
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage, hitEvent.ModifiersList);
var appliedDamage = new DamageSpecifier();
foreach (var entity in targets)
{
RaiseLocalEvent(entity, new AttackedEvent(meleeUid, user, ev.Coordinates));
var attackedEvent = new AttackedEvent(meleeUid, user, ev.Coordinates);
RaiseLocalEvent(entity, attackedEvent);
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin:user);
@@ -631,7 +672,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
if (appliedDamage.Total > FixedPoint2.Zero)
{
var target = entities.First();
PlayHitSound(target, user, GetHighestDamageSound(modifiedDamage, _protoManager), hitEvent.HitSoundOverride, component.HitSound);
PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component.HitSound);
}
else
{

View File

@@ -79,6 +79,12 @@ public partial class GunComponent : Component
#endregion
/// <summary>
/// Whether this gun is shot via the use key or the alt-use key.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("useKey"), AutoNetworkedField]
public bool UseKey = true;
/// <summary>
/// Where the gun is being requested to shoot.
/// </summary>

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Weapons.Ranged.Components;
/// <summary>
/// Indicates that this gun requires wielding to be useable.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class GunRequiresWieldComponent : Component
{
}

View File

@@ -0,0 +1,14 @@
using Content.Shared.Timing;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Weapons.Ranged.Components;
/// <summary>
/// Applies UseDelay whenever the entity shoots.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(UseDelayOnShootSystem))]
public sealed class UseDelayOnShootComponent : Component
{
}

View File

@@ -47,8 +47,10 @@ public sealed class RechargeBasicEntityAmmoSystem : EntitySystem
if (_gun.UpdateBasicEntityAmmoCount(uid, ammo.Count.Value + 1, ammo))
{
if (_netManager.IsClient && _timing.IsFirstTimePredicted)
_audio.Play(recharge.RechargeSound, Filter.Local(), uid, true);
// We don't predict this because occasionally on client it may not play.
// PlayPredicted will still be predicted on the client.
if (_netManager.IsServer)
_audio.PlayPvs(recharge.RechargeSound, uid);
}
if (ammo.Count == ammo.Capacity)

View File

@@ -13,11 +13,12 @@ using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Components;
@@ -70,7 +71,7 @@ public abstract partial class SharedGunSystem : EntitySystem
Sawmill.Level = LogLevel.Info;
SubscribeAllEvent<RequestShootEvent>(OnShootRequest);
SubscribeAllEvent<RequestStopShootEvent>(OnStopShootRequest);
SubscribeLocalEvent<GunComponent, MeleeAttackAttemptEvent>(OnGunMeleeAttempt);
SubscribeLocalEvent<GunComponent, MeleeHitEvent>(OnGunMelee);
// Ammo providers
InitializeBallistic();
@@ -103,19 +104,23 @@ public abstract partial class SharedGunSystem : EntitySystem
#endif
}
private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args)
{
if (!TryComp<MeleeWeaponComponent>(uid, out var melee))
return;
if (melee.NextAttack > component.NextFire)
{
component.NextFire = melee.NextAttack;
Dirty(component);
}
}
private void OnGunUnpaused(EntityUid uid, GunComponent component, ref EntityUnpausedEvent args)
{
component.NextFire += args.PausedTime;
}
private void OnGunMeleeAttempt(EntityUid uid, GunComponent component, ref MeleeAttackAttemptEvent args)
{
if (TagSystem.HasTag(args.User, "GunsDisabled"))
return;
args.Cancelled = true;
}
private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args)
{
var user = args.SenderSession.AttachedEntity;
@@ -214,15 +219,12 @@ public abstract partial class SharedGunSystem : EntitySystem
if (toCoordinates == null)
return;
if (TagSystem.HasTag(user, "GunsDisabled"))
{
if (Timing.IsFirstTimePredicted)
Popup(Loc.GetString("gun-disabled"), user, user);
return;
}
var curTime = Timing.CurTime;
// Maybe Raise an event for this? CanAttack doesn't seem appropriate.
if (TryComp<MeleeWeaponComponent>(gunUid, out var melee) && melee.NextAttack > curTime)
return;
// Need to do this to play the clicking sound for empty automatic weapons
// but not play anything for burst fire.
if (gun.NextFire > curTime)
@@ -232,7 +234,8 @@ public abstract partial class SharedGunSystem : EntitySystem
// First shot
// Previously we checked shotcounter but in some cases all the bullets got dumped at once
if (gun.NextFire < curTime - fireRate)
// curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker.
if (gun.NextFire < curTime - fireRate || gun.ShotCounter == 0 && gun.NextFire < curTime)
gun.NextFire = curTime;
var shots = 0;
@@ -263,6 +266,20 @@ public abstract partial class SharedGunSystem : EntitySystem
throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!");
}
var attemptEv = new AttemptShootEvent(user, null);
RaiseLocalEvent(gunUid, ref attemptEv);
if (attemptEv.Cancelled)
{
if (attemptEv.Message != null)
{
PopupSystem.PopupClient(attemptEv.Message, gunUid, user);
}
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
return;
}
var fromCoordinates = Transform(user).Coordinates;
// Remove ammo
var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
@@ -279,10 +296,7 @@ public abstract partial class SharedGunSystem : EntitySystem
// where the gun may be SemiAuto or Burst.
gun.ShotCounter += shots;
var attemptEv = new AttemptShootEvent(user);
RaiseLocalEvent(gunUid, ref attemptEv);
if (ev.Ammo.Count <= 0 || attemptEv.Cancelled)
if (ev.Ammo.Count <= 0)
{
// Play empty gun sounds if relevant
// If they're firing an existing clip then don't play anything.
@@ -415,7 +429,7 @@ public abstract partial class SharedGunSystem : EntitySystem
/// <param name="Cancelled">Set this to true if the shot should be cancelled.</param>
/// <param name="ThrowItems">Set this to true if the ammo shouldn't actually be fired, just thrown.</param>
[ByRefEvent]
public record struct AttemptShootEvent(EntityUid User, bool Cancelled = false, bool ThrowItems = false);
public record struct AttemptShootEvent(EntityUid User, string? Message, bool Cancelled = false, bool ThrowItems = false);
/// <summary>
/// Raised directed on the gun after firing.

View File

@@ -0,0 +1,20 @@
using Content.Shared.Timing;
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Shared.Weapons.Ranged.Systems;
public sealed class UseDelayOnShootSystem : EntitySystem
{
[Dependency] private readonly UseDelaySystem _delay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UseDelayOnShootComponent, GunShotEvent>(OnUseShoot);
}
private void OnUseShoot(EntityUid uid, UseDelayOnShootComponent component, ref GunShotEvent args)
{
_delay.BeginDelay(uid);
}
}

View File

@@ -8,6 +8,10 @@ using Content.Shared.Item;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Melee.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Wieldable.Components;
using Robust.Shared.Player;
@@ -34,9 +38,32 @@ public sealed class WieldableSystem : EntitySystem
SubscribeLocalEvent<WieldableComponent, GetVerbsEvent<InteractionVerb>>(AddToggleWieldVerb);
SubscribeLocalEvent<WieldableComponent, DisarmAttemptEvent>(OnDisarmAttemptEvent);
SubscribeLocalEvent<MeleeRequiresWieldComponent, AttemptMeleeEvent>(OnMeleeAttempt);
SubscribeLocalEvent<GunRequiresWieldComponent, AttemptShootEvent>(OnShootAttempt);
SubscribeLocalEvent<IncreaseDamageOnWieldComponent, MeleeHitEvent>(OnMeleeHit);
}
private void OnMeleeAttempt(EntityUid uid, MeleeRequiresWieldComponent component, ref AttemptMeleeEvent args)
{
if (TryComp<WieldableComponent>(uid, out var wieldable) &&
!wieldable.Wielded)
{
args.Cancelled = true;
args.Message = Loc.GetString("wieldable-component-requires", ("item", uid));
}
}
private void OnShootAttempt(EntityUid uid, GunRequiresWieldComponent component, ref AttemptShootEvent args)
{
if (TryComp<WieldableComponent>(uid, out var wieldable) &&
!wieldable.Wielded)
{
args.Cancelled = true;
args.Message = Loc.GetString("wieldable-component-requires", ("item", uid));
}
}
private void OnDisarmAttemptEvent(EntityUid uid, WieldableComponent component, DisarmAttemptEvent args)
{
if (component.Wielded)