#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" 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, times_fired) 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("You are the imaginary friend of [owner]!")) 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 /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