diff --git a/Content.Server/Ghost/Components/SpookySpeakerComponent.cs b/Content.Server/Ghost/Components/SpookySpeakerComponent.cs new file mode 100644 index 0000000000..e843793a13 --- /dev/null +++ b/Content.Server/Ghost/Components/SpookySpeakerComponent.cs @@ -0,0 +1,37 @@ +using Content.Shared.Dataset; +using Robust.Shared.Prototypes; + +namespace Content.Server.Ghost.Components; + +/// +/// Causes this entity to react to ghost player using the "Boo!" action by speaking +/// a randomly chosen message from a specified set. +/// +[RegisterComponent, AutoGenerateComponentPause] +public sealed partial class SpookySpeakerComponent : Component +{ + /// + /// ProtoId of the LocalizedDataset to use for messages. + /// + [DataField(required: true)] + public ProtoId MessageSet; + + /// + /// Probability that this entity will speak if activated by a Boo action. + /// This is so whole banks of entities don't trigger at the same time. + /// + [DataField] + public float SpeakChance = 0.5f; + + /// + /// Minimum time that must pass after speaking before this entity can speak again. + /// + [DataField] + public TimeSpan Cooldown = TimeSpan.FromSeconds(30); + + /// + /// Time when the cooldown will have elapsed and the entity can speak again. + /// + [DataField, AutoPausedField] + public TimeSpan NextSpeakTime; +} diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs index 71a75ee4c6..25319557ae 100644 --- a/Content.Server/Ghost/GhostSystem.cs +++ b/Content.Server/Ghost/GhostSystem.cs @@ -34,6 +34,7 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server.Ghost @@ -63,6 +64,7 @@ namespace Content.Server.Ghost [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IRobustRandom _random = default!; private EntityQuery _ghostQuery; private EntityQuery _physicsQuery; @@ -127,7 +129,9 @@ namespace Content.Server.Ghost if (args.Handled) return; - var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius); + var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius).ToList(); + // Shuffle the possible targets so we don't favor any particular entities + _random.Shuffle(entities); var booCounter = 0; foreach (var ent in entities) diff --git a/Content.Server/Ghost/SpookySpeakerSystem.cs b/Content.Server/Ghost/SpookySpeakerSystem.cs new file mode 100644 index 0000000000..5a83ca39b1 --- /dev/null +++ b/Content.Server/Ghost/SpookySpeakerSystem.cs @@ -0,0 +1,51 @@ +using Content.Server.Chat.Systems; +using Content.Server.Ghost.Components; +using Content.Shared.Random.Helpers; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Ghost; + +public sealed class SpookySpeakerSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly ChatSystem _chat = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGhostBoo); + } + + private void OnGhostBoo(Entity entity, ref GhostBooEvent args) + { + // Only activate sometimes, so groups don't all trigger together + if (!_random.Prob(entity.Comp.SpeakChance)) + return; + + var curTime = _timing.CurTime; + // Enforce a delay between messages to prevent spam + if (curTime < entity.Comp.NextSpeakTime) + return; + + if (!_proto.TryIndex(entity.Comp.MessageSet, out var messages)) + return; + + // Grab a random localized message from the set + var message = _random.Pick(messages); + // Chatcode moment: messages starting with '.' are considered radio messages unless prefixed with '>' + // So this is a stupid trick to make the "...Oooo"-style messages work. + message = '>' + message; + // Say the message + _chat.TrySendInGameICMessage(entity, message, InGameICChatType.Speak, hideChat: true); + + // Set the delay for the next message + entity.Comp.NextSpeakTime = curTime + entity.Comp.Cooldown; + + args.Handled = true; + } +} diff --git a/Resources/Locale/en-US/ghost/spooky-speaker.ftl b/Resources/Locale/en-US/ghost/spooky-speaker.ftl new file mode 100644 index 0000000000..9c962eee77 --- /dev/null +++ b/Resources/Locale/en-US/ghost/spooky-speaker.ftl @@ -0,0 +1,18 @@ +spooky-speaker-generic-1 = ...ooOoooOOoooo... +spooky-speaker-generic-2 = ...can anyone hear me...? +spooky-speaker-generic-3 = ...join us... +spooky-speaker-generic-4 = ...come play with us... +spooky-speaker-generic-5 = KkkhhkhKhhkhkKk +spooky-speaker-generic-6 = Khhggkkghkk +spooky-speaker-generic-7 = khhkkkkKkhkkHk +spooky-speaker-generic-8 = ... +spooky-speaker-generic-9 = ...h-h-hello...? +spooky-speaker-generic-10 = Bzzzt +spooky-speaker-generic-11 = Weh +spooky-speaker-generic-12 = TREMBLE, MORTALS! +spooky-speaker-generic-13 = 4444444444 +spooky-speaker-generic-14 = ...I found you... + +spooky-speaker-recycler-1 = I HUNGER +spooky-speaker-recycler-2 = MORE! GIVE ME MORE! +spooky-speaker-recycler-3 = FEED ME diff --git a/Resources/Prototypes/Datasets/spooky_speakers.yml b/Resources/Prototypes/Datasets/spooky_speakers.yml new file mode 100644 index 0000000000..a64ab0a80e --- /dev/null +++ b/Resources/Prototypes/Datasets/spooky_speakers.yml @@ -0,0 +1,11 @@ +- type: localizedDataset + id: SpookySpeakerMessagesGeneric + values: + prefix: spooky-speaker-generic- + count: 14 + +- type: localizedDataset + id: SpookySpeakerMessagesRecycler + values: + prefix: spooky-speaker-recycler- + count: 3 diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml index d7f0297101..d57e1c4bf8 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/arcades.yml @@ -36,6 +36,9 @@ - type: Speech speechVerb: Robotic speechSounds: Vending + - type: SpookySpeaker + messageSet: SpookySpeakerMessagesGeneric + speakChance: 0.2 - type: Anchorable - type: Pullable - type: StaticPrice diff --git a/Resources/Prototypes/Entities/Structures/Machines/recycler.yml b/Resources/Prototypes/Entities/Structures/Machines/recycler.yml index f62923fe2a..2058c3c050 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/recycler.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/recycler.yml @@ -108,3 +108,10 @@ interactFailureString: petting-failure-generic interactSuccessSound: path: /Audio/Items/drill_hit.ogg + - type: SpookySpeaker + speakChance: 0.1 + cooldown: 120 + messageSet: SpookySpeakerMessagesRecycler + - type: Speech + speechVerb: Robotic + speechSounds: SyndieBorg diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 108693242c..f66c58f199 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -78,6 +78,9 @@ - type: Speech speechVerb: Robotic speechSounds: Vending + - type: SpookySpeaker + messageSet: SpookySpeakerMessagesGeneric + speakChance: 0.2 - type: IntrinsicRadioReceiver - type: ActiveRadio channels: