diff --git a/citadel.dme b/citadel.dme
index 7c7f9707d0ac..813c46fd2a40 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -43,6 +43,7 @@
#include "code\__DEFINES\chat.dm"
#include "code\__DEFINES\chemistry.dm"
#include "code\__DEFINES\coloration.dm"
+#include "code\__DEFINES\colors.dm"
#include "code\__DEFINES\configuration.dm"
#include "code\__DEFINES\construction.dm"
#include "code\__DEFINES\damage_organs.dm"
@@ -377,6 +378,7 @@
#include "code\__HELPERS\atom_movables.dm"
#include "code\__HELPERS\chat.dm"
#include "code\__HELPERS\coloration.dm"
+#include "code\__HELPERS\colors.dm"
#include "code\__HELPERS\datum.dm"
#include "code\__HELPERS\debugging.dm"
#include "code\__HELPERS\do_after.dm"
@@ -603,6 +605,7 @@
#include "code\controllers\subsystem\preferences.dm"
#include "code\controllers\subsystem\radiation.dm"
#include "code\controllers\subsystem\repository.dm"
+#include "code\controllers\subsystem\runechat.dm"
#include "code\controllers\subsystem\server_maint.dm"
#include "code\controllers\subsystem\shuttles.dm"
#include "code\controllers\subsystem\simple_networks.dm"
@@ -702,6 +705,7 @@
#include "code\datums\category.dm"
#include "code\datums\changelog.dm"
#include "code\datums\character_profile.dm"
+#include "code\datums\chatmessage.dm"
#include "code\datums\computerfiles.dm"
#include "code\datums\datacore.dm"
#include "code\datums\datum.dm"
@@ -5381,6 +5385,12 @@
#include "interface\interface.dm"
#include "interface\stylesheet.dm"
#include "interface\skin.dmf"
+#include "interface\fonts\fonts_datum.dm"
+#include "interface\fonts\grand_9k.dm"
+#include "interface\fonts\pixellari.dm"
+#include "interface\fonts\spess_font.dm"
+#include "interface\fonts\tiny_unicode.dm"
+#include "interface\fonts\vcr_osd_mono.dm"
#include "interface\menus\_menus.dm"
#include "interface\menus\main.dm"
#include "maps\endeavour\endeavour-areas.dm"
diff --git a/code/__DEFINES/_lists.dm b/code/__DEFINES/_lists.dm
index 0496f1c3ef84..a36dd7f59569 100644
--- a/code/__DEFINES/_lists.dm
+++ b/code/__DEFINES/_lists.dm
@@ -25,6 +25,12 @@
#define LAZYLEN(L) length(L)
///Sets a list to null
#define LAZYNULL(L) L = null
+///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);
+///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; }
/// Null-safe L.Cut()
#define LAZYCLEARLIST(L) if(L) L.Cut()
/// Null-safe L.Copy()
diff --git a/code/__DEFINES/_tick.dm b/code/__DEFINES/_tick.dm
index d3d966c35454..21421e207fc8 100644
--- a/code/__DEFINES/_tick.dm
+++ b/code/__DEFINES/_tick.dm
@@ -32,6 +32,11 @@
/// runs stoplag if tick_usage is above the limit
#define CHECK_TICK ( TICK_CHECK ? stoplag() : 0 )
+/// Checks if a sleeping proc is running before or after the master controller
+#define RUNNING_BEFORE_MASTER ( Master.last_run != null && Master.last_run != world.time )
+/// Returns true if a verb ought to yield to the MC (IE: queue up to be processed by a subsystem)
+#define VERB_SHOULD_YIELD ( TICK_CHECK || RUNNING_BEFORE_MASTER )
+
/// Returns true if tick usage is above 95, for high priority usage
///
/// * Use for admin functions so they stay responsive and functional during lag.
diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
new file mode 100644
index 000000000000..55314e84bd37
--- /dev/null
+++ b/code/__DEFINES/colors.dm
@@ -0,0 +1,5 @@
+
+#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
diff --git a/code/__DEFINES/controllers/_subsystem-priority.dm b/code/__DEFINES/controllers/_subsystem-priority.dm
index 297142ed0734..9662104ecbfd 100644
--- a/code/__DEFINES/controllers/_subsystem-priority.dm
+++ b/code/__DEFINES/controllers/_subsystem-priority.dm
@@ -49,7 +49,8 @@
#define FIRE_PRIORITY_OVERLAYS 100
#define FIRE_PRIORITY_SMOOTHING 100
#define FIRE_PRIORITY_CHAT 100
-#define FIRE_PRIORITY_INPUT 100
+#define FIRE_PRIORITY_RUNECHAT 100
+#define FIRE_PRIORITY_INPUT 1000 // we dont want to drop user input right?
//? Ticker Subsystems - Highest priority
// Any subsystem flagged with SS_TICKER is here!
diff --git a/code/__DEFINES/controllers/_subsystem.dm b/code/__DEFINES/controllers/_subsystem.dm
index f9fce73f1a02..af2219e780e4 100644
--- a/code/__DEFINES/controllers/_subsystem.dm
+++ b/code/__DEFINES/controllers/_subsystem.dm
@@ -20,13 +20,24 @@
}\
/datum/controller/subsystem/##X
+/**
+ * Defines a timer subsystem.
+ */
+#define TIMER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/timer/##X);\
+/datum/controller/subsystem/timer/##X/New(){\
+ NEW_SS_GLOBAL(SS##X);\
+}\
+/datum/controller/subsystem/timer/##X/fire() {..() /*just so it shows up on the profiler*/} \
+/datum/controller/subsystem/timer/##X
+
/**
* Defines a processing subsystem.
*/
#define PROCESSING_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/processing/##X);\
/datum/controller/subsystem/processing/##X/New(){\
- NEW_SS_GLOBAL(SS##X);\
+ NEW_SS_GLOBAL(SS##X);\
}\
+/datum/controller/subsystem/processing/##X/fire() {..() /*just so it shows up on the profiler*/} \
/datum/controller/subsystem/processing/##X
//* Subsystem flags *//
diff --git a/code/__DEFINES/fonts.dm b/code/__DEFINES/fonts.dm
index 6349d55d1b5f..f96373cb2999 100644
--- a/code/__DEFINES/fonts.dm
+++ b/code/__DEFINES/fonts.dm
@@ -23,3 +23,7 @@
/// Emoji icon set
#define EMOJI_SET 'icons/ui_icons/emoji/emoji.dmi'
#define EMOJI32_SET 'icons/ui_icons/emoji/emoji32.dmi'
+
+// Font metrics bitfield
+/// Include leading A width and trailing C width in GetWidth() or in DrawText()
+#define INCLUDE_AC (1<<0)
diff --git a/code/__DEFINES/machinery.dm b/code/__DEFINES/machinery.dm
index 87eea72002c1..d60072dc8af3 100644
--- a/code/__DEFINES/machinery.dm
+++ b/code/__DEFINES/machinery.dm
@@ -219,3 +219,14 @@ if (!(DATUM.datum_flags & DF_ISPROCESSING)) {\
#define ORION_GAMER_PAMPHLET -1
//game begins to have a chance to warn sec and med
#define ORION_GAMER_REPORT_THRESHOLD 2
+
+/// Blank Status Display
+#define SD_BLANK 0
+/// Shows the emergency shuttle timer
+#define SD_EMERGENCY 1
+/// Shows an arbitrary message, user-set
+#define SD_MESSAGE 2
+/// Shows an alert picture (e.g. red alert, radiation, etc.)
+#define SD_PICTURE 3
+/// Shows the current station time
+#define SD_TIME 4
diff --git a/code/__DEFINES/text.dm b/code/__DEFINES/text.dm
index 13f660df4292..da537639a19d 100644
--- a/code/__DEFINES/text.dm
+++ b/code/__DEFINES/text.dm
@@ -3,6 +3,58 @@
/// Removes characters incompatible with file names.
#define SANITIZE_FILENAME(text) (GLOB.filename_forbidden_chars.Replace(text, ""))
+
+//! Maptext
+/// Standard maptext
+/// Prepares a text to be used for maptext. Use this so it doesn't look hideous.
+#define MAPTEXT(text) {"[##text]"}
+/// Prepares a text to be used for maptext. Use this so it doesn't look hideous.
+#define MAPTEXT_CENTER(text) {"[##text]"}
+
+/**
+ * Pixel-perfect scaled fonts for use in the MAP element as defined in skin.dmf
+ *
+ * Four sizes to choose from, use the sizes as mentioned below.
+ * Between the variations and a step there should be an option that fits your use case.
+ * BYOND uses pt sizing, different than px used in TGUI. Using px will make it look blurry due to poor antialiasing.
+ *
+ * Default sizes are prefilled in the macro for ease of use and a consistent visual look.
+ * To use a step other than the default in the macro, specify it in a span style.
+ * For example: MAPTEXT_PIXELLARI("Some large maptext here")
+ */
+/// Large size (ie: context tooltips) - Size options: 12pt 24pt.
+#define MAPTEXT_PIXELLARI(text) {"[##text]"}
+
+/// Standard size (ie: normal runechat) - Size options: 6pt 12pt 18pt.
+#define MAPTEXT_GRAND9K(text) {"[##text]"}
+
+/// Small size. (ie: context subtooltips, spell delays) - Size options: 12pt 24pt.
+#define MAPTEXT_TINY_UNICODE(text) {"[##text]"}
+
+/// Smallest size. (ie: whisper runechat) - Size options: 6pt 12pt 18pt.
+#define MAPTEXT_SPESSFONT(text) {"[##text]"}
+
+/**
+ * Prepares a text to be used for maptext, using a variable size font.
+ *
+ * More flexible but doesn't scale pixel perfect to BYOND icon resolutions.
+ * (May be blurry.) Can use any size in pt or px.
+ *
+ * You MUST Specify the size when using the macro
+ * For example: MAPTEXT_VCR_OSD_MONO("Some large maptext here")
+ */
+/// Prepares a text to be used for maptext, using a variable size font.
+/// Variable size font. More flexible but doesn't scale pixel perfect to BYOND icon resolutions. (May be blurry.) Can use any size in pt or px.
+#define MAPTEXT_VCR_OSD_MONO(text) {"[##text]"}
+
+/// 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);
+
/*
/// Simply removes the < and > characters, and limits the length of the message.
#define STRIP_HTML_SIMPLE(text, limit) (GLOB.angular_brackets.Replace(copytext(text, 1, limit), ""))
@@ -54,11 +106,3 @@
/// File location for cult shuttle curse descriptions
#define CULT_SHUTTLE_CURSE "cult_shuttle_curse.json"
*/
-
-//! Maptext
-/// Prepares a text to be used for maptext. Use this so it doesn't look hideous.
-#define MAPTEXT(text) {"[##text]"}
-/// Prepares a text to be used for maptext. Use this so it doesn't look hideous.
-#define MAPTEXT_CENTER(text) {"[##text]"}
-/// Macro from Lummox used to get height from a MeasureText proc
-#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1))
diff --git a/code/__HELPERS/colors.dm b/code/__HELPERS/colors.dm
new file mode 100644
index 000000000000..c592d6356fc8
--- /dev/null
+++ b/code/__HELPERS/colors.dm
@@ -0,0 +1,48 @@
+
+/**
+ * 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
+ */
+/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(s * sat_shift, 0, 1)
+ l = clamp(l * 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
+ switch(h_int)
+ if(0)
+ return "#[num2hex(c, 2)][num2hex(x, 2)][num2hex(m, 2)]"
+ if(1)
+ return "#[num2hex(x, 2)][num2hex(c, 2)][num2hex(m, 2)]"
+ if(2)
+ return "#[num2hex(m, 2)][num2hex(c, 2)][num2hex(x, 2)]"
+ if(3)
+ return "#[num2hex(m, 2)][num2hex(x, 2)][num2hex(c, 2)]"
+ if(4)
+ return "#[num2hex(x, 2)][num2hex(m, 2)][num2hex(c, 2)]"
+ if(5)
+ return "#[num2hex(c, 2)][num2hex(m, 2)][num2hex(x, 2)]"
+
+#define RANDOM_COLOUR (rgb(rand(0,255),rand(0,255),rand(0,255)))
diff --git a/code/controllers/subsystem/runechat.dm b/code/controllers/subsystem/runechat.dm
new file mode 100644
index 000000000000..663bb8cf347d
--- /dev/null
+++ b/code/controllers/subsystem/runechat.dm
@@ -0,0 +1,14 @@
+TIMER_SUBSYSTEM_DEF(runechat)
+ name = "Runechat"
+ priority = FIRE_PRIORITY_RUNECHAT
+
+ var/list/datum/callback/message_queue = list()
+
+/datum/controller/subsystem/timer/runechat/fire(resumed)
+ . = ..() //poggers
+ while(message_queue.len)
+ var/datum/callback/queued_message = message_queue[message_queue.len]
+ queued_message.Invoke()
+ message_queue.len--
+ if(MC_TICK_CHECK)
+ return
diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm
new file mode 100644
index 000000000000..dcce696a25fe
--- /dev/null
+++ b/code/datums/chatmessage.dm
@@ -0,0 +1,298 @@
+/// 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 width of chat message in pixels
+#define CHAT_MESSAGE_WIDTH 112
+/// The dimensions of the chat message icons
+#define CHAT_MESSAGE_ICON_SIZE 9
+
+///Base layer of chat elements
+#define CHAT_LAYER 1
+///Highest possible layer of chat elements
+#define CHAT_LAYER_MAX 2
+/// 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 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
+ /// 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("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner")
+ qdel(src)
+ return
+ INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, extra_classes, lifespan)
+
+/datum/chatmessage/Destroy()
+ if (!QDELING(owned_by))
+ if(REALTIMEOFDAY < animate_start + animate_lifespan)
+ stack_trace("Del'd before we finished fading, with [(animate_start + animate_lifespan) - REALTIMEOFDAY] 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 ..()
+
+/**
+ * Calls qdel on the chatmessage when its parent is deleted, used to register qdel signal
+ */
+/datum/chatmessage/proc/on_parent_qdel()
+ SIGNAL_HANDLER
+ 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
+ * * 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
+ RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel))
+
+ // 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/maxlen = 160
+ if (length_char(text) > maxlen)
+ text = copytext_char(text, 1, maxlen + 1) + "..." // BYOND index moment
+
+ // 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"
+
+ var/list/prefixes
+ var/chat_color_name_to_use = target.get_nametag_name() // use nametag as fallback
+
+ if (ishuman(target))
+ var/mob/living/carbon/human/H = target
+ chat_color_name_to_use = H.get_visible_name() // get ID name if exists
+
+ // Calculate target color if not already present
+ if (!target.chat_color || target.chat_color_name != chat_color_name_to_use)
+ target.chat_color = colorize_string(chat_color_name_to_use)
+ target.chat_color_darkened = colorize_string(chat_color_name_to_use, 0.85, 0.85)
+ target.chat_color_name = chat_color_name_to_use
+
+ 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 = "[text]"
+
+ var/mheight
+ WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight)
+
+
+ if(!VERB_SHOULD_YIELD)
+ return finish_image_generation(mheight, target, owner, complete_text, lifespan)
+
+ var/datum/callback/our_callback = CALLBACK(src, PROC_REF(finish_image_generation), mheight, target, owner, complete_text, lifespan)
+ SSrunechat.message_queue += our_callback
+ return
+
+///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 = REALTIMEOFDAY
+ approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
+ var/starting_height = target.maptext_height
+ // 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)
+ if(m.message.pixel_y < starting_height)
+ var/max_height = m.message.pixel_y + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
+ if(max_height > 0)
+ animate(m.message, pixel_y = m.message.pixel_y + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+ else if(mheight + starting_height >= m.message.pixel_y)
+ 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
+ if(m.message.pixel_y < starting_height)
+ var/max_height = m.message.pixel_y + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
+ if(max_height > 0)
+ animate(m.message, pixel_y = m.message.pixel_y + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+ else if(mheight + starting_height >= m.message.pixel_y)
+ animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+
+ // 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 = HUD_PLANE
+ message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
+ message.alpha = 0
+ message.pixel_y = starting_height
+ message.pixel_x = -target.base_pixel_x
+ message.maptext_width = CHAT_MESSAGE_WIDTH
+ message.maptext_height = mheight * 1.25 // We add extra because some characters are superscript, like actions
+ message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5
+ message.maptext = MAPTEXT(complete_text)
+
+ animate_start = rough_time
+ animate_lifespan = lifespan
+
+ // 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)
+
+ // Register with the runechat SS to handle destruction
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), lifespan + CHAT_MESSAGE_GRACE_PERIOD, TIMER_DELETE_ME, SSrunechat)
+
+/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_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
+ */
+/mob/proc/create_chat_message(atom/movable/speaker, datum/prototype/language/message_language, raw_message, list/spans, runechat_flags = NONE)
+ // 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 ourselves
+ // if (originalSpeaker != src && speaker == src)
+ // return
+
+ new /datum/chatmessage(raw_message, speaker, src, spans)
+
+#undef CHAT_LAYER_MAX_Z
+#undef CHAT_LAYER_Z_STEP
+#undef CHAT_MESSAGE_APPROX_LHEIGHT
+#undef CHAT_MESSAGE_GRACE_PERIOD
+#undef CHAT_MESSAGE_EOL_FADE
+#undef CHAT_MESSAGE_EXP_DECAY
+#undef CHAT_MESSAGE_HEIGHT_DECAY
+#undef CHAT_MESSAGE_ICON_SIZE
+#undef CHAT_MESSAGE_LIFESPAN
+#undef CHAT_MESSAGE_SPAWN_TIME
+#undef CHAT_MESSAGE_WIDTH
diff --git a/code/game/atoms/_atom.dm b/code/game/atoms/_atom.dm
index e4e9097fd3ed..5761d2896d71 100644
--- a/code/game/atoms/_atom.dm
+++ b/code/game/atoms/_atom.dm
@@ -180,6 +180,14 @@
/// Relative layer - position this atom should be in within things of the same base layer. defaults to 0.
var/relative_layer = 0
+ //? Chat colors 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
+
//? Pixel Offsets
/// Default pixel x shifting for the atom's icon.
var/base_pixel_x = 0
@@ -538,13 +546,26 @@
else
return 0
-/// Show a message to all mobs and objects in sight of this atom
-/// 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!"
-// todo: refactor
+/**
+ * Generate a visible message from this atom
+ *
+ * Show a message to all player mobs who sees this atom
+ *
+ * Show a message to the src mob (if the src is a mob)
+ *
+ * Use for atoms performing visible actions
+ *
+ * message is output to anyone who can see, e.g. `"The [src] does something!"`
+ *
+ * Vars:
+ * * message is the message output to anyone who can see.
+ * * 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!"
+ * * range (optional) define how many tiles away the message can be seen.
+ */
/atom/proc/visible_message(message, self_message, blind_message, range = world.view)
var/list/see
+
//! LEGACY
if(isbelly(loc))
var/obj/belly/B = loc
@@ -552,6 +573,7 @@
else
see = get_hearers_in_view(range, src)
//! end
+
for(var/atom/movable/AM as anything in see)
if(ismob(AM))
var/mob/M = AM
@@ -561,6 +583,9 @@
M.show_message(message, 1, blind_message, 2)
else if(blind_message)
M.show_message(blind_message, 2)
+
+ if(!M.is_blind() && M.client)
+ M.create_chat_message(src, raw_message = message)
else
AM.show_message(message, 1, blind_message, 2)
@@ -568,15 +593,19 @@
/atom/movable/proc/show_message(msg, type, alt, alt_type)//Message, type of message (1 or 2), alternative message, alt message type (1 or 2)
return
-/// Show a message to all mobs and objects in earshot of this atom
-/// Use for objects performing audible actions
-/// 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, datum/prototype/language/lang)
-
- var/range = hearing_distance || world.view
- var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src),range,remote_ghosts = FALSE)
+/**
+ * Show a message to all mobs in earshot of this atom
+ *
+ * Use for objects performing audible actions
+ *
+ * vars:
+ * * 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.
+ * * lang (optional) our language
+ */
+/atom/proc/audible_message(message, deaf_message, hearing_distance = world.view, datum/prototype/language/lang)
+ var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src), hearing_distance, remote_ghosts = FALSE)
var/list/hearing_mobs = hear["mobs"]
var/list/hearing_objs = hear["objs"]
@@ -585,16 +614,16 @@
var/obj/O = obj
O.show_message(message, 2, deaf_message, 1)
- var/no_runechat = FALSE
for(var/mob in hearing_mobs)
var/mob/M = mob
var/msg = message
if(lang && !(lang.name in M.languages))
msg = lang.scramble(msg)
M.show_message(msg, 2, deaf_message, 1)
+ // code above scrambles it
+ if(!M.is_blind() && M.client)
+ M.create_chat_message(src, raw_message = (message ? msg : deaf_message))
heard_to_floating_message += M
- if(!no_runechat && ismovable(src))
- INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, animate_chat), (message ? message : deaf_message), null, FALSE, heard_to_floating_message, 30)
/atom/movable/proc/dropInto(var/atom/destination)
while(istype(destination))
diff --git a/code/game/machinery/status_display.dm b/code/game/machinery/status_display.dm
index 62b3b1284c7a..cdc2ab5a7d2d 100644
--- a/code/game/machinery/status_display.dm
+++ b/code/game/machinery/status_display.dm
@@ -1,58 +1,66 @@
-#define FONT_SIZE "5pt"
-#define FONT_COLOR "#09f"
-#define FONT_STYLE "Small Fonts"
-#define SCROLL_SPEED 2
-
// Status display
-// (formerly Countdown timer display)
-// Use to show shuttle ETA/ETD times
-// Alert status
-// And arbitrary messages set by comms computer
+#define MAX_STATIC_WIDTH 22
+#define FONT_STYLE "12pt 'TinyUnicode'"
+#define SCROLL_RATE (0.04 SECONDS) // time per pixel
+#define SCROLL_PADDING 2 // how many pixels we chop to make a smooth loop
+#define LINE1_X 1
+#define LINE1_Y -4
+#define LINE2_X 1
+#define LINE2_Y -11
+GLOBAL_DATUM_INIT(status_font, /datum/font, new /datum/font/tiny_unicode/size_12pt())
+
+/// Status display which can show images and scrolling text.
/obj/machinery/status_display
+ name = "status display"
+ desc = null
icon = 'icons/obj/status_display.dmi'
icon_state = "frame"
plane = TURF_PLANE
layer = ABOVE_TURF_LAYER
- name = "status display"
anchored = TRUE
density = FALSE
use_power = USE_POWER_IDLE
idle_power_usage = 10
circuit = /obj/item/circuitboard/status_display
- var/mode = 1 // 0 = Blank
+
+
+ // We store overlays as keys, so multiple displays can use the same object safely
+ /// String key we use to index the first effect overlay displayed on us
+ var/message_key_1
+ /// String key we use to index the second effect overlay displayed on us
+ var/message_key_2
+ var/current_picture = ""
+ var/current_mode = 1 // 0 = Blank
// 1 = Shuttle timer
// 2 = Arbitrary message(s)
// 3 = Alert picture
// 4 = Supply shuttle timer
-
- var/picture_state // Icon_state of alert picture
var/message1 = "" // Message line 1
var/message2 = "" // Message line 2
- var/index1 // Display index for scrolling messages or 0 if non-scrolling
- var/index2
- var/picture = null
- var/frequency = FREQ_STATUS_DISPLAYS // Radio frequency
- var/friendc = 0 // Track if Friend Computer mode
- var/ignore_friendc = 0
+ /// Normal text color
+ var/text_color = "#22ccff"
+ /// Color for headers, eg. "- ETA -"
+ var/header_text_color = "#5d5dfc"
- maptext_height = 26
- maptext_width = 32
- maptext_y = -1
+ var/frequency = FREQ_STATUS_DISPLAYS // Radio frequency
- var/const/CHARS_PER_LINE = 5
- var/const/STATUS_DISPLAY_BLANK = 0
- var/const/STATUS_DISPLAY_TRANSFER_SHUTTLE_TIME = 1
- var/const/STATUS_DISPLAY_MESSAGE = 2
- var/const/STATUS_DISPLAY_ALERT = 3
- var/const/STATUS_DISPLAY_TIME = 4
- var/const/STATUS_DISPLAY_CUSTOM = 99
+ var/friendc = FALSE
+ var/last_picture // For when Friend Computer mode is undone
var/seclevel = "green"
+// Register for radio system
+/obj/machinery/status_display/Initialize(mapload, ndir, building)
+ . = ..()
+ update_appearance()
+ if(radio_controller)
+ radio_controller.add_object(src, frequency)
+
/obj/machinery/status_display/Destroy()
+ remove_messages()
if(radio_controller)
radio_controller.remove_object(src,frequency)
return ..()
@@ -64,114 +72,202 @@
attack_hand(user)
return
-// Register for radio system
-/obj/machinery/status_display/Initialize(mapload)
+/// Immediately change the display to the given picture.
+/obj/machinery/status_display/proc/set_picture(state)
+ if(state != current_picture)
+ current_picture = state
+
+ update_appearance()
+
+/// Immediately change the display to the given two lines.
+/obj/machinery/status_display/proc/set_messages(line1, line2)
+ line1 = uppertext(line1)
+ line2 = uppertext(line2)
+
+ var/message_changed = FALSE
+ if(line1 != message1)
+ message1 = line1
+ message_changed = TRUE
+
+ if(line2 != message2)
+ message2 = line2
+ message_changed = TRUE
+
+ if(message_changed)
+ update_appearance()
+
+/**
+ * Remove both message objs and null the fields.
+ * Don't call this in subclasses.
+ */
+/obj/machinery/status_display/proc/remove_messages()
+ var/obj/effect/overlay/status_display_text/overlay_1 = get_status_text(message_key_1)
+ message_key_1 = null
+ overlay_1?.disown(src)
+ var/obj/effect/overlay/status_display_text/overlay_2 = get_status_text(message_key_2)
+ message_key_2 = null
+ overlay_2?.disown(src)
+
+// List in the form key -> status display that shows said key
+GLOBAL_LIST_EMPTY(key_to_status_display)
+
+/proc/generate_status_text(line_y, message, x_offset, text_color, header_text_color, line_pair)
+ var/key = "[line_y]-[message]-[x_offset]-[text_color]-[header_text_color]-[line_pair]"
+ var/obj/effect/overlay/status_display_text/new_overlay = GLOB.key_to_status_display[key]
+ if(!new_overlay)
+ new_overlay = new(null, line_y, message, text_color, header_text_color, x_offset, line_pair, key)
+ GLOB.key_to_status_display[key] = new_overlay
+ return new_overlay
+
+/proc/get_status_text(key)
+ return GLOB.key_to_status_display[key]
+
+/**
+ * Create/update message overlay.
+ * They must be handled as real objects for the animation to run.
+ * Don't call this in subclasses.
+ * Arguments:
+ * * overlay - the current /obj/effect/overlay/status_display_text instance
+ * * line_y - The Y offset to render the text.
+ * * x_offset - Used to offset the text on the X coordinates, not usually needed.
+ * * message - the new message text.
+ * Returns new /obj/effect/overlay/status_display_text or null if unchanged.
+ */
+/obj/machinery/status_display/proc/update_message(current_key, line_y, message, x_offset, line_pair)
+ var/obj/effect/overlay/status_display_text/current_overlay = get_status_text(current_key)
+ var/obj/effect/overlay/status_display_text/new_overlay = generate_status_text(line_y, message, x_offset, text_color, header_text_color, line_pair)
+
+ if(current_overlay == new_overlay)
+ return current_key
+
+ current_overlay?.disown(src)
+ new_overlay.own(src)
+ return new_overlay.status_key
+
+/obj/machinery/status_display/update_appearance(updates=ALL)
. = ..()
- if(radio_controller)
- radio_controller.add_object(src, frequency)
+ if( \
+ (machine_stat & (NOPOWER|BROKEN)) || \
+ (current_mode == SD_BLANK) || \
+ (current_mode != SD_PICTURE && message1 == "" && message2 == "") \
+ )
+ set_light(0)
+ return
+ set_light(1.5, 0.7, "#CAF0FF") // blue light
-// Timed process
-/obj/machinery/status_display/process(delta_time)
- if(machine_stat & NOPOWER)
- remove_display()
+/obj/machinery/status_display/update_overlays(updates)
+ . = ..()
+
+ if(machine_stat & (NOPOWER|BROKEN))
+ remove_messages()
return
+
+ switch(current_mode)
+ if(SD_BLANK)
+ remove_messages()
+ // Turn off backlight.
+ return
+ if(SD_PICTURE)
+ remove_messages()
+ . += mutable_appearance('icons/obj/status_display.dmi', current_picture)
+ if(current_picture == "ai_off") // If the thing's off, don't display the emissive yeah?
+ return .
+ else
+ var/line1_metric
+ var/line2_metric
+ var/line_pair
+ line1_metric = GLOB.status_font.get_metrics(message1)
+ line2_metric = GLOB.status_font.get_metrics(message2)
+ line_pair = (line1_metric > line2_metric ? line1_metric : line2_metric)
+
+ message_key_1 = update_message(message_key_1, LINE1_Y, message1, LINE1_X, line_pair)
+ message_key_2 = update_message(message_key_2, LINE2_Y, message2, LINE2_X, line_pair)
+
+ // Turn off backlight if message is blank
+ if(message1 == "" && message2 == "")
+ return
+
+ . += emissive_appearance(icon, "outline", src, alpha = src.alpha)
+
+/obj/machinery/status_display/process()
+ if(machine_stat & NOPOWER)
+ // No power, no processing.
+ update_appearance()
+
+ if(friendc)
+ current_mode = SD_PICTURE
+ set_picture("ai_friend")
+ return PROCESS_KILL
+
+ switch(current_mode)
+ if(SD_EMERGENCY)
+ return display_escape_shuttle_status()
+
+ if(SD_MESSAGE)
+ return PROCESS_KILL
+
+ if(SD_PICTURE)
+ set_picture(last_picture)
+ return PROCESS_KILL
+
+ if(SD_TIME)
+ // will be constantly updating
+ set_messages("TIME", stationtime2text())
+ return
+
+ return PROCESS_KILL
+
+/// Update the display and, if necessary, re-enable processing.
+/obj/machinery/status_display/proc/update()
+ if (process(SSobj.wait/10) != PROCESS_KILL)
+ START_PROCESSING(SSobj, src)
+
+/obj/machinery/status_display/power_change()
+ . = ..()
update()
/obj/machinery/status_display/emp_act(severity)
- if(machine_stat & (BROKEN|NOPOWER))
- ..(severity)
+ . = ..()
+ if(machine_stat & (NOPOWER|BROKEN))
return
+ current_mode = SD_PICTURE
set_picture("ai_bsod")
- ..(severity)
-
-// Set what is displayed
-/obj/machinery/status_display/proc/update()
- remove_display()
- if(friendc && !ignore_friendc)
- set_picture("ai_friend")
- return 1
-
- switch(mode)
- if(STATUS_DISPLAY_BLANK) // Blank
- return 1
- if(STATUS_DISPLAY_TRANSFER_SHUTTLE_TIME) // Emergency shuttle timer
- if(!SSemergencyshuttle)
- message1 = "-ETA-"
- message2 = "Never" // You're here forever.
- return 1
- if(SSemergencyshuttle.waiting_to_leave())
- message1 = "-ETD-"
- if(SSemergencyshuttle.shuttle.is_launching())
- message2 = "Launch"
- else
- message2 = get_shuttle_timer_departure()
- if(length(message2) > CHARS_PER_LINE)
- message2 = "Error"
- update_display(message1, message2)
- else if(SSemergencyshuttle.has_eta())
- message1 = "-ETA-"
- message2 = get_shuttle_timer_arrival()
- if(length(message2) > CHARS_PER_LINE)
- message2 = "Error"
- update_display(message1, message2)
- return 1
- if(STATUS_DISPLAY_MESSAGE) // Custom messages
- var/line1
- var/line2
-
- if(!index1)
- line1 = message1
- else
- line1 = copytext(message1+"|"+message1, index1, index1+CHARS_PER_LINE)
- var/message1_len = length(message1)
- index1 += SCROLL_SPEED
- if(index1 > message1_len)
- index1 -= message1_len
-
- if(!index2)
- line2 = message2
- else
- line2 = copytext(message2+"|"+message2, index2, index2+CHARS_PER_LINE)
- var/message2_len = length(message2)
- index2 += SCROLL_SPEED
- if(index2 > message2_len)
- index2 -= message2_len
- update_display(line1, line2)
- return 1
- if(STATUS_DISPLAY_ALERT)
- display_alert(seclevel)
- return 1
- if(STATUS_DISPLAY_TIME)
- message1 = "TIME"
- message2 = stationtime2text()
- update_display(message1, message2)
- return 1
- return 0
-
-/obj/machinery/status_display/examine(mob/user, dist)
- . = ..(user)
- if(mode != STATUS_DISPLAY_BLANK && mode != STATUS_DISPLAY_ALERT)
- to_chat(user, "The display says:
\t[sanitize(message1)]
\t[sanitize(message2)]")
-
-/obj/machinery/status_display/proc/set_message(m1, m2)
- if(m1)
- index1 = (length(m1) > CHARS_PER_LINE)
- message1 = m1
- else
- message1 = ""
- index1 = 0
- if(m2)
- index2 = (length(m2) > CHARS_PER_LINE)
- message2 = m2
+/obj/machinery/status_display/examine(mob/user)
+ . = ..()
+ var/obj/effect/overlay/status_display_text/message1_overlay = get_status_text(message_key_1)
+ var/obj/effect/overlay/status_display_text/message2_overlay = get_status_text(message_key_2)
+ if (message1_overlay || message2_overlay)
+ . += "The display says:"
+ if (message1_overlay.message)
+ . += "\t[html_encode(message1_overlay.message)]"
+ if (message2_overlay.message)
+ . += "\t[html_encode(message2_overlay.message)]"
+
+// Helper procs.
+/obj/machinery/status_display/proc/display_escape_shuttle_status()
+ if(!SSemergencyshuttle.shuttle)
+ // the shuttle is missing - no processing
+ set_messages("shutl","not in service")
+ return PROCESS_KILL
+ else if(SSemergencyshuttle.waiting_to_leave() || SSemergencyshuttle.has_eta())
+ var/line1 = SSemergencyshuttle.has_eta() ? "-ETA-" : "-ETD-"
+ var/line2 = SSemergencyshuttle.has_eta() ? SSemergencyshuttle.estimate_arrival_time() : SSemergencyshuttle.estimate_launch_time()
+
+ if (line2 < 0)
+ line2 = "Now"
+ else
+ line2 = "[add_zero(num2text((line2 / 60) % 60),2)]:[add_zero(num2text(line2 % 60), 2)]"
+
+ set_messages(line1, line2)
else
- message2 = ""
- index2 = 0
+ // don't kill processing, the timer might turn back on
+ set_messages("", "")
/obj/machinery/status_display/proc/display_alert(newlevel)
- remove_display()
if(seclevel != newlevel)
seclevel = newlevel
+ set_picture("status_display_[seclevel]")
switch(seclevel)
if("green") set_light(l_range = 2, l_power = 0.25, l_color = "#00ff00")
if("yellow") set_light(l_range = 2, l_power = 0.25, l_color = "#ffff00")
@@ -180,38 +276,8 @@
if("blue") set_light(l_range = 2, l_power = 0.25, l_color = "#1024A9")
if("red") set_light(l_range = 4, l_power = 0.9, l_color = "#ff0000")
if("delta") set_light(l_range = 4, l_power = 0.9, l_color = "#FF6633")
- set_picture("status_display_[seclevel]")
-
-/obj/machinery/status_display/proc/set_picture(state)
- remove_display()
- if(!picture || picture_state != state)
- picture_state = state
- picture = image('icons/obj/status_display.dmi', icon_state=picture_state)
- add_overlay(picture)
-
-/obj/machinery/status_display/proc/update_display(line1, line2)
- line1 = uppertext(line1)
- line2 = uppertext(line2)
- var/new_text = {"