diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index 7b9f06c201..973d497455 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -220,9 +220,9 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem RaiseLocalEvent(new DamageEffectEvent(Color.Red, targets)); } - protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, MeleeWeaponComponent component, ICommonSession? session) + protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { - if (!base.DoDisarm(user, ev, component, session)) + if (!base.DoDisarm(user, ev, meleeUid, component, session)) return false; if (!TryComp(user, out var combatMode) || diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs index 2ba8c88464..666b2b257a 100644 --- a/Content.Server/Flash/FlashSystem.cs +++ b/Content.Server/Flash/FlashSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Server.Flash.Components; using Content.Server.Light.EntitySystems; using Content.Server.Stunnable; @@ -43,11 +44,12 @@ namespace Content.Server.Flash private void OnFlashMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent args) { - if (!args.IsHit) - return; - - if (!UseFlash(comp, args.User)) + if (!args.IsHit || + !args.HitEntities.Any() || + !UseFlash(comp, args.User)) + { return; + } args.Handled = true; foreach (var e in args.HitEntities) diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs index b29bd52b26..afef3dea0e 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Melee.cs @@ -95,17 +95,19 @@ public sealed partial class NPCCombatSystem foreach (var (comp, _) in EntityQuery()) { - if (!combatQuery.TryGetComponent(comp.Owner, out var combat) || !combat.IsInCombatMode) + var uid = comp.Owner; + + if (!combatQuery.TryGetComponent(uid, out var combat) || !combat.IsInCombatMode) { - RemComp(comp.Owner); + RemComp(uid); continue; } - Attack(comp, curTime, physicsQuery, xformQuery); + Attack(uid, comp, curTime, physicsQuery, xformQuery); } } - private void Attack(NPCMeleeCombatComponent component, TimeSpan curTime, EntityQuery physicsQuery, EntityQuery xformQuery) + private void Attack(EntityUid uid, NPCMeleeCombatComponent component, TimeSpan curTime, EntityQuery physicsQuery, EntityQuery xformQuery) { component.Status = CombatStatus.Normal; @@ -115,7 +117,7 @@ public sealed partial class NPCCombatSystem return; } - if (!xformQuery.TryGetComponent(component.Owner, out var xform) || + if (!xformQuery.TryGetComponent(uid, out var xform) || !xformQuery.TryGetComponent(component.Target, out var targetXform)) { component.Status = CombatStatus.TargetUnreachable; @@ -134,7 +136,7 @@ public sealed partial class NPCCombatSystem return; } - if (TryComp(component.Owner, out var steering) && + if (TryComp(uid, out var steering) && steering.Status == SteeringStatus.NoPath) { component.Status = CombatStatus.TargetUnreachable; @@ -147,11 +149,11 @@ public sealed partial class NPCCombatSystem return; } - steering = EnsureComp(component.Owner); + steering = EnsureComp(uid); steering.Range = MathF.Max(0.2f, weapon.Range - 0.4f); // Gets unregistered on component shutdown. - _steering.TryRegister(component.Owner, new EntityCoordinates(component.Target, Vector2.Zero), steering); + _steering.TryRegister(uid, new EntityCoordinates(component.Target, Vector2.Zero), steering); if (weapon.NextAttack > curTime || !Enabled) return; @@ -160,11 +162,11 @@ public sealed partial class NPCCombatSystem physicsQuery.TryGetComponent(component.Target, out var targetPhysics) && targetPhysics.LinearVelocity.LengthSquared != 0f) { - _melee.AttemptLightAttackMiss(component.Owner, weapon, targetXform.Coordinates.Offset(_random.NextVector2(0.5f))); + _melee.AttemptLightAttackMiss(uid, component.Weapon, weapon, targetXform.Coordinates.Offset(_random.NextVector2(0.5f))); } else { - _melee.AttemptLightAttack(component.Owner, weapon, component.Target); + _melee.AttemptLightAttack(uid, component.Weapon, weapon, component.Target); } } } diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs index 03f1fb06f9..d8c32482ce 100644 --- a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs +++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs @@ -127,7 +127,7 @@ public sealed partial class NPCSteeringSystem // TODO: Validate we can damage it if (destructibleQuery.HasComponent(ent)) { - _melee.AttemptLightAttack(component.Owner, meleeWeapon, ent); + _melee.AttemptLightAttack(component.Owner, component.Owner, meleeWeapon, ent); break; } } diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 4f7017c77d..d57583fd35 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -91,14 +91,14 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem return; if (user == null) - PopupSystem.PopupEntity(message, uid.Value); + PopupSystem.PopupEntity(message, uid.Value); else PopupSystem.PopupEntity(message, uid.Value, Filter.PvsExcept(user.Value, entityManager: EntityManager), true); } - protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, MeleeWeaponComponent component, ICommonSession? session) + protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { - if (!base.DoDisarm(user, ev, component, session)) + if (!base.DoDisarm(user, ev, meleeUid, component, session)) return false; if (!TryComp(user, out var combatMode) || @@ -228,11 +228,12 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem private void OnChemicalInjectorHit(EntityUid owner, MeleeChemicalInjectorComponent comp, MeleeHitEvent args) { - if (!args.IsHit) - return; - - if (!_solutions.TryGetSolution(owner, comp.Solution, out var solutionContainer)) + if (!args.IsHit || + !args.HitEntities.Any() || + !_solutions.TryGetSolution(owner, comp.Solution, out var solutionContainer)) + { return; + } var hitBloodstreams = new List(); var bloodQuery = GetEntityQuery(); diff --git a/Content.Shared/Damage/Systems/StaminaSystem.cs b/Content.Shared/Damage/Systems/StaminaSystem.cs index f2ecf2c090..e4a89aac93 100644 --- a/Content.Shared/Damage/Systems/StaminaSystem.cs +++ b/Content.Shared/Damage/Systems/StaminaSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Alert; using Content.Shared.CombatMode; @@ -139,10 +140,12 @@ public sealed class StaminaSystem : EntitySystem private void OnHit(EntityUid uid, StaminaDamageOnHitComponent component, MeleeHitEvent args) { - if (!args.IsHit) + if (!args.IsHit || + !args.HitEntities.Any() || + component.Damage <= 0f) + { return; - - if (component.Damage <= 0f) return; + } var ev = new StaminaDamageOnHitAttemptEvent(); RaiseLocalEvent(uid, ref ev); diff --git a/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs b/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs index 2b80cff7a1..6810cb6dca 100644 --- a/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs +++ b/Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs @@ -12,7 +12,7 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs /// /// The base amount of damage dealt by the melee hit. /// - public readonly DamageSpecifier BaseDamage = new(); + public readonly DamageSpecifier BaseDamage; /// /// Modifier sets to apply to the hit event when it's all said and done. @@ -31,18 +31,18 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs /// /// A list containing every hit entity. Can be zero. /// - public IEnumerable HitEntities { get; } + public IReadOnlyList HitEntities; /// /// Used to define a new hit sound in case you want to override the default GenericHit. /// Also gets a pitch modifier added to it. /// - public SoundSpecifier? HitSoundOverride {get; set;} + public SoundSpecifier? HitSoundOverride; /// /// The user who attacked with the melee weapon. /// - public EntityUid User { get; } + public readonly EntityUid User; /// /// Check if this is true before attempting to do something during a melee attack other than changing/adding bonus damage.
@@ -59,4 +59,4 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs User = user; BaseDamage = baseDamage; } -} \ No newline at end of file +} diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index 9c609394a6..362756a52c 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -40,6 +40,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem [Dependency] protected readonly SharedInteractionSystem Interaction = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StaminaSystem _stamina = default!; protected ISawmill Sawmill = default!; @@ -148,7 +149,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem if (weapon?.Owner != msg.Weapon) return; - AttemptAttack(args.SenderSession.AttachedEntity!.Value, weapon, msg, args.SenderSession); + AttemptAttack(args.SenderSession.AttachedEntity!.Value, msg.Weapon, weapon, msg, args.SenderSession); } private void OnStopHeavyAttack(StopHeavyAttackEvent msg, EntitySessionEventArgs args) @@ -186,7 +187,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem if (userWeapon != weapon) return; - AttemptAttack(args.SenderSession.AttachedEntity.Value, weapon, msg, args.SenderSession); + AttemptAttack(args.SenderSession.AttachedEntity.Value, msg.Weapon, weapon, msg, args.SenderSession); } private void OnDisarmAttack(DisarmAttackEvent msg, EntitySessionEventArgs args) @@ -201,7 +202,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem if (userWeapon == null) return; - AttemptAttack(args.SenderSession.AttachedEntity.Value, userWeapon, msg, args.SenderSession); + AttemptAttack(args.SenderSession.AttachedEntity.Value, userWeapon.Owner, userWeapon, msg, args.SenderSession); } private void OnGetState(EntityUid uid, MeleeWeaponComponent component, ref ComponentGetState args) @@ -264,31 +265,31 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem return null; } - public void AttemptLightAttackMiss(EntityUid user, MeleeWeaponComponent weapon, EntityCoordinates coordinates) + public void AttemptLightAttackMiss(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityCoordinates coordinates) { - AttemptAttack(user, weapon, new LightAttackEvent(null, weapon.Owner, coordinates), null); + AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(null, weaponUid, coordinates), null); } - public void AttemptLightAttack(EntityUid user, MeleeWeaponComponent weapon, EntityUid target) + public void AttemptLightAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) { if (!TryComp(target, out var targetXform)) return; - AttemptAttack(user, weapon, new LightAttackEvent(target, weapon.Owner, targetXform.Coordinates), null); + AttemptAttack(user, weaponUid, weapon, new LightAttackEvent(target, weaponUid, targetXform.Coordinates), null); } - public void AttemptDisarmAttack(EntityUid user, MeleeWeaponComponent weapon, EntityUid target) + public void AttemptDisarmAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, EntityUid target) { if (!TryComp(target, out var targetXform)) return; - AttemptAttack(user, weapon, new DisarmAttackEvent(target, targetXform.Coordinates), null); + AttemptAttack(user, weaponUid, weapon, new DisarmAttackEvent(target, targetXform.Coordinates), null); } /// /// Called when a windup is finished and an attack is tried. /// - private void AttemptAttack(EntityUid user, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session) + private void AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponComponent weapon, AttackEvent attack, ICommonSession? session) { var curTime = Timing.CurTime; @@ -327,17 +328,17 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem switch (attack) { case LightAttackEvent light: - DoLightAttack(user, light, weapon, session); + DoLightAttack(user, light, weaponUid, weapon, session); animation = weapon.ClickAnimation; break; case DisarmAttackEvent disarm: - if (!DoDisarm(user, disarm, weapon, session)) + if (!DoDisarm(user, disarm, weaponUid, weapon, session)) return; animation = weapon.ClickAnimation; break; case HeavyAttackEvent heavy: - DoHeavyAttack(user, heavy, weapon, session); + DoHeavyAttack(user, heavy, weaponUid, weapon, session); animation = weapon.WideAnimation; break; default: @@ -385,34 +386,34 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem protected abstract bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session); - protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, MeleeWeaponComponent component, ICommonSession? session) + protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { - // Can't attack yourself - // Not in LOS. - if (user == ev.Target || - ev.Target == null || - Deleted(ev.Target) || - // For consistency with wide attacks stuff needs damageable. - !HasComp(ev.Target) || - !TryComp(ev.Target, out var targetXform)) - { - Audio.PlayPredicted(component.SwingSound, component.Owner, user); - return; - } - - if (!InRange(user, ev.Target.Value, component.Range, session)) - { - Audio.PlayPredicted(component.SwingSound, component.Owner, user); - return; - } - var damage = component.Damage * GetModifier(component, true); + // Can't attack yourself + // For consistency with wide attacks stuff needs damageable. + if (user == ev.Target || + Deleted(ev.Target) || + !HasComp(ev.Target) || + !TryComp(ev.Target, out var targetXform) || + // Not in LOS. + !InRange(user, ev.Target.Value, component.Range, session)) + { + // Leave IsHit set to true, because the only time it's set to false + // is when a melee weapon is examined. Misses are inferred from an + // empty HitEntities. + // TODO: This needs fixing + var missEvent = new MeleeHitEvent(new List(), user, damage); + RaiseLocalEvent(meleeUid, missEvent); + Audio.PlayPredicted(component.SwingSound, meleeUid, user); + return; + } + // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}"); // Raise event before doing damage so we can cancel damage if the event is handled var hitEvent = new MeleeHitEvent(new List { ev.Target.Value }, user, damage); - RaiseLocalEvent(component.Owner, hitEvent); + RaiseLocalEvent(meleeUid, hitEvent); if (hitEvent.Handled) return; @@ -430,7 +431,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem Interaction.DoContactInteraction(user, ev.Target); // For stuff that cares about it being attacked. - RaiseLocalEvent(ev.Target.Value, new AttackedEvent(component.Owner, user, targetXform.Coordinates)); + RaiseLocalEvent(ev.Target.Value, new AttackedEvent(meleeUid, user, targetXform.Coordinates)); var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage, hitEvent.ModifiersList); var damageResult = Damageable.TryChangeDamage(ev.Target, modifiedDamage, origin:user); @@ -443,7 +444,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem _stamina.TakeStaminaDamage(ev.Target.Value, (bluntDamage * component.BluntStaminaDamageFactor).Float(), source:user, with:(component.Owner == user ? null : component.Owner)); } - if (component.Owner == user) + if (meleeUid == user) { AdminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(user):user} melee attacked {ToPrettyString(ev.Target.Value):target} using their hands and dealt {damageResult.Total:damage} damage"); @@ -460,11 +461,11 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem { if (hitEvent.HitSoundOverride != null) { - Audio.PlayPredicted(hitEvent.HitSoundOverride, component.Owner, user); + Audio.PlayPredicted(hitEvent.HitSoundOverride, meleeUid, user); } else { - Audio.PlayPredicted(component.NoDamageSound, component.Owner, user); + Audio.PlayPredicted(component.NoDamageSound, meleeUid, user); } } @@ -476,7 +477,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem protected abstract void DoDamageEffect(List targets, EntityUid? user, TransformComponent targetXform); - protected virtual void DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, MeleeWeaponComponent component, ICommonSession? session) + protected virtual void DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { // TODO: This is copy-paste as fuck with DoPreciseAttack if (!TryComp(user, out var userXform)) @@ -491,16 +492,21 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem return; } - var userPos = userXform.WorldPosition; + var userPos = _transform.GetWorldPosition(userXform); var direction = targetMap.Position - userPos; var distance = Math.Min(component.Range, direction.Length); + var damage = component.Damage * GetModifier(component, false); + // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes. var entities = ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user); if (entities.Count == 0) { - Audio.PlayPredicted(component.SwingSound, component.Owner, user); + var missEvent = new MeleeHitEvent(new List(), user, damage); + RaiseLocalEvent(meleeUid, missEvent); + + Audio.PlayPredicted(component.SwingSound, meleeUid, user); return; } @@ -516,12 +522,11 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem targets.Add(entity); } - var damage = component.Damage * GetModifier(component, false); // Sawmill.Debug($"Melee damage is {damage.Total} out of {component.Damage.Total}"); // Raise event before doing damage so we can cancel damage if the event is handled var hitEvent = new MeleeHitEvent(targets, user, damage); - RaiseLocalEvent(component.Owner, hitEvent); + RaiseLocalEvent(meleeUid, hitEvent); if (hitEvent.Handled) return; @@ -537,7 +542,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem // somewhat messy scuffle. See also, light attacks. Interaction.DoContactInteraction(user, target); - RaiseLocalEvent(target, new AttackedEvent(component.Owner, user, Transform(target).Coordinates)); + RaiseLocalEvent(target, new AttackedEvent(meleeUid, user, Transform(target).Coordinates)); } var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage, hitEvent.ModifiersList); @@ -545,7 +550,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem foreach (var entity in targets) { - RaiseLocalEvent(entity, new AttackedEvent(component.Owner, user, ev.Coordinates)); + RaiseLocalEvent(entity, new AttackedEvent(meleeUid, user, ev.Coordinates)); var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin:user); @@ -553,7 +558,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem { appliedDamage += damageResult; - if (component.Owner == user) + if (meleeUid == user) { AdminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(user):user} melee attacked {ToPrettyString(entity):target} using their hands and dealt {damageResult.Total:damage} damage"); @@ -577,11 +582,11 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem { if (hitEvent.HitSoundOverride != null) { - Audio.PlayPredicted(hitEvent.HitSoundOverride, component.Owner, user); + Audio.PlayPredicted(hitEvent.HitSoundOverride, meleeUid, user); } else { - Audio.PlayPredicted(component.NoDamageSound, component.Owner, user); + Audio.PlayPredicted(component.NoDamageSound, meleeUid, user); } } } @@ -703,14 +708,14 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem return highestDamageType; } - protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, MeleeWeaponComponent component, ICommonSession? session) + protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { if (Deleted(ev.Target) || user == ev.Target) return false; // Play a sound to give instant feedback; same with playing the animations - Audio.PlayPredicted(component.SwingSound, component.Owner, user); + Audio.PlayPredicted(component.SwingSound, meleeUid, user); return true; }