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/_macros.dm b/code/_macros.dm
index 776156746ef..f5dda34d90c 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -25,3 +25,5 @@
#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)
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 378ee3c2f10..39693fe7e19 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -751,19 +751,26 @@
/atom/proc/is_incorporeal()
return FALSE
-/atom/proc/atom_say(message)
+/atom/proc/atom_say(message, var/datum/language/speaking = null)
if(!message)
return
+ var/italics = 0
var/list/speech_bubble_hearers = list()
+ var/speech_bubble_test = say_test(message)
+ var/image/speech_bubble = image('icons/mob/talk_vr.dmi',src,"h[speech_bubble_test]")
+ speech_bubble.layer = layer
+ speech_bubble.plane = plane
for(var/mob/M in get_mobs_in_view(7, src))
M.show_message("[src] [atom_say_verb], \"[message]\"", 2, null, 1)
if(M.client)
speech_bubble_hearers += M.client
-
if(length(speech_bubble_hearers))
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(GLOBAL_PROC, .proc/animate_speech_bubble, speech_bubble, speech_bubble_hearers , 30)
+ INVOKE_ASYNC(src, /atom/movable/proc/animate_chat, message, speaking, italics, speech_bubble_hearers, 30)
+
/atom/proc/speech_bubble(bubble_state = "", bubble_loc = src, list/bubble_recipients = list())
return
diff --git a/code/modules/mob/floating_message.dm b/code/modules/mob/floating_message.dm
new file mode 100644
index 00000000000..fdb8bdb5f30
--- /dev/null
+++ b/code/modules/mob/floating_message.dm
@@ -0,0 +1,70 @@
+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)
+ set waitfor = FALSE
+
+ // 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 = 6
+ if(small)
+ fontsize = 4
+ var/limit = 50
+ if(copytext_char(message, length_char(message) - 1) == "!!")
+ fontsize = 8
+ limit = 80
+ style += "font-weight: bold;"
+
+ if(length_char(message) > limit)
+ message = "[copytext_char(message, 1, limit)]..."
+
+ if(!floating_chat_colors[name])
+ floating_chat_colors[name] = get_random_colour(0,160,230)
+ style += "color: [floating_chat_colors[name]];"
+
+ // 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())
+ if(C.mob.say_understands(null, speaking))
+ C.images += understood
+ else
+ C.images += gibberish
+
+/proc/generate_floating_text(atom/movable/holder, message, style, size, duration, show_to)
+ var/image/I = image(null, holder)
+ I.plane = PLANE_PLAYER_HUD
+ I.layer = PLANE_PLAYER_HUD_ITEMS
+ I.alpha = 0
+ I.maptext_width = 80
+ I.maptext_height = 64
+ I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
+ I.pixel_x = -round(I.maptext_width/2) + 16
+
+ //style = "font: 'Small Fonts'; -dm-text-outline: 1px black; font-size: [size]px; [style]"
+
+ I.maptext = "
[message]"
+ animate(I, 1, alpha = 255, pixel_y = 16)
+
+ 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)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_images_from_clients, I, show_to), duration + 4)
+
+ 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/living/say.dm b/code/modules/mob/living/say.dm
index e5815fcb6c1..2e6edf559a1 100644
--- a/code/modules/mob/living/say.dm
+++ b/code/modules/mob/living/say.dm
@@ -230,6 +230,7 @@ proc/get_radio_key_from_channel(var/channel)
//Handle language-specific verbs and adverb setup if necessary
if(!whispering) //Just doing normal 'say' (for now, may change below)
verb = say_quote(message, speaking)
+ usr.say_overhead(message, speaking)
else if(whispering && speaking.whisper_verb) //Language has defined whisper verb
verb = speaking.whisper_verb
w_not_heard = "[verb] something"
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 2ced3ed1d24..ee4e861dfd0 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -177,32 +177,7 @@ proc/getsensorlevel(A)
return zone
-/proc/stars(n, pr)
- if (pr == null)
- pr = 25
- if (pr < 0)
- return null
- else
- if (pr >= 100)
- return n
- var/te = n
- var/t = ""
- n = length(n)
- var/p = null
- p = 1
- var/intag = 0
- while(p <= n)
- var/char = copytext(te, p, p + 1)
- if (char == "<") //let's try to not break tags
- intag = !intag
- if (intag || char == " " || prob(pr))
- t = text("[][]", t, char)
- else
- t = text("[]*", t)
- if (char == ">")
- intag = !intag
- p++
- return t
+
proc/slur(phrase)
phrase = html_decode(phrase)
@@ -705,3 +680,41 @@ var/global/image/backplane
/mob/proc/can_see_reagents()
return stat == DEAD || issilicon(src) //Dead guys and silicons can always see reagents
+
+
+/proc/stars(n, pr = 25, re_encode = 1)
+ if (pr < 0)
+ return null
+ else if (pr >= 100)
+ return n
+
+ var/intag = 0
+ var/block = list()
+ . = list()
+ for(var/i = 1, i <= length_char(n), i++)
+ var/char = copytext_char(n, i, i+1)
+ if(!intag && (char == "<"))
+ intag = 1
+ . += stars_no_html(JOINTEXT(block), pr, re_encode) //stars added here
+ block = list()
+ block += char
+ if(intag && (char == ">"))
+ intag = 0
+ . += block //We don't mess up html tags with stars
+ block = list()
+ . += (intag ? block : stars_no_html(JOINTEXT(block), pr, re_encode))
+ . = JOINTEXT(.)
+
+//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 fa03fee1a34..ff258521b94 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -1,6 +1,18 @@
/mob/proc/say(var/message, var/datum/language/speaking = null, var/verb="says", var/alt_name="", var/whispering = 0)
return
+/mob/proc/say_overhead(var/message, var/datum/language/speaking = null)
+ var/list/speech_bubble_hearers = list()
+ var/italics = 0
+ for(var/mob/M in get_mobs_in_view(7, src))
+ if(M.client)
+ speech_bubble_hearers += M.client
+ if(length(speech_bubble_hearers))
+ 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, speaking, italics, speech_bubble_hearers, 30)
+
/mob/proc/whisper_wrapper()
var/message = input("","whisper (text)") as text|null
if(message)
diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm
index 02dde267ebc..e1e907983a0 100644
--- a/code/modules/mob/typing_indicator.dm
+++ b/code/modules/mob/typing_indicator.dm
@@ -1,12 +1,11 @@
-/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) //VOREStation Edit - talk_vr.dmi instead of talk.dmi for right-side icons
- I.appearance_flags |= (RESET_COLOR|PIXEL_SCALE) //VOREStation Edit
- if(istype(bubble_loc, /atom/movable))
- var/atom/movable/AM = bubble_loc
- var/x_scale = AM.get_icon_scale_x()
- if(abs(x_scale) < 2) // reset transform on bubbles, except for the Very Large
- I.pixel_z = (AM.icon_expected_height * (x_scale-1))
- I.appearance_flags |= RESET_TRANSFORM
+/proc/generate_speech_bubble(var/bubble_loc, var/speech_state, var/set_layer = FLOAT_LAYER, list/show_to)
+ var/image/I = image('icons/mob/talk_vr.dmi') //VOREStation Edit - talk_vr.dmi instead of talk.dmi for right-side icons
+
+ var/list/speech_bubble_hearers = list()
+ for(var/mob/M in get_mobs_in_view(7, src))
+ if(M.client)
+ speech_bubble_hearers += M.client
+ INVOKE_ASYNC(GLOBAL_PROC, .proc/animate_speech_bubble, I, speech_bubble_hearers , 30)
return I
diff --git a/vorestation.dme b/vorestation.dme
index c6992338743..e0c85ae8329 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -112,6 +112,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\atmospherics.dm"
#include "code\__HELPERS\atom_movables.dm"
@@ -2206,6 +2207,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\hear_say.dm"
#include "code\modules\mob\holder.dm"