Ports CM's langchat to replace floating chat. (#20818)

Works generally better and will allow for better customization and emote
display.

---------

Co-authored-by: realmattatlas <liermattia@gmail.com>
This commit is contained in:
Matt Atlas
2025-07-07 23:55:26 +00:00
committed by GitHub
co-authored by realmattatlas
parent 2700e9b9ca
commit 1738301ea7
26 changed files with 533 additions and 245 deletions
@@ -0,0 +1,5 @@
/// Called when something is added to a client's screen : /client/proc/add_to_screen(screen_add)
#define COMSIG_CLIENT_SCREEN_ADD "client_screen_add"
/// Called when something is removed from a client's screen : /client/proc/remove_from_screen(screen_remove)
#define COMSIG_CLIENT_SCREEN_REMOVE "client_screen_remove"
+3
View File
@@ -1,3 +1,6 @@
// Font metrics bitfield
/// Include leading A width and trailing C width in GetWidth() or in DrawText()
#define INCLUDE_AC (1<<0)
/// Maptext styles
#define MAP_STYLESHEET ".maptext { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; color: white; line-height: 1.1; } .center { text-align: center; } .langchat { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; } .langchat_small { font-size: 6px; } .langchat_yell { font-weight: bold; font-size: 10px; } .langchat_bolded { font-weight: bold; font-size: 8px; } .langchat_announce { font-weight: bold; font-size: 12px; } .langchat_bolditalicbig {font-weight: bold; font-size: 24px; font-style: italic; } .langchat_italic {font-style: italic; }"
+2
View File
@@ -185,6 +185,8 @@
/// The layer you should use when you -really- don't want an emissive overlay to be blocked.
#define EMISSIVE_LAYER_UNBLOCKABLE 9999
#define RUNECHAT_PLANE 500
//-------------------- Rendering ---------------------
/// Semantics - The final compositor or a filter effect renderer
+1
View File
@@ -81,6 +81,7 @@
///Delete and null the reference, if the object isn't null
#define QDEL_NULL(item) if(item) { qdel(item) ; item = null }
#define QDEL_NULL_LIST(x) if(x) { for(var/y in x) { qdel(y) }}; if(x) {x.Cut(); x = null } // Second x check to handle items that LAZYREMOVE on qdel.
#define QDEL_LIST(L) if(L) { for(var/I in L) qdel(I); L.Cut(); }
#define QDEL_LIST_IN(L, time) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(______qdel_list_wrapper), L), time, TIMER_STOPPABLE)
+2
View File
@@ -33,6 +33,8 @@
#define SPAN_HIGHDANGER(str) (FONT_LARGE(SPAN_DANGER(str)))
#define SPAN_LANGCHAT(X) "<span class='langchat'>[X]</span>"
/*
#####################
Font sizes
@@ -34,6 +34,8 @@ PROCESSING_SUBSYSTEM_DEF(tgui)
basehtml = replacetextEx(basehtml, "<!-- tgui:inline-polyfill -->", polyfill)
basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "NanoTrasen © 2457-[text2num(time2text(world.realtime, "YYYY")) + 442]")
basehtml = replacetext(basehtml, "tgui:stylesheet", MAP_STYLESHEET)
/datum/controller/subsystem/processing/tgui/OnConfigLoad()
var/storage_iframe = GLOB.config.storage_cdn_iframe
if(storage_iframe && storage_iframe != /datum/configuration::storage_cdn_iframe)
+189
View File
@@ -0,0 +1,189 @@
/atom/var/langchat_height = 32 // abovetile usually
/atom/var/langchat_color = "#FFFFFF"
/atom/var/langchat_styles = ""
#define LANGCHAT_LONGEST_TEXT 64
#define LANGCHAT_WIDTH 96
#define LANGCHAT_MAX_ALPHA 196
//pop defines
#define LANGCHAT_DEFAULT_POP 0 //normal message
#define LANGCHAT_PANIC_POP 1 //this causes shaking
#define LANGCHAT_FAST_POP 2 //this just makes it go away faster
// params for default pop
#define LANGCHAT_MESSAGE_POP_TIME 3
#define LANGCHAT_MESSAGE_POP_Y_SINK 8
// params for panic pop
#define LANGCHAT_MESSAGE_PANIC_POP_TIME 1
#define LANGCHAT_MESSAGE_PANIC_POP_Y_SINK 8
#define LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE 6
#define LANGCHAT_MESSAGE_PANIC_SHAKE_TIMES 6
#define LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN 1
// params for fast pop
#define LANGCHAT_MESSAGE_FAST_POP_TIME 1
#define LANGCHAT_MESSAGE_FAST_POP_Y_SINK 8
#define langchat_client_enabled(M) (M && M.client && M.client.prefs && (M.client.prefs.toggles_secondary & FLOATING_MESSAGES))
/atom/var/image/langchat_image
/atom/var/list/mob/langchat_listeners
///Hides the image, if one exists. Do not null the langchat image; it is rotated when the mob is buckled or proned to maintain text orientation.
/atom/proc/langchat_drop_image()
if(langchat_listeners)
for(var/mob/M in langchat_listeners)
if(M.client)
M.client.images -= langchat_image
langchat_listeners = null
/atom/proc/get_maxptext_x_offset(image/maptext_image)
return (world.icon_size / 2) - (maptext_image.maptext_width / 2)
/atom/movable/get_maxptext_x_offset(image/maptext_image)
return (bound_width / 2) - (maptext_image.maptext_width / 2)
/mob/get_maxptext_x_offset(image/maptext_image)
return (icon_size / 2) - (maptext_image.maptext_width / 2)
///Creates the image if one does not exist, resets settings that are modified by speech procs.
/atom/proc/langchat_make_image(override_color = null)
if(!langchat_image)
langchat_image = image(null, src)
langchat_image.layer = 20
langchat_image.plane = EFFECTS_ABOVE_LIGHTING_PLANE
langchat_image.appearance_flags = NO_CLIENT_COLOR|KEEP_APART|RESET_COLOR|RESET_TRANSFORM
langchat_image.maptext_y = langchat_height
langchat_image.maptext_height = 64
langchat_image.maptext_y -= LANGCHAT_MESSAGE_POP_Y_SINK
langchat_image.maptext_x = get_maxptext_x_offset(langchat_image)
langchat_image.filters = filter(type="drop_shadow", size = 1, color = COLOR_BLACK)
langchat_image.pixel_y = 0
langchat_image.alpha = 0
langchat_image.color = override_color ? override_color : langchat_color
if(appearance_flags & PIXEL_SCALE)
langchat_image.appearance_flags |= PIXEL_SCALE
/mob/langchat_make_image(override_color = null)
var/new_image = FALSE
if(!langchat_image)
new_image = TRUE
. = ..()
// Recenter for icons more than 32 wide
if(new_image)
langchat_image.maptext_x += (icon_size - 32) / 2
/mob/abstract/ghost/langchat_make_image(override_color = null)
if(!override_color)
override_color = "#c51fb7"
. = ..()
langchat_image.appearance_flags |= RESET_ALPHA
/atom/proc/langchat_speech(message, list/listeners, language, override_color, skip_language_check = FALSE, animation_style = LANGCHAT_DEFAULT_POP, list/additional_styles = list("langchat"))
langchat_drop_image()
langchat_make_image(override_color)
var/image/r_icon
var/use_mob_style = TRUE
var/text_to_display = message
if(length(text_to_display) > LANGCHAT_LONGEST_TEXT)
text_to_display = copytext_char(text_to_display, 1, LANGCHAT_LONGEST_TEXT + 1) + "..."
var/timer = (length(text_to_display) / LANGCHAT_LONGEST_TEXT) * 4 SECONDS + 2 SECONDS
if(additional_styles.Find("emote"))
additional_styles.Remove("emote")
use_mob_style = FALSE
r_icon = image('icons/mob/chat_icons.dmi', icon_state = "emote")
else if(additional_styles.Find("virtual-speaker"))
additional_styles.Remove("virtual-speaker")
r_icon = image('icons/mob/chat_icons.dmi', icon_state = "radio")
if(r_icon)
text_to_display = "\icon[r_icon]&zwsp;[text_to_display]"
text_to_display = "<span class='center [additional_styles != null ? additional_styles.Join(" ") : ""] [use_mob_style ? langchat_styles : ""] langchat'>[text_to_display]</span>"
langchat_image.maptext = text_to_display
langchat_image.maptext_width = LANGCHAT_WIDTH
langchat_image.maptext_x = get_maxptext_x_offset(langchat_image)
langchat_listeners = listeners
for(var/mob/M in langchat_listeners)
if(langchat_client_enabled(M) && !M.ear_deaf && (skip_language_check || M.say_understands(src, language)))
M.client.images += langchat_image
if(isturf(loc))
langchat_image.loc = src
else
langchat_image.loc = recursive_holder_check(src)
switch(animation_style)
if(LANGCHAT_DEFAULT_POP)
langchat_image.alpha = 0
animate(langchat_image, pixel_y = langchat_image.pixel_y + LANGCHAT_MESSAGE_POP_Y_SINK, alpha = LANGCHAT_MAX_ALPHA, time = LANGCHAT_MESSAGE_POP_TIME)
if(LANGCHAT_PANIC_POP)
langchat_image.alpha = LANGCHAT_MAX_ALPHA
animate(langchat_image, pixel_y = langchat_image.pixel_y + LANGCHAT_MESSAGE_PANIC_POP_Y_SINK, time = LANGCHAT_MESSAGE_PANIC_POP_TIME)
animate(pixel_x = langchat_image.pixel_x - LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING)
for(var/i = 1 to LANGCHAT_MESSAGE_PANIC_SHAKE_TIMES)
animate(pixel_x = langchat_image.pixel_x + 2*LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = 2*LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING)
animate(pixel_x = langchat_image.pixel_x - 2*LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING)
animate(pixel_x = langchat_image.pixel_x + LANGCHAT_MESSAGE_PANIC_SHAKE_SIZE, time = LANGCHAT_MESSAGE_PANIC_SHAKE_TIME_TAKEN, easing = CUBIC_EASING)
if(LANGCHAT_FAST_POP)
langchat_image.alpha = 0
animate(langchat_image, pixel_y = langchat_image.pixel_y + LANGCHAT_MESSAGE_FAST_POP_Y_SINK, alpha = LANGCHAT_MAX_ALPHA, time = LANGCHAT_MESSAGE_FAST_POP_TIME)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_drop_image), language), timer, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_NO_HASH_WAIT)
/atom/proc/langchat_long_speech(message, list/listeners, language)
langchat_drop_image()
langchat_make_image()
var/text_left = null
var/text_to_display = message
if(length(message) > LANGCHAT_LONGEST_TEXT)
text_to_display = copytext_char(message, 1, LANGCHAT_LONGEST_TEXT - 5) + "..."
text_left = "..." + copytext_char(message, LANGCHAT_LONGEST_TEXT - 5)
var/timer = 6 SECONDS
if(text_left)
timer = 4 SECONDS
text_to_display = "<span class='center [langchat_styles] langchat_announce langchat'>[text_to_display]</span>"
langchat_image.maptext = text_to_display
langchat_image.maptext_width = LANGCHAT_WIDTH * 2
langchat_image.maptext_x = get_maxptext_x_offset(langchat_image)
langchat_listeners = listeners
for(var/mob/M in langchat_listeners)
if(langchat_client_enabled(M) && !M.ear_deaf && M.say_understands(src, language))
M.client.images += langchat_image
if(isturf(loc))
langchat_image.loc = src
else
langchat_image.loc = recursive_holder_check(src)
animate(langchat_image, pixel_y = langchat_image.pixel_y + LANGCHAT_MESSAGE_POP_Y_SINK, alpha = LANGCHAT_MAX_ALPHA, time = LANGCHAT_MESSAGE_POP_TIME)
if(text_left)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_long_speech), text_left, listeners, language), timer, TIMER_OVERRIDE|TIMER_UNIQUE|TIMER_NO_HASH_WAIT)
else
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, langchat_drop_image), language), timer, TIMER_OVERRIDE|TIMER_UNIQUE|TIMER_NO_HASH_WAIT)
/** Displays image to a single listener after it was built above eg. for chaining different game logic than speech code
This does just that, doesn't check deafness or language! Do what you will in that regard **/
/atom/proc/langchat_display_image(mob/M)
if(langchat_image)
if(!langchat_client_enabled(M))
return
if(!langchat_listeners) // shouldn't happen
langchat_listeners = list()
langchat_listeners |= M
M.client.images += langchat_image
/// Will attempt to find what's holding this item if it's being contained by something, ie if it's in a satchel held by a human, this'll return the human
/proc/recursive_holder_check(obj/item/held_item, recursion_limit = 3)
if(recursion_limit <= 0)
return held_item
if(!held_item.loc || isturf(held_item.loc))
return held_item
recursion_limit--
return recursive_holder_check(held_item.loc, recursion_limit)
+10
View File
@@ -123,3 +123,13 @@ the HUD updates properly! */
state = "hudunknown"
return state
/// Wrapper for adding anything to a client's screen
/client/proc/add_to_screen(screen_add)
screen += screen_add
SEND_SIGNAL(src, COMSIG_CLIENT_SCREEN_ADD, screen_add)
/// Wrapper for removing anything from a client's screen
/client/proc/remove_from_screen(screen_remove)
screen -= screen_remove
SEND_SIGNAL(src, COMSIG_CLIENT_SCREEN_REMOVE, screen_remove)
-6
View File
@@ -587,9 +587,6 @@
ASSOC_UNSETEMPTY(recursive_contents, channel)
UNSETEMPTY(location.important_recursive_contents)
if(LAZYLEN(gone.stored_chat_text))
return_floating_text(gone)
GLOB.dir_set_event.unregister(src, gone, TYPE_PROC_REF(/atom, recursive_dir_set))
/atom/movable/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
@@ -608,9 +605,6 @@
SSspatial_grid.add_grid_awareness(location, channel)
recursive_contents[channel] |= arrived.important_recursive_contents[channel]
if (LAZYLEN(arrived.stored_chat_text))
give_floating_text(arrived)
if(GLOB.dir_set_event.has_listeners(arrived))
GLOB.dir_set_event.register(src, arrived, TYPE_PROC_REF(/atom, recursive_dir_set))
+4 -2
View File
@@ -909,9 +909,11 @@ GLOBAL_LIST_INIT(slot_flags_enumeration, list(
SSicon_cache.bloody_cache[type] = blood_overlay
/obj/item/proc/showoff(mob/user)
for (var/mob/M in view(user))
var/list/viewers = get_hearers_in_view(world.view, src)
user.langchat_speech("holds up [src].", viewers, GLOB.all_languages, skip_language_check = TRUE, animation_style = LANGCHAT_FAST_POP, additional_styles = list("langchat_small", "emote"))
for (var/mob/M in viewers)
if(!user.is_invisible_to(M))
M.show_message("<b>[user]</b> holds up [icon2html(src, viewers(get_turf(src)))] [src]. <a href='byond://?src=[REF(M)];lookitem=[REF(src)]>Take a closer look.</a>",1)
M.show_message("<b>[user]</b> holds up [icon2html(src, viewers)] [src]. <a href='byond://?src=[REF(M)];lookitem=[REF(src)]>Take a closer look.</a>",1)
/mob/living/carbon/verb/showoff()
set name = "Show Held Item"
+1 -1
View File
@@ -319,4 +319,4 @@
if(mob.client)
clients_in_hearers += mob.client
if(length(clients_in_hearers))
INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, animate_chat), message, null, FALSE, clients_in_hearers, overhead_time)
langchat_speech(message, hearers, GLOB.all_languages, skip_language_check = TRUE)
+4
View File
@@ -47,6 +47,8 @@ GLOBAL_LIST_INIT(admin_verbs_admin, list(
/client/proc/admin_cancel_shuttle, //allows us to cancel the emergency shuttle, sending it back to centcomm,
/client/proc/cmd_admin_direct_narrate, //send text directly to a player with no padding. Useful for narratives and fluff-text,
/client/proc/cmd_admin_local_narrate, //sends text to all mobs within 7 tiles of src.mob
/client/proc/cmd_admin_local_screen_text,
/client/proc/cmd_admin_global_screen_text,
/client/proc/cmd_admin_world_narrate, //sends text to all players with no padding,
/client/proc/cmd_admin_create_centcom_report,
/client/proc/check_ai_laws, //shows AI and borg laws,
@@ -305,6 +307,8 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
/client/proc/manage_silicon_laws,
/client/proc/cmd_admin_direct_narrate,
/client/proc/cmd_admin_local_narrate,
/client/proc/cmd_admin_local_screen_text,
/client/proc/cmd_admin_global_screen_text,
/client/proc/cmd_admin_world_narrate,
/client/proc/cmd_admin_grab_observers,
/client/proc/cmd_admin_create_centcom_report,
+59
View File
@@ -119,6 +119,65 @@
message_admins(SPAN_NOTICE("\bold LocalNarrate: [key_name_admin(usr)] : [msg]<BR>"), 1)
feedback_add_details("admin_verb", "LCLN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_local_screen_text()
set category = "Special Verbs"
set name = "Local Screen Text"
if(!check_rights(R_ADMIN, TRUE))
return
var/list/mob/message_mobs = list()
var/choice = tgui_alert(usr, "Local Screen Text will send a screen text message to mobs. Do you want the mobs messaged to be only ones that you can see, or ignore blocked vision and message everyone within seven tiles of you?", "Narrate Selection", list("View", "Range", "Cancel"))
if(choice != "Cancel")
if(choice == "View")
message_mobs = mobs_in_view(view, src.mob)
else
for(var/mob/M in range(view, src.mob))
message_mobs += M
else
return
var/msg = tgui_input_text(usr, "Insert the screen message you want to send.", "Local Screen Text")
if(!msg)
return
var/big_text = tgui_alert(src, "Do you want big or normal text?", "Local Screen Text", list("Big", "Normal"))
var/text_type = /atom/movable/screen/text/screen_text
if(big_text == "Big")
text_type = /atom/movable/screen/text/screen_text/command_order
for(var/mob/M in message_mobs)
if(M.client)
M.play_screen_text(msg,text_type, COLOR_RED)
log_admin("LocalScreenText: [key_name(usr)] : [msg]")
message_admins(SPAN_NOTICE("Local Screen Text: [key_name_admin(usr)] : [msg]"), 1)
feedback_add_details("admin_verb", "LSTX") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_global_screen_text()
set category = "Special Verbs"
set name = "Global Screen Text"
if(!check_rights(R_ADMIN, TRUE))
return
var/msg = tgui_input_text(usr, "Insert the screen message you want to send.", "Global Screen Text")
if(!msg)
return
var/big_text = tgui_alert(src, "Do you want big or normal text?", "Global Screen Text", list("Big", "Normal"))
var/text_type = /atom/movable/screen/text/screen_text
if(big_text == "Big")
text_type = /atom/movable/screen/text/screen_text/command_order
for(var/mob/M in GLOB.mob_list)
if(M.client)
M.play_screen_text(msg, text_type, COLOR_RED)
log_admin("GlobalScreenText: [key_name(usr)] : [msg]")
message_admins(SPAN_NOTICE("Global Screen Text: [key_name_admin(usr)] : [msg]"), 1)
feedback_add_details("admin_verb", "GSTX") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/cmd_admin_direct_narrate(var/mob/M) // Targetted narrate -- TLE
set category = "Special Verbs"
set name = "Direct Narrate"
+4
View File
@@ -100,3 +100,7 @@
/// The DPI scale of the client. 1 is equivalent to 100% window scaling, 2 will be 200% window scaling
var/window_scaling
//screen_text vars
///lazylist of screen_texts for this client, first in this list is the one playing
var/list/atom/movable/screen/text/screen_text/screen_texts
+2
View File
@@ -386,6 +386,8 @@ GLOBAL_LIST_INIT(localhost_addresses, list(
// Forcibly enable hardware-accelerated graphics, as we need them for the lighting overlays.
winset(src, null, "command=\".configure graphics-hwmode on\"")
winset(src, "map", "style=\"[MAP_STYLESHEET]\"")
if(IsGuestKey(key) && GLOB.config.external_auth)
src.authed = FALSE
var/mob/abstract/unauthed/m = new()
@@ -262,7 +262,7 @@
pref.floating_chat_color = new_fc_color
var/mob/living/carbon/human/H = preference_mob()
if(ishuman(H))
H.set_floating_chat_color(new_fc_color)
H.langchat_color = pref.floating_chat_color
return TOPIC_REFRESH
else if(href_list["speech_bubble_type"])
+1 -1
View File
@@ -412,7 +412,7 @@ GLOBAL_LIST_EMPTY_TYPED(preferences_datums, /datum/preferences)
character.set_species(species)
if(character.dna)
character.dna.real_name = character.real_name
character.set_floating_chat_color(floating_chat_color)
character.langchat_color = floating_chat_color
character.flavor_texts["general"] = flavor_texts["general"]
character.flavor_texts[BP_HEAD] = flavor_texts[BP_HEAD]
+6 -13
View File
@@ -149,7 +149,6 @@
return pretext + nametext + subtext
/mob/proc/custom_emote(var/m_type = VISIBLE_MESSAGE, var/message = null, var/do_show_observers = TRUE)
if((usr && stat) || (!use_me && usr == src))
to_chat(src, "You are unable to emote.")
return
@@ -157,27 +156,21 @@
if(!message)
return
var/animated_chat_message = message
message = format_emote(src, message)
if (message)
log_emote("[name]/[key] : [message]")
message = process_chat_markup(message, list("~", "_"))
var/langchat_message = message
message = format_emote(src, message)
var/list/emote_viewers = list()
if(m_type == VISIBLE_MESSAGE)
visible_message(message, show_observers = do_show_observers)
emote_viewers = viewers(world.view, src)
else
audible_message(message, ghost_hearing = do_show_observers)
emote_viewers = get_hearers_in_LOS(world.view, src)
var/list/hearers = get_hearers_in_view(7, src)
var/list/hear_clients = list()
for(var/mob/M in hearers)
if(M.client)
hear_clients |= M.client
animated_chat_message = SPAN_COLOR("#f3ef09", "*") + animated_chat_message
animate_chat(animated_chat_message, null, TRUE, hear_clients, 30)
langchat_speech(langchat_message, emote_viewers, GLOB.all_languages, skip_language_check = TRUE, additional_styles = list("emote", "langchat_small"))
// Specific mob type exceptions below.
/mob/living/silicon/ai/emote(var/act, var/type, var/message)
@@ -0,0 +1,118 @@
/*
* These are ported from TGMC and are hopefully more flexible than text blurbs
*/
/**
* proc for playing a screen_text on a mob.
* enqueues it if a screen text is running and plays i otherwise
* Arguments:
* * text: text we want to be displayed
* * alert_type: typepath for screen text type we want to play here
* * override_color: the color of the text to use
*/
/mob/proc/play_screen_text(text, alert_type = /atom/movable/screen/text/screen_text, override_color = "#FFFFFF")
var/atom/movable/screen/text/screen_text/text_box = new alert_type()
text_box.text_to_play = text
text_box.player = client
if(override_color)
text_box.color = override_color
LAZYADD(client.screen_texts, text_box)
if(LAZYLEN(client.screen_texts) == 1) //lets only play one at a time, for thematic effect and prevent overlap
INVOKE_ASYNC(text_box, TYPE_PROC_REF(/atom/movable/screen/text/screen_text, play_to_client))
/atom/movable/screen/text/screen_text
icon = null
icon_state = null
alpha = 255
maptext_height = 64
maptext_width = 480
maptext_x = 0
maptext_y = 0
screen_loc = "LEFT,TOP-3"
///Time taken to fade in as we start printing text
var/fade_in_time = 0
///Time before fade out after printing is finished
var/fade_out_delay = 2 SECONDS
///Time taken when fading out after fade_out_delay
var/fade_out_time = 0.5 SECONDS
///delay between playing each letter. in general use 1 for fluff and 0.5 for time sensitive messsages
var/play_delay = 0.5
///letters to update by per text to per play_delay
var/letters_per_update = 1
///opening styling for the message
var/style_open = "<span class='langchat' style=text-align:center valign='top'>"
///closing styling for the message
var/style_close = "</span>"
///var for the text we are going to play
var/text_to_play
///The client that this text is for
var/client/player
/atom/movable/screen/text/screen_text/command_order
maptext_height = 64
maptext_width = 480
maptext_x = 0
maptext_y = 0
screen_loc = "LEFT,TOP-3"
letters_per_update = 2
fade_out_delay = 4.5 SECONDS
style_open = "<span class='langchat' style=font-size:16pt;text-align:center valign='top'>"
style_close = "</span>"
///proc for actually playing this screen_text on a mob.
/atom/movable/screen/text/screen_text/proc/play_to_client()
player?.add_to_screen(src)
if(fade_in_time)
animate(src, alpha = 255)
var/list/lines_to_skip = list()
var/static/html_locate_regex = regex("<.*>")
var/tag_position = findtext(text_to_play, html_locate_regex)
var/reading_tag = TRUE
while(tag_position)
if(reading_tag)
if(text_to_play[tag_position] == ">")
reading_tag = FALSE
lines_to_skip += tag_position
tag_position++
else
tag_position = findtext(text_to_play, html_locate_regex, tag_position)
reading_tag = TRUE
for(var/letter = 2 to length(text_to_play) + letters_per_update step letters_per_update)
if(letter in lines_to_skip)
continue
maptext = "[style_open][copytext_char(text_to_play, 1, letter)][style_close]"
sleep(play_delay)
addtimer(CALLBACK(src, PROC_REF(after_play)), fade_out_delay)
///handles post-play effects like fade out after the fade out delay
/atom/movable/screen/text/screen_text/proc/after_play()
if(!fade_out_time)
end_play()
return
animate(src, alpha = 0, time = fade_out_time)
addtimer(CALLBACK(src, PROC_REF(end_play)), fade_out_time)
///ends the play then deletes this screen object and plays the next one in queue if it exists
/atom/movable/screen/text/screen_text/proc/end_play()
if(!player)
qdel(src)
return
player.remove_from_screen(src)
LAZYREMOVE(player.screen_texts, src)
qdel(src)
if(QDELETED(player))
QDEL_NULL_LIST(player.screen_texts)
return
if(LAZYLEN(player.screen_texts))
player.screen_texts[1].play_to_client() // Theres more?
@@ -142,6 +142,56 @@
var/datum/tgui_module/narrate_panel/NP = new /datum/tgui_module/narrate_panel(usr)
NP.ui_interact(usr)
/mob/abstract/ghost/storyteller/verb/local_screen_text()
set name = "Local Screen Text"
set category = "Storyteller"
var/list/mob/message_mobs = list()
var/choice = html_decode(sanitize(tgui_alert(src, "Local Screen Text will send a screen text message to mobs. Do you want the mobs messaged to be only ones that you can see, or ignore blocked vision and message everyone within seven tiles of you?", "Narrate Selection", list("View", "Range", "Cancel"))))
if(choice != "Cancel")
if(choice == "View")
message_mobs = mobs_in_view(world.view, src)
else
for(var/mob/M in range(world.view, src))
message_mobs += M
else
return
var/msg = tgui_input_text(src, "Insert the screen message you want to send.", "Local Screen Text")
if(!msg)
return
var/big_text = tgui_alert(src, "Do you want big or normal text?", "Local Screen Text", list("Big", "Normal"))
var/text_type = /atom/movable/screen/text/screen_text
if(big_text == "Big")
text_type = /atom/movable/screen/text/screen_text/command_order
for(var/mob/M in message_mobs)
if(M.client)
M.play_screen_text(msg, text_type, COLOR_PURPLE)
log_admin("LocalScreenText: [key_name(usr)] : [msg]")
message_admins(SPAN_NOTICE("Local Screen Text: [key_name_admin(usr)] : [msg]"), 1)
/mob/abstract/ghost/storyteller/verb/global_screen_text()
set name = "Global Screen Text"
set category = "Storyteller"
var/msg = html_decode(sanitize(tgui_input_text(src, "Insert the screen message you want to send.", "Global Screen Text")))
if(!msg)
return
var/big_text = tgui_alert(src, "Do you want big or normal text?", "Global Screen Text", list("Big", "Normal"))
var/text_type = /atom/movable/screen/text/screen_text
if(big_text == "Big")
text_type = /atom/movable/screen/text/screen_text/command_order
for(var/mob/M in GLOB.mob_list)
if(M.client)
M.play_screen_text(msg, text_type, COLOR_PURPLE)
log_admin("GlobalScreenText: [key_name(usr)] : [msg]")
message_admins(SPAN_NOTICE("Global Screen Text: [key_name_admin(usr)] : [msg]"), 1)
/mob/abstract/ghost/storyteller/verb/storyteller_direct_narrate(var/mob/M)
set name = "Direct Narrate"
set category = "Storyteller"
-219
View File
@@ -1,219 +0,0 @@
// Thanks to Burger from Burgerstation for the foundation for this
GLOBAL_LIST_INIT(floating_chat_colors, list())
///Compute an unique key that is used to associate an image to the client that received said image
#define STORED_CHAT_TEXT_HASH(client) "[client.ckey]"
/atom/movable
/**
* A lazy list with the following format:
*
* * K -> The hash function result of STORED_CHAT_TEXT_HASH, a string
* * V -> A `/list` of `/image`, the images are the runetext sent to be shown to the various clients (as per the hash function)
*/
VAR_PRIVATE/list/stored_chat_text
/atom/movable/proc/get_floating_chat_color()
return get_random_colour(0, 160, 230)
/atom/movable/proc/set_floating_chat_color(color)
GLOB.floating_chat_colors[name] = color
/atom/movable/proc/animate_chat(message, datum/language/language, small, list/show_to, duration, override_color)
SHOULD_NOT_SLEEP(TRUE)
var/style //additional style params for the message
var/fontsize = GENERATE_FLOATING_TEXT_MEDIUM
if(small)
fontsize = GENERATE_FLOATING_TEXT_SMALL
var/limit = 50
if(copytext(message, length(message) - 1) == "!!")
fontsize = GENERATE_FLOATING_TEXT_LARGE
limit = 30
style += "font-weight: bold;"
if(length(message) > limit)
message = "[copytext_char(message, 1, limit)]..."
if(istype(language, /datum/language/noise))
message = "<font color='#7F7F7F'>*</font> " + uncapitalize(message)
if(!GLOB.floating_chat_colors[name])
GLOB.floating_chat_colors[name] = get_floating_chat_color()
style += "color: [GLOB.floating_chat_colors[name]];"
send_chat_floating_text_to_clients(show_to, message, fontsize, style, duration, language)
/**
* Generates a floating message image and sent it to the clients
*
*
* * show_to - A `/list` of clients to show the image to
* * message - A string, the message to show
* * style - String, a CSS-DM that will be injected in the style of the maptext, MUST be semicolon (;) terminated
* * duration - Duration in deciseconds to show the message for
* * language - A `/datum/language`, to appropriately change the image based on the understanding of the mob that receives it
*/
/atom/movable/proc/send_chat_floating_text_to_clients(list/client/show_to, message, fontsize, style, duration, datum/language/language)
SHOULD_NOT_SLEEP(TRUE)
// create 2 messages, one that appears if you know the language, and one that appears when you don't know the language
for(var/client/C in show_to)
if(!(C.prefs.toggles_secondary & FLOATING_MESSAGES))
continue
var/message_to_put_in_image
//See if it needs an understood message, or a gibberish one, to be shown, and generate it
if(isnull(language) || C.mob.say_understands(null, language))
message_to_put_in_image = capitalize(message)
else if(!isnull(language))
message_to_put_in_image = language.scramble(message)
//Generate the image, sent it to the client
generate_floating_text(message_to_put_in_image, style, fontsize, duration, C)
/**
* Generates a floating text message and takes care of showing it to the client, and remove it
*
*
* * message - String, the text to put into the floating text
* * style - String, a CSS-DM that will be injected in the style of the maptext, MUST be semicolon (;) terminated
* * fontsize - One of the GENERATE_FLOATING_TEXT_* macros defined in `code\__DEFINES\text.dm`, determines the size
* * duration - Duration in deciseconds to show the message for
*
* This process can sleep as it uses the client to measure the size of text, it should only be called inside a non-blocking call chain
*
*/
/atom/movable/proc/generate_floating_text(message, style, fontsize = GENERATE_FLOATING_TEXT_MEDIUM, duration, client/show_to)
set waitfor = FALSE
PRIVATE_PROC(TRUE)
if(!istype(show_to))
crash_with("Wrong argument supplied, show_to is not a client!")
//No point if the source or the destination of the floating text are deleted
if(QDELETED(src) || QDELETED(show_to))
return FALSE
var/atom/movable/attached_holder = get_last_atom_before_turf(src)
var/image/I = image(null, attached_holder, layer = FLY_LAYER)
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART | PIXEL_SCALE
I.plane = HUD_PLANE
I.layer = UNDER_HUD_LAYER
I.appearance_flags = RESET_COLOR|RESET_ALPHA|RESET_TRANSFORM
I.alpha = 0
I.maptext_width = CHAT_MESSAGE_WIDTH
I.pixel_y = attached_holder.get_floating_chat_y_offset()
I.pixel_x = (-round(I.maptext_width/2) + 16) + attached_holder.get_floating_chat_x_offset()
//Select the various parameters for the maptext, to ensure pixel-perfect scaling
var/font_family
var/font_size
var/other
switch(fontsize)
if(GENERATE_FLOATING_TEXT_SMALL)
font_family = "Spess Font"
font_size = 6
other = "-dm-text-outline: 1px black sharp; line-height: 1.4;"
if(GENERATE_FLOATING_TEXT_MEDIUM)
font_family = "Grand9K Pixel"
font_size = 6
other = "-dm-text-outline: 1px black sharp;"
if(GENERATE_FLOATING_TEXT_LARGE)
font_family = "Grand9K Pixel"
font_size = 12
other = "-dm-text-outline: 1.2px black sharp;"
else
crash_with("Wrong size specified, use one of the defines!")
var/complete_text = "<span class='center'><span style='font-family: \"[font_family]\"; font-size: [font_size]pt; [other] [style] text-align: center;'>[message]</span></span>"
I.maptext = complete_text
//At this point we enter the wait, we patiently wait for the client to tell us how large
//the text will be on its screen, as we need that info for the image generation afterwards
var/mheight = 0
WXH_TO_HEIGHT(show_to.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight)
//Then we register a callback to complete the image generation, this is done to avoid
//the proc waking up at the end of a tick and cause overtime
SSrunechat.message_queue += CALLBACK(src, PROC_REF(finish_generate_floating_text), I, mheight, show_to, duration)
/**
* Runs after the calculation of the client for the runechat size measurement,
* takes care of the last things needed to be set on the image, to display it to the clients,
* and to register it for removal when it expires
*
* Should only, ever be called by the runechat subsystem as a consequence of the `generate_floating_text()` callback insertion
*/
/atom/movable/proc/finish_generate_floating_text(image/runetext_image, mheight, client/show_to, lifespan)
SHOULD_NOT_SLEEP(TRUE)
PRIVATE_PROC(TRUE)
//The client can go offline, which would cause a runtime below
//this prevents the runtime, checking for both null and being deleted
if(QDELETED(show_to))
return
runetext_image.maptext_height = mheight * 1.25
var/stored_chat_text_hash_cache = STORED_CHAT_TEXT_HASH(show_to)
//Older runetext messages are shifted up so they clear the visual for the new ones
for(var/image/old in LAZYACCESS(src.stored_chat_text, stored_chat_text_hash_cache))
animate(old, 2, pixel_y = old.pixel_y + min(runetext_image.maptext_height, 45) + 8)
//We register ourself as a stored chat text image
LAZYADDASSOCLIST(src.stored_chat_text, stored_chat_text_hash_cache, runetext_image)
//Send the image to the client, takes care to remove it afterwards
flick_overlay(runetext_image, list(show_to), (lifespan + 2))
//Animation to show the text "appearing up"
animate(runetext_image, 1, alpha = 255, pixel_y = runetext_image.pixel_y + 23)
//Register a timer for our removal from the list
addtimer(CALLBACK(src, PROC_REF(remove_floating_text), runetext_image, stored_chat_text_hash_cache), lifespan)
/// Gives floating text to src upon holder entering
/atom/movable/proc/give_floating_text(atom/movable/holder)
PRIVATE_PROC(TRUE)
SHOULD_NOT_SLEEP(TRUE)
if(!holder)
return
for(var/key in holder.stored_chat_text)
for(var/image/I in holder.stored_chat_text[key])
I.loc = src
/// Returns floating text to holder upon leaving src
/atom/movable/proc/return_floating_text(atom/movable/holder)
PRIVATE_PROC(TRUE)
SHOULD_NOT_SLEEP(TRUE)
if(!holder)
return
for(var/key in holder.stored_chat_text)
for(var/image/I in holder.stored_chat_text[key])
I.loc = holder
/atom/movable/proc/remove_floating_text(image/image_to_remove, stored_chat_text_hash)
PRIVATE_PROC(TRUE)
SHOULD_NOT_SLEEP(TRUE)
var/list/image/client_associated_images = LAZYACCESS(src.stored_chat_text, stored_chat_text_hash)
if(!(client_associated_images?.Find(image_to_remove)))
crash_with("Trying to remove a floating text image that is not there!")
animate(image_to_remove, 2, pixel_y = image_to_remove.pixel_y + 10, alpha = 0)
LAZYREMOVEASSOC(src.stored_chat_text, stored_chat_text_hash, image_to_remove)
#undef STORED_CHAT_TEXT_HASH
+5 -1
View File
@@ -352,7 +352,11 @@ var/list/channel_to_radio_key = new
speech_bubble.appearance_flags = RESET_COLOR|RESET_ALPHA
INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(animate_speechbubble), speech_bubble, hear_clients, 30)
animate_chat(message, speaking, italics, hear_clients, 30)
var/list/langchat_styles = list()
if(istype(speaking, /datum/language/noise))
langchat_styles = list("emote", "langchat_small")
langchat_speech(message, get_hearers_in_view(message_range, src), speaking, additional_styles = langchat_styles)
var/bypass_listen_obj = (speaking && (speaking.flags & PASSLISTENOBJ))
if(!bypass_listen_obj)
+2
View File
@@ -252,6 +252,8 @@
var/list/active_genes=list()
var/mob_size = MOB_MEDIUM
/// The icon size width of the mob. Used for langchat resizing.
var/icon_size = 32
var/list/progressbars