From 24c1fb35f17ff928ce9906e575e3ab2a502e3439 Mon Sep 17 00:00:00 2001 From: SuhEugene <32931701+SuhEugene@users.noreply.github.com> Date: Mon, 17 Jul 2023 01:36:29 +0300 Subject: [PATCH] Add Runechat --- baystation12.dme | 1 + code/__defines/__renderer.dm | 2 + code/__defines/_renderer.dm | 5 + code/__defines/lists.dm | 7 + code/datums/chat_message.dm | 382 ++++++++++++++++++ code/game/atoms.dm | 12 +- code/modules/client/client_defines.dm | 3 + .../preference_setup/global/preferences.dm | 18 + code/modules/emotes/emote_define.dm | 8 +- code/modules/emotes/emote_mob.dm | 4 +- code/modules/mob/hear_say.dm | 7 + code/modules/mob/mob.dm | 9 +- icons/chaticons.dmi | Bin 0 -> 337 bytes interface/skin.dmf | 1 + 14 files changed, 452 insertions(+), 7 deletions(-) create mode 100644 code/datums/chat_message.dm create mode 100644 icons/chaticons.dmi diff --git a/baystation12.dme b/baystation12.dme index 1e39fe61d1f54..ff1ac4da790c9 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -266,6 +266,7 @@ #include "code\datums\browser.dm" #include "code\datums\callbacks.dm" #include "code\datums\category.dm" +#include "code\datums\chat_message.dm" #include "code\datums\cinematic.dm" #include "code\datums\datum.dm" #include "code\datums\footsteps.dm" diff --git a/code/__defines/__renderer.dm b/code/__defines/__renderer.dm index 58f9737e378de..0928096cac28b 100644 --- a/code/__defines/__renderer.dm +++ b/code/__defines/__renderer.dm @@ -159,6 +159,8 @@ #define HUD_ABOVE_ITEM_LAYER 4 #define HUD_ABOVE_HUD_LAYER 5 +#define RUNECHAT_PLANE 7 + /// This plane masks out lighting, to create an "emissive" effect for e.g glowing screens in otherwise dark areas. #define EMISSIVE_PLANE 10 #define EMISSIVE_TARGET "*emissive" diff --git a/code/__defines/_renderer.dm b/code/__defines/_renderer.dm index 1e2f3aec47c51..ff9410d24046c 100644 --- a/code/__defines/_renderer.dm +++ b/code/__defines/_renderer.dm @@ -220,6 +220,11 @@ GLOBAL_LIST_EMPTY(zmimic_renderers) group = RENDER_GROUP_SCREEN plane = HUD_PLANE +/atom/movable/renderer/runechat + name = "Runechat" + group = RENDER_GROUP_SCREEN + plane = RUNECHAT_PLANE + /* * * Group renderers diff --git a/code/__defines/lists.dm b/code/__defines/lists.dm index a32c0b6c74795..9405c6561d57e 100644 --- a/code/__defines/lists.dm +++ b/code/__defines/lists.dm @@ -9,8 +9,15 @@ #define UNSETEMPTY(L) if (L && !length(L)) L = null // Removes I from list L, and sets I to null if it is now empty #define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } } +// Removes the value V from the item K, if the item K is empty will remove it from the list, if the list is empty will set the list to null +#define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; } // Adds I to L, initalizing L if necessary #define LAZYADD(L, I) if(!L) { L = list(); } L += I; +// Adds to the item K the value V, if the list is null it will initialize it +#define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += V; +// This is used to add onto lazy assoc list when the value you're adding is a /list/. +// This one has extra safety over lazyaddassoc because the value could be null (and thus cant be used to += objects) +#define LAZYADDASSOCLIST(L, K, V) if(!L) { L = list(); } L[K] += list(V); // Insert I into L at position X, initalizing L if necessary #define LAZYINSERT(L, I, X) if(!L) { L = list(); } L.Insert(X, I); // Adds I to L, initalizing L if necessary, if I is not already in L diff --git a/code/datums/chat_message.dm b/code/datums/chat_message.dm new file mode 100644 index 0000000000000..b385bcb13b190 --- /dev/null +++ b/code/datums/chat_message.dm @@ -0,0 +1,382 @@ +/// How long the chat message's spawn-in animation will occur for +#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS +/// How long the chat message will exist prior to any exponential decay +#define CHAT_MESSAGE_LIFESPAN 5 SECONDS +/// How long the chat message's end of life fading animation will occur for +#define CHAT_MESSAGE_EOL_FADE 0.7 SECONDS +/// Grace period for fade before we actually delete the chat message +#define CHAT_MESSAGE_GRACE_PERIOD 0.2 SECONDS + +/// Factor of how much the message index (number of messages) will account to exponential decay +#define CHAT_MESSAGE_EXP_DECAY 0.7 +/// Factor of how much height will account to exponential decay +#define CHAT_MESSAGE_HEIGHT_DECAY 0.9 +/// Approximate height in pixels of an 'average' line, used for height decay +#define CHAT_MESSAGE_APPROX_LHEIGHT 11 + +/// Max default runechat message length in characters +#define CHAT_MESSAGE_LENGTH 68 +/// Max extended runechat message length in characters +#define CHAT_MESSAGE_EXT_LENGTH 150 +/// Max default runechat message width in pixels +#define CHAT_MESSAGE_WIDTH 96 +/// Max extended runechat message width in pixels +#define CHAT_MESSAGE_EXT_WIDTH 128 + +// Tweak these defines to change the available color ranges +#define CM_COLOR_SAT_MIN 0.6 +#define CM_COLOR_SAT_MAX 0.7 +#define CM_COLOR_LUM_MIN 0.65 +#define CM_COLOR_LUM_MAX 0.8 + +/// Macro from Lummox used to get height from a MeasureText proc. +/// resolves the MeasureText() return value once, then resolves the height, then sets return_var to that. +#define WXH_TO_HEIGHT(measurement, return_var) \ + do { \ + var/_measurement = measurement; \ + return_var = text2num(copytext(_measurement, findtextEx(_measurement, "x") + 1)); \ + } while(FALSE); + +// Cached runechat icon +GLOBAL_LIST_EMPTY(runechat_image_cache) + +/hook/startup/proc/runechat_images() + var/image/radio_image = image('icons/chaticons.dmi', icon_state = "radio") + GLOB.runechat_image_cache["radio"] = radio_image + + var/image/emote_image = image('icons/chaticons.dmi', icon_state = "emote") + GLOB.runechat_image_cache["emote"] = emote_image + + return TRUE + +/** + * # Chat Message Overlay + * + * Datum for generating a message overlay on the map + * Ported from TGStation; https://github.com/tgstation/tgstation/pull/50608, author: bobbahbrown + */ +/datum/chatmessage + /// The visual element of the chat message + var/image/message + /// The location in which the message is appearing + var/atom/message_loc + /// The client who heard this message + var/client/owned_by + /// Contains the scheduled destruction time, used for scheduling EOL + var/scheduled_destruction + /// Contains the time that the EOL for the message will be complete, used for qdel scheduling + var/eol_complete + /// Contains the approximate amount of lines for height decay + var/approx_lines + /// Contains the reference to the next chatmessage in the bucket, used by runechat subsystem + var/datum/chatmessage/next + /// Contains the reference to the previous chatmessage in the bucket, used by runechat subsystem + var/datum/chatmessage/prev + /// The current index used for adjusting the layer of each sequential chat message such that recent messages will overlay older ones + var/static/current_z_idx = 0 + /// When we started animating the message + var/animate_start = 0 + /// Our animation lifespan, how long this message will last + var/animate_lifespan = 0 + +/** + * Constructs a chat message overlay + * + * Arguments: + * * text - The text content of the overlay + * * target - The target atom to display the overlay at + * * owner - The mob that owns this overlay, only this mob will be able to view it + * * extra_classes - Extra classes to apply to the span that holds the text + * * lifespan - The lifespan of the message in deciseconds + */ +/datum/chatmessage/New(text, atom/target, mob/owner, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN) + . = ..() + if (!istype(target)) + CRASH("Invalid target given for chatmessage") + if(QDELETED(owner) || !istype(owner) || !owner.client) + stack_trace("[src.type] created with [isnull(owner) ? "null" : "invalid"] mob owner") + qdel(src) + return + invoke_async(src, .proc/generate_image, text, target, owner, extra_classes, lifespan) + +/datum/chatmessage/Destroy() + if (!QDELING(owned_by)) + if(world.timeofday < animate_start + animate_lifespan) + stack_trace("Del'd before we finished fading, with [(animate_start + animate_lifespan) - world.timeofday] time left") + + if (owned_by.seen_messages) + LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src) + owned_by.images.Remove(message) + + owned_by = null + message_loc = null + message = null + return ..() + +/** + * Generates a chat message image representation + * + * Arguments: + * * text - The text content of the overlay + * * target - The target atom to display the overlay at + * * owner - The mob that owns this overlay, only this mob will be able to view it + * * extra_classes - Extra classes to apply to the span that holds the text + * * lifespan - The lifespan of the message in deciseconds + */ +/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, list/extra_classes, lifespan) + // Register client who owns this message + owned_by = owner.client + GLOB.destroyed_event.register(owned_by, src, .proc/qdel_self) + + // Remove spans in the message from things like the recorder + var/static/regex/span_check = new(@"<\/?span[^>]*>", "gi") + text = replacetext(text, span_check, "") + + // Clip message + var/extra_length = owned_by.get_preference_value(/datum/client_preference/runechat_messages_length) == GLOB.PREF_LONG + var/maxlen = extra_length ? CHAT_MESSAGE_EXT_LENGTH : CHAT_MESSAGE_LENGTH + var/msgwidth = extra_length ? CHAT_MESSAGE_EXT_WIDTH : CHAT_MESSAGE_WIDTH + if (length_char(text) > maxlen) + text = copytext_char(text, 1, maxlen + 1) + "..." // BYOND index moment + + // Calculate target color if not already present + if (!target.chat_color || target.chat_color_name != target.name) + target.chat_color = colorize_string(target.name) + target.chat_color_darkened = colorize_string(target.name, 0.85, 0.85) + target.chat_color_name = target.name + + // Get rid of any URL schemes that might cause BYOND to automatically wrap something in an anchor tag + var/static/regex/url_scheme = new(@"[A-Za-z][A-Za-z0-9+-\.]*:\/\/", "g") + text = replacetext(text, url_scheme, "") + + // Reject whitespace + var/static/regex/whitespace = new(@"^\s*$") + if (whitespace.Find(text)) + qdel(src) + return + + // Non mobs speakers can be small + if (!ismob(target)) + extra_classes |= "small" + + // Why are you yelling? + if(copytext_char(text, -2) == "!!") + extra_classes |= "yell" + + // Append radio icon if from a virtual speaker + if (extra_classes.Find("virtual-speaker")) + var/image/r_icon = image('icons/chaticons.dmi', icon_state = "radio") + text = "\icon[r_icon] " + text + else if (extra_classes.Find("emote")) + var/image/r_icon = image('icons/chaticons.dmi', icon_state = "emote") + text = "\icon[r_icon] " + text + + // We dim italicized text to make it more distinguishable from regular text + var/tgt_color = target.chat_color + if (extra_classes.Find("italics") || extra_classes.Find("emote")) + tgt_color = target.chat_color_darkened + + // Approximate text height + // Note we have to replace HTML encoded metacharacters otherwise MeasureText will return a zero height + // BYOND Bug #2563917 + // Construct text + var/static/regex/html_metachars = new(@"&[A-Za-z]{1,7};", "g") + var/complete_text = "[text]" + var/mheight + WXH_TO_HEIGHT(owned_by.MeasureText(replacetext(complete_text, html_metachars, "m"), null, msgwidth), mheight) + + invoke_async(src, .proc/finish_image_generation, mheight, target, owner, complete_text, lifespan) + +/// Finishes the image generation after the MeasureText() call in generate_image(). +/// Necessary because after that call the proc can resume at the end of the tick and cause overtime. +/datum/chatmessage/proc/finish_image_generation(mheight, atom/target, mob/owner, complete_text, lifespan) + var/rough_time = world.timeofday + approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT) + + // Translate any existing messages upwards, apply exponential decay factors to timers + message_loc = isturf(target) ? target : get_atom_on_turf(target) + if (owned_by.seen_messages) + var/idx = 1 + var/combined_height = approx_lines + for(var/datum/chatmessage/m as anything in owned_by.seen_messages[message_loc]) + combined_height += m.approx_lines + + var/time_spent = rough_time - m.animate_start + var/time_before_fade = m.animate_lifespan - CHAT_MESSAGE_EOL_FADE + + // When choosing to update the remaining time we have to be careful not to update the + // scheduled time once the EOL has been executed. + if (time_spent >= time_before_fade) + animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) + continue + + var/remaining_time = time_before_fade * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height) + // Ensure we don't accidentially spike alpha up or something silly like that + m.message.alpha = m.get_current_alpha(time_spent) + if (remaining_time > 0) + // Stay faded in for a while, then + animate(m.message, alpha = 255, remaining_time) + // Fade out + animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE) + m.animate_lifespan = remaining_time + CHAT_MESSAGE_EOL_FADE + else + // Your time has come my son + animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE) + // We run this after the alpha animate, because we don't want to interrup it, but also don't want to block it by running first + // Sooo instead we do this. bit messy but it fuckin works + animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) + + // Build message image + message = image(loc = message_loc, layer = ABOVE_HUMAN_LAYER) + message.plane = RUNECHAT_PLANE + message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART + message.alpha = 0 + message.pixel_y = target.maptext_height + message.maptext_width = CHAT_MESSAGE_WIDTH + message.maptext_height = mheight * 1.25 + message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5 + message.maptext = complete_text + + // View the message + LAZYADDASSOCLIST(owned_by.seen_messages, message_loc, src) + owned_by.images |= message + + // Fade in + animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME) + var/time_before_fade = lifespan - CHAT_MESSAGE_SPAWN_TIME - CHAT_MESSAGE_EOL_FADE + + // Stay faded in + animate(alpha = 255, time = time_before_fade) + + // Fade out + animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE) + + // Desctruct yourself + addtimer(new Callback(src, .proc/qdel_self), lifespan + CHAT_MESSAGE_GRACE_PERIOD, TIMER_UNIQUE|TIMER_OVERRIDE) + +/datum/chatmessage/proc/get_current_alpha(time_spent) + if(time_spent < CHAT_MESSAGE_SPAWN_TIME) + return (time_spent / CHAT_MESSAGE_SPAWN_TIME) * 255 + + var/time_before_fade = animate_lifespan - CHAT_MESSAGE_EOL_FADE + if(time_spent <= time_before_fade) + return 255 + + return (1 - ((time_spent - time_before_fade) / CHAT_MESSAGE_EOL_FADE)) * 255 + +/** + * Creates a message overlay at a defined location for a given speaker + * + * Arguments: + * * speaker - The atom who is saying this message + * * message - The text content of the message + * * italics - Decides if this should be small or not, as generally italics text are for whisper/radio overhear + * * existing_extra_classes - Additional classes to add to the message + */ +/mob/proc/create_chat_message(atom/movable/speaker, message, italics, list/existing_extra_classes, audible = TRUE) + if(!client) + return + + // Doesn't want to hear + if(ismob(speaker) && client.get_preference_value(/datum/client_preference/runechat_mob) != GLOB.PREF_YES) + return + if(isobj(speaker) && client.get_preference_value(/datum/client_preference/runechat_obj) != GLOB.PREF_YES) + return + + // Incapable of receiving + if((audible && is_deaf()) || (!audible && is_blind())) + return + + // Check for virtual speakers (aka hearing a message through a radio) + if(existing_extra_classes.Find("radio")) + return + + /* Not currently necessary + message = strip_html_properly(message) + if(!message) + return + */ + + var/list/extra_classes = list() + extra_classes += existing_extra_classes + + if(italics) + extra_classes |= "italics" + + // Display visual above source + new /datum/chatmessage(message, speaker, src, extra_classes) + +/** + * Gets a color for a name, will return the same color for a given string consistently within a round.atom + * + * Note that this proc aims to produce pastel-ish colors using the HSL colorspace. These seem to be favorable for displaying on the map. + * + * Arguments: + * * name - The name to generate a color for + * * sat_shift - A value between 0 and 1 that will be multiplied against the saturation + * * lum_shift - A value between 0 and 1 that will be multiplied against the luminescence + */ +/datum/chatmessage/proc/colorize_string(name, sat_shift = 1, lum_shift = 1) + // seed to help randomness + var/static/rseed = rand(1,26) + + // get hsl using the selected 6 characters of the md5 hash + var/hash = copytext(md5(name), rseed, rseed + 6) + var/h = hex2num(copytext(hash, 1, 3)) * (360 / 255) + var/s = SHIFTR(hex2num(copytext(hash, 3, 5)), 2) * ((CM_COLOR_SAT_MAX - CM_COLOR_SAT_MIN) / 63) + CM_COLOR_SAT_MIN + var/l = SHIFTR(hex2num(copytext(hash, 5, 7)), 2) * ((CM_COLOR_LUM_MAX - CM_COLOR_LUM_MIN) / 63) + CM_COLOR_LUM_MIN + + // adjust for shifts + s *= clamp(sat_shift, 0, 1) + l *= clamp(lum_shift, 0, 1) + + // convert to rgba + var/h_int = round(h/60) // mapping each section of H to 60 degree sections + var/c = (1 - abs(2 * l - 1)) * s + var/x = c * (1 - abs((h / 60) % 2 - 1)) + var/m = l - c * 0.5 + x = (x + m) * 255 + c = (c + m) * 255 + m *= 255 + switch(h_int) + if(0) + return rgb(c,x,m) + if(1) + return rgb(x,c,m) + if(2) + return rgb(m,c,x) + if(3) + return rgb(m,x,c) + if(4) + return rgb(x,m,c) + if(5) + return rgb(c,m,x) + +/atom/proc/runechat_message(message, range = world.view, italics, list/classes = list(), audible = TRUE) + var/list/hearing_mobs = list() + var/list/objs = list() + get_mobs_and_objs_in_view_fast(get_turf(src), range, hearing_mobs, objs, checkghosts = FALSE) + + for(var/mob in hearing_mobs) + var/mob/M = mob + if(!M.client) + continue + M.create_chat_message(src, message, italics, classes, audible) + + +#undef CHAT_MESSAGE_SPAWN_TIME +#undef CHAT_MESSAGE_LIFESPAN +#undef CHAT_MESSAGE_EOL_FADE +#undef CHAT_MESSAGE_GRACE_PERIOD +#undef CHAT_MESSAGE_EXP_DECAY +#undef CHAT_MESSAGE_HEIGHT_DECAY +#undef CHAT_MESSAGE_APPROX_LHEIGHT +#undef CHAT_MESSAGE_LENGTH +#undef CHAT_MESSAGE_EXT_LENGTH +#undef CHAT_MESSAGE_WIDTH +#undef CHAT_MESSAGE_EXT_WIDTH + +#undef CM_COLOR_SAT_MIN +#undef CM_COLOR_SAT_MAX +#undef CM_COLOR_LUM_MIN +#undef CM_COLOR_LUM_MAX diff --git a/code/game/atoms.dm b/code/game/atoms.dm index d01709522c080..5780e774561fc 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -36,6 +36,14 @@ /// This atom's cache of overlays that can only be removed explicitly, like C4. Do not manipulate directly- See SSoverlays. var/list/atom_protected_overlay_cache + /// Last name used to calculate a color for the chatmessage overlays + var/chat_color_name + /// Last color calculated for the the chatmessage overlays + var/chat_color + /// A luminescence-shifted value of the last color calculated for chatmessage overlays + var/chat_color_darkened + /// The chat color var, without alpha. + var/chat_color_hover /atom/New(loc, ...) SHOULD_CALL_PARENT(TRUE) @@ -741,7 +749,7 @@ * - `exclude_objs` - List of objects to not display the message to. * - `exclude_mobs` - List of mobs to not display the message to. */ -/atom/proc/audible_message(message, deaf_message, hearing_distance = world.view, checkghosts = null, list/exclude_objs = null, list/exclude_mobs = null) +/atom/proc/audible_message(message, deaf_message, hearing_distance = world.view, checkghosts = null, list/exclude_objs = null, list/exclude_mobs = null, runemessage = -1) var/turf/T = get_turf(src) var/list/mobs = list() var/list/objs = list() @@ -753,6 +761,8 @@ exclude_mobs -= M continue M.show_message(message,2,deaf_message,1) + if(runemessage != -1) + M.create_chat_message(src, "[runemessage]", FALSE, list("emote")) for(var/o in objs) var/obj/O = o diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index bbea8dae39e03..7afa107358e43 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -15,6 +15,9 @@ var/datum/preferences/prefs = null var/adminobs = null + // Runechat messages + var/list/seen_messages + ///datum that controls the displaying and hiding of tooltips var/datum/tooltip/tooltips diff --git a/code/modules/client/preference_setup/global/preferences.dm b/code/modules/client/preference_setup/global/preferences.dm index 21fea4f93dc16..730b4c3af1aac 100644 --- a/code/modules/client/preference_setup/global/preferences.dm +++ b/code/modules/client/preference_setup/global/preferences.dm @@ -303,6 +303,24 @@ var/global/list/_client_preferences_by_type options = list(GLOB.PREF_YES, GLOB.PREF_NO) default_value = GLOB.PREF_NO +/datum/client_preference/runechat_mob + description = "Enable mob runechat" + key = "RUNECHAT_MOB" + options = list(GLOB.PREF_YES, GLOB.PREF_NO) + default_value = GLOB.PREF_YES + +/datum/client_preference/runechat_obj + description = "Enable obj runechat" + key = "RUNECHAT_OBJ" + options = list(GLOB.PREF_YES, GLOB.PREF_NO) + default_value = GLOB.PREF_YES + +/datum/client_preference/runechat_messages_length + description = "Length of runechat messages" + key = "RUNECHAT_MESSAGES_LENGTH" + options = list(GLOB.PREF_SHORT, GLOB.PREF_LONG) + default_value = GLOB.PREF_SHORT + /******************** * General Staff Preferences * diff --git a/code/modules/emotes/emote_define.dm b/code/modules/emotes/emote_define.dm index 05bcc80be61d6..0d7b4626aba5c 100644 --- a/code/modules/emotes/emote_define.dm +++ b/code/modules/emotes/emote_define.dm @@ -63,6 +63,7 @@ var/use_3p var/use_1p + var/runemessage = -1 if(emote_message_1p) if(target && emote_message_1p_target) use_1p = get_emote_message_1p(user, target, extra_params) @@ -86,6 +87,9 @@ use_3p = replacetext(use_3p, "USER_THEM", user_pronouns.him) use_3p = replacetext(use_3p, "USER_THEIR", user_pronouns.his) use_3p = replacetext(use_3p, "USER_SELF", user_pronouns.self) + + runemessage = replacetext(use_3p, "USER", "") + use_3p = replacetext(use_3p, "USER", "\the [user]") use_3p = capitalize(use_3p) @@ -96,9 +100,9 @@ if(ismob(user)) var/mob/M = user if(message_type == AUDIBLE_MESSAGE) - M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, checkghosts = /datum/client_preference/ghost_sight) + M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, checkghosts = /datum/client_preference/ghost_sight, runemessage = runemessage) else - M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range, checkghosts = /datum/client_preference/ghost_sight) + M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range, checkghosts = /datum/client_preference/ghost_sight, runemessage = runemessage) do_extra(user, target) diff --git a/code/modules/emotes/emote_mob.dm b/code/modules/emotes/emote_mob.dm index 2e45a8a25fd88..11f760a59e9c3 100644 --- a/code/modules/emotes/emote_mob.dm +++ b/code/modules/emotes/emote_mob.dm @@ -137,9 +137,9 @@ //do not show NPC animal emotes to ghosts, it turns into hellscape var/check_ghosts = client ? /datum/client_preference/ghost_sight : null if(m_type == VISIBLE_MESSAGE) - visible_message(message, checkghosts = check_ghosts) + visible_message(message, checkghosts = check_ghosts, runemessage = input) else - audible_message(message, checkghosts = check_ghosts) + audible_message(message, checkghosts = check_ghosts, runemessage = input) // Specific mob type exceptions below. /mob/living/silicon/ai/emote(act, type, message) diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm index b3920f09e2929..3336eabcc68a1 100644 --- a/code/modules/mob/hear_say.dm +++ b/code/modules/mob/hear_say.dm @@ -75,6 +75,8 @@ if (italics) display_message = "[display_message]" + var/runechat_message = display_message + var/display_controls if (is_ghost) if (display_name != speaker.real_name && speaker.real_name) @@ -110,6 +112,11 @@ on_hear_say({"[SPAN_CLASS("game say", "[display_controls][SPAN_CLASS("name", display_name)][alt_name] [display_message]")]"}) + if (istype(language, /datum/language/noise)) + create_chat_message(speaker, runechat_message, italics, list("emote")) + else + create_chat_message(speaker, capitalize(runechat_message), italics, list()) + /mob/proc/on_hear_say(message) to_chat(src, message) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index dfc0f452b63c1..ff74966aef232 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -83,7 +83,7 @@ // message is the message output to anyone who can see e.g. "[src] does something!" // self_message (optional) is what the src mob sees e.g. "You do something!" // blind_message (optional) is what blind people will hear e.g. "You hear something!" -/mob/visible_message(message, self_message, blind_message, range = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null) +/mob/visible_message(message, self_message, blind_message, range = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null, runemessage = -1) set waitfor = FALSE var/turf/T = get_turf(src) var/list/mobs = list() @@ -122,6 +122,8 @@ if((!M.is_blind() && M.see_invisible >= src.invisibility) || narrate) M.show_message(mob_message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE) + if(runemessage != -1) + M.create_chat_message(src, "[runemessage]", FALSE, list("emote"), audible = FALSE) continue if(blind_message) @@ -137,7 +139,7 @@ // self_message (optional) is what the src mob hears. // deaf_message (optional) is what deaf people will see. // hearing_distance (optional) is the range, how many tiles away the message can be heard. -/mob/audible_message(message, self_message, deaf_message, hearing_distance = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null) +/mob/audible_message(message, self_message, deaf_message, hearing_distance = world.view, checkghosts = null, narrate = FALSE, list/exclude_objs = null, list/exclude_mobs = null, runemessage = -1) var/turf/T = get_turf(src) var/list/mobs = list() var/list/objs = list() @@ -168,6 +170,9 @@ else M.show_message(mob_message, AUDIBLE_MESSAGE) + if(runemessage != -1) + M.create_chat_message(src, "[runemessage]", FALSE, list("emote"), audible = TRUE) + for(var/o in objs) var/obj/O = o if (length(exclude_objs) && (O in exclude_objs)) diff --git a/icons/chaticons.dmi b/icons/chaticons.dmi new file mode 100644 index 0000000000000000000000000000000000000000..8cc4b2c5598fa8818a6b80684a33cc19c4475a2f GIT binary patch literal 337 zcmV-X0j~auP)V=-0C=2r%CQQAFc1ddIrkJlx|bGQ9BwI*l0n}gkh8hL29iTx-$Cf+_6=W; z_w)%1{2XYP+O+(@7#>e@hbGHljO4QDHyaouHH#)-bTuX|9l4~nf$<`@*0{;#|B|hk z+^rtd;^F5&C*08_dM&1E;{X5vlSxEDR49?HlCcfLFbo8rf_BLOZqkLl2k`)zBLjF4 z_wO!^2kZs*w~NREdynk}vPz!=MOp)!uLh zRx`*p32LABKi9{583f&E@bh(>?