From 2620ad735b5d33a45bdf9db7d99c304d8b20f0f5 Mon Sep 17 00:00:00 2001 From: Peter Wedder Date: Fri, 20 Dec 2024 01:46:07 +0200 Subject: [PATCH] Add new midround antag: the divergent clone (#37334) * Add new midround antag: the divergent clone Divergent clones are perfect copies of existing, potentially still living crew members, created in a freak cloner accident. They can remember the original's antag status, and may sometimes be evil which means they get traitor objectives. If the original is also a traitor, the evil clone will share their objectives. * give the spirits-in-waiting the default ghost sprite instead * skip records check if forcespawn is enabled * Forgot the role * Review fixes * duh --- __DEFINES/_macros.dm | 2 + __DEFINES/role_datums_defines.dm | 1 + .../dynamic/dynamic_rulesets_midround.dm | 81 +++ .../gamemode/factions/bloodcult/bloodcult.dm | 2 +- .../bloodcult/bloodcult_runespells.dm | 2 +- .../gamemode/factions/legacy_cult/cult.dm | 2 +- code/datums/gamemode/factions/plague_mice.dm | 3 +- .../gamemode/factions/syndicate/nukeops.dm | 2 +- code/datums/gamemode/factions/vox_shoal.dm | 2 +- code/datums/gamemode/misc_gamemode_procs.dm | 6 +- .../gamemode/objectives/divergentclone.dm | 63 ++ code/datums/gamemode/powers/powers.dm | 2 +- code/datums/gamemode/role/catbeast.dm | 3 +- code/datums/gamemode/role/divergentclone.dm | 605 ++++++++++++++++++ code/datums/gamemode/role/syndicate.dm | 4 +- code/datums/mind.dm | 29 +- code/datums/outfit/outfit.dm | 2 +- code/game/jobs/job/whitelisted.dm | 2 +- code/game/jobs/job_controller.dm | 4 +- .../objects/items/devices/PDA/cart/misc.dm | 2 +- .../implants/types/adrenalin_implant.dm | 2 +- .../types/compressed_matter_implant.dm | 2 +- .../implants/types/explosive_implant.dm | 2 +- .../weapons/implants/types/freedom_implant.dm | 2 +- .../weapons/implants/types/uplink_implant.dm | 2 +- code/modules/client/preferences.dm | 3 + code/modules/medical/cloning.dm | 444 +++++++++---- code/modules/medical/computer/cloning.dm | 4 +- .../mob/living/carbon/alien/humanoid/death.dm | 2 +- .../mob/living/carbon/alien/larva/death.dm | 2 +- code/modules/mob/living/carbon/brain/death.dm | 2 +- code/modules/mob/living/carbon/human/death.dm | 2 +- code/modules/mob/living/silicon/ai/ai.dm | 2 +- code/modules/mob/living/silicon/ai/death.dm | 2 +- code/modules/mob/living/silicon/ai/login.dm | 2 +- .../modules/mob/living/silicon/robot/death.dm | 2 +- .../modules/mob/living/silicon/robot/login.dm | 2 +- .../modules/mob/living/silicon/robot/robot.dm | 2 +- code/modules/mob/mob.dm | 4 +- code/modules/mob/new_player/new_player.dm | 2 +- code/modules/spells/changeling/absorb_dna.dm | 9 +- code/modules/spells/changeling/split.dm | 2 +- icons/logos.dmi | Bin 32685 -> 33904 bytes vgstation13.dme | 2 + 44 files changed, 1135 insertions(+), 184 deletions(-) create mode 100644 code/datums/gamemode/objectives/divergentclone.dm create mode 100644 code/datums/gamemode/role/divergentclone.dm diff --git a/__DEFINES/_macros.dm b/__DEFINES/_macros.dm index 205e10e70ee0..c492549e0c96 100644 --- a/__DEFINES/_macros.dm +++ b/__DEFINES/_macros.dm @@ -333,6 +333,8 @@ #define istimeagent(H) (H.mind && (H.mind.GetRole(TIMEAGENT) || (H.mind.GetRole(TIMEAGENTTWIN)))) +#define isdivergentclone(H) (H.mind && (H.mind.GetRole(DIVERGENTCLONE))) + #define isERT(H) (H.mind && H.mind.GetRole(RESPONDER)) #define isclownling(H) (H.mind && H.mind.GetRole(CLOWN_LING)) diff --git a/__DEFINES/role_datums_defines.dm b/__DEFINES/role_datums_defines.dm index f917f1b71fed..a3429fa04951 100644 --- a/__DEFINES/role_datums_defines.dm +++ b/__DEFINES/role_datums_defines.dm @@ -86,6 +86,7 @@ #define JUDGE "judge" #define GRUE "grue" #define NANOTRASENOFFICIAL "nanotrasen official" +#define DIVERGENTCLONE "divergent clone" #define GREET_DEFAULT "default" #define GREET_ROUNDSTART "roundstart" diff --git a/code/datums/gamemode/dynamic/dynamic_rulesets_midround.dm b/code/datums/gamemode/dynamic/dynamic_rulesets_midround.dm index a6ee5b3c71cf..17ac86b3c9e5 100644 --- a/code/datums/gamemode/dynamic/dynamic_rulesets_midround.dm +++ b/code/datums/gamemode/dynamic/dynamic_rulesets_midround.dm @@ -1095,3 +1095,84 @@ requirements = list(10,10,10,10,10,10,10,10,10,10) logo = "gun-logo" repeatable = TRUE + +////////////////////////////////////////////// +// // +// DIVERGENT CLONE // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/divergentclone + name = "Divergent Clone" + role_category = /datum/role/divergentclone + required_candidates = 1 + weight = 3 + weight_category = "Clone" + cost = 5 + requirements = list(40,30,20,10,10,10,10,10,10,10) + high_population_requirement = 10 + logo = "divergentclone-logo" + repeatable = TRUE + makeBody = FALSE + flags = MINOR_RULESET + +/datum/dynamic_ruleset/midround/from_ghosts/divergentclone/trim_candidates() + ..() + for(var/mob/M in dead_players) + if(isdivergentclone(M)) + dead_players -= M + for(var/mob/M in list_observers) + if(isdivergentclone(M)) + list_observers -= M + + +/datum/dynamic_ruleset/midround/from_ghosts/divergentclone/ready(var/forced = 0) + if(!config.revival_cloning) + return 0 + + //Check that we have at least one ghost who isn't already a clone waiting to spawn + var/list/candies = dead_players + list_observers + var/list/valids[0] + for(var/mob/dead/observer/G in candies) + if(isdivergentclone(G)) + continue + valids += G + if(valids.len == 0) + if(forced) + message_admins("Tried to force divergent clone, but no valid candidates found.") + return 0 + + var/list/clonepods = list() + for(var/obj/machinery/cloning/clonepod/pod in machines) + //Check that the pod has cloned something before + if(pod.cloned_records.len) + clonepods += pod + if(!forced && clonepods.len == 0) + return 0 + return ..() + +/datum/dynamic_ruleset/midround/from_ghosts/divergentclone/finish_setup(mob/new_character, index) + //Create a new dummy body to create a new mind for the ghost. + var/L = get_turf(new_character) + var/mob/living/carbon/human/H = new /mob/living/carbon/human(pick(latejoin)) + H.ckey = new_character.ckey + H.fully_replace_character_name(null, "\improper spirit of a divergent clone") + + //Give the about-to-be-a-ghost mob the provisional role and objective + var/datum/role/divergentclone/new_role = new /datum/role/divergentclone(H.mind, override=TRUE) + new_role.ForgeObjectives() + new_role.AnnounceObjectives() + + var/mob/dead/observer/G = H.ghostize(FALSE) + //Give it a more spirity looking icon. + G.icon = initial(G.icon) + G.icon_state = initial(G.icon_state) + QDEL_NULL(H) + G.forceMove(L) + G.add_spell(new /spell/targeted/ghost/divergentclone) + + to_chat(G, "You were selected to be a divergent clone, but will not be spawned in yet!") + to_chat(G, "You have been granted the \"Spawn as Divergent Clone\" ghost spell. Use this near a cloning pod to spawn in as a divergent clone of someone who was cloned, or is currently being cloned, in that pod.") + to_chat(G, "Using this spell on an unoccupied cloning pod will allow you to choose a record of a person previously cloned in that pod. Using it on an occupied pod will cause you to become a twin of the person currently in the pod, and be ejected from the pod at the same time as them.") + to_chat(G, "Remember: you can only use this spell once, and re-entering your corpse will remove it permanently. In fact, for your convenience we have removed your ability to re-enter your corpse.") + diff --git a/code/datums/gamemode/factions/bloodcult/bloodcult.dm b/code/datums/gamemode/factions/bloodcult/bloodcult.dm index 0bbe23d826fd..f08928b9585d 100644 --- a/code/datums/gamemode/factions/bloodcult/bloodcult.dm +++ b/code/datums/gamemode/factions/bloodcult/bloodcult.dm @@ -506,7 +506,7 @@ if (cult_reminders.len) to_chat(R.antag.current, "Other cultists have shared some of their knowledge. It will be stored in your memory (check your Notes under the IC tab).") for (var/reminder in cult_reminders) - R.antag.store_memory("Shared Cultist Knowledge: [reminder].") + R.antag.store_memory("Shared Cultist Knowledge: [reminder].", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) previously_converted |= R.antag if (R.antag.name in deconverted) deconverted -= R.antag.name diff --git a/code/datums/gamemode/factions/bloodcult/bloodcult_runespells.dm b/code/datums/gamemode/factions/bloodcult/bloodcult_runespells.dm index e6e5f3714d0d..5d39e3b5b1ea 100644 --- a/code/datums/gamemode/factions/bloodcult/bloodcult_runespells.dm +++ b/code/datums/gamemode/factions/bloodcult/bloodcult_runespells.dm @@ -463,7 +463,7 @@ if (iscultist(M.current))//failsafe for cultist brains put in MMIs to_chat(M.current, "[user.real_name]'s voice echoes in your head, [reminder]") to_chat(M.current, "This message will be remembered by all current cultists, and by new converts as well.") - M.store_memory("Cult reminder: [text].") + M.store_memory("Cult reminder: [text].", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) for(var/mob/living/simple_animal/astral_projection/A in astral_projections) to_chat(A, "[user.real_name] communicates, [reminder]. (Cult reminder)") diff --git a/code/datums/gamemode/factions/legacy_cult/cult.dm b/code/datums/gamemode/factions/legacy_cult/cult.dm index e0f1e0670b35..f481d932adcc 100644 --- a/code/datums/gamemode/factions/legacy_cult/cult.dm +++ b/code/datums/gamemode/factions/legacy_cult/cult.dm @@ -160,7 +160,7 @@ var/global/list/rnwords = list("ire","ego","nahlizet","certum","veri","jatkaa"," word=pick(allwords) var/wordexp = "[cult_words[word]] is [word]..." to_chat(cult_mob, "You remember one thing from the dark teachings of your master... [wordexp]") - cult_mob.mind.store_memory("You remember that [wordexp]", 0, 0) + cult_mob.mind.store_memory("You remember that [wordexp]", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) /datum/faction/cult/narsie/AdminPanelEntry() var/list/dat = ..() diff --git a/code/datums/gamemode/factions/plague_mice.dm b/code/datums/gamemode/factions/plague_mice.dm index 46c9d2ed4768..3082cb3e1b0c 100644 --- a/code/datums/gamemode/factions/plague_mice.dm +++ b/code/datums/gamemode/factions/plague_mice.dm @@ -32,8 +32,7 @@ /* With the disease set-up, store the detials of the disease in the mouse's memory */ var/datum/mind/mouse_mind = R.antag - mouse_mind.store_memory(plague.get_info(TRUE), forced = 1) - mouse_mind.store_memory("
", forced = 1) + mouse_mind.store_memory(plague.get_info(TRUE), category=MIND_MEMORY_ANTAGONIST, forced = 1) var/dat = "You carry a deadly plague with the following traits:" dat += "
Strength / Robustness: [plague.strength]% / [plague.robustness]%" dat += "
Infection chance: [plague.infectionchance]%" diff --git a/code/datums/gamemode/factions/syndicate/nukeops.dm b/code/datums/gamemode/factions/syndicate/nukeops.dm index b9d12fd857cc..b7f0a8bfe684 100644 --- a/code/datums/gamemode/factions/syndicate/nukeops.dm +++ b/code/datums/gamemode/factions/syndicate/nukeops.dm @@ -194,7 +194,7 @@ nuke_name_assign(nuke_last_name(synd_mind.current), title, members) //Allows time for the rest of the syndies to be chosen if(nuke_code) - synd_mind.store_memory("Syndicate Nuclear Bomb Code: [nuke_code]", 0, 0) + synd_mind.store_memory("Syndicate Nuclear Bomb Code: [nuke_code]", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) to_chat(synd_mind.current, "The nuclear authorization code is: [nuke_code]
Make sure to share it with your subordinates.") var/obj/item/weapon/paper/P = new P.info = "The nuclear authorization code is: [nuke_code]" diff --git a/code/datums/gamemode/factions/vox_shoal.dm b/code/datums/gamemode/factions/vox_shoal.dm index b78a7dc23a5b..9e5546ef6a4d 100644 --- a/code/datums/gamemode/factions/vox_shoal.dm +++ b/code/datums/gamemode/factions/vox_shoal.dm @@ -175,7 +175,7 @@ var/list/potential_bonus_items = list( var/datum/outfit/striketeam/voxraider/concrete_outfit = new concrete_outfit.equip(vox) vox.regenerate_icons() - vox.store_memory("The priority items for the day are: [english_list(bonus_items_of_the_day)]") + vox.mind.store_memory("The priority items for the day are: [english_list(bonus_items_of_the_day)]", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) /* spawn() diff --git a/code/datums/gamemode/misc_gamemode_procs.dm b/code/datums/gamemode/misc_gamemode_procs.dm index 850ed9b5b799..f5c4b75adf2e 100644 --- a/code/datums/gamemode/misc_gamemode_procs.dm +++ b/code/datums/gamemode/misc_gamemode_procs.dm @@ -179,7 +179,7 @@ if(!apprentice) to_chat(wizard_mob, "You will find a list of available spells in your spell book. Choose your magic arsenal carefully.") to_chat(wizard_mob, "In your pockets you will find a teleport scroll. Use it as needed.") - wizard_mob.mind.store_memory("Remember: do not forget to prepare your spells.") + wizard_mob.mind.store_memory("Remember: do not forget to prepare your spells.", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) return 1 /proc/name_wizard(mob/living/carbon/human/wizard_mob, role_name = "Space Wizard") @@ -346,13 +346,13 @@ if (syndicate_code_phrase) var/phrases = syndicate_code_phrase.Join(", ") words += "Code Phrases: [phrases].
" - agent.mind.store_memory("Code Phrases: [phrases].") + agent.mind.store_memory("Code Phrases: [phrases].", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) else words += "Unfortunately, the Syndicate did not provide you with a code phrase.
" if (syndicate_code_response) var/response = syndicate_code_response.Join(", ") words += "Code Response: [response].
" - agent.mind.store_memory("Code Response: [response].") + agent.mind.store_memory("Code Response: [response].", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) else words += "Unfortunately, the Syndicate did not provide you with a code response.
" diff --git a/code/datums/gamemode/objectives/divergentclone.dm b/code/datums/gamemode/objectives/divergentclone.dm new file mode 100644 index 000000000000..8696a8409482 --- /dev/null +++ b/code/datums/gamemode/objectives/divergentclone.dm @@ -0,0 +1,63 @@ +/datum/objective/freeform/divergentclone_neutral + explanation_text = "Convince the world that you are the real , or at least as real as the other copy." + +/datum/objective/freeform/divergentclone_neutral/format_explanation() + return "Convince the world that you are the real [owner.name], or at least as real as the other copy." + +/datum/objective/freeform/divergentclone_neutral/PostAppend() + explanation_text = format_explanation() + return TRUE + +/datum/objective/freeform/divergentclone_evil + explanation_text = "Convince the world that you are the real at any cost, be that by talk, violence or subterfuge. Do not let anyone get in your way, including your original copy." + +/datum/objective/freeform/divergentclone_evil/format_explanation() + return "Convince the world that you are the real [owner.name] at any cost, be that by talk, violence or subterfuge. Do not let anyone get in your way, including your original copy." + +/datum/objective/freeform/divergentclone_evil/PostAppend() + explanation_text = format_explanation() + return TRUE + +/datum/objective/divergentclone/spawn_in + explanation_text = "Reincarnate as a divergent clone." + name = "Reincarnate" + +/datum/objective/divergentclone/spawn_in/IsFulfilled() + if(..()) + return TRUE + if(!owner || !owner.current) + return FALSE + + var/datum/role/divergentclone/role = owner.GetRole(DIVERGENTCLONE) + if(role?.has_spawned_in) + return TRUE + +/datum/objective/acquire_personal_id + explanation_text = "Acquire an ID card matching your name or DNA." + name = "Acquire personal ID card" + +/datum/objective/acquire_personal_id/IsFulfilled() + if(..()) + return TRUE + + if(!owner || !owner.current) + return FALSE + + for(var/obj/O in get_contents_in_object(owner.current)) + var/obj/item/weapon/card/id/I + if(istype(O, /obj/item/weapon/card/id)) + I = O + else if(istype(O, /obj/item/device/pda)) + var/obj/item/device/pda/P = O + I = P.id + else + continue + var/datum/dna/D = owner.current.dna + if((I?.dna_hash == D.unique_enzymes) || (I?.registered_name == owner.name)) + return TRUE + + return FALSE + + + + diff --git a/code/datums/gamemode/powers/powers.dm b/code/datums/gamemode/powers/powers.dm index 466621160e5c..dd8e9aea065c 100644 --- a/code/datums/gamemode/powers/powers.dm +++ b/code/datums/gamemode/powers/powers.dm @@ -25,7 +25,7 @@ if (granttext) to_chat(R.antag.current, "[granttext]") if (store_in_memory) - R.antag.store_memory("[granttext]") + R.antag.store_memory("[granttext]", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) R.current_powers += src role = R grant_spell() diff --git a/code/datums/gamemode/role/catbeast.dm b/code/datums/gamemode/role/catbeast.dm index 74c7fad2d3a3..0ef593a6e9ec 100644 --- a/code/datums/gamemode/role/catbeast.dm +++ b/code/datums/gamemode/role/catbeast.dm @@ -51,8 +51,7 @@ var/list/catbeast_names = list("Meowth","Fluffy","Subject 246","Experiment 35a", D1.origin = "Loose Catbeast" D1.makerandom(str, rob, anti, bad) H.infect_disease2(D1, 1, "Loose Catbeast") - antag.store_memory(D1.get_info(TRUE), forced = 1) - antag.store_memory("
") + antag.store_memory(D1.get_info(TRUE), category=MIND_MEMORY_ANTAGONIST, forced=TRUE) /datum/role/catbeast/proc/infect_catbeast_tier1(mob/living/carbon/human/H) var/list/anti = list( diff --git a/code/datums/gamemode/role/divergentclone.dm b/code/datums/gamemode/role/divergentclone.dm new file mode 100644 index 000000000000..8d44228cbb5d --- /dev/null +++ b/code/datums/gamemode/role/divergentclone.dm @@ -0,0 +1,605 @@ +/** + Divergent clone + + The cloning machine has malfunctioned, and now there's a copy of you claiming to be the original! +**/ + +/datum/role/divergentclone + name = DIVERGENTCLONE + id = DIVERGENTCLONE + required_pref = DIVERGENTCLONE + special_role = DIVERGENTCLONE + logo_state = "divergentclone-logo" + wikiroute = DIVERGENTCLONE + default_admin_voice = "The Ancient Reptilian Brain" + admin_voice_style = "bold" + var/has_spawned_in = FALSE + var/datum/mind/force_spawn_as = null //for admins to force the clone to spawn as a clone of a specific character + var/datum/mind/original_mind = null + var/extra_role_memory = "" + var/uplink_pw_revealed = FALSE + var/datum/component/uplink/uplink = null + var/uplink_created_for_us = FALSE + + //If the clone is evil, they get traitor objectives: + // - If the clone is evil but the original is not a traitor, they get NEW objectives + // - If the clone is evil and the original is also a traitor, they get the SAME objectives as the original + // amnesia controls how much the clone remembers of the original's traitor status: + // - 0: The clone remembers everything, including the original's traitor status, traitor objectives, and uplink password + // - 1: The clone remembers most things, but not e.g. the uplink password. What they remember depends on whether the clone is evil or not; evil clones do not remember their original's traitor status, normal ones do. + // - 2: The clone does not remember whether the original is a traitor, nor their uplink password. + var/evil = 0 //0: neutral clone, 1: traitor + var/amnesia = 0 //0: excellent memory, 1: normal memory, 2: hazy memory + +/datum/role/divergentclone/New(var/datum/mind/M, var/datum/faction/fac=null, var/new_id, var/override = FALSE) + . = ..() + evil = prob(50) + amnesia = pick(0, 1, 2) + return 1 + +/datum/role/divergentclone/proc/on_spawn_in(var/datum/mind/original_mind = null) + if(has_spawned_in) + return 0 + + set_original_mind(original_mind) + if(!src.original_mind) + return 0 + + forge_memory() + + if(evil) + antag.current << sound('sound/voice/syndicate_intro.ogg') + find_or_create_uplink() + if(evil && uplink && amnesia != 1) + uplink_pw_revealed = TRUE + + has_spawned_in = TRUE + //Remove the "spawn in" objective + for(var/datum/objective/O in objectives.GetObjectives()) + if(istype(O, /datum/objective/divergentclone/spawn_in)) + objectives.objectives.Remove(O) + + Greet(GREET_DEFAULT) + ForgeObjectives() + AnnounceObjectives() + return 1 + +/datum/role/divergentclone/proc/find_or_create_uplink() + if(evil && !original_mind.GetRole(TRAITOR)) + //Find the original's PDA and add an uplink to it, if possible + var/origname = original_mind.name + for(var/obj/item/device/pda/P in PDAs) + if(P.owner == origname) + if(P.get_component(/datum/component/uplink)) + //If they somehow already have one, use that instead. + uplink = P.get_component(/datum/component/uplink) + else + uplink = P.add_component(/datum/component/uplink) + uplink_created_for_us = TRUE + antag.total_TC += uplink.telecrystals + break + else if(original_mind.GetRole(TRAITOR)) + var/datum/role/traitor/orig_role = original_mind.GetRole(TRAITOR) + if(orig_role) + uplink = orig_role.uplink + + if(!uplink) + return 0 + + var/obj/item/device/pda/P = uplink.parent + if(uplink_pw_revealed) + antag.store_memory("Uplink Passcode: [uplink.unlock_code] ([P.name]).", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) + else + antag.store_memory("Uplink Passcode: \[REDACTED\] ([P.name]).", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) + return 1 + +/datum/role/divergentclone/proc/set_original_mind(var/datum/mind/mind) + if(!mind) + return 0 + //Fix for recursive divergent clones + if(mind.GetRole(DIVERGENTCLONE)) + var/datum/role/divergentclone/clone_role = mind.GetRole(DIVERGENTCLONE) + original_mind = clone_role.original_mind + original_mind = mind + return 1 + + +/datum/role/divergentclone/Greet(var/greeting, var/custom) + if(!has_spawned_in) + return + + . = ..() + if(!greeting) + return + + to_chat(antag.current, "In a freak accident, the cloning machine has malfunctioned and created a divergent copy of you!") + if(evil) + to_chat(antag.current, "You must convince the world that you are the original at any cost, be that by talk, violence or subterfuge. Do not let anyone get in your way, including your original copy.") + else + to_chat(antag.current, "You must prove to the world that you are the original, or at the very least that you deserve to exist.") + to_chat(antag.current, "Remember that while your unique position may lead to conflict with your original copy, you are not an enemy of the station or the crew in general!") + to_chat(antag.current, "Your memory may contain useful information about the original you. You should review it using the Notes verb under the IC tab.") + + var/original_is_traitor = original_mind ? original_mind.GetRole(TRAITOR) : null + if(evil && amnesia == 0 && original_is_traitor) //Traitor and knows the original is too + to_chat(antag.current, "You are a Syndicate traitor through and through, just like your original copy. You have the same objectives they do, but cooperation is optional.") + else if(evil && amnesia == 0 && !original_is_traitor) //Traitor and knows the original is not + to_chat(antag.current, "The cloning process has awakened latent Syndicate brainwashing within you. Unlike your original copy, you are a Syndicate traitor.") + else if(evil) //Traitor, no idea if the original is + to_chat(antag.current, "Memories of Syndicate training flood into your waxing consciousness. You are a Syndicate traitor.") + else if(!evil && amnesia != 2 && original_is_traitor) //Not a traitor, but knows the original is + to_chat(antag.current, "The cloning process has undone the Syndicate brainwashing that used to affect you. You are not a Syndicate traitor, but your original copy is.") + + if(evil) + share_syndicate_codephrase(antag.current) + if(uplink) + var/obj/item/device/pda/P = uplink.parent + if(uplink_pw_revealed) + to_chat(antag.current, "You remember that your [P.name] is actually a Syndicate Uplink. If you manage to recover it, you may enter the code \"[uplink.unlock_code]\" as its ringtone to unlock its hidden features.") + else + to_chat(antag.current, "You remember that your [P.name] is actually a Syndicate Uplink. However, you can't seem to remember the passcode off the top of your head. It will come back to you if you manage to recover the device.") + else if(!uplink && evil) + to_chat(antag.current, "Unfortunately you don't remember having ever been provided with a Syndicate Uplink.") + + + +/datum/role/divergentclone/ForgeObjectives() + if(!has_spawned_in) + AppendObjective(/datum/objective/divergentclone/spawn_in) + return + + //The basic divergent clone objective + if(evil) + AppendObjective(/datum/objective/freeform/divergentclone_evil) + else + AppendObjective(/datum/objective/freeform/divergentclone_neutral) + AppendObjective(/datum/objective/acquire_personal_id) + + //Evil clones also get traitor objectives. New ones if the original is not a traitor, dupes of the original's objectives if they are. + if(evil) + if(original_mind.GetRole(TRAITOR)) + var/datum/role/traitor/orig_role = original_mind.antag_roles[TRAITOR] + var/datum/objective_holder/holder = orig_role.objectives + for(var/datum/objective/O in holder.GetObjectives()) + //AppendObjective(O) + //There really seems to be no other way to do this than check all of these one by one + if(istype(O, /datum/objective/target/assassinate)) + var/datum/objective/target/assassinate/orig_obj = O + var/datum/objective/target/assassinate/new_obj = new(auto_target = FALSE) + new_obj.target_amount = orig_obj.target_amount + if(orig_obj.delayed_target) + new_obj.target = orig_obj.delayed_target + else + new_obj.target = orig_obj.target + new_obj.explanation_text = new_obj.format_explanation() + AppendObjective(new_obj) + else if(istype(O, /datum/objective/target/steal)) + var/datum/objective/target/steal/orig_obj = O + var/datum/objective/target/steal/new_obj = new(auto_target = FALSE) + new_obj.target_amount = orig_obj.target_amount + new_obj.target_category = orig_obj.target_category + new_obj.steal_target = orig_obj.steal_target + new_obj.explanation_text = new_obj.format_explanation() + AppendObjective(new_obj) + else //Just create a new instance instead of deep copying + AppendObjective(new O.type) + + else //copypasted from syndicate.dm and yes I feel bad about it + if(prob(50)) + //50% chance of a freeform to obscure the preferences of the original + AppendObjective(/datum/objective/freeform/syndicate) + else + AppendObjective(/datum/objective/target/assassinate)//no delay + AppendObjective(/datum/objective/target/steal) + switch(rand(1,100)) + if(1 to 30) // Die glorious death + if(!(locate(/datum/objective/die) in objectives.GetObjectives()) && !(locate(/datum/objective/target/steal) in objectives.GetObjectives())) + AppendObjective(/datum/objective/die) + else + if(prob(85)) + if (!(locate(/datum/objective/escape) in objectives.GetObjectives())) + AppendObjective(/datum/objective/escape) + else + if(prob(50)) + if (!(locate(/datum/objective/hijack) in objectives.GetObjectives())) + AppendObjective(/datum/objective/hijack) + else + if (!(locate(/datum/objective/minimize_casualties) in objectives.GetObjectives())) + AppendObjective(/datum/objective/minimize_casualties) + if(31 to 90) + if (!(locate(/datum/objective/escape) in objectives.objectives)) + AppendObjective(/datum/objective/escape) + else + if(prob(50)) + if (!(locate(/datum/objective/hijack) in objectives.objectives)) + AppendObjective(/datum/objective/hijack) + else // Honk + if (!(locate(/datum/objective/minimize_casualties) in objectives.GetObjectives())) + AppendObjective(/datum/objective/minimize_casualties) + + +/datum/role/divergentclone/proc/forge_memory() + var/list/new_memory = list(MIND_MEMORY_GENERAL = "", MIND_MEMORY_ANTAGONIST = "", MIND_MEMORY_CUSTOM = "") + //All clones remember their original's general and custom memory + new_memory[MIND_MEMORY_GENERAL] = original_mind.memory[MIND_MEMORY_GENERAL] + new_memory[MIND_MEMORY_CUSTOM] = original_mind.memory[MIND_MEMORY_CUSTOM] + //antagonist memory is rebuilt from scratch + antag.memory = new_memory + + //Details on original into the role memory + var/rolemem = "
You can confidently remember the following details about the original you:
" + rolemem += "- They are [original_mind.name], the [original_mind.assigned_role].
" + //Antag status + if(amnesia == 0 || (amnesia == 1 && !evil)) + if(original_mind.antag_roles.len > 0) + for(var/R in original_mind.antag_roles) + rolemem += "- They are \a [R] with the following objectives:
" + var/datum/role/role = original_mind.antag_roles[R] + for(var/datum/objective/O in role.objectives.GetObjectives()) + rolemem += "  - [O.explanation_text]
" + else + rolemem += "- They are not an enemy of the station.
" + extra_role_memory = rolemem + +/datum/role/divergentclone/process() + ..() + if(!has_spawned_in || antag.current.gcDestroyed || antag.current.stat == DEAD) + return // dead or destroyed + + //check if we've found the uplink and should reveal the passcode + if(!uplink_pw_revealed && uplink) + var/obj/item/device/pda/P = uplink.parent + if(P in get_contents_in_object(antag.current)) + antag.current << sound('sound/voice/syndicate_intro.ogg') + to_chat(antag.current, "Upon recovering \the [P.name], you remember the passcode: \"[uplink.unlock_code]\". Enter it as the device's ringtone to unlock its hidden features.") + antag.memory[MIND_MEMORY_ANTAGONIST] = unredact_uplink_pw(antag.memory[MIND_MEMORY_ANTAGONIST], uplink) + uplink_pw_revealed = TRUE + +/datum/role/divergentclone/AdminPanelEntry(var/show_logo = FALSE,var/datum/admins/A) + var/icon/logo = icon(logo_icon, logo_state) + if(!antag) + return {"Mind destroyed. That shouldn't ever happen."} + if (!ismob(usr)) + return + var/mob/user = usr + if (!(user.ckey in voice_per_admin)) + voice_per_admin[user.ckey] = default_admin_voice + var/mob/M + if(has_spawned_in) + M = antag.current + else + for(var/mob/dead/observer/G in player_list) + if(G.mind == antag) + M = G + break + + if (M && has_spawned_in) + return {"[show_logo ? " " : "" ] + [name] [M.real_name]/[antag.key][M.client ? "" : " - ([loggedOutHow()])"][M.stat == DEAD ? " - (DEAD)" : ""] + - (admin PM) + - (role panel) + - (Message as:\[[voice_per_admin[user.ckey]]\])"} + else if (M && !has_spawned_in) + return {"[show_logo ? " " : "" ] + [name] [M.real_name]/[antag.key][M.client ? "" : " - ([loggedOutHow()])"][" - (NOT YET REINCARNATED)"] + - (admin PM) + - (role panel) + - (Message as:\[[voice_per_admin[user.ckey]]\])"} + else + return {"[show_logo ? " " : "" ] + [name] [antag.name]/[antag.key] - (DESTROYED) + - (priv msg) + - (role panel) + - (Message as:\[[voice_per_admin[user.ckey]]\])"} + +/datum/role/divergentclone/extraPanelButtons() + var/dat = "
" + if(!has_spawned_in) + dat += "Evil (will be traitor): [evil ? "Yes" : "No"] (Toggle)
" + + dat += "Amnesia level: " + if(amnesia == 0) + dat += "0 (Excellent memory)" + else if(amnesia == 1) + dat += "1 (Normal memory)" + else if(amnesia == 2) + dat += "2 (Hazy memory)" + if(amnesia != 0) + dat += " (Set to Excellent)" + if(amnesia != 1) + dat += " (Set to Normal)" + if(amnesia != 2) + dat += " (Set to Hazy)" + dat += "
" + dat += "Will spawn in as: [force_spawn_as ? force_spawn_as.name : "player's choice"] " + if (force_spawn_as) + dat += "(Clear)
" + else + dat += "(Pick character)
" + + if (force_spawn_as) + dat += " - (Force spawn NOW at nearest cloning pod)
" + else + if(uplink) + var/obj/item/device/pda/P = uplink.parent + var/uplink_name = P ? P.name : "unknown PDA" + dat += "Uplink found: [uplink_name] [uplink_pw_revealed ? "(knows the passcode)" : "(does not know passcode)"]
" + if(!uplink_pw_revealed) + dat += " - (Reveal passcode)
" + dat += " - Telecrystals: [uplink.telecrystals] (Set telecrystals)
" + dat += " - (Remove uplink)
" + dat += " - (Jump to uplink's position)
" + else + dat = " - (Give uplink)
" + return dat + +/datum/role/divergentclone/RoleTopic(href, href_list, var/datum/mind/M, var/admin_auth) + ..() + if(href_list["toggleEvil"]) + evil = !evil + to_chat(usr, "The clone will now reincarnate as [evil ? "a traitor" : "a neutral clone"].") + if(href_list["setAmnesia"]) + var/new_amnesia = text2num(href_list["setAmnesia"]) + if(new_amnesia < 0 || new_amnesia > 2) + return + amnesia = new_amnesia + to_chat(usr, "The clone's amnesia level has been set to [new_amnesia].") + if(href_list["setForceSpawn"]) + var/list/used_keys[0] + var/list/minds[0] + for(var/datum/mind/mind in ticker.minds) + if(mind == antag || (!mind.current && !mind.body_archive)) + continue + var/key = avoid_assoc_duplicate_keys(mind.name, used_keys) + minds[key] = mind + var/selection = input("Which character should the clone spawn in as?", "Choose a character", null, null) as null|anything in minds + if(selection) + var/datum/mind/mind = minds[selection] + if(!mind.current && !mind.body_archive) + to_chat(usr, "You picked some nonsense that has no body and no body archive. Pick something else.") + return + if(mind == antag) + to_chat(usr, "You can't forcespawn them as themselves! Pick something else.") + return + force_spawn_as = mind + to_chat(usr, "The clone will now spawn in as [mind].") + if(href_list["clearForceSpawn"]) + force_spawn_as = null + to_chat(usr, "The clone will now spawn in as the player's choice.") + if(href_list["forceSpawn"]) + if(!force_spawn_as) + to_chat(usr, "Cannot force spawn without a character selected!") + return + if(alert(usr, "Are you sure you want to force spawn the clone as [force_spawn_as]?", "Force spawn?", "Yes", "No") != "Yes") + return + var/obj/machinery/cloning/clonepod/pod + var/dist = 100 + for(var/obj/machinery/cloning/clonepod/P in range(usr, 7)) + var/new_dist = get_dist(P, usr) + if(new_dist < dist) + dist = new_dist + pod = P + if(!pod) + to_chat(usr, "No nearby cloning pods found!") + return + + var/mob/living/clone = null + if(force_spawn_as.current && istype(force_spawn_as.current, /mob/living/carbon/human)) + var/mob/living/O = force_spawn_as.current + original_mind = force_spawn_as + clone = pod.clone_divergent_twin(O, antag) + else + //Try to get a body from the mind's body archive + var/datum/dna2/record/D = force_spawn_as.body_archive.data["dna_records"] + original_mind = force_spawn_as + clone = pod.clone_divergent_record(D, antag) + if(!clone) + to_chat(usr, "Failed to spawn in the clone! This shouldn't happen, but maybe try again?") + return + if(!on_spawn_in(force_spawn_as)) + stack_trace("Divergent clone failed to spawn in.") + return + + if(href_list["jumpToUplink"]) + if(uplink) + usr.forceMove(get_turf(uplink.parent)) + if(href_list["revealUplinkPW"] && !uplink_pw_revealed) + antag.current << sound('sound/voice/syndicate_intro.ogg') + to_chat(antag.current, "You suddenly remember the passcode for your uplink: \"[uplink.unlock_code]\".") + antag.memory[MIND_MEMORY_ANTAGONIST] = unredact_uplink_pw(antag.memory[MIND_MEMORY_ANTAGONIST], uplink) + uplink_pw_revealed = TRUE + if(href_list["giveuplink"]) + find_or_create_uplink() + var/obj/item/device/pda/P = uplink.parent + if(P) + if(uplink_pw_revealed) + to_chat(antag.current, "You remember that your [P.name] is actually a Syndicate Uplink. If you manage to recover it, you may enter the code \"[uplink.unlock_code]\" as its ringtone to unlock its hidden features.") + else + to_chat(antag.current, "You remember that your [P.name] is actually a Syndicate Uplink. However, you can't seem to remember the passcode off the top of your head. It will come back to you if you manage to recover the device.") + to_chat(usr, "[P.name] is now the clone's uplink.") + if(href_list["telecrystalsSet"]) + if(!uplink) + to_chat(usr, "Oops, couldn't find the uplink! This shouldn't happen!") + var/amount = input("What would you like to set their crystal count to?", "Their current count is [uplink.telecrystals]") as null|num + if(isnum(amount) && amount >= 0) + to_chat(usr, "You have set [antag]'s uplink telecrystals to [amount].") + uplink.telecrystals = amount + if(href_list["removeuplink"]) + if(!uplink_created_for_us) + var/result = alert(usr, "This uplink was not created for this divergent clone, i.e. it likely belongs to a traitor who will miss it if you remove it! Are you sure?", "Remove uplink?", "Yes", "No") + if(result != "Yes") + return + QDEL_NULL(uplink) + uplink_pw_revealed = FALSE + uplink_created_for_us = FALSE + to_chat(antag.current, "You have been stripped of your uplink.") + +/datum/role/divergentclone/proc/redact_uplink_pw(var/memory) + var/regex/passcode_regex = new(@"Uplink Passcode: ([\d]{3} (?:Alpha|Bravo|Delta|Omega))") + var/result = passcode_regex.Find(memory) + if(result) + var/passcode = passcode_regex.group[1] + memory = replacetext(memory, passcode, "\[REDACTED\]") + + var/regex/frequency_regex = new(@"Uplink frequency: ([\d]{3}\.[\d])") + result = frequency_regex.Find(memory) + if(result) + var/frequency = frequency_regex.group[1] + memory = replacetext(memory, frequency, "\[REDACTED\]") + return memory + +/datum/role/divergentclone/proc/unredact_uplink_pw(var/memory, var/datum/component/uplink/uplink) + if(!uplink) + return memory + + var/regex/passcode_regex = new(@"Uplink Passcode: (\[REDACTED\])") + var/result = passcode_regex.Find(memory) + if(result) + var/passcode = passcode_regex.group[1] + memory = replacetext(memory, passcode, "[uplink.unlock_code]") + + var/regex/frequency_regex = new(@"Uplink frequency: (\[REDACTED\])") + result = frequency_regex.Find(memory) + if(result) + var/frequency = frequency_regex.group[1] + memory = replacetext(memory, frequency, "[uplink.unlock_frequency]") + return memory + +/datum/role/divergentclone/GetMemory(var/datum/mind/M, var/admin_edit = FALSE) + var/text = ..() + text += extra_role_memory + return text + +/spell/targeted/ghost/divergentclone + name = "Spawn as Divergent Clone" + desc = "Use while near a cloning pod to spawn in as a divergent clone." + override_icon = 'icons/logos.dmi' + hud_state = "divergentclone-logo" + +/spell/targeted/ghost/divergentclone/cast() + var/mob/dead/observer/ghost = holder + ASSERT(istype(ghost)) + + var/datum/role/divergentclone/role = ghost.mind.GetRole(DIVERGENTCLONE) + if(!role) //if they somehow don't have the role already, give it to them + role = new /datum/role/divergentclone(ghost.mind, override=TRUE) + if(!role) + stack_trace("Failed to give divergent clone role to ghost.") + to_chat(ghost, "Clone divergence failed. Please try again.") + return + + //Find nearest cloning pod and move to it + var/obj/machinery/cloning/clonepod/pod + var/dist = 100 + for(var/obj/machinery/cloning/clonepod/P in range(ghost, 7)) + var/new_dist = get_dist(P, ghost) + if(new_dist < dist) + dist = new_dist + pod = P + if(!pod) + switch(alert(ghost, "No nearby cloning pods found. Would you like to jump to the nearest eligible pod?", "Jump to nearest pod?", "Yes", "No")) + if("Yes") + pod = find_eligible_pod(ghost) + else + return + else if(!role.force_spawn_as && pod.occupants.len == 0 && pod.cloned_records.len == 0) + switch(alert(ghost, "This pod has never cloned anyone. Would you like to jump to the nearest eligible pod?", "Jump to nearest pod?", "Yes", "No")) + if("Yes") + pod = find_eligible_pod(ghost) + else + return + if(!pod) + to_chat(ghost, "No eligible cloning pods found. Please try again later.") + return + ghost.forceMove(get_turf(pod)) + + var/mob/living/clone = null + var/datum/mind/original_mind = null + if(role.force_spawn_as) + if(role.force_spawn_as.current && istype(role.force_spawn_as.current, /mob/living/carbon/human)) + to_chat(ghost, "A mysterious force causes you to reincarnate as a clone of [role.force_spawn_as.name]!") + var/mob/living/O = role.force_spawn_as.current + original_mind = role.force_spawn_as + clone = pod.clone_divergent_twin(O, ghost.mind) + else + //Try to get a body from the mind's body archive + var/datum/dna2/record/D = role.force_spawn_as.body_archive.data["dna_records"] + to_chat(ghost, "A mysterious force causes you to reincarnate as a clone of [D.dna.real_name]!") + original_mind = role.force_spawn_as + clone = pod.clone_divergent_record(D, ghost.mind) + //If something fails, remove force_spawn_as so the player can try again + if(!clone) + role.force_spawn_as = null + else + if(pod.occupants.len > 0) + var/mob/living/O = null + if(pod.occupants.len == 1) + O = pod.occupants[1] + var/occupant_name = O.real_name + switch(alert(ghost, "This pod is currently cloning [occupant_name]. Do you want to insert yourself as their twin?", "Insert as twin?", "Yes", "No")) + if("Yes") + if(!(O in pod.occupants)) + to_chat(ghost, "The occupant seems to have exited the pod. Please try again.") + return + else + to_chat(ghost, "Twin selection cancelled.") + return + else + var/list/used_keys[0] + var/list/occupants[0] + for(var/mob/living/occupant in pod.occupants) + var/key = avoid_assoc_duplicate_keys(occupant.name, used_keys) + occupants[key] = occupant + var/selection = input("This pod is currently cloning multiple people. Please select the person you would like to twin.", "Select twin target", null, null) as null|anything in occupants + if(!selection) + to_chat(ghost, "Twin selection cancelled.") + return + O = occupants[selection] + if(!(O in pod.occupants)) + to_chat(ghost, "The occupant seems to have exited the pod. Please try again.") + return + original_mind = O.mind + clone = pod.clone_divergent_twin(O, ghost.mind) + else if(pod.cloned_records.len > 0) + var/list/used_keys[0] + var/list/records[0] + for(var/datum/dna2/record/R in pod.cloned_records) + var/key = avoid_assoc_duplicate_keys(R.name, used_keys) + records[key] = R + var/selection = input("This pod has no occupants. Please select a record to clone.", "Divergent clone", null, null) as null|anything in records + if(!selection) + to_chat(ghost, "Twin selection cancelled.") + return + var/datum/dna2/record/record = records[selection] + original_mind = locate(record.mind) + clone = pod.clone_divergent_record(record, ghost.mind) + + if(!clone) + original_mind = null + to_chat(ghost, "Clone divergence failed. Please try again.") + return + + ghost.remove_spell(/spell/targeted/ghost/divergentclone) + + if(!role.on_spawn_in(original_mind)) + stack_trace("Divergent clone failed to spawn in.") + + +/spell/targeted/ghost/divergentclone/proc/find_eligible_pod(var/mob/dead/observer/ghost) + var/list/clonepods = list() + for(var/obj/machinery/cloning/clonepod/P in machines) + if(P.z == map.zCentcomm) + continue + if(P.occupants.len > 0 || P.cloned_records.len > 0) + clonepods += P + if(clonepods.len == 0) + return null + var/obj/machinery/cloning/clonepod/pod = clonepods[1] + var/dist = get_dist(pod, ghost) + for(var/obj/machinery/cloning/clonepod/P in clonepods) + var/new_dist = get_dist(P, ghost) + if(new_dist < dist) + dist = new_dist + pod = P + return pod + diff --git a/code/datums/gamemode/role/syndicate.dm b/code/datums/gamemode/role/syndicate.dm index 98a90cc0a4ca..d0daef682594 100644 --- a/code/datums/gamemode/role/syndicate.dm +++ b/code/datums/gamemode/role/syndicate.dm @@ -150,7 +150,7 @@ var/obj/item/device/pda/found_pda = locate() in contents if(found_pda) new_uplink = found_pda.add_component(/datum/component/uplink) - traitor_mob.mind.store_memory("Uplink Passcode: [new_uplink.unlock_code] ([found_pda.name]).") + traitor_mob.mind.store_memory("Uplink Passcode: [new_uplink.unlock_code] ([found_pda.name]).", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) traitor_mob.mind.total_TC += new_uplink.telecrystals to_chat(traitor_mob, "The Syndicate have cunningly disguised a Syndicate Uplink as your [found_pda.name]. Simply enter the code \"[new_uplink.unlock_code]\" as its ringtone to unlock its hidden features.") . = TRUE @@ -158,7 +158,7 @@ var/obj/item/device/radio/found_radio = locate() in contents if(found_radio) new_uplink = found_radio.add_component(/datum/component/uplink) - traitor_mob.mind.store_memory("Uplink frequency: [format_frequency(new_uplink.unlock_frequency)] ([found_radio.name]).") + traitor_mob.mind.store_memory("Uplink frequency: [format_frequency(new_uplink.unlock_frequency)] ([found_radio.name]).", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) traitor_mob.mind.total_TC += new_uplink.telecrystals to_chat(traitor_mob, "The Syndicate have cunningly disguised a Syndicate Uplink as your [found_radio.name]. Simply dial the frequency [format_frequency(new_uplink.unlock_frequency)] to unlock its hidden features.") . = TRUE diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 4533d22b5a75..6e2759c36f3b 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -26,13 +26,20 @@ */ +//General character knowledge, e.g. blood type, bank details, radio channels +#define MIND_MEMORY_GENERAL "General" +//Antagonist specific knowledge, e.g. uplink codes +#define MIND_MEMORY_ANTAGONIST "Antagonist" +//User-created notes, appears at the bottom of the list +#define MIND_MEMORY_CUSTOM "Custom" + /datum/mind var/key var/name //replaces mob/var/original_name var/mob/current var/active = 0 - var/memory + var/list/memory = list(MIND_MEMORY_GENERAL = "", MIND_MEMORY_ANTAGONIST = "", MIND_MEMORY_CUSTOM = "") var/datum/body_archive/body_archive var/assigned_role @@ -140,16 +147,19 @@ update_faction_icons() INVOKE_EVENT(src, /event/after_mind_transfer, "mind" = src) -/datum/mind/proc/store_memory(new_text, var/forced) +/datum/mind/proc/store_memory(new_text, var/category = MIND_MEMORY_GENERAL, var/forced = FALSE) + if(!category || !(category in memory)) + category = MIND_MEMORY_CUSTOM + if(!forced) - if(length(memory) > MAX_PAPER_MESSAGE_LEN) + if(length(memory[category]) > MAX_PAPER_MESSAGE_LEN) to_chat(current, "Your memory, however hazy, is full.") return if(length(new_text) > MAX_MESSAGE_LEN) to_chat(current, "That's a lot to memorize at once.") return if(new_text) - memory += "[new_text]
" + memory[category] += "[new_text]
" /datum/mind/proc/hasFactionsWithHUDIcons() @@ -163,8 +173,15 @@ var/output = "Your memory[current.real_name]'s memory
" if (memory) - output += memory - output += "
" + if(length(memory[MIND_MEMORY_GENERAL]) > 0) + output += memory[MIND_MEMORY_GENERAL] + output += "
" + if(length(memory[MIND_MEMORY_ANTAGONIST]) > 0) + output += memory[MIND_MEMORY_ANTAGONIST] + output += "
" + if(length(memory[MIND_MEMORY_CUSTOM]) > 0) + output += memory[MIND_MEMORY_CUSTOM] + output += "
" if(antag_roles.len) for(var/role in antag_roles) diff --git a/code/datums/outfit/outfit.dm b/code/datums/outfit/outfit.dm index 0d9e94ae654b..a1fab30c30ec 100644 --- a/code/datums/outfit/outfit.dm +++ b/code/datums/outfit/outfit.dm @@ -301,7 +301,7 @@ for(var/channel in frequency_list) var/frequency = frequency_list[channel] text += "[channel]: [format_frequency(frequency)]
" - mind.store_memory(jointext(text, null)) + mind.store_memory(jointext(text, null), category=MIND_MEMORY_GENERAL, forced=TRUE) // -- Things to do AFTER all the equipment is given (ex: accessories) /datum/outfit/proc/post_equip(var/mob/living/carbon/human/H) diff --git a/code/game/jobs/job/whitelisted.dm b/code/game/jobs/job/whitelisted.dm index 7da14bb7a51f..9850c97997fd 100644 --- a/code/game/jobs/job/whitelisted.dm +++ b/code/game/jobs/job/whitelisted.dm @@ -35,7 +35,7 @@ SStrade.loyal_customers[M.real_name] = 0 - M.mind.store_memory("The joint trader account is: #[trader_account.account_number]
Your shared account pin is: [trader_account.remote_access_pin]
") + M.mind.store_memory("The joint trader account is: #[trader_account.account_number]
Your shared account pin is: [trader_account.remote_access_pin]
", category=MIND_MEMORY_GENERAL, forced=TRUE) log_admin("([M.ckey]/[M]) started the game as a [job_title].") diff --git a/code/game/jobs/job_controller.dm b/code/game/jobs/job_controller.dm index f20dd25ec14e..b37ba4e4c40c 100644 --- a/code/game/jobs/job_controller.dm +++ b/code/game/jobs/job_controller.dm @@ -497,7 +497,7 @@ var/global/alt_job_limit = 0 //list of alternate jobs available for new hires if(M.transaction_log.len) var/datum/transaction/T = M.transaction_log[1] remembered_info += "Your account was created: [T.time], [T.date] at [T.source_terminal]
" - H.mind.store_memory(remembered_info) + H.mind.store_memory(remembered_info, category=MIND_MEMORY_GENERAL, forced=TRUE) H.mind.initial_account = M H.mind.initial_wallet_funds = balance_wallet @@ -512,7 +512,7 @@ var/global/alt_job_limit = 0 //list of alternate jobs available for new hires remembered_info += "Your department's account pin is: [department_account.remote_access_pin]
" remembered_info += "Your department's account funds are: $[department_account.money]
" - H.mind.store_memory(remembered_info) + H.mind.store_memory(remembered_info, category=MIND_MEMORY_GENERAL, forced=TRUE) spawn() to_chat(H, "Your bank account number is: [M.account_number], your bank account pin is: [M.remote_access_pin]") diff --git a/code/game/objects/items/devices/PDA/cart/misc.dm b/code/game/objects/items/devices/PDA/cart/misc.dm index 2538464e1864..cdfc2e029316 100644 --- a/code/game/objects/items/devices/PDA/cart/misc.dm +++ b/code/game/objects/items/devices/PDA/cart/misc.dm @@ -142,7 +142,7 @@ new_uplink.locked = FALSE U.show_message("Success! Unlock the PDA by entering [new_uplink.unlock_code] into it.", 1) if(U.mind) - U.mind.store_memory("Uplink Passcode: [new_uplink.unlock_code] ([P.name]).") + U.mind.store_memory("Uplink Passcode: [new_uplink.unlock_code] ([P.name]).", category=MIND_MEMORY_ANTAGONIST, forced=TRUE) /obj/item/weapon/cartridge/syndicatedoor name = "\improper Doorman Cartridge" diff --git a/code/game/objects/items/weapons/implants/types/adrenalin_implant.dm b/code/game/objects/items/weapons/implants/types/adrenalin_implant.dm index 063e666871a2..a93552054e84 100644 --- a/code/game/objects/items/weapons/implants/types/adrenalin_implant.dm +++ b/code/game/objects/items/weapons/implants/types/adrenalin_implant.dm @@ -29,7 +29,7 @@ /obj/item/weapon/implant/adrenalin/implanted(mob/implanter) imp_in.register_event(/event/emote, src, nameof(src::trigger())) - imp_in.mind.store_memory("The freedom implant can be activated by using the pale emote, say *pale to attempt to activate.", 0, 0) + imp_in.mind.store_memory("The freedom implant can be activated by using the pale emote, say *pale to attempt to activate.", category=MIND_MEMORY_GENERAL, forced=TRUE) to_chat(imp_in, "The implanted freedom implant can be activated by using the pale emote, say *pale to attempt to activate.") /obj/item/weapon/implant/adrenalin/handle_removal(mob/remover) diff --git a/code/game/objects/items/weapons/implants/types/compressed_matter_implant.dm b/code/game/objects/items/weapons/implants/types/compressed_matter_implant.dm index af86de400f42..8dad5d9052d9 100644 --- a/code/game/objects/items/weapons/implants/types/compressed_matter_implant.dm +++ b/code/game/objects/items/weapons/implants/types/compressed_matter_implant.dm @@ -39,7 +39,7 @@ /obj/item/weapon/implant/compressed/implanted(mob/implanter) imp_in.register_event(/event/emote, src, nameof(src::trigger())) activation_emote = input(implanter, "Choose activation emote:") in list("blink", "blink_r", "eyebrow", "chuckle", "twitch_s", "frown", "nod", "blush", "giggle", "grin", "groan", "shrug", "smile", "pale", "sniff", "whimper", "wink") - implanter.mind.store_memory("Compressed matter implant in [implanter == imp_in ? "yourself" : imp_in.name] can be activated by using the [activation_emote] emote, say *[activation_emote] to attempt to activate.", TRUE) + implanter.mind.store_memory("Compressed matter implant in [implanter == imp_in ? "yourself" : imp_in.name] can be activated by using the [activation_emote] emote, say *[activation_emote] to attempt to activate.", , category=MIND_MEMORY_GENERAL, forced=TRUE) to_chat(implanter, "The implanted compressed matter implant can be activated by using the [activation_emote] emote, say *[activation_emote] to attempt to activate.") /obj/item/weapon/implant/compressed/islegal() diff --git a/code/game/objects/items/weapons/implants/types/explosive_implant.dm b/code/game/objects/items/weapons/implants/types/explosive_implant.dm index 2a0e064a9b08..2c9320cfa120 100644 --- a/code/game/objects/items/weapons/implants/types/explosive_implant.dm +++ b/code/game/objects/items/weapons/implants/types/explosive_implant.dm @@ -49,7 +49,7 @@ phrase = input("Choose activation phrase:") as text var/static/list/replacechars = list("'" = "", "\"" = "", ">" = "", "<" = "", "(" = "", ")" = "") phrase = sanitize_simple(phrase, replacechars) - usr.mind.store_memory("Explosive implant in [imp_in] can be activated by saying something containing the phrase ''[src.phrase]'', say [src.phrase] to attempt to activate.", 0, 0) + usr.mind.store_memory("Explosive implant in [imp_in] can be activated by saying something containing the phrase ''[src.phrase]'', say [src.phrase] to attempt to activate.", category=MIND_MEMORY_GENERAL, forced=TRUE) to_chat(usr, "The implanted explosive implant in [imp_in] can be activated by saying something containing the phrase ''[src.phrase]'', say [src.phrase] to attempt to activate.") addHear() source.register_event(/event/emote, src, nameof(src::trigger())) diff --git a/code/game/objects/items/weapons/implants/types/freedom_implant.dm b/code/game/objects/items/weapons/implants/types/freedom_implant.dm index 201d99b3546f..292438d83a11 100644 --- a/code/game/objects/items/weapons/implants/types/freedom_implant.dm +++ b/code/game/objects/items/weapons/implants/types/freedom_implant.dm @@ -27,7 +27,7 @@ /obj/item/weapon/implant/freedom/implanted(mob/implanter) imp_in.register_event(/event/emote, src, nameof(src::trigger())) - imp_in.mind.store_memory("Freedom implant can be activated by using the [activation_emote] emote, say *[activation_emote] to attempt to activate.", 0, 0) + imp_in.mind.store_memory("Freedom implant can be activated by using the [activation_emote] emote, say *[activation_emote] to attempt to activate.", category=MIND_MEMORY_GENERAL, forced=TRUE) to_chat(imp_in, "The implanted freedom implant can be activated by using the [activation_emote] emote, say *[activation_emote] to attempt to activate.") /obj/item/weapon/implant/freedom/handle_removal(mob/remover) diff --git a/code/game/objects/items/weapons/implants/types/uplink_implant.dm b/code/game/objects/items/weapons/implants/types/uplink_implant.dm index a6322fc50b11..cacf8efd5eb9 100644 --- a/code/game/objects/items/weapons/implants/types/uplink_implant.dm +++ b/code/game/objects/items/weapons/implants/types/uplink_implant.dm @@ -11,7 +11,7 @@ /obj/item/weapon/implant/uplink/implanted(mob/implanter) imp_in.register_event(/event/emote, src, nameof(src::trigger())) activation_emote = input("Choose activation emote:") in list("blink", "blink_r", "eyebrow", "chuckle", "twitch_s", "frown", "nod", "blush", "giggle", "grin", "groan", "shrug", "smile", "pale", "sniff", "whimper", "wink") - imp_in.mind.store_memory("Uplink implant can be activated by using the [src.activation_emote] emote, say *[src.activation_emote] to attempt to activate.", 0, 0) + imp_in.mind.store_memory("Uplink implant can be activated by using the [src.activation_emote] emote, say *[src.activation_emote] to attempt to activate.", category=MIND_MEMORY_GENERAL, forced=TRUE) to_chat(imp_in, "The implanted uplink implant can be activated by using the [src.activation_emote] emote, say *[src.activation_emote] to attempt to activate.") return 1 diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index a601bf4f60ca..ca47d383f72f 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -29,6 +29,7 @@ var/global/list/special_roles = list( ROLE_MINOR = 1, ROLE_PRISONER = 1, ROLE_GRUE = 1, + DIVERGENTCLONE = 1, ) /var/list/antag_roles = list( @@ -52,6 +53,7 @@ var/global/list/special_roles = list( ROLE_MINOR = 1, ROLE_PRISONER = 1, ROLE_GRUE = 1, + DIVERGENTCLONE = 1, ) var/list/nonantag_roles = list( @@ -85,6 +87,7 @@ var/list/role_wiki=list( ROLE_MINOR = "Minor_Roles", ROLE_PRISONER = "Minor_Roles", ROLE_GRUE = "Grue", + DIVERGENTCLONE = "Divergent_Clone", ) var/list/special_popup_text2num = list( diff --git a/code/modules/medical/cloning.dm b/code/modules/medical/cloning.dm index 1e26c2a38bc2..19d5b13e0431 100644 --- a/code/modules/medical/cloning.dm +++ b/code/modules/medical/cloning.dm @@ -15,7 +15,9 @@ icon_state = "pod_0" req_access = list(access_genetics) //For premature unlocking. var/mob/living/occupant - var/heal_level = 90 //The clone is released once its health reaches this level. + //list of mob/living/ that are currently in the pod. Usually only one, but in exceptional circumstances there may be multiple. All are ejected at the same time (when everyone's ready) + var/list/occupants[0] + var/heal_level = 0 //The clone is released once its health reaches this level. var/locked = FALSE var/frequency = 0 var/obj/machinery/computer/cloning/connected = null //So we remember the connected clone machine. @@ -28,6 +30,7 @@ id_tag = "clone_pod" var/upgraded = 0 //if fully upgraded with T4 components, it will drastically improve and allow for some stuff var/obj/machinery/computer/cloning/cloning_computer = null + var/list/cloned_records = list() //List of all records this pod has cloned. machine_flags = EMAGGABLE | SCREWTOGGLE | CROWDESTROY | MULTITOOL_MENU | MULTIOUTPUT @@ -171,35 +174,48 @@ /obj/machinery/cloning/clonepod/attack_paw(mob/user as mob) return attack_hand(user) /obj/machinery/cloning/clonepod/attack_hand(mob/user as mob) - if ((isnull(occupant)) || (stat & (FORCEDISABLE|NOPOWER))) + if(occupants.len == 0 || (stat & (FORCEDISABLE|NOPOWER))) return - if ((!isnull(occupant)) && (occupant.stat != 2)) - var/completion = (100 * ((occupant.health + 100) / (heal_level + 100))) - to_chat(user, "Current clone cycle is [round(completion)]% complete.") + if(occupants.len > 0) + var/lowest_completion = 100 + for(var/mob/living/O in occupants) + var/completion = 100 * ((O.health + 100) / (heal_level + 100)) + if(completion < lowest_completion) + lowest_completion = completion + to_chat(user, "Current clone cycle is [round(lowest_completion)]% complete.") return //Clonepod //Start growing a human clone in the pod! -/obj/machinery/cloning/clonepod/proc/growclone(var/datum/dna2/record/R) - if(mess || working) +//force_clone forcibly clones the mind in the record even if its body isn't dead or it's not currently in its body. +//specifying a mob that's already inside the cloner in copy_progress_from will set the clone's health to match that mob's health +/obj/machinery/cloning/clonepod/proc/growclone(var/datum/dna2/record/R, var/mob/living/copy_progress_from = null, var/do_mind_transfer = TRUE, var/allow_multiple = FALSE, var/force_clone = FALSE) + if(mess) + return FALSE + if(!allow_multiple && working) return FALSE var/datum/mind/clonemind = locate(R.mind) if(!clonemind) //no mind return FALSE if(!istype(clonemind,/datum/mind)) //not a mind return FALSE - if(clonemind.current) + if(!force_clone && clonemind.current) if(clonemind.current.stat != DEAD) //mind is associated with a non-dead body return FALSE - if(clonemind.active) //somebody is using that mind - if(ckey(clonemind.key)!=R.ckey ) + //mind.active is supposed to indicate whether the mind is, you know, active. But it's zero if the player attached to the mind is active but a ghost. + //Which I suppose makes some kind of sense but is a bit confusing. + if(clonemind.active) + if(ckey(clonemind.key) != R.ckey) return FALSE else for(var/mob/G in player_list) if(G.ckey == R.ckey) if(isobserver(G)) - if(G:can_reenter_corpse) + if(force_clone) + break + var/mob/dead/observer/ghost = G + if(ghost.can_reenter_corpse()) break if((!G.mind.current) && G.mind.body_archive) //If the mind's body was destroyed and that mind has a body archive var/datum/dna2/record/D = G.mind.body_archive.data["dna_records"] //Retrieve the DNA records from the mind's body archive @@ -207,27 +223,150 @@ break //Proceed with cloning. This set of checks is to allow cloning players with completely destroyed bodies, that nevertheless had cloning data stored else return FALSE - else if(G) + else if(!G.mind) return FALSE if(G.mind.current) - if(G.mind.current.stat != DEAD) + if(!force_clone && G.mind.current.stat != DEAD) return FALSE if(G.mind != clonemind) return FALSE + + var/mob/living/carbon/human/H = new /mob/living/carbon/human(src, R.dna.species, delay_ready_dna = TRUE) + H.times_cloned = R.times_cloned + 1 + H.talkcount = R.talkcount + + if(isplasmaman(H)) + H.fire_sprite = "Plasmaman" + + H.dna = R.dna.Clone() + H.dna.flavor_text = R.dna.flavor_text + H.dna.species = R.dna.species + if(H.dna.species != "Human") + H.set_species(H.dna.species, TRUE) + + H.UpdateAppearance() + H.set_species(H.dna.species) + H.update_mutantrace() + + if(do_mind_transfer) + has_been_shade.Remove(clonemind) + clonemind.transfer_to(H) + H.ckey = R.ckey + + for(var/datum/language/L in R.languages) + H.add_language(L.name) + if (L == R.default_language) + H.default_language = R.default_language + H.attack_log = R.attack_log + H.real_name = H.dna.real_name + H.flavor_text = H.dna.flavor_text + + if(H.mind) + H.mind.suiciding = FALSE + H.update_name() + + cloned_records += R.Clone() + + return addclone(H, copy_progress_from) + +//Grows a twin of an existing living mob and puts the given mind into it. +//Unless force_clone is TRUE, the mind's body must be dead. +/obj/machinery/cloning/clonepod/proc/growtwin(var/mob/living/original, var/datum/mind/clonemind, var/do_mind_transfer = TRUE, var/allow_multiple = FALSE, var/force_clone = FALSE) + if(mess) + return FALSE + if(!allow_multiple && working) + return FALSE + if(!clonemind) + return FALSE + if(!istype(clonemind,/datum/mind)) + return FALSE + if(!force_clone && clonemind.current) + if(clonemind.current.stat != DEAD) + return FALSE + if(!clonemind.active) + for(var/mob/P in player_list) + if(P.ckey == ckey(clonemind.key)) + if(!force_clone && !isobserver(P)) + return FALSE + break + + var/original_in_cloner = (original in occupants) + var/mob/living/carbon/human/H = new /mob/living/carbon/human(src, original.dna.species, delay_ready_dna = TRUE) + if(original_in_cloner) + H.times_cloned = original.times_cloned + else + H.times_cloned = original.times_cloned + 1 + H.talkcount = original.talkcount + + if(isplasmaman(H)) + H.fire_sprite = "Plasmaman" + + H.dna = original.dna.Clone() + H.dna.flavor_text = original.dna.flavor_text + H.dna.species = original.dna.species + if(H.dna.species != "Human") + H.set_species(H.dna.species, TRUE) + + H.UpdateAppearance() + H.set_species(H.dna.species) + H.update_mutantrace() + + if(do_mind_transfer) + has_been_shade.Remove(clonemind) + clonemind.transfer_to(H) + H.key = clonemind.key - heal_level = upgraded ? 100 : rand(10,40) //Randomizes what health the clone is when ejected - working = TRUE //One at a time!! - locked = TRUE + for(var/datum/language/L in original.languages) + H.add_language(L.name) + if (L == original.default_language) + H.default_language = original.default_language + H.attack_log = original.attack_log + H.real_name = original.real_name + H.flavor_text = original.flavor_text + if(H.mind) + H.mind.suiciding = FALSE + H.update_name() + + var/datum/dna2/record/R = new /datum/dna2/record() + R.dna = H.dna.Clone() + R.ckey = H.ckey + R.id = copytext(md5(R.dna.real_name), 2, 6) + R.name = R.dna.real_name + R.types = DNA2_BUF_UI|DNA2_BUF_UE|DNA2_BUF_SE + R.languages = H.languages.Copy() + R.attack_log = H.attack_log.Copy() + R.default_language = H.default_language + R.times_cloned = H.times_cloned + R.talkcount = H.talkcount + if (!isnull(H.mind)) + R.mind = "\ref[H.mind]" + cloned_records += R + + var/mob/living/copy_progress_from = null + if(original_in_cloner) + copy_progress_from = original + return addclone(H, original, copy_progress_from) + + +//Adds a new clone into the pod. Probably don't call this directly, use growclone or growtwin instead. +//returns the new mob +/obj/machinery/cloning/clonepod/proc/addclone(var/mob/living/carbon/human/H, var/mob/living/copy_progress_from = null) + if(heal_level == 0) + heal_level = upgraded ? 100 : rand(10,40) //Randomizes what health the clone is when ejected + + //only lock if we're not already working on a clone + if(!working) + locked = TRUE + working = TRUE + + if(!eject_wait) + spawn(30) + eject_wait = FALSE eject_wait = TRUE - spawn(30) - eject_wait = FALSE - var/mob/living/carbon/human/H = new /mob/living/carbon/human(src, R.dna.species, delay_ready_dna = TRUE) - occupant = H - H.times_cloned = R.times_cloned +1 - H.talkcount = R.talkcount + occupants += H if(!connected.emagged) icon_state = "pod_1" @@ -236,29 +375,22 @@ connected.update_icon() - if(isplasmaman(H)) - H.fire_sprite = "Plasmaman" - - //Get the clone body ready - H.dna = R.dna.Clone() - H.dna.flavor_text = R.dna.flavor_text - H.dna.species = R.dna.species - if(H.dna.species != "Human") - H.set_species(H.dna.species, TRUE) - isslimeperson(H) ? H.adjustToxLoss(75) : H.adjustCloneLoss(150) // 75 for slime people due to their tox_mod of 2 H.adjustBrainLoss(upgraded ? 0 : (heal_level + 50 + rand(10, 30))) // The rand(10, 30) will come out as extra brain damage H.Paralyse(4) H.nobreath = 15 H.stat = H.status_flags & BUDDHAMODE ? CONSCIOUS : UNCONSCIOUS //There was a bug which allowed you to talk for a few seconds after being cloned, because your stat wasn't updated until next Life() tick. This is a fix for this! + if(copy_progress_from && (copy_progress_from in occupants)) + var/mob/living/C = copy_progress_from + H.setToxLoss(C.getToxLoss()) + H.setCloneLoss(C.getCloneLoss()) + H.setOxyLoss(C.getOxyLoss()) + H.setBrainLoss(C.getBrainLoss()) + //Here let's calculate their health so the pod doesn't immediately eject them!!! H.updatehealth() - has_been_shade.Remove(clonemind) - clonemind.transfer_to(H) - - H.ckey = R.ckey to_chat(H, "Consciousness slowly creeps over you as your body regenerates.
So this is what cloning feels like?
") if (H.mind.miming) @@ -272,86 +404,120 @@ V.check_vampire_upgrade() V.update_vamp_hud() - H.UpdateAppearance() - H.set_species(R.dna.species) - if(!upgraded) - randmutb(H) // sometimes the clones come out wrong. - H.dna.mutantrace = R.dna.mutantrace - H.update_mutantrace() - for(var/datum/language/L in R.languages) - H.add_language(L.name) - if (L == R.default_language) - H.default_language = R.default_language - H.attack_log = R.attack_log - H.real_name = H.dna.real_name - H.flavor_text = H.dna.flavor_text + return H + +//Used for cloning divergent clones +/obj/machinery/cloning/clonepod/proc/clone_divergent_twin(var/mob/living/original, var/datum/mind/clonemind) + var/mob/living/clone = growtwin(original, clonemind, do_mind_transfer=TRUE, allow_multiple=TRUE, force_clone=TRUE) + if(!clone) + return null + var/datum/mind/new_mind = clone.mind + var/datum/mind/orig_mind = original.mind + new_mind.name = orig_mind.name + //new_mind.memory = orig_mind.memory + new_mind.assigned_role = orig_mind.assigned_role + new_mind.body_archive = orig_mind.body_archive + new_mind.role_alt_title = orig_mind.role_alt_title + new_mind.miming = orig_mind.miming + new_mind.faith = orig_mind.faith + new_mind.initial_account = orig_mind.initial_account + new_mind.initial_wallet_funds = orig_mind.initial_wallet_funds + + return clone + +/obj/machinery/cloning/clonepod/proc/clone_divergent_record(var/datum/dna2/record/orig_record, var/datum/mind/clonemind) + var/datum/dna2/record/R = new /datum/dna2/record() + R.dna = orig_record.dna.Clone() + R.ckey = ckey(clonemind.key) + R.mind = "\ref[clonemind]" + R.id = copytext(md5(R.dna.real_name), 2, 6) + R.name = R.dna.real_name + R.types = DNA2_BUF_UI | DNA2_BUF_UE | DNA2_BUF_SE + R.languages = orig_record.languages.Copy() + R.attack_log = orig_record.attack_log.Copy() + R.default_language = orig_record.default_language + R.times_cloned = orig_record.times_cloned + R.talkcount = orig_record.talkcount + + var/mob/living/carbon/human/clone = growclone(R, copy_progress_from=null, do_mind_transfer=TRUE, allow_multiple=TRUE, force_clone=TRUE) + var/datum/mind/new_mind = clone.mind + var/datum/mind/orig_mind = locate(orig_record.mind) + new_mind.name = orig_mind.name + //new_mind.memory = orig_mind.memory + new_mind.assigned_role = orig_mind.assigned_role + new_mind.body_archive = orig_mind.body_archive + new_mind.role_alt_title = orig_mind.role_alt_title + new_mind.miming = orig_mind.miming + new_mind.faith = orig_mind.faith + new_mind.initial_account = orig_mind.initial_account + new_mind.initial_wallet_funds = orig_mind.initial_wallet_funds + + return clone - if(H.mind) - H.mind.suiciding = FALSE - H.update_name() - return TRUE //Grow clones to maturity then kick them out. FREELOADERS /obj/machinery/cloning/clonepod/process() - if(stat & (FORCEDISABLE|NOPOWER)) //Autoeject if power is lost - if (occupant) + if (occupants.len > 0) locked = FALSE go_out() return - if((occupant) && (occupant.loc == src)) - if((occupant.stat == DEAD) || (occupant.mind && occupant.mind.suiciding) || !occupant.key) //Autoeject corpses and suiciding dudes. - locked = FALSE - go_out() - connected_message("Clone Rejected: Deceased.") - return - - else if(occupant.health < heal_level) - occupant.Paralyse(4) + if(occupants.len > 0) + use_power(7500) + else + use_power(200) - //Slowly get that clone healed and finished. - isslimeperson(occupant) ? occupant.adjustToxLoss(-1*time_coeff) : occupant.adjustCloneLoss(-1*time_coeff) //Very slow, new parts = much faster + var/message = null + var/done_occupants = 0 - //Premature clones may have brain damage. - occupant.adjustBrainLoss(-1*time_coeff) //Ditto above + for(var/mob/living/O in occupants) + if(O.loc == src) + if((O.stat == DEAD) || (O.mind && O.mind.suiciding) || !O.key) //Autoeject corpses and suiciding dudes. + done_occupants += 1 + if(!message) + message = "Clone Rejected: Deceased." + continue - var/mob/living/carbon/human/H = occupant + O.Paralyse(4) + var/mob/living/carbon/human/H = O if(isvox(H)) - if(occupant.reagents.get_reagent_amount(NITROGEN) < 30) - occupant.reagents.add_reagent(NITROGEN, 60) + if(O.reagents.get_reagent_amount(NITROGEN) < 30) + O.reagents.add_reagent(NITROGEN, 60) //So clones don't die of oxyloss in a running pod. - else if(occupant.reagents.get_reagent_amount(INAPROVALINE) < 30) //Done like this because inaprovaline is toxic to vox - occupant.reagents.add_reagent(INAPROVALINE, 60) + else if(O.reagents.get_reagent_amount(INAPROVALINE) < 30) //Done like this because inaprovaline is toxic to vox + O.reagents.add_reagent(INAPROVALINE, 60) //Also heal some oxyloss ourselves because inaprovaline is so bad at preventing it!! - occupant.adjustOxyLoss(-4) - occupant.nobreath = 15 + O.adjustOxyLoss(-4) + O.nobreath = 15 - use_power(7500) //This might need tweaking. - return + if(O.health < heal_level) + //Slowly get that clone healed and finished. + isslimeperson(O) ? O.adjustToxLoss(-1*time_coeff) : O.adjustCloneLoss(-1*time_coeff) //Very slow, new parts = much faster - else if((occupant.health >= heal_level) && (!eject_wait)) - connected_message("Cloning Process Complete.") - locked = FALSE - go_out() - return + //Premature clones may have brain damage. + O.adjustBrainLoss(-1*time_coeff) //Ditto above + continue - else if ((!occupant) || (occupant.loc != src)) - occupant = null - if (locked) - locked = FALSE - if (!mess) - icon_state = "pod_0" - use_power(200) - return + else if((O.health >= heal_level) && (!eject_wait)) + done_occupants += 1 + message = "Clone Process Complete." + continue - return + else + occupants.Remove(O) + continue + + if(!eject_wait && done_occupants > 0 && done_occupants >= occupants.len) + connected_message(message) + locked = FALSE + go_out() /obj/machinery/cloning/clonepod/emag_act(mob/user as mob) - if(isnull(occupant)) + if(occupants.len == 0) return if(user) to_chat(user, "You force an emergency ejection.") @@ -360,7 +526,7 @@ return /obj/machinery/cloning/clonepod/crowbarDestroy(mob/user, obj/item/tool/crowbar/I) - if(occupant) + if(occupants.len > 0) to_chat(user, "You cannot disassemble \the [src], it's occupado.") return FALSE for(biomass; biomass > 0;biomass -= BIOMASS_CHUNK) @@ -385,7 +551,7 @@ if (!check_access(W)) to_chat(user, "Access Denied.") return - else if ((!locked) || (isnull(occupant))) + else if ((!locked) || (occupants.len == 0)) return else locked = FALSE @@ -434,52 +600,54 @@ icon_state = "pod_0" return - if (!(occupant)) + if (occupants.len == 0) return - if (occupant.client) - occupant.client.eye = occupant.client.mob - occupant.client.perspective = MOB_PERSPECTIVE - occupant.forceMove(exit) var/obj/machinery/conveyor/C = locate() in exit - if(C && C.operating != 0) - occupant << sound('sound/ambience/powerhouse.ogg') //the ride begins - icon_state = "pod_0" - eject_wait = FALSE //If it's still set somehow. - //do early ejection damage - var/completion = 10*((occupant.health + 100) / (heal_level + 100)) //same way completion is calculated for examine text, but out of 10 instead of 100 - var/damage_rolls = 10 - round(completion) - (round(resource_efficiency) - 1) // 1 roll for each 10% missing, each improved pair of manipulators reduces one roll - var/hits = 0 - while(damage_rolls > 0) - if(prob(25))//each roll has a 25% chance to give the occupant a bad time - hits++ - damage_rolls-- - //apply the damage - var/mob/living/carbon/human/H = occupant - while(hits>0) - if (hits>=4) - qdel(pick(H.internal_organs - H.internal_organs_by_name["brain"])) - hits -= 4 - else //if this pick lands on either torso part, those can't be droplimb'd. Get out of jail free, I guess - H.organs_by_name[pick(H.organs_by_name)].droplimb(override = 1, no_explode = 1, spawn_limb = 1, display_message = FALSE) - hits-- - - occupant.updatehealth() - - domutcheck(occupant) //Waiting until they're out before possible monkeyizing. - occupant = null - if(biomass > 0) - biomass -= CLONE_BIOMASS/resource_efficiency //Improve parts to use less biomass - else - biomass = 0 + for(var/mob/living/O in occupants) + if (O.client) + O.client.eye = O.client.mob + O.client.perspective = MOB_PERSPECTIVE + + O.forceMove(exit) + if(C && C.operating != 0) + O << sound('sound/ambience/powerhouse.ogg') //the ride begins + + //do early ejection damage + var/completion = 10*((O.health + 100) / (heal_level + 100)) //same way completion is calculated for examine text, but out of 10 instead of 100 + var/damage_rolls = 10 - round(completion) - (round(resource_efficiency) - 1) // 1 roll for each 10% missing, each improved pair of manipulators reduces one roll + var/hits = 0 + while(damage_rolls > 0) + if(prob(25))//each roll has a 25% chance to give the O a bad time + hits++ + damage_rolls-- + + //apply the damage + var/mob/living/carbon/human/H = O + while(hits>0) + if (hits>=4) + qdel(pick(H.internal_organs - H.internal_organs_by_name["brain"])) + hits -= 4 + else //if this pick lands on either torso part, those can't be droplimb'd. Get out of jail free, I guess + H.organs_by_name[pick(H.organs_by_name)].droplimb(override = 1, no_explode = 1, spawn_limb = 1, display_message = FALSE) + hits-- + + O.updatehealth() + domutcheck(O) //Waiting until they're out before possible monkeyizing. + occupants.Remove(O) + biomass = max(0, biomass - CLONE_BIOMASS/resource_efficiency) //Improve parts to use less biomass + + icon_state = "pod_0" + eject_wait = FALSE + heal_level = 0 //so that it will be re-randomized next time connected.update_icon() working = FALSE //NOW we're done. return TRUE /obj/machinery/cloning/clonepod/MouseDropFrom(over_object, src_location, var/turf/over_location, src_control, over_control, params) - if(!occupant || occupant == usr || (!ishigherbeing(usr) && !isrobot(usr)) || usr.incapacitated() || usr.lying) + if(occupants.len == 0 || (usr in occupants) || (!ishigherbeing(usr) && !isrobot(usr)) || usr.incapacitated() || usr.lying) return if(!istype(over_location) || over_location.density) return @@ -496,19 +664,21 @@ to_chat(usr, "You do not have the means to do this!") return - var/_occupant = occupant // occupant is null after go_out() + var/_occupants = occupants.Copy() // occupants is empty after go_out() if(go_out(over_location)) - visible_message("[usr] removes \the [_occupant] from \the [src].") + for(var/mob/living/O in _occupants) + visible_message("[usr] removes \the [O] from \the [src].") add_fingerprint(usr) /obj/machinery/cloning/clonepod/proc/malfunction() - if(occupant) + if(occupants.len > 0) connected_message("Critical Error!") mess = TRUE icon_state = "pod_g" - occupant.ghostize() - spawn(5) - qdel(occupant) + for(var/mob/living/O in occupants) + occupant.ghostize() + spawn(5) + qdel(occupant) return /obj/machinery/cloning/clonepod/relaymove(mob/user as mob) @@ -581,7 +751,7 @@ /obj/machinery/cloning/clonepod/kick_act() ..() - if(occupant && prob(5)) + if(occupants.len > 0 && prob(5)) visible_message("[src] buzzes.","You hear a buzz.") playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 0) locked = FALSE diff --git a/code/modules/medical/computer/cloning.dm b/code/modules/medical/computer/cloning.dm index 7cc625d38453..420267645e8e 100644 --- a/code/modules/medical/computer/cloning.dm +++ b/code/modules/medical/computer/cloning.dm @@ -339,7 +339,7 @@ if(!pod1 || !canLink(pod1)) //If the pod exists BUT it's too far away from the console temp = "Error: No Clonepod detected." return - else if(pod1.occupant) + else if(pod1.occupants.len > 0) temp = "Error: Clonepod is currently occupied." return else if(pod1.biomass < CLONE_BIOMASS) @@ -487,5 +487,5 @@ if(!(stat & (NOPOWER | BROKEN | FORCEDISABLE))) if(scanner && scanner.occupant) overlays += image(icon = icon, icon_state = "cloning-scan") - if(pod1 && pod1.occupant) + if(pod1 && pod1.occupants.len > 0) overlays += image(icon = icon, icon_state = "cloning-pod") diff --git a/code/modules/mob/living/carbon/alien/humanoid/death.dm b/code/modules/mob/living/carbon/alien/humanoid/death.dm index ba3638f23483..0a71ab6b4e0c 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/death.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/death.dm @@ -13,6 +13,6 @@ tod = worldtime2text() //weasellos time of death patch if(mind) - mind.store_memory("Time of death: [tod]", 0) + mind.store_memory("Time of death: [tod]", category=MIND_MEMORY_GENERAL, forced=TRUE) return ..(gibbed) diff --git a/code/modules/mob/living/carbon/alien/larva/death.dm b/code/modules/mob/living/carbon/alien/larva/death.dm index f88e921709d1..1e0db6ad4a03 100644 --- a/code/modules/mob/living/carbon/alien/larva/death.dm +++ b/code/modules/mob/living/carbon/alien/larva/death.dm @@ -13,7 +13,7 @@ tod = worldtime2text() //weasellos time of death patch if(mind) - mind.store_memory("Time of death: [tod]", 0) + mind.store_memory("Time of death: [tod]", category=MIND_MEMORY_GENERAL, forced=TRUE) living_mob_list -= src return ..(gibbed) diff --git a/code/modules/mob/living/carbon/brain/death.dm b/code/modules/mob/living/carbon/brain/death.dm index 7928e900c4b8..71e81662bc5d 100644 --- a/code/modules/mob/living/carbon/brain/death.dm +++ b/code/modules/mob/living/carbon/brain/death.dm @@ -12,7 +12,7 @@ tod = worldtime2text() //weasellos time of death patch if(mind) - mind.store_memory("Time of death: [tod]", 0) //mind. ? + mind.store_memory("Time of death: [tod]", category=MIND_MEMORY_GENERAL, forced=TRUE) //mind. ? return ..(gibbed) diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm index 36d90f8eb831..2a6480754949 100644 --- a/code/modules/mob/living/carbon/human/death.dm +++ b/code/modules/mob/living/carbon/human/death.dm @@ -116,7 +116,7 @@ stat = DEAD tod = worldtime2text() if(mind) - mind.store_memory("Time of death: [tod]", 0) + mind.store_memory("Time of death: [tod]", category=MIND_MEMORY_GENERAL, forced=TRUE) if(!(mind && mind.suiciding)) //Cowards don't count score.deadcrew++ if (dorfpod) diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index ebc5a583897f..b8d41c7b5522 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -157,7 +157,7 @@ var/list/ai_list = list() if (mind && !stored_freqs) to_chat(src, "The various frequencies used by the crew to communicate have been stored in your mind. Use the verb Notes to access them.") spawn(1) - mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
") + mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
", category=MIND_MEMORY_GENERAL, forced=TRUE) stored_freqs = 1 job = "AI" diff --git a/code/modules/mob/living/silicon/ai/death.dm b/code/modules/mob/living/silicon/ai/death.dm index ec3c126d5bd7..d94a13c223b1 100644 --- a/code/modules/mob/living/silicon/ai/death.dm +++ b/code/modules/mob/living/silicon/ai/death.dm @@ -66,7 +66,7 @@ tod = worldtime2text() //weasellos time of death patch if(mind) - mind.store_memory("Time of death: [tod]", 0) + mind.store_memory("Time of death: [tod]", category=MIND_MEMORY_GENERAL, forced=TRUE) if(!mind.suiciding) //Cowards don't count score.deadaipenalty += 1 diff --git a/code/modules/mob/living/silicon/ai/login.dm b/code/modules/mob/living/silicon/ai/login.dm index 4e69bd1f3472..8adcd04c1a24 100644 --- a/code/modules/mob/living/silicon/ai/login.dm +++ b/code/modules/mob/living/silicon/ai/login.dm @@ -15,7 +15,7 @@ if (mind && !stored_freqs) to_chat(src, "The various frequencies used by the crew to communicate have been stored in your mind. Use the verb Notes to access them.") spawn(1) - mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
") + mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
", category=MIND_MEMORY_GENERAL, forced=TRUE) stored_freqs = 1 var/datum/role/malfAI/M = mind.GetRole(MALF) if(M) diff --git a/code/modules/mob/living/silicon/robot/death.dm b/code/modules/mob/living/silicon/robot/death.dm index 8eab3f7d88c1..dcc56e20e1de 100644 --- a/code/modules/mob/living/silicon/robot/death.dm +++ b/code/modules/mob/living/silicon/robot/death.dm @@ -75,7 +75,7 @@ tod = worldtime2text() //weasellos time of death patch if(mind) - mind.store_memory("Time of death: [tod]", 0) + mind.store_memory("Time of death: [tod]", category=MIND_MEMORY_GENERAL, forced=TRUE) if(!mind.suiciding) score.deadsilicon += 1 diff --git a/code/modules/mob/living/silicon/robot/login.dm b/code/modules/mob/living/silicon/robot/login.dm index 5c6364cda6e6..b5a89296d007 100644 --- a/code/modules/mob/living/silicon/robot/login.dm +++ b/code/modules/mob/living/silicon/robot/login.dm @@ -7,7 +7,7 @@ module.UpdateModuleHolder(src) if (mind && !stored_freqs) spawn(1) - mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
") + mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
", category=MIND_MEMORY_GENERAL, forced=TRUE) stored_freqs = 1 /*if(mind) ticker.mode.remove_revolutionary(mind) diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index f8ec6c1dfac8..1decca9f2643 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -143,7 +143,7 @@ if(mind && !stored_freqs) spawn(1) - mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
") + mind.store_memory("Frequencies list:
Command: [COMM_FREQ]
Security: [SEC_FREQ]
Medical: [MED_FREQ]
Science: [SCI_FREQ]
Engineering: [ENG_FREQ]
Service: [SER_FREQ] Cargo: [SUP_FREQ]
AI private: [AIPRIV_FREQ]
", category=MIND_MEMORY_GENERAL, forced=TRUE) stored_freqs = 1 if(cell) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 2c83aed171bf..c10997c85db6 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1085,10 +1085,11 @@ Use this proc preferably at the end of an equipment loadout message_admins("[usr.key]/([usr.name]) added the following message to their memory. [msg]") log_admin("[usr.key]/([usr.name]) added the following message to their memory. [msg]") if(mind) - mind.store_memory(msg) + mind.store_memory(msg, category = MIND_MEMORY_CUSTOM) else to_chat(src, "The game appears to have misplaced your mind datum, so we can't show you your notes.") +/* /mob/proc/store_memory(msg as message, popup, sane = 1) msg = copytext(msg, 1, MAX_MESSAGE_LEN) @@ -1102,6 +1103,7 @@ Use this proc preferably at the end of an equipment loadout if (popup) memory() +*/ //mob verbs are faster than object verbs. See http://www.byond.com/forum/?post=1326139&page=2#comment8198716 for why this isn't atom/verb/examine() /mob/verb/examination(atom/A as mob|obj|turf in view(src)) //It used to be oview(12), but I can't really say why diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index a4618cf805fa..2fd1eb1e530d 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -704,7 +704,7 @@ new_character.dna.ready_dna(new_character) if(new_character.mind) - new_character.mind.store_memory("Your blood type is: [new_character.dna.b_type]
") + new_character.mind.store_memory("Your blood type is: [new_character.dna.b_type]
", category=MIND_MEMORY_GENERAL, forced=TRUE) if(prefs.disabilities & DISABILITY_FLAG_NEARSIGHTED) new_character.dna.SetSEState(GLASSESBLOCK,1,1) diff --git a/code/modules/spells/changeling/absorb_dna.dm b/code/modules/spells/changeling/absorb_dna.dm index f2771a195001..556b11181069 100644 --- a/code/modules/spells/changeling/absorb_dna.dm +++ b/code/modules/spells/changeling/absorb_dna.dm @@ -89,7 +89,14 @@ user.changeling_update_languages(changeling.absorbed_languages) //Steal their memories! (using this instead of mind.store_memory so the lings own notes and stuff are always at the bottom) - var/newmemory = "
[T.real_name]'s memories:

