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