From 8ab0e59db663c2b82bd7994a48b99ff5634a63e2 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 19 Apr 2025 11:38:22 +1000 Subject: [PATCH] Refactor disarms (#36546) * Refactor disarms - Move client stuff to shared - Cleanup a bunch of stuff - Ref events - Fix the swing sound mispredict (I noticed it on target dummies). * Revert this change * minor review * Rebiew --------- Co-authored-by: Milon --- .../Weapons/Melee/MeleeWeaponSystem.cs | 43 +---- .../CombatMode/Disarm/DisarmMalusComponent.cs | 16 -- Content.Server/Hands/Systems/HandsSystem.cs | 2 +- .../Weapons/Melee/MeleeWeaponSystem.cs | 134 -------------- .../Actions/Events/DisarmAttemptEvent.cs | 10 +- .../CombatMode/DisarmMalusComponent.cs | 17 ++ Content.Shared/CombatMode/DisarmedEvent.cs | 52 +++--- Content.Shared/Cuffs/SharedCuffableSystem.cs | 4 +- .../Damage/Systems/StaminaSystem.cs | 2 +- .../Weapons/Melee/SharedMeleeWeaponSystem.cs | 168 ++++++++++++++++-- 10 files changed, 214 insertions(+), 234 deletions(-) delete mode 100644 Content.Server/CombatMode/Disarm/DisarmMalusComponent.cs create mode 100644 Content.Shared/CombatMode/DisarmMalusComponent.cs diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index c4fef410c4..826436b88d 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -107,28 +107,28 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem { coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos); } - + // If the gun has AltFireComponent, it can be used to attack. if (TryComp(weaponUid, out var gun) && gun.UseKey) { if (!TryComp(weaponUid, out var altFireComponent) || altDown != BoundKeyState.Down) return; - + switch(altFireComponent.AttackType) { case AltFireAttackType.Light: ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon); break; - + case AltFireAttackType.Heavy: ClientHeavyAttack(entity, coordinates, weaponUid, weapon); break; - + case AltFireAttackType.Disarm: ClientDisarm(entity, mousePos, coordinates); break; } - + return; } @@ -166,35 +166,6 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem _color.RaiseEffect(Color.Red, targets, Filter.Local()); } - protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) - { - if (!base.DoDisarm(user, ev, meleeUid, component, session)) - return false; - - if (!TryComp(user, out var combatMode) || - combatMode.CanDisarm != true) - { - return false; - } - - var target = GetEntity(ev.Target); - - // They need to either have hands... - if (!HasComp(target!.Value)) - { - // or just be able to be shoved over. - if (TryComp(target, out var status) && status.AllowedEffects.Contains("KnockedDown")) - return true; - - if (Timing.IsFirstTimePredicted && HasComp(target.Value)) - PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value); - - return false; - } - - return true; - } - /// /// Raises a heavy attack event with the relevant attacked entities. /// This is to avoid lag effecting the client's perspective too much. @@ -222,7 +193,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem var entities = GetNetEntityList(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user).ToList()); RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities.GetRange(0, Math.Min(MaxTargets, entities.Count)), GetNetCoordinates(coordinates))); } - + private void ClientDisarm(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates) { EntityUid? target = null; @@ -232,7 +203,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates))); } - + private void ClientLightAttack(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates, EntityUid weaponUid, MeleeWeaponComponent meleeComponent) { var attackerPos = TransformSystem.GetMapCoordinates(attacker); diff --git a/Content.Server/CombatMode/Disarm/DisarmMalusComponent.cs b/Content.Server/CombatMode/Disarm/DisarmMalusComponent.cs deleted file mode 100644 index 49f677f286..0000000000 --- a/Content.Server/CombatMode/Disarm/DisarmMalusComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Content.Server.CombatMode.Disarm -{ - /// - /// Applies a malus to disarm attempts against this item. - /// - [RegisterComponent] - public sealed partial class DisarmMalusComponent : Component - { - /// - /// So, disarm chances are a % chance represented as a value between 0 and 1. - /// This default would be a 30% penalty to that. - /// - [DataField("malus")] - public float Malus = 0.3f; - } -} diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index 1e8e012c52..e45ee550e1 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -98,7 +98,7 @@ namespace Content.Server.Hands.Systems } } - private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent args) + private void OnDisarmed(EntityUid uid, HandsComponent component, ref DisarmedEvent args) { if (args.Handled) return; diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index ff975c4714..1e8f3334ac 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -1,26 +1,13 @@ using Content.Server.Chat.Systems; -using Content.Server.CombatMode.Disarm; using Content.Server.Movement.Systems; -using Content.Shared.Actions.Events; -using Content.Shared.Administration.Components; -using Content.Shared.CombatMode; using Content.Shared.Damage.Events; using Content.Shared.Damage.Systems; -using Content.Shared.Database; using Content.Shared.Effects; -using Content.Shared.Hands.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Mobs.Systems; -using Content.Shared.Popups; using Content.Shared.Speech.Components; -using Content.Shared.StatusEffect; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Player; -using Robust.Shared.Random; using System.Linq; using System.Numerics; @@ -28,12 +15,9 @@ namespace Content.Server.Weapons.Melee; public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem { - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly LagCompensationSystem _lag = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; public override void Initialize() @@ -85,106 +69,6 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem return true; } - protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) - { - if (!base.DoDisarm(user, ev, meleeUid, component, session)) - return false; - - if (!TryComp(user, out var combatMode) || - combatMode.CanDisarm != true) - { - return false; - } - - var target = GetEntity(ev.Target!.Value); - - if (_mobState.IsIncapacitated(target)) - { - return false; - } - - if (!TryComp(target, out var targetHandsComponent)) - { - if (!TryComp(target, out var status) || !status.AllowedEffects.Contains("KnockedDown")) - return false; - } - - if (!InRange(user, target, component.Range, session)) - { - return false; - } - - EntityUid? inTargetHand = null; - - if (targetHandsComponent?.ActiveHand is { IsEmpty: false }) - { - inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value; - } - - Interaction.DoContactInteraction(user, target); - - var attemptEvent = new DisarmAttemptEvent(target, user, inTargetHand); - - if (inTargetHand != null) - { - RaiseLocalEvent(inTargetHand.Value, attemptEvent); - } - - RaiseLocalEvent(target, attemptEvent); - - if (attemptEvent.Cancelled) - return false; - - var chance = CalculateDisarmChance(user, target, inTargetHand, combatMode); - - if (_random.Prob(chance)) - { - // Yknow something tells me this comment is hilariously out of date... - // Don't play a sound as the swing is already predicted. - // Also don't play popups because most disarms will miss. - return false; - } - - AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}"); - - var eventArgs = new DisarmedEvent { Target = target, Source = user, PushProbability = 1 - chance }; - RaiseLocalEvent(target, eventArgs); - - if (!eventArgs.Handled) - { - return false; - } - - _audio.PlayPvs(combatMode.DisarmSuccessSound, user, AudioParams.Default.WithVariation(0.025f).WithVolume(5f)); - AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}"); - - var targetEnt = Identity.Entity(target, EntityManager); - var userEnt = Identity.Entity(user, EntityManager); - - var msgOther = Loc.GetString( - eventArgs.PopupPrefix + "popup-message-other-clients", - ("performerName", userEnt), - ("targetName", targetEnt)); - - var msgUser = Loc.GetString(eventArgs.PopupPrefix + "popup-message-cursor", ("targetName", targetEnt)); - - var filterOther = Filter.PvsExcept(user, entityManager: EntityManager); - - PopupSystem.PopupEntity(msgOther, user, filterOther, true); - PopupSystem.PopupEntity(msgUser, target, user); - - if (eventArgs.IsStunned) - { - - PopupSystem.PopupEntity(Loc.GetString("stunned-component-disarm-success-others", ("source", userEnt), ("target", targetEnt)), targetEnt, Filter.PvsExcept(user), true, PopupType.LargeCaution); - PopupSystem.PopupCursor(Loc.GetString("stunned-component-disarm-success", ("target", targetEnt)), user, PopupType.Large); - - AdminLogger.Add(LogType.DisarmedKnockdown, LogImpact.Medium, $"{ToPrettyString(user):user} knocked down {ToPrettyString(target):target}"); - } - - return true; - } - protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session) { EntityCoordinates targetCoordinates; @@ -205,24 +89,6 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem _color.RaiseEffect(Color.Red, targets, filter); } - private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, CombatModeComponent disarmerComp) - { - if (HasComp(disarmer)) - return 1.0f; - - if (HasComp(disarmed)) - return 0.0f; - - var chance = disarmerComp.BaseDisarmFailChance; - - if (inTargetHand != null && TryComp(inTargetHand, out var malus)) - { - chance += malus.Malus; - } - - return Math.Clamp(chance, 0f, 1f); - } - public override void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true) { Filter filter; diff --git a/Content.Shared/Actions/Events/DisarmAttemptEvent.cs b/Content.Shared/Actions/Events/DisarmAttemptEvent.cs index 8252205846..002aaf33df 100644 --- a/Content.Shared/Actions/Events/DisarmAttemptEvent.cs +++ b/Content.Shared/Actions/Events/DisarmAttemptEvent.cs @@ -1,15 +1,21 @@ namespace Content.Shared.Actions.Events; -public sealed class DisarmAttemptEvent : CancellableEntityEventArgs +/// +/// Raised directed on the target OR their actively held entity. +/// +[ByRefEvent] +public record struct DisarmAttemptEvent { public readonly EntityUid TargetUid; public readonly EntityUid DisarmerUid; public readonly EntityUid? TargetItemInHandUid; + public bool Cancelled; + public DisarmAttemptEvent(EntityUid targetUid, EntityUid disarmerUid, EntityUid? targetItemInHandUid = null) { TargetUid = targetUid; DisarmerUid = disarmerUid; TargetItemInHandUid = targetItemInHandUid; } -} \ No newline at end of file +} diff --git a/Content.Shared/CombatMode/DisarmMalusComponent.cs b/Content.Shared/CombatMode/DisarmMalusComponent.cs new file mode 100644 index 0000000000..2a89eb1cf2 --- /dev/null +++ b/Content.Shared/CombatMode/DisarmMalusComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.CombatMode; + +/// +/// Applies a malus to disarm attempts against this item. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class DisarmMalusComponent : Component +{ + /// + /// So, disarm chances are a % chance represented as a value between 0 and 1. + /// This default would be a 30% penalty to that. + /// + [DataField] + public float Malus = 0.3f; +} diff --git a/Content.Shared/CombatMode/DisarmedEvent.cs b/Content.Shared/CombatMode/DisarmedEvent.cs index 884ae36628..3f8e5c81d1 100644 --- a/Content.Shared/CombatMode/DisarmedEvent.cs +++ b/Content.Shared/CombatMode/DisarmedEvent.cs @@ -1,31 +1,33 @@ -namespace Content.Shared.CombatMode +namespace Content.Shared.CombatMode; + +[ByRefEvent] +public record struct DisarmedEvent(EntityUid Target, EntityUid Source, float PushProb) { - public sealed class DisarmedEvent : HandledEntityEventArgs - { - /// - /// The entity being disarmed. - /// - public EntityUid Target { get; init; } + /// + /// The entity being disarmed. + /// + public readonly EntityUid Target = Target; - /// - /// The entity performing the disarm. - /// - public EntityUid Source { get; init; } + /// + /// The entity performing the disarm. + /// + public readonly EntityUid Source = Source; - /// - /// Probability for push/knockdown. - /// - public float PushProbability { get; init; } + /// + /// Probability for push/knockdown. + /// + public readonly float PushProbability = PushProb; - /// - /// Prefix for the popup message that will be displayed on a successful push. - /// Should be set before returning. - /// - public string PopupPrefix { get; set; } = ""; + /// + /// Prefix for the popup message that will be displayed on a successful push. + /// Should be set before returning. + /// + public string PopupPrefix = ""; - /// - /// Whether the entity was successfully stunned from a shove. - /// - public bool IsStunned { get; set; } - } + /// + /// Whether the entity was successfully stunned from a shove. + /// + public bool IsStunned; + + public bool Handled; } diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index bdb3a50454..99a4dad510 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -723,8 +723,8 @@ namespace Content.Shared.Cuffs // if combat mode is on, shove the person. if (_combatMode.IsInCombatMode(user) && target != user && user != null) { - var eventArgs = new DisarmedEvent { Target = target, Source = user.Value, PushProbability = 1}; - RaiseLocalEvent(target, eventArgs); + var eventArgs = new DisarmedEvent(target, user.Value, 1f); + RaiseLocalEvent(target, ref eventArgs); shoved = true; } diff --git a/Content.Shared/Damage/Systems/StaminaSystem.cs b/Content.Shared/Damage/Systems/StaminaSystem.cs index bd84b711e3..96454d20dd 100644 --- a/Content.Shared/Damage/Systems/StaminaSystem.cs +++ b/Content.Shared/Damage/Systems/StaminaSystem.cs @@ -119,7 +119,7 @@ public sealed partial class StaminaSystem : EntitySystem Dirty(uid, component); } - private void OnDisarmed(EntityUid uid, StaminaComponent component, DisarmedEvent args) + private void OnDisarmed(EntityUid uid, StaminaComponent component, ref DisarmedEvent args) { if (args.Handled) return; diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index 947c969a3e..59d1510d5f 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Content.Shared.ActionBlocker; +using Content.Shared.Actions.Events; +using Content.Shared.Administration.Components; using Content.Shared.Administration.Logs; using Content.Shared.CombatMode; using Content.Shared.Damage; @@ -10,22 +12,30 @@ using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Hands; using Content.Shared.Hands.Components; +using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Inventory; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; using Content.Shared.Physics; using Content.Shared.Popups; +using Content.Shared.StatusEffect; using Content.Shared.Weapons.Melee.Components; using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Timing; using ItemToggleMeleeWeaponComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleMeleeWeaponComponent; @@ -33,20 +43,24 @@ namespace Content.Shared.Weapons.Melee; public abstract class SharedMeleeWeaponSystem : EntitySystem { - [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!; - [Dependency] protected readonly ActionBlockerSystem Blocker = default!; - [Dependency] protected readonly SharedCombatModeSystem CombatMode = default!; - [Dependency] protected readonly DamageableSystem Damageable = default!; - [Dependency] protected readonly SharedInteractionSystem Interaction = default!; - [Dependency] protected readonly IMapManager MapManager = default!; - [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; - [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly MeleeSoundSystem _meleeSound = default!; - [Dependency] private readonly SharedPhysicsSystem _physics = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; - [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] protected readonly IMapManager MapManager = default!; + [Dependency] private readonly INetManager _netMan = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!; + [Dependency] protected readonly ActionBlockerSystem Blocker = default!; + [Dependency] protected readonly DamageableSystem Damageable = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly MeleeSoundSystem _meleeSound = default!; + [Dependency] protected readonly MobStateSystem MobState = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] protected readonly SharedCombatModeSystem CombatMode = default!; + [Dependency] protected readonly SharedInteractionSystem Interaction = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] protected readonly SharedPopupSystem PopupSystem = default!; + [Dependency] protected readonly SharedTransformSystem TransformSystem = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; private const int AttackMask = (int) (CollisionGroup.MobMask | CollisionGroup.Opaque); @@ -783,7 +797,25 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem return highestDamageType; } - protected virtual bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) + private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, CombatModeComponent disarmerComp) + { + if (HasComp(disarmer)) + return 1.0f; + + if (HasComp(disarmed)) + return 0.0f; + + var chance = disarmerComp.BaseDisarmFailChance; + + if (inTargetHand != null && TryComp(inTargetHand, out var malus)) + { + chance += malus.Malus; + } + + return Math.Clamp(chance, 0f, 1f); + } + + private bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { var target = GetEntity(ev.Target); @@ -793,8 +825,110 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem return false; } - // Play a sound to give instant feedback; same with playing the animations - _meleeSound.PlaySwingSound(user, meleeUid, component); + + if (MobState.IsIncapacitated(target.Value)) + { + return false; + } + + if (!TryComp(user, out var combatMode) || + combatMode.CanDisarm != true) + { + return false; + } + + // Need hands or to be able to be shoved over. + if (!TryComp(target, out var targetHandsComponent)) + { + if (!TryComp(target, out var status) || + !status.AllowedEffects.Contains("KnockedDown")) + { + // Notify disarmable + if (HasComp(target.Value)) + PopupSystem.PopupClient(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value); + + return false; + } + } + + if (!InRange(user, target.Value, component.Range, session)) + { + return false; + } + + EntityUid? inTargetHand = null; + + if (targetHandsComponent?.ActiveHand is { IsEmpty: false }) + { + inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value; + } + + var attemptEvent = new DisarmAttemptEvent(target.Value, user, inTargetHand); + + if (inTargetHand != null) + { + RaiseLocalEvent(inTargetHand.Value, ref attemptEvent); + } + + RaiseLocalEvent(target.Value, ref attemptEvent); + + if (attemptEvent.Cancelled) + return false; + + var chance = CalculateDisarmChance(user, target.Value, inTargetHand, combatMode); + + // At this point we diverge + if (_netMan.IsClient) + { + // Play a sound to give instant feedback; same with playing the animations + _meleeSound.PlaySwingSound(user, meleeUid, component); + return true; + } + + if (_random.Prob(chance)) + { + return false; + } + + var eventArgs = new DisarmedEvent(target.Value, user, 1 - chance); + RaiseLocalEvent(target.Value, ref eventArgs); + + // Nothing handled it so abort. + if (!eventArgs.Handled) + { + return false; + } + + Interaction.DoContactInteraction(user, target); + AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}"); + + AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}"); + + _audio.PlayPvs(combatMode.DisarmSuccessSound, target.Value, AudioParams.Default.WithVariation(0.025f).WithVolume(5f)); + var targetEnt = Identity.Entity(target.Value, EntityManager); + var userEnt = Identity.Entity(user, EntityManager); + + var msgOther = Loc.GetString( + eventArgs.PopupPrefix + "popup-message-other-clients", + ("performerName", userEnt), + ("targetName", targetEnt)); + + var msgUser = Loc.GetString(eventArgs.PopupPrefix + "popup-message-cursor", ("targetName", targetEnt)); + + var filterOther = Filter.PvsExcept(user, entityManager: EntityManager); + + PopupSystem.PopupEntity(msgOther, user, filterOther, true); + PopupSystem.PopupEntity(msgUser, target.Value, user); + + if (eventArgs.IsStunned) + { + + PopupSystem.PopupEntity(Loc.GetString("stunned-component-disarm-success-others", ("source", userEnt), ("target", targetEnt)), targetEnt, Filter.PvsExcept(user), true, PopupType.LargeCaution); + PopupSystem.PopupCursor(Loc.GetString("stunned-component-disarm-success", ("target", targetEnt)), user, PopupType.Large); + + AdminLogger.Add(LogType.DisarmedKnockdown, LogImpact.Medium, $"{ToPrettyString(user):user} knocked down {ToPrettyString(target):target}"); + } + return true; }