/// 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 /// 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 width of chat message in pixels #define CHAT_MESSAGE_WIDTH 96 /// Max length of chat message in characters #define CHAT_MESSAGE_MAX_LENGTH 110 /// The dimensions of the chat message icons #define CHAT_MESSAGE_ICON_SIZE 9 /// Maximum precision of float before rounding errors occur (in this context) #define CHAT_LAYER_Z_STEP 0.0001 /// The number of z-layer 'slices' usable by the chat message layering #define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP /** * # Chat Message Overlay * * Datum for generating a message overlay on the map */ /datum/chatmessage /// The visual element of the chat messsage 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 /** * 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 * * language - The language this message was spoken in * * 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, datum/language/language, 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("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner") qdel(src) return INVOKE_ASYNC(src, .proc/generate_image, text, target, owner, language, extra_classes, lifespan) /datum/chatmessage/Destroy() if (owned_by) 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 leave_subsystem() return ..() /** * Calls qdel on the chatmessage when its parent is deleted, used to register qdel signal */ /datum/chatmessage/proc/on_parent_qdel() qdel(src) /** * 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 * * language - The language this message was spoken in * * 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, datum/language/language, list/extra_classes, lifespan) /// Cached icons to show what language the user is speaking var/static/list/language_icons // Register client who owns this message owned_by = owner.client if(!owned_by) return RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, .proc/on_parent_qdel) // Clip message var/maxlen = owned_by.prefs.max_chat_length if (length_char(text) > maxlen) text = copytext_char(text, 1, maxlen + 1) + "..." // BYOND index moment //SKYRAT CHANGES BEGIND // Calculate target color if not already present if (!target.chat_color || target.chat_color_name != target.name) var/mob/M = target if(GLOB.runechat_color_names[target.name]) target.chat_color = GLOB.runechat_color_names[target.name] else if (ismob(target) && M.client?.prefs?.enable_personal_chat_color && M.name == M.real_name && M.name == M.client.prefs.real_name) var/per_color = M.client.prefs.personal_chat_color GLOB.runechat_color_names[target.name] = per_color target.chat_color = per_color else target.chat_color = colorize_string(target.name) target.chat_color_darkened = color_shift(target.chat_color, 0.85, 0.85) target.chat_color_name = target.name //SKYRAT CHANGES END // 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" var/list/prefixes // Append radio icon if from a virtual speaker if (extra_classes.Find("virtual-speaker")) var/image/r_icon = image('icons/ui_icons/chat/chat_icons.dmi', icon_state = "radio") LAZYADD(prefixes, "\icon[r_icon]") else if (extra_classes.Find("emote")) var/image/r_icon = image('icons/ui_icons/chat/chat_icons.dmi', icon_state = "emote") LAZYADD(prefixes, "\icon[r_icon]") // Append language icon if the language uses one var/datum/language/language_instance = GLOB.language_datum_instances[language] if (language_instance?.display_icon(owner)) var/icon/language_icon = LAZYACCESS(language_icons, language) if (isnull(language_icon)) language_icon = icon(language_instance.icon, icon_state = language_instance.icon_state) language_icon.Scale(CHAT_MESSAGE_ICON_SIZE, CHAT_MESSAGE_ICON_SIZE) LAZYSET(language_icons, language, language_icon) LAZYADD(prefixes, "\icon[language_icon]") text = "[prefixes?.Join(" ")][text]" // We dim italicized text to make it more distinguishable from regular text var/tgt_color = extra_classes.Find("italics") ? target.chat_color_darkened : target.chat_color // Approximate text height var/complete_text = "[owner.say_emphasis(text)]" var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH)) 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/msg in owned_by.seen_messages[message_loc]) var/datum/chatmessage/m = msg animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME) combined_height += m.approx_lines // When choosing to update the remaining time we have to be careful not to update the // scheduled time once the EOL completion time has been set. var/sched_remaining = m.scheduled_destruction - world.time if (!m.eol_complete) var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height) m.enter_subsystem(world.time + remaining_time) // push updated time to runechat SS // Reset z index if relevant if (current_z_idx >= CHAT_LAYER_MAX_Z) current_z_idx = 0 // Build message image message = image(loc = message_loc, layer = CHAT_LAYER + CHAT_LAYER_Z_STEP * current_z_idx++) message.plane = CHAT_PLANE message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART message.alpha = 0 message.pixel_y = owner.bound_height * 0.95 message.maptext_width = CHAT_MESSAGE_WIDTH message.maptext_height = mheight message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5 message.maptext = MAPTEXT(complete_text) // View the message LAZYADDASSOC(owned_by.seen_messages, message_loc, src) owned_by.images |= message animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME) // Register with the runechat SS to handle EOL and destruction scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE) enter_subsystem() /** * Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion * Arguments: * * fadetime - The amount of time to animate the message's fadeout for */ /datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE) eol_complete = scheduled_destruction + fadetime animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL) enter_subsystem(eol_complete) // re-enter the runechat SS with the EOL completion time to QDEL self /** * Creates a message overlay at a defined location for a given speaker * * Arguments: * * speaker - The atom who is saying this message * * message_language - The language that the message is said in * * raw_message - The text content of the message * * spans - Additional classes to be added to the message * * message_mode - Bitflags relating to the mode of the message */ /mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, message_mode) // Ensure the list we are using, if present, is a copy so we don't modify the list provided to us spans = spans ? spans.Copy() : list() // Check for virtual speakers (aka hearing a message through a radio) var/atom/movable/originalSpeaker = speaker if (istype(speaker, /atom/movable/virtualspeaker)) var/atom/movable/virtualspeaker/v = speaker speaker = v.source spans |= "virtual-speaker" // Ignore virtual speaker (most often radio messages) from ourself if (originalSpeaker != src && speaker == src) return //Skyrat changes if(!message_language && (lang_treat(speaker, message_language, raw_message, spans, null, TRUE) == "makes a strange sound.") && !("emote" in spans)) var/nospeak = "makes a strange sound." new /datum/chatmessage(nospeak, speaker, src, message_language, list("emote", "italics")) else if(message_language) new /datum/chatmessage(lang_treat(speaker, message_language, raw_message, spans, null, TRUE), speaker, src, message_language, spans) else new /datum/chatmessage(raw_message, speaker, src, message_language, spans) //End of skyrat changes // 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.75 /** * 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 + GLOB.round_id), rseed, rseed + 6) var/h = hex2num(copytext(hash, 1, 3)) * (360 / 255) var/s = (hex2num(copytext(hash, 3, 5)) >> 2) * ((CM_COLOR_SAT_MAX - CM_COLOR_SAT_MIN) / 63) + CM_COLOR_SAT_MIN var/l = (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 rgb 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 //Skyrat changes begin var/final_val switch(h_int) if(0) final_val = "#[num2hex(c, 2)][num2hex(x, 2)][num2hex(m, 2)]" if(1) final_val = "#[num2hex(x, 2)][num2hex(c, 2)][num2hex(m, 2)]" if(2) final_val = "#[num2hex(m, 2)][num2hex(c, 2)][num2hex(x, 2)]" if(3) final_val = "#[num2hex(m, 2)][num2hex(x, 2)][num2hex(c, 2)]" if(4) final_val = "#[num2hex(x, 2)][num2hex(m, 2)][num2hex(c, 2)]" if(5) final_val = "#[num2hex(c, 2)][num2hex(m, 2)][num2hex(x, 2)]" GLOB.runechat_color_names[name] = final_val return final_val //End of skyrat changes //Skyrat changes begin /datum/chatmessage/proc/color_shift(color, sat_shift = 1, lum_shift = 1) var/list/HSL = rgb2hsl(hex2num(copytext(color, 2, 4)), hex2num(copytext(color, 4, 6)), hex2num(copytext(color, 6, 8))) HSL[2] = HSL[2] * sat_shift HSL[3] = HSL[3] * lum_shift var/list/RGB = hsl2rgb(arglist(HSL)) return "#[num2hex(RGB[1],2)][num2hex(RGB[2],2)][num2hex(RGB[3],2)]" //End of skyrat changes