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;
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<SuperBonkComponent, ComponentShutdown>(OnBonkShutdown);
SubscribeLocalEvent<SuperBonkComponent, MobStateChangedEvent>(OnMobStateChanged);
+ SubscribeLocalEvent<SuperBonkComponent, ComponentShutdown>(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.
return;
}
-
var hadClumsy = EnsureComp<ClumsyComponent>(target, out _);
var tables = EntityQueryEnumerator<BonkableComponent>();
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<TransformComponent>(uid))
+ if (!HasComp<TransformComponent>(uid) || !TryComp<ClumsyComponent>(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)
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;
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)
+ {
+ _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)
{
- msgFormat = "hypospray-component-inject-self-clumsy-message";
- target = user;
+ _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);
using Content.Shared.Interaction.Components;
using Robust.Shared.Audio.Systems;
using Content.Shared.NameModifier.EntitySystems;
+using Content.Shared.Clumsy;
namespace Content.Server.Cluwne;
Zap(uid, target, args.User, component);
}
- public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null)
+ /// <summary>
+ /// Checks if you can actually defib a target.
+ /// </summary>
+ /// <param name="uid">Uid of the defib</param>
+ /// <param name="target">Uid of the target getting defibbed</param>
+ /// <param name="user">Uid of the entity using the defibrillator</param>
+ /// <param name="component">Defib component</param>
+ /// <param name="targetCanBeAlive">
+ /// 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.
+ /// </param>
+ /// <returns>
+ /// Returns true if the target is valid to be defibed, false otherwise.
+ /// </returns>
+ public bool CanZap(EntityUid uid, EntityUid target, EntityUid? user = null, DefibrillatorComponent? component = null, bool targetCanBeAlive = false)
{
if (!Resolve(uid, ref component))
return false;
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;
}
+ /// <summary>
+ /// Tries to start defibrillating the target. If the target is valid, will start the defib do-after.
+ /// </summary>
+ /// <param name="uid">Uid of the defib</param>
+ /// <param name="target">Uid of the target getting defibbed</param>
+ /// <param name="user">Uid of the entity using the defibrillator</param>
+ /// <param name="component">Defib component</param>
+ /// <returns>
+ /// Returns true if the defibrillation do-after started, otherwise false.
+ /// </returns>
public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null)
{
if (!Resolve(uid, ref component))
_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)
+ /// <summary>
+ /// Tries to defibrillate the target with the given defibrillator.
+ /// </summary>
+ 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<ClumsyComponent>(user) && user != target)
- {
- Zap(uid, user, user, component);
+ if (!_powerCell.TryUseActivatableCharge(uid, user: user))
return;
- }
- if (!_powerCell.TryUseActivatableCharge(uid, user: user))
+ 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<MobStateComponent>(target, out var mob) ||
+ !TryComp<MobThresholdsComponent>(target, out var thresholds))
return;
_audio.PlayPvs(component.ZapSound, uid);
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;
[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()
{
{
userImpulse = true;
- // Try a clumsy roll
- // TODO: Who put this here
- if (TryComp<ClumsyComponent>(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;
}
}
[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);
--- /dev/null
+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;
+ }
+}
+
+/// <summary>
+/// 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.
+/// </summary>
+public sealed class SelfBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent
+{
+ public SelfBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { }
+}
+
+/// <summary>
+/// This event is raised on the target before the hypospray is injected.
+/// The event is triggered on the target itself and all its clothing.
+/// </summary>
+public sealed class TargetBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent
+{
+ public TargetBeforeHyposprayInjectsEvent (EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { }
+}
using Content.Shared.Damage;
-using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared.Climbing.Components;
/// Makes entity do damage and stun entities with ClumsyComponent
/// upon DragDrop or Climb interactions.
/// </summary>
-[RegisterComponent, NetworkedComponent, Access(typeof(Systems.BonkSystem))]
+[RegisterComponent, NetworkedComponent]
public sealed partial class BonkableComponent : Component
{
/// <summary>
- /// Chance of bonk triggering if the user is clumsy.
+ /// How long to stun players on bonk, in seconds.
/// </summary>
- [DataField("bonkClumsyChance")]
- public float BonkClumsyChance = 0.5f;
+ [DataField]
+ public TimeSpan BonkTime = TimeSpan.FromSeconds(2);
/// <summary>
- /// Sound to play when bonking.
+ /// How much damage to apply on bonk.
/// </summary>
- /// <seealso cref="Bonk"/>
- [DataField("bonkSound")]
- public SoundSpecifier? BonkSound;
-
- /// <summary>
- /// How long to stun players on bonk, in seconds.
- /// </summary>
- /// <seealso cref="Bonk"/>
- [DataField("bonkTime")]
- public float BonkTime = 2;
-
- /// <summary>
- /// How much damage to apply on bonk.
- /// </summary>
- /// <seealso cref="Bonk"/>
- [DataField("bonkDamage")]
+ [DataField]
public DamageSpecifier? BonkDamage;
-
- /// <summary>
- /// How long it takes to bonk.
- /// </summary>
- [DataField("bonkDelay")]
- public float BonkDelay = 1.5f;
}
--- /dev/null
+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<ClimbableComponent> BeingClimbedOn;
+
+ public BeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity<ClimbableComponent> beingClimbedOn)
+ {
+ GettingPutOnTable = gettingPutOntable;
+ PuttingOnTable = puttingOnTable;
+ BeingClimbedOn = beingClimbedOn;
+ }
+}
+
+/// <summary>
+/// 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.
+/// </summary>
+public sealed class SelfBeforeClimbEvent : BeforeClimbEvent, IInventoryRelayEvent
+{
+ public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET;
+ public SelfBeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity<ClimbableComponent> beingClimbedOn) : base(gettingPutOntable, puttingOnTable, beingClimbedOn) { }
+}
+
+/// <summary>
+/// This event is raised on the thing being climbed on.
+/// </summary>
+public sealed class TargetBeforeClimbEvent : BeforeClimbEvent
+{
+ public TargetBeforeClimbEvent(EntityUid gettingPutOntable, EntityUid puttingOnTable, Entity<ClimbableComponent> beingClimbedOn) : base(gettingPutOntable, puttingOnTable, beingClimbedOn) { }
+}
+++ /dev/null
-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<BonkableComponent, BonkDoAfterEvent>(OnBonkDoAfter);
- SubscribeLocalEvent<BonkableComponent, AttemptClimbEvent>(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<ClumsyComponent>(climber) || !HasComp<HandsComponent>(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
- {
- }
-}
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;
--- /dev/null
+using Content.Shared.Damage;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Clumsy;
+
+/// <summary>
+/// A simple clumsy tag-component.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ClumsyComponent : Component
+{
+
+ // Standard options. Try to fit these in if you can!
+
+ /// <summary>
+ /// Sound to play when clumsy interactions fail.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg");
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public float ClumsyDefaultCheck = 0.5f;
+
+ /// <summary>
+ /// Default stun time.
+ /// If a system needs to use something else, add a new variable in the component, do not modify this number.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan ClumsyDefaultStunTime = TimeSpan.FromSeconds(2.5);
+
+ // Specific options
+
+ /// <summary>
+ /// Sound to play after hitting your head on a table. Ouch!
+ /// </summary>
+ [DataField]
+ public SoundCollectionSpecifier TableBonkSound = new SoundCollectionSpecifier("TrayHit");
+
+ /// <summary>
+ /// Stun time after failing to shoot a gun.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan GunShootFailStunTime = TimeSpan.FromSeconds(3);
+
+ /// <summary>
+ /// Stun time after failing to shoot a gun.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public DamageSpecifier? GunShootFailDamage;
+
+ /// <summary>
+ /// Noise to play after failing to shoot a gun. Boom!
+ /// </summary>
+ [DataField]
+ public SoundSpecifier GunShootFailSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg");
+}
--- /dev/null
+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<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent);
+ SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent);
+ SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent);
+ SubscribeLocalEvent<ClumsyComponent, SelfBeforeClimbEvent>(OnBeforeClimbEvent);
+ }
+
+ // If you add more clumsy interactions add them in this section!
+ #region Clumsy interaction events
+ private void BeforeHyposprayEvent(Entity<ClumsyComponent> 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<ClumsyComponent> 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<ClumsyComponent> 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<ClumsyComponent> 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
+ /// <summary>
+ /// "Hits" an entites head against the given table.
+ /// </summary>
+ // 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<ClumsyComponent> target, EntityUid table)
+ {
+ var stunTime = target.Comp.ClumsyDefaultStunTime;
+
+ if (TryComp<BonkableComponent>(table, out var bonkComp))
+ {
+ stunTime = bonkComp.BonkTime;
+ if (bonkComp.BonkDamage != null)
+ _damageable.TryChangeDamage(target, bonkComp.BonkDamage, true);
+ }
+
+ _stun.TryParalyze(target, stunTime, true);
+ }
+ #endregion
+}
+++ /dev/null
-using Content.Shared.Damage;
-using Robust.Shared.Audio;
-using Robust.Shared.GameStates;
-
-namespace Content.Shared.Interaction.Components;
-
-/// <summary>
-/// A simple clumsy tag-component.
-/// </summary>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
-public sealed partial class ClumsyComponent : Component
-{
- /// <summary>
- /// Damage dealt to a clumsy character when they try to fire a gun.
- /// </summary>
- [DataField(required: true), AutoNetworkedField]
- public DamageSpecifier ClumsyDamage = default!;
-
- /// <summary>
- /// Sound to play when clumsy interactions fail.
- /// </summary>
- [DataField]
- public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg");
-}
+++ /dev/null
-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);
- }
-
- /// <summary>
- /// Rolls a probability chance for a "bad action" if the target entity is clumsy.
- /// </summary>
- /// <param name="entity">The entity that the clumsy check is happening for.</param>
- /// <param name="chance">
- /// The chance that a "bad action" happens if the user is clumsy, between 0 and 1 inclusive.
- /// </param>
- /// <returns>True if a "bad action" happened, false if the normal action should happen.</returns>
- public bool TryRollClumsy(EntityUid entity, float chance, ClumsyComponent? component = null)
- {
- return Resolve(entity, ref component, false) && RollClumsy(component, chance);
- }
- }
-}
+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;
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;
SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, TransformSpeakerNameEvent>(RelayInventoryEvent);
+ SubscribeLocalEvent<InventoryComponent, SelfBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
+ SubscribeLocalEvent<InventoryComponent, TargetBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
+ SubscribeLocalEvent<InventoryComponent, SelfBeforeGunShotEvent>(RelayInventoryEvent);
+ SubscribeLocalEvent<InventoryComponent, SelfBeforeClimbEvent>(RelayInventoryEvent);
// by-ref events
SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent);
--- /dev/null
+using Content.Shared.Inventory;
+
+namespace Content.Shared.Medical;
+
+[ByRefEvent]
+public readonly record struct TargetDefibrillatedEvent(EntityUid User, Entity<DefibrillatorComponent> 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;
+ }
+}
+
+/// <summary>
+/// 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.
+/// </summary>
+public sealed class SelfBeforeDefibrillatorZapsEvent : BeforeDefibrillatorZapsEvent
+{
+ public SelfBeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibtarget) : base(entityUsingDefib, defib, defibtarget) { }
+}
+
+/// <summary>
+/// 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.
+/// </summary>
+public sealed class TargetBeforeDefibrillatorZapsEvent : BeforeDefibrillatorZapsEvent
+{
+ public TargetBeforeDefibrillatorZapsEvent(EntityUid entityUsingDefib, EntityUid defib, EntityUid defibtarget) : base(entityUsingDefib, defib, defibtarget) { }
+}
+++ /dev/null
-namespace Content.Shared.Medical;
-
-[ByRefEvent]
-public readonly record struct TargetDefibrillatedEvent(EntityUid User, Entity<DefibrillatorComponent> Defibrillator);
--- /dev/null
+using Content.Shared.Inventory;
+using Content.Shared.Weapons.Ranged.Components;
+
+namespace Content.Shared.Weapons.Ranged.Events;
+/// <summary>
+/// This event is triggered on an entity right before they shoot a gun.
+/// </summary>
+public sealed partial class SelfBeforeGunShotEvent : CancellableEntityEventArgs, IInventoryRelayEvent
+{
+ public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET;
+ public readonly EntityUid Shooter;
+ public readonly Entity<GunComponent> Gun;
+ public readonly List<(EntityUid? Entity, IShootable Shootable)> Ammo;
+ public SelfBeforeGunShotEvent(EntityUid shooter, Entity<GunComponent> gun, List<(EntityUid? Entity, IShootable Shootable)> ammo)
+ {
+ Shooter = shooter;
+ Gun = gun;
+ Ammo = ammo;
+ }
+}
-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) }!
rules: ghost-role-information-nonantagonist-rules
- type: GhostTakeoverAvailable
- type: Clumsy
- clumsyDamage:
+ gunShootFailDamage:
types:
Blunt: 5
Piercing: 4
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
- type: Hands
- type: ComplexInteraction
- type: Clumsy
- clumsyDamage:
+ gunShootFailDamage:
types:
Blunt: 5
Piercing: 4
bonkDamage:
types:
Blunt: 4
- bonkSound: !type:SoundCollectionSpecifier
- collection: TrayHit
- type: Clickable
- type: FootstepModifier
footstepSoundCollection:
- !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