Clumsy system refactor (#31147)

* First commit

* Fixes

* Added the noise

* Renames

* Timespan

* Fixed space

* entity -> ent

* This shouldn't work

* opps....

* Datafield name change

* Better comments

* small comment

* Personal skill issue

* Event renames and stuff

* Couple fixes

* Defib ref fixes (Silly me)

* Added clumsy back!

* no hard code clumsy!

* Identity fix

* Event name change

* Comment change

* Function name change

* opp

* Update names

* Damage stuff!

* Fixes!

* Fixes

* opps

* This was hidden away!!

* negative diff feeds me
This commit is contained in:
beck-thompson
2024-11-15 15:46:01 -08:00
committed by GitHub
parent 09d0565413
commit da4fa9bea9
25 changed files with 485 additions and 282 deletions

View File

@@ -22,6 +22,7 @@ using Content.Shared.Administration;
using Content.Shared.Administration.Components; using Content.Shared.Administration.Components;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Body.Part; using Content.Shared.Body.Part;
using Content.Shared.Clumsy;
using Content.Shared.Clothing.Components; using Content.Shared.Clothing.Components;
using Content.Shared.Cluwne; using Content.Shared.Cluwne;
using Content.Shared.Damage; using Content.Shared.Damage;

View File

@@ -1,27 +1,27 @@
using Content.Server.Administration.Components; using Content.Server.Administration.Components;
using Content.Shared.Climbing.Components; using Content.Shared.Climbing.Components;
using Content.Shared.Climbing.Events; using Content.Shared.Clumsy;
using Content.Shared.Climbing.Systems;
using Content.Shared.Interaction.Components;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Administration.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 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SuperBonkComponent, ComponentShutdown>(OnBonkShutdown);
SubscribeLocalEvent<SuperBonkComponent, MobStateChangedEvent>(OnMobStateChanged); 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. //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; return;
} }
var hadClumsy = EnsureComp<ClumsyComponent>(target, out _); var hadClumsy = EnsureComp<ClumsyComponent>(target, out _);
var tables = EntityQueryEnumerator<BonkableComponent>(); var tables = EntityQueryEnumerator<BonkableComponent>();
@@ -79,16 +78,17 @@ public sealed class SuperBonkSystem: EntitySystem
private void Bonk(SuperBonkComponent comp) private void Bonk(SuperBonkComponent comp)
{ {
var uid = comp.Tables.Current.Key; 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 // 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. // 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; return;
_transformSystem.SetCoordinates(comp.Target, Transform(uid).Coordinates); _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) private void OnMobStateChanged(EntityUid uid, SuperBonkComponent comp, MobStateChangedEvent args)

View File

@@ -1,6 +1,7 @@
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
@@ -85,14 +86,44 @@ public sealed class HypospraySystem : SharedHypospraySystem
string? msgFormat = null; string? msgFormat = null;
if (target == user) // Self event
msgFormat = "hypospray-component-inject-self-message"; var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target);
else if (EligibleEntity(user, EntityManager, component) && _interaction.TryRollClumsy(user, component.ClumsyFailChance)) RaiseLocalEvent(user, selfEvent);
if (selfEvent.Cancelled)
{ {
msgFormat = "hypospray-component-inject-self-clumsy-message"; _popup.PopupEntity(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user);
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) 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); _popup.PopupEntity(Loc.GetString("hypospray-component-empty-message"), target, user);

View File

@@ -16,6 +16,7 @@ using Content.Shared.Cluwne;
using Content.Shared.Interaction.Components; using Content.Shared.Interaction.Components;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Content.Shared.NameModifier.EntitySystems; using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Clumsy;
namespace Content.Server.Cluwne; namespace Content.Server.Cluwne;

View File

