diff --git a/interface/stylesheet.dm b/interface/stylesheet.dm
index e48121437fdb..68469c80fcf5 100644
--- a/interface/stylesheet.dm
+++ b/interface/stylesheet.dm
@@ -214,4 +214,5 @@ h1.alert, h2.alert {color: #000000;}
.monkeyhive {color: #774704;}
.monkeylead {color: #774704; font-size: 2;}
+.pnarrate {color: #009AB2;}
"}
diff --git a/modular_splurt/code/modules/mob/say_vr.dm b/modular_splurt/code/modules/mob/say_vr.dm
new file mode 100644
index 000000000000..cd49dd200ca6
--- /dev/null
+++ b/modular_splurt/code/modules/mob/say_vr.dm
@@ -0,0 +1,50 @@
+/datum/emote/living/narrate
+ key = "narrate"
+ key_third_person = "narrates"
+ message = null
+ mob_type_blacklist_typecache = list(/mob/living/brain)
+ emote_type = EMOTE_OMNI
+
+/datum/emote/living/narrate/proc/check_invalid(mob/user, input)
+ if(stop_bad_mime.Find(input, 1, 1))
+ to_chat(user, "Invalid emote.")
+ return TRUE
+ return FALSE
+
+/datum/emote/living/narrate/run_emote(mob/user, params, type_override, intentional)
+ . = TRUE
+ if(jobban_isbanned(user, "emote"))
+ to_chat(user, "You cannot send narrates (banned).")
+ return FALSE
+ if(user.client && user.client.prefs.muted & MUTE_IC)
+ to_chat(user, "You cannot send IC messages (muted).")
+ return FALSE
+ if(!params)
+ return FALSE
+ message = params
+ if(type_override)
+ emote_type = type_override
+ if(!can_run_emote(user) || check_invalid(user, message))
+ return FALSE
+
+ user.log_message(message, LOG_EMOTE)
+ message = "([user]) [message]"
+
+ for(var/mob/M in GLOB.dead_mob_list)
+ if(!M.client || isnewplayer(M))
+ continue
+ var/T = get_turf(src)
+ if(M.stat == DEAD && M.client && (M.client.prefs.chat_toggles & CHAT_GHOSTSIGHT) && !(M in viewers(T, null)) && (user.client))
+ M.show_message("[FOLLOW_LINK(M, user)] " + message)
+
+ user.visible_message(message = message, self_message = message, omni = TRUE)
+
+/mob/living/verb/player_narrate(message as message)
+ set category = "IC"
+ set name = "Narrate (Player)"
+ set desc = "Narrate an action or event! An alternative to emoting, for when your emote shouldn't start with your name!"
+ if(GLOB.say_disabled)
+ to_chat(usr, "Speech is currently admin-disabled.")
+ return
+ message = trim(html_encode(message), MAX_MESSAGE_LEN)
+ emote("narrate", message=message)
diff --git a/tgstation.dme b/tgstation.dme
index 2b99ff1679df..f6bb6c146fbe 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -4622,6 +4622,7 @@
#include "modular_splurt\code\modules\mob\emote.dm"
#include "modular_splurt\code\modules\mob\mob.dm"
#include "modular_splurt\code\modules\mob\mob_defines.dm"
+#include "modular_splurt\code\modules\mob\say_vr.dm"
#include "modular_splurt\code\modules\mob\splurt_emotes.dm"
#include "modular_splurt\code\modules\mob\dead\crew_manifest.dm"
#include "modular_splurt\code\modules\mob\dead\dead.dm"
diff --git a/tgui/packages/tgui-panel/styles/goon/chat-dark.scss b/tgui/packages/tgui-panel/styles/goon/chat-dark.scss
index 2d31efcae133..7e3ddccbd4be 100644
--- a/tgui/packages/tgui-panel/styles/goon/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/goon/chat-dark.scss
@@ -1107,6 +1107,10 @@ blockquote.brass {
font-size: 80%;
}
+.pnarrate {
+ color: #009AB2;
+}
+
.connectionClosed, .fatalError {
background: red;
color: white;
diff --git a/tgui/packages/tgui-panel/styles/goon/chat-light.scss b/tgui/packages/tgui-panel/styles/goon/chat-light.scss
index 69db6e9026fc..8ef1b3af6717 100644
--- a/tgui/packages/tgui-panel/styles/goon/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/goon/chat-light.scss
@@ -1142,6 +1142,10 @@ blockquote.brass {
font-size: 80%;
}
+.pnarrate {
+ color: #009AB2;
+}
+
.connectionClosed,
.fatalError {
background: red;