diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index 2187cbd68f..4cd1f95288 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -22,6 +22,7 @@ using Content.Shared.Administration; using Content.Shared.Administration.Components; using Content.Shared.Body.Components; using Content.Shared.Body.Part; +using Content.Shared.Clumsy; using Content.Shared.Clothing.Components; using Content.Shared.Cluwne; using Content.Shared.Damage; diff --git a/Content.Server/Administration/Systems/SuperBonkSystem.cs b/Content.Server/Administration/Systems/SuperBonkSystem.cs index 5488a8d6f4..5cd62d8357 100644 --- a/Content.Server/Administration/Systems/SuperBonkSystem.cs +++ b/Content.Server/Administration/Systems/SuperBonkSystem.cs @@ -1,27 +1,27 @@ using Content.Server.Administration.Components; using Content.Shared.Climbing.Components; -using Content.Shared.Climbing.Events; -using Content.Shared.Climbing.Systems; -using Content.Shared.Interaction.Components; +using Content.Shared.Clumsy; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; +using Robust.Shared.Audio.Systems; namespace Content.Server.Administration.Systems; -public sealed class SuperBonkSystem: EntitySystem +public sealed class SuperBonkSystem : EntitySystem { [Dependency] private readonly SharedTransformSystem _transformSystem = default!; - [Dependency] private readonly BonkSystem _bonkSystem = default!; + [Dependency] private readonly ClumsySystem _clumsySystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnBonkShutdown); SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnBonkShutdown); } - public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false ) + public void StartSuperBonk(EntityUid target, float delay = 0.1f, bool stopWhenDead = false) { //The other check in the code to stop when the target dies does not work if the target is already dead. @@ -31,7 +31,6 @@ public sealed class SuperBonkSystem: EntitySystem return; } - var hadClumsy = EnsureComp(target, out _); var tables = EntityQueryEnumerator(); @@ -79,16 +78,17 @@ public sealed class SuperBonkSystem: EntitySystem private void Bonk(SuperBonkComponent comp) { var uid = comp.Tables.Current.Key; - var bonkComp = comp.Tables.Current.Value; // It would be very weird for something without a transform component to have a bonk component // but just in case because I don't want to crash the server. - if (!HasComp(uid)) + if (!HasComp(uid) || !TryComp(comp.Target, out var clumsyComp)) return; _transformSystem.SetCoordinates(comp.Target, Transform(uid).Coordinates); - _bonkSystem.TryBonk(comp.Target, uid, bonkComp); + _clumsySystem.HitHeadClumsy((comp.Target, clumsyComp), uid); + + _audioSystem.PlayPvs(clumsyComp.TableBonkSound, comp.Target); } private void OnMobStateChanged(EntityUid uid, SuperBonkComponent comp, MobStateChangedEvent args) diff --git a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs index 9b78e81aa0..8345aa3c3d 100644 --- a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs +++ b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.Hypospray.Events; using Content.Shared.Chemistry; using Content.Shared.Database; using Content.Shared.FixedPoint; @@ -85,14 +86,44 @@ public sealed class HypospraySystem : SharedHypospraySystem string? msgFormat = null; - if (target == user) - msgFormat = "hypospray-component-inject-self-message"; - else if (EligibleEntity(user, EntityManager, component) && _interaction.TryRollClumsy(user, component.ClumsyFailChance)) + // Self event + var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target); + RaiseLocalEvent(user, selfEvent); + + if (selfEvent.Cancelled) { - msgFormat = "hypospray-component-inject-self-clumsy-message"; - target = user; + _popup.PopupEntity(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); + return false; } + target = selfEvent.TargetGettingInjected; + + if (!EligibleEntity(target, EntityManager, component)) + return false; + + // Target event + var targetEvent = new TargetBeforeHyposprayInjectsEvent(user, entity.Owner, target); + RaiseLocalEvent(target, targetEvent); + + if (targetEvent.Cancelled) + { + _popup.PopupEntity(Loc.GetString(targetEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); + return false; + } + + target = targetEvent.TargetGettingInjected; + + if (!EligibleEntity(target, EntityManager, component)) + return false; + + // The target event gets priority for the overriden message. + if (targetEvent.InjectMessageOverride != null) + msgFormat = targetEvent.InjectMessageOverride; + else if (selfEvent.InjectMessageOverride != null) + msgFormat = selfEvent.InjectMessageOverride; + else if (target == user) + msgFormat = "hypospray-component-inject-self-message"; + if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0) { _popup.PopupEntity(Loc.GetString("hypospray-component-empty-message"), target, user); diff --git a/Content.Server/Cluwne/CluwneSystem.cs b/Content.Server/Cluwne/CluwneSystem.cs index f24f0143f3..1fbb0c7269 100644 --- a/Content.Server/Cluwne/CluwneSystem.cs +++ b/Content.Server/Cluwne/CluwneSystem.cs @@ -16,6 +16,7 @@ using Content.Shared.Cluwne; using Content.Shared.Interaction.Components; using Robust.Shared.Audio.Systems; using Content.Shared.NameModifier.EntitySystems; +using Content.Shared.Clumsy; namespace Content.Server.Cluwne; diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs index c9cb6cc58d..fa0ea26385 100644 --- a/Content.Server/Medical/DefibrillatorSystem.cs +++ b/Content.Server/Medical/DefibrillatorSystem.cs @@ -77,7 +77,20 @@ public sealed class DefibrillatorSystem : EntitySystem Zap(uid, target, args.User, component); } - public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null) + /// + /// Checks if you can actually defib a target. + /// + /// Uid of the defib + /// Uid of the target getting defibbed + /// Uid of the entity using the defibrillator + /// Defib component + /// + /// If true, the target can be alive. If false, the function will check if the target is alive and will return false if they are. + /// + /// + /// Returns true if the target is valid to be defibed, false otherwise. + /// + public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null, bool targetCanBeAlive = false) { if (!Resolve(uid, ref component)) return false; @@ -98,15 +111,25 @@ public sealed class DefibrillatorSystem : EntitySystem if (!_powerCell.HasActivatableCharge(uid, user: user)) return false; - if (_mobState.IsAlive(target, mobState)) + if (!targetCanBeAlive && _mobState.IsAlive(target, mobState)) return false; - if (!component.CanDefibCrit && _mobState.IsCritical(target, mobState)) + if (!targetCanBeAlive && !component.CanDefibCrit && _mobState.IsCritical(target, mobState)) return false; return true; } + /// + /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after. + /// + /// Uid of the defib + /// Uid of the target getting defibbed + /// Uid of the entity using the defibrillator + /// Defib component + /// + /// Returns true if the defibrillation do-after started, otherwise false. + /// public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null) { if (!Resolve(uid, ref component)) @@ -118,27 +141,44 @@ public sealed class DefibrillatorSystem : EntitySystem _audio.PlayPvs(component.ChargeSound, uid); return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, component.DoAfterDuration, new DefibrillatorZapDoAfterEvent(), uid, target, uid) - { - NeedHand = true, - BreakOnMove = !component.AllowDoAfterMovement - }); + { + NeedHand = true, + BreakOnMove = !component.AllowDoAfterMovement + }); } - public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null, MobStateComponent? mob = null, MobThresholdsComponent? thresholds = null) + /// + /// Tries to defibrillate the target with the given defibrillator. + /// + public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null) { - if (!Resolve(uid, ref component) || !Resolve(target, ref mob, ref thresholds, false)) + if (!Resolve(uid, ref component)) return; - // clowns zap themselves - if (HasComp(user) && user != target) - { - Zap(uid, user, user, component); - return; - } - if (!_powerCell.TryUseActivatableCharge(uid, user: user)) return; + var selfEvent = new SelfBeforeDefibrillatorZapsEvent(user, uid, target); + RaiseLocalEvent(user, selfEvent); + + target = selfEvent.DefibTarget; + + // Ensure thet new target is still valid. + if (selfEvent.Cancelled || !CanZap(uid, target, user, component, true)) + return; + + var targetEvent = new TargetBeforeDefibrillatorZapsEvent(user, uid, target); + RaiseLocalEvent(target, targetEvent); + + target = targetEvent.DefibTarget; + + if (targetEvent.Cancelled || !CanZap(uid, target, user, component, true)) + return; + + if (!TryComp(target, out var mob) || + !TryComp(target, out var thresholds)) + return; + _audio.PlayPvs(component.ZapSound, uid); _electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true); component.NextZapTime = _timing.CurTime + component.ZapDelay; diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 701753a8ce..d22b5ec2af 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -1,15 +1,12 @@ using System.Linq; using System.Numerics; using Content.Server.Cargo.Systems; -using Content.Server.Interaction; using Content.Server.Power.EntitySystems; -using Content.Server.Stunnable; using Content.Server.Weapons.Ranged.Components; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Effects; -using Content.Shared.Interaction.Components; using Content.Shared.Projectiles; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Ranged; @@ -33,16 +30,13 @@ public sealed partial class GunSystem : SharedGunSystem [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!; - [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StaminaSystem _stamina = default!; - [Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly SharedContainerSystem _container = default!; private const float DamagePitchVariation = 0.05f; - public const float GunClumsyChance = 0.5f; public override void Initialize() { @@ -71,26 +65,14 @@ public sealed partial class GunSystem : SharedGunSystem { userImpulse = true; - // Try a clumsy roll - // TODO: Who put this here - if (TryComp(user, out var clumsy) && gun.ClumsyProof == false) + if (user != null) { - for (var i = 0; i < ammo.Count; i++) + var selfEvent = new SelfBeforeGunShotEvent(user.Value, (gunUid, gun), ammo); + RaiseLocalEvent(user.Value, selfEvent); + if (selfEvent.Cancelled) { - if (_interaction.TryRollClumsy(user.Value, GunClumsyChance, clumsy)) - { - // Wound them - Damageable.TryChangeDamage(user, clumsy.ClumsyDamage, origin: user); - _stun.TryParalyze(user.Value, TimeSpan.FromSeconds(3f), true); - - // Apply salt to the wound ("Honk!") - Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"), gunUid); - Audio.PlayPvs(clumsy.ClumsySound, gunUid); - - PopupSystem.PopupEntity(Loc.GetString("gun-clumsy"), user.Value); - userImpulse = false; - return; - } + userImpulse = false; + return; } } diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs index 05ef84bbaf..17d52f0ad9 100644 --- a/Content.Shared/Chemistry/Components/HyposprayComponent.cs +++ b/Content.Shared/Chemistry/Components/HyposprayComponent.cs @@ -11,11 +11,6 @@ public sealed partial class HyposprayComponent : Component [DataField] public string SolutionName = "hypospray"; - // TODO: This should be on clumsycomponent. - [DataField] - [ViewVariables(VVAccess.ReadWrite)] - public float ClumsyFailChance = 0.5f; - [DataField] [ViewVariables(VVAccess.ReadWrite)] public FixedPoint2 TransferAmount = FixedPoint2.New(5); diff --git a/Content.Shared/Chemistry/Events/HyposprayEvents.cs b/Content.Shared/Chemistry/Events/HyposprayEvents.cs new file mode 100644 index 0000000000..e8ed081a57 --- /dev/null +++ b/Content.Shared/Chemistry/Events/HyposprayEvents.cs @@ -0,0 +1,38 @@ +using Content.Shared.Inventory; + +namespace Content.Shared.Chemistry.Hypospray.Events; + +public abstract partial class BeforeHyposprayInjectsTargetEvent : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public EntityUid EntityUsingHypospray; + public readonly EntityUid Hypospray; + public EntityUid TargetGettingInjected; + public string? InjectMessageOverride; + + public BeforeHyposprayInjectsTargetEvent(EntityUid user, EntityUid hypospray, EntityUid target) + { + EntityUsingHypospray = user; + Hypospray = hypospray; + TargetGettingInjected = target; + InjectMessageOverride = null; + } +} + +/// +/// This event is raised on the user using the hypospray before the hypospray is injected. +/// The event is triggered on the user and all their clothing. +/// +public sealed class SelfBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent +{ + public SelfBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } +} + +/// +/// This event is raised on the target before the hypospray is injected. +/// The event is triggered on the target itself and all its clothing. +/// +public sealed class TargetBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent +{ + public TargetBeforeHyposprayInjectsEvent (EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } +} diff --git a/Content.Shared/Climbing/Components/BonkableComponent.cs b/Content.Shared/Climbing/Components/BonkableComponent.cs index 5e97396fba..cb4839cae7 100644 --- a/Content.Shared/Climbing/Components/BonkableComponent.cs +++ b/Content.Shared/Climbing/Components/BonkableComponent.cs @@ -1,5 +1,4 @@ using Content.Shared.Damage; -using Robust.Shared.Audio; using Robust.Shared.GameStates; namespace Content.Shared.Climbing.Components; @@ -8,39 +7,18 @@ namespace Content.Shared.Climbing.Components; /// Makes entity do damage and stun entities with ClumsyComponent /// upon DragDrop or Climb interactions. /// -[RegisterComponent, NetworkedComponent, Access(typeof(Systems.BonkSystem))] +[RegisterComponent, NetworkedComponent] public sealed partial class BonkableComponent : Component { /// - /// Chance of bonk triggering if the user is clumsy. + /// How long to stun players on bonk, in seconds. /// - [DataField("bonkClumsyChance")] - public float BonkClumsyChance = 0.5f; + [DataField] + public TimeSpan BonkTime = TimeSpan.FromSeconds(2); /// - /// Sound to play when bonking. + /// How much damage to apply on bonk. /// - /// - [DataField("bonkSound")] - public SoundSpecifier? BonkSound; - - /// - /// How long to stun players on bonk, in seconds. - /// - /// - [DataField("bonkTime")] - public float BonkTime = 2; - - /// - /// How much damage to apply on bonk. - /// - /// - [DataField("bonkDamage")] + [DataField] public DamageSpecifier? BonkDamage; - - /// - /// How long it takes to bonk. - /// - [DataField("bonkDelay")] - public float BonkDelay = 1.5f; } diff --git a/Content.Shared/Climbing/Events/BeforeClimbEvents.cs b/Content.Shared/Climbing/Events/BeforeClimbEvents.cs new file mode 100644 index 0000000000..85c40f9427 --- /dev/null +++ b/Content.Shared/Climbing/Events/BeforeClimbEvents.cs @@ -0,0 +1,36 @@ +using Content.Shared.Inventory; +using Content.Shared.Climbing.Components; + +namespace Content.Shared.Climbing.Events; + +public abstract partial class BeforeClimbEvent : CancellableEntityEventArgs +{ + public readonly EntityUid GettingPutOnTable; + public readonly EntityUid PuttingOnTable; + public readonly Entity BeingClimbedOn; + + public BeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity beingClimbedOn) + { + GettingPutOnTable = gettingPutOntable; + PuttingOnTable = puttingOnTable; + BeingClimbedOn = beingClimbedOn; + } +} + +/// +/// This event is raised on the the person either getting put on or going on the table. +/// The event is also called on their clothing as well. +/// +public sealed class SelfBeforeClimbEvent : BeforeClimbEvent, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public SelfBeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity beingClimbedOn) : base(gettingPutOntable, puttingOnTable, beingClimbedOn) { } +} + +/// +/// This event is raised on the thing being climbed on. +/// +public sealed class TargetBeforeClimbEvent : BeforeClimbEvent +{ + public TargetBeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity beingClimbedOn) : base(gettingPutOntable, puttingOnTable, beingClimbedOn) { } +} diff --git a/Content.Shared/Climbing/Systems/BonkSystem.cs b/Content.Shared/Climbing/Systems/BonkSystem.cs deleted file mode 100644 index f59fe92573..0000000000 --- a/Content.Shared/Climbing/Systems/BonkSystem.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Content.Shared.CCVar; -using Content.Shared.Climbing.Components; -using Content.Shared.Climbing.Events; -using Content.Shared.Damage; -using Content.Shared.DoAfter; -using Content.Shared.DragDrop; -using Content.Shared.Hands.Components; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Components; -using Content.Shared.Popups; -using Content.Shared.Stunnable; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Configuration; -using Robust.Shared.Player; -using Robust.Shared.Serialization; - -namespace Content.Shared.Climbing.Systems; - -public sealed partial class BonkSystem : EntitySystem -{ - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - [Dependency] private readonly SharedStunSystem _stunSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnBonkDoAfter); - SubscribeLocalEvent(OnAttemptClimb); - } - - private void OnBonkDoAfter(EntityUid uid, BonkableComponent component, BonkDoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Args.Used == null) - return; - - TryBonk(args.Args.Used.Value, uid, component, source: args.Args.User); - - args.Handled = true; - } - - - public bool TryBonk(EntityUid user, EntityUid bonkableUid, BonkableComponent? bonkableComponent = null, EntityUid? source = null) - { - if (!Resolve(bonkableUid, ref bonkableComponent, false)) - return false; - - // BONK! - var userName = Identity.Entity(user, EntityManager); - var bonkableName = Identity.Entity(bonkableUid, EntityManager); - - if (user == source) - { - // Non-local, non-bonking players - var othersMessage = Loc.GetString("bonkable-success-message-others", ("user", userName), ("bonkable", bonkableName)); - // Local, bonking player - var selfMessage = Loc.GetString("bonkable-success-message-user", ("user", userName), ("bonkable", bonkableName)); - - _popupSystem.PopupPredicted(selfMessage, othersMessage, user, user); - } - else if (source != null) - { - // Local, non-bonking player (dragger) - _popupSystem.PopupClient(Loc.GetString("bonkable-success-message-others", ("user", userName), ("bonkable", bonkableName)), user, source.Value); - // Non-local, non-bonking players - _popupSystem.PopupEntity(Loc.GetString("bonkable-success-message-others", ("user", userName), ("bonkable", bonkableName)), user, Filter.Pvs(user).RemoveWhereAttachedEntity(e => e == user || e == source.Value), true); - // Non-local, bonking player - _popupSystem.PopupEntity(Loc.GetString("bonkable-success-message-user", ("user", userName), ("bonkable", bonkableName)), user, user); - } - - - - if (source != null) - _audioSystem.PlayPredicted(bonkableComponent.BonkSound, bonkableUid, source); - else - _audioSystem.PlayPvs(bonkableComponent.BonkSound, bonkableUid); - - _stunSystem.TryParalyze(user, TimeSpan.FromSeconds(bonkableComponent.BonkTime), true); - - if (bonkableComponent.BonkDamage is { } bonkDmg) - _damageableSystem.TryChangeDamage(user, bonkDmg, true, origin: user); - - return true; - - } - - private bool TryStartBonk(EntityUid uid, EntityUid user, EntityUid climber, BonkableComponent? bonkableComponent = null) - { - if (!Resolve(uid, ref bonkableComponent, false)) - return false; - - if (!HasComp(climber) || !HasComp(user)) - return false; - - if (!_cfg.GetCVar(CCVars.GameTableBonk)) - { - // Not set to always bonk, try clumsy roll. - if (!_interactionSystem.TryRollClumsy(climber, bonkableComponent.BonkClumsyChance)) - return false; - } - - var doAfterArgs = new DoAfterArgs(EntityManager, user, bonkableComponent.BonkDelay, new BonkDoAfterEvent(), uid, target: uid, used: climber) - { - BreakOnMove = true, - BreakOnDamage = true, - DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget - }; - - return _doAfter.TryStartDoAfter(doAfterArgs); - } - - private void OnAttemptClimb(EntityUid uid, BonkableComponent component, ref AttemptClimbEvent args) - { - if (args.Cancelled) - return; - - if (TryStartBonk(uid, args.User, args.Climber, component)) - args.Cancelled = true; - } - - [Serializable, NetSerializable] - private sealed partial class BonkDoAfterEvent : SimpleDoAfterEvent - { - } -} diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index da194706f8..9bf481002a 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -251,6 +251,18 @@ public sealed partial class ClimbSystem : VirtualController if (!Resolve(climbable, ref comp, false)) return; + var selfEvent = new SelfBeforeClimbEvent(uid, user, (climbable, comp)); + RaiseLocalEvent(uid, selfEvent); + + if (selfEvent.Cancelled) + return; + + var targetEvent = new TargetBeforeClimbEvent(uid, user, (climbable, comp)); + RaiseLocalEvent(climbable, targetEvent); + + if (targetEvent.Cancelled) + return; + if (!ReplaceFixtures(uid, climbing, fixtures)) return; diff --git a/Content.Shared/Clumsy/ClumsyComponent.cs b/Content.Shared/Clumsy/ClumsyComponent.cs new file mode 100644 index 0000000000..c71f5d0008 --- /dev/null +++ b/Content.Shared/Clumsy/ClumsyComponent.cs @@ -0,0 +1,61 @@ +using Content.Shared.Damage; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Clumsy; + +/// +/// A simple clumsy tag-component. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ClumsyComponent : Component +{ + + // Standard options. Try to fit these in if you can! + + /// + /// Sound to play when clumsy interactions fail. + /// + [DataField] + public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); + + /// + /// Default chance to fail a clumsy interaction. + /// If a system needs to use something else, add a new variable in the component, do not modify this percentage. + /// + [DataField, AutoNetworkedField] + public float ClumsyDefaultCheck = 0.5f; + + /// + /// Default stun time. + /// If a system needs to use something else, add a new variable in the component, do not modify this number. + /// + [DataField, AutoNetworkedField] + public TimeSpan ClumsyDefaultStunTime = TimeSpan.FromSeconds(2.5); + + // Specific options + + /// + /// Sound to play after hitting your head on a table. Ouch! + /// + [DataField] + public SoundCollectionSpecifier TableBonkSound = new SoundCollectionSpecifier("TrayHit"); + + /// + /// Stun time after failing to shoot a gun. + /// + [DataField, AutoNetworkedField] + public TimeSpan GunShootFailStunTime = TimeSpan.FromSeconds(3); + + /// + /// Stun time after failing to shoot a gun. + /// + [DataField, AutoNetworkedField] + public DamageSpecifier? GunShootFailDamage; + + /// + /// Noise to play after failing to shoot a gun. Boom! + /// + [DataField] + public SoundSpecifier GunShootFailSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"); +} diff --git a/Content.Shared/Clumsy/ClumsySystem.cs b/Content.Shared/Clumsy/ClumsySystem.cs new file mode 100644 index 0000000000..e034458197 --- /dev/null +++ b/Content.Shared/Clumsy/ClumsySystem.cs @@ -0,0 +1,146 @@ +using Content.Shared.CCVar; +using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Climbing.Components; +using Content.Shared.Climbing.Events; +using Content.Shared.Damage; +using Content.Shared.IdentityManagement; +using Content.Shared.Medical; +using Content.Shared.Popups; +using Content.Shared.Stunnable; +using Content.Shared.Weapons.Ranged.Events; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.Clumsy; + +public sealed class ClumsySystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override void Initialize() + { + SubscribeLocalEvent(BeforeHyposprayEvent); + SubscribeLocalEvent(BeforeDefibrillatorZapsEvent); + SubscribeLocalEvent(BeforeGunShotEvent); + SubscribeLocalEvent(OnBeforeClimbEvent); + } + + // If you add more clumsy interactions add them in this section! + #region Clumsy interaction events + private void BeforeHyposprayEvent(Entity ent, ref SelfBeforeHyposprayInjectsEvent args) + { + // Clumsy people sometimes inject themselves! Apparently syringes are clumsy proof... + if (!_random.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + args.TargetGettingInjected = args.EntityUsingHypospray; + args.InjectMessageOverride = "hypospray-component-inject-self-clumsy-message"; + _audio.PlayPvs(ent.Comp.ClumsySound, ent); + } + + private void BeforeDefibrillatorZapsEvent(Entity ent, ref SelfBeforeDefibrillatorZapsEvent args) + { + // Clumsy people sometimes defib themselves! + if (!_random.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + args.DefibTarget = args.EntityUsingDefib; + _audio.PlayPvs(ent.Comp.ClumsySound, ent); + + } + + private void BeforeGunShotEvent(Entity ent, ref SelfBeforeGunShotEvent args) + { + // Clumsy people sometimes can't shoot :( + + if (args.Gun.Comp.ClumsyProof) + return; + + if (!_random.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + if (ent.Comp.GunShootFailDamage != null) + _damageable.TryChangeDamage(ent, ent.Comp.GunShootFailDamage, origin: ent); + + _stun.TryParalyze(ent, ent.Comp.GunShootFailStunTime, true); + + // Apply salt to the wound ("Honk!") (No idea what this comment means) + _audio.PlayPvs(ent.Comp.GunShootFailSound, ent); + _audio.PlayPvs(ent.Comp.ClumsySound, ent); + + _popup.PopupEntity(Loc.GetString("gun-clumsy"), ent, ent); + args.Cancel(); + } + + private void OnBeforeClimbEvent(Entity ent, ref SelfBeforeClimbEvent args) + { + // This event is called in shared, thats why it has all the extra prediction stuff. + var rand = new System.Random((int)_timing.CurTick.Value); + + // If someone is putting you on the table, always get past the guard. + if (!_cfg.GetCVar(CCVars.GameTableBonk) && args.PuttingOnTable == ent.Owner && !rand.Prob(ent.Comp.ClumsyDefaultCheck)) + return; + + HitHeadClumsy(ent, args.BeingClimbedOn); + + _audio.PlayPredicted(ent.Comp.ClumsySound, ent, ent); + + _audio.PlayPredicted(ent.Comp.TableBonkSound, ent, ent); + + var gettingPutOnTableName = Identity.Entity(args.GettingPutOnTable, EntityManager); + var puttingOnTableName = Identity.Entity(args.PuttingOnTable, EntityManager); + + if (args.PuttingOnTable == ent.Owner) + { + // You are slamming yourself onto the table. + _popup.PopupPredicted( + Loc.GetString("bonkable-success-message-user", ("bonkable", args.BeingClimbedOn)), + Loc.GetString("bonkable-success-message-others", ("victim", gettingPutOnTableName), ("bonkable", args.BeingClimbedOn)), + ent, + ent); + } + else + { + // Someone else slamed you onto the table. + // This is only run in server so you need to use popup entity. + _popup.PopupPredicted( + Loc.GetString("forced-bonkable-success-message", + ("bonker", puttingOnTableName), + ("victim", gettingPutOnTableName), + ("bonkable", args.BeingClimbedOn)), + ent, + null); + } + + args.Cancel(); + } + #endregion + + #region Helper functions + /// + /// "Hits" an entites head against the given table. + /// + // Oh this fucntion is public le- NO!! This is only public for the one admin command if you use this anywhere else I will cry. + public void HitHeadClumsy(Entity target, EntityUid table) + { + var stunTime = target.Comp.ClumsyDefaultStunTime; + + if (TryComp(table, out var bonkComp)) + { + stunTime = bonkComp.BonkTime; + if (bonkComp.BonkDamage != null) + _damageable.TryChangeDamage(target, bonkComp.BonkDamage, true); + } + + _stun.TryParalyze(target, stunTime, true); + } + #endregion +} diff --git a/Content.Shared/Interaction/Components/ClumsyComponent.cs b/Content.Shared/Interaction/Components/ClumsyComponent.cs deleted file mode 100644 index 824696c838..0000000000 --- a/Content.Shared/Interaction/Components/ClumsyComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Audio; -using Robust.Shared.GameStates; - -namespace Content.Shared.Interaction.Components; - -/// -/// A simple clumsy tag-component. -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -public sealed partial class ClumsyComponent : Component -{ - /// - /// Damage dealt to a clumsy character when they try to fire a gun. - /// - [DataField(required: true), AutoNetworkedField] - public DamageSpecifier ClumsyDamage = default!; - - /// - /// Sound to play when clumsy interactions fail. - /// - [DataField] - public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); -} diff --git a/Content.Shared/Interaction/SharedInteractionSystem.Clumsy.cs b/Content.Shared/Interaction/SharedInteractionSystem.Clumsy.cs deleted file mode 100644 index 9e45847e07..0000000000 --- a/Content.Shared/Interaction/SharedInteractionSystem.Clumsy.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Shared.Interaction.Components; -using Robust.Shared.Random; - -namespace Content.Shared.Interaction -{ - public partial class SharedInteractionSystem - { - public bool RollClumsy(ClumsyComponent component, float chance) - { - return component.Running && _random.Prob(chance); - } - - /// - /// Rolls a probability chance for a "bad action" if the target entity is clumsy. - /// - /// The entity that the clumsy check is happening for. - /// - /// The chance that a "bad action" happens if the user is clumsy, between 0 and 1 inclusive. - /// - /// True if a "bad action" happened, false if the normal action should happen. - public bool TryRollClumsy(EntityUid entity, float chance, ClumsyComponent? component = null) - { - return Resolve(entity, ref component, false) && RollClumsy(component, chance); - } - } -} diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index c910a9ae77..9573f9b43d 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -1,4 +1,7 @@ +using Content.Shared.Chat; using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Climbing.Events; using Content.Shared.Damage; using Content.Shared.Electrocution; using Content.Shared.Explosion; @@ -15,7 +18,7 @@ using Content.Shared.Slippery; using Content.Shared.Strip.Components; using Content.Shared.Temperature; using Content.Shared.Verbs; -using Content.Shared.Chat; +using Content.Shared.Weapons.Ranged.Events; namespace Content.Shared.Inventory; @@ -33,6 +36,10 @@ public partial class InventorySystem SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); // by-ref events SubscribeLocalEvent(RefRelayInventoryEvent); diff --git a/Content.Shared/Medical/DefibrillatorEvents.cs b/Content.Shared/Medical/DefibrillatorEvents.cs new file mode 100644 index 0000000000..54a21a40d4 --- /dev/null +++ b/Content.Shared/Medical/DefibrillatorEvents.cs @@ -0,0 +1,39 @@ +using Content.Shared.Inventory; + +namespace Content.Shared.Medical; + +[ByRefEvent] +public readonly record struct TargetDefibrillatedEvent(EntityUid User, Entity Defibrillator); + +public abstract class BeforeDefibrillatorZapsEvent : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public EntityUid EntityUsingDefib; + public readonly EntityUid Defib; + public EntityUid DefibTarget; + + public BeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibTarget) + { + EntityUsingDefib = entityUsingDefib; + Defib = defib; + DefibTarget = defibTarget; + } +} + +/// +/// This event is raised on the user using the defibrillator before is actually zaps someone. +/// The event is triggered on the user and all their clothing. +/// +public sealed class SelfBeforeDefibrillatorZapsEvent : BeforeDefibrillatorZapsEvent +{ + public SelfBeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibtarget) : base(entityUsingDefib, defib, defibtarget) { } +} + +/// +/// This event is raised on the target before it gets zapped with the defibrillator. +/// The event is triggered on the target itself and all its clothing. +/// +public sealed class TargetBeforeDefibrillatorZapsEvent : BeforeDefibrillatorZapsEvent +{ + public TargetBeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibtarget) : base(entityUsingDefib, defib, defibtarget) { } +} diff --git a/Content.Shared/Medical/TargetDefibrillatedEvent.cs b/Content.Shared/Medical/TargetDefibrillatedEvent.cs deleted file mode 100644 index 60d1a21584..0000000000 --- a/Content.Shared/Medical/TargetDefibrillatedEvent.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Content.Shared.Medical; - -[ByRefEvent] -public readonly record struct TargetDefibrillatedEvent(EntityUid User, Entity Defibrillator); diff --git a/Content.Shared/Weapons/Ranged/Events/BeforeGunShootEvent.cs b/Content.Shared/Weapons/Ranged/Events/BeforeGunShootEvent.cs new file mode 100644 index 0000000000..1d3317c840 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/BeforeGunShootEvent.cs @@ -0,0 +1,20 @@ +using Content.Shared.Inventory; +using Content.Shared.Weapons.Ranged.Components; + +namespace Content.Shared.Weapons.Ranged.Events; +/// +/// This event is triggered on an entity right before they shoot a gun. +/// +public sealed partial class SelfBeforeGunShotEvent : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + public readonly EntityUid Shooter; + public readonly Entity Gun; + public readonly List<(EntityUid? Entity, IShootable Shootable)> Ammo; + public SelfBeforeGunShotEvent(EntityUid shooter, Entity gun, List<(EntityUid? Entity, IShootable Shootable)> ammo) + { + Shooter = shooter; + Gun = gun; + Ammo = ammo; + } +} diff --git a/Resources/Locale/en-US/bonk/components/bonkable-component.ftl b/Resources/Locale/en-US/bonk/components/bonkable-component.ftl index 560b10c46e..1a79da3509 100644 --- a/Resources/Locale/en-US/bonk/components/bonkable-component.ftl +++ b/Resources/Locale/en-US/bonk/components/bonkable-component.ftl @@ -1,2 +1,4 @@ -bonkable-success-message-others = { CAPITALIZE(THE($user)) } bonks { POSS-ADJ($user) } head against { THE($bonkable) } -bonkable-success-message-user = You bonk your head against { THE($bonkable) } +forced-bonkable-success-message = { CAPITALIZE($bonker) } bonks {$victim}s head against { THE($bonkable) }! + +bonkable-success-message-user = You bonk your head against { THE($bonkable) }! +bonkable-success-message-others = {$victim} bonks their head against { THE($bonkable) }! diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 2280c3fecb..48c83c8d1e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1360,7 +1360,7 @@ rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 5 Piercing: 4 @@ -1536,7 +1536,7 @@ description: Cousins to the sentient race of lizard people, kobolds blend in with their natural habitat and are as nasty as monkeys; ready to pull out your hair and stab you to death. components: - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 2 Piercing: 7 diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index b21f19edd3..dca47e1c83 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -231,7 +231,7 @@ - type: Hands - type: ComplexInteraction - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: Blunt: 5 Piercing: 4 diff --git a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml index 2862980dad..4b3b4c1874 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml @@ -32,8 +32,6 @@ bonkDamage: types: Blunt: 4 - bonkSound: !type:SoundCollectionSpecifier - collection: TrayHit - type: Clickable - type: FootstepModifier footstepSoundCollection: diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml index 29232d9dbb..b8f289e70e 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml @@ -13,7 +13,7 @@ - !type:AddComponentSpecial components: - type: Clumsy - clumsyDamage: + gunShootFailDamage: types: #literally just picked semi random valus. i tested this once and tweaked it. Blunt: 5 Piercing: 4