Skip to content

Commit

Permalink
Mass Contests Remake (#480)
Browse files Browse the repository at this point in the history
# 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 <[email protected]>
Co-authored-by: DEATHB4DEFEAT <[email protected]>
Co-authored-by: Danger Revolution! <[email protected]>
  • Loading branch information
3 people committed Jul 13, 2024
1 parent 69bf295 commit 806dc01
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 19 deletions.
4 changes: 3 additions & 1 deletion Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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))
{
Expand Down
9 changes: 6 additions & 3 deletions Content.Server/Weapons/Ranged/Systems/GunSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -316,15 +318,16 @@ 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);
component.CurrentAngle = new Angle(newTheta);
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);
Expand Down
13 changes: 13 additions & 0 deletions Content.Shared/CCVar/CCVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,5 +2210,18 @@ public static readonly CVarDef<float>
/// </summary>
public static readonly CVarDef<float> StationGoalsChance =
CVarDef.Create("game.station_goals_chance", 0.1f, CVar.SERVERONLY);

/// <summary>
/// Toggles all MassContest functions. All mass contests output 1f when false
/// </summary>
public static readonly CVarDef<bool> DoMassContests =
CVarDef.Create("contests.do_mass_contests", true, CVar.REPLICATED | CVar.SERVER);

/// <summary>
/// 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
/// </summary>
public static readonly CVarDef<float> MassContestsMaxPercentage =
CVarDef.Create("contests.max_percentage", 0.25f, CVar.REPLICATED | CVar.SERVER);
}
}
116 changes: 116 additions & 0 deletions Content.Shared/Contests/ContestsSystem.cs
Original file line number Diff line number Diff line change
@@ -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!;

/// <summary>
/// The presumed average mass of a player entity
/// Defaulted to the average mass of an adult human
/// </summary>
private const float AverageMass = 71f;

#region Mass Contests
/// <summary>
/// Outputs the ratio of mass between a performer and the average human mass
/// </summary>
/// <param name="performerUid">Uid of Performer</param>
public float MassContest(EntityUid performerUid, float otherMass = AverageMass)
{
if (_cfg.GetCVar(CCVars.DoMassContests)
&& TryComp<PhysicsComponent>(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;
}

/// <inheritdoc cref="MassContest(EntityUid, float)"/>
/// <remarks>
/// MaybeMassContest, in case your entity doesn't exist
/// </remarks>
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;
}

/// <summary>
/// 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
/// </summary>
/// <param name="performerPhysics"></param>
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;
}

/// <summary>
/// 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 <see cref="MassContest(PhysicsComponent, float)" /> instead
/// </summary>
/// <param name="performerUid"></param>
/// <param name="targetUid"></param>
public float MassContest(EntityUid performerUid, EntityUid targetUid)
{
if (_cfg.GetCVar(CCVars.DoMassContests)
&& TryComp<PhysicsComponent>(performerUid, out var performerPhysics)
&& TryComp<PhysicsComponent>(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;
}

/// <inheritdoc cref="MassContest(EntityUid, EntityUid)"/>
public float MassContest(EntityUid performerUid, PhysicsComponent targetPhysics)
{
if (_cfg.GetCVar(CCVars.DoMassContests)
&& TryComp<PhysicsComponent>(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;
}

/// <inheritdoc cref="MassContest(EntityUid, EntityUid)"/>
public float MassContest(PhysicsComponent performerPhysics, EntityUid targetUid)
{
if (_cfg.GetCVar(CCVars.DoMassContests)
&& TryComp<PhysicsComponent>(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;
}

/// <inheritdoc cref="MassContest(EntityUid, EntityUid)"/>
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
}
}
35 changes: 21 additions & 14 deletions Content.Shared/Cuffs/Components/HandcuffComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,37 @@ public sealed partial class HandcuffComponent : Component
/// <summary>
/// The time it takes to cuff an entity.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float CuffTime = 3.5f;

/// <summary>
/// The time it takes to uncuff an entity.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float UncuffTime = 3.5f;

/// <summary>
/// The time it takes for a cuffed entity to uncuff itself.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float BreakoutTime = 15f;

/// <summary>
/// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float StunBonus = 2f;

/// <summary>
/// Will the cuffs break when removed?
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool BreakOnRemove;

/// <summary>
/// Will the cuffs break when removed?
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public EntProtoId? BrokenPrototype;

/// <summary>
Expand All @@ -55,35 +55,42 @@ public sealed partial class HandcuffComponent : Component
/// <summary>
/// The path of the RSI file used for the player cuffed overlay.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string? CuffedRSI = "Objects/Misc/handcuffs.rsi";

/// <summary>
/// The iconstate used with the RSI file for the player cuffed overlay.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
[DataField, AutoNetworkedField]
public string? BodyIconState = "body-overlay";

/// <summary>
/// An opptional color specification for <see cref="BodyIconState"/>
/// </summary>
[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");

/// <summary>
/// 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.
/// </summary>
[DataField]
public bool UncuffEasierWhenLarge = false;
}

/// <summary>
Expand Down
4 changes: 3 additions & 1 deletion Content.Shared/Cuffs/SharedCuffableSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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)
{
Expand Down
2 changes: 2 additions & 0 deletions Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 806dc01

Please sign in to comment.