From fd59427cb5d11ef6b3322f66a7c5ea6366723770 Mon Sep 17 00:00:00 2001 From: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:31:09 -0400 Subject: [PATCH] Corrupt borg speech if they are damaged or low power (#35318) * - Corrupt borg speech the more damaged they are - Corrupt long borg messages if battery is low or empty * twiddle values * Remove RNG based loop, hardcode repeating values for p=0.25 up to 10 repeats. * Make sure that DamagedSiliconAccentSystem is AFTER ReplacementAccentSystem * add missing base initializer call * use Entity pattern for event listener and clarify default values * Move corruption parameters to datafields * Add datafields to enable and disable the two types of corruption, and add datafields to override damage values and charge levels to support entities that don't have damageable components or power cells. * Use nullables for override values * Move DamagedSiliconAccentComponent to Shared and make it networked * Add DamagedSiliconAccent to cloning whitelist --- .../DamagedSiliconAccentSystem.cs | 174 ++++++++++++++++++ .../DamagedSiliconAccentComponent.cs | 80 ++++++++ .../Mobs/Cyborgs/base_borg_chassis.yml | 1 + .../Prototypes/Entities/Mobs/Player/clone.yml | 1 + 4 files changed, 256 insertions(+) create mode 100644 Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs create mode 100644 Content.Shared/Speech/Components/DamagedSiliconAccentComponent.cs diff --git a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs new file mode 100644 index 0000000000..757d668127 --- /dev/null +++ b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs @@ -0,0 +1,174 @@ +using System.Text; +using Content.Server.PowerCell; +using Content.Shared.Speech.Components; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Robust.Shared.Random; + +namespace Content.Server.Speech.EntitySystems; + +public sealed class DamagedSiliconAccentSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAccent, after: [typeof(ReplacementAccentSystem)]); + } + + private void OnAccent(Entity ent, ref AccentGetEvent args) + { + var uid = ent.Owner; + + if (ent.Comp.EnableChargeCorruption) + { + var currentChargeLevel = 0.0f; + if (ent.Comp.OverrideChargeLevel.HasValue) + { + currentChargeLevel = ent.Comp.OverrideChargeLevel.Value; + } + else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) + { + currentChargeLevel = battery.CurrentCharge / battery.MaxCharge; + } + currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f); + // Corrupt due to low power (drops characters on longer messages) + args.Message = CorruptPower(args.Message, currentChargeLevel, ref ent.Comp); + } + + if (ent.Comp.EnableDamageCorruption) + { + var damage = FixedPoint2.Zero; + if (ent.Comp.OverrideTotalDamage.HasValue) + { + damage = ent.Comp.OverrideTotalDamage.Value; + } + else if (TryComp(uid, out var damageable)) + { + damage = damageable.TotalDamage; + } + // Corrupt due to damage (drop, repeat, replace with symbols) + args.Message = CorruptDamage(args.Message, damage, ref ent.Comp); + } + } + + public string CorruptPower(string message, float chargeLevel, ref DamagedSiliconAccentComponent comp) + { + // The first idxMin characters are SAFE + var idxMin = comp.StartPowerCorruptionAtCharIdx; + // Probability will max at idxMax + var idxMax = comp.MaxPowerCorruptionAtCharIdx; + + // Fast bails, would not have an effect + if (chargeLevel > comp.ChargeThresholdForPowerCorruption || message.Length < idxMin) + { + return message; + } + + var outMsg = new StringBuilder(); + + var maxDropProb = comp.MaxDropProbFromPower * (1.0f - chargeLevel / comp.ChargeThresholdForPowerCorruption); + + var idx = -1; + foreach (var letter in message) + { + idx++; + if (idx < idxMin) // Fast character, no effect + { + outMsg.Append(letter); + continue; + } + + // use an x^2 interpolation to increase the drop probability until we hit idxMax + var probToDrop = idx >= idxMax + ? maxDropProb + : (float)Math.Pow(((double)idx - idxMin) / (idxMax - idxMin), 2.0) * maxDropProb; + // Ensure we're in the range for Prob() + probToDrop = Math.Clamp(probToDrop, 0.0f, 1.0f); + + if (_random.Prob(probToDrop)) // Lose a character + { + // Additional chance to change to dot for flavor instead of full drop + if (_random.Prob(comp.ProbToCorruptDotFromPower)) + { + outMsg.Append('.'); + } + } + else // Character is safe + { + outMsg.Append(letter); + } + } + return outMsg.ToString(); + } + + private string CorruptDamage(string message, FixedPoint2 totalDamage, ref DamagedSiliconAccentComponent comp) + { + var outMsg = new StringBuilder(); + // Linear interpolation of character damage probability + var damagePercent = Math.Clamp((float)totalDamage / (float)comp.DamageAtMaxCorruption, 0, 1); + var chanceToCorruptLetter = damagePercent * comp.MaxDamageCorruption; + foreach (var letter in message) + { + if (_random.Prob(chanceToCorruptLetter)) // Corrupt! + { + outMsg.Append(CorruptLetterDamage(letter)); + } + else // Safe! + { + outMsg.Append(letter); + } + } + return outMsg.ToString(); + } + + private string CorruptLetterDamage(char letter) + { + var res = _random.NextDouble(); + return res switch + { + < 0.0 => letter.ToString(), // shouldn't be less than 0! + < 0.5 => CorruptPunctuize(), // 50% chance to replace with random punctuation + < 0.75 => "", // 25% chance to remove character + < 1.00 => CorruptRepeat(letter), // 25% to repeat the character + _ => letter.ToString(), // shouldn't be greater than 1! + }; + } + + private string CorruptPunctuize() + { + const string punctuation = "\"\\`~!@#$%^&*()_+-={}[]|\\;:<>,.?/"; + return punctuation[_random.NextByte((byte)punctuation.Length)].ToString(); + } + + private string CorruptRepeat(char letter) + { + // 25% chance to add another character in the streak + // (kind of like "exploding dice") + // Solved numerically in closed form for streaks of bernoulli variables with p = 0.25 + // Can calculate for different p using python function: + /* + * def prob(streak, p): + * if streak == 0: + * return scipy.stats.binom(streak+1, p).pmf(streak) + * return prob(streak-1) * p + * def prob_cum(streak, p=.25): + * return np.sum([prob(i, p) for i in range(streak+1)]) + */ + var numRepeats = _random.NextDouble() switch + { + < 0.75000000 => 2, + < 0.93750000 => 3, + < 0.98437500 => 4, + < 0.99609375 => 5, + < 0.99902344 => 6, + < 0.99975586 => 7, + < 0.99993896 => 8, + < 0.99998474 => 9, + _ => 10, + }; + return new string(letter, numRepeats); + } +} diff --git a/Content.Shared/Speech/Components/DamagedSiliconAccentComponent.cs b/Content.Shared/Speech/Components/DamagedSiliconAccentComponent.cs new file mode 100644 index 0000000000..fa1377b379 --- /dev/null +++ b/Content.Shared/Speech/Components/DamagedSiliconAccentComponent.cs @@ -0,0 +1,80 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared.Speech.Components; + +[RegisterComponent] +[NetworkedComponent] +public sealed partial class DamagedSiliconAccentComponent : Component +{ + /// + /// Enable damage corruption effects + /// + [DataField] + public bool EnableDamageCorruption = true; + + /// + /// Override total damage for damage corruption effects + /// + [DataField] + public FixedPoint2? OverrideTotalDamage; + + /// + /// The probability that a character will be corrupted when total damage at or above . + /// + [DataField] + public float MaxDamageCorruption = 0.5f; + + /// + /// Probability of character corruption will increase linearly to once until + /// total damage is at or above this value. + /// + [DataField] + public FixedPoint2 DamageAtMaxCorruption = 300; + + /// + /// Enable charge level corruption effects + /// + [DataField] + public bool EnableChargeCorruption = true; + + /// + /// Override charge level for charge level corruption effects + /// + [DataField] + public float? OverrideChargeLevel; + + /// + /// If the power cell charge is below this value (as a fraction of maximum charge), + /// power corruption will begin to be applied. + /// + [DataField] + public float ChargeThresholdForPowerCorruption = 0.15f; + + /// + /// Regardless of charge level, this is how many characters at the start of a message will be 100% safe + /// from being dropped. + /// + [DataField] + public int StartPowerCorruptionAtCharIdx = 8; + + /// + /// The probability that a character will be dropped due to charge level will be maximum for characters past + /// this index. This has the effect of longer messages dropping more characters later in the message. + /// + [DataField] + public int MaxPowerCorruptionAtCharIdx = 40; + + /// + /// The maximum probability that a character will be dropped due to charge level. + /// + [DataField] + public float MaxDropProbFromPower = 0.5f; + + /// + /// If a character is "dropped", this is the probability that the character will be turned into a period instead + /// of completely deleting the character. + /// + [DataField] + public float ProbToCorruptDotFromPower = 0.6f; +} diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 3995e071fb..96e8eda43d 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -156,6 +156,7 @@ sounds: Unsexed: UnisexSilicon screamAction: null + - type: DamagedSiliconAccent - type: UnblockableSpeech - type: FootstepModifier footstepSoundCollection: diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml index b7bd8a0d54..6ecb751599 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/clone.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/clone.yml @@ -32,6 +32,7 @@ - BackwardsAccent - BarkAccent - BleatingAccent + - DamagedSiliconAccent - FrenchAccent - GermanAccent - LizardAccent -- 2.51.2