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"