diff --git a/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs index 806e122204..7f4ec946fc 100644 --- a/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -28,6 +28,7 @@ namespace Content.Client.GameObjects.EntitySystems public override void Initialize() { SubscribeNetworkEvent(PlayWeaponArc); + SubscribeNetworkEvent(PlayLunge); } public override void FrameUpdate(float frameTime) @@ -106,5 +107,13 @@ namespace Content.Client.GameObjects.EntitySystems }); } } + + private void PlayLunge(PlayLungeAnimationMessage msg) + { + EntityManager + .GetEntity(msg.Source) + .EnsureComponent() + .SetData(msg.Angle); + } } } diff --git a/Content.Server/Actions/DisarmAction.cs b/Content.Server/Actions/DisarmAction.cs new file mode 100644 index 0000000000..e166f02577 --- /dev/null +++ b/Content.Server/Actions/DisarmAction.cs @@ -0,0 +1,89 @@ +#nullable enable +using System; +using System.Linq; +using Content.Server.GameObjects.Components.GUI; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Pulling; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces.GameObjects; +using Content.Server.Utility; +using Content.Shared.Actions; +using Content.Shared.Audio; +using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.Components.Pulling; +using Content.Shared.GameObjects.EntitySystems.ActionBlocker; +using Content.Shared.Interfaces; +using Content.Shared.Utility; +using JetBrains.Annotations; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Random; +using Robust.Shared.Serialization; + +namespace Content.Server.Actions +{ + [UsedImplicitly] + public class DisarmAction : ITargetEntityAction + { + private float _failProb; + private float _pushProb; + private float _cooldown; + + public void ExposeData(ObjectSerializer serializer) + { + serializer.DataField(ref _failProb, "failProb", 0.4f); + serializer.DataField(ref _pushProb, "pushProb", 0.4f); + serializer.DataField(ref _cooldown, "cooldown", 1.5f); + } + + public void DoTargetEntityAction(TargetEntityActionEventArgs args) + { + var disarmedActs = args.Target.GetAllComponents().ToArray(); + + if (disarmedActs.Length == 0 || !args.Performer.InRangeUnobstructed(args.Target)) return; + if (!args.Performer.TryGetComponent(out var actions)) return; + if (args.Target == args.Performer || !args.Performer.CanAttack()) return; + + var random = IoCManager.Resolve(); + var audio = EntitySystem.Get(); + var system = EntitySystem.Get(); + + var angle = new Angle(args.Target.Transform.MapPosition.Position - args.Performer.Transform.MapPosition.Position); + + actions.Cooldown(ActionType.Disarm, Cooldowns.SecondsFromNow(_cooldown)); + + if (random.Prob(_failProb)) + { + audio.PlayFromEntity("/Audio/Weapons/punchmiss.ogg", args.Performer, + AudioHelpers.WithVariation(0.025f)); + args.Performer.PopupMessageOtherClients(Loc.GetString("{0} fails to disarm {1}!", args.Performer.Name, args.Target.Name)); + args.Performer.PopupMessageCursor(Loc.GetString("You fail to disarm {0}!", args.Target.Name)); + system.SendLunge(angle, args.Performer); + return; + } + + system.SendAnimation("disarm", angle, args.Performer, args.Performer, new []{ args.Target }); + + var eventArgs = new DisarmedActEventArgs() {Target = args.Target, Source = args.Performer, PushProbability = _pushProb}; + + // Sort by priority. + Array.Sort(disarmedActs, (a, b) => a.Priority.CompareTo(b.Priority)); + + foreach (var disarmedAct in disarmedActs) + { + if (disarmedAct.Disarmed(eventArgs)) + return; + } + + audio.PlayFromEntity("/Audio/Effects/thudswoosh.ogg", args.Performer, + AudioHelpers.WithVariation(0.025f)); + } + } +} diff --git a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs index 9d1aa7cef9..0182636f77 100644 --- a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs @@ -4,22 +4,31 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Pulling; using Content.Server.GameObjects.EntitySystems.Click; +using Content.Server.Interfaces.GameObjects; using Content.Server.Interfaces.GameObjects.Components.Items; +using Content.Server.Utility; +using Content.Shared.Audio; using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Items; +using Content.Shared.GameObjects.Components.Pulling; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; +using Content.Shared.Interfaces; using Content.Shared.Physics.Pull; using Robust.Server.GameObjects; using Robust.Server.GameObjects.Components.Container; using Robust.Server.GameObjects.EntitySystemMessages; +using Robust.Server.GameObjects.EntitySystems; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Players; @@ -32,7 +41,7 @@ namespace Content.Server.GameObjects.Components.GUI [ComponentReference(typeof(IHandsComponent))] [ComponentReference(typeof(ISharedHandsComponent))] [ComponentReference(typeof(SharedHandsComponent))] - public class HandsComponent : SharedHandsComponent, IHandsComponent, IBodyPartAdded, IBodyPartRemoved + public class HandsComponent : SharedHandsComponent, IHandsComponent, IBodyPartAdded, IBodyPartRemoved, IDisarmedAct { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; @@ -718,6 +727,43 @@ namespace Content.Server.GameObjects.Components.GUI RemoveHand(args.Slot); } + + bool IDisarmedAct.Disarmed(DisarmedActEventArgs eventArgs) + { + if (BreakPulls()) + return false; + + var source = eventArgs.Source; + + EntitySystem.Get().PlayFromEntity("/Audio/Effects/thudswoosh.ogg", source, + AudioHelpers.WithVariation(0.025f)); + + if (ActiveHand != null && Drop(ActiveHand, false)) + { + source.PopupMessageOtherClients(Loc.GetString("{0} disarms {1}!", source.Name, eventArgs.Target.Name)); + source.PopupMessageCursor(Loc.GetString("You disarm {0}!", eventArgs.Target.Name)); + } + else + { + source.PopupMessageOtherClients(Loc.GetString("{0} shoves {1}!", source.Name, eventArgs.Target.Name)); + source.PopupMessageCursor(Loc.GetString("You shove {0}!", eventArgs.Target.Name)); + } + + return true; + } + + // We want this to be the last disarm act to run. + int IDisarmedAct.Priority => int.MaxValue; + + private bool BreakPulls() + { + // What is this API?? + if (!Owner.TryGetComponent(out SharedPullerComponent? puller) + || puller.Pulling == null || !puller.Pulling.TryGetComponent(out PullableComponent? pullable)) + return false; + + return pullable.TryStopPull(); + } } public class Hand : IDisposable diff --git a/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs b/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs index 9f71567a8b..041542e389 100644 --- a/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs @@ -1,17 +1,23 @@ using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces.GameObjects; +using Content.Server.Utility; using Content.Shared.Alert; using Content.Shared.Audio; using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Movement; +using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using NFluidsynth; using Robust.Server.GameObjects.EntitySystems; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.Timers; using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.Random; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Random; using Robust.Shared.Timers; using Logger = Robust.Shared.Log.Logger; @@ -19,7 +25,7 @@ namespace Content.Server.GameObjects.Components.Mobs { [RegisterComponent] [ComponentReference(typeof(SharedStunnableComponent))] - public class StunnableComponent : SharedStunnableComponent + public class StunnableComponent : SharedStunnableComponent, IDisarmedAct { [Dependency] private readonly IGameTiming _gameTiming = default!; @@ -121,5 +127,23 @@ namespace Content.Server.GameObjects.Components.Mobs return new StunnableComponentState(StunnedTimer, KnockdownTimer, SlowdownTimer, WalkModifierOverride, RunModifierOverride); } + + bool IDisarmedAct.Disarmed(DisarmedActEventArgs eventArgs) + { + if (!IoCManager.Resolve().Prob(eventArgs.PushProbability)) + return false; + + Paralyze(4f); + + var source = eventArgs.Source; + + EntitySystem.Get().PlayFromEntity("/Audio/Effects/thudswoosh.ogg", source, + AudioHelpers.WithVariation(0.025f)); + + source.PopupMessageOtherClients(Loc.GetString("{0} pushes {1}!", source, eventArgs.Target.Name)); + source.PopupMessageCursor(Loc.GetString("You push {0}!", eventArgs.Target.Name)); + + return true; + } } } diff --git a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs index b8d470975c..edeb316154 100644 --- a/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/MeleeWeaponSystem.cs @@ -14,5 +14,10 @@ namespace Content.Server.GameObjects.EntitySystems RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayMeleeWeaponAnimationMessage(arc, angle, attacker.Uid, source.Uid, hits.Select(e => e.Uid).ToList(), textureEffect, arcFollowAttacker)); } + + public void SendLunge(Angle angle, IEntity source) + { + RaiseNetworkEvent(new MeleeWeaponSystemMessages.PlayLungeAnimationMessage(angle, source.Uid)); + } } } diff --git a/Content.Server/Interfaces/GameObjects/IDisarmedAct.cs b/Content.Server/Interfaces/GameObjects/IDisarmedAct.cs new file mode 100644 index 0000000000..8d2fd3b190 --- /dev/null +++ b/Content.Server/Interfaces/GameObjects/IDisarmedAct.cs @@ -0,0 +1,42 @@ +using System; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.Interfaces.GameObjects +{ + /// + /// Implements behavior when an entity is disarmed. + /// + public interface IDisarmedAct + { + /// + /// Behavior when the entity is disarmed. + /// Return true to prevent the default disarm behavior, + /// or rest of IDisarmAct behaviors that come after this one from happening. + /// + bool Disarmed(DisarmedActEventArgs eventArgs); + + /// + /// Priority for this disarm act. + /// Used to determine act execution order. + /// + int Priority => 0; + } + + public class DisarmedActEventArgs : EventArgs + { + /// + /// The entity being disarmed. + /// + public IEntity Target { get; init; } + + /// + /// The entity performing the disarm. + /// + public IEntity Source { get; init; } + + /// + /// Probability for push/knockdown. + /// + public float PushProbability { get; init; } + } +} diff --git a/Content.Shared/Actions/ActionType.cs b/Content.Shared/Actions/ActionType.cs index 151d78b1dd..90160db3d4 100644 --- a/Content.Shared/Actions/ActionType.cs +++ b/Content.Shared/Actions/ActionType.cs @@ -7,6 +7,7 @@ { Error, HumanScream, + Disarm, DebugInstant, DebugToggle, DebugTargetPoint, diff --git a/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs index 1d16d09083..d949c33c76 100644 --- a/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs +++ b/Content.Shared/GameObjects/EntitySystemMessages/MeleeWeaponSystemMessages.cs @@ -30,5 +30,18 @@ namespace Content.Shared.GameObjects.EntitySystemMessages public bool TextureEffect { get; } public bool ArcFollowAttacker { get; } } + + [Serializable, NetSerializable] + public sealed class PlayLungeAnimationMessage : EntitySystemMessage + { + public Angle Angle { get; } + public EntityUid Source { get; } + + public PlayLungeAnimationMessage(Angle angle, EntityUid source) + { + Angle = angle; + Source = source; + } + } } } diff --git a/Resources/Prototypes/Actions/actions.yml b/Resources/Prototypes/Actions/actions.yml index c30aef5800..d588369a6b 100644 --- a/Resources/Prototypes/Actions/actions.yml +++ b/Resources/Prototypes/Actions/actions.yml @@ -22,6 +22,17 @@ - /Audio/Voice/Human/femalescream_5.ogg wilhelm: /Audio/Voice/Human/wilhelm_scream.ogg +- type: action + actionType: Disarm + icon: Interface/Actions/disarm.png + name: "[color=red]Disarm[/color]" + description: "Attempt to [color=red]disarm[/color] someone." + filters: + - human + behaviorType: TargetEntity + repeat: true + behavior: !type:DisarmAction { } + - type: action actionType: DebugInstant icon: Interface/Alerts/Human/human1.png diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index 84f57d23b0..afe8be1e5b 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -14,6 +14,7 @@ - type: Actions innateActions: - HumanScream + - Disarm - type: OverlayEffectsUI - type: Eye zoom: 0.5, 0.5 diff --git a/Resources/Textures/Interface/Actions/disarm.png b/Resources/Textures/Interface/Actions/disarm.png new file mode 100644 index 0000000000..f1efa1df68 Binary files /dev/null and b/Resources/Textures/Interface/Actions/disarm.png differ diff --git a/Resources/Textures/Interface/Actions/meta.json b/Resources/Textures/Interface/Actions/meta.json index 6ccd7cf002..a62b2f5582 100644 --- a/Resources/Textures/Interface/Actions/meta.json +++ b/Resources/Textures/Interface/Actions/meta.json @@ -18,6 +18,10 @@ { "name": "scream", "directions": 1 + }, + { + "name": "disarm", + "directions": 1 } ] }