diff --git a/Content.Client/Prayer/PrayerSystem.cs b/Content.Client/Prayer/PrayerSystem.cs
new file mode 100644
index 0000000000..30f4a647da
--- /dev/null
+++ b/Content.Client/Prayer/PrayerSystem.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Administration;
+using Content.Shared.Prayer;
+
+namespace Content.Client.Prayer;
+///
+/// System to handle subtle messages and praying
+///
+public sealed class PrayerSystem : SharedPrayerSystem
+{
+ protected override void OnPrayerTextMessage(PrayerTextMessage message, EntitySessionEventArgs eventArgs)
+ {
+ var bwoinkMessage = new SharedBwoinkSystem.BwoinkTextMessage(eventArgs.SenderSession.UserId,
+ eventArgs.SenderSession.UserId, message.Text);
+
+ RaiseNetworkEvent(bwoinkMessage);
+ }
+}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs
index a03dd94513..6d36c52d1a 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs
@@ -10,6 +10,7 @@ using Content.Server.Ghost.Roles;
using Content.Server.Mind.Commands;
using Content.Server.Mind.Components;
using Content.Server.Players;
+using Content.Server.Prayer;
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Administration;
@@ -45,6 +46,7 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem = default!;
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly PrayerSystem _prayerSystem = default!;
private readonly Dictionary _openSolutionUis = new();
@@ -80,6 +82,21 @@ namespace Content.Server.Administration.Systems
verb.Impact = LogImpact.Low;
args.Verbs.Add(verb);
+ // Subtle Messages
+ Verb prayerVerb = new();
+ prayerVerb.Text = Loc.GetString("prayer-verbs-subtle-message");
+ prayerVerb.Category = VerbCategory.Admin;
+ prayerVerb.IconTexture = "/Textures/Interface/pray.svg.png";
+ prayerVerb.Act = () =>
+ {
+ _quickDialog.OpenDialog(player, "Subtle Message", "Message", "Popup Message", (string message, string popupMessage) =>
+ {
+ _prayerSystem.SendSubtleMessage(targetActor.PlayerSession, message, popupMessage == "" ? Loc.GetString("prayer-popup-subtle-default") : popupMessage);
+ });
+ };
+ prayerVerb.Impact = LogImpact.Low;
+ args.Verbs.Add(prayerVerb);
+
// Freeze
var frozen = HasComp(args.Target);
args.Verbs.Add(new Verb
diff --git a/Content.Server/Prayer/PrayableComponent.cs b/Content.Server/Prayer/PrayableComponent.cs
new file mode 100644
index 0000000000..b174bb8c28
--- /dev/null
+++ b/Content.Server/Prayer/PrayableComponent.cs
@@ -0,0 +1,17 @@
+namespace Content.Server.Prayer
+{
+ ///
+ /// Allows an entity to be prayed on in the context menu
+ ///
+ [RegisterComponent]
+ public sealed class PrayableComponent : Component
+ {
+ ///
+ /// If bible users are only allowed to use this prayable entity
+ ///
+ [DataField("bibleUserOnly")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool BibleUserOnly;
+ }
+}
+
diff --git a/Content.Server/Prayer/PrayerSystem.cs b/Content.Server/Prayer/PrayerSystem.cs
new file mode 100644
index 0000000000..2b12f37dd9
--- /dev/null
+++ b/Content.Server/Prayer/PrayerSystem.cs
@@ -0,0 +1,95 @@
+using Content.Server.Administration;
+using Content.Server.Bible.Components;
+using Content.Server.Chat.Managers;
+using Content.Server.Popups;
+using Content.Shared.Database;
+using Content.Shared.Popups;
+using Robust.Server.Player;
+using Robust.Shared.Player;
+using Content.Shared.Chat;
+using Content.Shared.Prayer;
+using Content.Shared.Verbs;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Prayer;
+///
+/// System to handle subtle messages and praying
+///
+public sealed class PrayerSystem : SharedPrayerSystem
+{
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly QuickDialogSystem _quickDialog = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(AddPrayVerb);
+ }
+
+ private void AddPrayVerb(EntityUid uid, PrayableComponent comp, GetVerbsEvent args)
+ {
+ // if it doesn't have an actor and we can't reach it then don't add the verb
+ if (!EntityManager.TryGetComponent(args.User, out var actor))
+ return;
+
+ if (!args.CanAccess)
+ return;
+
+ ActivationVerb prayerVerb = new();
+ prayerVerb.Text = Loc.GetString("prayer-verbs-pray");
+ prayerVerb.IconTexture = "/Textures/Interface/pray.svg.png";
+ prayerVerb.Act = () =>
+ {
+ if (comp.BibleUserOnly && !EntityManager.TryGetComponent(args.User, out var bibleUser))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("prayer-popup-notify-locked"), uid, Filter.Empty().AddPlayer(actor.PlayerSession), PopupType.Large);
+ return;
+ }
+
+ _quickDialog.OpenDialog(actor.PlayerSession, "Pray", "Message", (string message) =>
+ {
+ Pray(actor.PlayerSession, message);
+ });
+ };
+ prayerVerb.Impact = LogImpact.Low;
+ args.Verbs.Add(prayerVerb);
+ }
+
+ ///
+ /// Subtly messages a player by giving them a popup and a chat message.
+ ///
+ /// The IPlayerSession that you want to send the message to
+ /// The main message sent to the player via the chatbox
+ /// The popup to notify the player, also prepended to the messageString
+ public void SendSubtleMessage(IPlayerSession target, string messageString, string popupMessage)
+ {
+ if (target.AttachedEntity == null)
+ return;
+
+ _popupSystem.PopupEntity(popupMessage, target.AttachedEntity.Value, Filter.Empty().AddPlayer(target), PopupType.Large);
+ _chatManager.ChatMessageToOne(ChatChannel.Local, messageString, popupMessage + " \"{0}\"", EntityUid.Invalid, false, target.ConnectedClient);
+ }
+
+ ///
+ /// Sends a message to the admin channel with a message and username
+ ///
+ /// The IPlayerSession who sent the original message
+ /// Message to be sent to the admin chat
+ ///
+ /// You may be wondering, "Why the admin chat, specifically? Nobody even reads it!"
+ /// Exactly.
+ ///
+ public void Pray(IPlayerSession sender, string message)
+ {
+ if (sender.AttachedEntity == null)
+ return;
+
+ _popupSystem.PopupEntity(Loc.GetString("prayer-popup-notify-sent"), sender.AttachedEntity.Value, Filter.Empty().AddPlayer(sender), PopupType.Medium);
+
+ var networkMessage = new PrayerTextMessage(Loc.GetString("prayer-chat-notify", ("message", message)));
+
+ RaiseNetworkEvent(networkMessage, Filter.Empty().AddPlayer(sender));
+ }
+}
diff --git a/Content.Shared/Prayer/SharedPrayerSystem.cs b/Content.Shared/Prayer/SharedPrayerSystem.cs
new file mode 100644
index 0000000000..fafdbfe043
--- /dev/null
+++ b/Content.Shared/Prayer/SharedPrayerSystem.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Prayer;
+
+///
+/// Shared system for handling Prayers
+///
+public abstract class SharedPrayerSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ SubscribeNetworkEvent(OnPrayerTextMessage);
+ }
+
+ protected virtual void OnPrayerTextMessage(PrayerTextMessage message, EntitySessionEventArgs eventArgs)
+ {
+ // Specific side code in target.
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class PrayerTextMessage : EntityEventArgs
+ {
+ public string Text { get; }
+
+ public PrayerTextMessage(string text)
+ {
+ Text = text;
+ }
+ }
+}
diff --git a/Resources/Locale/en-US/prayers/prayers.ftl b/Resources/Locale/en-US/prayers/prayers.ftl
new file mode 100644
index 0000000000..09e9759e03
--- /dev/null
+++ b/Resources/Locale/en-US/prayers/prayers.ftl
@@ -0,0 +1,7 @@
+prayer-verbs-subtle-message = Subtle Message
+prayer-verbs-pray = Pray
+prayer-chat-notify = PRAYER: {$message}
+
+prayer-popup-subtle-default = You hear a voice in your head...
+prayer-popup-notify-sent = Your message has been sent to the gods...
+prayer-popup-notify-locked = You don't feel worthy enough...
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
index 0b48fdf2c4..f948e85685 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
@@ -18,6 +18,8 @@
damageOnUntrainedUse: ## What a non-chaplain takes when attempting to heal someone
groups:
Burn: 10
+ - type: Prayable
+ bibleUserOnly: true
- type: Summonable
specialItem: SpawnPointGhostRemilia
- type: ItemCooldown
diff --git a/Resources/Prototypes/Entities/Structures/Furniture/altar.yml b/Resources/Prototypes/Entities/Structures/Furniture/altar.yml
index 061ef7cd39..77dc6a85b0 100644
--- a/Resources/Prototypes/Entities/Structures/Furniture/altar.yml
+++ b/Resources/Prototypes/Entities/Structures/Furniture/altar.yml
@@ -8,6 +8,7 @@
- type: Anchorable
- type: Transform
noRot: true
+ - type: Prayable
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
diff --git a/Resources/Textures/Interface/pray.svg b/Resources/Textures/Interface/pray.svg
new file mode 100644
index 0000000000..37023f12cd
--- /dev/null
+++ b/Resources/Textures/Interface/pray.svg
@@ -0,0 +1,7 @@
+
diff --git a/Resources/Textures/Interface/pray.svg.png b/Resources/Textures/Interface/pray.svg.png
new file mode 100644
index 0000000000..c2a8df8266
Binary files /dev/null and b/Resources/Textures/Interface/pray.svg.png differ
diff --git a/Resources/Textures/Interface/pray.svg.png.yml b/Resources/Textures/Interface/pray.svg.png.yml
new file mode 100644
index 0000000000..dabd6601f7
--- /dev/null
+++ b/Resources/Textures/Interface/pray.svg.png.yml
@@ -0,0 +1,2 @@
+sample:
+ filter: true