[T.mind.memory]

[user.real_name]'s memories:

[user.mind.memory]" + var/list/newmemory = list(MIND_MEMORY_GENERAL = "", MIND_MEMORY_ANTAGONIST = "", MIND_MEMORY_CUSTOM = "") + for(var/M in newmemory) + if(T.mind.memory[M] && user.mind.memory[M]) + newmemory[M] = "[T.real_name]'s memories:

[T.mind.memory[M]]

[user.real_name]'s memories:

[user.mind.memory[M]]" + else if(T.mind.memory[M]) + newmemory[M] = "[T.real_name]'s memories:

[T.mind.memory[M]]" + else if(user.mind.memory[M]) + newmemory[M] = "[user.real_name]'s memories:

[user.mind.memory[M]]" user.mind.memory = newmemory //Steal their species! diff --git a/code/modules/spells/changeling/split.dm b/code/modules/spells/changeling/split.dm index 6cc9f0eb5597..6c5a0cd0a6c5 100644 --- a/code/modules/spells/changeling/split.dm +++ b/code/modules/spells/changeling/split.dm @@ -78,7 +78,7 @@ newbody.real_name = owner.current.real_name newbody.name = owner.current.name newbody.flavor_text = owner.current.flavor_text - newbody.mind.memory = owner.memory + newbody.mind.memory = owner.memory.Copy() if(oldspecies != newbody.dna.species) newbody.set_species(newbody.dna.species, 0) newbody.UpdateAppearance() diff --git a/icons/logos.dmi b/icons/logos.dmi index 8d4bef1843dde549b1775ed91568e8ce7e80815c..cc84af4ead5f8f8876dbc7b67bef5b4fdf12cae0 100644 GIT binary patch delta 14719 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 H(~$oGL@E## delta 13490 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 diff --git a/vgstation13.dme b/vgstation13.dme index 96f9dae1fad9..ac9bdb2ebf0b 100644 --- a/vgstation13.dme +++ b/vgstation13.dme @@ -433,6 +433,7 @@ #include "code\datums\gamemode\objectives\custom.dm" #include "code\datums\gamemode\objectives\destroyfaction.dm" #include "code\datums\gamemode\objectives\die.dm" +#include "code\datums\gamemode\objectives\divergentclone.dm" #include "code\datums\gamemode\objectives\escape.dm" #include "code\datums\gamemode\objectives\freeform.dm" #include "code\datums\gamemode\objectives\grue.dm" @@ -498,6 +499,7 @@ #include "code\datums\gamemode\role\changeling.dm" #include "code\datums\gamemode\role\clockwork.dm" #include "code\datums\gamemode\role\cultist.dm" +#include "code\datums\gamemode\role\divergentclone.dm" #include "code\datums\gamemode\role\giant_spider.dm" #include "code\datums\gamemode\role\grinch.dm" #include "code\datums\gamemode\role\grue.dm"