Files
Bubberstation/code/datums/elements/immerse.dm
MrMelbert c1ed62915b Adds UPSIDE_DOWN movetype for negative gravity / makes Atrocinator affected by less things (#79785)
## About The Pull Request

Fixes #79764

I was going to tackle this issue by slamming `TRAIT_NO_SLIP_ALL` on
Atrocinator users and calling it a day, but like, that didn't feel
proper.

So I thought hey, we could just give them the flying movetype, even
though they technically aren't flying it means they're unaffected by
things that flying would make you unaffected by.

Nope, this means the mob technically "negates gravity", so no falling
and no feetsteps.

Let's try floating - this give us feetsteps but no falling upwards. 

So instead of going back to square one, with `TRAIT_NO_SLIP_ALL`, I
decided to go for the more complex route of just adding a movetype.

Hence, move type `UPSIDE_DOWN`. This covers situations where a mob would
be "floating" above the ground, but still walking. ...Negative gravity.

This means overall the Atrociator acts more as you'd expect - you don't
slip on ice, you don't trigger bear traps or mouse traps, you can walk
over railings, unaffected by conveyor belts, etc.

## Why It's Good For The Game

Makes the Atrocinator a lot more consistent with how you'd expect for it
to work.

Admittedly it is a bit niche use of movetypes, but it can possibly be
expanded to more things in the future, who knows? I applied it to mobs
on meat spikes (even though they don't move), just for proof of concept.

## Changelog

🆑 Melbert
fix: Atrocinating mobs will now behave more as you'd expect. Meaning
they don't slip on wet patches, can't trigger bear traps / landmines /
mouse traps, ignore conveyors, and can walk over tables and railings.
fix: Floating mobs are unaffected by conveyor belts, acid (on the
ground), glass tables
fix: Floating mobs won't squish stuff like roaches anymore 
fix: Fixes bear traps triggering on floating / flying mobs 
/🆑

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
2023-11-20 21:19:13 +00:00

414 lines
19 KiB
Plaintext

/**
* A visual element that makes movables entering the attached turfs look immersed into that turf.
*
* Abandon all hope, ye who read forth, for this immerse works on mind-numbing workarounds,
*/
/datum/element/immerse
element_flags = ELEMENT_DETACH_ON_HOST_DESTROY | ELEMENT_BESPOKE
argument_hash_start_idx = 2
///An association list of turfs that have this element attached and their affected contents.
var/list/attached_turfs_and_movables = list()
/**
* A list of movables that shouldn't be affected by the element, either because it'd look bad
* or barely perceptible.
*/
var/static/list/movables_to_ignore
///A list of icons generated from a target and a mask, later used as appearances for the overlays.
var/static/list/generated_immerse_icons = list()
///A list of instances of /atom/movable/immerse_overlay then used as visual overlays for the immersed movables.
var/list/generated_visual_overlays = list()
///An association list of movables as key and overlays as assoc.
var/list/immersed_movables
var/icon
var/icon_state
var/mask_icon
var/color
var/alpha
/datum/element/immerse/Attach(turf/target, icon, icon_state, mask_icon, color = "#777777", alpha = 180)
. = ..()
if(!isturf(target) || !icon || !icon_state || !mask_icon)
return ELEMENT_INCOMPATIBLE
if(!movables_to_ignore)
movables_to_ignore = typecacheof(list(
/obj/effect,
/mob/dead,
/obj/projectile,
))
movables_to_ignore += GLOB.WALLITEMS_INTERIOR
movables_to_ignore += GLOB.WALLITEMS_EXTERIOR
src.icon = icon
src.icon_state = icon_state
src.color = color
src.alpha = alpha
src.mask_icon = mask_icon
/**
* Hello, you may be wondering why we're blending icons and not simply
* overlaying one mutable appearance with the blend multiply on another.
* Well, the latter option doesn't work as neatly when added
* to an atom with the KEEP_TOGETHER appearance flag, with the mask icon also
* showing on said atom, while we don't want it to.
*
* Also using KEEP_APART isn't an option, because unless it's drawn as one with
* its visual loation, the whole plane the atom belongs to will count as part of the
* mask of the final visual overlay since that's how the BLEND_INSET_OVERLAY blend mode works here.
* In layman terms, with KEEP_APART on, if a flying monkey gets nears an immersed
* human, the visual overlay will appear on the flying monkey even if it shouldn't.
*/
var/icon/immerse_icon = generated_immerse_icons["[icon]-[icon_state]-[mask_icon]"]
if(!immerse_icon)
immerse_icon = icon(icon, icon_state)
var/icon/sub_mask = icon('icons/effects/effects.dmi', mask_icon)
immerse_icon.Blend(sub_mask, ICON_MULTIPLY)
immerse_icon = fcopy_rsc(immerse_icon)
generated_immerse_icons["[icon]-[icon_state]-[mask_icon]"] = immerse_icon
RegisterSignal(target, SIGNAL_ADDTRAIT(TRAIT_IMMERSE_STOPPED), PROC_REF(stop_immersion))
RegisterSignal(target, SIGNAL_REMOVETRAIT(TRAIT_IMMERSE_STOPPED), PROC_REF(start_immersion))
if(!HAS_TRAIT(target, TRAIT_IMMERSE_STOPPED))
start_immersion(target)
/datum/element/immerse/Detach(turf/source)
UnregisterSignal(source, list(SIGNAL_ADDTRAIT(TRAIT_IMMERSE_STOPPED), SIGNAL_REMOVETRAIT(TRAIT_IMMERSE_STOPPED)))
if(!HAS_TRAIT(source, TRAIT_IMMERSE_STOPPED))
stop_immersion(source)
return ..()
///Makes the element start affecting the turf and its contents. Called on Attach() or when TRAIT_IMMERSE_STOPPED is removed.
/datum/element/immerse/proc/start_immersion(turf/source)
SIGNAL_HANDLER
RegisterSignals(source, list(COMSIG_ATOM_ABSTRACT_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON), PROC_REF(on_init_or_entered))
RegisterSignal(source, COMSIG_ATOM_ABSTRACT_EXITED, PROC_REF(on_atom_exited))
attached_turfs_and_movables += source
for(var/atom/movable/movable as anything in source)
if(!(movable.flags_1 & INITIALIZED_1))
continue
on_init_or_entered(source, movable)
///Stops the element from affecting on the turf and its contents. Called on Detach() or when TRAIT_IMMERSE_STOPPED is added.
/datum/element/immerse/proc/stop_immersion(turf/source)
SIGNAL_HANDLER
UnregisterSignal(source, list(COMSIG_ATOM_ABSTRACT_ENTERED, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, COMSIG_ATOM_ABSTRACT_EXITED))
for(var/atom/movable/movable as anything in attached_turfs_and_movables[source])
remove_from_element(source, movable)
attached_turfs_and_movables -= source
/**
* If the movable is within the right layers and planes, not in the list of movable types to ignore,
* or already affected by the element for that matter, Signals will be registered and,
* unless the movable (or whatever it's buckled to) is flying, it'll appear as if immersed in that water.
*/
/datum/element/immerse/proc/on_init_or_entered(turf/source, atom/movable/movable)
SIGNAL_HANDLER
if(HAS_TRAIT(movable, TRAIT_IMMERSED))
return
if(movable.layer >= ABOVE_ALL_MOB_LAYER || !ISINRANGE(movable.plane, MUTATE_PLANE(FLOOR_PLANE, source), MUTATE_PLANE(GAME_PLANE_UPPER_FOV_HIDDEN, source)))
return
if(is_type_in_typecache(movable, movables_to_ignore))
return
var/atom/movable/buckled
if(isliving(movable))
var/mob/living/living_mob = movable
RegisterSignal(living_mob, COMSIG_LIVING_SET_BUCKLED, PROC_REF(on_set_buckled))
buckled = living_mob.buckled
try_immerse(movable, buckled)
RegisterSignal(movable, COMSIG_QDELETING, PROC_REF(on_movable_qdel))
LAZYADD(attached_turfs_and_movables[source], movable)
ADD_TRAIT(movable, TRAIT_IMMERSED, ELEMENT_TRAIT(src))
/datum/element/immerse/proc/on_movable_qdel(atom/movable/source)
SIGNAL_HANDLER
remove_from_element(source.loc, source)
/**
* The main proc, which adds a visual overlay to the movable that has entered the turf to make it look immersed.
* It's kind of iffy but basically, we want the overlay to cover as much area as needed to
* avoid the movable's icon from spilling horizontally or below.
* Also, while these visual overlays are mainly cached movables, for certain movables, such as living mobs,
* we want them to have their own unique vis overlay with additional signals registered.
* This allows the vis overlay to look more or less unchanged while its owner is spinning or resting
* without otherwise affecting other movables with identical overlays.
*/
/datum/element/immerse/proc/add_immerse_overlay(atom/movable/movable)
var/list/icon_dimensions = get_icon_dimensions(movable.icon)
var/width = icon_dimensions["width"] || world.icon_size
var/height = icon_dimensions["height"] || world.icon_size
var/is_below_water = movable.layer < WATER_LEVEL_LAYER ? "underwater-" : ""
var/atom/movable/immerse_overlay/vis_overlay = generated_visual_overlays["[is_below_water][width]x[height]"]
if(!vis_overlay) //create the overlay if not already done.
vis_overlay = generate_vis_overlay(width, height, is_below_water)
ADD_KEEP_TOGETHER(movable, ELEMENT_TRAIT(src))
/**
* Let's give an unique immerse visual only to those movables that would
* benefit from this the most, for the sake of a smidge of lightweightness.
*/
if(HAS_TRAIT(movable, TRAIT_UNIQUE_IMMERSE))
var/atom/movable/immerse_overlay/original_vis_overlay = vis_overlay
vis_overlay = new(null)
vis_overlay.appearance = original_vis_overlay
vis_overlay.extra_width = original_vis_overlay.extra_width
vis_overlay.extra_height = original_vis_overlay.extra_height
vis_overlay.overlay_appearance = original_vis_overlay.overlay_appearance
SEND_SIGNAL(movable, COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY, vis_overlay)
RegisterSignal(movable, COMSIG_ATOM_SPIN_ANIMATION, PROC_REF(on_spin_animation))
RegisterSignal(movable, COMSIG_LIVING_POST_UPDATE_TRANSFORM, PROC_REF(on_update_transform))
movable.vis_contents |= vis_overlay
LAZYSET(immersed_movables, movable, vis_overlay)
///Initializes and caches a new visual overlay given parameters such as width, height and whether it should appear fully underwater.
/datum/element/immerse/proc/generate_vis_overlay(width, height, is_below_water)
var/atom/movable/immerse_overlay/vis_overlay = new(null, src)
/**
* vis contents spin around the center of the icon of their vis locs
* but since we want the appearance to stay where it should be,
* we have to counteract this one.
*/
var/extra_width = (width - world.icon_size) * 0.5
var/extra_height = (height - world.icon_size) * 0.5
var/mutable_appearance/overlay_appearance = new()
var/icon/immerse_icon = generated_immerse_icons["[icon]-[icon_state]-[mask_icon]"]
var/last_i = width/world.icon_size
for(var/i in -1 to last_i)
var/mutable_appearance/underwater = mutable_appearance(icon, icon_state)
underwater.pixel_x = world.icon_size * i - extra_width
underwater.pixel_y = -world.icon_size - extra_height
overlay_appearance.overlays += underwater
var/mutable_appearance/water_level = is_below_water ? underwater : mutable_appearance(immerse_icon)
water_level.pixel_x = world.icon_size * i - extra_width
water_level.pixel_y = -extra_height
overlay_appearance.overlays += water_level
vis_overlay.color = color
vis_overlay.alpha = alpha
vis_overlay.overlays = list(overlay_appearance)
vis_overlay.extra_width = extra_width
vis_overlay.extra_height = extra_height
vis_overlay.overlay_appearance = overlay_appearance
generated_visual_overlays["[is_below_water][width]x[height]"] = vis_overlay
return vis_overlay
///This proc removes the vis_overlay, the keep together trait and some signals from the movable.
/datum/element/immerse/proc/remove_immerse_overlay(atom/movable/movable)
var/atom/movable/immerse_overlay/vis_overlay = LAZYACCESS(immersed_movables, movable)
if(!vis_overlay)
return
movable.vis_contents -= vis_overlay
LAZYREMOVE(immersed_movables, movable)
if(HAS_TRAIT(movable, TRAIT_UNIQUE_IMMERSE))
UnregisterSignal(movable, list(COMSIG_ATOM_SPIN_ANIMATION, COMSIG_LIVING_POST_UPDATE_TRANSFORM))
qdel(vis_overlay)
REMOVE_KEEP_TOGETHER(movable, ELEMENT_TRAIT(src))
/**
* Called by init_or_entered() and on_set_buckled().
* This applies the overlay if neither the movable or whatever is buckled to (exclusive to living mobs) are flying
* as well as movetype signals when the movable isn't buckled.
*/
/datum/element/immerse/proc/try_immerse(atom/movable/movable, atom/movable/buckled)
var/atom/movable/to_check = buckled || movable
if(!(to_check.movement_type & MOVETYPES_NOT_TOUCHING_GROUND) && !movable.throwing)
add_immerse_overlay(movable)
if(!buckled)
RegisterSignal(movable, COMSIG_MOVETYPE_FLAG_ENABLED, PROC_REF(on_move_flag_enabled))
RegisterSignal(movable, COMSIG_MOVETYPE_FLAG_DISABLED, PROC_REF(on_move_flag_disabled))
RegisterSignal(movable, COMSIG_MOVABLE_POST_THROW, PROC_REF(on_throw))
RegisterSignal(movable, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(on_throw_landed))
/**
* Called by on_set_buckled() and remove_from_element().
* This removes the filter and signals from the movable unless it doesn't have them.
*/
/datum/element/immerse/proc/try_unimmerse(atom/movable/movable, atom/movable/buckled)
var/atom/movable/to_check = buckled || movable
if(!(to_check.movement_type & MOVETYPES_NOT_TOUCHING_GROUND) && !movable.throwing)
remove_immerse_overlay(movable)
if(!buckled)
UnregisterSignal(movable, list(COMSIG_MOVETYPE_FLAG_ENABLED, COMSIG_MOVETYPE_FLAG_DISABLED, COMSIG_MOVABLE_POST_THROW, COMSIG_MOVABLE_THROW_LANDED))
/datum/element/immerse/proc/on_set_buckled(mob/living/source, atom/movable/new_buckled)
SIGNAL_HANDLER
try_unimmerse(source, source.buckled)
try_immerse(source, new_buckled)
///Removes the overlay from mob and bucklees is flying.
/datum/element/immerse/proc/on_move_flag_enabled(atom/movable/source, flag, old_movement_type)
SIGNAL_HANDLER
if(!(flag & MOVETYPES_NOT_TOUCHING_GROUND) || (old_movement_type & MOVETYPES_NOT_TOUCHING_GROUND) || source.throwing)
return
remove_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
remove_immerse_overlay(buckled_mob)
///Works just like on_move_flag_enabled, except it only has to check that movable isn't flying
/datum/element/immerse/proc/on_throw(atom/movable/source)
SIGNAL_HANDLER
if(source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
return
remove_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
remove_immerse_overlay(buckled_mob)
///Readds the overlay to the mob and bucklees if no longer flying.
/datum/element/immerse/proc/on_move_flag_disabled(atom/movable/source, flag, old_movement_type)
SIGNAL_HANDLER
if(!(flag & MOVETYPES_NOT_TOUCHING_GROUND) || (source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND) || source.throwing)
return
add_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
add_immerse_overlay(buckled_mob)
///Works just like on_move_flag_disabled, except it only has to check that movable isn't flying
/datum/element/immerse/proc/on_throw_landed(atom/movable/source)
SIGNAL_HANDLER
if(source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
return
add_immerse_overlay(source)
for(var/mob/living/buckled_mob as anything in source.buckled_mobs)
add_immerse_overlay(buckled_mob)
/**
* Called when a movable exits the turf. If its new location is not in the list of turfs with this element,
* Remove the movable from the element.
*/
/datum/element/immerse/proc/on_atom_exited(turf/source, atom/movable/exited, direction)
SIGNAL_HANDLER
if(!(exited.loc in attached_turfs_and_movables))
remove_from_element(source, exited)
else
LAZYREMOVE(attached_turfs_and_movables[source], exited)
LAZYADD(attached_turfs_and_movables[exited.loc], exited)
///Remove any signal, overlay, trait given to the movable and reference to it within the element.
/datum/element/immerse/proc/remove_from_element(turf/source, atom/movable/movable)
var/atom/movable/buckled
if(isliving(movable))
var/mob/living/living_mob = movable
buckled = living_mob.buckled
try_unimmerse(movable, buckled)
UnregisterSignal(movable, list(COMSIG_LIVING_SET_BUCKLED, COMSIG_QDELETING))
REMOVE_TRAIT(movable, TRAIT_IMMERSED, ELEMENT_TRAIT(src))
LAZYREMOVE(attached_turfs_and_movables[source], movable)
/// A band-aid to keep the (unique) visual overlay from scaling and rotating along with its owner. I'm sorry.
/datum/element/immerse/proc/on_update_transform(mob/living/source, resize, new_lying_angle, is_opposite_angle)
SIGNAL_HANDLER
var/matrix/new_transform = matrix()
new_transform.Scale(1/source.current_size)
new_transform.Turn(-new_lying_angle)
var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[source]
if(is_opposite_angle)
vis_overlay.transform = new_transform
vis_overlay.adjust_living_overlay_offset(source)
return
/**
* Here, we temporarily switch from the offset of the mutable appearance to one for movable used as visual overlay.
* Why? While visual overlays can be animated, their fixed point stays at the center of the icon of the atom
* they're attached to and not theirs, which can make manipulating the transform var a pain, but because
* we cannot do that with normal overlay or filters (reliably), we have to bend a knee and try to compensate it.
*/
vis_overlay.overlays = list(vis_overlay.overlay_appearance)
/// Oh, yeah, didn't I mention turning a visual overlay affects its pixel x/y/w/z too? Yeah, it sucks.
var/new_x = vis_overlay.extra_width
var/new_y = vis_overlay.extra_height
var/old_div = source.current_size/resize
var/offset_lying = source.rotate_on_lying ? PIXEL_Y_OFFSET_LYING : source.get_pixel_y_offset_standing(source.current_size/resize)
switch(source.lying_prev)
if(270)
vis_overlay.pixel_x += -offset_lying / old_div
if(90)
vis_overlay.pixel_x += offset_lying / old_div
if(0)
vis_overlay.pixel_y += -source.get_pixel_y_offset_standing(source.current_size/resize) / old_div
switch(new_lying_angle)
if(270)
new_x += -source.body_position_pixel_y_offset / source.current_size
if(90)
new_x += source.body_position_pixel_y_offset / source.current_size
if(0)
new_y += -source.body_position_pixel_y_offset / source.current_size
animate(vis_overlay, transform = new_transform, pixel_x = new_x, pixel_y = new_y, time = UPDATE_TRANSFORM_ANIMATION_TIME, easing = (EASE_IN|EASE_OUT))
addtimer(CALLBACK(vis_overlay, TYPE_PROC_REF(/atom/movable/immerse_overlay, adjust_living_overlay_offset), source), UPDATE_TRANSFORM_ANIMATION_TIME)
///Spin the overlay in the opposite direction so it doesn't look like it's spinning at all.
/datum/element/immerse/proc/on_spin_animation(atom/source, speed, loops, segments, segment)
SIGNAL_HANDLER
var/atom/movable/immerse_overlay/vis_overlay = immersed_movables[source]
vis_overlay.do_spin_animation(speed, loops, segments, -segment)
///We need to make sure to remove hard refs from the element when deleted.
/datum/element/immerse/proc/clear_overlay_refs(atom/movable/immerse_overlay/source)
//Assume that every vis loc is also in the immersed_movables list
for(var/atom/movable/vis_loc as anything in source.vis_locs)
remove_from_element(vis_loc.loc, vis_loc)
LAZYREMOVE(generated_visual_overlays, source)
source.overlay_appearance = null
///The not-quite-perfect movable used by the immerse element for its nefarious deeds.
/atom/movable/immerse_overlay
appearance_flags = RESET_TRANSFORM|RESET_COLOR|RESET_ALPHA|KEEP_TOGETHER
vis_flags = VIS_INHERIT_PLANE|VIS_INHERIT_ID
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
blend_mode = BLEND_INSET_OVERLAY
layer = WATER_VISUAL_OVERLAY_LAYER
plane = FLOAT_PLANE
/**
* The actual overlay used to make the mob look like it's half-covered in water.
*
* For visual overlays, pixel y/x/w/z are amplified by the a, b, d, e variables
* of the transform matrix of the movable they're attached to.
* For example, if a mob is twice its normal size (a = 2, e = 2),
* offsetting the movable used as visual overlay by 4 pixels to the right will result
* in the visual overlay moving 8 pixels to the right.
*
* This however, doesn't extend to the overlays of our visual overlay. which is why there's
* a mutable appearance variable that we use for those pixel offsets that really shouldn't be affected
* by the transform of our vis loc(s) in the first place.
*/
var/mutable_appearance/overlay_appearance
///The base pixel x offset of this movable
var/extra_width = 0
///The base pixel y offset of this movable
var/extra_height = 0
/atom/movable/immerse_overlay/Initialize(mapload, datum/element/immerse/element)
. = ..()
verbs.Cut() //"Cargo cultttttt" or something. Either way, they're better off without verbs.
element?.RegisterSignal(src, COMSIG_QDELETING, TYPE_PROC_REF(/datum/element/immerse, clear_overlay_refs))
///Called by COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY for living mobs and a few procs from the immerse element.
/atom/movable/immerse_overlay/proc/adjust_living_overlay_offset(mob/living/source)
pixel_x = extra_width
pixel_y = extra_height
overlay_appearance.pixel_y = -source.body_position_pixel_y_offset
overlays = list(overlay_appearance)
overlay_appearance.pixel_y = 0