Files
MrMelbertandGitHub 45b23412f8 [MDB Ignore] Medical larp: Adds the SDSM-35 (an in game brain trauma reference) (#94761)
## About The Pull Request

Medbay, the CMO, the Psychologist, (possibly) the Library, and some
maint rooms across all maps now have multiple copies of the SDSM-35

This exists as an in game ability to reference what brain traumas do. No
special bells or whistles, not even a search bar - it's a book after
all.

<img width="819" height="589" alt="image"
src="https://github.com/user-attachments/assets/ace867b9-a0f0-4302-99f9-46ed4d6fe377"
/>

<img width="802" height="546" alt="image"
src="https://github.com/user-attachments/assets/c96941d3-8da8-48f7-859b-93953fe01473"
/>

## Why It's Good For The Game

De-wikification: Rather than needing to pull up the wiki to figure out
what the brain trauma is doing, you can refer to the in game book and
cross reference the patient's behavior.

Medial larp: It's a reference to the DSM-5. If that wasn't obvious. 

## Changelog

🆑 Melbert
add: You can now find the SDMS-35 in Medbay, the CMO's office, the
Psychologist's office, and possibly the Library. Quite simply, it's a
reference book for all the brain trauma's you may experience.
/🆑
2026-01-08 19:47:04 -07:00

549 lines
20 KiB
Plaintext

#define IMAGINARY_FRIEND_RANGE 9
#define IMAGINARY_FRIEND_SPEECH_RANGE IMAGINARY_FRIEND_RANGE
#define IMAGINARY_FRIEND_EXTENDED_SPEECH_RANGE 999
/datum/brain_trauma/special/imaginary_friend
name = "Imaginary Friend"
desc = "Patient can see and hear an imaginary person."
scan_desc = "partial schizophrenia"
symptoms = "Exhibits signs of interacting with an unseen individual, including talking to themselves, \
responding to unheard stimuli, and displaying behaviors that suggest the presence of a companion. \
This \"imaginary friend\" may influence the patient's actions and emotional state, \
leading to social withdrawal, altered perceptions of reality, or atypical activities."
gain_text = span_notice("You feel in good company, for some reason.")
lose_text = span_warning("You feel lonely again.")
var/mob/eye/imaginary_friend/friend
var/friend_initialized = FALSE
/datum/brain_trauma/special/imaginary_friend/on_gain()
var/mob/living/M = owner
if(M.stat == DEAD || !M.client)
return FALSE
. = ..()
make_friend()
get_ghost()
/datum/brain_trauma/special/imaginary_friend/on_life(seconds_per_tick)
if(get_dist(owner, friend) > 9)
friend.recall()
if(!friend)
qdel(src)
return
if(!friend.client && friend_initialized)
addtimer(CALLBACK(src, PROC_REF(reroll_friend)), 1 MINUTES)
/datum/brain_trauma/special/imaginary_friend/on_death()
..()
qdel(src) //friend goes down with the ship
/datum/brain_trauma/special/imaginary_friend/on_lose()
..()
QDEL_NULL(friend)
//If the friend goes afk, make a brand new friend. Plenty of fish in the sea of imagination.
/datum/brain_trauma/special/imaginary_friend/proc/reroll_friend()
if(friend.client) //reconnected
return
friend_initialized = FALSE
QDEL_NULL(friend)
make_friend()
get_ghost()
/datum/brain_trauma/special/imaginary_friend/proc/make_friend()
friend = new(get_turf(owner))
/// Tries a poll for the imaginary friend
/datum/brain_trauma/special/imaginary_friend/proc/get_ghost()
var/mob/chosen_one = SSpolling.poll_ghosts_for_target(
question = "Do you want to play as [span_danger("[owner.real_name]'s")] [span_notice("imaginary friend")]?",
check_jobban = ROLE_PAI,
poll_time = 20 SECONDS,
checked_target = owner,
ignore_category = POLL_IGNORE_IMAGINARYFRIEND,
alert_pic = owner,
role_name_text = "imaginary friend",
)
add_friend(chosen_one)
/// Yay more friends!
/datum/brain_trauma/special/imaginary_friend/proc/add_friend(mob/dead/observer/ghost)
if(isnull(ghost))
qdel(src)
return
friend.PossessByPlayer(ghost.ckey)
friend.attach_to_owner(owner)
friend.setup_appearance()
friend_initialized = TRUE
friend.log_message("became [key_name(owner)]'s split personality.", LOG_GAME)
message_admins("[ADMIN_LOOKUPFLW(friend)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.")
/mob/eye/imaginary_friend
name = "imaginary friend"
real_name = "imaginary friend"
move_on_shuttle = TRUE
desc = "A wonderful yet fake friend."
sight = NONE
mouse_opacity = MOUSE_OPACITY_ICON
see_invisible = SEE_INVISIBLE_LIVING
invisibility = INVISIBILITY_MAXIMUM
has_emotes = TRUE
var/icon/human_image
var/image/current_image
var/hidden = FALSE
var/move_delay = 0
var/mob/living/owner
var/bubble_icon = "default"
/// Whether our host and other imaginary friends can hear us only when nearby or practically anywhere.
var/extended_message_range = TRUE
/mob/eye/imaginary_friend/Login()
. = ..()
if(!. || !client)
return FALSE
if(owner)
greet()
Show()
/mob/eye/imaginary_friend/proc/greet()
to_chat(src, span_notice("<b>You are the imaginary friend of [owner]!</b>"))
to_chat(src, span_notice("You are absolutely loyal to your friend, no matter what."))
to_chat(src, span_notice("You cannot directly influence the world around you, but you can see what [owner] cannot."))
/**
* Arguments:
* * imaginary_friend_owner - The living mob that owns the imaginary friend.
* * appearance_from_prefs - If this is a valid set of prefs, the appearance of the imaginary friend is based on these prefs.
*/
/mob/eye/imaginary_friend/Initialize(mapload)
. = ..()
var/static/list/grantable_actions = list(
/datum/action/innate/imaginary_join,
/datum/action/innate/imaginary_hide,
)
grant_actions_by_list(grantable_actions)
/// Links this imaginary friend to the provided mob
/mob/eye/imaginary_friend/proc/attach_to_owner(mob/living/imaginary_friend_owner)
owner = imaginary_friend_owner
if(!owner.imaginary_group)
owner.imaginary_group = list(owner)
owner.imaginary_group += src
greet()
/// Copies appearance from passed player prefs, or randomises them if none are provided
/mob/eye/imaginary_friend/proc/setup_appearance(datum/preferences/appearance_from_prefs = null)
if(appearance_from_prefs)
INVOKE_ASYNC(src, PROC_REF(setup_friend_from_prefs), appearance_from_prefs)
else
INVOKE_ASYNC(src, PROC_REF(setup_friend))
/// Randomise friend name and appearance
/mob/eye/imaginary_friend/proc/setup_friend()
gender = pick(MALE, FEMALE)
real_name = generate_random_name_species_based(gender, FALSE, /datum/species/human)
name = real_name
human_image = get_flat_human_icon(null, pick(SSjob.joinable_occupations))
Show()
/**
* Sets up the imaginary friend's name and look using a set of datum preferences.
*
* Arguments:
* * appearance_from_prefs - If this is a valid set of prefs, the appearance of the imaginary friend is based on the currently selected character in them. Otherwise, it's random.
*/
/mob/eye/imaginary_friend/proc/setup_friend_from_prefs(datum/preferences/appearance_from_prefs)
if(!istype(appearance_from_prefs))
stack_trace("Attempted to create imaginary friend appearance from null prefs. Using random appearance.")
setup_friend()
return
real_name = appearance_from_prefs.read_preference(/datum/preference/name/real_name)
name = real_name
// Determine what job is marked as 'High' priority.
var/datum/job/appearance_job
var/highest_pref = 0
for(var/job in appearance_from_prefs.job_preferences)
var/this_pref = appearance_from_prefs.job_preferences[job]
if(this_pref > highest_pref)
appearance_job = SSjob.get_job(job)
highest_pref = this_pref
if(!appearance_job)
appearance_job = SSjob.get_job(JOB_ASSISTANT)
if(istype(appearance_job, /datum/job/ai))
human_image = icon('icons/mob/silicon/ai.dmi', icon_state = resolve_ai_icon(appearance_from_prefs.read_preference(/datum/preference/choiced/ai_core_display)), dir = SOUTH)
else if(istype(appearance_job, /datum/job/cyborg))
human_image = icon('icons/mob/silicon/robots.dmi', icon_state = "robot")
else
human_image = get_flat_human_icon(null, appearance_job, appearance_from_prefs)
Show()
/// Returns all member clients of the imaginary_group
/mob/eye/imaginary_friend/proc/group_clients()
var/group_clients = list()
for(var/mob/person as anything in owner.imaginary_group)
if(person.client)
group_clients += person.client
return group_clients
/mob/eye/imaginary_friend/proc/Show()
if(!client || !owner) //nobody home
return
var/list/friend_clients = group_clients() - src.client
//Remove old image from group
remove_image_from_clients(current_image, friend_clients)
//Generate image from the static icon and the current dir
current_image = image(human_image, src, , MOB_LAYER, dir=src.dir)
current_image.override = TRUE
current_image.name = name
if(hidden)
current_image.alpha = 150
//Add new image to owner and friend
if(!hidden)
add_image_to_clients(current_image, friend_clients)
src.client.images |= current_image
/mob/eye/imaginary_friend/Destroy()
if(owner?.client)
owner.client.images.Remove(human_image)
if(client)
client.images.Remove(human_image)
owner.imaginary_group -= src
return ..()
/mob/eye/imaginary_friend/Hear(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, freq_name, freq_color, list/spans, list/message_mods = list(), message_range)
if (safe_read_pref(client, /datum/preference/toggle/enable_runechat) && (safe_read_pref(client, /datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker)))
create_chat_message(speaker, message_language, raw_message, spans)
to_chat(src, compose_message(speaker, message_language, raw_message, radio_freq, freq_name, freq_color, spans, message_mods))
/mob/eye/imaginary_friend/send_speech(message, range = IMAGINARY_FRIEND_SPEECH_RANGE, obj/source = src, bubble_type = bubble_icon, list/spans = list(), datum/language/message_language = null, list/message_mods = list(), forced = null)
message = get_message_mods(message, message_mods)
if(message_mods[RADIO_EXTENSION] == MODE_ADMIN)
SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/cmd_admin_say, message)
return
if(message_mods[RADIO_EXTENSION] == MODE_DEADMIN)
SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/dsay, message)
return
if(check_emote(message, forced))
return
message = check_for_custom_say_emote(message, message_mods)
message = capitalize(message)
if(message_mods[MODE_SING])
var/randomnote = pick("♩", "♪", "♫")
message = "[randomnote] [capitalize(message)] [randomnote]"
spans |= SPAN_SINGING
if(extended_message_range)
range = IMAGINARY_FRIEND_EXTENDED_SPEECH_RANGE
var/eavesdrop_range = 0
if(!(message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]))
if(message_mods[WHISPER_MODE] == MODE_WHISPER)
spans |= SPAN_ITALICS
eavesdrop_range = EAVESDROP_EXTRA_RANGE
range = WHISPER_RANGE
log_sayverb_talk(message, message_mods, tag = "imaginary friend", forced_by = forced)
var/messagepart = generate_messagepart(message, spans, message_mods)
var/dead_rendered = "[span_name("[name] (Imaginary friend of [owner])")] [messagepart]"
var/language = message_language || owner.get_selected_language()
Hear(src, language, message, null, null, null, spans, message_mods) // We always hear what we say
var/group = owner.imaginary_group - src // The people in our group don't, so we have to exclude ourselves not to hear twice
for(var/mob/person in group)
person.Hear(src, language, message, null, null, null, spans, message_mods, range)
// Speech bubble, but only for those who have runechat off
var/list/speech_bubble_recipients = list()
for(var/mob/user as anything in (group + src)) // Add ourselves back in
if((safe_read_pref(user.client, /datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES))))
speech_bubble_recipients.Add(user.client)
var/image/bubble = image('icons/mob/effects/talk.dmi', src, "[bubble_type][say_test(message)]", FLY_LAYER)
SET_PLANE_EXPLICIT(bubble, ABOVE_GAME_PLANE, src)
bubble.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(flick_overlay_global), bubble, speech_bubble_recipients, 3 SECONDS)
LAZYADD(update_on_z, bubble)
addtimer(CALLBACK(src, PROC_REF(clear_saypopup), bubble), 3.5 SECONDS)
var/turf/center_turf = get_turf(src)
if(!center_turf)
return
for(var/mob/dead_player in GLOB.dead_mob_list)
if(dead_player.z != z || get_dist(src, dead_player) > 7)
if(eavesdrop_range)
if(!(get_chat_toggles(dead_player.client) & CHAT_GHOSTWHISPER))
continue
else if(!(get_chat_toggles(dead_player.client) & CHAT_GHOSTEARS))
continue
var/link = FOLLOW_LINK(dead_player, owner)
to_chat(dead_player, "[link] [dead_rendered]")
/mob/eye/imaginary_friend/proc/clear_saypopup(image/say_popup)
LAZYREMOVE(update_on_z, say_popup)
/mob/eye/imaginary_friend/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language, ignore_spam = FALSE, forced, filterproof)
if(!message)
return
say("#[message]", bubble_type, spans, sanitize, language, ignore_spam, forced, filterproof)
/datum/emote/imaginary_friend
mob_type_allowed_typecache = /mob/eye/imaginary_friend
// We have to create our own since we can only show emotes to ourselves and our owner
/datum/emote/imaginary_friend/run_emote(mob/user, params, type_override, intentional = FALSE)
user.log_talk(message, LOG_EMOTE)
if(!can_run_emote(user, FALSE, intentional))
return FALSE
var/msg = select_message_type(user, message, intentional)
if(params && message_param)
msg = select_param(user, params)
msg = replace_pronoun(user, msg)
if(!msg)
return TRUE
var/mob/eye/imaginary_friend/friend = user
var/dchatmsg = "[span_bold("[friend] (Imaginary friend of [friend.owner])")] [msg]"
message = "[span_name("[user]")] [msg]"
var/user_turf = get_turf(user)
if (user.client)
for(var/mob/ghost as anything in GLOB.dead_mob_list)
if(!ghost.client || isnewplayer(ghost))
continue
if(get_chat_toggles(ghost.client) & CHAT_GHOSTSIGHT && !(ghost in viewers(user_turf, null)))
ghost.show_message("[FOLLOW_LINK(ghost, user)] [dchatmsg]")
for(var/mob/person in friend.owner.imaginary_group)
to_chat(person, message)
if(safe_read_pref(person.client, /datum/preference/toggle/enable_runechat))
person.create_chat_message(friend, raw_message = msg, runechat_flags = EMOTE_MESSAGE)
return TRUE
/datum/emote/imaginary_friend/point
key = "point"
key_third_person = "points"
message = "points."
message_param = "points at %t."
/datum/emote/imaginary_friend/point/run_emote(mob/eye/imaginary_friend/friend, params, type_override, intentional)
message_param = initial(message_param) // reset
return ..()
/datum/emote/imaginary_friend/custom
key = "me"
key_third_person = "custom"
message = null
/datum/emote/imaginary_friend/custom/can_run_emote(mob/user, status_check, intentional)
return ..() && intentional
/datum/emote/imaginary_friend/custom/run_emote(mob/user, params, type_override = null, intentional = FALSE)
if(!can_run_emote(user, TRUE, intentional))
return FALSE
if(is_banned_from(user.ckey, "Emote"))
to_chat(user, span_boldwarning("You cannot send custom emotes (banned)."))
return FALSE
else if(QDELETED(user))
return FALSE
else if(user.client && user.client.prefs.muted & MUTE_IC)
to_chat(user, span_boldwarning("You cannot send IC messages (muted)."))
return FALSE
else if(!params)
message = copytext(sanitize(input("Choose an emote to display.") as text|null), 1, MAX_MESSAGE_LEN)
else
message = params
. = ..()
message = null
/datum/emote/imaginary_friend/custom/replace_pronoun(mob/user, message)
return message
// Another snowflake proc, when will they end... should have refactored it differently
/mob/eye/imaginary_friend/point_at(atom/pointed_atom)
if(!isturf(loc))
return
if (pointed_atom in src)
create_point_bubble(pointed_atom)
return
var/turf/tile = get_turf(pointed_atom)
if (!tile)
return
var/turf/our_tile = get_turf(src)
var/obj/visual = image('icons/hud/screen_gen.dmi', our_tile, "arrow", FLY_LAYER)
INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(flick_overlay_global), visual, group_clients(), 2.5 SECONDS)
animate(visual, pixel_x = (tile.x - our_tile.x) * ICON_SIZE_X + pointed_atom.pixel_x, pixel_y = (tile.y - our_tile.y) * ICON_SIZE_Y + pointed_atom.pixel_y, time = 1.7, easing = SINE_EASING|EASE_OUT)
/mob/eye/imaginary_friend/create_thinking_indicator()
if(active_thinking_indicator || active_typing_indicator || !HAS_TRAIT(src, TRAIT_THINKING_IN_CHARACTER))
return FALSE
active_thinking_indicator = image('icons/mob/effects/talk.dmi', src, "[bubble_icon]3", TYPING_LAYER)
add_image_to_clients(active_thinking_indicator, group_clients())
/mob/eye/imaginary_friend/remove_thinking_indicator()
if(!active_thinking_indicator)
return FALSE
remove_image_from_clients(active_thinking_indicator, group_clients())
active_thinking_indicator = null
/mob/eye/imaginary_friend/create_typing_indicator()
if(active_typing_indicator || active_thinking_indicator || !HAS_TRAIT(src, TRAIT_THINKING_IN_CHARACTER))
return FALSE
active_typing_indicator = image('icons/mob/effects/talk.dmi', src, "[bubble_icon]0", TYPING_LAYER)
add_image_to_clients(active_typing_indicator, group_clients())
/mob/eye/imaginary_friend/remove_typing_indicator()
if(!active_typing_indicator)
return FALSE
remove_image_from_clients(active_typing_indicator, group_clients())
active_typing_indicator = null
/mob/eye/imaginary_friend/remove_all_indicators()
REMOVE_TRAIT(src, TRAIT_THINKING_IN_CHARACTER, CURRENTLY_TYPING_TRAIT)
remove_thinking_indicator()
remove_typing_indicator()
/mob/eye/imaginary_friend/Move(NewLoc, Dir = 0)
if(world.time < move_delay)
return FALSE
setDir(Dir)
if(get_dist(src, owner) > 9)
recall()
move_delay = world.time + 10
return FALSE
abstract_move(NewLoc)
move_delay = world.time + 1
/mob/eye/imaginary_friend/setDir(newdir)
. = ..()
Show() // The image does not actually update until Show() gets called
/mob/eye/imaginary_friend/proc/recall()
if(!owner || loc == owner)
return FALSE
abstract_move(owner)
/datum/action/innate/imaginary_join
name = "Join"
desc = "Join your owner, following them from inside their mind."
button_icon = 'icons/mob/actions/actions_minor_antag.dmi'
background_icon_state = "bg_revenant"
overlay_icon_state = "bg_revenant_border"
button_icon_state = "join"
/datum/action/innate/imaginary_join/Activate()
var/mob/eye/imaginary_friend/I = owner
I.recall()
/datum/action/innate/imaginary_hide
name = "Hide"
desc = "Hide yourself from your owner's sight."
button_icon = 'icons/mob/actions/actions_minor_antag.dmi'
background_icon_state = "bg_revenant"
overlay_icon_state = "bg_revenant_border"
button_icon_state = "hide"
/datum/action/innate/imaginary_hide/proc/update_status()
var/mob/eye/imaginary_friend/I = owner
if(I.hidden)
name = "Show"
desc = "Become visible to your owner."
button_icon_state = "unhide"
else
name = "Hide"
desc = "Hide yourself from your owner's sight."
button_icon_state = "hide"
build_all_button_icons()
/datum/action/innate/imaginary_hide/Activate()
var/mob/eye/imaginary_friend/fake_friend = owner
fake_friend.hidden = !fake_friend.hidden
fake_friend.Show()
build_all_button_icons(UPDATE_BUTTON_NAME|UPDATE_BUTTON_ICON)
/datum/action/innate/imaginary_hide/update_button_name(atom/movable/screen/movable/action_button/button, force)
var/mob/eye/imaginary_friend/fake_friend = owner
if(fake_friend.hidden)
name = "Show"
desc = "Become visible to your owner."
else
name = "Hide"
desc = "Hide yourself from your owner's sight."
return ..()
/datum/action/innate/imaginary_hide/apply_button_icon(atom/movable/screen/movable/action_button/current_button, force = FALSE)
var/mob/eye/imaginary_friend/fake_friend = owner
if(fake_friend.hidden)
button_icon_state = "unhide"
else
button_icon_state = "hide"
return ..()
//down here is the trapped mind
//like imaginary friend but a lot less imagination and more like mind prison//
/datum/brain_trauma/special/imaginary_friend/trapped_owner
name = "Trapped Victim"
desc = "Patient appears to be targeted by an invisible entity."
gain_text = ""
lose_text = ""
random_gain = FALSE
known_trauma = FALSE
/datum/brain_trauma/special/imaginary_friend/trapped_owner/make_friend()
friend = new /mob/eye/imaginary_friend/trapped(get_turf(owner), src)
/datum/brain_trauma/special/imaginary_friend/trapped_owner/reroll_friend() //no rerolling- it's just the last owner's hell
if(friend.client) //reconnected
return
friend_initialized = FALSE
QDEL_NULL(friend)
qdel(src)
/datum/brain_trauma/special/imaginary_friend/trapped_owner/get_ghost() //no randoms
return
/mob/eye/imaginary_friend/trapped
name = "figment of imagination?"
real_name = "figment of imagination?"
desc = "The previous host of this body."
/mob/eye/imaginary_friend/trapped/greet()
to_chat(src, span_notice(span_bold("You have managed to hold on as a figment of the new host's imagination!")))
to_chat(src, span_notice("All hope is lost for you, but at least you may interact with your host. You do not have to be loyal to them."))
to_chat(src, span_notice("You cannot directly influence the world around you, but you can see what the host cannot."))
/mob/eye/imaginary_friend/trapped/setup_friend()
real_name = "[owner.real_name]?"
name = real_name
human_image = icon('icons/mob/simple/lavaland/lavaland_monsters.dmi', icon_state = "curseblob")
#undef IMAGINARY_FRIEND_RANGE
#undef IMAGINARY_FRIEND_SPEECH_RANGE
#undef IMAGINARY_FRIEND_EXTENDED_SPEECH_RANGE