@@ -77,7 +77,20 @@ public sealed class DefibrillatorSystem : EntitySystem
Zap(uid, target, args.User, component); 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)) if (!Resolve(uid, ref component))
return false; return false;
@@ -98,15 +111,25 @@ public sealed class DefibrillatorSystem : EntitySystem
if (!_powerCell.HasActivatableCharge(uid, user: user)) if (!_powerCell.HasActivatableCharge(uid, user: user))
return false; return false;
if (_mobState.IsAlive(target, mobState)) if (!targetCanBeAlive && _mobState.IsAlive(target, mobState))
return false; return false;
if (!component.CanDefibCrit && _mobState.IsCritical(target, mobState)) if (!targetCanBeAlive && !component.CanDefibCrit && _mobState.IsCritical(target, mobState))
return false; return false;
return true; 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) public bool TryStartZap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorComponent? component = null)
{ {
if (!Resolve(uid, ref component)) if (!Resolve(uid, ref component))
@@ -124,21 +147,38 @@ public sealed class DefibrillatorSystem : EntitySystem
}); });
} }
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; return;
// clowns zap themselves
if (HasComp<ClumsyComponent>(user) && user != target)
{
Zap(uid, user, user, component);
return;
}
if (!_powerCell.TryUseActivatableCharge(uid, user: user)) if (!_powerCell.TryUseActivatableCharge(uid, user: user))
return; 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<MobStateComponent>(target, out var mob) ||
!TryComp<MobThresholdsComponent>(target, out var thresholds))
return;
_audio.PlayPvs(component.ZapSound, uid); _audio.PlayPvs(component.ZapSound, uid);
_electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true); _electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true);
component.NextZapTime = _timing.CurTime + component.ZapDelay; component.NextZapTime = _timing.CurTime + component.ZapDelay;

View File

@@ -1,15 +1,12 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Server.Cargo.Systems; using Content.Server.Cargo.Systems;
using Content.Server.Interaction;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Server.Stunnable;
using Content.Server.Weapons.Ranged.Components; using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Systems; using Content.Shared.Damage.Systems;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Effects; using Content.Shared.Effects;
using Content.Shared.Interaction.Components;
using Content.Shared.Projectiles; using Content.Shared.Projectiles;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged; using Content.Shared.Weapons.Ranged;
@@ -33,16 +30,13 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!; [Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StaminaSystem _stamina = default!; [Dependency] private readonly StaminaSystem _stamina = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedContainerSystem _container = default!;
private const float DamagePitchVariation = 0.05f; private const float DamagePitchVariation = 0.05f;
public const float GunClumsyChance = 0.5f;
public override void Initialize() public override void Initialize()
{ {
@@ -71,28 +65,16 @@ public sealed partial class GunSystem : SharedGunSystem
{ {
userImpulse = true; userImpulse = true;
// Try a clumsy roll if (user != null)
// TODO: Who put this here
if (TryComp<ClumsyComponent>(user, out var clumsy) && gun.ClumsyProof == false)
{ {
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; userImpulse = false;
return; return;
} }
} }
}
var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem); var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem);
var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);

View File

@@ -11,11 +11,6 @@ public sealed partial class HyposprayComponent : Component
[DataField] [DataField]
public string SolutionName = "hypospray"; public string SolutionName = "hypospray";
// TODO: This should be on clumsycomponent.
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public float ClumsyFailChance = 0.5f;
[DataField] [DataField]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 TransferAmount = FixedPoint2.New(5); public FixedPoint2 TransferAmount = FixedPoint2.New(5);

View File

@@ -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;
}
}
/// <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) { }
}

View File

@@ -1,5 +1,4 @@
using Content.Shared.Damage; using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
namespace Content.Shared.Climbing.Components; namespace Content.Shared.Climbing.Components;
@@ -8,39 +7,18 @@ namespace Content.Shared.Climbing.Components;
/// Makes entity do damage and stun entities with ClumsyComponent /// Makes entity do damage and stun entities with ClumsyComponent
/// upon DragDrop or Climb interactions. /// upon DragDrop or Climb interactions.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(Systems.BonkSystem))] [RegisterComponent, NetworkedComponent]
public sealed partial class BonkableComponent : Component public sealed partial class BonkableComponent : Component
{ {
/// <summary>
/// Chance of bonk triggering if the user is clumsy.
/// </summary>
[DataField("bonkClumsyChance")]
public float BonkClumsyChance = 0.5f;
/// <summary>
/// Sound to play when bonking.
/// </summary>
/// <seealso cref="Bonk"/>
[DataField("bonkSound")]
public SoundSpecifier? BonkSound;
/// <summary> /// <summary>
/// How long to stun players on bonk, in seconds. /// How long to stun players on bonk, in seconds.
/// </summary> /// </summary>
/// <seealso cref="Bonk"/> [DataField]
[DataField("bonkTime")] public TimeSpan BonkTime = TimeSpan.FromSeconds(2);
public float BonkTime = 2;
/// <summary> /// <summary>
/// How much damage to apply on bonk. /// How much damage to apply on bonk.
/// </summary> /// </summary>
/// <seealso cref="Bonk"/> [DataField]
[DataField("bonkDamage")]
public DamageSpecifier? BonkDamage; public DamageSpecifier? BonkDamage;
/// <summary>
/// How long it takes to bonk.
/// </summary>
[DataField("bonkDelay")]
public float BonkDelay = 1.5f;
} }

