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"