diff --git a/code/__HELPERS/animations.dm b/code/__HELPERS/animations.dm new file mode 100644 index 00000000000..6633d80eaae --- /dev/null +++ b/code/__HELPERS/animations.dm @@ -0,0 +1,29 @@ +/** + * Causes the passed atom / image to appear floating, + * playing a simple animation where they move up and down by 2 pixels (looping) + * + * In most cases you should NOT call this manually, instead use [/datum/element/movetype_handler]! + * This is just so you can apply the animation to things which can be animated but are not movables (like images) + */ +#define DO_FLOATING_ANIM(target) \ + animate(target, pixel_y = 2, time = 1 SECONDS, loop = -1, flags = ANIMATION_RELATIVE); \ + animate(pixel_y = -2, time = 1 SECONDS, flags = ANIMATION_RELATIVE) + +/** + * Stops the passed atom / image from appearing floating + * (Living mobs also have a 'body_position_pixel_y_offset' variable that has to be taken into account here) + * + * In most cases you should NOT call this manually, instead use [/datum/element/movetype_handler]! + * This is just so you can apply the animation to things which can be animated but are not movables (like images) + */ +#define STOP_FLOATING_ANIM(target) \ + var/final_pixel_y = 0; \ + if(ismovable(target)) { \ + var/atom/movable/movable_target = target; \ + final_pixel_y = movable_target.base_pixel_y; \ + }; \ + if(isliving(target)) { \ + var/mob/living/living_target = target; \ + final_pixel_y += living_target.body_position_pixel_y_offset; \ + }; \ + animate(target, pixel_y = final_pixel_y, time = 1 SECONDS) diff --git a/code/datums/elements/movetype_handler.dm b/code/datums/elements/movetype_handler.dm index f1c6486d7bd..f7ab8f9be2d 100644 --- a/code/datums/elements/movetype_handler.dm +++ b/code/datums/elements/movetype_handler.dm @@ -1,7 +1,3 @@ -#define DO_FLOATING_ANIM(target) \ - animate(target, pixel_y = 2, time = 1 SECONDS, loop = -1, flags = ANIMATION_RELATIVE);\ - animate(pixel_y = -2, time = 1 SECONDS, flags = ANIMATION_RELATIVE) - /** * An element that enables and disables movetype bitflags whenever the relative traits are added or removed. * It also handles the +2/-2 pixel y anim loop typical of mobs possessing the FLYING or FLOATING movetypes. @@ -44,7 +40,7 @@ attached_atoms -= source paused_floating_anim_atoms -= source - stop_floating(source) + STOP_FLOATING_ANIM(source) return ..() /// Called when a movement type trait is added to the movable. Enables the relative bitflag. @@ -68,7 +64,7 @@ var/old_state = source.movement_type source.movement_type &= ~flag if((old_state & (FLOATING|FLYING)) && !(source.movement_type & (FLOATING|FLYING))) - stop_floating(source) + STOP_FLOATING_ANIM(source) var/turf/pitfall = source.loc //Things that don't fly fall in open space. if(istype(pitfall)) pitfall.zFall(source) @@ -77,7 +73,7 @@ /// Called when the TRAIT_NO_FLOATING_ANIM trait is added to the movable. Stops it from bobbing up and down. /datum/element/movetype_handler/proc/on_no_floating_anim_trait_gain(atom/movable/source, trait) SIGNAL_HANDLER - stop_floating(source) + STOP_FLOATING_ANIM(source) /// Called when the TRAIT_NO_FLOATING_ANIM trait is removed from the mob. Restarts the bobbing animation. /datum/element/movetype_handler/proc/on_no_floating_anim_trait_loss(atom/movable/source, trait) @@ -89,7 +85,7 @@ /datum/element/movetype_handler/proc/pause_floating_anim(atom/movable/source, timer) SIGNAL_HANDLER if(paused_floating_anim_atoms[source] < world.time + timer) - stop_floating(source) + STOP_FLOATING_ANIM(source) if(!length(paused_floating_anim_atoms)) START_PROCESSING(SSdcs, src) //1 second tickrate. paused_floating_anim_atoms[source] = world.time + timer @@ -103,13 +99,3 @@ paused_floating_anim_atoms -= paused if(!length(paused_floating_anim_atoms)) STOP_PROCESSING(SSdcs, src) - -/// Stops the above. Also not a comsig proc. -/datum/element/movetype_handler/proc/stop_floating(atom/movable/target) - var/final_pixel_y = target.base_pixel_y - if(isliving(target)) //Living mobs also have a 'body_position_pixel_y_offset' variable that has to be taken into account here. - var/mob/living/living_target = target - final_pixel_y += living_target.body_position_pixel_y_offset - animate(target, pixel_y = final_pixel_y, time = 1 SECONDS) - -#undef DO_FLOATING_ANIM diff --git a/code/modules/hallucination/body.dm b/code/modules/hallucination/body.dm index 25b0d1fe0c2..53c2dd26319 100644 --- a/code/modules/hallucination/body.dm +++ b/code/modules/hallucination/body.dm @@ -1,4 +1,4 @@ -/// Makes a random body appear and disappear quickly. +/// Makes a random body appear and disappear quickly in view of the hallucinator. /datum/hallucination/body abstract_hallucination_parent = /datum/hallucination/body /// The file to make the body image from. @@ -7,6 +7,8 @@ var/body_image_state /// The actual image we made and showed show. var/image/shown_body + /// Whether we apply the floating anim to the body + var/body_floats = FALSE /// The layer this body will be drawn on, in case we want to bypass lighting var/body_layer = TURF_LAYER /// if TRUE, spawns the body under the hallucinator instead of somewhere in view @@ -21,13 +23,19 @@ if(spawn_under_hallucinator) possible_points += get_turf(hallucinator) else - for(var/turf/open/floor/open_floor in view(hallucinator)) - possible_points += open_floor + for(var/turf/open/open_turf in view(hallucinator)) + if(open_turf.is_blocked_turf()) + continue + possible_points += open_turf if(!length(possible_points)) return FALSE - shown_body = make_body_image(pick(possible_points)) + var/turf/picked = pick(possible_points) + if(isspaceturf(picked) || !picked.has_gravity()) + body_floats = TRUE + + shown_body = make_body_image(picked) hallucinator.client?.images |= shown_body return queue_cleanup() @@ -43,15 +51,18 @@ /// Makes the image of the body to show at the location passed. /datum/hallucination/body/proc/make_body_image(turf/location) - return image(body_image_file, location, body_image_state, body_layer) + var/image/created_image = image(body_image_file, location, body_image_state, body_layer) + if(body_floats) + DO_FLOATING_ANIM(created_image) + return created_image /datum/hallucination/body/husk - random_hallucination_weight = 4 + random_hallucination_weight = 8 body_image_file = 'icons/mob/species/human/human.dmi' body_image_state = "husk" /datum/hallucination/body/husk/sideways - random_hallucination_weight = 2 + random_hallucination_weight = 4 /datum/hallucination/body/husk/sideways/make_body_image(turf/location) var/image/body = ..() @@ -60,19 +71,101 @@ body.transform = turn_matrix return body -/datum/hallucination/body/alien +/datum/hallucination/body/ghost + random_hallucination_weight = 2 + body_image_file = 'icons/mob/simple/mob.dmi' + body_image_state = "ghost" + body_floats = TRUE + +/datum/hallucination/body/hole random_hallucination_weight = 1 + body_image_file = 'icons/effects/effects.dmi' + body_image_state = "blank" + +/datum/hallucination/body/staticguy + random_hallucination_weight = 1 + body_image_file = 'icons/effects/effects.dmi' + body_image_state = "static" + /// Our QDEL_IN timer id, so we can cancel it + var/del_timerid + +/datum/hallucination/body/staticguy/Destroy() + if(!QDELETED(hallucinator)) + UnregisterSignal(hallucinator, COMSIG_MOVABLE_MOVED) + if(del_timerid) + deltimer(del_timerid) + del_timerid = null + return ..() + +/datum/hallucination/body/staticguy/queue_cleanup() + RegisterSignal(hallucinator, COMSIG_MOVABLE_MOVED, .proc/on_move) + del_timerid = QDEL_IN(src, rand(2 MINUTES, 3 MINUTES)) + return TRUE + +/// Signal proc for [COMSIG_MOVABLE_MOVED] - if we move out of view of the hallucination, it disappears, how spooky +/datum/hallucination/body/staticguy/proc/on_move(datum/source) + SIGNAL_HANDLER + + // Entering its turf will cause it to fade out then delete + if(shown_body.loc == hallucinator.loc) + animate(shown_body, alpha = 0, time = 0.5 SECONDS) + deltimer(del_timerid) + del_timerid = QDEL_IN(src, 0.6 SECONDS) + return + + // Staying in view will do nothing + if(shown_body.loc in view(hallucinator)) + return + + // Leaving view will delete it immediately + deltimer(del_timerid) + del_timerid = null + qdel(src) + +/datum/hallucination/body/weird + random_hallucination_weight = 0.1 // These are very uncommon + abstract_hallucination_parent = /datum/hallucination/body/weird + +/datum/hallucination/body/weird/alien body_image_file = 'icons/mob/nonhuman-player/alien.dmi' body_image_state = "alienother" + body_floats = TRUE -/datum/hallucination/body/freezer - random_hallucination_weight = 1 +/datum/hallucination/body/weird/mini_bubblegum + body_image_file = 'icons/mob/simple/mob.dmi' + body_image_state = "horror" + +/datum/hallucination/body/weird/chrono + body_image_file = 'icons/mob/simple/mob.dmi' + body_image_state = "chronostuck" + body_floats = TRUE + +/datum/hallucination/body/weird/god + body_image_file = 'icons/mob/simple/mob.dmi' + body_image_state = "god" + body_floats = TRUE + +/datum/hallucination/body/weird/sling + body_image_file = 'icons/mob/simple/mob.dmi' + body_image_state = "shadowling_ascended" + body_floats = TRUE + +/datum/hallucination/body/weird/faceless + body_image_file = 'icons/mob/simple/simple_human.dmi' + body_image_state = "faceless" + +/datum/hallucination/body/weird/bones + body_image_file = 'icons/mob/simple/simple_human.dmi' + body_image_state = "mrbones" + +/datum/hallucination/body/weird/freezer + random_hallucination_weight = 0.3 // Slightly more common since it's cool (heh) body_image_file = 'icons/effects/effects.dmi' body_image_state = "the_freezer" body_layer = ABOVE_ALL_MOB_LAYER spawn_under_hallucinator = TRUE -/datum/hallucination/body/freezer/make_body_image(turf/location) +/datum/hallucination/body/weird/freezer/make_body_image(turf/location) var/image/body = ..() body.pixel_x = pick(rand(-208,-48), rand(48, 208)) body.pixel_y = pick(rand(-208,-48), rand(48, 208)) @@ -80,20 +173,19 @@ SET_PLANE_EXPLICIT(body, ABOVE_HUD_PLANE, location) return body -/datum/hallucination/body/freezer/queue_cleanup() +/datum/hallucination/body/weird/freezer/queue_cleanup() QDEL_IN(src, 12 SECONDS) //The freezer stays on screen while you're frozen addtimer(CALLBACK(src, .proc/freeze_player), 1 SECONDS) // You barely have a moment to react before you're frozen addtimer(CALLBACK(src, .proc/freeze_intimidate), 11.8 SECONDS) hallucinator.cause_hallucination(/datum/hallucination/fake_sound/weird/radio_static, "freezer hallucination") return TRUE -/datum/hallucination/body/freezer/proc/freeze_player() +/datum/hallucination/body/weird/freezer/proc/freeze_player() if(QDELETED(src)) return hallucinator.cause_hallucination(/datum/hallucination/ice, "freezer hallucination", duration = 11 SECONDS, play_freeze_sound = FALSE) - -/datum/hallucination/body/freezer/proc/freeze_intimidate() +/datum/hallucination/body/weird/freezer/proc/freeze_intimidate() if(QDELETED(src)) return // Spook 'em before we delete diff --git a/code/modules/hallucination/delusions.dm b/code/modules/hallucination/delusions.dm index d09745fdb15..543025038c5 100644 --- a/code/modules/hallucination/delusions.dm +++ b/code/modules/hallucination/delusions.dm @@ -26,24 +26,30 @@ /datum/hallucination/delusion/New( mob/living/hallucinator, - duration = 30 SECONDS, - affects_us = TRUE, - affects_others = FALSE, - skip_nearby = TRUE, - play_wabbajack = FALSE, + duration, + affects_us, + affects_others, + skip_nearby, + play_wabbajack, ) - src.duration = duration - src.affects_us = affects_us - src.affects_others = affects_others - src.skip_nearby = skip_nearby - src.play_wabbajack = play_wabbajack + if(isnum(duration)) + src.duration = duration + if(!isnull(affects_us)) + src.affects_us = affects_us + if(!isnull(affects_others)) + src.affects_others = affects_others + if(!isnull(skip_nearby)) + src.skip_nearby = skip_nearby + if(!isnull(play_wabbajack)) + src.play_wabbajack = play_wabbajack + return ..() /datum/hallucination/delusion/Destroy() - if(!QDELETED(hallucinator)) - for(var/image/to_remove as anything in delusions) - hallucinator.client?.images -= to_remove + if(!QDELETED(hallucinator) && LAZYLEN(delusions)) + hallucinator.client?.images -= delusions + LAZYNULL(delusions) return ..() @@ -99,11 +105,11 @@ /datum/hallucination/delusion/custom/New( mob/living/hallucinator, - duration = 30 SECONDS, - affects_us = TRUE, - affects_others = FALSE, - skip_nearby = FALSE, - play_wabbajack = FALSE, + duration, + affects_us, + affects_others, + skip_nearby, + play_wabbajack, custom_icon_file, custom_icon_state, custom_name, @@ -167,11 +173,50 @@ delusion_name = "demon" /datum/hallucination/delusion/preset/cyborg - play_wabbajack = TRUE delusion_icon_file = 'icons/mob/silicon/robots.dmi' delusion_icon_state = "robot" delusion_name = "cyborg" + play_wabbajack = TRUE /datum/hallucination/delusion/preset/cyborg/make_delusion_image(mob/over_who) . = ..() hallucinator.playsound_local(get_turf(over_who), 'sound/voice/liveagain.ogg', 75, TRUE) + +/datum/hallucination/delusion/preset/ghost + delusion_icon_file = 'icons/mob/simple/mob.dmi' + delusion_icon_state = "ghost" + delusion_name = "ghost" + affects_others = TRUE + +/datum/hallucination/delusion/preset/ghost/make_delusion_image(mob/over_who) + var/image/funny_image = ..() + funny_image.name = over_who.name + DO_FLOATING_ANIM(funny_image) + return funny_image + +/datum/hallucination/delusion/preset/syndies + random_hallucination_weight = 1 + delusion_icon_file = 'icons/mob/simple/simple_human.dmi' + delusion_icon_state = "syndicate_space" + delusion_name = "Syndicate" + affects_others = TRUE + affects_us = FALSE + +/datum/hallucination/delusion/preset/syndies/make_delusion_image(mob/over_who) + var/static/list/syndicate_icon_states + + if(!syndicate_icon_states) + syndicate_icon_states = list() + for(var/state in icon_states(delusion_icon_file)) + if(!findtext(state, "syndicate")) + continue + + syndicate_icon_states += state + + if(length(syndicate_icon_states) > 0) + delusion_name = over_who.name + delusion_icon_state = pick(syndicate_icon_states) + else + stack_trace("Hey! The hallucination [type] couldn't find a single icon state to use, it'll be invisible. Correct this.") + + return ..() diff --git a/code/modules/hallucination/fake_death.dm b/code/modules/hallucination/fake_death.dm index c6c0bc1e3c7..31bd63014d6 100644 --- a/code/modules/hallucination/fake_death.dm +++ b/code/modules/hallucination/fake_death.dm @@ -1,3 +1,4 @@ +// This hallucinations makes us suddenly think we died, stopping us / changing our hud / sending a fake deadchat message. /datum/hallucination/death random_hallucination_weight = 1 @@ -80,3 +81,55 @@ if(!QDELETED(src)) qdel(src) + +// A subtype of death which plays a dusted animation. +/datum/hallucination/death/dust + /// List of all images we created to convey the effect to the hallucinator (so we can remove them after) + var/list/image/created_images + +/datum/hallucination/death/dust/Destroy() + if(!QDELETED(hallucinator) && LAZYLEN(created_images)) + hallucinator.client?.images -= created_images + LAZYNULL(created_images) + + return ..() + +/datum/hallucination/death/dust/start() + + if(!ishuman(hallucinator)) + return FALSE + + var/mob/living/carbon/human/hallucinating_human = hallucinator + var/dust_icon_state = hallucinating_human.dna?.species?.dust_anim + if(!dust_icon_state) + return FALSE + + . = ..() + if(!.) + return + + created_images = list() + var/turf/below_hallucinating = get_turf(hallucinator) + + // Apply a blank / empty image to make them look invisible to themselves + var/image/make_them_invisible = image(loc = hallucinator) + make_them_invisible.override = TRUE + created_images += make_them_invisible + + // Grab the typepath of the dust animation visual so we can steal its icon (for consistency and futureproofing) + var/obj/effect/temp_visual/dust_animation/dust_source = /obj/effect/temp_visual/dust_animation + var/image/fake_dust_animation = image(initial(dust_source.icon), below_hallucinating, dust_icon_state, layer = ABOVE_MOB_LAYER) + created_images += fake_dust_animation + + // Grab the typepath of remains so we can steal its icon and state (futureproofing) + var/obj/effect/decal/remains/human/remains_source = /obj/effect/decal/remains/human + var/image/fake_remains_image = image(initial(remains_source.icon), below_hallucinating, initial(remains_source.icon_state)) + created_images += fake_remains_image + + // Grab the typepath of an observer so we can steal its icon and state (futureproofing) + var/mob/dead/observer/observer_source = /mob/dead/observer + var/image/fake_ghost = image(initial(observer_source.icon), below_hallucinating, initial(observer_source.icon_state)) + DO_FLOATING_ANIM(fake_ghost) + created_images += fake_ghost + + hallucinator.client?.images |= created_images diff --git a/tgstation.dme b/tgstation.dme index 59a1ed88f04..6134f3785a2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -296,6 +296,7 @@ #include "code\__HELPERS\_string_lists.dm" #include "code\__HELPERS\admin.dm" #include "code\__HELPERS\ai.dm" +#include "code\__HELPERS\animations.dm" #include "code\__HELPERS\areas.dm" #include "code\__HELPERS\atmospherics.dm" #include "code\__HELPERS\atoms.dm"