View File

@@ -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<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) { }
}

View File

@@ -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<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
{
}
}

View File

@@ -251,6 +251,18 @@ public sealed partial class ClimbSystem : VirtualController
if (!Resolve(climbable, ref comp, false)) if (!Resolve(climbable, ref comp, false))
return; 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)) if (!ReplaceFixtures(uid, climbing, fixtures))
return; return;

View File

@@ -0,0 +1,61 @@
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");
}

View File

@@ -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<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
}

View File

@@ -1,24 +0,0 @@
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");
}

View File

@@ -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);
}
/// <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);
}
}
}

View File

@@ -1,4 +1,7 @@
using Content.Shared.Chat;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Climbing.Events;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Electrocution; using Content.Shared.Electrocution;
using Content.Shared.Explosion; using Content.Shared.Explosion;
@@ -15,7 +18,7 @@ using Content.Shared.Slippery;
using Content.Shared.Strip.Components; using Content.Shared.Strip.Components;
using Content.Shared.Temperature; using Content.Shared.Temperature;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Content.Shared.Chat; using Content.Shared.Weapons.Ranged.Events;
namespace Content.Shared.Inventory; namespace Content.Shared.Inventory;
@@ -33,6 +36,10 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, TransformSpeakerNameEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, TransformSpeakerNameEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SelfBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, TargetBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SelfBeforeGunShotEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SelfBeforeClimbEvent>(RelayInventoryEvent);
// by-ref events // by-ref events
SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent);

View File

@@ -0,0 +1,39 @@
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) { }
}

View File

@@ -1,4 +0,0 @@
namespace Content.Shared.Medical;
[ByRefEvent]
public readonly record struct TargetDefibrillatedEvent(EntityUid User, Entity<DefibrillatorComponent> Defibrillator);

View File

@@ -0,0 +1,20 @@
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;
}
}

View File

@@ -1,2 +1,4 @@
bonkable-success-message-others = { CAPITALIZE(THE($user)) } bonks { POSS-ADJ($user) } 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-user = You bonk your head against { THE($bonkable) }!
bonkable-success-message-others = {$victim} bonks their head against { THE($bonkable) }!

View File

@@ -1360,7 +1360,7 @@
rules: ghost-role-information-nonantagonist-rules rules: ghost-role-information-nonantagonist-rules
- type: GhostTakeoverAvailable - type: GhostTakeoverAvailable
- type: Clumsy - type: Clumsy
clumsyDamage: gunShootFailDamage:
types: types:
Blunt: 5 Blunt: 5
Piercing: 4 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. 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: components:
- type: Clumsy - type: Clumsy
clumsyDamage: gunShootFailDamage:
types: types:
Blunt: 2 Blunt: 2
Piercing: 7 Piercing: 7

View File

@@ -231,7 +231,7 @@
- type: Hands - type: Hands
- type: ComplexInteraction - type: ComplexInteraction
- type: Clumsy - type: Clumsy
clumsyDamage: gunShootFailDamage:
types: types:
Blunt: 5 Blunt: 5
Piercing: 4 Piercing: 4

View File

@@ -32,8 +32,6 @@
bonkDamage: bonkDamage:
types: types:
Blunt: 4 Blunt: 4
bonkSound: !type:SoundCollectionSpecifier
collection: TrayHit
- type: Clickable - type: Clickable
- type: FootstepModifier - type: FootstepModifier
footstepSoundCollection: footstepSoundCollection:

View File

@@ -13,7 +13,7 @@
- !type:AddComponentSpecial - !type:AddComponentSpecial
components: components:
- type: Clumsy - type: Clumsy
clumsyDamage: gunShootFailDamage:
types: #literally just picked semi random valus. i tested this once and tweaked it. types: #literally just picked semi random valus. i tested this once and tweaked it.
Blunt: 5 Blunt: 5
Piercing: 4 Piercing: 4