diff --git a/Content.Client/Speech/EntitySystems/StutteringSystem.cs b/Content.Client/Speech/EntitySystems/StutteringSystem.cs new file mode 100644 index 0000000000..0f3a99cbfa --- /dev/null +++ b/Content.Client/Speech/EntitySystems/StutteringSystem.cs @@ -0,0 +1,9 @@ +using Content.Shared.Speech.EntitySystems; + +namespace Content.Client.Speech.EntitySystems +{ + public class StutteringSystem : SharedStutteringSystem + { + + } +} diff --git a/Content.Server/Speech/AccentSystem.cs b/Content.Server/Speech/AccentSystem.cs index 591d40c183..71b0ebf52f 100644 --- a/Content.Server/Speech/AccentSystem.cs +++ b/Content.Server/Speech/AccentSystem.cs @@ -10,7 +10,7 @@ namespace Content.Server.Speech { [Dependency] private readonly IChatManager _chatManager = default!; - public static readonly Regex SentenceRegex = new(@"(?<=[\.!\?])"); + public static readonly Regex SentenceRegex = new(@"(?<=[\.!\?])", RegexOptions.Compiled); public override void Initialize() { diff --git a/Content.Server/Speech/Components/StutteringAccentComponent.cs b/Content.Server/Speech/Components/StutteringAccentComponent.cs new file mode 100644 index 0000000000..e54ce164da --- /dev/null +++ b/Content.Server/Speech/Components/StutteringAccentComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Speech.Components +{ + [RegisterComponent] + public class StutteringAccentComponent : Component + { + public override string Name => "StutteringAccent"; + } +} diff --git a/Content.Server/Speech/EntitySystems/StutteringSystem.cs b/Content.Server/Speech/EntitySystems/StutteringSystem.cs new file mode 100644 index 0000000000..fdf3e8a286 --- /dev/null +++ b/Content.Server/Speech/EntitySystems/StutteringSystem.cs @@ -0,0 +1,84 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; +using Content.Server.Alert; +using Content.Server.Speech.Components; +using Content.Shared.Alert; +using Content.Shared.Speech.EntitySystems; +using Content.Shared.StatusEffect; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Random; + +namespace Content.Server.Speech.EntitySystems +{ + public class StutteringSystem : SharedStutteringSystem + { + [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + private const string StutterKey = "Stutter"; + + // Regex of characters to stutter. + private static readonly Regex Stutter = new(@"[b-df-hj-np-tv-wxyz]", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public override void Initialize() + { + SubscribeLocalEvent(OnAccent); + } + + public override void DoStutter(EntityUid uid, TimeSpan time, StatusEffectsComponent? status = null, SharedAlertsComponent? alerts = null) + { + if (!Resolve(uid, ref status, false)) + return; + + if (!_statusEffectsSystem.HasStatusEffect(uid, StutterKey, status)) + _statusEffectsSystem.TryAddStatusEffect(uid, StutterKey, time, status, alerts); + else + _statusEffectsSystem.TryAddTime(uid, StutterKey, time, status); + } + + private void OnAccent(EntityUid uid, StutteringAccentComponent component, AccentGetEvent args) + { + args.Message = Accentuate(args.Message); + } + + public string Accentuate(string message) + { + var length = message.Length; + + var finalMessage = new StringBuilder(); + + string newLetter; + + for (var i = 0; i < length; i++) + { + newLetter = message[i].ToString(); + if (Stutter.IsMatch(newLetter) && _random.Prob(0.8f)) + { + if (_random.Prob(0.1f)) + { + newLetter = $"{newLetter}-{newLetter}-{newLetter}-{newLetter}"; + } + else if (_random.Prob(0.2f)) + { + newLetter = $"{newLetter}-{newLetter}-{newLetter}"; + } + else if (_random.Prob(0.05f)) + { + newLetter = ""; + } + else + { + newLetter = $"{newLetter}-{newLetter}"; + } + } + + finalMessage.Append(newLetter); + } + + return finalMessage.ToString(); + } + } +} diff --git a/Content.Server/Stunnable/StunbatonSystem.cs b/Content.Server/Stunnable/StunbatonSystem.cs index d6c23433c3..d7c105f879 100644 --- a/Content.Server/Stunnable/StunbatonSystem.cs +++ b/Content.Server/Stunnable/StunbatonSystem.cs @@ -3,6 +3,7 @@ using System.Linq; using Content.Server.Items; using Content.Server.Jittering; using Content.Server.PowerCell.Components; +using Content.Server.Speech.EntitySystems; using Content.Server.Stunnable.Components; using Content.Server.Weapon.Melee; using Content.Shared.ActionBlocker; @@ -27,6 +28,7 @@ namespace Content.Server.Stunnable public class StunbatonSystem : EntitySystem { [Dependency] private readonly StunSystem _stunSystem = default!; + [Dependency] private readonly StutteringSystem _stutteringSystem = default!; [Dependency] private readonly SharedJitteringSystem _jitterSystem = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; @@ -139,7 +141,9 @@ namespace Content.Server.Stunnable _stunSystem.TrySlowdown(entity.Uid, TimeSpan.FromSeconds(comp.SlowdownTime), 0.5f, 0.5f, status); } - _jitterSystem.DoJitter(entity.Uid, TimeSpan.FromSeconds(comp.SlowdownTime)); + var slowdownTime = TimeSpan.FromSeconds(comp.SlowdownTime); + _jitterSystem.DoJitter(entity.Uid, slowdownTime, status:status); + _stutteringSystem.DoStutter(entity.Uid, slowdownTime, status); if (!comp.Owner.TryGetComponent(out var slot) || slot.Cell == null || !(slot.Cell.CurrentCharge < comp.EnergyPerUse)) return; diff --git a/Content.Shared/Speech/EntitySystems/SharedStutteringSystem.cs b/Content.Shared/Speech/EntitySystems/SharedStutteringSystem.cs new file mode 100644 index 0000000000..ed9c5d545f --- /dev/null +++ b/Content.Shared/Speech/EntitySystems/SharedStutteringSystem.cs @@ -0,0 +1,15 @@ +using System; +using Content.Shared.Alert; +using Content.Shared.StatusEffect; +using Robust.Shared.GameObjects; + +namespace Content.Shared.Speech.EntitySystems +{ + public abstract class SharedStutteringSystem : EntitySystem + { + // For code in shared... I imagine we ain't getting accent prediction anytime soon so let's not bother. + public virtual void DoStutter(EntityUid uid, TimeSpan time, StatusEffectsComponent? status = null, SharedAlertsComponent? alerts = null) + { + } + } +} diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs index be854aacb7..dc19da84d8 100644 --- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs @@ -213,9 +213,11 @@ namespace Content.Shared.StatusEffect Resolve(uid, ref alerts, false); var state = status.ActiveEffects[key]; - if (state.RelevantComponent != null) + + // There are cases where a status effect component might be server-only, so TryGetRegistration... + if (state.RelevantComponent != null && _componentFactory.TryGetRegistration(state.RelevantComponent, out var registration)) { - var type = _componentFactory.GetRegistration(state.RelevantComponent).Type; + var type = registration.Type; // Make sure the component is actually there first. // Maybe a badmin badminned the component away, diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 1a686e2ac0..d26b84579b 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -67,6 +67,7 @@ - Stun - KnockedDown - SlowedDown + - Stutter # Other - type: Inventory - type: Clickable diff --git a/Resources/Prototypes/status_effects.yml b/Resources/Prototypes/status_effects.yml index 4cd110b1fd..866de48908 100644 --- a/Resources/Prototypes/status_effects.yml +++ b/Resources/Prototypes/status_effects.yml @@ -15,3 +15,6 @@ - type: statusEffect id: Jitter alwaysAllowed: true + +- type: statusEffect + id: Stutter