]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Corrupt borg speech if they are damaged or low power (#35318)
authorQuantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Fri, 14 Mar 2025 15:31:09 +0000 (11:31 -0400)
committerGitHub <noreply@github.com>
Fri, 14 Mar 2025 15:31:09 +0000 (02:31 +1100)
* - 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<T> 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

Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs [new file with mode: 0644]
Content.Shared/Speech/Components/DamagedSiliconAccentComponent.cs [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
Resources/Prototypes/Entities/Mobs/Player/clone.yml

diff --git a/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs b/Content.Server/Speech/EntitySystems/DamagedSiliconAccentSystem.cs
new file mode 100644 (file)
index 0000000..757d668
--- /dev/null
@@ -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<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);
+    }
+}
diff --git a/Content.Shared/Speech/Components/DamagedSiliconAccentComponent.cs b/Content.Shared/Speech/Components/DamagedSiliconAccentComponent.cs
new file mode 100644 (file)
index 0000000..fa1377b
--- /dev/null
@@ -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
+{
+    /// <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;
+}
index 3995e071fb38d61a975526a3d1552049a32f8917..96e8eda43d6a1a98cedd15b731390cb51c494282 100644 (file)
     sounds:
       Unsexed: UnisexSilicon
     screamAction: null
+  - type: DamagedSiliconAccent
   - type: UnblockableSpeech
   - type: FootstepModifier
     footstepSoundCollection:
index b7bd8a0d549ac5c00e4c2c768e1224b11ea6cc68..6ecb75159902a248ac6dc8bcb847c40c53c3e4c2 100644 (file)
@@ -32,6 +32,7 @@
   - BackwardsAccent
   - BarkAccent
   - BleatingAccent
+  - DamagedSiliconAccent
   - FrenchAccent
   - GermanAccent
   - LizardAccent