diff --git a/code/modules/admin/verbs/entity_narrate.dm b/code/modules/admin/verbs/entity_narrate.dm
index 20926803f5..a33d14fbf7 100644
--- a/code/modules/admin/verbs/entity_narrate.dm
+++ b/code/modules/admin/verbs/entity_narrate.dm
@@ -7,6 +7,20 @@
var/list/entity_refs = list()
+ //TGUI Helper Vars
+ var/tgui_id = "EntityNarrate"
+ var/tgui_selection_mode = 0 //0 for single entity, 1 for multi entity
+ var/tgui_selected_name = "" //String for single selection in-game name
+ var/tgui_selected_type = "" //String for single selection type
+ var/tgui_selected_id = "" //String to retrieve ref from entity_refs
+ var/tgui_selected_refs //object references
+ var/list/tgui_selected_id_multi = list() //List of strings containing mob ids for multi selection
+ var/tgui_narrate_mode = 0 //0 for speak, 1 for emote
+ var/tgui_narrate_privacy = 0 //0 for loud, 1 for subtle
+ var/tgui_last_message = 0 // int to avoid spam
+
+
+
//Appears as a right click verb on any obj and mob within view range.
//when not right clicking we get a list to pick from in aforementioned view range.
@@ -32,11 +46,16 @@
if(istype(E, /mob/living))
var/mob/living/L = E
if(L.client)
- to_chat(usr, "You may not speak for players!")
- log_and_message_admins("attempted to speak for [L.ckey]'s mob", usr)
+ to_chat(usr, SPAN_NOTICE("[L.name] is a player. All attempts to speak through them \
+ gets logged in case of abuse."))
+ log_and_message_admins("has added [L.ckey]'s mob to their entity narrate list", usr)
return
var/unique_name = sanitize(tgui_input_text(usr, "Please give the entity a unique name to track internally. \
This doesn't override how it appears in game", "tracker", L.name))
+ if(unique_name in holder.entity_names)
+ to_chat(usr, SPAN_NOTICE("[unique_name] is not unique! Pick another!"))
+ add_mob_for_narration(L) //Recursively calling ourselves until cancelled or a unique name is given.
+ return
holder.entity_names += unique_name
holder.entity_refs[unique_name] = L
log_and_message_admins("added [L.name] for their personal list to narrate", usr) //Logging here to avoid spam, while still safeguarding abuse
@@ -46,6 +65,10 @@
var/atom/A = E
var/unique_name = sanitize(tgui_input_text(usr, "Please give the entity a unique name to track internally. \
This doesn't override how it appears in game", "tracker", A.name))
+ if(unique_name in holder.entity_names)
+ to_chat(usr, SPAN_NOTICE("[unique_name] is not unique! Pick another!"))
+ add_mob_for_narration(A)
+ return
holder.entity_names += unique_name
holder.entity_refs[unique_name] = A
log_and_message_admins("added [A.name] for their personal list to narrate", usr) //Logging here to avoid spam, while still safeguarding abuse
@@ -98,13 +121,18 @@
//Obtaining and sanitizing arguments for the actual proc
- var/which_entity = tgui_input_list(usr, "Choose which mob to narrate", "Narrate mob", holder.entity_names, null)
+ var/choices = holder.entity_names + "Open TGUI"
+ var/which_entity = tgui_input_list(usr, "Choose which mob to narrate", "Narrate mob", choices, null)
if(!which_entity) return
- var/mode = tgui_alert(usr, "Speak or emote?", "mode", list("Speak", "Emote", "Cancel"))
- if(mode == "Cancel") return
- var/message = sanitize(tgui_input_text(usr, "Input what you want [which_entity] to say or do", "narrate", null, multiline = TRUE, prevent_enter = TRUE))
- if(message)
- narrate_mob_args(which_entity, mode, message)
+ if(which_entity == "Open TGUI")
+ holder.tgui_interact(usr)
+ else
+ var/mode = tgui_alert(usr, "Speak or emote?", "mode", list("Speak", "Emote", "Cancel"))
+ if(mode == "Cancel") return
+ var/message = tgui_input_text(usr, "Input what you want [which_entity] to [mode]", "narrate",
+ null, multiline = TRUE, prevent_enter = TRUE)
+ if(message)
+ narrate_mob_args(which_entity, mode, message)
//The actual logic of the verb. Called by narrate_mob() when used.
/client/proc/narrate_mob_args(name as text, mode as text, message as text)
@@ -127,8 +155,6 @@
//Sanitizing args
name = sanitize(name)
mode = sanitize(mode)
- if(message)
- message = sanitize(message)
if(!(mode in list("Speak", "Emote")))
to_chat(usr, SPAN_NOTICE("Valid modes are 'Speak' and 'Emote'."))
@@ -141,11 +167,9 @@
if(istype(holder.entity_refs[name], /mob/living))
var/mob/living/our_entity = holder.entity_refs[name]
if(our_entity.client) //Making sure we can't speak for players
- to_chat(usr, SPAN_NOTICE("Cannot narrate mobs with active clients!"))
- log_and_message_admins("attempted to speak for [our_entity.ckey]'s mob", usr)
- return
+ log_and_message_admins("used entity-narrate to speak through [our_entity.ckey]'s mob", usr)
if(!message)
- message = sanitize(tgui_input_text(usr, "Input what you want [our_entity] to [mode]", "narrate", null))
+ message = tgui_input_text(usr, "Input what you want [our_entity] to [mode]", "narrate", null) //say/emote sanitize already
if(message && mode == "Speak")
our_entity.say(message)
else if(message && mode == "Emote")
@@ -158,10 +182,135 @@
else if(istype(holder.entity_refs[name], /atom))
var/atom/our_entity = holder.entity_refs[name]
if(!message)
- message = sanitize(tgui_input_text(usr, "Input what you want [our_entity] to [mode]", "narrate", null))
+ message = tgui_input_text(usr, "Input what you want [our_entity] to [mode]", "narrate", null)
+ message = sanitize(message)
if(message && mode == "Speak")
our_entity.audible_message("[our_entity.name] [message]")
else if(message && mode == "Emote")
our_entity.visible_message("[our_entity.name] [message]")
else
return
+
+
+/datum/entity_narrate/tgui_state(mob/user)
+ return GLOB.tgui_admin_state
+
+/datum/entity_narrate/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, tgui_id, "Entity Narration")
+ ui.open()
+
+/datum/entity_narrate/tgui_data(mob/user)
+ var/list/data = list()
+ data["mode_select"] = tgui_narrate_mode
+ data["privacy_select"] = tgui_narrate_privacy
+ data["selected_id"] = tgui_selected_id
+ data["selected_name"] = tgui_selected_name
+ data["selected_type"] = tgui_selected_type
+ data["selection_mode"] = tgui_selection_mode
+ data["multi_id_selection"] = tgui_selected_id_multi
+ data["number_mob_selected"] = LAZYLEN(tgui_selected_id_multi)
+ data["entity_names"] = entity_names
+
+ return data
+
+/datum/entity_narrate/tgui_act(action, list/params)
+ . = ..()
+
+ if(.) return
+ if(!check_rights_for(usr.client, R_FUN)) return
+
+ switch(action)
+ if("change_mode_multi")
+ tgui_selection_mode = !tgui_selection_mode
+ //Clearing selections after switching mode
+ tgui_selected_id_multi = list()
+ tgui_selected_id = ""
+ tgui_selected_type = ""
+ tgui_selected_name = ""
+ tgui_selected_refs = null
+ if("change_mode_privacy")
+ tgui_narrate_privacy = !tgui_narrate_privacy
+ if("change_mode_narration")
+ tgui_narrate_mode = !tgui_narrate_mode
+ if("select_entity")
+ if(tgui_selection_mode)
+ if(params["id_selected"] in tgui_selected_id_multi)
+ tgui_selected_id_multi -= params["id_selected"]
+ else
+ tgui_selected_id_multi += params["id_selected"]
+ else
+ if(params["id_selected"] in tgui_selected_id_multi)
+ tgui_selected_id_multi -= params["id_selected"]
+ tgui_selected_id = ""
+ tgui_selected_type = ""
+ tgui_selected_name = ""
+ tgui_selected_refs = null
+ else
+ tgui_selected_id_multi = list() //Using the same var for ease of implementation. Thus, we must reset to empty each time.
+ tgui_selected_id_multi += params["id_selected"]
+ tgui_selected_id = params["id_selected"]
+ tgui_selected_refs = entity_refs[tgui_selected_id]
+ if(istype(tgui_selected_refs, /mob/living))
+ var/mob/living/L = tgui_selected_refs
+ if(L.client)
+ tgui_selected_type = "!!!!PLAYER!!!!"
+ tgui_selected_name = L.name
+ else
+ tgui_selected_type = L.type
+ tgui_selected_name = L.name
+ else if(istype(tgui_selected_refs, /atom))
+ var/atom/A = tgui_selected_refs
+ tgui_selected_type = A.type
+ tgui_selected_name = A.name
+ if("narrate")
+ if(world.time < (tgui_last_message + 0.5 SECONDS))
+ to_chat(usr, SPAN_NOTICE("You can't messages that quickly! Wait at least half a second"))
+ else
+ to_chat(usr, SPAN_NOTICE("Message successfully sent!"))
+ tgui_last_message = world.time
+ var/message = params["message"] //Sanitizing before speaking it
+ if(tgui_selection_mode)
+ for(var/entity in tgui_selected_id_multi)
+ var/ref = entity_refs[entity]
+ if(istype(ref, /mob/living))
+ var/mob/living/L = ref
+ if(L.client)
+ log_and_message_admins("used entity-narrate to speak through [L.ckey]'s mob", usr)
+ narrate_tgui_mob(L, message)
+ else if(istype(ref, /atom))
+ var/atom/A = ref
+ narrate_tgui_atom(A, message)
+ else
+ var/ref = entity_refs[tgui_selected_id]
+ if(istype(ref, /mob/living))
+ var/mob/living/L = ref
+ if(L.client)
+ log_and_message_admins("used entity-narrate to speak through [L.ckey]'s mob", usr)
+ narrate_tgui_mob(L, message)
+ else if(istype(ref, /atom))
+ var/atom/A = ref
+ narrate_tgui_atom(A, message)
+
+/datum/entity_narrate/proc/narrate_tgui_mob(mob/living/L, message as text)
+ //say and custom_emote sanitize it themselves, not sanitizing here to avoid double encoding.
+ if(tgui_narrate_mode && tgui_narrate_privacy)
+ L.custom_emote_vr(m_type = VISIBLE_MESSAGE, message = message)
+ else if(tgui_narrate_mode && !tgui_narrate_privacy)
+ L.custom_emote(VISIBLE_MESSAGE, message)
+ else if(!tgui_narrate_mode && tgui_narrate_privacy)
+ L.say(message, whispering = 1)
+ else if(!tgui_narrate_mode && !tgui_narrate_privacy)
+ L.say(message)
+
+/datum/entity_narrate/proc/narrate_tgui_atom(atom/A, message as text)
+ message = sanitize(message)
+ if(tgui_narrate_mode && tgui_narrate_privacy)
+ A.visible_message("[A.name] [message]", range = 1)
+ else if(tgui_narrate_mode && !tgui_narrate_privacy)
+ A.visible_message("[A.name] [message]",)
+ else if(!tgui_narrate_mode && tgui_narrate_privacy)
+ A.audible_message("[A.name] [message]", hearing_distance = 1)
+ else if(!tgui_narrate_mode && !tgui_narrate_privacy)
+ A.audible_message("[A.name] [message]")
diff --git a/tgui/packages/tgui/interfaces/EntityNarrate.tsx b/tgui/packages/tgui/interfaces/EntityNarrate.tsx
new file mode 100644
index 0000000000..d4e73709da
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/EntityNarrate.tsx
@@ -0,0 +1,182 @@
+import { BooleanLike } from 'common/react';
+import { useBackend, useLocalState } from '../backend';
+import { Flex, Tabs, Section, Button, Box, TextArea, Divider } from '../components';
+import { Window } from '../layouts';
+
+type data = {
+ mode_select: BooleanLike; // Data for emote/talk. 0 for talk, 1 for emote
+ privacy_select: BooleanLike; // Data for subtle/loud, 0 for loud, 1 for subtle
+ selection_mode: BooleanLike; // Data for whether we multiselect entities, 0 for single, 1 for multi
+ number_mob_selected: number; // Data to display on how many we selected
+ entity_names: string[]; // List of IDs to populate selection
+ selected_id: string; // To be used for single-mode details view
+ selected_name: string; // To be used for single-mode details view
+ selected_type: string; // To be used for single-mode details view
+ multi_id_selection: string[]; // To be used to highlight selection, and multi-use narrate
+};
+
+export const EntityNarrate = (props, context) => {
+ const { act, data } = useBackend(context);
+ return (
+
+
+
+
+
+ );
+};
+
+// Selects entity from a vertical list, with mode to allow multiple selections.
+// Clicking the tab again removes it
+export const EntitySelection = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { selection_mode, multi_id_selection, entity_names } = data;
+ return (
+
+
+ act('change_mode_multi')}
+ />
+ }>
+
+ {entity_names.map((name) => (
+ act('select_entity', { id_selected: name })}>
+ {name}
+
+ ))}
+
+
+
+
+ );
+};
+
+export const DisplayDetails = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ selection_mode,
+ number_mob_selected,
+ selected_id,
+ selected_name,
+ selected_type,
+ } = data;
+ if (selection_mode) {
+ return (
+
+ Number of entities selected: {number_mob_selected}
+
+ );
+ } else {
+ return (
+
+ Selected ID: {selected_id}
+ Selected Name: {selected_name}
+ Selected Type: {selected_type}
+
+ );
+ }
+};
+
+export const ModeSelector = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { privacy_select, mode_select } = data;
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const NarrationInput = (props, context) => {
+ const { act, data } = useBackend(context);
+ const [narration, setNarration] = useLocalState(context, 'narration', '');
+ return (
+ act('narrate', { message: narration })}
+ content="Send Narration"
+ />
+ }>
+
+
+
+
+
+ );
+};