diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 21c44e8ea25..5496b62277c 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -978,6 +978,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_CHASM_DESTROYED "chasm_destroyed" /// Trait from being under the floor in some manner #define TRAIT_UNDERFLOOR "underfloor" +/// If the movable shouldn't be reflected by mirrors. +#define TRAIT_NO_MIRROR_REFLECTION "no_mirror_reflection" /// If this movable is currently treading in a turf with the immerse element. #define TRAIT_IMMERSED "immersed" /** diff --git a/code/datums/components/reflection.dm b/code/datums/components/reflection.dm new file mode 100644 index 00000000000..b8b51359ec6 --- /dev/null +++ b/code/datums/components/reflection.dm @@ -0,0 +1,125 @@ +/** + * A simple-ish component that reflects the icons of movables on the parent like a mirror. + * Sadly, there's no easy way to make the SOUTH dir reflection flip the visual so that you can see + * the back NORTH dir of a target while it's facing SOUTH beside adding the VIS_INHERIT_DIR flag + * to the target movable, which I'm not doing to spare eventual issues with other vis overlays in the future. + */ +/datum/component/reflection + /** + * The direction from which the component gets its visual overlays. + * The visuals are also flipped horizontally or vertically based on it. + */ + var/reflected_dir + /// the movable which the reflected movables are attached to, in turn added to the vis contents of the parent. + var/atom/movable/reflection_holder + /// A lazy assoc list that keeps track of which movables are being reflected and the associated reflections. + var/list/reflected_movables + /// A callback used check to know which movables should be reflected and which not. + var/datum/callback/can_reflect + ///the base matrix used by reflections + var/matrix/reflection_matrix + ///the filter data added to reflection holder. + var/list/reflection_filter + ///the transparency channel value of the reflection holder. + var/alpha + ///A list of signals that when sent to the parent, will force the comp to recalculate the reflected movables. + var/list/update_signals + +/datum/component/reflection/Initialize(reflected_dir = NORTH, list/reflection_filter, matrix/reflection_matrix, datum/callback/can_reflect, alpha = 150, list/update_signals) + if(!ismovable(parent)) + return COMPONENT_INCOMPATIBLE + + var/static/list/connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_movable_entered_or_initialized), + COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON = PROC_REF(on_movable_entered_or_initialized), + COMSIG_ATOM_EXITED = PROC_REF(on_movable_exited) + ) + var/atom/movable/mov_parent = parent + AddComponent(/datum/component/connect_range, parent, connections, 1, works_in_containers = FALSE) + src.reflected_dir = reflected_dir + src.reflection_matrix = reflection_matrix + src.reflection_filter = reflection_filter + src.can_reflect = can_reflect + reflection_holder = new + reflection_holder.alpha = alpha + reflection_holder.appearance_flags = KEEP_TOGETHER + reflection_holder.vis_flags = VIS_INHERIT_ID + reflection_holder.alpha = alpha + reflection_holder.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + if(reflection_filter) + reflection_holder.add_filter("reflection", 1, reflection_filter) + mov_parent.vis_contents += reflection_holder + + set_reflection(new_dir = mov_parent.dir) + + RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_dir_change)) + var/list/reflect_update_signals = list(COMSIG_MOVABLE_MOVED) + update_signals + RegisterSignals(parent, reflect_update_signals, PROC_REF(get_reflection_targets)) + +/datum/component/reflection/Destroy() + QDEL_LIST_ASSOC_VAL(reflected_movables) + QDEL_NULL(reflection_holder) + return ..() + +///Called when the parent changes its direction. +/datum/component/reflection/proc/on_dir_change(atom/movable/source, old_dir, new_dir) + SIGNAL_HANDLER + set_reflection(old_dir, new_dir) + +///Turns the allowed reflected direction alongside the parent's dir. then calls get_reflection_targets. +/datum/component/reflection/proc/set_reflection(old_dir = SOUTH, new_dir = SOUTH) + if(old_dir == new_dir) + return + + reflected_dir = turn(reflected_dir, dir2angle(new_dir) - dir2angle(old_dir)) + get_reflection_targets() + +///Unsets the old reflected movables and sets it with new ones. +/datum/component/reflection/proc/get_reflection_targets(atom/movable/source) + SIGNAL_HANDLER + QDEL_LIST_ASSOC_VAL(reflected_movables) + for(var/atom/movable/target in view(1, source)) + if(check_can_reflect(target, FALSE)) + set_reflected(target) + +///Checks if the target movable can be reflected or not. +/datum/component/reflection/proc/check_can_reflect(atom/movable/target, check_view = TRUE) + if(target == parent || (check_view && !(target in view(1, parent)))) + return FALSE + var/atom/movable/mov_parent = parent + if(target.loc != mov_parent.loc && get_dir(mov_parent, target) != reflected_dir) + return FALSE + if(can_reflect && !can_reflect.Invoke(target)) + return FALSE + return TRUE + +///Called when a movable enters a turf within the connected range +/datum/component/reflection/proc/on_movable_entered_or_initialized(atom/movable/source, atom/movable/arrived) + SIGNAL_HANDLER + if(LAZYACCESS(reflected_movables, arrived) || !check_can_reflect(arrived)) + return + set_reflected(arrived) + +///Called when a movable exits a turf within the connected range +/datum/component/reflection/proc/on_movable_exited(atom/movable/source, atom/movable/gone) + SIGNAL_HANDLER + var/atom/movable/reflection = LAZYACCESS(reflected_movables, gone) + if(!reflection || check_can_reflect(gone)) + return + qdel(reflection) + LAZYREMOVE(reflected_movables, gone) + +///Sets up a visual overlay of the target movables, which is added to the parent's vis contents. +/datum/component/reflection/proc/set_reflected(atom/movable/target) + SIGNAL_HANDLER + var/atom/movable/reflection = new + reflection.vis_contents += target + ///The filter is added to the reflection holder; the matrix is not, otherwise that'd go affecting the filter. + if(reflection_matrix) + reflection.transform = reflection_matrix + if(reflected_dir == NORTH) + reflection.transform = reflection.transform.Scale(1, -1) + else if(reflected_dir != SOUTH) + reflection.transform = reflection.transform.Scale(-1, 1) + LAZYSET(reflected_movables, target, reflection) + reflection_holder.vis_contents += reflection diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm index 53ed501787c..21767551886 100644 --- a/code/game/objects/structures/mirror.dm +++ b/code/game/objects/structures/mirror.dm @@ -9,6 +9,22 @@ max_integrity = 200 integrity_failure = 0.5 +/obj/structure/mirror/Initialize(mapload) + . = ..() + var/static/list/reflection_filter = alpha_mask_filter(icon = icon('icons/obj/watercloset.dmi', "mirror_mask")) + var/static/matrix/reflection_matrix = matrix(0.75, 0, 0, 0, 0.75, 0) + var/datum/callback/can_reflect = CALLBACK(src, PROC_REF(can_reflect)) + var/list/update_signals = list(COMSIG_ATOM_BREAK) + AddComponent(/datum/component/reflection, reflection_filter = reflection_filter, reflection_matrix = reflection_matrix, can_reflect = can_reflect, update_signals = update_signals) + +/obj/structure/mirror/proc/can_reflect(atom/movable/target) + ///I'm doing it this way too, because the signal is sent before the broken variable is set to TRUE. + if(atom_integrity <= integrity_failure * max_integrity) + return FALSE + if(broken || !isliving(target) || HAS_TRAIT(target, TRAIT_NO_MIRROR_REFLECTION)) + return FALSE + return TRUE + MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror, 28) /obj/structure/mirror/broken diff --git a/code/modules/mob/living/basic/vermin/space_bat.dm b/code/modules/mob/living/basic/vermin/space_bat.dm index 6fa1fd7aed5..3ce1b41173d 100644 --- a/code/modules/mob/living/basic/vermin/space_bat.dm +++ b/code/modules/mob/living/basic/vermin/space_bat.dm @@ -37,7 +37,7 @@ . = ..() AddElement(/datum/element/simple_flying) AddElement(/datum/element/ai_retaliate) - add_traits(list(TRAIT_SPACEWALK, TRAIT_VENTCRAWLER_ALWAYS), INNATE_TRAIT) + add_traits(list(TRAIT_SPACEWALK, TRAIT_VENTCRAWLER_ALWAYS, TRAIT_NO_MIRROR_REFLECTION), INNATE_TRAIT) ///Controller for space bats, has nothing unique, just retaliation. /datum/ai_controller/basic_controller/space_bat diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm index 4fff061ebf1..d24e5f3d272 100644 --- a/code/modules/mob/living/carbon/human/species_types/vampire.dm +++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm @@ -14,6 +14,7 @@ TRAIT_NOBREATH, TRAIT_NOHUNGER, TRAIT_USES_SKINTONES, + TRAIT_NO_MIRROR_REFLECTION, ) inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID mutant_bodyparts = list("wings" = "None") diff --git a/icons/obj/watercloset.dmi b/icons/obj/watercloset.dmi index 3358d1e3ea5..93ab67c804c 100644 Binary files a/icons/obj/watercloset.dmi and b/icons/obj/watercloset.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 5e702e13707..449bcab7bfc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1104,6 +1104,7 @@ #include "code\datums\components\radioactive_exposure.dm" #include "code\datums\components\reagent_refiller.dm" #include "code\datums\components\redirect_attack_hand_from_turf.dm" +#include "code\datums\components\reflection.dm" #include "code\datums\components\regenerator.dm" #include "code\datums\components\religious_tool.dm" #include "code\datums\components\rename.dm"