--- /dev/null
+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<DamagedSiliconAccentComponent, AccentGetEvent>(OnAccent, after: [typeof(ReplacementAccentSystem)]);
+ }
+
+ private void OnAccent(Entity<DamagedSiliconAccentComponent> 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<DamageableComponent>(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);
+ }
+}
--- /dev/null
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Speech.Components;
+
+[RegisterComponent]
+[NetworkedComponent]
+public sealed partial class DamagedSiliconAccentComponent : Component
+{
+ /// <summary>
+ /// Enable damage corruption effects
+ /// </summary>
+ [DataField]
+ public bool EnableDamageCorruption = true;
+
+ /// <summary>
+ /// Override total damage for damage corruption effects
+ /// </summary>
+ [DataField]
+ public FixedPoint2? OverrideTotalDamage;
+
+ /// <summary>
+ /// The probability that a character will be corrupted when total damage at or above <see cref="MaxDamageCorruption" />.
+ /// </summary>
+ [DataField]
+ public float MaxDamageCorruption = 0.5f;
+
+ /// <summary>
+ /// Probability of character corruption will increase linearly to <see cref="MaxDamageCorruption" /> once until
+ /// total damage is at or above this value.
+ /// </summary>
+ [DataField]
+ public FixedPoint2 DamageAtMaxCorruption = 300;
+
+ /// <summary>
+ /// Enable charge level corruption effects
+ /// </summary>
+ [DataField]
+ public bool EnableChargeCorruption = true;
+
+ /// <summary>
+ /// Override charge level for charge level corruption effects
+ /// </summary>
+ [DataField]
+ public float? OverrideChargeLevel;
+
+ /// <summary>
+ /// If the power cell charge is below this value (as a fraction of maximum charge),
+ /// power corruption will begin to be applied.
+ /// </summary>
+ [DataField]
+ public float ChargeThresholdForPowerCorruption = 0.15f;
+
+ /// <summary>
+ /// Regardless of charge level, this is how many characters at the start of a message will be 100% safe
+ /// from being dropped.
+ /// </summary>
+ [DataField]
+ public int StartPowerCorruptionAtCharIdx = 8;
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ [DataField]
+ public int MaxPowerCorruptionAtCharIdx = 40;
+
+ /// <summary>
+ /// The maximum probability that a character will be dropped due to charge level.
+ /// </summary>
+ [DataField]
+ public float MaxDropProbFromPower = 0.5f;
+
+ /// <summary>
+ /// If a character is "dropped", this is the probability that the character will be turned into a period instead
+ /// of completely deleting the character.
+ /// </summary>
+ [DataField]
+ public float ProbToCorruptDotFromPower = 0.6f;
+}