diff --git a/code/__DEFINES/say.dm b/code/__DEFINES/say.dm
index e71561defcdb..72a7cdc5ed02 100644
--- a/code/__DEFINES/say.dm
+++ b/code/__DEFINES/say.dm
@@ -104,3 +104,4 @@
//Used in visible_message_flags, audible_message_flags and runechat_flags
#define EMOTE_MESSAGE (1<<0)
+#define LOOC_MESSAGE (1<<1) //monke: looc
diff --git a/code/__DEFINES/speech_channels.dm b/code/__DEFINES/speech_channels.dm
index d17b6b8f9a84..65ab0d51fe68 100644
--- a/code/__DEFINES/speech_channels.dm
+++ b/code/__DEFINES/speech_channels.dm
@@ -5,3 +5,4 @@
#define OOC_CHANNEL "OOC"
#define ADMIN_CHANNEL "Admin"
#define MENTOR_CHANNEL "Mentor"
+#define LOOC_CHANNEL "LOOC" // monkestation edit: add LOOC
diff --git a/code/__DEFINES/~monkestation/chat.dm b/code/__DEFINES/~monkestation/chat.dm
new file mode 100644
index 000000000000..1b1a4abda2d8
--- /dev/null
+++ b/code/__DEFINES/~monkestation/chat.dm
@@ -0,0 +1 @@
+#define MESSAGE_TYPE_LOOC "looc"
diff --git a/code/__DEFINES/~monkestation/keybinding.dm b/code/__DEFINES/~monkestation/keybinding.dm
new file mode 100644
index 000000000000..b074c433ef49
--- /dev/null
+++ b/code/__DEFINES/~monkestation/keybinding.dm
@@ -0,0 +1 @@
+#define COMSIG_KB_CLIENT_LOOC_DOWN "keybinding_client_looc_down"
diff --git a/code/__DEFINES/~monkestation/logging.dm b/code/__DEFINES/~monkestation/logging.dm
new file mode 100644
index 000000000000..af3bff1232d8
--- /dev/null
+++ b/code/__DEFINES/~monkestation/logging.dm
@@ -0,0 +1 @@
+#define LOG_CATEGORY_GAME_LOOC "game-looc"
diff --git a/code/__DEFINES/~monkestation/span.dm b/code/__DEFINES/~monkestation/span.dm
index 11666ba5c1e9..4da802b4f72a 100644
--- a/code/__DEFINES/~monkestation/span.dm
+++ b/code/__DEFINES/~monkestation/span.dm
@@ -5,5 +5,4 @@
#define span_clockred(str) ("" + str + "")
#define span_ratvar(str) ("" + str + "")
-
#define REQUEST_MENTORHELP "request_mentorhelp"
diff --git a/code/controllers/configuration/entries/monkestation.dm b/code/controllers/configuration/entries/monkestation.dm
index 9df2b8d34dca..9410ab93edbd 100644
--- a/code/controllers/configuration/entries/monkestation.dm
+++ b/code/controllers/configuration/entries/monkestation.dm
@@ -26,3 +26,5 @@
//Endpoint for Github Issues, the `owner/repo` part.
/datum/config_entry/string/issue_slug
protection = CONFIG_ENTRY_LOCKED
+
+/datum/config_entry/flag/looc_enabled
diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm
index 3d9123a0c317..7459e196a6b5 100644
--- a/code/datums/chatmessage.dm
+++ b/code/datums/chatmessage.dm
@@ -299,8 +299,10 @@
return
// Display visual above source
- if(runechat_flags & EMOTE_MESSAGE)
+ if(CHECK_BITFIELD(runechat_flags, EMOTE_MESSAGE))
new /datum/chatmessage(raw_message, speaker, src, message_language, list("emote", "italics"))
+ else if(CHECK_BITFIELD(runechat_flags, LOOC_MESSAGE))
+ new /datum/chatmessage(raw_message, speaker, src, message_language, list("looc", "italics"))
else
new /datum/chatmessage(raw_message, speaker, src, message_language, spans)
diff --git a/code/datums/keybinding/living.dm b/code/datums/keybinding/living.dm
index 38e324e9769c..9fa539c0aa1d 100644
--- a/code/datums/keybinding/living.dm
+++ b/code/datums/keybinding/living.dm
@@ -61,7 +61,7 @@
return TRUE
/datum/keybinding/living/rest
- hotkey_keys = list("U")
+ hotkey_keys = list("R") // monke: move this, so LOOC can be U, adjacent to other communication keys.
name = "rest"
full_name = "Rest"
description = "Lay down, or get up."
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index beed817d2ffe..745402dc9a3d 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -42,6 +42,7 @@ GLOBAL_PROTECT(admin_verbs_admin)
/datum/admins/proc/toggleguests, /*toggles whether guests can join the current game*/
/datum/admins/proc/toggleooc, /*toggles ooc on/off for everyone*/
/datum/admins/proc/toggleoocdead, /*toggles ooc on/off for everyone who is dead*/
+ /datum/admins/proc/togglelooc, /*MONKESTATION EDIT; toggles looc on/off for everyone*/
/datum/admins/proc/trophy_manager,
/datum/admins/proc/view_all_circuits,
/datum/admins/proc/open_artifactpanel,
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index bc55615573b2..4c0c0e11a297 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -1133,6 +1133,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if(OOC_CHANNEL)
var/ooc = tgui_say_create_open_command(OOC_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[ooc]")
+ if(LOOC_CHANNEL) // monke edit: looc
+ var/looc = tgui_say_create_open_command(LOOC_CHANNEL)
+ winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[looc]")
if(ADMIN_CHANNEL)
if(holder)
var/asay = tgui_say_create_open_command(ADMIN_CHANNEL)
diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm
index 9ff47aae3fe1..444d67812ae1 100644
--- a/code/modules/tgui_input/say_modal/modal.dm
+++ b/code/modules/tgui_input/say_modal/modal.dm
@@ -82,7 +82,7 @@
if(!payload?["channel"])
CRASH("No channel provided to an open TGUI-Say")
window_open = TRUE
- if(payload["channel"] != OOC_CHANNEL && (payload["channel"] != ADMIN_CHANNEL) && (payload["channel"] != MENTOR_CHANNEL))
+ if(payload["channel"] != OOC_CHANNEL && payload["channel"] != LOOC_CHANNEL && (payload["channel"] != ADMIN_CHANNEL) && (payload["channel"] != MENTOR_CHANNEL)) // monke: add LOOC
start_thinking()
if(client.typing_indicators)
log_speech_indicators("[key_name(client)] started typing at [loc_name(client.mob)], indicators enabled.")
diff --git a/code/modules/tgui_input/say_modal/speech.dm b/code/modules/tgui_input/say_modal/speech.dm
index 4601ebdaabd9..247f29bac59d 100644
--- a/code/modules/tgui_input/say_modal/speech.dm
+++ b/code/modules/tgui_input/say_modal/speech.dm
@@ -44,6 +44,9 @@
if(OOC_CHANNEL)
client.ooc(entry)
return TRUE
+ if(LOOC_CHANNEL)
+ client.looc(entry)
+ return TRUE
if(ADMIN_CHANNEL)
client.cmd_admin_say(entry)
return TRUE
@@ -91,7 +94,7 @@
return TRUE
if(type == "force")
var/target_channel = payload["channel"]
- if(target_channel == ME_CHANNEL || target_channel == OOC_CHANNEL)
+ if(target_channel == ME_CHANNEL || target_channel == OOC_CHANNEL || target_channel == LOOC_CHANNEL) // monkestation: add looc
target_channel = SAY_CHANNEL // No ooc leaks
delegate_speech(alter_entry(payload), target_channel)
return TRUE
diff --git a/config/game_options.txt b/config/game_options.txt
index 68a8f1f1d132..b336b336d268 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -15,6 +15,10 @@ REVIVAL_BRAIN_LIFE -1
## Comment this out if you want OOC to be automatically disabled during the round, it will be enabled during the lobby and after the round end results.
OOC_DURING_ROUND
+## LOOC
+## Comment this out to disable LOOC
+LOOC_ENABLED
+
## EMOJI ###
## Comment this out if you want to disable emojis
EMOJIS
diff --git a/interface/stylesheet.dm b/interface/stylesheet.dm
index faa70d81cee9..b85562b90d87 100644
--- a/interface/stylesheet.dm
+++ b/interface/stylesheet.dm
@@ -27,6 +27,7 @@ em {font-style: normal; font-weight: bold;}
.oocplain {}
.warningplain {}
.ooc { font-weight: bold;}
+.looc { font-weight: bold;}
.adminobserverooc {color: #0099cc; font-weight: bold;}
.adminooc {color: #700038; font-weight: bold;}
diff --git a/monkestation/code/datums/keybinding/communication.dm b/monkestation/code/datums/keybinding/communication.dm
new file mode 100644
index 000000000000..502ddd406349
--- /dev/null
+++ b/monkestation/code/datums/keybinding/communication.dm
@@ -0,0 +1,5 @@
+/datum/keybinding/client/communication/looc
+ hotkey_keys = list("U")
+ name = LOOC_CHANNEL
+ full_name = "Local Out Of Character Say (LOOC)"
+ keybind_signal = COMSIG_KB_CLIENT_LOOC_DOWN
diff --git a/monkestation/code/modules/client/preferences/admin.dm b/monkestation/code/modules/client/preferences/admin.dm
new file mode 100644
index 000000000000..75d9cff3d9ff
--- /dev/null
+++ b/monkestation/code/modules/client/preferences/admin.dm
@@ -0,0 +1,13 @@
+/datum/preference/choiced/admin_hear_looc
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "admin_hear_looc"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/choiced/admin_hear_looc/init_possible_values()
+ return list("Always", "When Observing", "Never")
+
+/datum/preference/choiced/admin_hear_looc/create_default_value()
+ return "Always"
+
+/datum/preference/choiced/admin_hear_looc/is_accessible(datum/preferences/preferences)
+ return ..() && is_admin(preferences.parent) && CONFIG_GET(flag/looc_enabled)
diff --git a/monkestation/code/modules/client/preferences/runechat.dm b/monkestation/code/modules/client/preferences/runechat.dm
new file mode 100644
index 000000000000..e3d618a22589
--- /dev/null
+++ b/monkestation/code/modules/client/preferences/runechat.dm
@@ -0,0 +1,7 @@
+/datum/preference/toggle/enable_runechat_looc
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "see_looc_on_map"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/enable_runechat_looc/is_accessible(datum/preferences/preferences)
+ return ..() && CONFIG_GET(flag/looc_enabled)
diff --git a/monkestation/code/modules/client/verbs/looc.dm b/monkestation/code/modules/client/verbs/looc.dm
new file mode 100644
index 000000000000..255d1af8d365
--- /dev/null
+++ b/monkestation/code/modules/client/verbs/looc.dm
@@ -0,0 +1,131 @@
+// LOOC ported from Bee, which was in turn ported from Citadel
+
+GLOBAL_VAR_INIT(looc_allowed, TRUE)
+
+/client/verb/looc(msg as text)
+ set name = "LOOC"
+ set desc = "Local OOC, seen only by those in view."
+ set category = "OOC"
+
+ if(GLOB.say_disabled) //This is here to try to identify lag problems
+ to_chat(usr, span_danger("Speech is currently admin-disabled."))
+ return
+
+ if(!mob)
+ return
+
+ VALIDATE_CLIENT(src)
+
+ if(is_banned_from(mob.ckey, "OOC"))
+ to_chat(src, "You have been banned from OOC and LOOC.")
+ return
+ if(!CHECK_BITFIELD(prefs.chat_toggles, CHAT_OOC))
+ to_chat(src, span_danger("You have OOC (and therefore LOOC) muted."))
+ return
+
+ msg = trim(sanitize(msg), MAX_MESSAGE_LEN)
+ if(!length(msg))
+ return
+
+ var/raw_msg = msg
+
+ var/list/filter_result = is_ooc_filtered(msg)
+ if (!CAN_BYPASS_FILTER(usr) && filter_result)
+ REPORT_CHAT_FILTER_TO_USER(usr, filter_result)
+ log_filter("LOOC", msg, filter_result)
+ return
+
+ // Protect filter bypassers from themselves.
+ // Demote hard filter results to soft filter results if necessary due to the danger of accidentally speaking in OOC.
+ var/list/soft_filter_result = filter_result || is_soft_ooc_filtered(msg)
+
+ if (soft_filter_result)
+ if(tgui_alert(usr, "Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to say it?", "Soft Blocked Word", list("Yes", "No")) != "Yes")
+ return
+ message_admins("[ADMIN_LOOKUPFLW(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term. Message: \"[msg]\"")
+ log_admin_private("[key_name(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term. Message: \"[msg]\"")
+
+ // letting mentors use this as they might actually use this to help people. this cannot possibly go wrong! :clueless:
+ if(!holder)
+ if(!CONFIG_GET(flag/looc_enabled))
+ to_chat(src, span_danger("LOOC is disabled."))
+ return
+ if(!GLOB.dooc_allowed && (mob.stat == DEAD) && SSticker.current_state < GAME_STATE_FINISHED && !mentor_datum)
+ to_chat(usr, span_danger("LOOC for dead mobs has been turned off."))
+ return
+ if(CHECK_BITFIELD(prefs.muted, MUTE_OOC))
+ to_chat(src, span_danger("You cannot use LOOC (muted)."))
+ return
+ if(handle_spam_prevention(msg, MUTE_OOC))
+ return
+ if(findtext(msg, "byond://"))
+ to_chat(src, span_danger("Advertising other servers is not allowed."))
+ log_admin("[key_name(src)] has attempted to advertise in LOOC: [msg]")
+ return
+ if(mob.stat && SSticker.current_state < GAME_STATE_FINISHED && !mentor_datum)
+ to_chat(src, span_danger("You cannot salt in LOOC while unconscious or dead."))
+ return
+ if(isdead(mob) && SSticker.current_state < GAME_STATE_FINISHED && !mentor_datum)
+ to_chat(src, span_danger("You cannot use LOOC while ghosting."))
+ return
+ if(is_banned_from(ckey, "OOC"))
+ to_chat(src, span_danger("You have been banned from OOC."))
+ return
+ if(QDELETED(src))
+ return
+
+ msg = emoji_parse(msg)
+ mob.log_talk(raw_msg, LOG_OOC, tag = "LOOC")
+
+ var/list/hearers = list()
+ for(var/mob/hearer in get_hearers_in_view(9, mob))
+ var/client/client = hearer.client
+ if(QDELETED(client) || !CHECK_BITFIELD(client.prefs.chat_toggles, CHAT_OOC))
+ continue
+ hearers[client] = TRUE
+ if((client in GLOB.admins) && is_admin_looc_omnipotent(client))
+ continue
+ to_chat(hearer, span_looc("[span_prefix("LOOC:")] [span_name("[mob.name]")]: [msg]"), type = MESSAGE_TYPE_LOOC, avoid_highlighting = (hearer == mob))
+ if(client.prefs.read_preference(/datum/preference/toggle/enable_runechat_looc))
+ hearer.create_chat_message(mob, /datum/language/common, "\[LOOC: [raw_msg]\]", runechat_flags = LOOC_MESSAGE)
+
+ for(var/client/client in GLOB.admins)
+ if(!CHECK_BITFIELD(client.prefs.chat_toggles, CHAT_OOC) || !is_admin_looc_omnipotent(client))
+ continue
+ var/prefix = "[hearers[client] ? "" : "(R)"]LOOC"
+ if(client.prefs.read_preference(/datum/preference/toggle/enable_runechat_looc))
+ client.mob?.create_chat_message(mob, /datum/language/common, "\[LOOC: [raw_msg]\]", runechat_flags = LOOC_MESSAGE)
+ to_chat(client, span_looc("[span_prefix("[prefix]:")] [ADMIN_LOOKUPFLW(mob)]: [msg]"), type = MESSAGE_TYPE_LOOC, avoid_highlighting = (client == src))
+
+/// Logging for messages sent in LOOC
+/proc/log_looc(text, list/data)
+ logger.Log(LOG_CATEGORY_GAME_LOOC, text, data)
+
+//admin tool
+/proc/toggle_looc(toggle = null)
+ if(!isnull(toggle)) //if we're specifically en/disabling ooc
+ GLOB.looc_allowed = toggle
+ else //otherwise just toggle it
+ GLOB.looc_allowed = !GLOB.looc_allowed
+ to_chat(world, "LOOC channel has been globally [GLOB.looc_allowed ? "enabled" : "disabled"].")
+
+/datum/admins/proc/togglelooc()
+ set category = "Server"
+ set name = "Toggle LOOC"
+ if(!check_rights(R_ADMIN))
+ return
+ toggle_looc()
+ log_admin("[key_name(usr)] toggled LOOC.")
+ message_admins("[key_name_admin(usr)] toggled LOOC.")
+ SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle LOOC", "[GLOB.looc_allowed ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+
+/proc/is_admin_looc_omnipotent(client/admin)
+ if(QDELETED(admin))
+ return FALSE
+ switch(admin.prefs.read_preference(/datum/preference/choiced/admin_hear_looc))
+ if("Always")
+ return TRUE
+ if("When Observing")
+ return isdead(admin.mob) || admin.mob.stat == DEAD
+ else
+ return FALSE
diff --git a/monkestation/code/modules/logging/categories/log_category_game.dm b/monkestation/code/modules/logging/categories/log_category_game.dm
new file mode 100644
index 000000000000..2e476a846204
--- /dev/null
+++ b/monkestation/code/modules/logging/categories/log_category_game.dm
@@ -0,0 +1,4 @@
+/datum/log_category/game_looc
+ category = LOG_CATEGORY_GAME_LOOC
+ config_flag = /datum/config_entry/flag/log_ooc
+ master_category = /datum/log_category/game
diff --git a/tgstation.dme b/tgstation.dme
index c0a7690447a5..2c9087587f41 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -385,6 +385,7 @@
#include "code\__DEFINES\~monkestation\antagonists.dm"
#include "code\__DEFINES\~monkestation\artifact.dm"
#include "code\__DEFINES\~monkestation\asteroids.dm"
+#include "code\__DEFINES\~monkestation\chat.dm"
#include "code\__DEFINES\~monkestation\clock_cult.dm"
#include "code\__DEFINES\~monkestation\colors.dm"
#include "code\__DEFINES\~monkestation\combat.dm"
@@ -393,7 +394,9 @@
#include "code\__DEFINES\~monkestation\DNA.dm"
#include "code\__DEFINES\~monkestation\factions.dm"
#include "code\__DEFINES\~monkestation\interaction_particles.dm"
+#include "code\__DEFINES\~monkestation\keybinding.dm"
#include "code\__DEFINES\~monkestation\level_traits.dm"
+#include "code\__DEFINES\~monkestation\logging.dm"
#include "code\__DEFINES\~monkestation\maps.dm"
#include "code\__DEFINES\~monkestation\mecha.dm"
#include "code\__DEFINES\~monkestation\misc.dm"
@@ -5643,6 +5646,7 @@
#include "monkestation\code\datums\diseases\advance\symptoms\clockwork.dm"
#include "monkestation\code\datums\elements\area_locked.dm"
#include "monkestation\code\datums\keybinding\carbon.dm"
+#include "monkestation\code\datums\keybinding\communication.dm"
#include "monkestation\code\datums\keybinding\living.dm"
#include "monkestation\code\datums\quirks\negative_quirks.dm"
#include "monkestation\code\datums\quirks\neutral_quirks.dm"
@@ -6040,6 +6044,7 @@
#include "monkestation\code\modules\client\preference_savefile.dm"
#include "monkestation\code\modules\client\preferences.dm"
#include "monkestation\code\modules\client\verbs.dm"
+#include "monkestation\code\modules\client\preferences\admin.dm"
#include "monkestation\code\modules\client\preferences\anime_implant.dm"
#include "monkestation\code\modules\client\preferences\bloom.dm"
#include "monkestation\code\modules\client\preferences\context_menu_requires_shift.dm"
@@ -6047,6 +6052,7 @@
#include "monkestation\code\modules\client\preferences\interaction_mode.dm"
#include "monkestation\code\modules\client\preferences\inventory.dm"
#include "monkestation\code\modules\client\preferences\loadout_override_preference.dm"
+#include "monkestation\code\modules\client\preferences\runechat.dm"
#include "monkestation\code\modules\client\preferences\sounds.dm"
#include "monkestation\code\modules\client\preferences\alt_jobs\_job.dm"
#include "monkestation\code\modules\client\preferences\alt_jobs\titles.dm"
@@ -6056,6 +6062,7 @@
#include "monkestation\code\modules\client\preferences\species_features\ipc.dm"
#include "monkestation\code\modules\client\preferences\species_features\secondary_mut_color.dm"
#include "monkestation\code\modules\client\preferences\species_features\simians.dm"
+#include "monkestation\code\modules\client\verbs\looc.dm"
#include "monkestation\code\modules\clothing\accessories\accessories.dm"
#include "monkestation\code\modules\clothing\costumes\gnome.dm"
#include "monkestation\code\modules\clothing\gloves\gloves.dm"
@@ -6211,6 +6218,7 @@
#include "monkestation\code\modules\loadouts\items\under\under.dm"
#include "monkestation\code\modules\loafing\code\loaf.dm"
#include "monkestation\code\modules\loafing\code\loafer.dm"
+#include "monkestation\code\modules\logging\categories\log_category_game.dm"
#include "monkestation\code\modules\mapping\access_helpers.dm"
#include "monkestation\code\modules\mapping\mapping_helpers.dm"
#include "monkestation\code\modules\maptext\maptext_image_helper.dm"
diff --git a/tgui/packages/tgui-panel/chat/constants.js b/tgui/packages/tgui-panel/chat/constants.js
index fa9bb50b41b3..353e81702e80 100644
--- a/tgui/packages/tgui-panel/chat/constants.js
+++ b/tgui/packages/tgui-panel/chat/constants.js
@@ -28,6 +28,7 @@ export const MESSAGE_TYPE_INFO = 'info';
export const MESSAGE_TYPE_WARNING = 'warning';
export const MESSAGE_TYPE_DEADCHAT = 'deadchat';
export const MESSAGE_TYPE_OOC = 'ooc';
+export const MESSAGE_TYPE_LOOC = 'looc'; // monkestation edit: looc
export const MESSAGE_TYPE_ADMINPM = 'adminpm';
export const MESSAGE_TYPE_COMBAT = 'combat';
export const MESSAGE_TYPE_ADMINCHAT = 'adminchat';
@@ -89,6 +90,12 @@ export const MESSAGE_TYPES = [
description: 'The bluewall of global OOC messages',
selector: '.ooc, .adminooc, .adminobserverooc, .oocplain',
},
+ {
+ type: MESSAGE_TYPE_LOOC,
+ name: 'LOOC',
+ description: 'Local Out Of Character',
+ selector: '.looc',
+ },
{
type: MESSAGE_TYPE_ADMINPM,
name: 'Admin PMs',
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
index c9ef4e4c0932..8018e2e1374e 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -1184,3 +1184,8 @@ $border-width-px: $border-width * 1px;
color: #ff70c1;
font-family: 'Comic Sans MS', cursive, sans-serif;
}
+
+.looc {
+ color: #ffde5c;
+ font-weight: bold;
+}
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
index a48a709ca3e0..63d2cc270b65 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
@@ -1221,3 +1221,8 @@ $border-width-px: $border-width * 1px;
color: #ff69bf;
font-family: 'Comic Sans MS', cursive, sans-serif;
}
+
+.looc {
+ color: #2e57d1;
+ font-weight: bold;
+}
diff --git a/tgui/packages/tgui-say/constants/index.tsx b/tgui/packages/tgui-say/constants/index.tsx
index e987408421d8..778aa2f11692 100644
--- a/tgui/packages/tgui-say/constants/index.tsx
+++ b/tgui/packages/tgui-say/constants/index.tsx
@@ -4,6 +4,7 @@ export const CHANNELS = [
'Radio',
'Me',
'OOC',
+ 'LOOC', // monke: looc
'Admin',
'Mentor',
] as const;
diff --git a/tgui/packages/tgui-say/styles/colors.scss b/tgui/packages/tgui-say/styles/colors.scss
index b25da4dfa830..b1c733d9c9ae 100644
--- a/tgui/packages/tgui-say/styles/colors.scss
+++ b/tgui/packages/tgui-say/styles/colors.scss
@@ -13,6 +13,7 @@ $say: #a4bad6;
$radio: #1ecc43;
$me: #5975da;
$ooc: #cca300;
+$looc: #fafa3b; // monke: looc
////////////////////////////////////////////////
// Subchannel chat colors
@@ -36,6 +37,7 @@ $_channel_map: (
'radio': $radio,
'me': $me,
'ooc': $ooc,
+ 'looc': $looc,
'ai': $ai,
'admin': $admin,
'mentor': $mentor,
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/monkestation/looc.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/monkestation/looc.tsx
new file mode 100644
index 000000000000..bc6e5a8607a0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/monkestation/looc.tsx
@@ -0,0 +1,15 @@
+import { CheckboxInput, FeatureToggle, FeatureChoiced, FeatureDropdownInput } from '../../base';
+
+export const see_looc_on_map: FeatureToggle = {
+ name: 'Enable LOOC Runechat',
+ category: 'RUNECHAT',
+ description: 'LOOC messages will show above heads.',
+ component: CheckboxInput,
+};
+
+export const admin_hear_looc: FeatureChoiced = {
+ name: 'LOOC Omnipotence',
+ category: 'ADMIN',
+ description: 'When to show non-local LOOC messages.',
+ component: FeatureDropdownInput,
+};