Mind readers can read what people are typing (#93059)

## About The Pull Request

When a Mind Reader examines someone who is typing, it will show them
what they are actively typing

<img width="518" height="97" alt="image"
src="https://github.com/user-attachments/assets/8d54aa56-85fc-4e03-b0a3-bfb8e475beff"
/>

No, it won't read OOC messages.

## Why It's Good For The Game

Your next line is, "This sounds really funny for gimmicks like security
interrogations or fortune telling, or for getting the jump on someone as
they try to get the jump on you, or just to be a badass by finishing
people sentences"

## Changelog

🆑 Melbert
add: When a mind reader examines a mob, they'll get a glimpse into what
that mob is currently typing, before they even send the message.
qol: Mind Reader now groups up all the information it gives you in a box
admin: Mind Reader now logs everything the reader gleamed from the
read-ee
/🆑
This commit is contained in:
MrMelbert
2025-10-03 18:29:31 -05:00
committed by GitHub
parent ee20fa4a1c
commit ce50179f7c
5 changed files with 107 additions and 9 deletions

View File

@@ -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("<i>You plunge into [cast_on]'s mind and discover...</i>")
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 += "&emsp;\"[spoken_memory]\"..."
log_info += "Recent speech: \"[spoken_memory]\""
discovered_info += jointext(speech_block, "<br>")
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 <b>[carbon_cast_on.combat_mode ? "harm" : "help"]</b>."
discovered_info += "...True identity of <b>[carbon_cast_on.mind.name]</b>."
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, "<br>"))))
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("<i>You analyze [examined]'s current thoughts...</i><br>&emsp;\"[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)
..()

View File

@@ -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

View File

@@ -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

View File

@@ -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 */

View File

@@ -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;