diff --git a/Content.Server/CombatMode/CombatModeSystem.cs b/Content.Server/CombatMode/CombatModeSystem.cs index 730c10778c..0c39a528e9 100644 --- a/Content.Server/CombatMode/CombatModeSystem.cs +++ b/Content.Server/CombatMode/CombatModeSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Administration.Logs; using Content.Server.CombatMode.Disarm; using Content.Server.Hands.Components; using Content.Server.Popups; +using Content.Server.Contests; using Content.Server.Weapon.Melee; using Content.Shared.ActionBlocker; using Content.Shared.Audio; @@ -29,6 +30,8 @@ namespace Content.Server.CombatMode [Dependency] private readonly IAdminLogManager _adminLogger= default!; [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ContestsSystem _contests = default!; + public override void Initialize() { base.Initialize(); @@ -108,33 +111,15 @@ namespace Content.Server.CombatMode private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, SharedCombatModeComponent disarmerComp) { - float healthMod = 0; - if (HasComp(disarmer)) return 1.0f; if (HasComp(disarmed)) return 0.0f; - if (TryComp(disarmer, out var disarmerDamage) && TryComp(disarmed, out var disarmedDamage)) - { - // I wanted this to consider their mob state thresholds too but I'm not touching that shitcode after having a go at this. - healthMod = (((float) disarmedDamage.TotalDamage - (float) disarmerDamage.TotalDamage) / 200); // Ex. You have 0 damage, they have 90, you get a 45% chance increase - } + var contestResults = 1 - _contests.OverallStrengthContest(disarmer, disarmed); - float massMod = 0; - - if (TryComp(disarmer, out var disarmerPhysics) && TryComp(disarmed, out var disarmedPhysics)) - { - if (disarmerPhysics.FixturesMass != 0) // yeah this will never happen but let's not kill the server if it does - massMod = (((disarmedPhysics.FixturesMass / disarmerPhysics.FixturesMass - 1 ) / 2)); // Ex, you weigh 120, they weigh 70, you get a 29% bonus - } - - float chance = (disarmerComp.BaseDisarmFailChance - healthMod - massMod); - if (HasComp(disarmer)) // might need to revisit this part after stamina damage, right now this is basically "pre-stun" - chance += 0.35f; - if (HasComp(disarmed)) - chance -= 0.35f; + float chance = (disarmerComp.BaseDisarmFailChance + contestResults); if (inTargetHand != null && TryComp(inTargetHand, out var malus)) { diff --git a/Content.Server/Contests/ContestsSystem.cs b/Content.Server/Contests/ContestsSystem.cs new file mode 100644 index 0000000000..05ada4b0cd --- /dev/null +++ b/Content.Server/Contests/ContestsSystem.cs @@ -0,0 +1,123 @@ +using Content.Shared.Damage; +using Content.Shared.MobState.EntitySystems; +using Content.Shared.MobState.Components; +using Content.Server.Damage.Components; + +namespace Content.Server.Contests +{ + /// + /// Standardized contests. + /// A contest is figuring out, based on data in components on two entities, + /// which one has an advantage in a situation. The advantage is expressed by a multiplier. + /// 1 = No advantage to either party. + /// >1 = Advantage to roller + /// <1 = Advantage to target + /// Roller should be the entity with an advantage from being bigger/healthier/more skilled, etc. + /// + public sealed class ContestsSystem : EntitySystem + { + [Dependency] private readonly SharedMobStateSystem _mobStateSystem = default!; + + /// + /// Returns the roller's mass divided by the target's. + /// + public float MassContest(EntityUid roller, EntityUid target, PhysicsComponent? rollerPhysics = null, PhysicsComponent? targetPhysics = null) + { + if (!Resolve(roller, ref rollerPhysics) || !Resolve(target, ref targetPhysics)) + return 1f; + + if (rollerPhysics == null || targetPhysics == null) + return 1f; + + if (targetPhysics.FixturesMass == 0) + return 1f; + + return (rollerPhysics.FixturesMass / targetPhysics.FixturesMass); + } + + /// + /// Tries to compare both entities damage to the damage they will enter crit at. + /// After that, it runs the % towards crit through a converter that smooths out and makes + /// the the ratios less severe. + /// Returns the roller's adjusted damage value divided by the target's. Higher is better for the roller. + /// + public float DamageContest(EntityUid roller, EntityUid target, DamageableComponent? rollerDamage = null, DamageableComponent? targetDamage = null) + { + if (!Resolve(roller, ref rollerDamage) || !Resolve(target, ref targetDamage)) + return 1f; + + if (rollerDamage == null || targetDamage == null) + return 1f; + + // First, we'll see what health they go into crit at. + float rollerThreshold = 100f; + float targetThreshold = 100f; + + if (TryComp(roller, out var rollerState) && rollerState != null && + _mobStateSystem.TryGetEarliestIncapacitatedState(rollerState, 10000, out _, out var rollerCritThreshold)) + rollerThreshold = (float) rollerCritThreshold; + + if (TryComp(target, out var targetState) && targetState != null && + _mobStateSystem.TryGetEarliestIncapacitatedState(targetState, 10000, out _, out var targetCritThreshold)) + targetThreshold = (float) targetCritThreshold; + + // Next, we'll see how their damage compares + float rollerDamageScore = (float) rollerDamage.TotalDamage / rollerThreshold; + float targetDamageScore = (float) targetDamage.TotalDamage / targetThreshold; + + return DamageThresholdConverter(rollerDamageScore) / DamageThresholdConverter(targetDamageScore); + } + + /// + /// Finds the % of each entity's stamina damage towards its stam crit threshold. + /// It then runs the % towards the converter to smooth/soften it. + /// Returns the roller's decimal % divided by the target's. Higher value is better for the roller. + /// + public float StaminaContest(EntityUid roller, EntityUid target, StaminaComponent? rollerStamina = null, StaminaComponent? targetStamina = null) + { + if (!Resolve(roller, ref rollerStamina) || !Resolve(target, ref targetStamina)) + return 1f; + + if (rollerStamina == null || targetStamina == null) + return 1f; + + var rollerDamageScore= rollerStamina.StaminaDamage / rollerStamina.CritThreshold; + var targetDamageScore = targetStamina.StaminaDamage / targetStamina.CritThreshold; + + return DamageThresholdConverter(rollerDamageScore) / DamageThresholdConverter(targetDamageScore); + } + + /// + /// This will compare the roller and target's damage, mass, and stamina. See the functions for what each one does. + /// You can change the 'weighting' to make the tests weigh more or less than the other tests. + /// Returns a weighted average of all 3 tests. + /// + public float OverallStrengthContest(EntityUid roller, EntityUid target, float damageWeight = 1f, float massWeight = 1f, float stamWeight = 1f) + { + var weightTotal = damageWeight + massWeight + stamWeight; + var damageMultiplier = damageWeight / weightTotal; + var massMultiplier = massWeight / weightTotal; + var stamMultiplier = stamWeight / weightTotal; + + return ((DamageContest(roller, target) * damageMultiplier) + (MassContest(roller, target) * massMultiplier) + + (StaminaContest(roller, target) * stamMultiplier)); + } + + /// + /// This softens out the huge advantages that damage contests would lead to otherwise. + /// Once you are crit or near crit, we just let the massive advantages roll with what could be a 20x. + /// + public float DamageThresholdConverter(float score) + { + return score switch + { + <= 0 => 1f, + <= 0.25f => 0.9f, + <= 0.5f => 0.75f, + <= 0.75f => 0.6f, + <= 0.95f => 0.45f, + _ => 0.05f + }; + } + } +}