mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-09 07:46:20 +00:00
364 lines
15 KiB
Plaintext
364 lines
15 KiB
Plaintext
/// 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
|
|
/// Callback to finish_image_generation passed to SSrunechat
|
|
var/datum/callback/finish_callback
|
|
|
|
/**
|
|
* 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_REF(generate_image), text, target, owner, language, 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)
|
|
|
|
if (finish_callback)
|
|
SSrunechat.message_queue -= finish_callback
|
|
finish_callback = null
|
|
|
|
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
|
|
* * 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
|
|
RegisterSignal(owned_by, COMSIG_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 = owned_by.prefs.read_preference(/datum/preference/numeric/max_chat_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 = get_chat_color_string(target.name) // SKYRAT EDIT CHANGE - ORIGINAL: target.chat_color = colorize_string(target.name)
|
|
target.chat_color_darkened = get_chat_color_string(target.name, darkened = TRUE) // SKYRAT EDIT CHANGE - ORIGINAL: 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 |= SPAN_YELL
|
|
|
|
var/list/prefixes
|
|
var/chat_color_name_to_use
|
|
|
|
// Append radio icon if from a virtual speaker
|
|
if (extra_classes.Find("virtual-speaker"))
|
|
var/image/r_icon = image('icons/ui/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/chat/chat_icons.dmi', icon_state = "emote")
|
|
LAZYADD(prefixes, "\icon[r_icon]")
|
|
chat_color_name_to_use = target.get_visible_name(add_id_name = FALSE) // use face name for nonverbal messages
|
|
|
|
if(isnull(chat_color_name_to_use))
|
|
if(HAS_TRAIT(target, TRAIT_SIGN_LANG))
|
|
chat_color_name_to_use = target.get_visible_name(add_id_name = FALSE) // use face name for signers too
|
|
else
|
|
chat_color_name_to_use = target.get_voice() // for everything else, use the target's voice name
|
|
|
|
// 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
|
|
|
|
// 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 = "<span style='color: [tgt_color]'><span class='center [extra_classes.Join(" ")]'>[owner.apply_message_emphasis(text)]</span></span>"
|
|
|
|
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)
|
|
|
|
finish_callback = CALLBACK(src, PROC_REF(finish_image_generation), mheight, target, owner, complete_text, lifespan)
|
|
SSrunechat.message_queue += finish_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)
|
|
finish_callback = null
|
|
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 && 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.
|
|
var/continuing = 0
|
|
if (time_spent >= time_before_fade)
|
|
if(m.message.pixel_z < starting_height)
|
|
var/max_height = m.message.pixel_z + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
|
|
if(max_height > 0)
|
|
animate(m.message, pixel_z = m.message.pixel_z + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
else if(mheight + starting_height >= m.message.pixel_z)
|
|
animate(m.message, pixel_z = m.message.pixel_z + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
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)
|
|
if(time_spent < CHAT_MESSAGE_SPAWN_TIME)
|
|
// We haven't even had the time to fade in yet!
|
|
animate(m.message, alpha = 255, CHAT_MESSAGE_SPAWN_TIME - time_spent, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
// Stay faded in for a while, then
|
|
animate(m.message, alpha = 255, time = remaining_time, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
// Fade out
|
|
animate(m.message, alpha = 0, time = CHAT_MESSAGE_EOL_FADE, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
m.animate_lifespan = remaining_time + CHAT_MESSAGE_EOL_FADE
|
|
else
|
|
// Your time has come my son
|
|
animate(m.message, alpha = 0, time = CHAT_MESSAGE_EOL_FADE, flags=continuing)
|
|
continuing |= ANIMATION_CONTINUE
|
|
// 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_z < starting_height)
|
|
var/max_height = m.message.pixel_z + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
|
|
if(max_height > 0)
|
|
animate(m.message, pixel_z = m.message.pixel_z + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
else if(mheight + starting_height >= m.message.pixel_z)
|
|
animate(m.message, pixel_z = m.message.pixel_z + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = continuing | ANIMATION_PARALLEL)
|
|
continuing |= ANIMATION_CONTINUE
|
|
|
|
// 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++)
|
|
SET_PLANE_EXPLICIT(message, RUNECHAT_PLANE, message_loc)
|
|
message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
|
|
message.alpha = 0
|
|
message.pixel_z = starting_height
|
|
message.pixel_w = -target.base_pixel_w
|
|
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
|
|
if(owned_by)
|
|
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)
|
|
RegisterSignal(message_loc, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(loc_z_changed))
|
|
|
|
// 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
|
|
|
|
/datum/chatmessage/proc/loc_z_changed(datum/source, turf/old_turf, turf/new_turf, same_z_layer)
|
|
SIGNAL_HANDLER
|
|
SET_PLANE(message, RUNECHAT_PLANE, new_turf)
|
|
|
|
/**
|
|
* 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/language/message_language, raw_message, list/spans, runechat_flags = NONE)
|
|
if(SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(speaker, TRAIT_BYPASS_MEASURES))
|
|
return
|
|
if(HAS_TRAIT(speaker, TRAIT_RUNECHAT_HIDDEN))
|
|
return
|
|
// 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
|
|
|
|
// Display visual above source
|
|
if(runechat_flags & EMOTE_MESSAGE)
|
|
new /datum/chatmessage(raw_message, speaker, src, message_language, list("emote", "italics"))
|
|
else
|
|
new /datum/chatmessage(raw_message, speaker, src, message_language, 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
|