diff --git a/code/__DEFINES/~~bubber_defines/traits/sources.dm b/code/__DEFINES/~~bubber_defines/traits/sources.dm
index b8f92f074db..5b6f1a6b5f1 100644
--- a/code/__DEFINES/~~bubber_defines/traits/sources.dm
+++ b/code/__DEFINES/~~bubber_defines/traits/sources.dm
@@ -9,3 +9,6 @@
#define TRAIT_SOURCE_TAURLAY "taur-laydown"
#define TRAIT_SOURCE_TAURCROP "taur-crop"
+
+/// Transformation component
+#define TRAIT_TRANSFORMATION "transformation"
diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm
index 83833dadf41..b43f165cfdf 100644
--- a/code/modules/mob/living/carbon/carbon_update_icons.dm
+++ b/code/modules/mob/living/carbon/carbon_update_icons.dm
@@ -481,8 +481,14 @@
update_wound_overlays()
var/limb_count_update = 0
var/list/new_limbs = list()
- for(var/body_zone, limb_untyped in get_bodyparts_by_zones())
+ var/datum/component/transformation/transform = GetComponent(/datum/component/transformation) // BUBBER EDIT START - Transformation component override.
+ if(transform)
+ transform.on_transform_limb_icon(src)
+ for(var/body_zone, limb_untyped in transform ? transform.dummy.get_bodyparts_by_zones() : get_bodyparts_by_zones()) // BUBBER EDIT END | ORG: for(var/body_zone, limb_untyped in get_bodyparts_by_zones())
var/obj/item/bodypart/limb = limb_untyped
+ var/obj/item/bodypart/stump_limb_check = get_bodypart(body_zone, TRUE) // BUBBER EDIT START
+ if(transform && (isnull(stump_limb_check) || IS_STUMP(stump_limb_check)))
+ limb = stump_limb_check // BUBBER EDIT END
if(isnull(limb) || IS_STUMP(limb))
if(icon_render_keys[body_zone])
icon_render_keys -= body_zone
diff --git a/modular_zubbers/code/datums/components/transformation.dm b/modular_zubbers/code/datums/components/transformation.dm
new file mode 100644
index 00000000000..ffcdcc14427
--- /dev/null
+++ b/modular_zubbers/code/datums/components/transformation.dm
@@ -0,0 +1,147 @@
+/** OK THIS IS GOING TO SOUND WEIRD, BUT STAY WITH ME. I'VE PUT A LOT OF THOUGHT ON HOW TO DO THIS WITHOUT FUCKING UP HOW THE GAME CREATES OUR FURRY HOMUNCULUS CREATURES.
+ * -----------
+ * How literally everything else handles any form of transformation (NIF, Changeling, that one lavaland knife I doubt you know about) will mostly dna.copy_dna() from the old to the new.
+ * But the issue is, literally EVERYTHING fucking breaks the second you're using limbs that are entirely incompatible. (Teshari, synths) and the spaceman that pops out becomes a weird ass frankenstein mess.
+ * That is, unless you force load one set of prefs to the other mob. (Which is a valid and cool option) But overall, to fix the above issue you HAVE TO change their species.
+ * This is more an illusionary effect and me tricking the game. The base mob is mostly unaffected. You will look like a synth and still bleed red. How will this work?
+ * /mob/living/carbon/proc/update_body_parts() detects if the mob has this component, then will create the required icon_render_keys based off a separate entity.
+ * It can't be the source player directly, cause you chop off their head or they put on a hat, it'll mirror exactly to the component holder which is also undesired jank.
+ * The solution? We still do the whole changeling bullshit to a mob that sits in null space, then have the affected player mirror it.
+ * If something looks jank, it's because the nullspace dummy we're using for this is not properly mirroring certain icon events. (Hair hiding with hats, gasmasks not clipping with snouts, etc)
+ * With that, my dear reader, just need to make sure on_transform_limb_icon() properly modifies our nullspace dummy with said event or you add another cached_feature to temporarily change the player's mob.
+ *
+ * TDLR: This whole thing is so you can transform people without changing their species, and for it not to look like shit.
+ */
+
+/datum/component/transformation
+ /// Source mob that's transferring.
+ var/mob/living/carbon/human/original
+ /// Dummy human sitting in null space
+ var/mob/living/carbon/human/consistent/dummy
+
+ var/list/cached_features = list()
+ var/list/cached_limbs = list()
+
+/datum/component/transformation/Initialize(source, id)
+ if(!ishuman(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.original = source
+
+/datum/component/transformation/RegisterWithParent()
+ var/mob/living/carbon/human/owner = parent
+
+ /// Stores everything we need to keep inside a cache.
+ cached_features["feature_dna"] += owner.dna.features.Copy()
+ cached_features["name"] += owner.real_name
+ cached_features["gender"] += owner.gender
+ cached_features["bodyshape"] += owner.bodyshape
+
+ /// Now changes everything after we've just shoved it into the closet.
+
+ dummy = new
+ original.dna.copy_dna(dummy.dna, COPY_DNA_SE|COPY_DNA_SPECIES|COPY_DNA_MUTATIONS)
+
+ owner.real_name = original.real_name
+ owner.name = original.real_name
+ owner.gender = original.gender
+ owner.bodyshape = original.bodyshape
+
+ owner.dna.features["custom_species"] = original.dna.features["custom_species"]
+ owner.dna.features["flavor_text"] = original.dna.features["flavor_text"]
+ owner.dna.features["flavor_text_nsfw"] = original.dna.features["flavor_text_nsfw"]
+ owner.dna.features["custom_species_lore"] = original.dna.features["custom_species_lore"]
+
+ owner.dna.features["headshot"] = original.dna.features["headshot"]
+ owner.dna.features["headshot_nsfw"] = original.dna.features["headshot_nsfw"]
+ owner.dna.features["art_ref"] = original.dna.features["art_ref"]
+
+ /// Limb bullshit so worn sprites don't fuck up.
+ for(var/zone, untyped_limb in original.get_bodyparts_by_zones())
+ var/obj/item/bodypart/originating_limbs = untyped_limb
+ var/obj/item/bodypart/dummy_limb = dummy.get_bodypart(zone)
+ var/obj/item/bodypart/limb = owner.get_bodypart(zone)
+
+ cached_limbs += list(list("zone" = zone, "limb_id" = limb.limb_id, "bodyshape" = limb.bodyshape))
+ limb.limb_id = originating_limbs.limb_id
+ limb.bodyshape = originating_limbs.bodyshape
+ dummy_limb.limb_gender = originating_limbs.limb_gender
+
+ /// Eyes
+ var/obj/item/organ/eyes/owner_eyes = owner.get_organ_slot(ORGAN_SLOT_EYES)
+ cached_features["eyes"] += list("color_l" = owner.eye_color_left, "color_r" = owner.eye_color_right, "blinking" = owner_eyes.blink_animation)
+ owner.set_eye_color(original.eye_color_left, original.eye_color_right)
+ owner_eyes.blink_animation = FALSE
+ /// Quad eyes
+ cached_features["quadeyes"] += HAS_TRAIT_FROM(owner, TRAIT_QUAD_EYES, TRAIT_GENERIC)
+ cached_features["quadoffset"] += owner.quad_eyes_offset
+ cached_features["quadoffwidth"] += owner.quad_eyes_offset_width
+ if(!cached_features["quadeyes"])
+ if(HAS_TRAIT_FROM(original, TRAIT_QUAD_EYES, TRAIT_GENERIC))
+ ADD_TRAIT(owner, TRAIT_QUAD_EYES, TRAIT_TRANSFORMATION)
+ else
+ REMOVE_TRAIT(owner, TRAIT_QUAD_EYES, TRAIT_GENERIC)
+ owner.quad_eyes_offset = original.quad_eyes_offset
+ owner.quad_eyes_offset_width = original.quad_eyes_offset_width
+
+ var/obj/item/bodypart/head/head = original.get_bodypart(BODY_ZONE_HEAD)
+ dummy.set_hairstyle(original.hairstyle, FALSE)
+ dummy.set_haircolor(original.hair_color)
+ dummy.set_facial_hairstyle(original.facial_hairstyle, FALSE)
+ dummy.set_facial_haircolor(original.facial_hair_color)
+ dummy.set_hair_gradient_style(head.gradient_styles[GRADIENT_HAIR_KEY], FALSE)
+ dummy.set_hair_gradient_color(head.gradient_colors[GRADIENT_FACIAL_HAIR_KEY])
+ dummy.set_facial_hair_gradient_style(head.gradient_styles[GRADIENT_FACIAL_HAIR_KEY], FALSE)
+ dummy.set_facial_hair_gradient_color(head.gradient_colors[GRADIENT_FACIAL_HAIR_KEY])
+
+ owner.regenerate_icons()
+
+/datum/component/transformation/UnregisterFromParent()
+ var/mob/living/carbon/human/owner = parent
+ /// All the features we stored in cache needs to go back properly.
+ owner.dna.features = cached_features["feature_dna"]
+ owner.real_name = cached_features["name"]
+ owner.name = cached_features["name"]
+ owner.gender = cached_features["gender"]
+ owner.bodyshape = cached_features["bodyshape"]
+
+ /// Reset our limb bullshit
+ for(var/cached in cached_limbs)
+ var/zone = cached["zone"]
+ var/obj/item/bodypart/limb = owner.get_bodypart(zone)
+ limb.limb_id = cached["limb_id"]
+ limb.bodyshape = cached["bodyshape"]
+
+ /// Reset our eyes
+ var/list/eye_cache = cached_features["eyes"]
+ var/obj/item/organ/eyes/owner_eyes = owner.get_organ_slot(ORGAN_SLOT_EYES)
+ owner.set_eye_color(eye_cache["color_l"], eye_cache["color_r"])
+ owner_eyes.blink_animation = eye_cache["blinking"]
+ if(!cached_features["quadeyes"] && HAS_TRAIT(owner, TRAIT_QUAD_EYES))
+ REMOVE_TRAIT(owner, TRAIT_QUAD_EYES, TRAIT_TRANSFORMATION)
+ if(cached_features["quadeyes"])
+ ADD_TRAIT(owner, TRAIT_QUAD_EYES, TRAIT_GENERIC)
+ owner.quad_eyes_offset = cached_features["quadoffset"]
+ owner.quad_eyes_offset_width = cached_features["quadoffwidth"]
+
+ original = null
+ QDEL_NULL(dummy)
+ owner.regenerate_icons()
+
+/// TODO: Digigrade legs and bodyshape
+/datum/component/transformation/proc/on_transform_limb_icon(mob/source) // Easy way to properly reflect icon events.
+ var/mob/living/carbon/human/owner = source
+ var/list/old_abstractions = dummy.get_equipped_items()
+ QDEL_LIST(old_abstractions)
+ for(var/obj/item/item in owner.get_equipped_items())
+ if(item.slot_flags & (ITEM_SLOT_ICLOTHING|ITEM_SLOT_OCLOTHING|ITEM_SLOT_HEAD|ITEM_SLOT_BACK))
+ var/obj/item/abstraction = new item.type
+ abstraction.flags_inv = item.flags_inv
+ abstraction.equip_to_best_slot(dummy)
+
+
+
+
+
+
+
+
diff --git a/modular_zubbers/code/modules/customization/species/proteans/protean_ui.dm b/modular_zubbers/code/modules/customization/species/proteans/protean_ui.dm
index 498662d57a1..13a997695f6 100644
--- a/modular_zubbers/code/modules/customization/species/proteans/protean_ui.dm
+++ b/modular_zubbers/code/modules/customization/species/proteans/protean_ui.dm
@@ -1,3 +1,5 @@
+/datum/species/protean
+ COOLDOWN_DECLARE(transform_cooldown)
/datum/species/protean/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
@@ -13,6 +15,7 @@
data["lock"] = species_modsuit?.modlocked
data["icon"] = species_modsuit?.icon
data["icon_state"] = species_modsuit?.icon_state
+ data["transform"] = species_modsuit?.wearer?.GetComponent(/datum/component/transformation)
return data
/datum/species/protean/ui_status(mob/user) // Protean's UI
@@ -35,8 +38,29 @@
owner.suit_transformation(owner == usr ? FALSE : TRUE) // Others can force the protean to fold.
if("heal")
owner.protean_heal()
+ if("protean_transform")
+ if(COOLDOWN_FINISHED(src, transform_cooldown)) // Anti-spam
+ var/component = species_modsuit.wearer?.GetComponent(/datum/component/transformation)
+ if(component)
+ playsound(owner, 'sound/machines/click.ogg', 25)
+ qdel(component)
+ else if(species_modsuit.wearer == owner)
+ to_chat(owner, span_notice("You already look like yourself!"))
+ else
+ playsound(owner, 'sound/machines/click.ogg', 25)
+ species_modsuit.wearer.AddComponent(/datum/component/transformation, source = owner)
+ RegisterSignal(species_modsuit, COMSIG_ITEM_PRE_UNEQUIP, PROC_REF(protean_transform_unequip))
+ COOLDOWN_START(src, transform_cooldown, 1 SECONDS)
return TRUE
+/datum/species/protean/proc/protean_transform_unequip(/obj/item/source)
+ SIGNAL_HANDLER
+
+ var/component = species_modsuit.wearer.GetComponent(/datum/component/transformation)
+ if(component)
+ UnregisterSignal(species_modsuit, COMSIG_ITEM_PRE_UNEQUIP)
+ qdel(component)
+
/datum/action/protean
name = "Protean Interface"
desc = "Left Click Opens the Protean interface.
Right Click toggles suit transformation."
diff --git a/tgstation.dme b/tgstation.dme
index 3dfb22ee6b3..aeaba30c979 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -9002,6 +9002,7 @@
#include "modular_zubbers\code\datums\components\sign_language.dm"
#include "modular_zubbers\code\datums\components\squeak.dm"
#include "modular_zubbers\code\datums\components\tameable.dm"
+#include "modular_zubbers\code\datums\components\transformation.dm"
#include "modular_zubbers\code\datums\components\udder.dm"
#include "modular_zubbers\code\datums\components\weatherannouncer.dm"
#include "modular_zubbers\code\datums\components\crafting\containers.dm"
diff --git a/tgui/packages/tgui/interfaces/ProteanUI.tsx b/tgui/packages/tgui/interfaces/ProteanUI.tsx
index 17270a74769..e6d3779aa3f 100644
--- a/tgui/packages/tgui/interfaces/ProteanUI.tsx
+++ b/tgui/packages/tgui/interfaces/ProteanUI.tsx
@@ -18,6 +18,7 @@ type ProteanData = {
metal: number;
metal_max: number;
low_power: boolean;
+ transformation: boolean;
lock: boolean;
icon: string;
icon_state: string;
@@ -33,7 +34,15 @@ export const ProteanUI = () => {
export const Protean = () => {
const { data, act } = useBackend();
- const { lock, metal, metal_max, icon, icon_state, low_power } = data;
+ const {
+ lock,
+ metal,
+ metal_max,
+ icon,
+ icon_state,
+ low_power,
+ transformation,
+ } = data;
return (
{
Heal Organs
+
+ Wearer Transformation
+
@@ -96,6 +108,14 @@ export const Protean = () => {
+
+