diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index 2912c7b30fce..0b9378eeeb4b 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -1289,9 +1289,10 @@ update_icon_dropped() //This foot gun needs a safety - if(!icon_exists(icon_holder, "[limb_id]_[body_zone][is_dimorphic ? "_[limb_gender]" : ""]")) + var/resulting_icon = "[limb_id]_[body_zone][is_dimorphic ? "_[limb_gender]" : ""]" + if(!icon_exists(icon_holder, resulting_icon)) reset_appearance() - stack_trace("change_appearance([icon], [id], [greyscale], [dimorphic]) generated null icon") + stack_trace("change_appearance([icon], [id], [greyscale], [dimorphic]) generated null icon ([resulting_icon])") ///Resets the base appearance of a limb to it's default values. /obj/item/bodypart/proc/reset_appearance() diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_synth.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_synth.png index d9d2cabcebe7..d793ca168ba0 100644 Binary files a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_synth.png and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_synth.png differ diff --git a/maplestation.dme b/maplestation.dme index 7103c2b7586e..6fe1a8d9aeb1 100644 --- a/maplestation.dme +++ b/maplestation.dme @@ -6251,6 +6251,7 @@ #include "maplestation_modules\code\modules\client\preferences\species\lizard.dm" #include "maplestation_modules\code\modules\client\preferences\species\ornithid.dm" #include "maplestation_modules\code\modules\client\preferences\species\skrell.dm" +#include "maplestation_modules\code\modules\client\preferences\species\synth.dm" #include "maplestation_modules\code\modules\client\preferences\spellbook\spellbook.dm" #include "maplestation_modules\code\modules\client\preferences\spellbook\spellbook_helpers.dm" #include "maplestation_modules\code\modules\client\preferences\spellbook\spellbook_item_customization_menu.dm" @@ -6413,8 +6414,9 @@ #include "maplestation_modules\code\modules\mob\living\carbon\human\species_types\skeletons.dm" #include "maplestation_modules\code\modules\mob\living\carbon\human\species_types\skrell.dm" #include "maplestation_modules\code\modules\mob\living\carbon\human\species_types\species.dm" -#include "maplestation_modules\code\modules\mob\living\carbon\human\species_types\synths.dm" #include "maplestation_modules\code\modules\mob\living\carbon\human\species_types\vampire.dm" +#include "maplestation_modules\code\modules\mob\living\carbon\human\species_types\synth\synth.dm" +#include "maplestation_modules\code\modules\mob\living\carbon\human\species_types\synth\synth_ion.dm" #include "maplestation_modules\code\modules\mob\living\silicon\robot\robot_defines.dm" #include "maplestation_modules\code\modules\mob\living\simple_animal\corpse.dm" #include "maplestation_modules\code\modules\mod\mod_control.dm" diff --git a/maplestation_modules/code/__DEFINES/signals.dm b/maplestation_modules/code/__DEFINES/signals.dm index cc4750b76687..59edbe51490e 100644 --- a/maplestation_modules/code/__DEFINES/signals.dm +++ b/maplestation_modules/code/__DEFINES/signals.dm @@ -1,3 +1,9 @@ +// These are just here so the newer coders understand global signals better +#define RegisterGlobalSignal(sig, procref) RegisterSignal(SSdcs, sig, procref) +#define UnregisterGlobalSignal(sig) UnregisterSignal(SSdcs, sig) + +#define COMSIG_GLOB_ION_STORM "!ion_storm" + /// Sent from [/mob/living/examine] late, after the first signal is sent, but BEFORE flavor text handling, /// for when you prefer something guaranteed to appear at the bottom of the stack /// (Flavor text should stay at the very very bottom though) diff --git a/maplestation_modules/code/modules/client/preferences/languages.dm b/maplestation_modules/code/modules/client/preferences/languages.dm index d9ad432b93f3..1254cc2e0452 100644 --- a/maplestation_modules/code/modules/client/preferences/languages.dm +++ b/maplestation_modules/code/modules/client/preferences/languages.dm @@ -3,9 +3,6 @@ /// Simple define to denote no language. #define NO_LANGUAGE "No Language" -/// List of species IDs of species's that can't get an additional language -#define BLACKLISTED_SPECIES_FROM_LANGUAGES list(/datum/species/synth, /datum/species/android) - /datum/preference/choiced/language savefile_key = "bilingual_language" @@ -29,9 +26,6 @@ return NO_LANGUAGE var/datum/species/species = preferences.read_preference(/datum/preference/choiced/species) - if(species in BLACKLISTED_SPECIES_FROM_LANGUAGES) - return NO_LANGUAGE - var/banned = initial(lang_to_add.banned_from_species) var/req = initial(lang_to_add.required_species) if((banned && ispath(species, banned)) || (req && !ispath(species, req))) @@ -191,8 +185,7 @@ data["base_languages"] = base_languages data["bonus_languages"] = bonus_languages - data["blacklisted_species"] = BLACKLISTED_SPECIES_FROM_LANGUAGES + data["blacklisted_species"] = list() return data #undef NO_LANGUAGE -#undef BLACKLISTED_SPECIES_FROM_LANGUAGES diff --git a/maplestation_modules/code/modules/client/preferences/species/skrell.dm b/maplestation_modules/code/modules/client/preferences/species/skrell.dm index 32dd79f40a31..7657a822ebee 100644 --- a/maplestation_modules/code/modules/client/preferences/species/skrell.dm +++ b/maplestation_modules/code/modules/client/preferences/species/skrell.dm @@ -1,4 +1,3 @@ - /datum/preference/choiced/skrell_hair savefile_key = "feature_head_tentacles" savefile_identifier = PREFERENCE_CHARACTER diff --git a/maplestation_modules/code/modules/client/preferences/species/synth.dm b/maplestation_modules/code/modules/client/preferences/species/synth.dm new file mode 100644 index 000000000000..4830f5a9c37f --- /dev/null +++ b/maplestation_modules/code/modules/client/preferences/species/synth.dm @@ -0,0 +1,47 @@ +#define NO_DISGUISE "(No Disguise)" + +/datum/preference/choiced/synth_species + savefile_key = "feature_synth_species" + savefile_identifier = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + priority = PREFERENCE_PRIORITY_GENDER + +/datum/preference/choiced/synth_species/init_possible_values() + var/datum/species/synth/synth = new() + . = synth.valid_species.Copy() + . += NO_DISGUISE + qdel(synth) + return . + +/datum/preference/choiced/synth_species/create_default_value() + return SPECIES_HUMAN + +/datum/preference/choiced/synth_species/apply_to_human(mob/living/carbon/human/target, value) + var/datum/species/synth/synth = target.dna?.species + if(!istype(synth)) + return + if(value == NO_DISGUISE) + synth.drop_disguise(target) + return + if(value in synth.valid_species) + synth.disguise_as(target, GLOB.species_list[value]) + return + +/datum/preference/numeric/synth_damage_threshold + savefile_key = "feature_synth_damage_threshold" + savefile_identifier = PREFERENCE_CHARACTER + category = PREFERENCE_CATEGORY_SECONDARY_FEATURES + priority = PREFERENCE_PRIORITY_GENDER + minimum = -100 + maximum = 75 + +/datum/preference/numeric/synth_damage_threshold/create_default_value() + return 25 + +/datum/preference/numeric/synth_damage_threshold/apply_to_human(mob/living/carbon/human/target, value) + var/datum/species/synth/synth = target.dna?.species + if(!istype(synth)) + return + synth.disuise_damage_threshold = value + +#undef NO_DISGUISE diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/abductor.dm b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/abductor.dm index 63b1695a975a..bfcb4cace1c9 100644 --- a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/abductor.dm +++ b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/abductor.dm @@ -2,10 +2,15 @@ return /datum/species/abductor/get_species_description() - return "Abductors, colloquially known as \"Greys\" (or \"Grays\"), are pale skinned inquisitive aliens who can't commicate to the average crew-member." + return "Abductors, colloquially known as \"Greys\" (or \"Grays\"), \ + are pale skinned inquisitive aliens who can't communicate well to the average crew-member." /datum/species/abductor/get_species_lore() - return list("Abductor lore.") + return list( + "Little are known about Abductors. \ + While they (as a species) have been known to abduct other species of 'lesser intellect' for experimentation, \ + some have been known to - on rare occasions - work with the very species they abduct, for reasons unknown.", + ) /datum/species/abductor/create_pref_unique_perks() var/list/perks = list() diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/podpeople.dm b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/podpeople.dm index aa6ef93378f2..930064b1d493 100644 --- a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/podpeople.dm +++ b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/podpeople.dm @@ -11,17 +11,22 @@ human.update_body(is_creating = TRUE) /datum/species/pod/get_species_description() - return "Podpeople are made of plants!" + return "Podpeople are largely peaceful plant based lifeforms, resembling a humanoid figure made of leaves, flowers, and vines." /datum/species/pod/get_species_lore() - return list("Podpserson lore.") + return list( + "Little is known about the origins of the Podpeople. \ + Many assume them to be the result of a long forgotten botanical experiment, slowly mutating for years on years until they became the beings they are today. \ + Ever since they were uncovered long ago, their kind have been found on board stations and planets across the galaxy, \ + often working in hydroponics bays, kitchens, or science departments, working with plants and other botanical lifeforms.", + ) /datum/species/pod/create_pref_unique_perks() - var/list/perks = list() + var/list/perks = ..() perks += list(list( SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, - SPECIES_PERK_ICON = "leaf", + SPECIES_PERK_ICON = FA_ICON_LEAF, SPECIES_PERK_NAME = "Green Thumbs", SPECIES_PERK_DESC = "Podpeople are friend to all plants. Hostile sentient \ plants will not harm them and dangerous botanical produce can \ @@ -29,25 +34,32 @@ )) perks += list(list( SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, - SPECIES_PERK_ICON = "sun", + SPECIES_PERK_ICON = FA_ICON_SUN, SPECIES_PERK_NAME = "Photosynthesis", SPECIES_PERK_DESC = "Podpeople feed themselves and heal when exposed to light, \ but wilt and starve when living in darkness.", )) perks += list(list( SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, - SPECIES_PERK_ICON = "first-aid", + SPECIES_PERK_ICON = FA_ICON_FIRST_AID, SPECIES_PERK_NAME = "Plant Matter", SPECIES_PERK_DESC = "Podpeople must use plant analyzers to scan themselves \ instead of heath analyzers.", )) perks += list(list( SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, - SPECIES_PERK_ICON = "paw", + SPECIES_PERK_ICON = FA_ICON_BIOHAZARD, + SPECIES_PERK_NAME = "Weedkiller Susceptability", + SPECIES_PERK_DESC = "Being a floral life form, you are susceptable to anti-florals and will take extra toxin damage from it!" + )) + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = FA_ICON_PAW, SPECIES_PERK_NAME = "Herbivore Target", SPECIES_PERK_DESC = "Being made of plants and leaves, podpeople are a target \ of herbivorous creatures such as goats.", )) + return perks /datum/species/pod/create_pref_damage_perks() diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synth/synth.dm b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synth/synth.dm new file mode 100644 index 000000000000..e789aa8b17d3 --- /dev/null +++ b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synth/synth.dm @@ -0,0 +1,435 @@ +// -- Synth additions (though barely functional) -- + +#define BODYPART_ID_SYNTH "synth" + +/datum/species/synth + name = "Synth" + id = SPECIES_SYNTH + sexes = FALSE + inherent_traits = list( + TRAIT_AGEUSIA, + TRAIT_NOBREATH, + TRAIT_NODISMEMBER, + TRAIT_NOHUNGER, + TRAIT_NOLIMBDISABLE, + TRAIT_NO_DNA_COPY, + TRAIT_VIRUSIMMUNE, + ) + inherent_biotypes = MOB_ROBOTIC|MOB_HUMANOID + meat = null + changesource_flags = MIRROR_BADMIN|MIRROR_PRIDE|MIRROR_MAGIC + species_language_holder = /datum/language_holder/synthetic + + bodypart_overrides = list( + BODY_ZONE_HEAD = /obj/item/bodypart/head/synth, + BODY_ZONE_CHEST = /obj/item/bodypart/chest/synth, + BODY_ZONE_R_ARM = /obj/item/bodypart/arm/right/synth, + BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left/synth, + BODY_ZONE_R_LEG = /obj/item/bodypart/leg/right/synth, + BODY_ZONE_L_LEG = /obj/item/bodypart/leg/left/synth, + ) + mutantbrain = /obj/item/organ/internal/brain/cybernetic + mutanttongue = /obj/item/organ/internal/tongue/robot + mutantstomach = /obj/item/organ/internal/stomach/cybernetic/tier2 + mutantappendix = null + mutantheart = /obj/item/organ/internal/heart/cybernetic/tier2 + mutantliver = /obj/item/organ/internal/liver/cybernetic/tier2 + mutantlungs = null + mutanteyes = /obj/item/organ/internal/eyes/robotic + mutantears = /obj/item/organ/internal/ears/cybernetic + species_pain_mod = 0.2 + /// Reference to the species we're disguised as. + VAR_FINAL/datum/species/disguise_species + /// If TRUE, synth limbs will update when attached and detached. + /// If FALSE, they will retain their disguise appearance forever. + /// Changing this at runtime will not affect anything + var/limb_updates_on_change = TRUE + /// Species which generally work well with synth, and can be disguised as. + var/list/valid_species = list( + SPECIES_ABDUCTOR, + SPECIES_FELINE, + SPECIES_HUMAN, + SPECIES_LIZARD, + SPECIES_MOTH, + SPECIES_ORNITHID, + ) + /// Reference to the action we give Synths to change species + var/datum/action/cooldown/change_disguise/disguise_action + /// Typepath to species to disguise set on species gain, for code shenanigans + var/initial_disguise + /// If health is lower than this %, the synth will start to show signs of damage. + var/disuise_damage_threshold = 25 + +/datum/species/synth/on_species_gain(mob/living/carbon/human/synth, datum/species/old_species) + . = ..() + synth.AddComponent(/datum/component/ion_storm_randomization) + synth.set_safe_hunger_level() + + if(limb_updates_on_change) + RegisterSignal(synth, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(disguise_damage)) + + disguise_action = new(src) + disguise_action.Grant(synth) + + if(initial_disguise) + disguise_as(synth, initial_disguise) + +/datum/species/synth/on_species_loss(mob/living/carbon/human/synth) + qdel(synth.GetComponent(/datum/component/ion_storm_randomization)) + drop_disguise(synth) + UnregisterSignal(synth, COMSIG_CARBON_LIMB_DAMAGED) + + for(var/obj/item/bodypart/limb as anything in synth.bodyparts) + if(initial(limb.limb_id) == BODYPART_ID_SYNTH) + limb.change_exempt_flags &= ~BP_BLOCK_CHANGE_SPECIES + + QDEL_NULL(disguise_action) + return ..() + +/datum/species/synth/get_species_description() + return "While they appear organic, Synths are secretly Androids disguised as the various species of the Galaxy." + +/datum/species/synth/get_species_lore() + return list( + "The reasons for a Synth's existence can vary. \ + Some were created as robotic assistants with a fresh coat of paint, to acclimate better to their organic counterparts. \ + Others were designed to be spies, infiltrating the ranks of other species to gather information. \ + There are even rumors some Synths are not even aware they ARE synthetic, and truly 'believe' they are the species they are disguised as. \ + Regardless of their origins, Synths are a diverse and mysterious group of beings." + ) + +/datum/species/synth/create_pref_unique_perks() + var/list/perks = list() + + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = FA_ICON_ROBOT, + SPECIES_PERK_NAME = "Robot Rock", + SPECIES_PERK_DESC = "Synths are robotic instead of organic, and as such may be affected by or immune to some things \ + normal humanoids are or aren't.", + )) + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = FA_ICON_MEDKIT, + SPECIES_PERK_NAME = "Partially Organic", + SPECIES_PERK_DESC = "Your limbs are part organic, part synthetic. \ + Both organic (sutures, meshes) and synthetic (welder, cabling) healing methods work on you.", + )) + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = FA_ICON_USER_SECRET, + SPECIES_PERK_NAME = "Incognito Mode", + SPECIES_PERK_DESC = "Synths are secretly synthetic androids that disguise as another species.", + )) + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, + SPECIES_PERK_ICON = FA_ICON_SHIELD_ALT, + SPECIES_PERK_NAME = "Silicon Supremecy", + SPECIES_PERK_DESC = "Being synthetic, Synths gain many resistances that come \ + with silicons. They're immune to viruses, dismemberment, having \ + limbs disabled, and they don't need to eat or breath.", + )) + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, + SPECIES_PERK_ICON =FA_ICON_THEATER_MASKS, + SPECIES_PERK_NAME = "Full Copy", + SPECIES_PERK_DESC = "Synths take on some the traits of species they disguise as. \ + This includes both positive and negative.", + )) + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = FA_ICON_USER_COG, + SPECIES_PERK_NAME = "Error: Disguise Failure", + SPECIES_PERK_DESC = "Ion Storms, can temporarily disrupt your disguise, \ + causing some of your features to change sporatically.", + )) + perks += list(list( + SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, + SPECIES_PERK_ICON = FA_ICON_WRENCH, + SPECIES_PERK_NAME = "Error: Damage Sustained", + SPECIES_PERK_DESC = "Physical damage to your synthetic body can cause your disguise to fail, \ + revealing your true form.", + )) + return perks + +/datum/species/synth/handle_body(mob/living/carbon/human/species_human) + if(disguise_species) + return disguise_species.handle_body(species_human) + return ..() + +/datum/species/synth/handle_mutant_bodyparts(mob/living/carbon/human/source, forced_colour) + if(disguise_species) + return disguise_species.handle_mutant_bodyparts(source, forced_colour) + return ..() + +/datum/species/synth/regenerate_organs(mob/living/carbon/organ_holder, datum/species/old_species, replace_current = TRUE, list/excluded_zones, visual_only = FALSE) + . = ..() + disguise_species?.regenerate_organs(organ_holder, replace_current = FALSE, excluded_zones = excluded_zones, visual_only = visual_only) + +/datum/species/synth/proc/disguise_as(mob/living/carbon/human/synth, datum/species/new_species_type) + if(ispath(new_species_type, /datum/species/synth)) + CRASH("disguise_as a synth as a synth, very funny.") + + if(istype(new_species_type, /datum/species)) + CRASH("disguise_as being passed species datum when it should be passed species typepath") + + drop_disguise(synth, skip_bodyparts = TRUE) + + disguise_species = new new_species_type() + + update_no_equip_flags(synth, no_equip_flags | disguise_species.no_equip_flags) + sexes = disguise_species.sexes + name = disguise_species.name + fixed_mut_color = disguise_species.fixed_mut_color + hair_color = disguise_species.hair_color + + synth.add_traits(disguise_species.inherent_traits, "synth_disguise_[SPECIES_TRAIT]") + + synth.update_body(TRUE) + regenerate_organs(synth, replace_current = FALSE) + + if(limb_updates_on_change) + for(var/obj/item/bodypart/part as anything in synth.bodyparts) + limb_gained(synth, part, update = FALSE) + + synth.update_body_parts(TRUE) + RegisterSignal(synth, COMSIG_CARBON_REMOVE_LIMB, PROC_REF(limb_lost_sig)) + RegisterSignal(synth, COMSIG_CARBON_ATTACH_LIMB, PROC_REF(limb_gained_sig)) + +/datum/species/synth/proc/drop_disguise(mob/living/carbon/human/synth, skip_bodyparts = FALSE) + if(isnull(disguise_species)) + return + + update_no_equip_flags(synth, initial(no_equip_flags)) + sexes = initial(sexes) + name = initial(name) + fixed_mut_color = initial(fixed_mut_color) + hair_color = initial(hair_color) + + synth.remove_traits(disguise_species.inherent_traits, "synth_disguise_[SPECIES_TRAIT]") + + if(limb_updates_on_change) + if(!skip_bodyparts) + for(var/obj/item/bodypart/part as anything in synth.bodyparts) + limb_lost(synth, part, update = FALSE) + + synth.update_body_parts(TRUE) + UnregisterSignal(synth, COMSIG_CARBON_REMOVE_LIMB) + UnregisterSignal(synth, COMSIG_CARBON_ATTACH_LIMB) + + QDEL_NULL(disguise_species) + synth.update_body(TRUE) + regenerate_organs(synth) + +/datum/species/synth/proc/limb_lost_sig(mob/living/carbon/human/source, obj/item/bodypart/limb, ...) + SIGNAL_HANDLER + + if(QDELING(limb)) + return + if(!limb_lost(source, limb, update = TRUE)) + return + source.visible_message(span_warning("[source]'s [limb.plaintext_zone] changes appearance!")) + +/datum/species/synth/proc/limb_lost(mob/living/carbon/human/synth, obj/item/bodypart/limb, update = FALSE) + if(initial(limb.limb_id) != BODYPART_ID_SYNTH) + return FALSE + + limb.change_appearance_into(limb, update) + return TRUE + +/datum/species/synth/proc/limb_gained_sig(mob/living/carbon/human/source, obj/item/bodypart/limb, ...) + SIGNAL_HANDLER + + if(!limb_gained(source, limb, update = TRUE)) + return + source.visible_message(span_warning("[source]'s [limb.plaintext_zone] changes appearance!")) + +/datum/species/synth/proc/limb_gained(mob/living/carbon/human/synth, obj/item/bodypart/limb, update = FALSE) + if(initial(limb.limb_id) != BODYPART_ID_SYNTH) + return FALSE + + limb.change_appearance_into(disguise_species.bodypart_overrides[limb.body_zone], update) + return TRUE + +/datum/species/synth/proc/disguise_damage(mob/living/carbon/human/synth) + SIGNAL_HANDLER + + if(!limb_updates_on_change) + return + + var/list/obj/item/bodypart/changed_limbs = list() + for(var/obj/item/bodypart/limb as anything in synth.bodyparts) + var/below_threshold = (limb.max_damage - limb.get_damage()) / limb.max_damage * 100 <= disuise_damage_threshold + if(limb.limb_id == BODYPART_ID_SYNTH) + if(below_threshold) + limb_gained(synth, limb, update = FALSE) + changed_limbs += limb + else + if(!below_threshold) + limb_lost(synth, limb, update = FALSE) + changed_limbs += limb + + var/num_changes = length(changed_limbs) + if(num_changes > 0) + if(num_changes == length(synth.bodyparts)) + synth.visible_message(span_danger("[synth] changes appearance!")) + else if(num_changes == 1) + synth.visible_message(span_warning("[synth]'s [changed_limbs[1].plaintext_zone] changes appearance!")) + synth.update_body_parts(TRUE) + + var/obj/item/organ/internal/tongue/tongue = synth.get_organ_slot(ORGAN_SLOT_TONGUE) + if(!isnull(tongue)) + if((synth.getBruteLoss() + synth.getFireLoss()) / synth.maxHealth * 100 <= disuise_damage_threshold) + tongue.temp_say_mod = "whirrs" + else if(tongue.temp_say_mod == "whirrs") + tongue.temp_say_mod = null + +/// Like change appearance, but passing it a bodypart will change the appearance to that of the bodypart. +/obj/item/bodypart/proc/change_appearance_into(obj/item/bodypart/other_part, update = TRUE) + draw_color = initial(other_part.draw_color) + species_color = initial(other_part.species_color) + skin_tone = initial(other_part.skin_tone) + icon_state = initial(other_part.icon_state) + icon = initial(other_part.icon) + icon_static = initial(other_part.icon_static) + icon_greyscale = initial(other_part.icon_greyscale) + limb_id = initial(other_part.limb_id) + should_draw_greyscale = initial(other_part.should_draw_greyscale) + is_dimorphic = initial(other_part.is_dimorphic) + + if(!update) + return + + if(owner) + owner.update_body_parts(TRUE) + else + update_icon_dropped() + +/datum/action/cooldown/change_disguise + name = "Change Disguise Species" + desc = "Changes your disguise to another species." + button_icon_state = "chameleon_outfit" + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_INCAPACITATED + cooldown_time = 6 SECONDS + +/datum/action/cooldown/change_disguise/Activate(atom/target) + . = TRUE + + var/mob/living/carbon/human/synth = owner + var/datum/species/synth/synth_species = synth.dna.species + var/list/synth_disguise_species = list() + for(var/species_id in get_selectable_species() & synth_species.valid_species) + var/datum/species/species_type = GLOB.species_list[species_id] + synth_disguise_species[initial(species_type.name)] = species_type + + synth_disguise_species["(Drop Disguise)"] = /datum/species/synth + + var/picked = tgui_input_list( + owner, + "Pick a disguise. Note, you do not gain all abilities (or downsides) of the select species.", + "Synth Disguise", + synth_disguise_species, + synth_species.disguise_species?.name, + ) + if(!picked || QDELETED(src) || QDELETED(synth_species) || QDELETED(synth) || !IsAvailable()) + return + var/picked_species = synth_disguise_species[picked] + if(ispath(picked_species, /datum/species/synth)) + synth_species.drop_disguise(synth) + return + + if(!istype(synth_species.disguise_species, picked_species)) + synth_species.disguise_as(synth, synth_disguise_species[picked]) + return + +/obj/item/bodypart/head/change_appearance_into(obj/item/bodypart/head/other_part, update = TRUE) + head_flags = initial(other_part.head_flags) + return ..() + +/obj/item/bodypart/head/synth + limb_id = BODYPART_ID_SYNTH + icon_static = 'icons/mob/human/bodyparts.dmi' + icon = 'icons/mob/human/bodyparts.dmi' + icon_state = "synth_head" + should_draw_greyscale = FALSE + obj_flags = CONDUCTS_ELECTRICITY + is_dimorphic = FALSE + bodytype = BODYTYPE_HUMANOID | BODYTYPE_ROBOTIC + brute_modifier = 0.8 + burn_modifier = 0.8 + biological_state = BIO_ROBOTIC + head_flags = NONE + change_exempt_flags = BP_BLOCK_CHANGE_SPECIES + +/obj/item/bodypart/chest/synth + limb_id = BODYPART_ID_SYNTH + icon_static = 'icons/mob/human/bodyparts.dmi' + icon = 'icons/mob/human/bodyparts.dmi' + icon_state = "synth_chest" + obj_flags = CONDUCTS_ELECTRICITY + is_dimorphic = FALSE + should_draw_greyscale = FALSE + bodytype = BODYTYPE_HUMANOID | BODYTYPE_ROBOTIC + brute_modifier = 0.8 + burn_modifier = 0.8 + biological_state = BIO_ROBOTIC + wing_types = list(/obj/item/organ/external/wings/functional/angel, /obj/item/organ/external/wings/functional/robotic) + change_exempt_flags = BP_BLOCK_CHANGE_SPECIES + +/obj/item/bodypart/arm/right/synth + limb_id = BODYPART_ID_SYNTH + icon_static = 'icons/mob/human/bodyparts.dmi' + icon = 'icons/mob/human/bodyparts.dmi' + icon_state = "synth_r_arm" + obj_flags = CONDUCTS_ELECTRICITY + is_dimorphic = FALSE + should_draw_greyscale = FALSE + bodytype = BODYTYPE_HUMANOID | BODYTYPE_ROBOTIC + brute_modifier = 0.8 + burn_modifier = 0.8 + biological_state = BIO_ROBOTIC + change_exempt_flags = BP_BLOCK_CHANGE_SPECIES + +/obj/item/bodypart/arm/left/synth + limb_id = BODYPART_ID_SYNTH + icon_static = 'icons/mob/human/bodyparts.dmi' + icon = 'icons/mob/human/bodyparts.dmi' + icon_state = "synth_l_arm" + obj_flags = CONDUCTS_ELECTRICITY + is_dimorphic = FALSE + should_draw_greyscale = FALSE + bodytype = BODYTYPE_HUMANOID | BODYTYPE_ROBOTIC + brute_modifier = 0.8 + burn_modifier = 0.8 + biological_state = BIO_ROBOTIC + change_exempt_flags = BP_BLOCK_CHANGE_SPECIES + +/obj/item/bodypart/leg/right/synth + limb_id = BODYPART_ID_SYNTH + icon_static = 'icons/mob/human/bodyparts.dmi' + icon = 'icons/mob/human/bodyparts.dmi' + icon_state = "synth_r_leg" + obj_flags = CONDUCTS_ELECTRICITY + is_dimorphic = FALSE + should_draw_greyscale = FALSE + bodytype = BODYTYPE_HUMANOID | BODYTYPE_ROBOTIC + brute_modifier = 0.8 + burn_modifier = 0.8 + biological_state = BIO_ROBOTIC + change_exempt_flags = BP_BLOCK_CHANGE_SPECIES + +/obj/item/bodypart/leg/left/synth + limb_id = BODYPART_ID_SYNTH + icon_static = 'icons/mob/human/bodyparts.dmi' + icon = 'icons/mob/human/bodyparts.dmi' + icon_state = "synth_l_leg" + obj_flags = CONDUCTS_ELECTRICITY + is_dimorphic = FALSE + should_draw_greyscale = FALSE + bodytype = BODYTYPE_HUMANOID | BODYTYPE_ROBOTIC + brute_modifier = 0.8 + burn_modifier = 0.8 + biological_state = BIO_ROBOTIC + change_exempt_flags = BP_BLOCK_CHANGE_SPECIES + +#undef BODYPART_ID_SYNTH diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synth/synth_ion.dm b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synth/synth_ion.dm new file mode 100644 index 000000000000..9215c2d1d416 --- /dev/null +++ b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synth/synth_ion.dm @@ -0,0 +1,62 @@ +/// Whenver an ion storm rolls through, synthetic species may have issues +/datum/round_event/ion_storm/start() + . = ..() + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_ION_STORM) + +/datum/component/ion_storm_randomization + var/datum/dna/original_dna + +/datum/component/ion_storm_randomization/Initialize(...) + if(!ishuman(parent)) + return COMPONENT_INCOMPATIBLE + + RegisterGlobalSignal(COMSIG_GLOB_ION_STORM, PROC_REF(on_ion_storm)) + +/datum/component/ion_storm_randomization/Destroy() + UnregisterGlobalSignal(COMSIG_GLOB_ION_STORM) + if(!QDELING(parent) && !isnull(original_dna)) + return_to_normal() + QDEL_NULL(original_dna) + return ..() + +/datum/component/ion_storm_randomization/proc/on_ion_storm(datum/source) + SIGNAL_HANDLER + + var/mob/living/carbon/human/target = parent + var/datum/species/synth/synth = target.dna.species + if(isnull(synth.disguise_species)) + to_chat(target, span_danger("[ion_num()]. I0n1c D1strbance d3tcted!")) + return + + to_chat(target, span_userdanger("[ion_num()]. I0N1C D1STRBANCE D3TCTED!")) + original_dna = new() + target.dna.copy_dna(original_dna) + target.adjust_slurring(40 SECONDS) + for(var/i in 1 to rand(2, 4)) + addtimer(CALLBACK(src, PROC_REF(mutate_after_time), target), i * 6 SECONDS, TIMER_DELETE_ME) + + addtimer(CALLBACK(src, PROC_REF(return_to_normal), target), 30 SECONDS, TIMER_DELETE_ME) + +/// For use in a callback in [on_ion_storm]. +/datum/component/ion_storm_randomization/proc/mutate_after_time() + var/mob/living/carbon/human/target = parent + var/datum/species/synth/synth = target.dna.species + if(isnull(synth.disguise_species)) + return + + to_chat(target, span_warning("Your disguise glitches!")) + target.random_mutate_unique_features() + target.random_mutate_unique_identity() + +/// For use in a callback in [on_ion_storm]. +/datum/component/ion_storm_randomization/proc/return_to_normal() + var/mob/living/carbon/human/target = parent + var/datum/species/synth/synth = target.dna.species + if(!isnull(synth.disguise_species)) + to_chat(target, span_notice("Your disguise returns to normal.")) + target.dna.features = original_dna.features.Copy() + target.dna.unique_features = original_dna.unique_features + target.dna.unique_enzymes = original_dna.unique_enzymes + target.dna.unique_identity = original_dna.unique_identity + target.updateappearance() + QDEL_NULL(original_dna) diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synths.dm b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synths.dm deleted file mode 100644 index 33b0946ae531..000000000000 --- a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/synths.dm +++ /dev/null @@ -1,217 +0,0 @@ -// -- Synth additions (though barely functional) -- - -#define COMSIG_GLOB_ION_STORM "!ion_storm" - -/// Whenver an ion storm rolls through, synthetic species may have issues -/datum/round_event/ion_storm/start() - . = ..() - SEND_GLOBAL_SIGNAL(COMSIG_GLOB_ION_STORM) - -/datum/species/synth - name = "Synth" //inherited from the real species, for health scanners and things - id = SPECIES_SYNTH - // say_mod = "beep boops" //inherited from a user's real species - sexes = FALSE - inherent_traits = list( - TRAIT_VIRUSIMMUNE, - TRAIT_NODISMEMBER, - TRAIT_NOLIMBDISABLE, - TRAIT_NOHUNGER, - TRAIT_NOBREATH, - TRAIT_NO_DNA_COPY, - ) - inherent_biotypes = MOB_ROBOTIC|MOB_HUMANOID - meat = null - changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC - species_language_holder = /datum/language_holder/synthetic - /// 2021: - /// If your health becomes equal to or less than this value, your disguise is supposed to break. - /// Unfortunately, that feature currently isn't implemented, so currently, all this threshold is - /// used for is (I kid you not) determining whether or not your speech uses SPAN_CLOWN while you're - /// disguised as a bananium golem. See the handle_speech() proc further down in this file for more information on that check. - /// 2023 update: - /// Bananium golems are removed, this is now completely dead code. God save us all - var/disguise_fail_health = 75 - ///a species to do most of our work for us, unless we're damaged - var/datum/species/fake_species - ///for getting these values back for assume_disguise() - var/list/initial_species_traits - var/list/initial_inherent_traits - - /// Hack ahead - /// We need to react to [COMSIG_GLOB_ION_STORM], unfortunately we can't do that easily in a species, so we keep a weakref from Grant - var/datum/weakref/silly_owner_weakref - -/datum/species/synth/New() - initial_inherent_traits = inherent_traits.Copy() - return ..() - -/datum/species/synth/on_species_gain(mob/living/carbon/human/H, datum/species/old_species) - . = ..() - assume_disguise(old_species, H) - RegisterSignal(SSdcs, COMSIG_GLOB_ION_STORM, PROC_REF(on_ion_storm)) - H.set_safe_hunger_level() - silly_owner_weakref = WEAKREF(H) - // Adds robot wings to chest wing options (if it is not already robotic) - var/obj/item/bodypart/chest/chest = H.get_bodypart(BODY_ZONE_CHEST) - if(!IS_ROBOTIC_LIMB(chest)) - chest.wing_types |= /obj/item/organ/external/wings/functional/robotic - -/datum/species/synth/on_species_loss(mob/living/carbon/human/H) - . = ..() - UnregisterSignal(SSdcs, COMSIG_GLOB_ION_STORM) - silly_owner_weakref = null - var/obj/item/bodypart/chest/chest = H.get_bodypart(BODY_ZONE_CHEST) - if(!IS_ROBOTIC_LIMB(chest)) - chest.wing_types -= /obj/item/organ/external/wings/functional/robotic - -/datum/species/synth/get_species_description() - return "Synths are disguised robots." - -/datum/species/synth/get_species_lore() - return list("Synth lore.") - -/datum/species/synth/create_pref_unique_perks() - var/list/perks = list() - - perks += list(list( - SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, - SPECIES_PERK_ICON = "robot", - SPECIES_PERK_NAME = "Robot Rock", - SPECIES_PERK_DESC = "Synths are robotic instead of organic, and as such may be affected by or immune to some things \ - normal humanoids are or aren't.", - )) - perks += list(list( - SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, - SPECIES_PERK_ICON = "user-secret", - SPECIES_PERK_NAME = "Incognito Mode", - SPECIES_PERK_DESC = "Synths are secretly synthetic androids that disguise as another species.", - )) - perks += list(list( - SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, - SPECIES_PERK_ICON = "shield-alt", - SPECIES_PERK_NAME = "Silicon Supremecy", - SPECIES_PERK_DESC = "Being synthetic, Synths gain many resistances that come \ - with silicons. They're immune to viruses, dismemberment, having \ - limbs disabled, and they don't need to eat or breath.", - )) - perks += list(list( - SPECIES_PERK_TYPE = SPECIES_NEUTRAL_PERK, - SPECIES_PERK_ICON = "theater-masks", - SPECIES_PERK_NAME = "Full Copy", - SPECIES_PERK_DESC = "Synths take on all the traits of species they disguise as, \ - both positive and negative.", - )) - perks += list(list( - SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK, - SPECIES_PERK_ICON = "users-cog", - SPECIES_PERK_NAME = "Error: Disguise Failure", - SPECIES_PERK_DESC = "Ion Storms can temporarily mess with your disguise, \ - causing some of your features to change sporatically.", - )) - return perks - -/datum/species/synth/create_pref_language_perk() - var/list/perks = list() - - perks += list(list( - SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK, - SPECIES_PERK_ICON = "language", - SPECIES_PERK_NAME = "Language Processor", - SPECIES_PERK_DESC = "Synths can understand and speak a wide variety of \ - additional languages, including Encoded Audio Language, the language \ - of silicon and synthetics.", - )) - - return perks - -/* -/datum/species/synth/handle_chemical(datum/reagent/chem, mob/living/carbon/human/affected, seconds_per_tick, times_fired) - if(chem.type == /datum/reagent/medicine/c2/synthflesh) - chem.expose_mob(affected, TOUCH, 2 * REAGENTS_EFFECT_MULTIPLIER * seconds_per_tick, FALSE) //heal a little - affected.reagents.remove_reagent(chem.type, REAGENTS_METABOLISM * seconds_per_tick) - return TRUE - return ..() -*/ - -/datum/species/synth/proc/assume_disguise(datum/species/S, mob/living/carbon/human/H) - if(S && !istype(S, type)) - name = S.name - sexes = S.sexes - inherent_traits = initial_inherent_traits.Copy() - inherent_traits |= S.inherent_traits - meat = S.meat - mutant_bodyparts = S.mutant_bodyparts.Copy() - mutant_organs = S.mutant_organs.Copy() - no_equip_flags = S.no_equip_flags - fixed_mut_color = S.fixed_mut_color - hair_color = S.hair_color - fake_species = new S.type - else - name = initial(name) - inherent_traits = initial_inherent_traits.Copy() - mutant_bodyparts = list() - no_equip_flags = NONE - qdel(fake_species) - fake_species = null - meat = initial(meat) - sexes = 0 - fixed_mut_color = "" - hair_color = "" - - for(var/X in H.bodyparts) //propagates the damage_overlay changes - var/obj/item/bodypart/BP = X - BP.update_limb() - H.update_body_parts() //to update limb icon cache with the new damage overlays - - var/obj/item/organ/internal/tongue/disguise_tongue = initial(fake_species.mutanttongue) // handles the say_mod for species disguise. - // this (below) is a major make or break for the code, this should set the synth tongue to be identical to the default species tongue the mob is disguised as. - var/obj/item/organ/internal/tongue/my_tongue = H.get_organ_by_type(/obj/item/organ/internal/tongue) - if(my_tongue && disguise_tongue) - my_tongue.say_mod = initial(disguise_tongue.say_mod) - -/datum/species/synth/handle_body(mob/living/carbon/human/H) - if(fake_species) - fake_species.handle_body(H) - else - return ..() - -/datum/species/synth/handle_mutant_bodyparts(mob/living/carbon/human/H, forced_colour) - if(fake_species) - fake_species.handle_mutant_bodyparts(H,forced_colour) - else - return ..() - -/datum/species/synth/prepare_human_for_preview(mob/living/carbon/human/human) - human.dna.transfer_identity(human) // Makes the synth look like... a synth. - -/datum/species/synth/proc/on_ion_storm(datum/source) - SIGNAL_HANDLER - - var/mob/living/carbon/human/target = silly_owner_weakref?.resolve() - if(QDELETED(target)) - return - - to_chat(target, span_userdanger("[ion_num()]. I0N1C D1STRBANCE D3TCTED!")) - target.adjust_slurring(40 SECONDS) - var/datum/dna/original_dna = new - target.dna.copy_dna(original_dna) - for(var/i in 1 to rand(2, 4)) - addtimer(CALLBACK(src, PROC_REF(mutate_after_time), target), i * 3 SECONDS) - addtimer(CALLBACK(src, PROC_REF(return_to_normal), target, original_dna), 30 SECONDS) - -/// For use in a callback in [on_ion_storm]. -/datum/species/synth/proc/mutate_after_time(mob/living/carbon/human/target) - to_chat(target, span_warning("Your disguise glitches!")) - target.random_mutate_unique_features() - target.random_mutate_unique_identity() - -/// For use in a callback in [on_ion_storm]. -/datum/species/synth/proc/return_to_normal(mob/living/carbon/human/target, datum/dna/original) - to_chat(target, span_notice("Your disguise returns to normal.")) - target.dna.features = original.features.Copy() - target.dna.unique_features = original.unique_features - target.dna.unique_enzymes = original.unique_enzymes - target.dna.unique_identity = original.unique_identity - target.updateappearance() - qdel(original) diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/_modular_species_features.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/_modular_species_features.tsx index 8266e85f51a3..ee3b4d742f90 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/_modular_species_features.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/_modular_species_features.tsx @@ -4,6 +4,8 @@ import { FeatureChoiced, FeatureColorInput, FeatureDropdownInput, + FeatureNumberInput, + FeatureNumeric, FeatureToggle, } from './base'; @@ -18,6 +20,19 @@ export const hair_lizard: FeatureToggle = { component: CheckboxInput, }; +export const feature_synth_species: FeatureChoiced = { + name: 'Synth Species', + description: 'Determines what species you spawn disguised as.', + component: FeatureDropdownInput, +}; + +export const feature_synth_damage_threshold: FeatureNumeric = { + name: 'Synth Damage Threshold', + description: + 'Determines how much damage you can take before your disguise is broken.', + component: FeatureNumberInput, +}; + export const feature_arm_wings: FeatureChoiced = { name: 'Arm Wings', component: FeatureDropdownInput,