diff --git a/Content.Client/Stunnable/StunSystem.cs b/Content.Client/Stunnable/StunSystem.cs new file mode 100644 index 0000000000..e519a0d5f0 --- /dev/null +++ b/Content.Client/Stunnable/StunSystem.cs @@ -0,0 +1,9 @@ +using Content.Shared.Stunnable; + +namespace Content.Client.Stunnable +{ + public sealed class StunSystem : SharedStunSystem + { + + } +} diff --git a/Content.Client/Stunnable/StunnableComponent.cs b/Content.Client/Stunnable/StunnableComponent.cs deleted file mode 100644 index 41af623f99..0000000000 --- a/Content.Client/Stunnable/StunnableComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.Movement.Components; -using Content.Shared.Stunnable; -using Robust.Shared.GameObjects; - -namespace Content.Client.Stunnable -{ - [RegisterComponent] - [ComponentReference(typeof(SharedStunnableComponent))] - public class StunnableComponent : SharedStunnableComponent - { - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); - - if (curState is not StunnableComponentState state) - { - return; - } - - StunnedTimer = state.StunnedTimer; - KnockdownTimer = state.KnockdownTimer; - SlowdownTimer = state.SlowdownTimer; - - WalkModifierOverride = state.WalkModifierOverride; - RunModifierOverride = state.RunModifierOverride; - - if (Owner.TryGetComponent(out MovementSpeedModifierComponent? movement)) - { - movement.RefreshMovementSpeedModifiers(); - } - } - } -} diff --git a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs index f870e563a6..0c044e9e57 100644 --- a/Content.IntegrationTests/Tests/InventoryHelpersTest.cs +++ b/Content.IntegrationTests/Tests/InventoryHelpersTest.cs @@ -1,8 +1,11 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Content.Server.Inventory; using Content.Server.Inventory.Components; using Content.Server.Items; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; +using Content.Shared.Stunnable; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -73,7 +76,7 @@ namespace Content.IntegrationTests.Tests Assert.That(inventory.TryGetSlotItem(Slots.INNERCLOTHING, out ItemComponent uniform)); Assert.That(uniform.Owner.Prototype != null && uniform.Owner.Prototype.ID == "InventoryJumpsuitJanitorDummy"); - stun.Stun(1f); + EntitySystem.Get().Stun(human.Uid, TimeSpan.FromSeconds(1f), stun); // Since the mob is stunned, they can't equip this. Assert.That(inventory.SpawnItemInSlot(Slots.IDCARD, "InventoryIDCardDummy", true), Is.False); diff --git a/Content.Server/Act/IDisarmedAct.cs b/Content.Server/Act/IDisarmedAct.cs index a51147ecdd..d32912451d 100644 --- a/Content.Server/Act/IDisarmedAct.cs +++ b/Content.Server/Act/IDisarmedAct.cs @@ -7,7 +7,7 @@ namespace Content.Server.Act /// /// Implements behavior when an entity is disarmed. /// - [RequiresExplicitImplementation] + [RequiresExplicitImplementation, Obsolete("Use the directed event instead.")] public interface IDisarmedAct { /// @@ -15,7 +15,7 @@ namespace Content.Server.Act /// Return true to prevent the default disarm behavior, /// or rest of IDisarmedAct behaviors that come after this one from happening. /// - bool Disarmed(DisarmedActEventArgs eventArgs); + bool Disarmed(DisarmedActEvent @event); /// /// Priority for this disarm act. @@ -24,7 +24,7 @@ namespace Content.Server.Act int Priority => 0; } - public class DisarmedActEventArgs : EventArgs + public class DisarmedActEvent : HandledEntityEventArgs { /// /// The entity being disarmed. diff --git a/Content.Server/Actions/Actions/DisarmAction.cs b/Content.Server/Actions/Actions/DisarmAction.cs index e4177a6757..53624bfca3 100644 --- a/Content.Server/Actions/Actions/DisarmAction.cs +++ b/Content.Server/Actions/Actions/DisarmAction.cs @@ -91,11 +91,18 @@ namespace Content.Server.Actions.Actions system.SendAnimation("disarm", angle, args.Performer, args.Performer, new[] { args.Target }); - var eventArgs = new DisarmedActEventArgs() { Target = args.Target, Source = args.Performer, PushProbability = _pushProb }; + var eventArgs = new DisarmedActEvent() { Target = args.Target, Source = args.Performer, PushProbability = _pushProb }; + + IoCManager.Resolve().EventBus.RaiseLocalEvent(args.Target.Uid, eventArgs); + + // Check if the event has been handled, and if so, do nothing else! + if (eventArgs.Handled) + return; // Sort by priority. Array.Sort(disarmedActs, (a, b) => a.Priority.CompareTo(b.Priority)); + // TODO: Remove this shit. foreach (var disarmedAct in disarmedActs) { if (disarmedAct.Disarmed(eventArgs)) diff --git a/Content.Server/Administration/Commands/RejuvenateCommand.cs b/Content.Server/Administration/Commands/RejuvenateCommand.cs index b653065895..63eb7fa6d9 100644 --- a/Content.Server/Administration/Commands/RejuvenateCommand.cs +++ b/Content.Server/Administration/Commands/RejuvenateCommand.cs @@ -2,12 +2,14 @@ using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.Nutrition.Components; using Content.Server.Nutrition.EntitySystems; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Shared.Administration; using Content.Shared.Damage; using Content.Shared.Jittering; using Content.Shared.MobState; using Content.Shared.Nutrition.Components; +using Content.Shared.Stunnable; using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.GameObjects; @@ -56,7 +58,11 @@ namespace Content.Server.Administration.Commands target.GetComponentOrNull()?.UpdateState(0); target.GetComponentOrNull()?.ResetFood(); target.GetComponentOrNull()?.ResetThirst(); - target.GetComponentOrNull()?.ResetStuns(); + + if (target.TryGetComponent(out StunnableComponent? stunnable)) + { + EntitySystem.Get().Reset(target.Uid, stunnable); + } if (target.TryGetComponent(out FlammableComponent? flammable)) { diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index 2827e20c83..5cf6530e6f 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -1,6 +1,7 @@ using System; using Content.Server.Alert; using Content.Server.Atmos.Components; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Server.Temperature.Components; using Content.Shared.ActionBlocker; @@ -9,6 +10,7 @@ using Content.Shared.Atmos; using Content.Shared.Damage; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Stunnable; using Content.Shared.Temperature; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; @@ -24,6 +26,7 @@ namespace Content.Server.Atmos.EntitySystems [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly StunSystem _stunSystem = default!; private const float MinimumFireStacks = -10f; private const float MaximumFireStacks = 20f; @@ -148,7 +151,10 @@ namespace Content.Server.Atmos.EntitySystems UpdateAppearance(uid, flammable); } - public void Resist(EntityUid uid, FlammableComponent? flammable = null, StunnableComponent? stunnable = null) + public void Resist(EntityUid uid, + FlammableComponent? flammable = null, + StunnableComponent? stunnable = null, + ServerAlertsComponent? alerts = null) { if (!Resolve(uid, ref flammable, ref stunnable)) return; @@ -159,8 +165,9 @@ namespace Content.Server.Atmos.EntitySystems flammable.Resisting = true; flammable.Owner.PopupMessage(Loc.GetString("flammable-component-resist-message")); - stunnable.Paralyze(2f); + _stunSystem.Paralyze(uid, TimeSpan.FromSeconds(2f), stunnable, alerts); + // TODO FLAMMABLE: Make this not use TimerComponent... flammable.Owner.SpawnTimer(2000, () => { flammable.Resisting = false; diff --git a/Content.Server/Buckle/Components/BuckleComponent.cs b/Content.Server/Buckle/Components/BuckleComponent.cs index fc30eb1892..3d9b6c7f61 100644 --- a/Content.Server/Buckle/Components/BuckleComponent.cs +++ b/Content.Server/Buckle/Components/BuckleComponent.cs @@ -13,6 +13,7 @@ using Content.Shared.Popups; using Content.Shared.Pulling; using Content.Shared.Pulling.Components; using Content.Shared.Standing; +using Content.Shared.Stunnable; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Containers; diff --git a/Content.Server/Conveyor/ConveyorSystem.cs b/Content.Server/Conveyor/ConveyorSystem.cs index 097a9849ea..898c4ef476 100644 --- a/Content.Server/Conveyor/ConveyorSystem.cs +++ b/Content.Server/Conveyor/ConveyorSystem.cs @@ -1,13 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Content.Server.Items; using Content.Server.MachineLinking.Events; using Content.Server.MachineLinking.Models; using Content.Server.Power.Components; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Shared.Conveyor; using Content.Shared.MachineLinking; using Content.Shared.Movement.Components; using Content.Shared.Popups; +using Content.Shared.Stunnable; using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.GameObjects; @@ -20,6 +23,7 @@ namespace Content.Server.Conveyor { public class ConveyorSystem : EntitySystem { + [Dependency] private StunSystem _stunSystem = default!; [Dependency] private IEntityLookup _entityLookup = default!; public override void Initialize() @@ -60,7 +64,7 @@ namespace Content.Server.Conveyor args.Cancel(); if (args.Attemptee.TryGetComponent(out var stunnableComponent)) { - stunnableComponent.Paralyze(2); + _stunSystem.Paralyze(uid, TimeSpan.FromSeconds(2f), stunnableComponent); component.Owner.PopupMessage(args.Attemptee, Loc.GetString("conveyor-component-failed-link")); } } diff --git a/Content.Server/Cuffs/Components/HandcuffComponent.cs b/Content.Server/Cuffs/Components/HandcuffComponent.cs index f175e89a26..368997af92 100644 --- a/Content.Server/Cuffs/Components/HandcuffComponent.cs +++ b/Content.Server/Cuffs/Components/HandcuffComponent.cs @@ -9,6 +9,7 @@ using Content.Shared.Interaction; using Content.Shared.Interaction.Helpers; using Content.Shared.Popups; using Content.Shared.Sound; +using Content.Shared.Stunnable; using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.Localization; diff --git a/Content.Server/Damage/Systems/DamageOnHighSpeedImpactSystem.cs b/Content.Server/Damage/Systems/DamageOnHighSpeedImpactSystem.cs index 6540382903..308dff0423 100644 --- a/Content.Server/Damage/Systems/DamageOnHighSpeedImpactSystem.cs +++ b/Content.Server/Damage/Systems/DamageOnHighSpeedImpactSystem.cs @@ -1,7 +1,10 @@ +using System; using Content.Server.Damage.Components; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Shared.Audio; using Content.Shared.Damage; +using Content.Shared.Stunnable; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.GameObjects; @@ -19,6 +22,7 @@ namespace Content.Server.Damage.Systems [Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly StunSystem _stunSystem = default!; public override void Initialize() { @@ -43,7 +47,7 @@ namespace Content.Server.Damage.Systems component.LastHit = _gameTiming.CurTime; if (EntityManager.TryGetComponent(uid, out StunnableComponent? stun) && _robustRandom.Prob(component.StunChance)) - stun.Stun(component.StunSeconds); + _stunSystem.Stun(uid, TimeSpan.FromSeconds(component.StunSeconds), stun); var damageScale = (speed / component.MinimumSpeed) * component.Factor; _damageableSystem.TryChangeDamage(uid, component.Damage * damageScale); diff --git a/Content.Server/DoAfter/DoAfter.cs b/Content.Server/DoAfter/DoAfter.cs index aa97387d90..0590cb7984 100644 --- a/Content.Server/DoAfter/DoAfter.cs +++ b/Content.Server/DoAfter/DoAfter.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Content.Server.Hands.Components; using Content.Server.Items; using Content.Server.Stunnable.Components; +using Content.Shared.Stunnable; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; diff --git a/Content.Server/Doors/Components/ServerDoorComponent.cs b/Content.Server/Doors/Components/ServerDoorComponent.cs index 10835bc822..adc0dd2a57 100644 --- a/Content.Server/Doors/Components/ServerDoorComponent.cs +++ b/Content.Server/Doors/Components/ServerDoorComponent.cs @@ -8,6 +8,7 @@ using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.Construction.Components; using Content.Server.Hands.Components; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Server.Tools; using Content.Server.Tools.Components; @@ -15,6 +16,7 @@ using Content.Shared.Damage; using Content.Shared.Doors; using Content.Shared.Interaction; using Content.Shared.Sound; +using Content.Shared.Stunnable; using Content.Shared.Tools; using Content.Shared.Tools.Components; using Robust.Shared.Audio; @@ -553,12 +555,6 @@ namespace Content.Server.Doors.Components // Crush foreach (var e in collidingentities) { - if (!e.Owner.TryGetComponent(out StunnableComponent? stun) - || !e.Owner.HasComponent()) - { - continue; - } - var percentage = e.GetWorldAABB().IntersectPercentage(doorAABB); if (percentage < 0.1f) @@ -567,9 +563,11 @@ namespace Content.Server.Doors.Components hitsomebody = true; CurrentlyCrushing.Add(e.Owner.Uid); - EntitySystem.Get().TryChangeDamage(e.Owner.Uid, CrushDamage); + if (e.Owner.HasComponent()) + EntitySystem.Get().TryChangeDamage(e.Owner.Uid, CrushDamage); - stun.Paralyze(DoorStunTime); + if(e.Owner.TryGetComponent(out StunnableComponent? stun)) + EntitySystem.Get().Paralyze(e.Owner.Uid, TimeSpan.FromSeconds(DoorStunTime), stun); } // If we hit someone, open up after stun (opens right when stun ends) diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs index b6017d1a63..73b65772fa 100644 --- a/Content.Server/Flash/FlashSystem.cs +++ b/Content.Server/Flash/FlashSystem.cs @@ -1,6 +1,8 @@ +using System; using Content.Server.Flash.Components; using Content.Server.Inventory.Components; using Content.Server.Items; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Server.Weapon.Melee; using Content.Shared.Examine; @@ -11,6 +13,7 @@ using Content.Shared.Inventory; using Content.Shared.Physics; using Content.Shared.Popups; using Content.Shared.Sound; +using Content.Shared.Stunnable; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.GameObjects; @@ -25,6 +28,7 @@ namespace Content.Server.Flash { [Dependency] private readonly IEntityLookup _entityLookup = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly StunSystem _stunSystem = default!; public override void Initialize() { @@ -127,9 +131,9 @@ namespace Content.Server.Flash flashable.Dirty(); } - if (EntityManager.TryGetComponent(target, out var stunnableComponent)) + if (EntityManager.TryGetComponent(target, out var stunnable)) { - stunnableComponent.Slowdown(flashDuration / 1000f, slowTo, slowTo); + _stunSystem.Slowdown(target, TimeSpan.FromSeconds(flashDuration/1000f), slowTo, slowTo, stunnable); } if (displayPopup && user != null && target != user) diff --git a/Content.Server/Hands/Components/HandsComponent.cs b/Content.Server/Hands/Components/HandsComponent.cs index a92c6e0ad7..83dc42a3da 100644 --- a/Content.Server/Hands/Components/HandsComponent.cs +++ b/Content.Server/Hands/Components/HandsComponent.cs @@ -129,13 +129,13 @@ namespace Content.Server.Hands.Components RemoveHand(args.Slot); } - bool IDisarmedAct.Disarmed(DisarmedActEventArgs eventArgs) + bool IDisarmedAct.Disarmed(DisarmedActEvent @event) { if (BreakPulls()) return false; - var source = eventArgs.Source; - var target = eventArgs.Target; + var source = @event.Source; + var target = @event.Target; if (source != null) { diff --git a/Content.Server/Instruments/InstrumentComponent.cs b/Content.Server/Instruments/InstrumentComponent.cs index 4b44643772..8ac61641d7 100644 --- a/Content.Server/Instruments/InstrumentComponent.cs +++ b/Content.Server/Instruments/InstrumentComponent.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Server.UserInterface; using Content.Shared.ActionBlocker; @@ -8,6 +9,7 @@ using Content.Shared.Instruments; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Standing; +using Content.Shared.Stunnable; using Content.Shared.Throwing; using Robust.Server.GameObjects; using Robust.Server.Player; @@ -364,7 +366,7 @@ namespace Content.Server.Instruments if (mob.TryGetComponent(out StunnableComponent? stun)) { - stun.Stun(1); + EntitySystem.Get().Stun(mob.Uid, TimeSpan.FromSeconds(1), stun); Clean(); } diff --git a/Content.Server/MobState/States/CriticalMobState.cs b/Content.Server/MobState/States/CriticalMobState.cs index 17a03ac1ee..f0347e29a5 100644 --- a/Content.Server/MobState/States/CriticalMobState.cs +++ b/Content.Server/MobState/States/CriticalMobState.cs @@ -1,5 +1,7 @@ -using Content.Server.Stunnable.Components; +using Content.Server.Stunnable; +using Content.Server.Stunnable.Components; using Content.Shared.MobState.State; +using Content.Shared.Stunnable; using Robust.Shared.GameObjects; namespace Content.Server.MobState.States @@ -12,7 +14,7 @@ namespace Content.Server.MobState.States if (entity.TryGetComponent(out StunnableComponent? stun)) { - stun.CancelAll(); + EntitySystem.Get().Reset(entity.Uid, stun); } } } diff --git a/Content.Server/MobState/States/DeadMobState.cs b/Content.Server/MobState/States/DeadMobState.cs index 2379afe0ef..8e3d16f150 100644 --- a/Content.Server/MobState/States/DeadMobState.cs +++ b/Content.Server/MobState/States/DeadMobState.cs @@ -1,8 +1,10 @@ using Content.Server.Alert; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Shared.Alert; using Content.Shared.MobState; using Content.Shared.MobState.State; +using Content.Shared.Stunnable; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; @@ -21,7 +23,8 @@ namespace Content.Server.MobState.States if (entity.TryGetComponent(out StunnableComponent? stun)) { - stun.CancelAll(); + // TODO: Use resolves to pass ServerAlertsComponent here. + EntitySystem.Get().Reset(entity.Uid, stun); } } } diff --git a/Content.Server/Stunnable/Components/StunOnCollideComponent.cs b/Content.Server/Stunnable/Components/StunOnCollideComponent.cs index 305604b134..b93a4b4116 100644 --- a/Content.Server/Stunnable/Components/StunOnCollideComponent.cs +++ b/Content.Server/Stunnable/Components/StunOnCollideComponent.cs @@ -1,3 +1,4 @@ +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; @@ -6,18 +7,26 @@ namespace Content.Server.Stunnable.Components /// /// Adds stun when it collides with an entity /// - [RegisterComponent] - internal sealed class StunOnCollideComponent : Component + [RegisterComponent, Friend(typeof(StunOnCollideSystem))] + public sealed class StunOnCollideComponent : Component { // TODO: Can probably predict this. public override string Name => "StunOnCollide"; // See stunnable for what these do [DataField("stunAmount")] - internal int StunAmount = default; + public int StunAmount; + [DataField("knockdownAmount")] - internal int KnockdownAmount = default; + public int KnockdownAmount; + [DataField("slowdownAmount")] - internal int SlowdownAmount = default; + public int SlowdownAmount; + + [DataField("walkSpeedMultiplier")] + public float WalkSpeedMultiplier = 1f; + + [DataField("runSpeedMultiplier")] + public float RunSpeedMultiplier = 1f; } } diff --git a/Content.Server/Stunnable/Components/StunnableComponent.cs b/Content.Server/Stunnable/Components/StunnableComponent.cs deleted file mode 100644 index 574848ecce..0000000000 --- a/Content.Server/Stunnable/Components/StunnableComponent.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Content.Server.Act; -using Content.Server.Popups; -using Content.Shared.Audio; -using Content.Shared.MobState; -using Content.Shared.Popups; -using Content.Shared.Sound; -using Content.Shared.Standing; -using Content.Shared.Stunnable; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Player; -using Robust.Shared.Random; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Server.Stunnable.Components -{ - [RegisterComponent] - [ComponentReference(typeof(SharedStunnableComponent))] - public class StunnableComponent : SharedStunnableComponent, IDisarmedAct - { - [DataField("stunAttemptSound")] private SoundSpecifier _stunAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); - - protected override void OnKnockdown() - { - EntitySystem.Get().Down(Owner); - } - - protected override void OnKnockdownEnd() - { - if (Owner.TryGetComponent(out IMobStateComponent? mobState) && !mobState.IsIncapacitated()) - EntitySystem.Get().Stand(Owner); - } - - public void CancelAll() - { - KnockdownTimer = null; - StunnedTimer = null; - Dirty(); - } - - public void ResetStuns() - { - StunnedTimer = null; - SlowdownTimer = null; - - if (KnockedDown && - Owner.TryGetComponent(out IMobStateComponent? mobState) && !mobState.IsIncapacitated()) - { - EntitySystem.Get().Stand(Owner); - } - - KnockdownTimer = null; - Dirty(); - } - - protected override void OnInteractHand() - { - SoundSystem.Play(Filter.Pvs(Owner), _stunAttemptSound.GetSound(), Owner, AudioHelpers.WithVariation(0.05f)); - } - - bool IDisarmedAct.Disarmed(DisarmedActEventArgs eventArgs) - { - if (!IoCManager.Resolve().Prob(eventArgs.PushProbability)) - return false; - - Paralyze(4f); - - var source = eventArgs.Source; - var target = eventArgs.Target; - - if (source != null) - { - SoundSystem.Play(Filter.Pvs(source), _stunAttemptSound.GetSound(), source, AudioHelpers.WithVariation(0.025f)); - if (target != null) - { - source.PopupMessageOtherClients(Loc.GetString("stunnable-component-disarm-success-others", ("source", source.Name), ("target", target.Name))); - source.PopupMessageCursor(Loc.GetString("stunnable-component-disarm-success", ("target", target.Name))); - } - } - - return true; - } - } -} diff --git a/Content.Server/Stunnable/StunOnCollideSystem.cs b/Content.Server/Stunnable/StunOnCollideSystem.cs index 178b733596..a191e82875 100644 --- a/Content.Server/Stunnable/StunOnCollideSystem.cs +++ b/Content.Server/Stunnable/StunOnCollideSystem.cs @@ -1,6 +1,14 @@ +using System; +using Content.Server.Alert; using Content.Server.Stunnable.Components; +using Content.Shared.Alert; +using Content.Shared.Movement.Components; +using Content.Shared.Standing; +using Content.Shared.Stunnable; using JetBrains.Annotations; +using Robust.Server.GameObjects; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Physics.Dynamics; namespace Content.Server.Stunnable @@ -8,6 +16,8 @@ namespace Content.Server.Stunnable [UsedImplicitly] internal sealed class StunOnCollideSystem : EntitySystem { + [Dependency] private readonly StunSystem _stunSystem = default!; + public override void Initialize() { base.Initialize(); @@ -16,11 +26,25 @@ namespace Content.Server.Stunnable private void HandleCollide(EntityUid uid, StunOnCollideComponent component, StartCollideEvent args) { - if (args.OtherFixture.Body.Owner.TryGetComponent(out StunnableComponent? stunnableComponent)) + var otherUid = args.OtherFixture.Body.Owner.Uid; + + if (EntityManager.TryGetComponent(otherUid, out StunnableComponent? stunnableComponent)) { - stunnableComponent.Stun(component.StunAmount); - stunnableComponent.Knockdown(component.KnockdownAmount); - stunnableComponent.Slowdown(component.SlowdownAmount); + ServerAlertsComponent? alerts = null; + StandingStateComponent? standingState = null; + AppearanceComponent? appearance = null; + MovementSpeedModifierComponent? speedModifier = null; + + // Let the actual methods log errors for these. + Resolve(otherUid, ref alerts, ref standingState, ref appearance, ref speedModifier, false); + + _stunSystem.Stun(otherUid, TimeSpan.FromSeconds(component.StunAmount), stunnableComponent, alerts); + + _stunSystem.Knockdown(otherUid, TimeSpan.FromSeconds(component.KnockdownAmount), stunnableComponent, + alerts, standingState, appearance); + + _stunSystem.Slowdown(otherUid, TimeSpan.FromSeconds(component.SlowdownAmount), + component.WalkSpeedMultiplier, component.RunSpeedMultiplier, stunnableComponent, speedModifier, alerts); } } } diff --git a/Content.Server/Stunnable/StunSystem.cs b/Content.Server/Stunnable/StunSystem.cs new file mode 100644 index 0000000000..c947063c6a --- /dev/null +++ b/Content.Server/Stunnable/StunSystem.cs @@ -0,0 +1,52 @@ +using System; +using Content.Server.Act; +using Content.Server.Popups; +using Content.Shared.Audio; +using Content.Shared.Popups; +using Content.Shared.Stunnable; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.Stunnable +{ + public sealed class StunSystem : SharedStunSystem + { + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDisarmed); + } + + private void OnDisarmed(EntityUid uid, StunnableComponent stunnable, DisarmedActEvent args) + { + if (args.Handled || !_random.Prob(args.PushProbability)) + return; + + Paralyze(uid, TimeSpan.FromSeconds(4f), stunnable); + + var source = args.Source; + var target = args.Target; + + if (source != null) + { + SoundSystem.Play(Filter.Pvs(source), stunnable.StunAttemptSound.GetSound(), source, AudioHelpers.WithVariation(0.025f)); + + if (target != null) + { + // TODO: Use PopupSystem + source.PopupMessageOtherClients(Loc.GetString("stunnable-component-disarm-success-others", ("source", source.Name), ("target", target.Name))); + source.PopupMessageCursor(Loc.GetString("stunnable-component-disarm-success", ("target", target.Name))); + } + } + + args.Handled = true; + } + } +} diff --git a/Content.Server/Stunnable/StunbatonSystem.cs b/Content.Server/Stunnable/StunbatonSystem.cs index f5acf7eb51..4e09e2052e 100644 --- a/Content.Server/Stunnable/StunbatonSystem.cs +++ b/Content.Server/Stunnable/StunbatonSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Audio; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Stunnable; using Content.Shared.Throwing; using Robust.Server.GameObjects; using Robust.Shared.Audio; @@ -23,6 +24,7 @@ namespace Content.Server.Stunnable { public class StunbatonSystem : EntitySystem { + [Dependency] private readonly StunSystem _stunSystem = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; public override void Initialize() @@ -119,20 +121,22 @@ namespace Content.Server.Stunnable { if (!entity.TryGetComponent(out StunnableComponent? stunnable) || !comp.Activated) return; + // TODO: Make slowdown inflicted customizable. + SoundSystem.Play(Filter.Pvs(comp.Owner), comp.StunSound.GetSound(), comp.Owner, AudioHelpers.WithVariation(0.25f)); if (!stunnable.SlowedDown) { if (_robustRandom.Prob(comp.ParalyzeChanceNoSlowdown)) - stunnable.Paralyze(comp.ParalyzeTime); + _stunSystem.Paralyze(entity.Uid, TimeSpan.FromSeconds(comp.ParalyzeTime), stunnable); else - stunnable.Slowdown(comp.SlowdownTime); + _stunSystem.Slowdown(entity.Uid, TimeSpan.FromSeconds(comp.SlowdownTime), 0.5f, 0.5f, stunnable); } else { if (_robustRandom.Prob(comp.ParalyzeChanceWithSlowdown)) - stunnable.Paralyze(comp.ParalyzeTime); + _stunSystem.Paralyze(entity.Uid, TimeSpan.FromSeconds(comp.ParalyzeTime), stunnable); else - stunnable.Slowdown(comp.SlowdownTime); + _stunSystem.Slowdown(entity.Uid, TimeSpan.FromSeconds(comp.SlowdownTime), 0.5f, 0.5f, stunnable); } diff --git a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs b/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs index 44bdfc85f9..a76168eda2 100644 --- a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs +++ b/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs @@ -3,6 +3,7 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.CombatMode; using Content.Server.Hands.Components; using Content.Server.Interaction.Components; +using Content.Server.Stunnable; using Content.Server.Stunnable.Components; using Content.Server.Weapon.Ranged.Barrels.Components; using Content.Shared.ActionBlocker; @@ -10,6 +11,7 @@ using Content.Shared.Damage; using Content.Shared.Hands; using Content.Shared.Popups; using Content.Shared.Sound; +using Content.Shared.Stunnable; using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Audio; using Robust.Shared.GameObjects; @@ -175,7 +177,7 @@ namespace Content.Server.Weapon.Ranged // Knock them down if (user.TryGetComponent(out StunnableComponent? stun)) { - stun.Paralyze(3f); + EntitySystem.Get().Paralyze(user.Uid, TimeSpan.FromSeconds(3f), stun); } // Apply salt to the wound ("Honk!") diff --git a/Content.Shared/Hands/SharedHandsSystem.cs b/Content.Shared/Hands/SharedHandsSystem.cs index 130676adc5..d56510a34d 100644 --- a/Content.Shared/Hands/SharedHandsSystem.cs +++ b/Content.Shared/Hands/SharedHandsSystem.cs @@ -20,8 +20,15 @@ namespace Content.Shared.Hands public void DropHandItems(IEntity entity, bool doMobChecks = true) { - if (!entity.TryGetComponent(out SharedHandsComponent? handsComponent)) return; - DropHandItems(handsComponent, doMobChecks); + DropHandItems(entity.Uid, doMobChecks); + } + + public void DropHandItems(EntityUid uid, bool doMobChecks = true, SharedHandsComponent? hands = null) + { + if (!Resolve(uid, ref hands)) + return; + + DropHandItems(hands, doMobChecks); } private void DropHandItems(SharedHandsComponent handsComponent, bool doMobChecks = true) diff --git a/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs index f7bca869a5..4afb9fb5a0 100644 --- a/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/SharedCreamPieSystem.cs @@ -1,14 +1,18 @@ +using System; using Content.Shared.Nutrition.Components; using Content.Shared.Stunnable; using Content.Shared.Throwing; using JetBrains.Annotations; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; namespace Content.Shared.Nutrition.EntitySystems { [UsedImplicitly] public abstract class SharedCreamPieSystem : EntitySystem { + [Dependency] private SharedStunSystem _stunSystem = default!; + public override void Initialize() { base.Initialize(); @@ -64,9 +68,9 @@ namespace Content.Shared.Nutrition.EntitySystems CreamedEntity(uid, creamPied, args); - if (EntityManager.TryGetComponent(uid, out SharedStunnableComponent? stun)) + if (EntityManager.TryGetComponent(uid, out StunnableComponent? stun)) { - stun.Paralyze(creamPie.ParalyzeTime); + _stunSystem.Paralyze(uid, TimeSpan.FromSeconds(creamPie.ParalyzeTime), stun); } } diff --git a/Content.Shared/Slippery/SharedSlipperySystem.cs b/Content.Shared/Slippery/SharedSlipperySystem.cs index e23a0a0857..96bca7d3c1 100644 --- a/Content.Shared/Slippery/SharedSlipperySystem.cs +++ b/Content.Shared/Slippery/SharedSlipperySystem.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.EffectBlocker; @@ -6,6 +7,7 @@ using Content.Shared.Stunnable; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Physics; using Robust.Shared.Physics.Dynamics; @@ -14,6 +16,8 @@ namespace Content.Shared.Slippery [UsedImplicitly] public abstract class SharedSlipperySystem : EntitySystem { + [Dependency] private readonly SharedStunSystem _stunSystem = default!; + private List _slipped = new(); public override void Initialize() @@ -45,12 +49,12 @@ namespace Content.Shared.Slippery } } - public bool CanSlip(SlipperyComponent component, EntityUid uid, [NotNullWhen(true)] out SharedStunnableComponent? stunnableComponent) + public bool CanSlip(SlipperyComponent component, EntityUid uid, [NotNullWhen(true)] out StunnableComponent? stunnableComponent) { if (!component.Slippery || component.Owner.IsInContainer() || component.Slipped.Contains(uid) - || !EntityManager.TryGetComponent(uid, out stunnableComponent)) + || !EntityManager.TryGetComponent(uid, out stunnableComponent)) { stunnableComponent = null; return false; @@ -82,7 +86,7 @@ namespace Content.Shared.Slippery otherBody.LinearVelocity *= component.LaunchForwardsMultiplier; - stun.Paralyze(5); + _stunSystem.Paralyze(component.Owner.Uid, TimeSpan.FromSeconds(5), stun); component.Slipped.Add(otherBody.Owner.Uid); component.Dirty(); diff --git a/Content.Shared/Standing/StandingStateComponent.cs b/Content.Shared/Standing/StandingStateComponent.cs index 6be3d48390..cd246587cf 100644 --- a/Content.Shared/Standing/StandingStateComponent.cs +++ b/Content.Shared/Standing/StandingStateComponent.cs @@ -1,6 +1,7 @@ using System; using Content.Shared.EffectBlocker; using Content.Shared.Sound; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Players; @@ -10,8 +11,8 @@ using Robust.Shared.ViewVariables; namespace Content.Shared.Standing { - [RegisterComponent] - [NetworkedComponent] + [Friend(typeof(StandingStateSystem))] + [RegisterComponent, NetworkedComponent] public sealed class StandingStateComponent : Component, IEffectBlocker { public override string Name => "StandingState"; diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index 6898ca2c02..ed6a125f8e 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -1,92 +1,117 @@ +using System; using Content.Shared.Audio; using Content.Shared.Hands; +using Content.Shared.Hands.Components; using Content.Shared.Rotation; using Robust.Shared.Audio; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Player; namespace Content.Shared.Standing { public sealed class StandingStateSystem : EntitySystem { + [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!; + + [Obsolete("Use the EntityUid overloads instead.")] public bool IsDown(IEntity entity) { - if (entity.TryGetComponent(out StandingStateComponent? standingState) && - standingState.Standing) return true; - - return false; + return IsDown(entity.Uid); } + public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) + { + if (!Resolve(uid, ref standingState, false)) + return false; + + return !standingState.Standing; + } + + [Obsolete("Use the EntityUid overloads instead.")] public void Down(IEntity entity, bool playSound = true, bool dropHeldItems = true) { - if (!entity.TryGetComponent(out StandingStateComponent? comp)) return; - Down(comp, playSound, dropHeldItems); + Down(entity.Uid, playSound, dropHeldItems); } - public void Stand(IEntity entity) + public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true, + StandingStateComponent? standingState = null, + SharedAppearanceComponent? appearance = null, + SharedHandsComponent? hands = null) { - if (!entity.TryGetComponent(out StandingStateComponent? comp)) return; - Stand(comp); - } + // TODO: This should actually log missing comps... + if (!Resolve(uid, ref standingState, false)) + return false; - public void Down(StandingStateComponent component, bool playSound = true, bool dropHeldItems = true) - { - if (!component.Standing) return; + // Optional component. + Resolve(uid, ref appearance, ref hands, false); - var entity = component.Owner; - var uid = entity.Uid; + if (!standingState.Standing) + return true; // This is just to avoid most callers doing this manually saving boilerplate // 99% of the time you'll want to drop items but in some scenarios (e.g. buckling) you don't want to. // We do this BEFORE downing because something like buckle may be blocking downing but we want to drop hand items anyway // and ultimately this is just to avoid boilerplate in Down callers + keep their behavior consistent. - if (dropHeldItems) + if (dropHeldItems && hands != null) { - Get().DropHandItems(entity, false); + _sharedHandsSystem.DropHandItems(uid, false, hands); } var msg = new DownAttemptEvent(); - EntityManager.EventBus.RaiseLocalEvent(uid, msg); + RaiseLocalEvent(uid, msg, false); - if (msg.Cancelled) return; + if (msg.Cancelled) + return false; - component.Standing = false; - component.Dirty(); - EntityManager.EventBus.RaiseLocalEvent(uid, new DownedEvent()); + standingState.Standing = false; + standingState.Dirty(); + RaiseLocalEvent(uid, new DownedEvent(), false); // Seemed like the best place to put it - if (entity.TryGetComponent(out SharedAppearanceComponent? appearance)) - { - appearance.SetData(RotationVisuals.RotationState, RotationState.Horizontal); - } + appearance?.SetData(RotationVisuals.RotationState, RotationState.Horizontal); // Currently shit is only downed by server but when it's predicted we can probably only play this on server / client if (playSound) { - SoundSystem.Play(Filter.Pvs(entity), component.DownSoundCollection.GetSound(), entity, AudioHelpers.WithVariation(0.25f)); + SoundSystem.Play(Filter.Pvs(uid), standingState.DownSoundCollection.GetSound(), uid, AudioHelpers.WithVariation(0.25f)); } + + return true; } - public void Stand(StandingStateComponent component) + [Obsolete("Use the EntityUid overloads instead.")] + public void Stand(IEntity entity) { - if (component.Standing) return; + Stand(entity.Uid); + } - var entity = component.Owner; - var uid = entity.Uid; + public bool Stand(EntityUid uid, + StandingStateComponent? standingState = null, + SharedAppearanceComponent? appearance = null) + { + // TODO: This should actually log missing comps... + if (!Resolve(uid, ref standingState, false)) + return false; + + // Optional component. + Resolve(uid, ref appearance, false); + + if (standingState.Standing) + return true; var msg = new StandAttemptEvent(); - EntityManager.EventBus.RaiseLocalEvent(uid, msg); + RaiseLocalEvent(uid, msg, false); - if (msg.Cancelled) return; + if (msg.Cancelled) + return false; - component.Standing = true; - component.Dirty(); - EntityManager.EventBus.RaiseLocalEvent(uid, new StoodEvent()); + standingState.Standing = true; + standingState.Dirty(); + RaiseLocalEvent(uid, new StoodEvent(), false); - if (entity.TryGetComponent(out SharedAppearanceComponent? appearance)) - { - appearance.SetData(RotationVisuals.RotationState, RotationState.Vertical); - } + appearance?.SetData(RotationVisuals.RotationState, RotationState.Vertical); + return true; } } diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs new file mode 100644 index 0000000000..f904342ef8 --- /dev/null +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -0,0 +1,402 @@ +using System; +using Content.Shared.Alert; +using Content.Shared.Audio; +using Content.Shared.DragDrop; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory.Events; +using Content.Shared.Item; +using Content.Shared.Movement; +using Content.Shared.Movement.Components; +using Content.Shared.Speech; +using Content.Shared.Standing; +using Content.Shared.Throwing; +using JetBrains.Annotations; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.IoC; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Shared.Stunnable +{ + [UsedImplicitly] + public abstract class SharedStunSystem : EntitySystem + { + [Dependency] private readonly StandingStateSystem _standingStateSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnInteractHand); + + // Attempt event subscriptions. + SubscribeLocalEvent(OnMoveAttempt); + SubscribeLocalEvent(OnInteractAttempt); + SubscribeLocalEvent(OnUseAttempt); + SubscribeLocalEvent(OnThrowAttempt); + SubscribeLocalEvent(OnDropAttempt); + SubscribeLocalEvent(OnPickupAttempt); + SubscribeLocalEvent(OnAttackAttempt); + SubscribeLocalEvent(OnEquipAttempt); + SubscribeLocalEvent(OnUnequipAttempt); + SubscribeLocalEvent(OnStandAttempt); + } + + private void OnGetState(EntityUid uid, StunnableComponent stunnable, ref ComponentGetState args) + { + args.State = new StunnableComponentState(stunnable.StunnedTimer, stunnable.KnockdownTimer, stunnable.SlowdownTimer, stunnable.WalkSpeedMultiplier, stunnable.RunSpeedMultiplier); + } + + private void OnHandleState(EntityUid uid, StunnableComponent stunnable, ref ComponentHandleState args) + { + if (args.Current is not StunnableComponentState state) + return; + + stunnable.StunnedTimer = state.StunnedTimer; + stunnable.KnockdownTimer = state.KnockdownTimer; + stunnable.SlowdownTimer = state.SlowdownTimer; + + stunnable.WalkSpeedMultiplier = state.WalkSpeedMultiplier; + stunnable.RunSpeedMultiplier = state.RunSpeedMultiplier; + + if (EntityManager.TryGetComponent(uid, out MovementSpeedModifierComponent? movement)) + movement.RefreshMovementSpeedModifiers(); + } + + private TimeSpan AdjustTime(TimeSpan time, (TimeSpan Start, TimeSpan End)? timer, float cap) + { + if (timer != null) + { + time = timer.Value.End - timer.Value.Start + time; + } + + if (time.TotalSeconds > cap) + time = TimeSpan.FromSeconds(cap); + + return time; + } + + // TODO STUN: Make events for different things. (Getting modifiers, attempt events, informative events...) + + /// + /// Stuns the entity, disallowing it from doing many interactions temporarily. + /// + public void Stun(EntityUid uid, TimeSpan time, + StunnableComponent? stunnable = null, + SharedAlertsComponent? alerts = null) + { + if (!Resolve(uid, ref stunnable)) + return; + + time = AdjustTime(time, stunnable.StunnedTimer, stunnable.StunCap); + + if (time <= TimeSpan.Zero) + return; + + stunnable.StunnedTimer = (_gameTiming.CurTime, _gameTiming.CurTime + time); + + SetAlert(uid, stunnable, alerts); + + stunnable.Dirty(); + } + + /// + /// Knocks down the entity, making it fall to the ground. + /// + public void Knockdown(EntityUid uid, TimeSpan time, + StunnableComponent? stunnable = null, + SharedAlertsComponent? alerts = null, + StandingStateComponent? standingState = null, + SharedAppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref stunnable)) + return; + + time = AdjustTime(time, stunnable.KnockdownTimer, stunnable.KnockdownCap); + + if (time <= TimeSpan.Zero) + return; + + // Check if we can actually knock down the mob. + if (!_standingStateSystem.Down(uid, standingState:standingState, appearance:appearance)) + return; + + stunnable.KnockdownTimer = (_gameTiming.CurTime, _gameTiming.CurTime + time);; + + SetAlert(uid, stunnable, alerts); + + stunnable.Dirty(); + } + /// + /// Applies knockdown and stun to the entity temporarily. + /// + public void Paralyze(EntityUid uid, TimeSpan time, + StunnableComponent? stunnable = null, + SharedAlertsComponent? alerts = null) + { + if (!Resolve(uid, ref stunnable)) + return; + + // Optional component. + Resolve(uid, ref alerts, false); + + Stun(uid, time, stunnable, alerts); + Knockdown(uid, time, stunnable, alerts); + } + + /// + /// Slows down the mob's walking/running speed temporarily + /// + public void Slowdown(EntityUid uid, TimeSpan time, float walkSpeedMultiplier = 1f, float runSpeedMultiplier = 1f, + StunnableComponent? stunnable = null, + MovementSpeedModifierComponent? speedModifier = null, + SharedAlertsComponent? alerts = null) + { + if (!Resolve(uid, ref stunnable)) + return; + + // "Optional" component. + Resolve(uid, ref speedModifier, false); + + time = AdjustTime(time, stunnable.SlowdownTimer, stunnable.SlowdownCap); + + if (time <= TimeSpan.Zero) + return; + + // Doesn't make much sense to have the "Slowdown" method speed up entities now does it? + walkSpeedMultiplier = Math.Clamp(walkSpeedMultiplier, 0f, 1f); + runSpeedMultiplier = Math.Clamp(runSpeedMultiplier, 0f, 1f); + + stunnable.WalkSpeedMultiplier *= walkSpeedMultiplier; + stunnable.RunSpeedMultiplier *= runSpeedMultiplier; + + stunnable.SlowdownTimer = (_gameTiming.CurTime, _gameTiming.CurTime + time); + + speedModifier?.RefreshMovementSpeedModifiers(); + + SetAlert(uid, stunnable, alerts); + stunnable.Dirty(); + } + + public void Reset(EntityUid uid, + StunnableComponent? stunnable = null, + MovementSpeedModifierComponent? speedModifier = null, + StandingStateComponent? standingState = null, + SharedAppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref stunnable)) + return; + + // Optional component. + Resolve(uid, ref speedModifier, false); + + stunnable.StunnedTimer = null; + stunnable.SlowdownTimer = null; + stunnable.KnockdownTimer = null; + + speedModifier?.RefreshMovementSpeedModifiers(); + _standingStateSystem.Stand(uid, standingState, appearance); + + stunnable.Dirty(); + } + + private void SetAlert(EntityUid uid, + StunnableComponent? stunnable = null, + SharedAlertsComponent? alerts = null) + { + // This method is really just optional, doesn't matter if the entity doesn't support alerts. + if (!Resolve(uid, ref stunnable, ref alerts, false)) + return; + + if (GetTimers(uid, stunnable) is not {} timers) + return; + + alerts.ShowAlert(AlertType.Stun, cooldown:timers); + } + + private (TimeSpan, TimeSpan)? GetTimers(EntityUid uid, StunnableComponent? stunnable = null) + { + if (!Resolve(uid, ref stunnable)) + return null; + + // Don't do anything if no stuns are applied. + if (!stunnable.AnyStunActive) + return null; + + TimeSpan start = TimeSpan.MaxValue, end = TimeSpan.MinValue; + + if (stunnable.StunnedTimer != null) + { + if (stunnable.StunnedTimer.Value.Start < start) + start = stunnable.StunnedTimer.Value.Start; + + if (stunnable.StunnedTimer.Value.End > end) + end = stunnable.StunnedTimer.Value.End; + } + + if (stunnable.KnockdownTimer != null) + { + if (stunnable.KnockdownTimer.Value.Start < start) + start = stunnable.KnockdownTimer.Value.Start; + + if (stunnable.KnockdownTimer.Value.End > end) + end = stunnable.KnockdownTimer.Value.End; + } + + if (stunnable.SlowdownTimer != null) + { + if (stunnable.SlowdownTimer.Value.Start < start) + start = stunnable.SlowdownTimer.Value.Start; + + if (stunnable.SlowdownTimer.Value.End > end) + end = stunnable.SlowdownTimer.Value.End; + } + + return (start, end); + } + + private void OnInteractHand(EntityUid uid, StunnableComponent stunnable, InteractHandEvent args) + { + if (args.Handled || stunnable.HelpTimer > 0f || !stunnable.KnockedDown) + return; + + // Set it to half the help interval so helping is actually useful... + stunnable.HelpTimer = stunnable.HelpInterval/2f; + + stunnable.KnockdownTimer = (stunnable.KnockdownTimer!.Value.Start, stunnable.KnockdownTimer.Value.End - TimeSpan.FromSeconds(stunnable.HelpInterval)); + + SoundSystem.Play(Filter.Pvs(uid), stunnable.StunAttemptSound.GetSound(), uid, AudioHelpers.WithVariation(0.05f)); + + SetAlert(uid, stunnable); + stunnable.Dirty(); + + args.Handled = true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _gameTiming.CurTime; + + foreach (var stunnable in EntityManager.EntityQuery()) + { + var uid = stunnable.Owner.Uid; + + if(stunnable.HelpTimer > 0f) + // If it goes negative, that's okay. + stunnable.HelpTimer -= frameTime; + + if (stunnable.StunnedTimer != null) + { + if (stunnable.StunnedTimer.Value.End <= curTime) + { + stunnable.StunnedTimer = null; + stunnable.Dirty(); + } + } + + if (stunnable.KnockdownTimer != null) + { + if (stunnable.KnockdownTimer.Value.End <= curTime) + { + stunnable.KnockdownTimer = null; + + // Try to stand up the mob... + _standingStateSystem.Stand(uid); + + stunnable.Dirty(); + } + } + + if (stunnable.SlowdownTimer != null) + { + if (stunnable.SlowdownTimer.Value.End <= curTime) + { + if (EntityManager.TryGetComponent(uid, out MovementSpeedModifierComponent? movement)) + movement.RefreshMovementSpeedModifiers(); + + + stunnable.SlowdownTimer = null; + stunnable.Dirty(); + } + } + + if (stunnable.AnyStunActive || !EntityManager.TryGetComponent(uid, out SharedAlertsComponent? status) + || !status.IsShowingAlert(AlertType.Stun)) + continue; + + status.ClearAlert(AlertType.Stun); + } + } + + #region Attempt Event Handling + + private void OnMoveAttempt(EntityUid uid, StunnableComponent stunnable, MovementAttemptEvent args) + { + if (stunnable.Stunned) + args.Cancel(); + } + + private void OnInteractAttempt(EntityUid uid, StunnableComponent stunnable, InteractionAttemptEvent args) + { + if(stunnable.Stunned) + args.Cancel(); + } + + private void OnUseAttempt(EntityUid uid, StunnableComponent stunnable, UseAttemptEvent args) + { + if(stunnable.Stunned) + args.Cancel(); + } + + private void OnThrowAttempt(EntityUid uid, StunnableComponent stunnable, ThrowAttemptEvent args) + { + if (stunnable.Stunned) + args.Cancel(); + } + + private void OnDropAttempt(EntityUid uid, StunnableComponent stunnable, DropAttemptEvent args) + { + if(stunnable.Stunned) + args.Cancel(); + } + + private void OnPickupAttempt(EntityUid uid, StunnableComponent stunnable, PickupAttemptEvent args) + { + if(stunnable.Stunned) + args.Cancel(); + } + + private void OnAttackAttempt(EntityUid uid, StunnableComponent stunnable, AttackAttemptEvent args) + { + if(stunnable.Stunned) + args.Cancel(); + } + + private void OnEquipAttempt(EntityUid uid, StunnableComponent stunnable, EquipAttemptEvent args) + { + if(stunnable.Stunned) + args.Cancel(); + } + + private void OnUnequipAttempt(EntityUid uid, StunnableComponent stunnable, UnequipAttemptEvent args) + { + if(stunnable.Stunned) + args.Cancel(); + } + + private void OnStandAttempt(EntityUid uid, StunnableComponent stunnable, StandAttemptEvent args) + { + if(stunnable.KnockedDown) + args.Cancel(); + } + + #endregion + + } +} diff --git a/Content.Shared/Stunnable/SharedStunnableComponent.cs b/Content.Shared/Stunnable/SharedStunnableComponent.cs deleted file mode 100644 index 0ca20b8b77..0000000000 --- a/Content.Shared/Stunnable/SharedStunnableComponent.cs +++ /dev/null @@ -1,388 +0,0 @@ -using System; -using System.Threading; -using Content.Shared.ActionBlocker; -using Content.Shared.Alert; -using Content.Shared.Interaction; -using Content.Shared.Movement.Components; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.IoC; -using Robust.Shared.Players; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Timing; -using Robust.Shared.ViewVariables; - -namespace Content.Shared.Stunnable -{ - [NetworkedComponent()] - public abstract class SharedStunnableComponent : Component, IMoveSpeedModifier, IActionBlocker, IInteractHand - { - [Dependency] private readonly IGameTiming _gameTiming = default!; - - public sealed override string Name => "Stunnable"; - - public (TimeSpan Start, TimeSpan End)? StunnedTimer { get; protected set; } - public (TimeSpan Start, TimeSpan End)? KnockdownTimer { get; protected set; } - public (TimeSpan Start, TimeSpan End)? SlowdownTimer { get; protected set; } - - [ViewVariables] public float StunnedSeconds => - StunnedTimer == null ? 0f : (float)(StunnedTimer.Value.End - StunnedTimer.Value.Start).TotalSeconds; - [ViewVariables] public float KnockdownSeconds => - KnockdownTimer == null ? 0f : (float)(KnockdownTimer.Value.End - KnockdownTimer.Value.Start).TotalSeconds; - [ViewVariables] public float SlowdownSeconds => - SlowdownTimer == null ? 0f : (float)(SlowdownTimer.Value.End - SlowdownTimer.Value.Start).TotalSeconds; - - [ViewVariables] public bool AnyStunActive => Stunned || KnockedDown || SlowedDown; - [ViewVariables] public bool Stunned => StunnedTimer != null; - [ViewVariables] public bool KnockedDown => KnockdownTimer != null; - [ViewVariables] public bool SlowedDown => SlowdownTimer != null; - - [DataField("stunCap")] - protected float _stunCap = 20f; - - [DataField("knockdownCap")] - protected float _knockdownCap = 20f; - - [DataField("slowdownCap")] - protected float _slowdownCap = 20f; - - [DataField("helpInterval")] - private float _helpInterval = 1f; - - private bool _canHelp = true; - - protected CancellationTokenSource StatusRemoveCancellation = new(); - - [ViewVariables] protected float WalkModifierOverride = 0f; - [ViewVariables] protected float RunModifierOverride = 0f; - - private float StunTimeModifier - { - get - { - var modifier = 1.0f; - var components = Owner.GetAllComponents(); - - foreach (var component in components) - { - modifier *= component.StunTimeModifier; - } - - return modifier; - } - } - - private float KnockdownTimeModifier - { - get - { - var modifier = 1.0f; - var components = Owner.GetAllComponents(); - - foreach (var component in components) - { - modifier *= component.KnockdownTimeModifier; - } - - return modifier; - } - } - - private float SlowdownTimeModifier - { - get - { - var modifier = 1.0f; - var components = Owner.GetAllComponents(); - - foreach (var component in components) - { - modifier *= component.SlowdownTimeModifier; - } - - return modifier; - } - } - - /// - /// Stuns the entity, disallowing it from doing many interactions temporarily. - /// - /// How many seconds the mob will stay stunned. - /// Whether or not the owner was stunned. - public bool Stun(float seconds) - { - seconds = MathF.Min(StunnedSeconds + (seconds * StunTimeModifier), _stunCap); - - if (seconds <= 0f) - { - return false; - } - - StunnedTimer = (_gameTiming.CurTime, _gameTiming.CurTime.Add(TimeSpan.FromSeconds(seconds))); - - SetAlert(); - OnStun(); - - Dirty(); - - return true; - } - - protected virtual void OnStun() { } - - /// - /// Knocks down the mob, making it fall to the ground. - /// - /// How many seconds the mob will stay on the ground. - /// Whether or not the owner was knocked down. - public bool Knockdown(float seconds) - { - seconds = MathF.Min(KnockdownSeconds + (seconds * KnockdownTimeModifier), _knockdownCap); - - if (seconds <= 0f) - { - return false; - } - - KnockdownTimer = (_gameTiming.CurTime, _gameTiming.CurTime.Add(TimeSpan.FromSeconds(seconds)));; - - SetAlert(); - OnKnockdown(); - - Dirty(); - - return true; - } - - protected virtual void OnKnockdown() { } - - /// - /// Applies knockdown and stun to the mob temporarily. - /// - /// How many seconds the mob will be paralyzed- - /// Whether or not the owner of this component was paralyzed- - public bool Paralyze(float seconds) - { - return Stun(seconds) && Knockdown(seconds); - } - - /// - /// Slows down the mob's walking/running speed temporarily - /// - /// How many seconds the mob will be slowed down - /// Walk speed modifier. Set to 0 or negative for default value. (0.5f) - /// Run speed modifier. Set to 0 or negative for default value. (0.5f) - public void Slowdown(float seconds, float walkModifierOverride = 0f, float runModifierOverride = 0f) - { - seconds = MathF.Min(SlowdownSeconds + (seconds * SlowdownTimeModifier), _slowdownCap); - - if (seconds <= 0f) - return; - - WalkModifierOverride = walkModifierOverride; - RunModifierOverride = runModifierOverride; - - SlowdownTimer = (_gameTiming.CurTime, _gameTiming.CurTime.Add(TimeSpan.FromSeconds(seconds))); - - if (Owner.TryGetComponent(out MovementSpeedModifierComponent? movement)) - movement.RefreshMovementSpeedModifiers(); - - SetAlert(); - Dirty(); - } - - private (TimeSpan, TimeSpan)? GetTimers() - { - // Don't do anything if no stuns are applied. - if (!AnyStunActive) - return null; - - TimeSpan start = TimeSpan.MaxValue, end = TimeSpan.MinValue; - - if (StunnedTimer != null) - { - if (StunnedTimer.Value.Start < start) - start = StunnedTimer.Value.Start; - - if (StunnedTimer.Value.End > end) - end = StunnedTimer.Value.End; - } - - if (KnockdownTimer != null) - { - if (KnockdownTimer.Value.Start < start) - start = KnockdownTimer.Value.Start; - - if (KnockdownTimer.Value.End > end) - end = KnockdownTimer.Value.End; - } - - if (SlowdownTimer != null) - { - if (SlowdownTimer.Value.Start < start) - start = SlowdownTimer.Value.Start; - - if (SlowdownTimer.Value.End > end) - end = SlowdownTimer.Value.End; - } - - return (start, end); - } - - private void SetAlert() - { - if (!Owner.TryGetComponent(out SharedAlertsComponent? status)) - { - return; - } - - var timers = GetTimers(); - - if (timers == null) - return; - - status.ShowAlert(AlertType.Stun, cooldown:timers); - StatusRemoveCancellation.Cancel(); - StatusRemoveCancellation = new CancellationTokenSource(); - } - - protected virtual void OnInteractHand() { } - - bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) - { - if (!_canHelp || !KnockedDown) - { - return false; - } - - _canHelp = false; - Owner.SpawnTimer((int) _helpInterval * 1000, () => _canHelp = true); - - KnockdownTimer = (KnockdownTimer!.Value.Start, KnockdownTimer.Value.End.Subtract(TimeSpan.FromSeconds(_helpInterval))); - - OnInteractHand(); - - SetAlert(); - Dirty(); - - return true; - } - - public override ComponentState GetComponentState(ICommonSession player) - { - return new StunnableComponentState(StunnedTimer, KnockdownTimer, SlowdownTimer, WalkModifierOverride, RunModifierOverride); - } - - protected virtual void OnKnockdownEnd() - { - } - - public void Update(float delta) - { - var curTime = _gameTiming.CurTime; - - if (StunnedTimer != null) - { - if (StunnedTimer.Value.End <= curTime) - { - StunnedTimer = null; - Dirty(); - } - } - - if (KnockdownTimer != null) - { - if (KnockdownTimer.Value.End <= curTime) - { - OnKnockdownEnd(); - - KnockdownTimer = null; - Dirty(); - } - } - - if (SlowdownTimer != null) - { - if (SlowdownTimer.Value.End <= curTime) - { - if (Owner.TryGetComponent(out MovementSpeedModifierComponent? movement)) - { - movement.RefreshMovementSpeedModifiers(); - } - - SlowdownTimer = null; - Dirty(); - } - } - - if (AnyStunActive || !Owner.TryGetComponent(out SharedAlertsComponent? status) || !status.IsShowingAlert(AlertType.Stun)) - return; - - status.ClearAlert(AlertType.Stun); - } - - #region ActionBlockers - public bool CanInteract() => (!Stunned); - - public bool CanUse() => (!Stunned); - - public bool CanThrow() => (!Stunned); - - public bool CanSpeak() => true; - - public bool CanDrop() => (!Stunned); - - public bool CanPickup() => (!Stunned); - - public bool CanEmote() => true; - - public bool CanAttack() => (!Stunned); - - public bool CanEquip() => (!Stunned); - - public bool CanUnequip() => (!Stunned); - public bool CanChangeDirection() => true; - - public bool CanShiver() => !Stunned; - public bool CanSweat() => true; - - #endregion - - [ViewVariables] - public float WalkSpeedModifier => (SlowedDown ? (WalkModifierOverride <= 0f ? 0.5f : WalkModifierOverride) : 1f); - [ViewVariables] - public float SprintSpeedModifier => (SlowedDown ? (RunModifierOverride <= 0f ? 0.5f : RunModifierOverride) : 1f); - - [Serializable, NetSerializable] - protected sealed class StunnableComponentState : ComponentState - { - public (TimeSpan Start, TimeSpan End)? StunnedTimer { get; } - public (TimeSpan Start, TimeSpan End)? KnockdownTimer { get; } - public (TimeSpan Start, TimeSpan End)? SlowdownTimer { get; } - public float WalkModifierOverride { get; } - public float RunModifierOverride { get; } - - public StunnableComponentState( - (TimeSpan Start, TimeSpan End)? stunnedTimer, (TimeSpan Start, TimeSpan End)? knockdownTimer, - (TimeSpan Start, TimeSpan End)? slowdownTimer, float walkModifierOverride, float runModifierOverride) - { - StunnedTimer = stunnedTimer; - KnockdownTimer = knockdownTimer; - SlowdownTimer = slowdownTimer; - WalkModifierOverride = walkModifierOverride; - RunModifierOverride = runModifierOverride; - } - } - } - - /// - /// This interface allows components to multiply the time in seconds of various stuns by a number. - /// - public interface IStunModifier - { - float StunTimeModifier => 1.0f; - float KnockdownTimeModifier => 1.0f; - float SlowdownTimeModifier => 1.0f; - } -} diff --git a/Content.Shared/Stunnable/StunSystem.cs b/Content.Shared/Stunnable/StunSystem.cs deleted file mode 100644 index e73c8d7123..0000000000 --- a/Content.Shared/Stunnable/StunSystem.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Movement; -using JetBrains.Annotations; -using Robust.Shared.GameObjects; - -namespace Content.Shared.Stunnable -{ - [UsedImplicitly] - internal sealed class StunSystem : EntitySystem - { - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(HandleMoveAttempt); - } - - private void HandleMoveAttempt(EntityUid uid, SharedStunnableComponent component, MovementAttemptEvent args) - { - if (component.Stunned) - args.Cancel(); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - foreach (var component in EntityManager.EntityQuery(true)) - { - component.Update(frameTime); - } - } - } -} diff --git a/Content.Shared/Stunnable/StunnableComponent.cs b/Content.Shared/Stunnable/StunnableComponent.cs new file mode 100644 index 0000000000..392d111bba --- /dev/null +++ b/Content.Shared/Stunnable/StunnableComponent.cs @@ -0,0 +1,96 @@ +using System; +using Content.Shared.Movement.Components; +using Content.Shared.Sound; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.Stunnable +{ + [Friend(typeof(SharedStunSystem))] + [RegisterComponent, NetworkedComponent] + public sealed class StunnableComponent : Component, IMoveSpeedModifier + { + public sealed override string Name => "Stunnable"; + + public (TimeSpan Start, TimeSpan End)? StunnedTimer { get; set; } + public (TimeSpan Start, TimeSpan End)? KnockdownTimer { get; set; } + public (TimeSpan Start, TimeSpan End)? SlowdownTimer { get; set; } + + [ViewVariables] + public float StunnedSeconds => + StunnedTimer == null ? 0f : (float)(StunnedTimer.Value.End - StunnedTimer.Value.Start).TotalSeconds; + + [ViewVariables] + public float KnockdownSeconds => + KnockdownTimer == null ? 0f : (float)(KnockdownTimer.Value.End - KnockdownTimer.Value.Start).TotalSeconds; + + [ViewVariables] + public float SlowdownSeconds => + SlowdownTimer == null ? 0f : (float)(SlowdownTimer.Value.End - SlowdownTimer.Value.Start).TotalSeconds; + + [ViewVariables] + public bool AnyStunActive => Stunned || KnockedDown || SlowedDown; + + [ViewVariables] + public bool Stunned => StunnedTimer != null; + + [ViewVariables] + public bool KnockedDown => KnockdownTimer != null; + + [ViewVariables] + public bool SlowedDown => SlowdownTimer != null; + + [DataField("stunCap")] + public float StunCap { get; set; } = 20f; + + [DataField("knockdownCap")] + public float KnockdownCap { get; set; } = 20f; + + [DataField("slowdownCap")] + public float SlowdownCap { get; set; } = 20f; + + [DataField("helpInterval")] + public float HelpInterval { get; set; } = 1f; + + [DataField("stunAttemptSound")] + public SoundSpecifier StunAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); + + [ViewVariables] + public float HelpTimer { get; set; } = 0f; + + [ViewVariables] + public float WalkSpeedMultiplier { get; set; } = 0f; + [ViewVariables] + public float RunSpeedMultiplier { get; set; } = 0f; + + [ViewVariables] + public float WalkSpeedModifier => (SlowedDown ? (WalkSpeedMultiplier <= 0f ? 0.5f : WalkSpeedMultiplier) : 1f); + [ViewVariables] + public float SprintSpeedModifier => (SlowedDown ? (RunSpeedMultiplier <= 0f ? 0.5f : RunSpeedMultiplier) : 1f); + } + + [Serializable, NetSerializable] + public sealed class StunnableComponentState : ComponentState + { + public (TimeSpan Start, TimeSpan End)? StunnedTimer { get; } + public (TimeSpan Start, TimeSpan End)? KnockdownTimer { get; } + public (TimeSpan Start, TimeSpan End)? SlowdownTimer { get; } + public float WalkSpeedMultiplier { get; } + public float RunSpeedMultiplier { get; } + + public StunnableComponentState( + (TimeSpan Start, TimeSpan End)? stunnedTimer, (TimeSpan Start, TimeSpan End)? knockdownTimer, + (TimeSpan Start, TimeSpan End)? slowdownTimer, float walkSpeedMultiplier, float runSpeedMultiplier) + { + StunnedTimer = stunnedTimer; + KnockdownTimer = knockdownTimer; + SlowdownTimer = slowdownTimer; + WalkSpeedMultiplier = walkSpeedMultiplier; + RunSpeedMultiplier = runSpeedMultiplier; + } + } +} diff --git a/Content.Shared/Tabletop/SharedTabletopSystem.cs b/Content.Shared/Tabletop/SharedTabletopSystem.cs index ac2ea6ef55..a80051257a 100644 --- a/Content.Shared/Tabletop/SharedTabletopSystem.cs +++ b/Content.Shared/Tabletop/SharedTabletopSystem.cs @@ -51,7 +51,7 @@ namespace Content.Shared.Tabletop protected static bool StunnedOrNoHands(IEntity playerEntity) { - var stunned = playerEntity.TryGetComponent(out var stun) && + var stunned = playerEntity.TryGetComponent(out var stun) && stun.Stunned; var hasHand = playerEntity.TryGetComponent(out var handsComponent) && handsComponent.Hands.Count > 0;