From 806dc0107bbe9fce99658a944a64c96b33bf1b09 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Sat, 13 Jul 2024 04:05:51 -0400 Subject: [PATCH] Mass Contests Remake (#480) # Description Part of Issue #467 This is a complete re imagining of the Nyanotrasen Mass Contest System(Long since removed from the game). This system adds a highly flexible function that outputs a ratio of a given entity's mass, that is optionally relative to another entity. I've primarily written this system to be used in conjunction with PR #458 , as it adds several new implementations of variable player mass to the game. How this differs from the original Mass Contest system is that it is configured via hotloaded CVars, and is inherently clamped so that character mass only modifies functions by a finite amount rather than providing infinite scaling. This essentially means that while an Oni is 25% better at shoving a Felinid to the floor thanks to their different masses, a 2000kg Lamia is also only 25% better at shoving a Felinid to the floor, rather than 50000% better. The inverse is also true, a small player character can only be 25% better or worse at a given implementation. These implementations are not handled directly by the ContestSystem, and are instead handled directly by other systems that call upon it. This percentage limit can be modified by a new CVar at any time. Additionally, the entire MassContest system can be optionally toggled off completely at any time via CVar, without needing to modify any code that calls upon it. At this time, I have included three different implementations to serve as suitable examples for how MassContest can be used. 1. Weapon recoil is now modified by an entity's mass relative to the human average baseline. Smaller characters experience more recoil, larger characters experience less recoil 2. Disarm/Shove is now modified by Mass Contests. Entities that are sized differently from their target have their shove/disarm chance modified based on the ratio of performer and target mass. 3. Certain types of handcuffs(such as Cablecuffs and zipties) are now faster to slip out of if you are smaller than the average. # Changelog :cl: - add: Mass Contests have returned in a new reworked form. - add: Weapon Recoil is now resisted by character mass. More massive characters take less recoil, less massive characters take more recoil. - add: Disarm and Shove actions are now modified by relative character mass. It is easier to shove people around if you're bigger than them. - add: Cablecuffs and Zipties are now easier to escape out of if you're smaller. --------- Signed-off-by: VMSolidus Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> --- .../Weapons/Melee/MeleeWeaponSystem.cs | 4 +- .../Weapons/Ranged/Systems/GunSystem.cs | 9 +- Content.Shared/CCVar/CCVars.cs | 13 ++ Content.Shared/Contests/ContestsSystem.cs | 116 ++++++++++++++++++ .../Cuffs/Components/HandcuffComponent.cs | 35 +++--- Content.Shared/Cuffs/SharedCuffableSystem.cs | 4 +- .../Entities/Objects/Misc/handcuffs.yml | 2 + 7 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 Content.Shared/Contests/ContestsSystem.cs 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 ea44bd4e2e7..9db00d2949c 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -5,6 +5,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; @@ -37,6 +38,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; @@ -95,7 +97,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) @@ -316,7 +318,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); @@ -324,7 +326,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 a772d7c12ad..bb8b7481f0b 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2210,5 +2210,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 5cade56aca1..641d2bffc2d 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