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/__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/_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/mimic_disguise.dm b/code/datums/components/mimic_disguise.dm new file mode 100644 index 0000000000000..efdb7cbb7b8c9 --- /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 new file mode 100644 index 0000000000000..5c6719205690f --- /dev/null +++ b/code/datums/components/perfect_mimicry.dm @@ -0,0 +1,194 @@ +#define INTEGRITY_PER_WCLASS 10 + +/mob/living/proc/grant_mimicry() + 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 + */ +/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 + + var/cooldown_after_use = 3 SECONDS // Cooldown after mimicry ends + +/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." + + click_to_activate = TRUE + cooldown_time = 1 SECOND + 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(/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) + 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()) + 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, owner.drop_location()) + 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) + 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 + 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 + + 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(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)) + to_chat(owner, span_notice("[mimic_target.name] is too complex to mimic.")) + return FALSE + if(owner.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) + if(!isitem(target_item)) + return FALSE + if(!target_item.uses_integrity) + return FALSE + if(length(banned_objects) && is_type_in_list(target_item, banned_objects)) + return FALSE + if(length(allowed_objects) && !is_type_in_list(target_item, allowed_objects)) + return FALSE + return TRUE + +/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(start_mimicry(mimic_target)) + click_to_activate = FALSE + StartCooldown() + return TRUE + return FALSE + +/datum/action/cooldown/mimic_ability/mimic_object/proc/start_mimicry(obj/mimic_item) + 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) + 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()) + 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)) + 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 + + 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 + +#undef INTEGRITY_PER_WCLASS 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) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index dd5fad237e677..49a821748dd0c 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 08a8499d0ce3b..282c61b4bf7a2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1270,6 +1270,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" @@ -1287,6 +1288,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"