diff --git a/code/datums/mutations/antenna.dm b/code/datums/mutations/antenna.dm
index 7f564d6fa04..f5347fab41c 100644
--- a/code/datums/mutations/antenna.dm
+++ b/code/datums/mutations/antenna.dm
@@ -68,10 +68,12 @@
if (!owner)
return
ADD_TRAIT(grant_to, TRAIT_MIND_READER, GENETIC_MUTATION)
+ RegisterSignal(grant_to, COMSIG_MOB_EXAMINATE, PROC_REF(on_examining))
/datum/action/cooldown/spell/pointed/mindread/Remove(mob/remove_from)
. = ..()
REMOVE_TRAIT(remove_from, TRAIT_MIND_READER, GENETIC_MUTATION)
+ UnregisterSignal(remove_from, COMSIG_MOB_EXAMINATE)
/datum/action/cooldown/spell/pointed/mindread/is_valid_target(atom/cast_on)
if(!isliving(cast_on))
@@ -83,12 +85,15 @@
if(living_cast_on.stat == DEAD)
to_chat(owner, span_warning("[cast_on] is dead!"))
return FALSE
+ if(living_cast_on.mob_biotypes & MOB_ROBOTIC)
+ to_chat(owner, span_warning("[cast_on] is robotic, you can't read [cast_on.p_their()] mind!"))
+ return FALSE
return TRUE
/datum/action/cooldown/spell/pointed/mindread/cast(mob/living/cast_on)
. = ..()
- if(cast_on.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0))
+ if(cast_on.can_block_magic(antimagic_flags, charge_cost = 0))
to_chat(owner, span_warning("As you reach into [cast_on]'s mind, \
you are stopped by a mental blockage. It seems you've been foiled."))
return
@@ -102,21 +107,66 @@
you feel the overwhelming emptiness within. A truly evil being. \
[HAS_TRAIT(owner, TRAIT_EVIL) ? "It's nice to find someone who is like-minded." : "What is wrong with this person?"]"))
- to_chat(owner, span_boldnotice("You plunge into [cast_on]'s mind..."))
+ var/list/log_info = list()
+ var/list/discovered_info = list("You plunge into [cast_on]'s mind and discover...")
if(prob(20))
// chance to alert the read-ee
to_chat(cast_on, span_danger("You feel something foreign enter your mind."))
+ log_info += "Target alerted!"
var/list/recent_speech = cast_on.copy_recent_speech(copy_amount = 3, line_chance = 50)
if(length(recent_speech))
- to_chat(owner, span_boldnotice("You catch some drifting memories of their past conversations..."))
+ discovered_info += "...Drifting memories of past conversations:"
+ var/list/speech_block = list()
for(var/spoken_memory in recent_speech)
- to_chat(owner, span_notice("[spoken_memory]"))
+ speech_block += " \"[spoken_memory]\"..."
+ log_info += "Recent speech: \"[spoken_memory]\""
+ discovered_info += jointext(speech_block, "
")
if(iscarbon(cast_on))
var/mob/living/carbon/carbon_cast_on = cast_on
- to_chat(owner, span_boldnotice("You find that their intent is to [carbon_cast_on.combat_mode ? "harm" : "help"]..."))
- to_chat(owner, span_boldnotice("You uncover that [carbon_cast_on.p_their()] true identity is [carbon_cast_on.mind.name]."))
+ discovered_info += "...Intent to [carbon_cast_on.combat_mode ? "harm" : "help"]."
+ discovered_info += "...True identity of [carbon_cast_on.mind.name]."
+ log_info += "Intent: \"[carbon_cast_on.combat_mode ? "harm" : "help"]\""
+ log_info += "Identity: \"[carbon_cast_on.mind.name]\""
+
+ to_chat(owner, boxed_message(span_notice(jointext(discovered_info, "
"))))
+ log_combat(owner, cast_on, "mind read (cast intentionally)", null, "info: [english_list(log_info, and_text = ", ")]")
+
+/datum/action/cooldown/spell/pointed/mindread/proc/on_examining(mob/examiner, atom/examining)
+ SIGNAL_HANDLER
+ if(!isliving(examining) || examiner == examining)
+ return
+
+ INVOKE_ASYNC(src, PROC_REF(read_mind), examiner, examining)
+
+/datum/action/cooldown/spell/pointed/mindread/proc/read_mind(mob/living/examiner, mob/living/examined)
+ if(examined.stat >= UNCONSCIOUS || isnull(examined.mind) || (examined.mob_biotypes & MOB_ROBOTIC))
+ return
+
+ var/antimagic = examined.can_block_magic(antimagic_flags, charge_cost = 0)
+ var/read_text = ""
+ if(!antimagic)
+ read_text = examined.get_typing_text()
+ if(!read_text)
+ return
+
+ sleep(0.5 SECONDS) // small pause so it comes after all examine text and effects
+ if(QDELETED(examiner))
+ return
+ if(antimagic)
+ to_chat(examiner, boxed_message(span_warning("You attempt to analyze [examined]'s current thoughts, but fail to penetrate [examined.p_their()] mind - It seems you've been foiled.")))
+ return
+
+ var/list/log_info = list()
+ if(prob(10))
+ to_chat(examined, span_danger("You feel something foreign enter your mind."))
+ log_info += "Target alerted!"
+
+ to_chat(examiner, boxed_message(span_notice("You analyze [examined]'s current thoughts...
\"[read_text]\"...")))
+ log_info += "Current thought: \"[read_text]\""
+
+ log_combat(examiner, examined, "mind read (triggered on examine)", null, "info: [english_list(log_info, and_text = ", ")]")
/datum/mutation/mindreader/New(datum/mutation/copymut)
..()
diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm
index 876c4476f67..551b8fc12e1 100644
--- a/code/modules/tgui_input/say_modal/modal.dm
+++ b/code/modules/tgui_input/say_modal/modal.dm
@@ -24,13 +24,15 @@
/// The user who opened the window
var/client/client
/// Injury phrases to blurt out
- var/list/hurt_phrases = list("GACK!", "GLORF!", "OOF!", "AUGH!", "OW!", "URGH!", "HRNK!")
+ var/static/list/hurt_phrases = list("GACK!", "GLORF!", "OOF!", "AUGH!", "OW!", "URGH!", "HRNK!")
/// Max message length
var/max_length = MAX_MESSAGE_LEN
/// The modal window
var/datum/tgui_window/window
/// Boolean for whether the tgui_say was opened by the user.
var/window_open
+ /// What text was present in the say box the last time save_text was called
+ var/saved_text = ""
/** Creates the new input window to exist in the background. */
/datum/tgui_say/New(client/client, id)
@@ -128,7 +130,7 @@
if (type == "typing")
start_typing()
return TRUE
- if (type == "entry" || type == "force")
+ if (type == "entry" || type == "force" || type == "save")
handle_entry(type, payload)
return TRUE
return FALSE
diff --git a/code/modules/tgui_input/say_modal/speech.dm b/code/modules/tgui_input/say_modal/speech.dm
index 0d95b855a15..b5b7099868e 100644
--- a/code/modules/tgui_input/say_modal/speech.dm
+++ b/code/modules/tgui_input/say_modal/speech.dm
@@ -57,6 +57,13 @@
window.send_message("force")
stop_typing()
+/**
+ * Exports whatever text is currently in the input box to this datum
+ */
+/datum/tgui_say/proc/save_text()
+ saved_text = null
+ window.send_message("save")
+
/**
* Makes the player force say what's in their current input box.
*/
@@ -70,6 +77,19 @@
log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators DISABLED.")
SEND_SIGNAL(src, COMSIG_HUMAN_FORCESAY)
+/**
+ * Gets whatever text is currently in this mob's say box and returns it.
+ *
+ * Note: Sleeps, due to waiting for say to respond.
+ */
+/mob/proc/get_typing_text()
+ if(!client?.tgui_say?.window_open)
+ return
+ client.tgui_say.save_text()
+ var/safety = world.time
+ UNTIL(istext(client?.tgui_say?.saved_text) || world.time - safety > 2 SECONDS)
+ return client?.tgui_say?.saved_text
+
/**
* Handles text entry and forced speech.
*
@@ -93,4 +113,10 @@
target_channel = SAY_CHANNEL // No ooc leaks
delegate_speech(alter_entry(payload), target_channel)
return TRUE
+ if(type == "save")
+ saved_text = "" // so we can differentiate null (nothing saved) and empty (nothing typed)
+ var/target_channel = payload["channel"]
+ if(target_channel == SAY_CHANNEL || target_channel == RADIO_CHANNEL)
+ saved_text = payload["entry"] // only save IC text
+ return TRUE
return FALSE
diff --git a/tgui/packages/tgui-say/TguiSay.tsx b/tgui/packages/tgui-say/TguiSay.tsx
index fc3cdc2a34a..0fdcab89d7c 100644
--- a/tgui/packages/tgui-say/TguiSay.tsx
+++ b/tgui/packages/tgui-say/TguiSay.tsx
@@ -82,7 +82,11 @@ export function TguiSay() {
setButtonContent(currentPrefix.current ?? iterator.current());
// Empty input, resets the channel
- } else if (currentPrefix.current && iterator.isSay() && value?.length === 0) {
+ } else if (
+ currentPrefix.current &&
+ iterator.isSay() &&
+ value?.length === 0
+ ) {
setCurrentPrefix(null);
setButtonContent(iterator.current());
}
@@ -152,6 +156,15 @@ export function TguiSay() {
handleClose();
}
+ function handleSaveText(): void {
+ const iterator = channelIterator.current;
+ const currentValue = innerRef.current?.value;
+
+ if (!currentValue || !iterator.isVisible()) return;
+
+ messages.current.saveText(currentValue, iterator.current());
+ }
+
function handleIncrementChannel(): void {
const iterator = channelIterator.current;
@@ -248,6 +261,7 @@ export function TguiSay() {
Byond.subscribeTo('props', handleProps);
Byond.subscribeTo('force', handleForceSay);
Byond.subscribeTo('open', handleOpen);
+ Byond.subscribeTo('save', handleSaveText);
}, []);
/** Value has changed, we need to check if the size of the window is ok */
diff --git a/tgui/packages/tgui-say/timers.ts b/tgui/packages/tgui-say/timers.ts
index b13e09ede75..6b4a9f46f08 100644
--- a/tgui/packages/tgui-say/timers.ts
+++ b/tgui/packages/tgui-say/timers.ts
@@ -17,6 +17,12 @@ export const byondMessages = {
1 * SECONDS,
true,
),
+ saveText: debounce(
+ (entry: string, channel: Channel) =>
+ Byond.sendMessage('save', { entry, channel }),
+ 1 * SECONDS,
+ true,
+ ),
// Throttle: Prevents spamming the server
typingMsg: throttle(() => Byond.sendMessage('typing'), 4 * SECONDS),
} as const;