diff --git a/code/__defines/_planes+layers.dm b/code/__defines/_planes+layers.dm
index 96d65c6f49..111349d887 100644
--- a/code/__defines/_planes+layers.dm
+++ b/code/__defines/_planes+layers.dm
@@ -93,6 +93,8 @@ What is the naming convention for planes or layers?
#define BELOW_MOB_LAYER 3.9 // Should be converted to plane swaps
#define ABOVE_MOB_LAYER 4.1 // Should be converted to plane swaps
+#define ABOVE_MOB_PLANE -24
+
// Invisible things plane
#define CLOAKED_PLANE -15
diff --git a/code/_helpers/time.dm b/code/_helpers/time.dm
index 6e63dabb45..b75aeb77e3 100644
--- a/code/_helpers/time.dm
+++ b/code/_helpers/time.dm
@@ -20,6 +20,8 @@
#define TICKS2DS(T) ((T) TICKS) // Convert ticks to deciseconds
#define DS2NEARESTTICK(DS) TICKS2DS(-round(-(DS2TICKS(DS))))
+var/world_startup_time
+
/proc/get_game_time()
var/global/time_offset = 0
var/global/last_time = 0
diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm
index 5701521ec5..fe2b51195a 100644
--- a/code/_helpers/unsorted.dm
+++ b/code/_helpers/unsorted.dm
@@ -1691,3 +1691,19 @@ GLOBAL_REAL_VAR(list/stack_trace_storage)
return "CLIENT: [D]"
else
return "Unknown data type: [D]"
+
+/*
+ is_holder_of(): Returns 1 if A is a holder of B, meaning, A is B.loc or B.loc.loc or B.loc.loc.loc etc.
+ This is essentially the same as calling (locate(B) in A), but a little clearer as to what you're doing, and locate() has been known to bug out or be extremely slow in the past.
+*/
+/proc/is_holder_of(const/atom/movable/A, const/atom/movable/B)
+ if(istype(A, /turf) || istype(B, /turf)) //Clicking on turfs is a common thing and turfs are also not /atom/movable, so it was causing the assertion to fail.
+ return 0
+ ASSERT(istype(A) && istype(B))
+ var/atom/O = B
+ while(O && !isturf(O))
+ if(O == A)
+ return 1
+ O = O.loc
+ return 0
+
\ No newline at end of file
diff --git a/code/datums/chat_message.dm b/code/datums/chat_message.dm
new file mode 100644
index 0000000000..37fdca02f7
--- /dev/null
+++ b/code/datums/chat_message.dm
@@ -0,0 +1,293 @@
+#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS
+#define CHAT_MESSAGE_LIFESPAN 5 SECONDS
+#define CHAT_MESSAGE_EOL_FADE 0.7 SECONDS
+#define CHAT_MESSAGE_EXP_DECAY 0.8 // Messages decay at pow(factor, idx in stack)
+#define CHAT_MESSAGE_HEIGHT_DECAY 0.7 // Increase message decay based on the height of the message
+#define CHAT_MESSAGE_APPROX_LHEIGHT 11 // Approximate height in pixels of an 'average' line, used for height decay
+#define CHAT_MESSAGE_WIDTH 96 // pixels
+#define CHAT_MESSAGE_NORM_LENGTH 68 // characters
+#define CHAT_MESSAGE_EXT_LENGTH 150 // characters
+#define CHAT_MESSAGE_MOB 1
+#define CHAT_MESSAGE_OBJ 2
+#define WXH_TO_HEIGHT(x) text2num(copytext((x), findtextEx((x), "x") + 1)) // thanks lummox
+
+/**
+ * # Chat Message Overlay
+ *
+ * Datum for generating a message overlay on the map
+ * Ported from TGStation; https://github.com/tgstation/tgstation/pull/50608/, author: bobbahbrown
+ */
+
+// Cached runechat icon
+var/runechat_icon = null
+
+/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
+ var/scheduled_destruction
+ /// Contains the approximate amount of lines for height decay
+ var/approx_lines
+
+/**
+ * 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 = null, lifespan = CHAT_MESSAGE_LIFESPAN)
+ . = ..()
+ if (!istype(target))
+ CRASH("Invalid target given for chatmessage")
+ if(!istype(owner) || QDELETED(owner) || !owner.client)
+ stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner")
+ qdel(src)
+ return
+ generate_image(text, target, owner, extra_classes, lifespan)
+
+/datum/chatmessage/Destroy()
+ if (owned_by)
+ owned_by.seen_messages.Remove(src)
+ owned_by.images.Remove(message)
+ UnregisterSignal(owned_by, COMSIG_PARENT_QDELETING)
+ 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)
+ set waitfor = FALSE
+ // Register client who owns this message
+ owned_by = owner.client
+ RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, .proc/qdel_self)
+
+ // Clip message
+ var/maxlen = owned_by.is_preference_enabled(/datum/client_preference/runechat_long_messages) ? CHAT_MESSAGE_EXT_LENGTH : CHAT_MESSAGE_NORM_LENGTH
+ 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"
+
+ // If we heard our name, it's important
+ var/list/names = splittext(owner.name, " ")
+ for (var/word in names)
+ text = replacetext(text, word, "[word]")
+
+ // Append radio icon if comes from a radio
+ if (extra_classes.Find("spoken_into_radio"))
+ if (!runechat_icon)
+ var/image/r_icon = image('icons/UI_Icons/chat/chat_icons.dmi', icon_state = "radio")
+ runechat_icon = "\icon[r_icon] "
+ text = runechat_icon + 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
+ // 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 = ""
+ var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(replacetext(complete_text, html_metachars, "m"), 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 = target
+ if (owned_by.seen_messages)
+ var/idx = 1
+ var/combined_height = approx_lines
+ for(var/msg in owned_by.seen_messages)
+ 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
+ var/sched_remaining = m.scheduled_destruction - world.time
+ if (sched_remaining > CHAT_MESSAGE_SPAWN_TIME)
+ var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
+ m.scheduled_destruction = world.time + remaining_time
+ spawn(remaining_time)
+ m.end_of_life()
+
+ // Build message image
+ message = image(loc = message_loc, layer = ABOVE_MOB_LAYER)
+ message.plane = PLANE_LIGHTING_ABOVE
+ 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 = complete_text
+
+ if (is_holder_of(owner, target)) // Special case, holding an atom speaking (pAI, recorder...)
+ message.plane = PLANE_PLAYER_HUD_ABOVE
+
+ // View the message
+ owned_by.seen_messages.Add(src)
+ owned_by.images += message
+ animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME)
+
+ // Prepare for destruction
+ scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE)
+ spawn(lifespan - CHAT_MESSAGE_EOL_FADE)
+ end_of_life()
+
+/**
+ * Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion
+ */
+/datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE)
+ if (gc_destroyed)
+ return
+ animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
+ spawn(fadetime)
+ qdel(src)
+
+/**
+ * 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.is_preference_enabled(/datum/client_preference/runechat_mob))
+ return
+ else if(isobj(speaker) && !client.is_preference_enabled(/datum/client_preference/runechat_obj))
+ 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"
+
+ if (client.is_preference_enabled(/datum/client_preference/runechat_border))
+ extra_classes |= "black_outline"
+
+ var/dist = get_dist(src, speaker)
+ switch (dist)
+ if (4 to 5)
+ extra_classes |= "small"
+ if (5 to 16)
+ extra_classes |= "very_small"
+
+ // Display visual above source
+ new /datum/chatmessage(message, speaker, src, extra_classes)
+
+// Tweak these defines to change the available color ranges
+#define CM_COLOR_SAT_MIN 0.6
+#define CM_COLOR_SAT_MAX 0.95
+#define CM_COLOR_LUM_MIN 0.70
+#define CM_COLOR_LUM_MAX 0.90
+
+/**
+ * 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 + "[world_startup_time]"), 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 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/hear = get_mobs_and_objs_in_view_fast(get_turf(src), range, remote_ghosts = FALSE)
+
+ var/list/hearing_mobs = hear["mobs"]
+
+ for(var/mob in hearing_mobs)
+ var/mob/M = mob
+ if(!M.client)
+ continue
+ M.create_chat_message(src, message, italics, classes, audible)
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 46f001c8e1..f4117d37e8 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -34,6 +34,15 @@
// Track if we are already had initialize() called to prevent double-initialization.
var/initialized = FALSE
+ /// 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, ...)
// Don't call ..() unless /datum/New() ever exists
@@ -490,7 +499,7 @@
// Use for objects performing visible actions
// message is output to anyone who can see, e.g. "The [src] does something!"
// blind_message (optional) is what blind people will hear e.g. "You hear something!"
-/atom/proc/visible_message(var/message, var/blind_message, var/list/exclude_mobs, var/range = world.view)
+/atom/proc/visible_message(var/message, var/blind_message, var/list/exclude_mobs, var/range = world.view, var/runemessage = "ðŸ‘")
//VOREStation Edit
var/list/see
@@ -513,6 +522,8 @@
var/mob/M = mob
if(M.see_invisible >= invisibility && MOB_CAN_SEE_PLANE(M, plane))
M.show_message(message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "* [runemessage || message] *", FALSE, list("emote"), audible = FALSE)
else if(blind_message)
M.show_message(blind_message, AUDIBLE_MESSAGE)
@@ -521,7 +532,7 @@
// message is the message output to anyone who can hear.
// deaf_message (optional) is what deaf people will see.
// hearing_distance (optional) is the range, how many tiles away the message can be heard.
-/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance, var/radio_message)
+/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance, var/radio_message, var/runemessage)
var/range = hearing_distance || world.view
var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src),range,remote_ghosts = FALSE)
@@ -542,6 +553,8 @@
var/mob/M = mob
var/msg = message
M.show_message(msg, AUDIBLE_MESSAGE, deaf_message, VISIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "* [runemessage || message] *", FALSE, list("emote"))
/atom/movable/proc/dropInto(var/atom/destination)
while(istype(destination))
diff --git a/code/game/world.dm b/code/game/world.dm
index 9f84772b9f..2895a191db 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -1,5 +1,6 @@
#define RECOMMENDED_VERSION 501
/world/New()
+ world_startup_time = world.timeofday
to_world_log("Map Loading Complete")
//logs
//VOREStation Edit Start
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index d8deb0dce6..b0f553a487 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -324,8 +324,7 @@
var/message = sanitize(input("What do you want the message to be?", "Make Sound") as text|null)
if(!message)
return
- for (var/mob/V in hearers(O))
- V.show_message(message, 2)
+ O.audible_message(message)
log_admin("[key_name(usr)] made [O] at [O.x], [O.y], [O.z]. make a sound")
message_admins("[key_name_admin(usr)] made [O] at [O.x], [O.y], [O.z]. make a sound.", 1)
feedback_add_details("admin_verb","MS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm
index 9fb7295266..abd55097a8 100644
--- a/code/modules/client/client defines.dm
+++ b/code/modules/client/client defines.dm
@@ -76,3 +76,6 @@
var/connection_realtime
///world.timeofday they connected
var/connection_timeofday
+
+ // Runechat messages
+ var/list/seen_messages = list()
diff --git a/code/modules/client/preference_setup/global/setting_datums.dm b/code/modules/client/preference_setup/global/setting_datums.dm
index 24cca9ca18..25eae07ff1 100644
--- a/code/modules/client/preference_setup/global/setting_datums.dm
+++ b/code/modules/client/preference_setup/global/setting_datums.dm
@@ -290,6 +290,32 @@ var/list/_client_preferences_by_type
enabled_description = "Show"
disabled_description = "Hide"
+/datum/client_preference/runechat_mob
+ description = "Runechat (Mobs)"
+ key = "RUNECHAT_MOB"
+ enabled_description = "Show"
+ disabled_description = "Hide"
+
+/datum/client_preference/runechat_obj
+ description = "Runechat (Objs)"
+ key = "RUNECHAT_OBJ"
+ enabled_description = "Show"
+ disabled_description = "Hide"
+
+/datum/client_preference/runechat_border
+ description = "Runechat Message Border"
+ key = "RUNECHAT_BORDER"
+ enabled_description = "Show"
+ disabled_description = "Hide"
+ enabled_by_default = FALSE
+
+/datum/client_preference/runechat_long_messages
+ description = "Runechat Message Length"
+ key = "RUNECHAT_LONG"
+ enabled_description = "ERP KING"
+ disabled_description = "Normie"
+ enabled_by_default = FALSE
+
/datum/client_preference/status_indicators/toggled(mob/preference_mob, enabled)
. = ..()
if(preference_mob && preference_mob.plane_holder)
diff --git a/code/modules/emotes/emote_define.dm b/code/modules/emotes/emote_define.dm
index 6b50b09dde..d8184b9bae 100644
--- a/code/modules/emotes/emote_define.dm
+++ b/code/modules/emotes/emote_define.dm
@@ -103,11 +103,14 @@ var/global/list/emotes_by_key
if(target)
use_1p = replace_target_tokens(use_1p, target)
use_1p = "[capitalize(replace_user_tokens(use_1p, user))]"
- var/use_3p = get_emote_message_3p(user, target, extra_params)
- if(use_3p)
+ var/prefinal_3p
+ var/use_3p
+ var/raw_3p = get_emote_message_3p(user, target, extra_params)
+ if(raw_3p)
if(target)
- use_3p = replace_target_tokens(use_3p, target)
- use_3p = "\The [user] [replace_user_tokens(use_3p, user)]"
+ raw_3p = replace_target_tokens(raw_3p, target)
+ prefinal_3p = replace_user_tokens(raw_3p, user)
+ use_3p = "\The [user] [prefinal_3p]"
var/use_radio = get_radio_message(user)
if(use_radio)
if(target)
@@ -124,12 +127,12 @@ var/global/list/emotes_by_key
if(isliving(user))
var/mob/living/L = user
if(L.silent)
- M.visible_message(message = "[user] opens their mouth silently!", self_message = "You cannot say anything!", blind_message = emote_message_impaired)
+ M.visible_message(message = "[user] opens their mouth silently!", self_message = "You cannot say anything!", blind_message = emote_message_impaired, runemessage = "opens their mouth silently!")
return
else
- M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, radio_message = use_radio)
+ M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, radio_message = use_radio, runemessage = prefinal_3p)
else
- M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range)
+ M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range, runemessage = prefinal_3p)
do_extra(user, target)
do_sound(user)
diff --git a/code/modules/emotes/emote_mob.dm b/code/modules/emotes/emote_mob.dm
index 6154471580..ef4129ae46 100644
--- a/code/modules/emotes/emote_mob.dm
+++ b/code/modules/emotes/emote_mob.dm
@@ -86,7 +86,8 @@
return
if(use_emote.message_type == AUDIBLE_MESSAGE && is_muzzled())
- audible_message("\The [src] [use_emote.emote_message_muffled || "makes a muffled sound."]")
+ var/muffle_message = use_emote.emote_message_muffled || "makes a muffled sound."
+ audible_message("\The [src] [muffle_message]", runemessage = "* [muffle_message] *")
return
next_emote = world.time + use_emote.emote_delay
@@ -164,9 +165,13 @@
input = message
var/list/formatted
+ var/runemessage
if(input)
formatted = format_emote(src, message)
message = formatted["pretext"] + formatted["nametext"] + formatted["subtext"]
+ runemessage = formatted["subtext"]
+ // This is just personal preference (but I'm objectively right) that custom emotes shouldn't have periods at the end in runechat
+ runemessage = replacetext(runemessage,".","",length(runemessage),length(runemessage)+1)
else
return
@@ -194,6 +199,7 @@
if(isobserver(M))
message = "[src] ([ghost_follow_link(src, M)]) [input]"
M.show_message(message, m_type)
+ M.create_chat_message(src, "* [runemessage] *", FALSE, list("emote"), (m_type == AUDIBLE_MESSAGE))
for(var/obj in o_viewers)
var/obj/O = obj
diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm
index df959f07a2..8e4fd3970e 100644
--- a/code/modules/mob/hear_say.dm
+++ b/code/modules/mob/hear_say.dm
@@ -116,6 +116,7 @@
message_to_send = "[message_to_send]"
on_hear_say(message_to_send)
+ create_chat_message(speaker, combined["raw"], italics, list())
if(speech_sound && (get_dist(speaker, src) <= world.view && z == speaker.z))
var/turf/source = speaker ? get_turf(speaker) : get_turf(src)
diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm
index ae1b09ae71..07ae837760 100644
--- a/code/modules/mob/living/say.dm
+++ b/code/modules/mob/living/say.dm
@@ -40,18 +40,18 @@ var/list/department_radio_keys = list(
//kinda localization -- rastaf0
//same keys as above, but on russian keyboard layout. This file uses cp1251 as encoding.
- ":ê" = "right ear", ".ê" = "right ear",
- ":ä" = "left ear", ".ä" = "left ear",
- ":ø" = "intercom", ".ø" = "intercom",
- ":ð" = "department", ".ð" = "department",
- ":ñ" = "Command", ".ñ" = "Command",
- ":ò" = "Science", ".ò" = "Science",
- ":ü" = "Medical", ".ü" = "Medical",
- ":ó" = "Engineering", ".ó" = "Engineering",
- ":û" = "Security", ".û" = "Security",
- ":ö" = "whisper", ".ö" = "whisper",
- ":å" = "Mercenary", ".å" = "Mercenary",
- ":é" = "Supply", ".é" = "Supply",
+ ":�" = "right ear", ".�" = "right ear",
+ ":�" = "left ear", ".�" = "left ear",
+ ":�" = "intercom", ".�" = "intercom",
+ ":�" = "department", ".�" = "department",
+ ":�" = "Command", ".�" = "Command",
+ ":�" = "Science", ".�" = "Science",
+ ":�" = "Medical", ".�" = "Medical",
+ ":�" = "Engineering", ".�" = "Engineering",
+ ":�" = "Security", ".�" = "Security",
+ ":�" = "whisper", ".�" = "whisper",
+ ":�" = "Mercenary", ".�" = "Mercenary",
+ ":�" = "Supply", ".�" = "Supply",
)
@@ -362,16 +362,17 @@ proc/get_radio_key_from_channel(var/channel)
//VOREStation Add End
var/dst = get_dist(get_turf(M),get_turf(src))
+ var/runechat_enabled = M.client?.is_preference_enabled(/datum/client_preference/runechat_mob)
if(dst <= message_range || (M.stat == DEAD && !forbid_seeing_deadchat)) //Inside normal message range, or dead with ears (handled in the view proc)
- if(M.client)
+ if(M.client && !runechat_enabled)
var/image/I1 = listening[M] || speech_bubble
images_to_clients[I1] |= M.client
M << I1
M.hear_say(message_pieces, verb, italics, src, speech_sound, sound_vol)
if(whispering && !isobserver(M)) //Don't even bother with these unless whispering
if(dst > message_range && dst <= w_scramble_range) //Inside whisper scramble range
- if(M.client)
+ if(M.client && !runechat_enabled)
var/image/I2 = listening[M] || speech_bubble
images_to_clients[I2] |= M.client
M << I2
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 00d705f8e1..6626b68972 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -77,7 +77,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(var/message, var/self_message, var/blind_message, var/list/exclude_mobs = null, var/range = world.view)
+/mob/visible_message(var/message, var/self_message, var/blind_message, var/list/exclude_mobs = null, var/range = world.view, var/runemessage)
if(self_message)
if(LAZYLEN(exclude_mobs))
exclude_mobs |= src
@@ -87,7 +87,9 @@
// Transfer messages about what we are doing to upstairs
if(shadow)
shadow.visible_message(message, self_message, blind_message, exclude_mobs, range)
- . = ..(message, blind_message, exclude_mobs, range) // Really not ideal that atom/visible_message has different arg numbering :(
+ if(isnull(runemessage))
+ runemessage = -1
+ . = ..(message, blind_message, exclude_mobs, range, runemessage) // Really not ideal that atom/visible_message has different arg numbering :(
// Returns an amount of power drawn from the object (-1 if it's not viable).
// If drain_check is set it will not actually drain power, just return a value.
@@ -102,7 +104,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(var/message, var/deaf_message, var/hearing_distance, var/self_message, var/radio_message)
+/mob/audible_message(var/message, var/deaf_message, var/hearing_distance, var/self_message, var/radio_message, var/runemessage)
var/range = hearing_distance || world.view
var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src),range,remote_ghosts = FALSE)
@@ -110,6 +112,9 @@
var/list/hearing_mobs = hear["mobs"]
var/list/hearing_objs = hear["objs"]
+ if(isnull(runemessage))
+ runemessage = -1 // Symmetry with mob/audible_message, despite the fact this one doesn't call parent. Maybe it should!
+
if(radio_message)
for(var/obj in hearing_objs)
var/obj/O = obj
@@ -125,6 +130,8 @@
if(self_message && M==src)
msg = self_message
M.show_message(msg, AUDIBLE_MESSAGE, deaf_message, VISIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "* [runemessage || message] *", FALSE, list("emote"), audible = FALSE)
/mob/proc/findname(msg)
for(var/mob/M in mob_list)
diff --git a/code/modules/ventcrawl/ventcrawl_atmospherics.dm b/code/modules/ventcrawl/ventcrawl_atmospherics.dm
index 1fd9dd077c..80348c7699 100644
--- a/code/modules/ventcrawl/ventcrawl_atmospherics.dm
+++ b/code/modules/ventcrawl/ventcrawl_atmospherics.dm
@@ -42,7 +42,16 @@
user.client.eye = target_move //if we don't do this, Byond only updates the eye every tick - required for smooth movement
if(world.time > user.next_play_vent)
user.next_play_vent = world.time+30
- playsound(src, 'sound/machines/ventcrawl.ogg', 50, 1, -3)
+ var/turf/T = get_turf(src)
+ playsound(T, 'sound/machines/ventcrawl.ogg', 50, 1, -3)
+ var/message = pick(
+ prob(90);"* clunk *",
+ prob(90);"* thud *",
+ prob(90);"* clatter *",
+ prob(1);"* à¶ž *"
+ )
+ T.runechat_message(message)
+
else
if((direction & initialize_directions) || is_type_in_list(src, ventcrawl_machinery) && src.can_crawl_through()) //if we move in a way the pipe can connect, but doesn't - or we're in a vent
user.remove_ventcrawl()
diff --git a/interface/skin.dmf b/interface/skin.dmf
index fc14eb8a69..b1d9a6267e 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -1282,7 +1282,7 @@ window "mapwindow"
saved-params = "icon-size"
on-show = ".winset\"mainwindow.mainvsplit.left=mapwindow\""
on-hide = ".winset\"mainwindow.mainvsplit.left=\""
- style=".center { text-align: center; } .maptext { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; color: white; line-height: 1.1; } .small { font-size: 6px; } .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .clown { color: #FF69Bf;} .tajaran {color: #803B56;} .skrell {color: #00CED1;} .solcom {color: #22228B;} .com_srus {color: #7c4848;} .zombie {color: #ff0000;} .soghun {color: #228B22;} .vox {color: #AA00AA;} .diona {color: #804000; font-weight: bold;} .trinary {color: #727272;} .kidan {color: #664205;} .slime {color: #0077AA;} .drask {color: #a3d4eb;} .vulpkanin {color: #B97A57;} .abductor {color: #800080; font-style: italic;} .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; }"
+ style=".center { text-align: center; } .runechatdiv {background-color: #20202070} .black_outline { -dm-text-outline: 1px black } .boldtext { font-weight: bold; } .maptext { font-family: 'Small Fonts'; font-size: 7px; color: white; line-height: 1.1; } .command_headset { font-weight: bold; font-size: 8px; } .small { font-size: 6px; } .very_small { font-size: 5px;} .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .greentext { color: #00FF00; font-size: 7px; } .redtext { color: #FF0000; font-size: 7px; } .clown { color: #FF69Bf; font-size: 7px; font-weight: bold; } .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; } .italics { font-size: 7px; font-style: italic; }"
window "outputwindow"
elem "outputwindow"
diff --git a/vorestation.dme b/vorestation.dme
index f3e9b29028..f593832163 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -308,6 +308,7 @@
#include "code\datums\browser.dm"
#include "code\datums\callback.dm"
#include "code\datums\category.dm"
+#include "code\datums\chat_message.dm"
#include "code\datums\computerfiles.dm"
#include "code\datums\datacore.dm"
#include "code\datums\datum.dm"