diff --git a/code/__DEFINES/dcs/datum_signals.dm b/code/__DEFINES/dcs/datum_signals.dm index c1ca2c4701a..6151c8bd003 100644 --- a/code/__DEFINES/dcs/datum_signals.dm +++ b/code/__DEFINES/dcs/datum_signals.dm @@ -29,6 +29,8 @@ #define COMSIG_BODY_TRANSFER_TO "body_transfer_to" ///called when the mind is initialized (called every time the mob logins) #define COMSIG_MIND_INITIALIZE "mind_initialize" +/// called when character creation robotic limbs are applied +#define COMSIG_HUMAN_ROBOTIC_LIMBS_APPLIED "human_robotic_limbs_applied" // Sent from a surgery step when blood is being splashed. (datum/surgery, mob/user, mob/target, zone, obj/item/tool) #define COMSIG_SURGERY_BLOOD_SPLASH "surgery_blood_splash" diff --git a/code/datums/uplink_items/uplink_traitor.dm b/code/datums/uplink_items/uplink_traitor.dm index 58cbb8bde10..e2e8df793c6 100644 --- a/code/datums/uplink_items/uplink_traitor.dm +++ b/code/datums/uplink_items/uplink_traitor.dm @@ -663,6 +663,16 @@ cost = 40 excludefrom = list(UPLINK_TYPE_NUCLEAR, UPLINK_TYPE_SST) //No, nukies do not get to dodge bullets. +/datum/uplink_item/species_restricted/skinmonger + name = "Skinmonger Autoimplanter" + desc = "A strange implant that continuously fabricates synthetic epidermis, covering up prosthetics. \ + When implanted, the Skinmonger will bond to its host, first covering every limb in synthetic skin, then replacing destroyed skin periodically. \ + IPCs can use this implant to disguise themselves as human. However, it will not cover monitor-shaped heads." + reference = "SKINMON" + item = /obj/item/autosurgeon/organ/syndicate/oneuse/skinmonger + cost = 10 + species = list("Machine") + /datum/uplink_item/badass/syndiecards name = "Syndicate Playing Cards" desc = "A special deck of space-grade playing cards with a mono-molecular edge and metal reinforcement, making them lethal weapons both when wielded as a blade and when thrown. \ diff --git a/code/game/objects/items/tools/epidermal_applicator.dm b/code/game/objects/items/tools/epidermal_applicator.dm new file mode 100644 index 00000000000..f1a95bf5d5a --- /dev/null +++ b/code/game/objects/items/tools/epidermal_applicator.dm @@ -0,0 +1,209 @@ +/obj/item/epidermal_applicator + name = "epidermal applicator" + desc = "A pen-shaped device developed by Zeng-Hu Pharmaceuticals and used to apply synthetic skin to prosthetic limbs." + icon = 'icons/obj/surgery.dmi' + icon_state = "skin_applicator" + origin_tech = "biotech=5;materials=3;engineering=4" + new_attack_chain = TRUE + + /// Current amount of metal stored in the applicator + var/metal_stored = 0 + /// Maximum metal that can be stored + var/max_metal_stored = 5 + /// Metal required per application + var/metal_per_use = 5 + + var/applying = FALSE + +/obj/item/epidermal_applicator/Initialize(mapload) + . = ..() + // Start with empty storage + metal_stored = 0 + +/obj/item/epidermal_applicator/examine(mob/user) + . = ..() + + if(metal_stored >= metal_per_use) + . += SPAN_NOTICE("It is loaded and ready to apply an epidermal layer to a body part.") + else + . += SPAN_NOTICE("It needs [metal_per_use - metal_stored] more metal sheets.") + +/obj/item/epidermal_applicator/proc/is_insertion_ready(mob/user) + if(!user) + return FALSE + return TRUE + +/obj/item/epidermal_applicator/item_interaction(mob/living/user, obj/item/used, list/modifiers) + if(!istype(used, /obj/item/stack/sheet/metal)) + return NONE + + var/space_left = max_metal_stored - metal_stored + if(space_left <= 0) + to_chat(user, SPAN_NOTICE("[src] is already full!")) + return ITEM_INTERACT_COMPLETE + + var/obj/item/stack/sheet/metal/M = used + var/to_load = min(space_left, M.amount) + M.use(to_load) + metal_stored += to_load + + to_chat(user, SPAN_NOTICE("You load [to_load] sheet\s of metal into [src].")) + return ITEM_INTERACT_COMPLETE + +/obj/item/epidermal_applicator/activate_self(mob/user) + . = ..() + attack(user, user, null) + +/obj/item/epidermal_applicator/attack(mob/living/M, mob/living/user, params) + . = ..(M, user, params) + if(.) + return . + + if(!ishuman(M)) + return + + var/def_zone = user.zone_selected + if(!def_zone) + to_chat(user, SPAN_WARNING("You need to select a body part first!")) + return TRUE + + var/mob/living/carbon/human/target = M + var/obj/item/organ/external/affected = target.get_organ(def_zone) + + if(!affected) + to_chat(user, SPAN_WARNING("[target] doesn't have a [parse_zone(def_zone)]!")) + return TRUE + + // Show these identically so it can't be used to test whether a limb is synthetic or not. + if(!affected.is_robotic() || affected.has_synthetic_skin) + to_chat(user, SPAN_WARNING("The [affected.name] doesn't need skin.")) + return TRUE + + // Do not put skin on a monitor. No. + if(ismachineperson(target) && def_zone == BODY_ZONE_HEAD && affected.model) + var/datum/robolimb/R = GLOB.all_robolimbs[affected.model] + if(R && R.is_monitor) + to_chat(user, SPAN_WARNING("The applicator fails to find purchase on your big cube head. Probably for the best.")) + return TRUE + + // Check if we have enough metal + if(metal_stored < metal_per_use) + to_chat(user, SPAN_WARNING("[src] needs [metal_per_use] metal to function.")) + return TRUE + + if(applying) + to_chat(user, SPAN_WARNING("[src] is already in use!")) + return TRUE + + // Start application process + apply_synthetic_skin(target, affected, user, def_zone) + return TRUE + +/obj/item/epidermal_applicator/proc/apply_synthetic_skin(mob/living/carbon/human/target, obj/item/organ/external/affected, mob/living/user, def_zone) + applying = TRUE + + var/chosen_identity = "Unknown" // Default identity + + // Ask for identity choice if applying to head + if(def_zone == BODY_ZONE_HEAD) + var/list/identity_choices = list("Unknown") + var/list/choice_map = list("Unknown" = "Unknown") + + // Add real name option + if(target.dna?.real_name) + var/real_name_choice = "[target.dna.real_name]" + identity_choices += real_name_choice + choice_map[real_name_choice] = target.dna.real_name + + // Check for offhand ID + var/obj/item/offhand_item = user.get_inactive_hand() + var/offhand_id_name = null + if(istype(offhand_item, /obj/item/card/id)) + var/obj/item/card/id/id_card = offhand_item + offhand_id_name = id_card.registered_name + else if(is_pda(offhand_item)) + var/obj/item/pda/pda = offhand_item + offhand_id_name = pda.owner + + // Add ID name option if found + if(offhand_id_name && offhand_id_name != "") + var/id_name_choice = "[offhand_id_name]" + identity_choices += id_name_choice + choice_map[id_name_choice] = offhand_id_name + + // Present choice to user + var/chosen_identity_option = tgui_input_list(user, "Choose facial identity:", "Identity Selection", identity_choices) + if(!chosen_identity_option) + applying = FALSE + return // Cancel if no choice made + + chosen_identity = choice_map[chosen_identity_option] + + user.visible_message( + SPAN_NOTICE("[user] begins applying synthetic skin to [target == user ? "their" : "[target]'s"] [affected.name] with [src]."), + SPAN_NOTICE("You begin applying synthetic skin to [target == user ? "your" : "[target]'s"] [affected.name]...") + ) + + // Play start sound + playsound(get_turf(src), 'sound/effects/spray.ogg', 50, TRUE) + + // 10 second do-after + if(do_after(user, 10 SECONDS, target = target)) + // Play end sound + playsound(get_turf(src), 'sound/effects/spray3.ogg', 50, TRUE) + + // Consume metal + metal_stored -= metal_per_use + + // Apply synthetic skin + affected.has_synthetic_skin = TRUE + + // Apply owner skin color to synthetic skin + if(ishuman(target)) + affected.synthetic_skin_colour = target.skin_colour + + // Set identity if applying to head (we already got the choice above) + if(def_zone == BODY_ZONE_HEAD) + affected.synthetic_skin_identity = chosen_identity + if(ishuman(target)) + target.real_name = chosen_identity + + // Clear force_icon so it unsticks from the robotic sprites + affected.force_icon = null + + // Apply to connected torso parts as well + if(def_zone == BODY_ZONE_CHEST) + var/obj/item/organ/external/groin_limb = target.bodyparts_by_name["groin"] + if(groin_limb && groin_limb.is_robotic() && !groin_limb.has_synthetic_skin) + groin_limb.has_synthetic_skin = TRUE + groin_limb.synthetic_skin_colour = target.skin_colour + groin_limb.force_icon = null + groin_limb.mob_icon = null + groin_limb.compile_icon() + + if(def_zone == BODY_ZONE_PRECISE_GROIN) + var/obj/item/organ/external/chest_limb = target.bodyparts_by_name["chest"] + if(chest_limb && chest_limb.is_robotic() && !chest_limb.has_synthetic_skin) + chest_limb.has_synthetic_skin = TRUE + chest_limb.synthetic_skin_colour = target.skin_colour + chest_limb.force_icon = null + chest_limb.mob_icon = null + chest_limb.compile_icon() + + // Force the organ to completely regenerate its mob_icon + affected.mob_icon = null + affected.compile_icon() + + // Force complete body rebuild to bypass icon cache + target.update_body(rebuild_base = TRUE) + target.UpdateDamageIcon() + + user.visible_message( + SPAN_NOTICE("[user] applies synthetic skin to [target == user ? "their" : "[target]'s"] [affected.name]."), + SPAN_NOTICE("You apply synthetic skin to [target == user ? "your" : "[target]'s"] [affected.name].") + ) + + if(target != user) + to_chat(target, SPAN_NOTICE("You feel a thin layer of synthetic skin form over your [affected.name].")) + + applying = FALSE diff --git a/code/modules/client/preference/character.dm b/code/modules/client/preference/character.dm index 3f24f1b8a88..79d1b0584ac 100644 --- a/code/modules/client/preference/character.dm +++ b/code/modules/client/preference/character.dm @@ -1964,6 +1964,9 @@ if(status == "cybernetic") I.robotize() + // Send signal that robotic limbs have been applied + SEND_SIGNAL(character, COMSIG_HUMAN_ROBOTIC_LIMBS_APPLIED) + character.dna.blood_type = b_type // Wheelchair necessary? diff --git a/code/modules/client/preference/quirks/positive_quirks.dm b/code/modules/client/preference/quirks/positive_quirks.dm index ac1893b7615..edcc2f51516 100644 --- a/code/modules/client/preference/quirks/positive_quirks.dm +++ b/code/modules/client/preference/quirks/positive_quirks.dm @@ -36,6 +36,50 @@ trait_to_apply = TRAIT_GLUTTON species_flags = QUIRK_MACHINE_INCOMPATIBLE +/datum/quirk/lifelike + name = "Lifelike" + desc = "Your prosthetic limbs have been fitted with a synthetic epidermis, making them appear natural. \ + For IPCs, this covers all body parts, making them look human (except monitor-shaped heads). \ + For all others, it covers prosthetic limbs." + cost = 4 + +/datum/quirk/lifelike/apply_quirk_effects(mob/living/carbon/human/target, character) + . = ..(target, character) + // Apply synthetic skin after robotic limbs are applied or the quirk doesn't work very well + RegisterSignal(target, COMSIG_HUMAN_ROBOTIC_LIMBS_APPLIED, PROC_REF(apply_synthetic_skin_on_signal)) + +/datum/quirk/lifelike/proc/apply_synthetic_skin_on_signal(mob/living/carbon/human/target) + SIGNAL_HANDLER // COMSIG_HUMAN_ROBOTIC_LIMBS_APPLIED + + for(var/obj/item/organ/external/limb as anything in target.bodyparts) + if(!limb) + continue + + // Skip monitor heads + if(limb.limb_name == "head" && limb.model) + var/datum/robolimb/R = GLOB.all_robolimbs[limb.model] + if(R && R.is_monitor) + continue + + if(ismachineperson(target) || limb.is_robotic()) + limb.has_synthetic_skin = TRUE + // Apply owner's skin color to synthetic skin + limb.synthetic_skin_colour = target.skin_colour + // Set real identity for head + if(limb.limb_name == "head") + limb.synthetic_skin_identity = target.dna.real_name + // Clear cached limb icon because otherwise it's sticky + limb.force_icon = null + // Force mob icon regeneration + limb.mob_icon = null + limb.compile_icon() + + // Now rebuild appearance + target.update_body(rebuild_base = TRUE) + + // Unregister the signal since we're done with it + UnregisterSignal(target, COMSIG_HUMAN_ROBOTIC_LIMBS_APPLIED) + /obj/item/storage/box/papersack/prepped_meal name = "packed meal" var/list/entree_options = list( diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 3070f33a27d..f8d85668010 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -409,6 +409,11 @@ emp_act update_hair() update_fhair() + if(affecting.has_synthetic_skin) + visible_message(SPAN_WARNING("The synthetic skin on [src]'s [affecting.name] bubbles and melts away."), \ + SPAN_WARNING("The synthetic skin on your [affecting.name] bubbles and melts away.")) + affecting.remove_synthetic_skin(TRUE) + UpdateDamageIcon() //MELTING INVENTORY ITEMS// diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index 4b3e790f906..6ef19c1b14f 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -116,6 +116,8 @@ var/list/quirks = list() /// The cooldown for jumping into a closet or crate COOLDOWN_DECLARE(skittish_cooldown) + /// Cache whether or not an IPC appears human during examine to avoid needless recalculation + var/ipc_masquerade_status /mob/living/carbon/human/fake flags = ABSTRACT diff --git a/code/modules/mob/living/carbon/human/human_examine.dm b/code/modules/mob/living/carbon/human/human_examine.dm index c027ca61f31..f25a8e4af18 100644 --- a/code/modules/mob/living/carbon/human/human_examine.dm +++ b/code/modules/mob/living/carbon/human/human_examine.dm @@ -53,6 +53,12 @@ if(C.species_disguise) displayed_species = C.species_disguise break + + // If an IPC's covered in synthetic skin, they can appear human. + if(calculate_ipc_masquerade_status()) + displayed_species = "Human" + examine_color = "#d1aa2e" + if(skip_jumpsuit && skip_face || HAS_TRAIT(src, TRAIT_NOEXAMINE)) //either obscured or on the nospecies list msg += "!" //omit the species when examining else @@ -122,7 +128,7 @@ continue if(!ismachineperson(src)) - if(E.is_robotic()) + if(E.is_robotic() && !E.has_synthetic_skin) wound_flavor_text["[E.limb_name]"] = "[p_they(TRUE)] [p_have()] a robotic [E.name]!\n" else if(E.status & ORGAN_SPLINTED) @@ -256,5 +262,61 @@ return msg +/mob/living/carbon/human/proc/calculate_ipc_masquerade_status() + if(!ismachineperson(src)) + return FALSE + + var/all_visible_parts_have_skin = TRUE + + for(var/obj/item/organ/external/limb as anything in bodyparts) + if(!limb || !limb.is_robotic()) + continue + + // If it's covered by clothing then it doesn't need to have skin for the masquerade + if(is_bodypart_covered_by_clothing(limb.limb_name)) + continue + + if(!limb.has_synthetic_skin) + return FALSE + + return all_visible_parts_have_skin + /mob/living/carbon/human/examine_get_brute_message() - return !ismachineperson(src) ? "bruising" : "denting" + if(!ismachineperson(src) || calculate_ipc_masquerade_status()) + return "bruising" + + return "denting" + +/// Checks if a body part is covered by clothing +/mob/living/carbon/human/proc/is_bodypart_covered_by_clothing(part_name) + var/bodypart_clothing_bitflag = bodypart_name_to_clothing_bitflag(part_name) + if(!bodypart_clothing_bitflag) + return FALSE + + // Masks + if(bodypart_clothing_bitflag & HEAD) + var/obj/item/clothing/mask/current_mask = wear_mask + if(istype(current_mask) && (current_mask.body_parts_covered & bodypart_clothing_bitflag)) + return TRUE + + // Jumpsuit/uniform + var/chest_groin_arms_legs_bitflag = ARMS | LEGS | UPPER_TORSO | LOWER_TORSO + if(bodypart_clothing_bitflag & chest_groin_arms_legs_bitflag) + if(w_uniform && (w_uniform.body_parts_covered & bodypart_clothing_bitflag)) + return TRUE + if(wear_suit && (wear_suit.body_parts_covered & bodypart_clothing_bitflag)) + return TRUE + + // Gloves + if(bodypart_clothing_bitflag & HANDS) + var/obj/item/clothing/gloves/current_gloves = gloves + if(istype(current_gloves) && (current_gloves.body_parts_covered & bodypart_clothing_bitflag)) + return TRUE + + // Shoes + if(bodypart_clothing_bitflag & FEET) + var/obj/item/clothing/shoes/current_shoes = shoes + if(istype(current_shoes) && (current_shoes.body_parts_covered & bodypart_clothing_bitflag)) + return TRUE + + return FALSE diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm index e92b634c4a6..9ade814065c 100644 --- a/code/modules/mob/living/carbon/human/human_update_icons.dm +++ b/code/modules/mob/living/carbon/human/human_update_icons.dm @@ -198,6 +198,8 @@ GLOBAL_LIST_EMPTY(damage_icon_parts) base_icon = chest.get_icon(skeleton) for(var/obj/item/organ/external/part in bodyparts) + part.sync_colour_to_human(src) + // We just drew the chest, don't draw it twice. if(part == chest) continue diff --git a/code/modules/reagents/chemistry/reagents/toxins.dm b/code/modules/reagents/chemistry/reagents/toxins.dm index bbf3027b0b5..5c9a1de0470 100644 --- a/code/modules/reagents/chemistry/reagents/toxins.dm +++ b/code/modules/reagents/chemistry/reagents/toxins.dm @@ -348,6 +348,16 @@ if(istype(affecting)) affecting.disfigure() + var/was_skin_removed = FALSE + for(var/obj/item/organ/external/limb in H.bodyparts) + if(limb.has_synthetic_skin) + was_skin_removed = TRUE + limb.remove_synthetic_skin(TRUE) + + if(was_skin_removed) + H.visible_message("The synthetic skin on [H]'s body bubbles and melts away.", \ + "The synthetic skin on your body bubbles and melts away.") + /datum/reagent/acid/reaction_obj(obj/O, volume) if(ismob(O.loc)) //handled in human acid_act() return diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm index abac1db0661..26deabf73d6 100644 --- a/code/modules/research/designs/medical_designs.dm +++ b/code/modules/research/designs/medical_designs.dm @@ -71,6 +71,16 @@ build_path = /obj/item/reagent_containers/applicator category = list("Medical") +/datum/design/epidermal_applicator + name = "Epidermal Applicator" + desc = "A pen-shaped device developed by Zeng-Hu Pharmaceuticals and used to apply synthetic skin to prosthetic limbs." + id = "epidermal_applicator" + req_tech = list("biotech" = 5, "materials" = 6, "engineering" = 4) + build_type = PROTOLATHE + materials = list(MAT_METAL = 2000, MAT_GLASS = 1000, MAT_SILVER = 500) + build_path = /obj/item/epidermal_applicator + category = list("Medical") + /datum/design/handheld_defib name = "Handheld Defibrillator" desc = "A smaller defibrillator only capable of treating cardiac arrest." diff --git a/code/modules/surgery/organs/augments_internal.dm b/code/modules/surgery/organs/augments_internal.dm index 878e225907f..03c6b6f0031 100644 --- a/code/modules/surgery/organs/augments_internal.dm +++ b/code/modules/surgery/organs/augments_internal.dm @@ -1094,6 +1094,182 @@ REMOVE_TRAIT(M, TRAIT_IPC_CAN_EAT, "ipc_food[UID()]") return ..() +/obj/item/organ/internal/cyberimp/chest/skinmonger + name = "\improper Skinmonger" + desc = "A strange implant that continuously produces and replaces synthetic skin. \ + Will not apply skin to a monitor-shaped head." + implant_color = "#FFDDAA" + slot = "chest_synthetic_skin" + w_class = WEIGHT_CLASS_TINY + origin_tech = "materials=6;biotech=7;syndicate=1" + augment_icon = "nutripump" + /// Whether regeneration is allowed. This is disabled temporarily by an EMP + var/regeneration_active = TRUE + /// How long to wait after finding a target body part to replace skin on + var/regen_cooldown = 30 SECONDS + /// Whether we're currently regenerating + var/regenerating = FALSE + /// Normally synthetic skin doesn't have memory of the identity we're disguised as. This implant does, though + var/configured_identity = "Unknown" + /// Flag for tracking if we've used up the implant's initial deployment of skin + var/initial_surge_used = FALSE + +/obj/item/organ/internal/cyberimp/chest/skinmonger/insert(mob/living/carbon/M, special = 0) + . = ..() + + if(!initial_surge_used) + apply_full_synthetic_skin(M) + to_chat(M, SPAN_WARNING("In an instant, a surge of skin wraps around you and binds itself to your body!")) + playsound(M, 'sound/surgery/organ2.ogg', 70, TRUE, frequency = 0.8) + initial_surge_used = TRUE + +/obj/item/organ/internal/cyberimp/chest/skinmonger/remove(mob/living/carbon/M, special = 0) + . = ..() + regenerating = FALSE + remove_all_synthetic_skin(M) + +// Called when synthetic skin is removed from any part - starts regeneration cycle +/obj/item/organ/internal/cyberimp/chest/skinmonger/proc/start_regeneration() + if(!regeneration_active || regenerating) + return + + regenerating = TRUE + // Wait before we trigger the first regen. + addtimer(CALLBACK(src, PROC_REF(regenerate_next_part)), regen_cooldown) + +/obj/item/organ/internal/cyberimp/chest/skinmonger/proc/regenerate_next_part() + if(!owner || !regeneration_active || !regenerating) + return + + var/mob/living/carbon/human/H = owner + var/list/unskinned_parts = list() + + // Find all robotic parts without synthetic skin. + for(var/obj/item/organ/external/E in H.bodyparts) + if(!E.is_robotic() || E.has_synthetic_skin) + continue + + // Skip monitor heads + if(E.limb_name == "head" && E.model) + var/datum/robolimb/R = GLOB.all_robolimbs[E.model] + if(R && R.is_monitor) + continue + + unskinned_parts += E + + // If no parts need skin, kill the regen cycle + if(!length(unskinned_parts)) + regenerating = FALSE + return + + // Pick a random part and apply skin + var/obj/item/organ/external/chosen_part = pick(unskinned_parts) + chosen_part.has_synthetic_skin = TRUE + // Apply owner's skin color to synthetic skin + if(ishuman(H)) + chosen_part.synthetic_skin_colour = H.skin_colour + // Set identity for head regeneration + if(chosen_part.limb_name == "head") + var/identity_to_use = configured_identity + chosen_part.synthetic_skin_identity = identity_to_use + if(ishuman(H)) + H.real_name = identity_to_use + + // Chest and lower body tied together for regeneration + if(chosen_part.limb_name == "chest") + var/obj/item/organ/external/groin_limb = H.bodyparts_by_name["groin"] + if(groin_limb && groin_limb.is_robotic() && !groin_limb.has_synthetic_skin) + groin_limb.has_synthetic_skin = TRUE + groin_limb.synthetic_skin_colour = H.skin_colour + groin_limb.force_icon = null + groin_limb.mob_icon = null + groin_limb.compile_icon() + + if(chosen_part.limb_name == "groin") + var/obj/item/organ/external/chest_limb = H.bodyparts_by_name["chest"] + if(chest_limb && chest_limb.is_robotic() && !chest_limb.has_synthetic_skin) + chest_limb.has_synthetic_skin = TRUE + chest_limb.synthetic_skin_colour = H.skin_colour + chest_limb.force_icon = null + chest_limb.mob_icon = null + chest_limb.compile_icon() + + // Refresh sprite + chosen_part.force_icon = null + chosen_part.mob_icon = null + chosen_part.compile_icon() + H.update_body(rebuild_base = TRUE) + + to_chat(H, SPAN_NOTICE("You feel a wave of synthetic skin gush forth and bind to your [chosen_part.name].")) + + // Schedule next regeneration if more parts need skin. Otherwise, we're done + if(length(unskinned_parts) > 1) + addtimer(CALLBACK(src, PROC_REF(regenerate_next_part)), regen_cooldown) + else + regenerating = FALSE + +// One-time application of skin across the entire chassis when the implant is inserted. +/obj/item/organ/internal/cyberimp/chest/skinmonger/proc/apply_full_synthetic_skin(mob/living/carbon/human/target) + if(!ishuman(target)) + return + + var/mob/living/carbon/human/H = target + var/head_skinned = FALSE + for(var/obj/item/organ/external/E in H.bodyparts) + if(!E.is_robotic() || E.has_synthetic_skin) + continue + + // Skip monitor heads + if(E.limb_name == "head" && E.model) + var/datum/robolimb/R = GLOB.all_robolimbs[E.model] + if(R && R.is_monitor) + continue + + E.has_synthetic_skin = TRUE + // Apply owner's skin color to synthetic skin + E.synthetic_skin_colour = H.skin_colour + // Set configured identity for head + if(E.limb_name == "head") + E.synthetic_skin_identity = configured_identity + head_skinned = TRUE + // Clear sprite cache + E.force_icon = null + E.mob_icon = null + E.compile_icon() + + // Apply facial identity if head was skinned + if(head_skinned) + H.real_name = configured_identity + + H.update_body(rebuild_base = TRUE) + +/obj/item/organ/internal/cyberimp/chest/skinmonger/proc/remove_all_synthetic_skin(mob/living/carbon/human/target) + if(!ishuman(target)) + return + + var/mob/living/carbon/human/H = target + for(var/obj/item/organ/external/E in H.bodyparts) + if(E.is_robotic() && E.has_synthetic_skin) + E.remove_synthetic_skin(silent = TRUE) // silent because it spams the user otherwise + + to_chat(H, SPAN_WARNING("You feel your synthetic skin melt away.")) + H.update_body(rebuild_base = TRUE) + +// EMP wipes all synthetic skin and puts the implant on cooldown +/obj/item/organ/internal/cyberimp/chest/skinmonger/emp_act(severity) + . = ..() + + regenerating = FALSE + regeneration_active = FALSE + remove_all_synthetic_skin(owner) + + addtimer(CALLBACK(src, PROC_REF(restart_regeneration)), regen_cooldown) + +/obj/item/organ/internal/cyberimp/chest/skinmonger/proc/restart_regeneration() + if(owner) + regeneration_active = TRUE + start_regeneration() + //BOX O' IMPLANTS /obj/item/storage/box/cyber_implants diff --git a/code/modules/surgery/organs/autosurgeon.dm b/code/modules/surgery/organs/autosurgeon.dm index c702c1c632f..a35d3abceec 100644 --- a/code/modules/surgery/organs/autosurgeon.dm +++ b/code/modules/surgery/organs/autosurgeon.dm @@ -190,4 +190,44 @@ /obj/item/autosurgeon/organ/syndicate/oneuse/syndie_mantis/l starting_organ = /obj/item/organ/internal/cyberimp/arm/syndie_mantis/l +/obj/item/autosurgeon/organ/syndicate/oneuse/skinmonger + starting_organ = /obj/item/organ/internal/cyberimp/chest/skinmonger + +/obj/item/autosurgeon/organ/syndicate/oneuse/skinmonger/attack_self__legacy__attackchain(mob/user) + if(!storedorgan) + return ..() + + // Configure identity before implantation... + var/obj/item/organ/internal/cyberimp/chest/skinmonger/implant = storedorgan + + // Check if they have a monitor head + var/has_monitor_head = FALSE + if(ishuman(user)) + var/mob/living/carbon/human/H = user + var/obj/item/organ/external/head/head_organ = H.bodyparts_by_name["head"] + if(head_organ?.model) + var/datum/robolimb/R = GLOB.all_robolimbs[head_organ.model] + if(R?.is_monitor) + has_monitor_head = TRUE + to_chat(user, SPAN_WARNING("The Skinmonger is confused by your freakishly large head, but attempts to disguise you anyway.")) + + // Only ask for identity if they're a machine person and don't have a monitor head + if(ismachineperson(user) && !has_monitor_head) + if(!implant.configured_identity || implant.configured_identity == "Unknown") + var/chosen_name = tgui_input_text(user, "The Skinmonger generously offers to turn you into someone else. But who?", default = "Unknown", max_length = MAX_NAME_LEN) + if(!chosen_name) + return + + // Allow to appear as "Unknown" (default value) + if(chosen_name != "Unknown") + chosen_name = reject_bad_name(chosen_name, max_length = MAX_NAME_LEN) + if(!chosen_name) + to_chat(user, SPAN_WARNING("The implanter quietly hisses, rejecting your choice.")) + return + + implant.configured_identity = chosen_name + + // ... then implant + return ..() + #undef INFINITE diff --git a/code/modules/surgery/organs/organ_external.dm b/code/modules/surgery/organs/organ_external.dm index 28c408ab219..e51c162c1d6 100644 --- a/code/modules/surgery/organs/organ_external.dm +++ b/code/modules/surgery/organs/organ_external.dm @@ -64,6 +64,12 @@ var/fragile = FALSE ///The level of false skin used to cover robotic organs on the limb. Updated when too damaged, when installed, or when an organ with it is installed. var/augmented_skin_cover_level = 0 + /// Whether this robotic limb has synthetic skin applied + var/has_synthetic_skin = FALSE + /// Stored facial identity for synthetic skin (head only) + var/synthetic_skin_identity = null + /// Stored skin color for synthetic skin + var/synthetic_skin_colour = null // When the limb is not on a person, make sure it faces south so it's always visible. /obj/item/organ/external/setDir() @@ -317,6 +323,10 @@ droplimb(1) //Clean loss, just drop the limb and be done var/mob/living/carbon/owner_old = owner //Need to update health, but need a reference in case the below check cuts off a limb. + + // Check if synthetic skin should be removed from damage + check_synthetic_skin_damage() + //If limb took enough damage, try to cut or tear it off if(owner) if(sharp && !(limb_flags & CANNOT_DISMEMBER)) @@ -578,7 +588,68 @@ Note that amputating the affected organ does in fact remove the infection from t if((brute_dam >= max_damage * (augmented_skin_cover_level / 3) && brute) || (burn_dam >= max_damage * (augmented_skin_cover_level / 3) && burn)) // 66% for level 2, 100% for level 3 break_augmented_skin() -// new damage icon system +/obj/item/organ/external/proc/check_synthetic_skin_damage() + if(!has_synthetic_skin || !is_robotic()) + return + + var/damage_percentage = (brute_dam + burn_dam) / max_damage + if(damage_percentage >= 0.8) + remove_synthetic_skin() + + // Upper body and lower body are tied together + check_connected_limb_skin_damage() + +// Chest and lower body are tied together with synthetic skin. +/obj/item/organ/external/proc/check_connected_limb_skin_damage() + if(!owner || !has_synthetic_skin) + return + + if(limb_name == "chest") + var/damage_percentage = (brute_dam + burn_dam) / max_damage + if(damage_percentage >= 0.8) + var/obj/item/organ/external/groin_limb = owner.bodyparts_by_name["groin"] + if(groin_limb?.has_synthetic_skin) + groin_limb.remove_synthetic_skin() + + if(limb_name == "groin") + var/damage_percentage = (brute_dam + burn_dam) / max_damage + if(damage_percentage >= 0.8) + var/obj/item/organ/external/chest_limb = owner.bodyparts_by_name["chest"] + if(chest_limb?.has_synthetic_skin) + chest_limb.remove_synthetic_skin() + +/obj/item/organ/external/proc/remove_synthetic_skin(silent = FALSE) + if(!has_synthetic_skin) + return + + has_synthetic_skin = FALSE + + // Restore original identity if this is a head + if(limb_name == "head" && owner && ishuman(owner)) + var/mob/living/carbon/human/H = owner + synthetic_skin_identity = null + H.real_name = H.dna.real_name + + // Restore original robotic appearance + if(model) + var/datum/robolimb/R = GLOB.all_robolimbs[model] + if(R) + force_icon = R.icon + + if(ishuman(owner)) + var/mob/living/carbon/human/H = owner + if(!silent) + to_chat(H, SPAN_WARNING("The synthetic skin on your [name] is destroyed!")) + // Force the sprite to update + mob_icon = null + compile_icon() + H.update_body(rebuild_base = TRUE) + H.UpdateDamageIcon() + + var/obj/item/organ/internal/cyberimp/chest/skinmonger/regen = H.get_int_organ(/obj/item/organ/internal/cyberimp/chest/skinmonger) + if(regen) + regen.start_regeneration() + // returns just the brute/burn damage code /obj/item/organ/external/proc/damage_state_text() var/tburn = 0 @@ -627,6 +698,14 @@ Note that amputating the affected organ does in fact remove the infection from t if(limb_flags & CANNOT_DISMEMBER || !owner) return + // Loose limbs should not have any synthetic skin + if(has_synthetic_skin) + remove_synthetic_skin() + if(children) + for(var/obj/item/organ/external/child in children) + if(child.has_synthetic_skin) + child.remove_synthetic_skin() + if(HAS_TRAIT(owner, TRAIT_I_WANT_BRAINS) && !clean) fracture() return diff --git a/code/modules/surgery/organs/organ_icon.dm b/code/modules/surgery/organs/organ_icon.dm index 9b3c239993e..ea38be15d0f 100644 --- a/code/modules/surgery/organs/organ_icon.dm +++ b/code/modules/surgery/organs/organ_icon.dm @@ -23,6 +23,10 @@ /obj/item/organ/external/proc/sync_colour_to_human(mob/living/carbon/human/H) if(is_robotic() && !istype(dna.species, /datum/species/machine)) //machine people get skin color + // For robotic parts with synthetic skin, use stored synthetic skin color + if(has_synthetic_skin && synthetic_skin_colour) + s_col = synthetic_skin_colour + s_tone = null return if(dna.species && H.dna.species && dna.species.name != H.dna.species.name) return @@ -67,7 +71,7 @@ var/icon_file = new_icons[1] var/new_icon_state = new_icons[2] mob_icon = new /icon(icon_file, new_icon_state) - if(!skeletal && !is_robotic()) + if(!skeletal && (!is_robotic() || (is_robotic() && has_synthetic_skin))) if(status & ORGAN_DEAD) mob_icon.ColorTone(COLORTONE_DEAD_EXT_ORGAN) mob_icon.SetIntensity(0.7) @@ -169,8 +173,10 @@ if(skeletal) icon_file = 'icons/mob/human_races/r_skeleton.dmi' - else if(is_robotic()) + else if(is_robotic() && !has_synthetic_skin) icon_file = 'icons/mob/human_races/robotic.dmi' + else if(has_synthetic_skin && dna.species && istype(dna.species, /datum/species/machine)) + icon_file = 'icons/mob/human_races/r_human.dmi' else // Congratulations, you are normal icon_file = icobase diff --git a/code/modules/surgery/robotics.dm b/code/modules/surgery/robotics.dm index 031db43d3ce..202582115b1 100644 --- a/code/modules/surgery/robotics.dm +++ b/code/modules/surgery/robotics.dm @@ -787,11 +787,21 @@ var/new_gender = gender_list[gender_key] var/old_name = target.real_name - target.real_name = new_name + var/identity_type = "core identity parameters" + + // Prioritize replacing a synthetic skin identity over the IPC's actual identity + var/obj/item/organ/external/head = target.bodyparts_by_name["head"] + if(head && head.has_synthetic_skin) + head.synthetic_skin_identity = new_name + target.real_name = new_name + identity_type = "synthetic facial identity" + else + target.real_name = new_name + target.gender = new_gender user.visible_message( - SPAN_NOTICE("[user] edits [old_name]'s identity parameters with [tool]; [target.p_they()] [target.p_are()] now known as [new_name]."), - SPAN_NOTICE("You alter [old_name]'s identity parameters with [tool]; [target.p_they()] [target.p_are()] now known as [new_name]."), + SPAN_NOTICE("[user] edits [old_name]'s [identity_type] with [tool]; [target.p_they()] [target.p_are()] now known as [new_name]."), + SPAN_NOTICE("You alter [old_name]'s [identity_type] with [tool]; [target.p_they()] [target.p_are()] now known as [new_name]."), chat_message_type = MESSAGE_TYPE_COMBAT ) diff --git a/icons/obj/surgery.dmi b/icons/obj/surgery.dmi index ab8c24ac077..6ebcf6beff0 100644 Binary files a/icons/obj/surgery.dmi and b/icons/obj/surgery.dmi differ diff --git a/paradise.dme b/paradise.dme index e76c1d5bfcd..7e5c9c81ef5 100644 --- a/paradise.dme +++ b/paradise.dme @@ -1445,6 +1445,7 @@ #include "code\game\objects\items\tanks\tank_types.dm" #include "code\game\objects\items\tanks\watertank.dm" #include "code\game\objects\items\tools\crowbar.dm" +#include "code\game\objects\items\tools\epidermal_applicator.dm" #include "code\game\objects\items\tools\hammer.dm" #include "code\game\objects\items\tools\multitool.dm" #include "code\game\objects\items\tools\screwdriver.dm"