Proto-kinetic crusher (#16277)
Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
53
Content.Shared/PowerCell/PowerCellDrawComponent.cs
Normal file
53
Content.Shared/PowerCell/PowerCellDrawComponent.cs
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
Content.Shared/Weapons/Marker/DamageMarkerComponent.cs
Normal file
38
Content.Shared/Weapons/Marker/DamageMarkerComponent.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
81
Content.Shared/Weapons/Marker/SharedDamageMarkerSystem.cs
Normal file
81
Content.Shared/Weapons/Marker/SharedDamageMarkerSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
7
Content.Shared/Weapons/Melee/Events/AttemptMeleeEvent.cs
Normal file
7
Content.Shared/Weapons/Melee/Events/AttemptMeleeEvent.cs
Normal 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);
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user