diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 31c9b0d2b86..4749fd8b4b3 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -8,6 +8,7 @@ using Content.Shared.Actions.Events; using Content.Shared.Administration.Components; using Content.Shared.CombatMode; +using Content.Shared.Contests; using Content.Shared.Damage.Events; using Content.Shared.Damage.Systems; using Content.Shared.Database; @@ -43,6 +44,7 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly SolutionContainerSystem _solutions = default!; [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly ContestsSystem _contests = default!; public override void Initialize() { @@ -138,7 +140,7 @@ protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid if (attemptEvent.Cancelled) return false; - var chance = CalculateDisarmChance(user, target, inTargetHand, combatMode); + var chance = CalculateDisarmChance(user, target, inTargetHand, combatMode) * _contests.MassContest(user, target); if (_random.Prob(chance)) { diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index c0adf730f11..c471fb00f4f 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Power.EntitySystems; using Content.Server.Stunnable; using Content.Server.Weapons.Ranged.Components; +using Content.Shared.Contests; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.Database; @@ -39,6 +40,7 @@ public sealed partial class GunSystem : SharedGunSystem [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StaminaSystem _stamina = default!; [Dependency] private readonly StunSystem _stun = default!; + [Dependency] private readonly ContestsSystem _contests = default!; public const float DamagePitchVariation = SharedMeleeWeaponSystem.DamagePitchVariation; public const float GunClumsyChance = 0.5f; @@ -97,7 +99,7 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem); var mapDirection = toMap - fromMap.Position; var mapAngle = mapDirection.ToAngle(); - var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle()); + var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle(), user); // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map. var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out var grid) @@ -318,7 +320,7 @@ private Angle[] LinearSpread(Angle start, Angle end, int intervals) return angles; } - private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction) + private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction, EntityUid? shooter) { var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds; var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncreaseModified.Theta - component.AngleDecayModified.Theta * timeSinceLastFire, component.MinAngleModified.Theta, component.MaxAngleModified.Theta); @@ -326,7 +328,8 @@ private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle dir component.LastFire = component.NextFire; // Convert it so angle can go either side. - var random = Random.NextFloat(-0.5f, 0.5f); + + var random = Random.NextFloat(-0.5f, 0.5f) / _contests.MassContest(shooter); var spread = component.CurrentAngle.Theta * random; var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random); DebugTools.Assert(spread <= component.MaxAngleModified.Theta); diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index c29cf2ce2fa..5e168ae04fe 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2180,5 +2180,18 @@ public static readonly CVarDef /// public static readonly CVarDef StationGoalsChance = CVarDef.Create("game.station_goals_chance", 0.1f, CVar.SERVERONLY); + + /// + /// Toggles all MassContest functions. All mass contests output 1f when false + /// + public static readonly CVarDef DoMassContests = + CVarDef.Create("contests.do_mass_contests", true, CVar.REPLICATED | CVar.SERVER); + + /// + /// The maximum amount that Mass Contests can modify a physics multiplier, given as a +/- percentage + /// Default of 0.25f outputs between * 0.75f and 1.25f + /// + public static readonly CVarDef MassContestsMaxPercentage = + CVarDef.Create("contests.max_percentage", 0.25f, CVar.REPLICATED | CVar.SERVER); } } diff --git a/Content.Shared/Contests/ContestsSystem.cs b/Content.Shared/Contests/ContestsSystem.cs new file mode 100644 index 00000000000..6386bfd7a23 --- /dev/null +++ b/Content.Shared/Contests/ContestsSystem.cs @@ -0,0 +1,116 @@ +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using Robust.Shared.Physics.Components; + +namespace Content.Shared.Contests +{ + public sealed partial class ContestsSystem : EntitySystem + { + [Dependency] private readonly IConfigurationManager _cfg = default!; + + /// + /// The presumed average mass of a player entity + /// Defaulted to the average mass of an adult human + /// + private const float AverageMass = 71f; + + #region Mass Contests + /// + /// Outputs the ratio of mass between a performer and the average human mass + /// + /// Uid of Performer + public float MassContest(EntityUid performerUid, float otherMass = AverageMass) + { + if (_cfg.GetCVar(CCVars.DoMassContests) + && TryComp(performerUid, out var performerPhysics) + && performerPhysics.Mass != 0) + return Math.Clamp(performerPhysics.Mass / otherMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); + + return 1f; + } + + /// + /// + /// MaybeMassContest, in case your entity doesn't exist + /// + public float MassContest(EntityUid? performerUid, float otherMass = AverageMass) + { + if (_cfg.GetCVar(CCVars.DoMassContests)) + { + var ratio = performerUid is { } uid ? MassContest(uid, otherMass) : 1f; + return ratio; + } + + return 1f; + } + + /// + /// Outputs the ratio of mass between a performer and the average human mass + /// If a function already has the performer's physics component, this is faster + /// + /// + public float MassContest(PhysicsComponent performerPhysics, float otherMass = AverageMass) + { + if (_cfg.GetCVar(CCVars.DoMassContests) + && performerPhysics.Mass != 0) + return Math.Clamp(performerPhysics.Mass / otherMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); + + return 1f; + } + + /// + /// Outputs the ratio of mass between a performer and a target, accepts either EntityUids or PhysicsComponents in any combination + /// If you have physics components already in your function, use instead + /// + /// + /// + public float MassContest(EntityUid performerUid, EntityUid targetUid) + { + if (_cfg.GetCVar(CCVars.DoMassContests) + && TryComp(performerUid, out var performerPhysics) + && TryComp(targetUid, out var targetPhysics) + && performerPhysics.Mass != 0 + && targetPhysics.InvMass != 0) + return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); + + return 1f; + } + + /// + public float MassContest(EntityUid performerUid, PhysicsComponent targetPhysics) + { + if (_cfg.GetCVar(CCVars.DoMassContests) + && TryComp(performerUid, out var performerPhysics) + && performerPhysics.Mass != 0 + && targetPhysics.InvMass != 0) + return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); + + return 1f; + } + + /// + public float MassContest(PhysicsComponent performerPhysics, EntityUid targetUid) + { + if (_cfg.GetCVar(CCVars.DoMassContests) + && TryComp(targetUid, out var targetPhysics) + && performerPhysics.Mass != 0 + && targetPhysics.InvMass != 0) + return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); + + return 1f; + } + + /// + public float MassContest(PhysicsComponent performerPhysics, PhysicsComponent targetPhysics) + { + if (_cfg.GetCVar(CCVars.DoMassContests) + && performerPhysics.Mass != 0 + && targetPhysics.InvMass != 0) + return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); + + return 1f; + } + + #endregion + } +} diff --git a/Content.Shared/Cuffs/Components/HandcuffComponent.cs b/Content.Shared/Cuffs/Components/HandcuffComponent.cs index 77a77cf2f84..d305f6067d0 100644 --- a/Content.Shared/Cuffs/Components/HandcuffComponent.cs +++ b/Content.Shared/Cuffs/Components/HandcuffComponent.cs @@ -12,37 +12,37 @@ public sealed partial class HandcuffComponent : Component /// /// The time it takes to cuff an entity. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float CuffTime = 3.5f; /// /// The time it takes to uncuff an entity. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float UncuffTime = 3.5f; /// /// The time it takes for a cuffed entity to uncuff itself. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float BreakoutTime = 15f; /// /// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float StunBonus = 2f; /// /// Will the cuffs break when removed? /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool BreakOnRemove; /// /// Will the cuffs break when removed? /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntProtoId? BrokenPrototype; /// @@ -55,35 +55,42 @@ public sealed partial class HandcuffComponent : Component /// /// The path of the RSI file used for the player cuffed overlay. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string? CuffedRSI = "Objects/Misc/handcuffs.rsi"; /// /// The iconstate used with the RSI file for the player cuffed overlay. /// - [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + [DataField, AutoNetworkedField] public string? BodyIconState = "body-overlay"; /// /// An opptional color specification for /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public Color Color = Color.White; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier StartCuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_start.ogg"); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier EndCuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_end.ogg"); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier StartBreakoutSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_breakout_start.ogg"); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier StartUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_start.ogg"); - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier EndUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg"); + + /// + /// Acts as a two-state option for handcuff speed. When true, handcuffs will be easier to get out of if you are larger than average. Representing the use of strength to break things like zipties. + /// When false, handcuffs are easier to get out of if you are smaller than average, representing the use of dexterity to slip the cuffs. + /// + [DataField] + public bool UncuffEasierWhenLarge = false; } /// diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index fc005fd30fa..e4990320a59 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Alert; using Content.Shared.Atmos.Piping.Unary.Components; using Content.Shared.Buckle.Components; +using Content.Shared.Contests; using Content.Shared.Cuffs.Components; using Content.Shared.Database; using Content.Shared.DoAfter; @@ -58,6 +59,7 @@ public abstract partial class SharedCuffableSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly UseDelaySystem _delay = default!; + [Dependency] private readonly ContestsSystem _contests = default!; public override void Initialize() { @@ -559,7 +561,7 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove return; } - var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime; + var uncuffTime = (isOwner ? cuff.BreakoutTime : cuff.UncuffTime) * (cuff.UncuffEasierWhenLarge ? 1 / _contests.MassContest(user) : _contests.MassContest(user)); if (isOwner) { diff --git a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml index 5f970da1840..527233920d5 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml @@ -52,6 +52,7 @@ path: /Audio/Items/Handcuffs/rope_breakout.ogg startBreakoutSound: path: /Audio/Items/Handcuffs/rope_takeoff.ogg + uncuffEasierWhenLarge: true - type: Construction graph: makeshifthandcuffs node: cuffscable @@ -93,6 +94,7 @@ path: /Audio/Items/Handcuffs/rope_breakout.ogg startBreakoutSound: path: /Audio/Items/Handcuffs/rope_takeoff.ogg + uncuffEasierWhenLarge: true - type: Sprite sprite: Objects/Misc/zipties.rsi state: cuff