diff --git a/citadel.dme b/citadel.dme
index 8c82ff04d50..5fc26828f86 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -207,6 +207,7 @@
#include "code\__HELPERS\_global_objects.dm"
#include "code\__HELPERS\_lists_tg.dm"
#include "code\__HELPERS\_logging.dm"
+#include "code\__HELPERS\animations.dm"
#include "code\__HELPERS\areas.dm"
#include "code\__HELPERS\atom_movables.dm"
#include "code\__HELPERS\chat.dm"
@@ -2667,6 +2668,7 @@
#include "code\modules\mob\animations.dm"
#include "code\modules\mob\death.dm"
#include "code\modules\mob\emote.dm"
+#include "code\modules\mob\floating_message.dm"
#include "code\modules\mob\gender.dm"
#include "code\modules\mob\health.dm"
#include "code\modules\mob\hear_say.dm"
diff --git a/code/__HELPERS/animations.dm b/code/__HELPERS/animations.dm
new file mode 100644
index 00000000000..6a8cc78538d
--- /dev/null
+++ b/code/__HELPERS/animations.dm
@@ -0,0 +1,49 @@
+/proc/remove_images_from_clients(image/I, list/show_to)
+ for(var/client/C in show_to)
+ C.images -= I
+ qdel(I)
+
+/proc/fade_out(image/I, list/show_to)
+ animate(I, alpha = 0, time = 0.5 SECONDS, easing = EASE_IN)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_images_from_clients, I, show_to), 0.5 SECONDS)
+
+/proc/animate_speech_bubble(image/I, list/show_to, duration)
+ var/matrix/M = matrix()
+ M.Scale(0,0)
+ I.transform = M
+ I.alpha = 0
+ for(var/client/C in show_to)
+ C.images += I
+ animate(I, transform = 0, alpha = 255, time = 0.2 SECONDS, easing = EASE_IN)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/fade_out, I, show_to), (duration - 0.5 SECONDS))
+
+/proc/animate_receive_damage(atom/A)
+ var/pixel_x_diff = rand(-2,2)
+ var/pixel_y_diff = rand(-2,2)
+ animate(A, pixel_x = A.pixel_x + pixel_x_diff, pixel_y = A.pixel_y + pixel_y_diff, time = 2)
+ animate(pixel_x = initial(A.pixel_x), pixel_y = initial(A.pixel_y), time = 2)
+
+/proc/animate_throw(atom/A)
+ var/ipx = A.pixel_x
+ var/ipy = A.pixel_y
+ var/mpx = 0
+ var/mpy = 0
+
+ if(A.dir & NORTH)
+ mpy += 3
+ else if(A.dir & SOUTH)
+ mpy -= 3
+ if(A.dir & EAST)
+ mpx += 3
+ else if(A.dir & WEST)
+ mpx -= 3
+
+ var/x = mpx + ipx
+ var/y = mpy + ipy
+
+ animate(A, pixel_x = x, pixel_y = y, time = 0.6, easing = EASE_OUT)
+
+ var/matrix/M = matrix(A.transform)
+ animate(transform = turn(A.transform, (mpx - mpy) * 4), time = 0.6, easing = EASE_OUT)
+ animate(pixel_x = ipx, pixel_y = ipy, time = 0.6, easing = EASE_IN)
+ animate(transform = M, time = 0.6, easing = EASE_IN)
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 7832340f21b..3c1d61cfb13 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -634,3 +634,54 @@ datum/projectile_data
min(list_y),
max(list_x),
max(list_y))
+
+/proc/recursive_mob_check(var/atom/O, var/list/L = list(), var/recursion_limit = 3, var/client_check = 1, var/sight_check = 1, var/include_radio = 1)
+
+ //GLOB.debug_mob += O.contents.len
+ if(!recursion_limit)
+ return L
+ for(var/atom/A in O.contents)
+
+ if(ismob(A))
+ var/mob/M = A
+ if(client_check && !M.client)
+ L |= recursive_mob_check(A, L, recursion_limit - 1, client_check, sight_check, include_radio)
+ continue
+ if(sight_check && !isInSight(A, O))
+ continue
+ L |= M
+ //log_world("[recursion_limit] = [M] - [get_turf(M)] - ([M.x], [M.y], [M.z])")
+
+ else if(include_radio && istype(A, /obj/item/radio))
+ if(sight_check && !isInSight(A, O))
+ continue
+ L |= A
+
+ if(isobj(A) || ismob(A))
+ L |= recursive_mob_check(A, L, recursion_limit - 1, client_check, sight_check, include_radio)
+ return L
+
+/proc/get_mobs_in_view(var/R, var/atom/source, var/include_clientless = FALSE)
+ // Returns a list of mobs in range of R from source. Used in radio and say code.
+
+ var/turf/T = get_turf(source)
+ var/list/hear = list()
+
+ if(!T)
+ return hear
+
+ var/list/range = hear(R, T)
+
+ for(var/atom/A in range)
+ if(ismob(A))
+ var/mob/M = A
+ if(M.client || include_clientless)
+ hear += M
+ //log_world("Start = [M] - [get_turf(M)] - ([M.x], [M.y], [M.z])")
+ else if(istype(A, /obj/item/radio))
+ hear += A
+
+ if(isobj(A) || ismob(A))
+ hear |= recursive_mob_check(A, hear, 3, 1, 0, 1)
+
+ return hear
diff --git a/code/_macros.dm b/code/_macros.dm
index fa458fc3e33..e8c450742ab 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -24,3 +24,6 @@
#define random_id(key,min_id,max_id) uniqueness_repository.Generate(/datum/uniqueness_generator/id_random, key, min_id, max_id)
#define ARGS_DEBUG log_debug("[__FILE__] - [__LINE__]") ; for(var/arg in args) { log_debug("\t[log_info_line(arg)]") }
+
+#define JOINTEXT(X) jointext(X, null)
+//thank you Kevin for not running checks again, now I have to update one file with a comment - Papalus
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 96bb90f1b54..f5112de091d 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -769,7 +769,7 @@
for(var/mob in seeing_mobs)
var/mob/M = mob
if(self_message && (M == src))
- M.show_message( self_message, 1, blind_message, 2)
+ M.show_message(self_message, 1, blind_message, 2)
else if((M.see_invisible >= invisibility) && MOB_CAN_SEE_PLANE(M, plane))
M.show_message(message, 1, blind_message, 2)
else if(blind_message)
@@ -787,7 +787,7 @@
var/list/hearing_mobs = hear["mobs"]
var/list/hearing_objs = hear["objs"]
-
+ var/list/heard_to_floating_message
for(var/obj in hearing_objs)
var/obj/O = obj
O.show_message(message, 2, deaf_message, 1)
@@ -796,6 +796,9 @@
var/mob/M = mob
var/msg = message
M.show_message(msg, 2, deaf_message, 1)
+ M += heard_to_floating_message
+ INVOKE_ASYNC(src, /atom/movable/proc/animate_chat, (message ? message : deaf_message), null, FALSE, heard_to_floating_message, 30)
+
/atom/movable/proc/dropInto(var/atom/destination)
while(istype(destination))
@@ -836,7 +839,7 @@
if(!message)
return
var/list/speech_bubble_hearers = list()
- for(var/mob/M in get_hearers_in_view(7, src))
+ for(var/mob/M in get_hearers_in_view(MESSAGE_RANGE_COMBAT_LOUD, src))
M.show_message("[src] [atom_say_verb], \"[message]\"", 2, null, 1)
if(M.client)
speech_bubble_hearers += M.client
@@ -845,6 +848,18 @@
var/image/I = generate_speech_bubble(src, "[bubble_icon][say_test(message)]", FLY_LAYER)
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
INVOKE_ASYNC(GLOBAL_PROC, /.proc/flick_overlay, I, speech_bubble_hearers, 30)
+ INVOKE_ASYNC(src, /atom/movable/proc/animate_chat, message, null, FALSE, speech_bubble_hearers, 30)
+
+/atom/proc/say_overhead(var/message, whispering, message_range = 7, var/datum/language/speaking = null, var/list/passed_hearing_list)
+ var/list/speech_bubble_hearers = list()
+ var/italics
+ if(whispering)
+ italics = TRUE
+ for(var/mob/M in get_mobs_in_view(message_range, src))
+ if(M.client)
+ speech_bubble_hearers += M.client
+ if(length(speech_bubble_hearers))
+ INVOKE_ASYNC(src, /atom/movable/proc/animate_chat, message, speaking, italics, speech_bubble_hearers, 30)
/proc/generate_speech_bubble(var/bubble_loc, var/speech_state, var/set_layer = FLOAT_LAYER)
var/image/I = image('icons/mob/talk_vr.dmi', bubble_loc, speech_state, set_layer)
@@ -854,7 +869,6 @@
/atom/proc/speech_bubble(bubble_state = "", bubble_loc = src, list/bubble_recipients = list())
return
-
//! ## Atom Colour Priority System
/**
* A System that gives finer control over which atom colour to colour the atom with.
diff --git a/code/game/objects/items/devices/text_to_speech.dm b/code/game/objects/items/devices/text_to_speech.dm
index 179a52b26f2..e75f40116b8 100644
--- a/code/game/objects/items/devices/text_to_speech.dm
+++ b/code/game/objects/items/devices/text_to_speech.dm
@@ -26,6 +26,8 @@
if(message)
var/obj/item/text_to_speech/O = src
audible_message("[icon2html(thing = O, target = world)] \The [O.name] states, \"[message]\"")
+ user.say_overhead(message, FALSE, MESSAGE_RANGE_COMBAT_LOUD) // I don't like this, I wish I could just invoke what this calls directly!
+
/obj/item/text_to_speech/AltClick(mob/user) // QOL Change
attack_self(user)
diff --git a/code/modules/client/preference_setup/global/setting_datums.dm b/code/modules/client/preference_setup/global/setting_datums.dm
index 1bffd17da3d..241d127cc7b 100644
--- a/code/modules/client/preference_setup/global/setting_datums.dm
+++ b/code/modules/client/preference_setup/global/setting_datums.dm
@@ -286,6 +286,12 @@ var/list/_client_preferences_by_type
/datum/client_preference/parallax/toggled(mob/preference_mob, enabled)
. = ..()
preference_mob?.client?.parallax_holder?.Reset()
+/datum/client_preference/overhead_chat
+ description = "Overhead Chat"
+ key = "OVERHEAD_CHAT"
+ enabled_description = "Show"
+ disabled_description = "Hide"
+ enabled_by_default = TRUE
/********************
* Staff Preferences *
diff --git a/code/modules/client/preferences_toggle_procs.dm b/code/modules/client/preferences_toggle_procs.dm
index 10dbf9c5d66..838e6edbf84 100644
--- a/code/modules/client/preferences_toggle_procs.dm
+++ b/code/modules/client/preferences_toggle_procs.dm
@@ -292,6 +292,20 @@
to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/status_indicators)) ? "see" : "not see"] status indicators.")
feedback_add_details("admin_verb","TStatusIndicators")
+
+/client/verb/toggle_overhead_chat()
+ set name = "Toggle Overhead Chat"
+ set category = "Preferences"
+ set desc = "Enable/Disable seeing overhead chat messages."
+
+ var/pref_path = /datum/client_preference/overhead_chat
+ toggle_preference(pref_path)
+ SScharacter_setup.queue_preferences_save(prefs)
+
+ to_chat(src, "You will now [(is_preference_enabled(/datum/client_preference/overhead_chat)) ? "see" : "not see"] overhead chat messages..")
+
+ feedback_add_details("admin_verb","TOHChat")
+
//Toggles for Staff
//Developers
diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm
index 8ad3c99d306..059a8a71a60 100644
--- a/code/modules/mob/emote.dm
+++ b/code/modules/mob/emote.dm
@@ -23,6 +23,8 @@
if (message)
message = say_emphasis(message)
+ var/overhead_message = ("** [message] **")
+ say_overhead(overhead_message, FALSE, range)
SEND_SIGNAL(src, COMSIG_MOB_CUSTOM_EMOTE, src, message)
// Hearing gasp and such every five seconds is not good emotes were not global for a reason.
diff --git a/code/modules/mob/floating_message.dm b/code/modules/mob/floating_message.dm
new file mode 100644
index 00000000000..f92a2670e80
--- /dev/null
+++ b/code/modules/mob/floating_message.dm
@@ -0,0 +1,74 @@
+var/list/floating_chat_colors = list()
+
+/atom/movable
+ var/list/stored_chat_text
+
+/atom/movable/proc/animate_chat(message, var/datum/language/speaking = null, small, list/show_to, duration = 30)
+ set waitfor = FALSE
+ if(!speaking)
+ var/datum/language/noise/noise
+ speaking = noise
+ // 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")
+ message = replacetext(message, url_scheme, "")
+
+ var/static/regex/html_metachars = new(@"&[A-Za-z]{1,7};", "g")
+ message = replacetext(message, html_metachars, "")
+
+ var/style //additional style params for the message
+ var/fontsize = 4
+ if(small)
+ fontsize = 2
+ var/limit = 160
+ if(copytext_char(message, length_char(message) - 1) == "!!")
+ fontsize = 8
+ limit = 160
+ style += "font-weight: bold;"
+
+ if(length_char(message) > limit)
+ message = "[copytext_char(message, 1, limit)]..."
+
+ if(!floating_chat_colors[src])
+ floating_chat_colors[src] = get_random_colour(0,160,230)
+ style += "color: [floating_chat_colors[src]];"
+
+ // create 2 messages, one that appears if you know the language, and one that appears when you don't know the language
+ var/image/understood = generate_floating_text(src, capitalize(message), style, fontsize, duration, show_to)
+ var/image/gibberish = speaking ? generate_floating_text(src, speaking.scramble(message), style, fontsize, duration, show_to) : understood
+
+ for(var/client/C in show_to)
+ if(!C.mob.is_deaf() && C.is_preference_enabled(/datum/client_preference/overhead_chat))
+ if(C.mob.say_understands(null, speaking))
+ C.images += understood
+ else
+ C.images += gibberish
+
+#define MAPTEXT(text) {"[##text]"}
+
+/proc/generate_floating_text(atom/movable/holder, message, style, size, duration, show_to)
+ var/image/I = image(null, holder)
+ var/mob/living/X
+ if(isliving(holder))
+ X = holder
+ I.plane = PLANE_PLAYER_HUD
+ I.layer = PLANE_PLAYER_HUD_ITEMS
+ I.alpha = 15
+ I.maptext_width = 160
+ I.maptext_height = 64
+ I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
+ I.pixel_x = -round(I.maptext_width/2) + 16
+
+ I.maptext = MAPTEXT("[message]") // whoa calm down!!
+ animate(I, 1, alpha = 255, pixel_y = 24 * (X?.size_multiplier || 1))
+ for(var/image/old in holder.stored_chat_text)
+ animate(old, 2, pixel_y = old.pixel_y + 8)
+ LAZYADD(holder.stored_chat_text, I)
+
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_floating_text, holder, I), duration + 16)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_images_from_clients, I, show_to), duration + 18)
+
+ return I
+
+/proc/remove_floating_text(atom/movable/holder, image/I)
+ animate(I, 2, pixel_y = I.pixel_y + 10, alpha = 0)
+ LAZYREMOVE(holder.stored_chat_text, I)
diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm
index d18ce8a7b96..5bc0353b5df 100644
--- a/code/modules/mob/hear_say.dm
+++ b/code/modules/mob/hear_say.dm
@@ -71,6 +71,7 @@
if(check_mentioned(message) && is_preference_enabled(/datum/client_preference/check_mention))
message_to_send = "[message_to_send]"
+
on_hear_say(message_to_send)
if (speech_sound && (get_dist(speaker, src) <= world.view && src.z == speaker.z))
@@ -126,6 +127,15 @@
input = replacetext_char(input, strikethrough, "$1")
return input
+/mob/proc/say_emphasis_strip(input)
+ var/static/regex/italics = regex("\\|(?=\\S)(.*?)(?=\\S)\\|", "g")
+ input = replacetext_char(input, italics, "$1")
+ var/static/regex/bold = regex("\\+(?=\\S)(.*?)(?=\\S)\\+", "g")
+ input = replacetext_char(input, bold, "$1")
+ var/static/regex/underline = regex("_(?=\\S)(.*?)(?=\\S)_", "g")
+ input = replacetext_char(input, underline, "$1")
+ return input
+
/mob/proc/hear_radio(var/message, var/verb="says", var/datum/language/language=null, var/part_a, var/part_b, var/part_c, var/mob/speaker = null, var/hard_to_hear = 0, var/vname ="")
if(!client)
diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm
index 626c7aa4ac0..e8805194778 100644
--- a/code/modules/mob/living/say.dm
+++ b/code/modules/mob/living/say.dm
@@ -287,7 +287,7 @@ proc/get_radio_key_from_channel(var/channel)
var/msg
if(!speaking || !(speaking.flags & NO_TALK_MSG))
msg = "\The [src] talks into \the [used_radios[1]]"
- for(var/mob/living/M in hearers(5, src))
+ for(var/mob/living/M in hearers(7, src))
if((M != src) && msg)
M.show_message(msg)
if (speech_sound)
@@ -380,6 +380,10 @@ proc/get_radio_key_from_channel(var/channel)
listening[item] = z_speech_bubble
listening_obj |= results["objs"]
above = above.shadow
+ var/atom/emitter = src
+ if(!isobserver(emitter) || !IsAdminGhost(emitter))
+ emitter.say_overhead(say_emphasis_strip(message), whispering, message_range, speaking)
+
//Main 'say' and 'whisper' message delivery
for(var/mob/M in listening)
@@ -395,12 +399,14 @@ proc/get_radio_key_from_channel(var/channel)
SEND_IMAGE(M, I1)
M.hear_say(message, verb, speaking, alt_name, italics, src, speech_sound, sound_vol)
if(whispering) //Don't even bother with these unless whispering
+
if(dst > message_range && dst <= w_scramble_range) //Inside whisper scramble range
if(M.client)
var/image/I2 = listening[M] || speech_bubble
images_to_clients[I2] |= M.client
SEND_IMAGE(M, I2)
M.hear_say(stars(message), verb, speaking, alt_name, italics, src, speech_sound, sound_vol*0.2)
+
if(dst > w_scramble_range && dst <= world.view) //Inside whisper 'visible' range
M.show_message("[src.name] [w_not_heard].", 2)
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index e75fd28d01e..a087786e517 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -611,3 +611,17 @@ var/list/global/organ_rel_size = list(
/mob/proc/can_see_reagents()
return stat == DEAD || issilicon(src) //Dead guys and silicons can always see reagents
+
+//Ingnores the possibility of breaking tags.
+/proc/stars_no_html(text, pr, re_encode)
+ text = html_decode(text) //We don't want to screw up escaped characters
+ . = list()
+ for(var/i = 1, i <= length_char(text), i++)
+ var/char = copytext_char(text, i, i+1)
+ if(char == " " || prob(pr))
+ . += char
+ else
+ . += "*"
+ . = JOINTEXT(.)
+ if(re_encode)
+ . = html_encode(.)
diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm
index c5dac842346..c8231189244 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -1,6 +1,7 @@
/mob/proc/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/whispering = 0)
return
+
/mob/proc/whisper_wrapper()
var/message = input("","whisper (text)") as text|null
if(message)
diff --git a/interface/skin.dmf b/interface/skin.dmf
index ae5eb98282e..d6ee15f5fdf 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -2,21 +2,21 @@ macro "default"
menu "menu"
- elem
+ elem
name = "&File"
command = ""
saved-params = "is-checked"
- elem
+ elem
name = "&Quick screenshot\tF2"
command = ".screenshot auto"
category = "&File"
saved-params = "is-checked"
- elem
+ elem
name = "&Save screenshot as...\tShift+F2"
command = ".screenshot"
category = "&File"
saved-params = "is-checked"
- elem
+ elem
name = ""
command = ""
category = "&File"
@@ -26,12 +26,12 @@ menu "menu"
command = ".reconnect"
category = "&File"
saved-params = "is-checked"
- elem
+ elem
name = "&Quit\tAlt-F4"
command = ".quit"
category = "&File"
saved-params = "is-checked"
- elem
+ elem
name = "&Icons"
command = ""
saved-params = "is-checked"
@@ -78,7 +78,7 @@ menu "menu"
can-check = true
group = "size"
saved-params = "is-checked"
- elem
+ elem
name = ""
command = ""
category = "&Icons"
@@ -89,16 +89,16 @@ menu "menu"
category = "&Icons"
can-check = true
saved-params = "is-checked"
- elem
+ elem
name = "&Help"
command = ""
saved-params = "is-checked"
- elem
+ elem
name = "&Admin Help\tF1"
command = "adminhelp"
category = "&Help"
saved-params = "is-checked"
- elem
+ elem
name = "&Hotkeys"
command = "hotkeys-help"
category = "&Help"
diff --git a/interface/stylesheet.dm b/interface/stylesheet.dm
index aae68a3abac..63a8a7bfdb1 100644
--- a/interface/stylesheet.dm
+++ b/interface/stylesheet.dm
@@ -254,5 +254,6 @@ h1.alert, h2.alert {color: #000000;}
.debug_info {}
.debug_debug {color:#0000FF;}
.debug_trace {color:#888888;}
+.maptext { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; color: white; line-height: 1.1; text-align: center; }
"}