From 04323a748b3b4b694fcb6d298823aaf51050ba20 Mon Sep 17 00:00:00 2001 From: Siro Date: Sun, 18 Jan 2026 15:17:58 -0700 Subject: [PATCH 01/15] mimic component started --- code/datums/components/perfect_mimicry.dm | 13 +++++++++++++ tgstation.dme | 1 + 2 files changed, 14 insertions(+) create mode 100644 code/datums/components/perfect_mimicry.dm diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm new file mode 100644 index 0000000000000..163798b310a79 --- /dev/null +++ b/code/datums/components/perfect_mimicry.dm @@ -0,0 +1,13 @@ +/datum/component/perfect_mimicry + var/obj/item/mimic_target + +/datum/component/perfect_mimicry/Initialize() + . = ..() + if(!ismob(parent)) + return COMPONENT_INCOMPATIBLE + +/datum/component/perfect_mimicry/RegisterWithParent() + . = ..() + +/datum/component/perfect_mimicry/UnregisterFromParent() + . = ..() diff --git a/tgstation.dme b/tgstation.dme index 4bbdf0e2f9a0d..98b7c0a98abf0 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1273,6 +1273,7 @@ #include "code\datums\components\parry.dm" #include "code\datums\components\payment.dm" #include "code\datums\components\pellet_cloud.dm" +#include "code\datums\components\perfect_mimicry.dm" #include "code\datums\components\phylactery.dm" #include "code\datums\components\pinata.dm" #include "code\datums\components\plundering_attacks.dm" From 3940bc195c1127d28aab047922ffdbcfaf3002c7 Mon Sep 17 00:00:00 2001 From: Siro Date: Sun, 18 Jan 2026 20:03:22 -0700 Subject: [PATCH 02/15] Add skeleton for base mimic ability and tying to mimicry datum. --- code/datums/components/perfect_mimicry.dm | 56 ++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 163798b310a79..9921d2d08814a 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -1,13 +1,65 @@ /datum/component/perfect_mimicry var/obj/item/mimic_target + var/list/allowed_objects = list() -/datum/component/perfect_mimicry/Initialize() +/datum/component/perfect_mimicry/Initialize(list/allowed_objects = list()) . = ..() - if(!ismob(parent)) + if(!isliving(parent)) return COMPONENT_INCOMPATIBLE + src.allowed_objects = allowed_objects + + var/datum/action/cooldown/mimic_object/action = new(src) + action.Grant(parent) + /datum/component/perfect_mimicry/RegisterWithParent() . = ..() /datum/component/perfect_mimicry/UnregisterFromParent() . = ..() + +/datum/component/perfect_mimicry/proc/is_allowed_object(obj/item/target_item) + if(!target_item || !isitem(target_item)) + return FALSE + if(length(allowed_objects) && !is_type_in_typecache(target_item, allowed_objects)) + return FALSE + return TRUE + +/datum/component/perfect_mimicry/proc/mimic_object(obj/item/target_item) + if(!is_allowed_object(target_item)) + return FALSE + +/datum/action/cooldown/mimic_object + name = "Mimic Object" + desc = "Take on the appearance and behavior of a nearby object. Use again to reveal yourself." + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_INCAPACITATED + + click_to_activate = TRUE + cooldown_time = 1 SECOND + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + +/datum/action/cooldown/mimic_object/New(Target) + . = ..() + if(!istype(Target, /datum/component/perfect_mimicry)) + stack_trace("[name] ([type]) was instantiated on a non-perfect_mimicry target, this doesn't work.") + qdel(src) + return + +/datum/action/cooldown/mimic_object/PreActivate(atom/target) + var/datum/component/perfect_mimicry/mimicker = src.target + if(target == owner) + to_chat(owner, span_notice("You cannot mimic yourself.")) + return + if(!isturf(target.loc, owner)) // Prevent transformation in/from some inventory + return + if(get_dist(owner, target) > 2) + to_chat(owner, span_notice("Object is too far away.")) + return + if(!mimicker.is_allowed_object(target)) + to_chat(owner, span_notice("Object is too complex to mimic.")) + return + return ..() + +/datum/action/cooldown/mimic_object/Activate(atom/target) + StartCooldown() + return TRUE From dfbcd11c484078459470a44fe271c5c3bcc383a1 Mon Sep 17 00:00:00 2001 From: Siro Date: Mon, 19 Jan 2026 16:59:29 -0700 Subject: [PATCH 03/15] Base item mimicing added, mimic goes invisible and has basic click and move blocks, player movement synced with mimicked item movement --- code/__DEFINES/sight.dm | 1 + code/datums/components/perfect_mimicry.dm | 82 +++++++++++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/code/__DEFINES/sight.dm b/code/__DEFINES/sight.dm index b84f2bf75e713..3dc8beb53138e 100644 --- a/code/__DEFINES/sight.dm +++ b/code/__DEFINES/sight.dm @@ -65,6 +65,7 @@ // INVISIBILITY PRIORITIES #define INVISIBILITY_PRIORITY_ADMIN 100 +#define INVISIBILITY_PRIORITY_ABSTRACT 21 //for abstract objects/mobs (e.g. mimic while mimicing), things that are not really there. #define INVISIBILITY_PRIORITY_TURRET_COVER 20 #define INVISIBILITY_PRIORITY_BASIC_ANTI_INVISIBILITY 10 #define INVISIBILITY_PRIORITY_NONE 0 diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 9921d2d08814a..7d80e176a22f5 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -1,7 +1,9 @@ /datum/component/perfect_mimicry - var/obj/item/mimic_target var/list/allowed_objects = list() + var/obj/item/mimic_target // The object we are currently mimicking + var/datum/movement_detector/tracker // Tracker to keep the mob "glued" to the mimic target + /datum/component/perfect_mimicry/Initialize(list/allowed_objects = list()) . = ..() if(!isliving(parent)) @@ -14,9 +16,12 @@ /datum/component/perfect_mimicry/RegisterWithParent() . = ..() + RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_normal_movement)) + RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(block_normal_clicks)) /datum/component/perfect_mimicry/UnregisterFromParent() . = ..() + UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) /datum/component/perfect_mimicry/proc/is_allowed_object(obj/item/target_item) if(!target_item || !isitem(target_item)) @@ -25,10 +30,65 @@ return FALSE return TRUE +/datum/component/perfect_mimicry/proc/block_normal_movement(datum/source, atom/entering_loc) + SIGNAL_HANDLER + if(!isnull(mimic_target)) + return COMPONENT_MOVABLE_BLOCK_PRE_MOVE + return + +/datum/component/perfect_mimicry/proc/block_normal_clicks(datum/source, atom/target, list/modifiers) + SIGNAL_HANDLER + if(!isnull(mimic_target)) + return COMSIG_MOB_CANCEL_CLICKON + return + /datum/component/perfect_mimicry/proc/mimic_object(obj/item/target_item) - if(!is_allowed_object(target_item)) + if(!is_allowed_object(target_item) || !isliving(parent)) + return + + var/mob/living/mimic = parent + mimic_target = new target_item.type(mimic.loc) + mimic_target.name = target_item.name + mimic_target.appearance = target_item.appearance + mimic_target.copy_overlays(target_item) + mimic_target.alpha = max(target_item.alpha, 150) + mimic_target.transform = initial(target_item.transform) + mimic_target.pixel_x = target_item.base_pixel_x + mimic_target.pixel_y = target_item.base_pixel_y + + if(ismovable(target_item)) + tracker = new(mimic_target, CALLBACK(src, PROC_REF(sync_mimic_position))) + + mimic.SetInvisibility(INVISIBILITY_MAXIMUM, id=REF(src), priority=INVISIBILITY_PRIORITY_ABSTRACT) + return mimic_target + +/datum/component/perfect_mimicry/proc/return_form() + if(isnull(mimic_target) || !isliving(parent)) return FALSE + var/mob/living/mimic = parent + mimic_target.transfer_observers_to(mimic) + + mimic.RemoveInvisibility(REF(src)) + QDEL_NULL(mimic_target) + QDEL_NULL(tracker) + return TRUE + +/datum/component/perfect_mimicry/proc/sync_mimic_position(atom/movable/master, atom/mover, atom/oldloc, direction) + var/mob/living/mimic = parent + if(master.loc == oldloc) + return + + var/turf/newturf = get_turf(master) + if(!newturf) + //Handle this condition gracefully + return + + if(QDELETED(mimic) || mimic.loc == newturf) + return + + mimic.abstract_move(newturf) + /datum/action/cooldown/mimic_object name = "Mimic Object" desc = "Take on the appearance and behavior of a nearby object. Use again to reveal yourself." @@ -47,10 +107,12 @@ /datum/action/cooldown/mimic_object/PreActivate(atom/target) var/datum/component/perfect_mimicry/mimicker = src.target + if(!isnull(mimicker.mimic_target)) + return ..() if(target == owner) to_chat(owner, span_notice("You cannot mimic yourself.")) return - if(!isturf(target.loc, owner)) // Prevent transformation in/from some inventory + if(!isturf(target.loc, owner.loc)) // Prevent transformation in/from some inventory return if(get_dist(owner, target) > 2) to_chat(owner, span_notice("Object is too far away.")) @@ -61,5 +123,15 @@ return ..() /datum/action/cooldown/mimic_object/Activate(atom/target) - StartCooldown() - return TRUE + var/datum/component/perfect_mimicry/mimicker = src.target + if(!isnull(mimicker.mimic_target) && mimicker.return_form()) + click_to_activate = TRUE + StartCooldown() + return TRUE + + if(mimicker.mimic_object(target)) + click_to_activate = FALSE + StartCooldown() + return TRUE + + return FALSE From fbabcb4f97e2ad52de33c18d4631d5e805e9c22e Mon Sep 17 00:00:00 2001 From: Siro Date: Fri, 23 Jan 2026 22:08:12 -0700 Subject: [PATCH 04/15] attempt to rewrite and add proper GC --- code/datums/components/perfect_mimicry.dm | 164 ++++++++++------------ 1 file changed, 74 insertions(+), 90 deletions(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 7d80e176a22f5..a9e5830d2763f 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -1,137 +1,121 @@ +#define DISGUISED TRUE +#define UNDISGUISED FALSE + /datum/component/perfect_mimicry - var/list/allowed_objects = list() + dupe_mode = COMPONENT_DUPE_UNIQUE + + var/list/allowed_objects = list() // typecache of allowed objects to mimic + var/list/applied_traits = list() + var/currently_disguised = UNDISGUISED // Simple flag to track if we are disguised or not var/obj/item/mimic_target // The object we are currently mimicking var/datum/movement_detector/tracker // Tracker to keep the mob "glued" to the mimic target +//if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER) + /datum/component/perfect_mimicry/Initialize(list/allowed_objects = list()) . = ..() if(!isliving(parent)) return COMPONENT_INCOMPATIBLE - src.allowed_objects = allowed_objects + src.allowed_objects = typecacheof(allowed_objects) + //var/datum/action/cooldown/mimic_object/action = new(src) + //action.Grant(parent) - var/datum/action/cooldown/mimic_object/action = new(src) - action.Grant(parent) +/datum/component/perfect_mimicry/Destroy(force) + stop_mimicry() + allowed_objects = null + applied_traits = null + return ..() /datum/component/perfect_mimicry/RegisterWithParent() . = ..() - RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_normal_movement)) - RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(block_normal_clicks)) + //RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_normal_movement)) + //RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(block_normal_clicks)) /datum/component/perfect_mimicry/UnregisterFromParent() . = ..() - UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) + //UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) /datum/component/perfect_mimicry/proc/is_allowed_object(obj/item/target_item) - if(!target_item || !isitem(target_item)) + if(!isitem(target_item)) return FALSE if(length(allowed_objects) && !is_type_in_typecache(target_item, allowed_objects)) return FALSE return TRUE +/datum/component/perfect_mimicry/proc/start_mimicry(obj/item/target_item) + if(QDELETED(parent) || !isliving(parent)) + return + if(currently_disguised == DISGUISED) + return // Already disguised + if(!is_allowed_object(target_item)) + return // Not an allowed object + + //make new mimic target + //Handle object specific cases like emptying reagents + +/datum/component/perfect_mimicry/proc/stop_mimicry() + if(QDELETED(parent) || !isliving(parent)) + QDEL_NULL(tracker) + if(!QDELETED(mimic_target)) + // unreg signals from mimic target + QDEL_NULL(mimic_target) + return FALSE + if(!isnull(mimic_target)) + QDEL_NULL(tracker) + // unreg signals from mimic target + QDEL_NULL(mimic_target) + + if(currently_disguised == UNDISGUISED) + return FALSE // Already undisguised + +/* + * Signal handlers + */ /datum/component/perfect_mimicry/proc/block_normal_movement(datum/source, atom/entering_loc) SIGNAL_HANDLER - if(!isnull(mimic_target)) + if(currently_disguised == DISGUISED) return COMPONENT_MOVABLE_BLOCK_PRE_MOVE return /datum/component/perfect_mimicry/proc/block_normal_clicks(datum/source, atom/target, list/modifiers) SIGNAL_HANDLER - if(!isnull(mimic_target)) + if(currently_disguised == DISGUISED) return COMSIG_MOB_CANCEL_CLICKON return -/datum/component/perfect_mimicry/proc/mimic_object(obj/item/target_item) - if(!is_allowed_object(target_item) || !isliving(parent)) - return - - var/mob/living/mimic = parent - mimic_target = new target_item.type(mimic.loc) - mimic_target.name = target_item.name - mimic_target.appearance = target_item.appearance - mimic_target.copy_overlays(target_item) - mimic_target.alpha = max(target_item.alpha, 150) - mimic_target.transform = initial(target_item.transform) - mimic_target.pixel_x = target_item.base_pixel_x - mimic_target.pixel_y = target_item.base_pixel_y - - if(ismovable(target_item)) - tracker = new(mimic_target, CALLBACK(src, PROC_REF(sync_mimic_position))) - - mimic.SetInvisibility(INVISIBILITY_MAXIMUM, id=REF(src), priority=INVISIBILITY_PRIORITY_ABSTRACT) - return mimic_target - -/datum/component/perfect_mimicry/proc/return_form() - if(isnull(mimic_target) || !isliving(parent)) - return FALSE - - var/mob/living/mimic = parent - mimic_target.transfer_observers_to(mimic) - - mimic.RemoveInvisibility(REF(src)) - QDEL_NULL(mimic_target) - QDEL_NULL(tracker) - return TRUE - +/* + * Tracker callback + */ /datum/component/perfect_mimicry/proc/sync_mimic_position(atom/movable/master, atom/mover, atom/oldloc, direction) - var/mob/living/mimic = parent - if(master.loc == oldloc) - return - var/turf/newturf = get_turf(master) - if(!newturf) - //Handle this condition gracefully - return - - if(QDELETED(mimic) || mimic.loc == newturf) - return - - mimic.abstract_move(newturf) - -/datum/action/cooldown/mimic_object - name = "Mimic Object" - desc = "Take on the appearance and behavior of a nearby object. Use again to reveal yourself." +/* + * Mimicry actions + */ +/datum/action/cooldown/mimic_ability + name = "Base Mimic Ability" + desc = "You should not be seeing this. This is an error alert developers." check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_INCAPACITATED - click_to_activate = TRUE - cooldown_time = 1 SECOND - ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' - -/datum/action/cooldown/mimic_object/New(Target) +// These abilities require a perfect_mimicry component otherwise they are useless. +/datum/action/cooldown/mimic_ability/New(Target) . = ..() if(!istype(Target, /datum/component/perfect_mimicry)) stack_trace("[name] ([type]) was instantiated on a non-perfect_mimicry target, this doesn't work.") qdel(src) return -/datum/action/cooldown/mimic_object/PreActivate(atom/target) - var/datum/component/perfect_mimicry/mimicker = src.target - if(!isnull(mimicker.mimic_target)) - return ..() - if(target == owner) - to_chat(owner, span_notice("You cannot mimic yourself.")) - return - if(!isturf(target.loc, owner.loc)) // Prevent transformation in/from some inventory - return - if(get_dist(owner, target) > 2) - to_chat(owner, span_notice("Object is too far away.")) - return - if(!mimicker.is_allowed_object(target)) - to_chat(owner, span_notice("Object is too complex to mimic.")) - return - return ..() +/datum/action/cooldown/mimic_ability/mimic_object + name = "Mimic Object" + desc = "Take on the appearance and behavior of a nearby object. Use again to reveal yourself." -/datum/action/cooldown/mimic_object/Activate(atom/target) - var/datum/component/perfect_mimicry/mimicker = src.target - if(!isnull(mimicker.mimic_target) && mimicker.return_form()) - click_to_activate = TRUE - StartCooldown() - return TRUE + click_to_activate = TRUE + cooldown_time = 1 SECOND + var/cooldown_after_use = 3 SECONDS // Cooldown after mimicry ends + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' - if(mimicker.mimic_object(target)) - click_to_activate = FALSE - StartCooldown() - return TRUE +/datum/action/cooldown/mimic_ability/throw_self - return FALSE +#undef DISGUISED +#undef UNDISGUISED From 466fe8f72783dc6ca8c196676bd9883f0ee8d4f0 Mon Sep 17 00:00:00 2001 From: Siro Date: Fri, 23 Jan 2026 23:56:31 -0700 Subject: [PATCH 05/15] add 3 second cool down after item is deleted, qdel signal on mimicked item --- code/datums/components/perfect_mimicry.dm | 88 ++++++++++++++++++++--- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index a9e5830d2763f..cc88f6fd6389a 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -5,7 +5,7 @@ dupe_mode = COMPONENT_DUPE_UNIQUE var/list/allowed_objects = list() // typecache of allowed objects to mimic - var/list/applied_traits = list() + var/list/applied_traits = list() // List of traits applied to mob when mimicking to make it "intangible". var/currently_disguised = UNDISGUISED // Simple flag to track if we are disguised or not var/obj/item/mimic_target // The object we are currently mimicking @@ -19,8 +19,6 @@ return COMPONENT_INCOMPATIBLE src.allowed_objects = typecacheof(allowed_objects) - //var/datum/action/cooldown/mimic_object/action = new(src) - //action.Grant(parent) /datum/component/perfect_mimicry/Destroy(force) stop_mimicry() @@ -30,11 +28,14 @@ /datum/component/perfect_mimicry/RegisterWithParent() . = ..() - //RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_normal_movement)) + RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_normal_movement)) //RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(block_normal_clicks)) + var/datum/action/cooldown/mimic_ability/mimic_object/action = new(src) + action.Grant(parent) /datum/component/perfect_mimicry/UnregisterFromParent() . = ..() + UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE)) //UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) /datum/component/perfect_mimicry/proc/is_allowed_object(obj/item/target_item) @@ -44,6 +45,9 @@ return FALSE return TRUE +// Handle special cases for certain object types (Eg. emptying reagents from beakers) +/datum/component/perfect_mimicry/proc/handle_special_types(obj/item/mimic_item) + /datum/component/perfect_mimicry/proc/start_mimicry(obj/item/target_item) if(QDELETED(parent) || !isliving(parent)) return @@ -52,24 +56,45 @@ if(!is_allowed_object(target_item)) return // Not an allowed object - //make new mimic target - //Handle object specific cases like emptying reagents + var/mob/living/mimic = parent + mimic_target = new target_item.type(mimic.loc) + handle_special_types(mimic_target) + mimic_target.name = target_item.name + mimic_target.appearance = target_item.appearance + mimic_target.copy_overlays(target_item) + mimic_target.alpha = max(target_item.alpha, 150) + mimic_target.transform = initial(target_item.transform) + mimic_target.pixel_x = target_item.base_pixel_x + mimic_target.pixel_y = target_item.base_pixel_y + + if(QDELETED(mimic_target)) + mimic_target = null + return // Failed to create mimic target + RegisterSignal(mimic_target, COMSIG_QDELETING, PROC_REF(mimic_target_deleted)) + if(ismovable(target_item)) + tracker = new(mimic_target, CALLBACK(src, PROC_REF(sync_mimic_position))) + currently_disguised = DISGUISED + return mimic_target /datum/component/perfect_mimicry/proc/stop_mimicry() if(QDELETED(parent) || !isliving(parent)) QDEL_NULL(tracker) if(!QDELETED(mimic_target)) - // unreg signals from mimic target + UnregisterSignal(mimic_target, COMSIG_QDELETING) QDEL_NULL(mimic_target) return FALSE if(!isnull(mimic_target)) QDEL_NULL(tracker) - // unreg signals from mimic target - QDEL_NULL(mimic_target) + UnregisterSignal(mimic_target, COMSIG_QDELETING) + if(!QDELETED(mimic_target)) + QDEL_NULL(mimic_target) if(currently_disguised == UNDISGUISED) return FALSE // Already undisguised + currently_disguised = UNDISGUISED + return TRUE + /* * Signal handlers */ @@ -85,6 +110,15 @@ return COMSIG_MOB_CANCEL_CLICKON return +/datum/component/perfect_mimicry/proc/mimic_target_deleted(datum/source, force) + SIGNAL_HANDLER + stop_mimicry() + if(!QDELETED(parent) && isliving(parent)) + var/mob/living/mimic = parent + for(var/datum/action/cooldown/mimic_ability/mimic_object/A in mimic.actions) + A.click_to_activate = TRUE + A.StartCooldown(A.cooldown_after_use) + /* * Tracker callback */ @@ -115,6 +149,42 @@ var/cooldown_after_use = 3 SECONDS // Cooldown after mimicry ends ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' +/datum/action/cooldown/mimic_ability/mimic_object/PreActivate(atom/target) + var/datum/component/perfect_mimicry/mimicker = src.target + if(!istype(mimicker)) + return + if(mimicker.currently_disguised == DISGUISED) + return ..() + if(target == owner) + to_chat(owner, span_notice("You cannot mimic yourself.")) + return + //if(!isturf(target.loc, owner.loc)) // Prevent transformation in/from some inventory + // return + if(get_dist(owner, target) > 2) + to_chat(owner, span_notice("[target.name] is too far away.")) + return + if(!mimicker.is_allowed_object(target)) + to_chat(owner, span_notice("[target.name] is too complex to mimic.")) + return + return ..() + +/datum/action/cooldown/mimic_ability/mimic_object/Activate(atom/target) + var/datum/component/perfect_mimicry/mimicker = src.target + if(!istype(mimicker)) + return FALSE + + if((mimicker.currently_disguised == DISGUISED) && mimicker.stop_mimicry()) + click_to_activate = TRUE + StartCooldown(cooldown_after_use) + return TRUE + + if((mimicker.currently_disguised == UNDISGUISED) && mimicker.start_mimicry(target)) + click_to_activate = FALSE + StartCooldown() + return TRUE + + return FALSE + /datum/action/cooldown/mimic_ability/throw_self #undef DISGUISED From 58b918a64888fdc0889fcd829da1e6440671443e Mon Sep 17 00:00:00 2001 From: Siro Date: Sat, 24 Jan 2026 20:10:20 -0700 Subject: [PATCH 06/15] Using undense trait and readded invisibility readding tracking. --- code/datums/components/perfect_mimicry.dm | 35 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index cc88f6fd6389a..8c46ae2a9e91a 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -6,6 +6,8 @@ var/list/allowed_objects = list() // typecache of allowed objects to mimic var/list/applied_traits = list() // List of traits applied to mob when mimicking to make it "intangible". + /// List of /datum/action instance that we've registered `COMSIG_ACTION_TRIGGER` on. + //var/list/datum/action/registered_actions var/currently_disguised = UNDISGUISED // Simple flag to track if we are disguised or not var/obj/item/mimic_target // The object we are currently mimicking @@ -29,14 +31,15 @@ /datum/component/perfect_mimicry/RegisterWithParent() . = ..() RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_normal_movement)) - //RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(block_normal_clicks)) + RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(block_normal_clicks)) + var/datum/action/cooldown/mimic_ability/mimic_object/action = new(src) action.Grant(parent) /datum/component/perfect_mimicry/UnregisterFromParent() . = ..() UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE)) - //UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) + UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) /datum/component/perfect_mimicry/proc/is_allowed_object(obj/item/target_item) if(!isitem(target_item)) @@ -73,6 +76,8 @@ RegisterSignal(mimic_target, COMSIG_QDELETING, PROC_REF(mimic_target_deleted)) if(ismovable(target_item)) tracker = new(mimic_target, CALLBACK(src, PROC_REF(sync_mimic_position))) + ADD_TRAIT(mimic, TRAIT_UNDENSE, REF(src)) + mimic.SetInvisibility(INVISIBILITY_MAXIMUM, id=REF(src), priority=INVISIBILITY_PRIORITY_ABSTRACT) currently_disguised = DISGUISED return mimic_target @@ -83,15 +88,24 @@ UnregisterSignal(mimic_target, COMSIG_QDELETING) QDEL_NULL(mimic_target) return FALSE + var/mob/living/mimic = parent + var/drop_loc if(!isnull(mimic_target)) QDEL_NULL(tracker) UnregisterSignal(mimic_target, COMSIG_QDELETING) + mimic_target.transfer_observers_to(parent) + drop_loc = mimic_target.drop_location() + if(!get_turf(drop_loc)) + drop_loc = mimic.loc if(!QDELETED(mimic_target)) QDEL_NULL(mimic_target) if(currently_disguised == UNDISGUISED) return FALSE // Already undisguised - + if(!isnull(drop_loc)) + mimic.abstract_move(drop_loc) + REMOVE_TRAIT(mimic, TRAIT_UNDENSE, REF(src)) + mimic.RemoveInvisibility(REF(src)) currently_disguised = UNDISGUISED return TRUE @@ -102,13 +116,11 @@ SIGNAL_HANDLER if(currently_disguised == DISGUISED) return COMPONENT_MOVABLE_BLOCK_PRE_MOVE - return /datum/component/perfect_mimicry/proc/block_normal_clicks(datum/source, atom/target, list/modifiers) SIGNAL_HANDLER if(currently_disguised == DISGUISED) return COMSIG_MOB_CANCEL_CLICKON - return /datum/component/perfect_mimicry/proc/mimic_target_deleted(datum/source, force) SIGNAL_HANDLER @@ -123,7 +135,20 @@ * Tracker callback */ /datum/component/perfect_mimicry/proc/sync_mimic_position(atom/movable/master, atom/mover, atom/oldloc, direction) + var/mob/living/mimic = parent + if(master.loc == oldloc) + return + + var/turf/newturf = get_turf(master) + if(!newturf) + mimic.abstract_move(oldloc) + QDEL_NULL(master) + return + + if(QDELETED(mimic) || mimic.loc == newturf) + return + mimic.abstract_move(newturf) /* * Mimicry actions */ From 4bf020e1db983b1a58b6f42ac53ed2e864dfd5fe Mon Sep 17 00:00:00 2001 From: Siro Date: Wed, 28 Jan 2026 09:55:30 -0700 Subject: [PATCH 07/15] Moved mob traits and applied via list, Register and unregister procs made for mimic_target organization for item signal handlers. --- code/datums/components/perfect_mimicry.dm | 31 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 8c46ae2a9e91a..5c9689d8f786b 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -13,6 +13,8 @@ var/obj/item/mimic_target // The object we are currently mimicking var/datum/movement_detector/tracker // Tracker to keep the mob "glued" to the mimic target + var/list/applied_mob_traits = list(TRAIT_UNDENSE) // Applied traits when mimicking. Attempts to abstract mob during mimicking. + //if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER) /datum/component/perfect_mimicry/Initialize(list/allowed_objects = list()) @@ -41,6 +43,13 @@ UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE)) UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) +/datum/component/perfect_mimicry/proc/RegisterWithTarget() + RegisterSignal(src.mimic_target, COMSIG_QDELETING, PROC_REF(mimic_target_deleted)) +//COMSIG_ATOM_TAKE_DAMAGE + +/datum/component/perfect_mimicry/proc/UnregisterFromTarget() + UnregisterSignal(src.mimic_target, COMSIG_QDELETING) + /datum/component/perfect_mimicry/proc/is_allowed_object(obj/item/target_item) if(!isitem(target_item)) return FALSE @@ -73,10 +82,10 @@ if(QDELETED(mimic_target)) mimic_target = null return // Failed to create mimic target - RegisterSignal(mimic_target, COMSIG_QDELETING, PROC_REF(mimic_target_deleted)) + RegisterWithTarget() if(ismovable(target_item)) tracker = new(mimic_target, CALLBACK(src, PROC_REF(sync_mimic_position))) - ADD_TRAIT(mimic, TRAIT_UNDENSE, REF(src)) + mimic.add_traits(applied_mob_traits, REF(src)) mimic.SetInvisibility(INVISIBILITY_MAXIMUM, id=REF(src), priority=INVISIBILITY_PRIORITY_ABSTRACT) currently_disguised = DISGUISED return mimic_target @@ -85,14 +94,14 @@ if(QDELETED(parent) || !isliving(parent)) QDEL_NULL(tracker) if(!QDELETED(mimic_target)) - UnregisterSignal(mimic_target, COMSIG_QDELETING) + UnregisterFromTarget() QDEL_NULL(mimic_target) return FALSE var/mob/living/mimic = parent var/drop_loc if(!isnull(mimic_target)) QDEL_NULL(tracker) - UnregisterSignal(mimic_target, COMSIG_QDELETING) + UnregisterFromTarget() mimic_target.transfer_observers_to(parent) drop_loc = mimic_target.drop_location() if(!get_turf(drop_loc)) @@ -104,13 +113,13 @@ return FALSE // Already undisguised if(!isnull(drop_loc)) mimic.abstract_move(drop_loc) - REMOVE_TRAIT(mimic, TRAIT_UNDENSE, REF(src)) + mimic.remove_traits(applied_mob_traits, REF(src)) mimic.RemoveInvisibility(REF(src)) currently_disguised = UNDISGUISED return TRUE /* - * Signal handlers + * Signal handlers for the mob */ /datum/component/perfect_mimicry/proc/block_normal_movement(datum/source, atom/entering_loc) SIGNAL_HANDLER @@ -131,6 +140,16 @@ A.click_to_activate = TRUE A.StartCooldown(A.cooldown_after_use) +/* + * Signal handlers for the mimic_target + */ + +/* +/datum/component/perfect_mimicry/proc/on_take_damage(obj/target, damage_amt, danage_type, damage_flag, sound_effect, attack_dir, armour_penetration) + SIGNAL_HANDLER + to_chat(world, span_adminsay("Item took [damage_amt] damage")) +*/ + /* * Tracker callback */ From aea7b10d23c48fb99fae4073e8eefc68de653ccc Mon Sep 17 00:00:00 2001 From: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:38:01 -0500 Subject: [PATCH 08/15] mimic is not a component --- code/__HELPERS/duplicating.dm | 26 +- code/datums/components/perfect_mimicry.dm | 284 +++++++--------------- 2 files changed, 105 insertions(+), 205 deletions(-) diff --git a/code/__HELPERS/duplicating.dm b/code/__HELPERS/duplicating.dm index 225dca91fb5b1..235d41f4a92ca 100644 --- a/code/__HELPERS/duplicating.dm +++ b/code/__HELPERS/duplicating.dm @@ -8,15 +8,19 @@ GLOBAL_LIST_INIT(duplicate_forbidden_vars, list( "area", "atmos_adjacent_turfs", "bodyparts", + "buckled_mobs", "ckey", "client_mobs_in_contents", "_listen_lookup", "computer_id", "contents", "cooldowns", + "currently_z_moving", "_datum_components", "external_organs", "external_organs_slot", + "force_moving", + "grab_state", "group", "hand_bodyparts", "held_items", @@ -31,18 +35,29 @@ GLOBAL_LIST_INIT(duplicate_forbidden_vars, list( "locs", "managed_overlays", "managed_vis_overlays", + "moving_diagonally", + "moving_from_pull", + "move_packet", "overlays", "overlays_standing", + "orbit_target", + "orbiters", + "orbiting", "parent", "parent_type", + "pixloc", "power_supply", + "pulledby", + "pulling", "quirks", "reagents", + "spatial_grid_key", "_signal_procs", "stat", "status_effects", "_status_traits", "tag", + "thrownby", "tgui_shared_states", "type", "update_on_z", @@ -55,17 +70,17 @@ GLOBAL_PROTECT(duplicate_forbidden_vars) /** * # duplicate_object * - * Makes a copy of an item and transfers most vars over, barring GLOB.duplicate_forbidden_vars + * Makes a copy of a movable atom and transfers most vars over, barring GLOB.duplicate_forbidden_vars * Args: - * original - Atom being duplicated + * original - Movable atom being duplicated * spawning_location - Turf where the duplicated atom will be spawned at. */ -/proc/duplicate_object(atom/original, turf/spawning_location) +/proc/duplicate_object(atom/movable/original, turf/spawning_location) RETURN_TYPE(original.type) - if(!original) + if(!original || !istype(original)) return - var/atom/made_copy = new original.type(spawning_location) + var/atom/movable/made_copy = new original.type(spawning_location) for(var/atom_vars in original.vars - GLOB.duplicate_forbidden_vars) if(islist(original.vars[atom_vars])) @@ -94,4 +109,5 @@ GLOBAL_PROTECT(duplicate_forbidden_vars) for(var/datum/quirk/original_quirks as anything in original_living.quirks) copied_living.add_quirk(original_quirks.type) + made_copy.forceMove(spawning_location) return made_copy diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 5c9689d8f786b..05bbb721a3b4a 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -1,173 +1,7 @@ -#define DISGUISED TRUE -#define UNDISGUISED FALSE - -/datum/component/perfect_mimicry - dupe_mode = COMPONENT_DUPE_UNIQUE - - var/list/allowed_objects = list() // typecache of allowed objects to mimic - var/list/applied_traits = list() // List of traits applied to mob when mimicking to make it "intangible". - /// List of /datum/action instance that we've registered `COMSIG_ACTION_TRIGGER` on. - //var/list/datum/action/registered_actions - - var/currently_disguised = UNDISGUISED // Simple flag to track if we are disguised or not - var/obj/item/mimic_target // The object we are currently mimicking - var/datum/movement_detector/tracker // Tracker to keep the mob "glued" to the mimic target - - var/list/applied_mob_traits = list(TRAIT_UNDENSE) // Applied traits when mimicking. Attempts to abstract mob during mimicking. - -//if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER) - -/datum/component/perfect_mimicry/Initialize(list/allowed_objects = list()) - . = ..() - if(!isliving(parent)) - return COMPONENT_INCOMPATIBLE - - src.allowed_objects = typecacheof(allowed_objects) - -/datum/component/perfect_mimicry/Destroy(force) - stop_mimicry() - allowed_objects = null - applied_traits = null - return ..() - -/datum/component/perfect_mimicry/RegisterWithParent() - . = ..() - RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_normal_movement)) - RegisterSignal(parent, COMSIG_MOB_CLICKON, PROC_REF(block_normal_clicks)) - +/mob/living/proc/grant_mimicry() var/datum/action/cooldown/mimic_ability/mimic_object/action = new(src) - action.Grant(parent) - -/datum/component/perfect_mimicry/UnregisterFromParent() - . = ..() - UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE)) - UnregisterSignal(parent, list(COMSIG_MOVABLE_PRE_MOVE, COMSIG_MOB_CLICKON)) - -/datum/component/perfect_mimicry/proc/RegisterWithTarget() - RegisterSignal(src.mimic_target, COMSIG_QDELETING, PROC_REF(mimic_target_deleted)) -//COMSIG_ATOM_TAKE_DAMAGE - -/datum/component/perfect_mimicry/proc/UnregisterFromTarget() - UnregisterSignal(src.mimic_target, COMSIG_QDELETING) - -/datum/component/perfect_mimicry/proc/is_allowed_object(obj/item/target_item) - if(!isitem(target_item)) - return FALSE - if(length(allowed_objects) && !is_type_in_typecache(target_item, allowed_objects)) - return FALSE - return TRUE - -// Handle special cases for certain object types (Eg. emptying reagents from beakers) -/datum/component/perfect_mimicry/proc/handle_special_types(obj/item/mimic_item) - -/datum/component/perfect_mimicry/proc/start_mimicry(obj/item/target_item) - if(QDELETED(parent) || !isliving(parent)) - return - if(currently_disguised == DISGUISED) - return // Already disguised - if(!is_allowed_object(target_item)) - return // Not an allowed object - - var/mob/living/mimic = parent - mimic_target = new target_item.type(mimic.loc) - handle_special_types(mimic_target) - mimic_target.name = target_item.name - mimic_target.appearance = target_item.appearance - mimic_target.copy_overlays(target_item) - mimic_target.alpha = max(target_item.alpha, 150) - mimic_target.transform = initial(target_item.transform) - mimic_target.pixel_x = target_item.base_pixel_x - mimic_target.pixel_y = target_item.base_pixel_y - - if(QDELETED(mimic_target)) - mimic_target = null - return // Failed to create mimic target - RegisterWithTarget() - if(ismovable(target_item)) - tracker = new(mimic_target, CALLBACK(src, PROC_REF(sync_mimic_position))) - mimic.add_traits(applied_mob_traits, REF(src)) - mimic.SetInvisibility(INVISIBILITY_MAXIMUM, id=REF(src), priority=INVISIBILITY_PRIORITY_ABSTRACT) - currently_disguised = DISGUISED - return mimic_target - -/datum/component/perfect_mimicry/proc/stop_mimicry() - if(QDELETED(parent) || !isliving(parent)) - QDEL_NULL(tracker) - if(!QDELETED(mimic_target)) - UnregisterFromTarget() - QDEL_NULL(mimic_target) - return FALSE - var/mob/living/mimic = parent - var/drop_loc - if(!isnull(mimic_target)) - QDEL_NULL(tracker) - UnregisterFromTarget() - mimic_target.transfer_observers_to(parent) - drop_loc = mimic_target.drop_location() - if(!get_turf(drop_loc)) - drop_loc = mimic.loc - if(!QDELETED(mimic_target)) - QDEL_NULL(mimic_target) - - if(currently_disguised == UNDISGUISED) - return FALSE // Already undisguised - if(!isnull(drop_loc)) - mimic.abstract_move(drop_loc) - mimic.remove_traits(applied_mob_traits, REF(src)) - mimic.RemoveInvisibility(REF(src)) - currently_disguised = UNDISGUISED - return TRUE - -/* - * Signal handlers for the mob - */ -/datum/component/perfect_mimicry/proc/block_normal_movement(datum/source, atom/entering_loc) - SIGNAL_HANDLER - if(currently_disguised == DISGUISED) - return COMPONENT_MOVABLE_BLOCK_PRE_MOVE - -/datum/component/perfect_mimicry/proc/block_normal_clicks(datum/source, atom/target, list/modifiers) - SIGNAL_HANDLER - if(currently_disguised == DISGUISED) - return COMSIG_MOB_CANCEL_CLICKON - -/datum/component/perfect_mimicry/proc/mimic_target_deleted(datum/source, force) - SIGNAL_HANDLER - stop_mimicry() - if(!QDELETED(parent) && isliving(parent)) - var/mob/living/mimic = parent - for(var/datum/action/cooldown/mimic_ability/mimic_object/A in mimic.actions) - A.click_to_activate = TRUE - A.StartCooldown(A.cooldown_after_use) - -/* - * Signal handlers for the mimic_target - */ + action.Grant(src) -/* -/datum/component/perfect_mimicry/proc/on_take_damage(obj/target, damage_amt, danage_type, damage_flag, sound_effect, attack_dir, armour_penetration) - SIGNAL_HANDLER - to_chat(world, span_adminsay("Item took [damage_amt] damage")) -*/ - -/* - * Tracker callback - */ -/datum/component/perfect_mimicry/proc/sync_mimic_position(atom/movable/master, atom/mover, atom/oldloc, direction) - var/mob/living/mimic = parent - if(master.loc == oldloc) - return - - var/turf/newturf = get_turf(master) - if(!newturf) - mimic.abstract_move(oldloc) - QDEL_NULL(master) - return - - if(QDELETED(mimic) || mimic.loc == newturf) - return - - mimic.abstract_move(newturf) /* * Mimicry actions */ @@ -176,13 +10,7 @@ desc = "You should not be seeing this. This is an error alert developers." check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_INCAPACITATED -// These abilities require a perfect_mimicry component otherwise they are useless. -/datum/action/cooldown/mimic_ability/New(Target) - . = ..() - if(!istype(Target, /datum/component/perfect_mimicry)) - stack_trace("[name] ([type]) was instantiated on a non-perfect_mimicry target, this doesn't work.") - qdel(src) - return + var/cooldown_after_use = 3 SECONDS // Cooldown after mimicry ends /datum/action/cooldown/mimic_ability/mimic_object name = "Mimic Object" @@ -190,46 +18,102 @@ click_to_activate = TRUE cooldown_time = 1 SECOND - var/cooldown_after_use = 3 SECONDS // Cooldown after mimicry ends ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' -/datum/action/cooldown/mimic_ability/mimic_object/PreActivate(atom/target) - var/datum/component/perfect_mimicry/mimicker = src.target - if(!istype(mimicker)) - return - if(mimicker.currently_disguised == DISGUISED) + var/static/list/allowed_objects = list() // typecache of allowed objects to mimic + + COOLDOWN_DECLARE(move_cooldown) + var/obj/mimicked_object + var/obj/fake_storage + +/datum/action/cooldown/mimic_ability/mimic_object/PreActivate(atom/mimic_target) + if(!isnull(mimicked_object)) return ..() - if(target == owner) + if(mimic_target == owner) to_chat(owner, span_notice("You cannot mimic yourself.")) - return - //if(!isturf(target.loc, owner.loc)) // Prevent transformation in/from some inventory + return FALSE + //if(!isturf(mimic_target.loc, owner.loc)) // Prevent transformation in/from some inventory // return - if(get_dist(owner, target) > 2) - to_chat(owner, span_notice("[target.name] is too far away.")) - return - if(!mimicker.is_allowed_object(target)) - to_chat(owner, span_notice("[target.name] is too complex to mimic.")) - return + if(get_dist(owner, mimic_target) > 2) + to_chat(owner, span_notice("[mimic_target.name] is too far away.")) + return FALSE + if(!is_allowed_object(mimic_target)) + to_chat(owner, span_notice("[mimic_target.name] is too complex to mimic.")) + return FALSE return ..() -/datum/action/cooldown/mimic_ability/mimic_object/Activate(atom/target) - var/datum/component/perfect_mimicry/mimicker = src.target - if(!istype(mimicker)) +/datum/action/cooldown/mimic_ability/mimic_object/proc/is_allowed_object(obj/item/target_item) + if(!isitem(target_item)) return FALSE + if(length(allowed_objects) && !is_type_in_typecache(target_item, allowed_objects)) + return FALSE + return TRUE - if((mimicker.currently_disguised == DISGUISED) && mimicker.stop_mimicry()) +/datum/action/cooldown/mimic_ability/mimic_object/Activate(atom/mimic_target) + if(!isnull(mimicked_object)) + stop_mimicry() click_to_activate = TRUE StartCooldown(cooldown_after_use) return TRUE - - if((mimicker.currently_disguised == UNDISGUISED) && mimicker.start_mimicry(target)) + if(start_mimicry(mimic_target)) click_to_activate = FALSE StartCooldown() return TRUE - return FALSE -/datum/action/cooldown/mimic_ability/throw_self +/datum/action/cooldown/mimic_ability/mimic_object/proc/start_mimicry(obj/mimic_item) + mimicked_object = duplicate_object(mimic_item, get_turf(owner)) + RegisterSignal(mimicked_object, COMSIG_ATOM_RELAYMOVE, PROC_REF(on_user_move)) + RegisterSignal(mimicked_object, COMSIG_QDELETING, PROC_REF(on_object_qdel)) + owner.forceMove(mimicked_object) + mimicked_object.buckle_mob(owner) + if(mimicked_object.atom_storage) + fake_storage = new(src) + fake_storage.clone_storage(mimicked_object.atom_storage) + mimicked_object.atom_storage.set_real_location(fake_storage) + return mimicked_object + +/datum/action/cooldown/mimic_ability/mimic_object/proc/stop_mimicry() + owner.forceMove(mimicked_object.drop_location()) + mimicked_object.atom_storage.remove_all(mimicked_object.drop_location()) + if(fake_storage) + QDEL_NULL(fake_storage) + if(QDELETED(mimicked_object)) + mimicked_object = null + else + QDEL_NULL(mimicked_object) + +///The user can move while inside of the item. +/datum/action/cooldown/mimic_ability/mimic_object/proc/on_user_move(obj/moved_source, mob/user_moving, direction) + SIGNAL_HANDLER -#undef DISGUISED -#undef UNDISGUISED + if(!COOLDOWN_FINISHED(src, move_cooldown)) + return COMSIG_BLOCK_RELAYMOVE + var/turf/next = get_step(moved_source, direction) + var/turf/current = get_turf(moved_source) + if(!istype(next) || !istype(current)) + return COMSIG_BLOCK_RELAYMOVE + if(next.density) + return COMSIG_BLOCK_RELAYMOVE + if(!isturf(moved_source.loc)) + return COMSIG_BLOCK_RELAYMOVE + + step(moved_source, direction) + var/last_move_diagonal = ((direction & (direction - 1)) && (moved_source.loc == next)) + COOLDOWN_START(src, move_cooldown, ((last_move_diagonal ? 1 : 0.5)) SECOND) + + if(QDELETED(src)) + return COMSIG_BLOCK_RELAYMOVE + return TRUE + +///Called when the host of the mob is qdeleted. +/datum/action/cooldown/mimic_ability/mimic_object/proc/on_object_qdel(datum/source, force) + SIGNAL_HANDLER + stop_mimicry() + if(QDELETED(owner)) + return + for(var/datum/action/cooldown/mimic_ability/mimic_abilities in owner.actions) + mimic_abilities.click_to_activate = TRUE + mimic_abilities.StartCooldown(mimic_abilities.cooldown_after_use) + +/datum/action/cooldown/mimic_ability/throw_self From a4aca3a33df66a882f601e7f3c98ab27fe84e7b2 Mon Sep 17 00:00:00 2001 From: Siro Date: Tue, 3 Feb 2026 10:31:12 -0700 Subject: [PATCH 09/15] Stubbed in damage reflection, atom integrity adjustment for weight class, specific setup for mimicking nuclear authentication disk, switched to duplicate)object, mob speech redirected to mimicked object, add a banned object list, use integrity check, mob traits added/removed using list. --- code/datums/components/perfect_mimicry.dm | 67 +++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 05bbb721a3b4a..c756dcd34114d 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -1,3 +1,5 @@ +#define INTEGRITY_PER_WCLASS 10 + /mob/living/proc/grant_mimicry() var/datum/action/cooldown/mimic_ability/mimic_object/action = new(src) action.Grant(src) @@ -21,19 +23,60 @@ ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' var/static/list/allowed_objects = list() // typecache of allowed objects to mimic + var/static/list/banned_objects = list() // typecache of banned objects that should absolutely not be mimicked + var/list/applied_mob_traits = list(TRAIT_HANDS_BLOCKED, TRAIT_UI_BLOCKED, TRAIT_PULL_BLOCKED, TRAIT_NOBREATH) COOLDOWN_DECLARE(move_cooldown) var/obj/mimicked_object var/obj/fake_storage +/datum/action/cooldown/mimic_ability/mimic_object/proc/block_abilities() + SIGNAL_HANDLER + +// Item adjustments for specific cases. +/datum/action/cooldown/mimic_ability/mimic_object/proc/handle_mimic_target(obj/item/target_item) + if(!isitem(target_item)) + return + var/obj/item/new_item + if(istype(target_item, /obj/item/disk/nuclear)) // Can mimic disk but it is fake and can be destroyed. + var/obj/item/disk/nuclear/fake/nuclear = new(owner.drop_location()) + nuclear.resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF + new_item = nuclear + if(!istype(new_item)) + new_item = duplicate_object(target_item, get_turf(owner)) + + if(new_item.uses_integrity) // Mimicked items can break easier + var/weight_multiplier = max(1, new_item.w_class) + var/adjusted_integrity = 5 + (weight_multiplier * INTEGRITY_PER_WCLASS) + new_item.modify_max_integrity(clamp(adjusted_integrity, 10, 60)) + + return new_item + +/datum/action/cooldown/mimic_ability/mimic_object/proc/reflect_damage(datum/source, damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armour_penetration) + SIGNAL_HANDLER + if(!damage_amount) + return + +/datum/action/cooldown/mimic_ability/mimic_object/proc/handle_speech(datum/source, list/speech_args) + SIGNAL_HANDLER + + if(QDELETED(mimicked_object)) // Return early and prevent speech. Object cleanup should be happening. + speech_args[SPEECH_MESSAGE] = "" + return + + mimicked_object.say(speech_args[SPEECH_MESSAGE], speech_args[SPEECH_BUBBLE_TYPE], \ + speech_args[SPEECH_SPANS], speech_args[SPEECH_SANITIZE], speech_args[SPEECH_LANGUAGE], \ + speech_args[SPEECH_IGNORE_SPAM], speech_args[SPEECH_FORCED], speech_args[SPEECH_FILTERPROOF], \ + speech_args[SPEECH_RANGE], speech_args[SPEECH_SAYMODE]) + + speech_args[SPEECH_MESSAGE] = "" + /datum/action/cooldown/mimic_ability/mimic_object/PreActivate(atom/mimic_target) if(!isnull(mimicked_object)) return ..() if(mimic_target == owner) to_chat(owner, span_notice("You cannot mimic yourself.")) return FALSE - //if(!isturf(mimic_target.loc, owner.loc)) // Prevent transformation in/from some inventory - // return if(get_dist(owner, mimic_target) > 2) to_chat(owner, span_notice("[mimic_target.name] is too far away.")) return FALSE @@ -45,6 +88,10 @@ /datum/action/cooldown/mimic_ability/mimic_object/proc/is_allowed_object(obj/item/target_item) if(!isitem(target_item)) return FALSE + if(!target_item.uses_integrity) + return FALSE + if(length(banned_objects) && is_type_in_typecache(target_item, banned_objects)) + return FALSE if(length(allowed_objects) && !is_type_in_typecache(target_item, allowed_objects)) return FALSE return TRUE @@ -62,11 +109,17 @@ return FALSE /datum/action/cooldown/mimic_ability/mimic_object/proc/start_mimicry(obj/mimic_item) - mimicked_object = duplicate_object(mimic_item, get_turf(owner)) + mimicked_object = handle_mimic_target(mimic_item) + if(isnull(mimicked_object)) + return RegisterSignal(mimicked_object, COMSIG_ATOM_RELAYMOVE, PROC_REF(on_user_move)) + RegisterSignal(mimicked_object, COMSIG_ATOM_TAKE_DAMAGE, PROC_REF(reflect_damage)) RegisterSignal(mimicked_object, COMSIG_QDELETING, PROC_REF(on_object_qdel)) + RegisterSignal(owner, COMSIG_MOB_SAY, PROC_REF(handle_speech)) owner.forceMove(mimicked_object) mimicked_object.buckle_mob(owner) + if(length(applied_mob_traits)) + owner.add_traits(applied_mob_traits, REF(src)) if(mimicked_object.atom_storage) fake_storage = new(src) fake_storage.clone_storage(mimicked_object.atom_storage) @@ -75,7 +128,11 @@ /datum/action/cooldown/mimic_ability/mimic_object/proc/stop_mimicry() owner.forceMove(mimicked_object.drop_location()) - mimicked_object.atom_storage.remove_all(mimicked_object.drop_location()) + UnregisterSignal(owner, COMSIG_MOB_SAY) + if(mimicked_object.atom_storage) + mimicked_object.atom_storage.remove_all(mimicked_object.drop_location()) + if(length(applied_mob_traits)) + owner.remove_traits(applied_mob_traits, REF(src)) if(fake_storage) QDEL_NULL(fake_storage) if(QDELETED(mimicked_object)) @@ -117,3 +174,5 @@ mimic_abilities.StartCooldown(mimic_abilities.cooldown_after_use) /datum/action/cooldown/mimic_ability/throw_self + +#undef INTEGRITY_PER_WCLASS From 074631bbcd5a27008e64f326c4cbd55e2abe2693 Mon Sep 17 00:00:00 2001 From: Siro Date: Fri, 27 Feb 2026 18:13:43 -0700 Subject: [PATCH 10/15] Spoof resistance flags component, remove mimicry proc, spawned disk deletes, --- code/datums/components/mimic_disguise.dm | 35 +++++++++++++++++++++++ code/datums/components/perfect_mimicry.dm | 10 +++++-- code/game/objects/items.dm | 13 +++++---- tgstation.dme | 1 + 4 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 code/datums/components/mimic_disguise.dm diff --git a/code/datums/components/mimic_disguise.dm b/code/datums/components/mimic_disguise.dm new file mode 100644 index 0000000000000..2055f9e0488af --- /dev/null +++ b/code/datums/components/mimic_disguise.dm @@ -0,0 +1,35 @@ +#define MIMIC_SPOOF_RESIST_MASK ( \ + LAVA_PROOF | \ + FIRE_PROOF | \ + UNACIDABLE | \ + ACID_PROOF | \ + INDESTRUCTIBLE \ +) +/datum/component/mimic_disguise + dupe_mode = COMPONENT_DUPE_UNIQUE + var/spoofed_flags = 0 + +/datum/component/mimic_disguise/Initialize() + . = ..() + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + var/obj/item/disguise_item = parent + var/to_spoof = disguise_item.resistance_flags & MIMIC_SPOOF_RESIST_MASK + + if(to_spoof) + spoofed_flags = to_spoof + disguise_item.resistance_flags &= ~to_spoof + + if(!(disguise_item.obj_flags & CAN_BE_HIT)) // Mimics are attackable. + disguise_item.obj_flags |= CAN_BE_HIT + +/datum/component/mimic_disguise/Destroy(force) + if(QDELETED(parent)) + return ..() + var/obj/item/disguise_item = parent + if(spoofed_flags) + disguise_item.resistance_flags |= spoofed_flags + if(!(initial(disguise_item.obj_flags) & CAN_BE_HIT)) + disguise_item.obj_flags &= ~CAN_BE_HIT + return ..() +#undef MIMIC_SPOOF_RESIST_MASK diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index c756dcd34114d..555ac32ea30a2 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -4,6 +4,10 @@ var/datum/action/cooldown/mimic_ability/mimic_object/action = new(src) action.Grant(src) +/mob/living/proc/remove_mimicry() + for(var/datum/action/cooldown/mimic_ability/mimic_object/action in src.actions) + action.Remove(src) + /* * Mimicry actions */ @@ -40,11 +44,13 @@ var/obj/item/new_item if(istype(target_item, /obj/item/disk/nuclear)) // Can mimic disk but it is fake and can be destroyed. var/obj/item/disk/nuclear/fake/nuclear = new(owner.drop_location()) - nuclear.resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF + var/datum/component/stationloving/stationcomp = nuclear.GetComponent(/datum/component/stationloving) + if(stationcomp) + stationcomp.allow_item_destruction = TRUE new_item = nuclear if(!istype(new_item)) new_item = duplicate_object(target_item, get_turf(owner)) - + new_item.AddComponent(/datum/component/mimic_disguise) if(new_item.uses_integrity) // Mimicked items can break easier var/weight_multiplier = max(1, new_item.w_class) var/adjusted_integrity = 5 + (weight_multiplier * INTEGRITY_PER_WCLASS) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 2bc94b64da9a8..8057422e115bd 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -447,16 +447,19 @@ . += "[gender == PLURAL ? "They are" : "It is"] a [weight_class_to_text(w_class)] item." - if(resistance_flags & INDESTRUCTIBLE) + // Check for mimic disguise component + var/datum/component/mimic_disguise/fake_item = GetComponent(/datum/component/mimic_disguise) + var/flags_to_check = resistance_flags | (fake_item ? fake_item.spoofed_flags : 0) + if(flags_to_check & INDESTRUCTIBLE) . += "[src] seems extremely robust! It'll probably withstand anything that could happen to it!" else - if(resistance_flags & LAVA_PROOF) + if(flags_to_check & LAVA_PROOF) . += "[src] is made of an extremely heat-resistant material, it'd probably be able to withstand lava!" - if(resistance_flags & (ACID_PROOF | UNACIDABLE)) + if(flags_to_check & (ACID_PROOF | UNACIDABLE)) . += "[src] looks pretty robust! It'd probably be able to withstand acid!" - if(resistance_flags & FREEZE_PROOF) + if(flags_to_check & FREEZE_PROOF) . += "[src] is made of cold-resistant materials." - if(resistance_flags & FIRE_PROOF) + if(flags_to_check & FIRE_PROOF) . += "[src] is made of fire-retardant materials." return diff --git a/tgstation.dme b/tgstation.dme index 901305e04175a..3b0296e2a128e 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1260,6 +1260,7 @@ #include "code\datums\components\manual_blinking.dm" #include "code\datums\components\manual_breathing.dm" #include "code\datums\components\material_bane.dm" +#include "code\datums\components\mimic_disguise.dm" #include "code\datums\components\mind_linker.dm" #include "code\datums\components\mirage_border.dm" #include "code\datums\components\mirv.dm" From c7db1b597d6987be6eb3ec3cdbb9b6336ab7a5c2 Mon Sep 17 00:00:00 2001 From: Siro Date: Fri, 27 Feb 2026 18:21:14 -0700 Subject: [PATCH 11/15] Change to drop location. Prevent mimic while vent crawling. --- code/datums/components/perfect_mimicry.dm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 555ac32ea30a2..3c2afde86f5f6 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -49,7 +49,7 @@ stationcomp.allow_item_destruction = TRUE new_item = nuclear if(!istype(new_item)) - new_item = duplicate_object(target_item, get_turf(owner)) + new_item = duplicate_object(target_item, owner.drop_location()) new_item.AddComponent(/datum/component/mimic_disguise) if(new_item.uses_integrity) // Mimicked items can break easier var/weight_multiplier = max(1, new_item.w_class) @@ -89,6 +89,9 @@ if(!is_allowed_object(mimic_target)) to_chat(owner, span_notice("[mimic_target.name] is too complex to mimic.")) return FALSE + if(movement_type & VENTCRAWLING) + to_chat(owner, span_notice("You cannot mimic objects while ventcrawling.")) + return FALSE return ..() /datum/action/cooldown/mimic_ability/mimic_object/proc/is_allowed_object(obj/item/target_item) From 057e708643d2354827f2b078b2d3d1062e95d1d2 Mon Sep 17 00:00:00 2001 From: Siro Date: Fri, 27 Feb 2026 18:47:45 -0700 Subject: [PATCH 12/15] oops. Linters thank you. --- code/datums/components/perfect_mimicry.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 3c2afde86f5f6..0ff2db973f780 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -89,7 +89,7 @@ if(!is_allowed_object(mimic_target)) to_chat(owner, span_notice("[mimic_target.name] is too complex to mimic.")) return FALSE - if(movement_type & VENTCRAWLING) + if(owner.movement_type & VENTCRAWLING) to_chat(owner, span_notice("You cannot mimic objects while ventcrawling.")) return FALSE return ..() From 4ac4391175912f62ad546d1c2c1989985dc9429e Mon Sep 17 00:00:00 2001 From: Siro Date: Fri, 27 Feb 2026 18:52:20 -0700 Subject: [PATCH 13/15] Why don't these things complain on compile UGH --- code/datums/components/mimic_disguise.dm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/code/datums/components/mimic_disguise.dm b/code/datums/components/mimic_disguise.dm index 2055f9e0488af..efdb7cbb7b8c9 100644 --- a/code/datums/components/mimic_disguise.dm +++ b/code/datums/components/mimic_disguise.dm @@ -1,9 +1,9 @@ #define MIMIC_SPOOF_RESIST_MASK ( \ - LAVA_PROOF | \ - FIRE_PROOF | \ - UNACIDABLE | \ - ACID_PROOF | \ - INDESTRUCTIBLE \ + LAVA_PROOF | \ + FIRE_PROOF | \ + UNACIDABLE | \ + ACID_PROOF | \ + INDESTRUCTIBLE \ ) /datum/component/mimic_disguise dupe_mode = COMPONENT_DUPE_UNIQUE From 8d4160f4bca92c7f3c68bd23239675ccdd8ea70a Mon Sep 17 00:00:00 2001 From: Siro Date: Wed, 4 Mar 2026 23:20:46 -0700 Subject: [PATCH 14/15] Add preattack check for mimic_disguise and for atom_storage base_item_interaction so storage can be hit in harm intent and prevent item hits. Increasted mimicry range to 3. Added several banned objects, remove outline effect on mimicking inventory items and reflect damage from item hit to mimicking mob. --- code/_onclick/item_attack.dm | 2 ++ code/datums/components/perfect_mimicry.dm | 16 ++++++++++++---- code/game/atoms/atom_tool_acts.dm | 2 ++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 1940b66bebedb..321485be3e3fa 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -110,6 +110,8 @@ /obj/item/proc/pre_attack(atom/target, mob/living/user, list/modifiers, list/attack_modifiers) //do stuff before attackby! if(SEND_SIGNAL(src, COMSIG_ITEM_PRE_ATTACK, target, user, modifiers, attack_modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN) return TRUE + if(target.GetComponent(/datum/component/mimic_disguise) && !(user?.istate & (ISTATE_HARM))) + return TRUE // Mimicked objects should not be attacked when not trying to harm them. return FALSE //return TRUE to avoid calling attackby after this proc does stuff /** diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 0ff2db973f780..251fb94436046 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -27,7 +27,9 @@ ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' var/static/list/allowed_objects = list() // typecache of allowed objects to mimic - var/static/list/banned_objects = list() // typecache of banned objects that should absolutely not be mimicked + var/static/list/banned_objects = list(/obj/item/folder/biscuit, /obj/item/modular_computer, /obj/item/card, \ + /obj/item/holochip, /obj/item/stack + ) // typecache of banned objects that should absolutely not be mimicked var/list/applied_mob_traits = list(TRAIT_HANDS_BLOCKED, TRAIT_UI_BLOCKED, TRAIT_PULL_BLOCKED, TRAIT_NOBREATH) COOLDOWN_DECLARE(move_cooldown) @@ -50,6 +52,9 @@ new_item = nuclear if(!istype(new_item)) new_item = duplicate_object(target_item, owner.drop_location()) + if(!new_item.loc?.atom_storage) + new_item.item_flags &= ~(IN_INVENTORY | IN_STORAGE) // Prevent hover outline when mimicking inventory items. + new_item.remove_filter(HOVER_OUTLINE_FILTER) new_item.AddComponent(/datum/component/mimic_disguise) if(new_item.uses_integrity) // Mimicked items can break easier var/weight_multiplier = max(1, new_item.w_class) @@ -62,6 +67,9 @@ SIGNAL_HANDLER if(!damage_amount) return + if(isliving(owner)) + var/mob/living/living_owner = owner + living_owner.apply_damage(damage_amount, damage_type, null, 0, FALSE, TRUE, 0, 0, NONE, attack_dir) /datum/action/cooldown/mimic_ability/mimic_object/proc/handle_speech(datum/source, list/speech_args) SIGNAL_HANDLER @@ -83,7 +91,7 @@ if(mimic_target == owner) to_chat(owner, span_notice("You cannot mimic yourself.")) return FALSE - if(get_dist(owner, mimic_target) > 2) + if(get_dist(owner, mimic_target) > 3) to_chat(owner, span_notice("[mimic_target.name] is too far away.")) return FALSE if(!is_allowed_object(mimic_target)) @@ -99,9 +107,9 @@ return FALSE if(!target_item.uses_integrity) return FALSE - if(length(banned_objects) && is_type_in_typecache(target_item, banned_objects)) + if(length(banned_objects) && is_type_in_list(target_item, banned_objects)) return FALSE - if(length(allowed_objects) && !is_type_in_typecache(target_item, allowed_objects)) + if(length(allowed_objects) && !is_type_in_list(target_item, allowed_objects)) return FALSE return TRUE diff --git a/code/game/atoms/atom_tool_acts.dm b/code/game/atoms/atom_tool_acts.dm index a73bdd64da4da..3902772d0bd18 100644 --- a/code/game/atoms/atom_tool_acts.dm +++ b/code/game/atoms/atom_tool_acts.dm @@ -51,6 +51,8 @@ // We have to manually handle storage in item_interaction because storage is blocking in 99% of interactions, which stifles a lot // Yeah it sucks not being able to signalize this, but the other option is to have a second signal here just for storage which is also not great if(atom_storage) + if(src.GetComponent(/datum/component/mimic_disguise) && user?.istate & (ISTATE_HARM)) + return NONE // Mimicked storages should be attackable when hit with a tool. if(is_left_clicking) if(atom_storage.insert_on_attack) return atom_storage.item_interact_insert(user, tool) From 47dcf89870e5db0947aab88638865bfcd02958a9 Mon Sep 17 00:00:00 2001 From: Siro Date: Wed, 4 Mar 2026 23:48:37 -0700 Subject: [PATCH 15/15] Not really sure where or how to put this. Assuming this will just be on the ground. --- code/datums/components/perfect_mimicry.dm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/datums/components/perfect_mimicry.dm b/code/datums/components/perfect_mimicry.dm index 251fb94436046..5c6719205690f 100644 --- a/code/datums/components/perfect_mimicry.dm +++ b/code/datums/components/perfect_mimicry.dm @@ -52,9 +52,8 @@ new_item = nuclear if(!istype(new_item)) new_item = duplicate_object(target_item, owner.drop_location()) - if(!new_item.loc?.atom_storage) - new_item.item_flags &= ~(IN_INVENTORY | IN_STORAGE) // Prevent hover outline when mimicking inventory items. - new_item.remove_filter(HOVER_OUTLINE_FILTER) + new_item.remove_filter(HOVER_OUTLINE_FILTER) + new_item.item_flags &= ~(IN_INVENTORY | IN_STORAGE) // Prevent hover outline when mimicking inventory items. new_item.AddComponent(/datum/component/mimic_disguise) if(new_item.uses_integrity) // Mimicked items can break easier var/weight_multiplier = max(1, new_item.w_class)