From da9eb945fcffd7f4644afc73fc99c5a1078456c9 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Fri, 12 Sep 2025 09:44:11 -0500 Subject: [PATCH] Adds a 1/5 chance for Paradox Clone to spawn two for the price of one (#92855) ## About The Pull Request Paradox Clone ruleset has a 1/5 chance of spawning an additional clone of the same target. This chance is configurable (so server operators can disable it), but the amount of clones it adds is not. The second clone is *not* on a team with the first. Paradox Clone's primary objective has been updated to `Be the only [x] alive`, rather than `Kill [x]`. - [ ] I tested this pr ## Why It's Good For The Game A while back someone suggested "What if Paradox Clone spawned a third clone with the objective to protect the guy instead of kill them" I thought this sounded pretty neat but the idea of spawning an antag that worked against antags seemed to be grounds for problems. So I stole half the idea. This adds a little more variety in Paradox Clone happenings: - Clones have no idea or indication if there are other clones (unless you spawn together - which is not guaranteed), so each clone is less sure of things and has to improvise more - Neither the clones nor the crew can be fully complacent after dealing with one "clone" - More plausible deniability for Genetics or Changelings to mess with people - And yes, even the clones aren't 100% sure if the *other* clone is real or fake, so now they're roped in on the fun ## Changelog :cl: Melbert add: There's a 1/5 chance that Paradox Clone spawns a second clone if there are enough candidates. Neither clone is aware of or on a team with the other - There can only be one. add: Paradox Clones are now tasked to `Be the only [x] alive`, meaning means of identity cloning (such as Genetics or Changelings) may throw a wrench in your plan. /:cl: --- .../dynamic/dynamic_ruleset_midround.dm | 15 ++++-- .../paradox_clone/paradox_clone.dm | 54 +++++++++---------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm index 452979890e2..d9fbab9245a 100644 --- a/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm +++ b/code/controllers/subsystem/dynamic/dynamic_ruleset_midround.dm @@ -678,21 +678,28 @@ min_pop = 10 max_antag_cap = 1 signup_atom_appearance = /obj/effect/bluespace_stream + /// Chance of getting another clone for the price of free + var/bonus_clone_chance = 20 + +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/New(list/dynamic_config) + . = ..() + max_antag_cap += prob(bonus_clone_chance) /datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/can_be_selected() return ..() && !isnull(find_clone()) && !isnull(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)) +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/create_execute_args() + return list(find_clone()) + /datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/create_ruleset_body() return // handled by assign_role() entirely -/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/assign_role(datum/mind/candidate) - var/mob/living/carbon/human/good_version = find_clone() +/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/assign_role(datum/mind/candidate, mob/living/carbon/human/good_version) var/mob/living/carbon/human/bad_version = good_version.make_full_human_copy(find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)) candidate.transfer_to(bad_version, force_key_move = TRUE) var/datum/antagonist/paradox_clone/antag = candidate.add_antag_datum(/datum/antagonist/paradox_clone) - antag.original_ref = WEAKREF(good_version.mind) - antag.setup_clone() + antag.setup_clone(good_version.mind) playsound(bad_version, 'sound/items/weapons/zapbang.ogg', 30, TRUE) bad_version.put_in_hands(new /obj/item/storage/toolbox/mechanical()) //so they dont get stuck in maints diff --git a/code/modules/antagonists/paradox_clone/paradox_clone.dm b/code/modules/antagonists/paradox_clone/paradox_clone.dm index c5100bed01b..d009d9b5d48 100644 --- a/code/modules/antagonists/paradox_clone/paradox_clone.dm +++ b/code/modules/antagonists/paradox_clone/paradox_clone.dm @@ -8,9 +8,6 @@ suicide_cry = "THERE CAN BE ONLY ONE!!" preview_outfit = /datum/outfit/paradox_clone - ///Weakref to the mind of the original, the clone's target. - var/datum/weakref/original_ref - /datum/antagonist/paradox_clone/get_preview_icon() var/icon/final_icon = render_preview_outfit(preview_outfit) @@ -28,27 +25,27 @@ return clone_icon -/datum/antagonist/paradox_clone/Destroy() - original_ref = null - return ..() +/datum/antagonist/paradox_clone/proc/setup_clone(datum/mind/original_mind) + if(isnull(original_mind)) + CRASH("Tried to set up a paradox clone without an original mind!") -/datum/antagonist/paradox_clone/proc/setup_clone() - var/datum/mind/original_mind = original_ref?.resolve() - - var/datum/objective/assassinate/paradox_clone/kill = new + var/datum/objective/accept_no_substitutes/kill = new() kill.owner = owner - kill.target = original_mind - kill.update_explanation_text() + kill.set_target_name(original_mind.name || "Unknown") objectives += kill + var/datum/objective/paradox_clone_replace/replace = new() + replace.owner = owner + replace.name_and_job = "[original_mind.name], the [original_mind.assigned_role.title]" + replace.update_explanation_text() + objectives += replace + owner.set_assigned_role(SSjob.get_job_type(/datum/job/paradox_clone)) //clone doesnt show up on message lists var/obj/item/modular_computer/pda/messenger = locate() in owner.current - if(messenger) - var/datum/computer_file/program/messenger/message_app = locate() in messenger.stored_files - if(message_app) - message_app.invisible = TRUE + var/datum/computer_file/program/messenger/message_app = locate() in messenger?.stored_files + message_app?.invisible = TRUE //dont want anyone noticing there's two now var/mob/living/carbon/human/clone_human = owner.current @@ -64,6 +61,17 @@ /datum/antagonist/paradox_clone/roundend_report_header() return span_header("A paradox clone appeared on the station!
") +/datum/objective/paradox_clone_replace + name = "clone replace" + /// Name and job of the original to replace + var/name_and_job + +/datum/objective/paradox_clone_replace/update_explanation_text() + explanation_text = "Take [name_and_job || "someone's"]'s place. Avoid collateral damage - remember, your mission is to blend in!" + +/datum/objective/paradox_clone_replace/check_completion() + return completed || considered_alive(owner) + /datum/outfit/paradox_clone name = "Paradox Clone (Preview only)" @@ -71,20 +79,6 @@ gloves = /obj/item/clothing/gloves/color/black head = /obj/item/clothing/head/soft/purple -/** - * Paradox clone assassinate objective - * Similar to the original, but with a different flavortext. - */ -/datum/objective/assassinate/paradox_clone - name = "clone assassinate" - -/datum/objective/assassinate/paradox_clone/update_explanation_text() - . = ..() - if(!target?.current) - explanation_text = "Free Objective" - CRASH("WARNING! [ADMIN_LOOKUPFLW(owner)] paradox clone objectives forged without an original!") - explanation_text = "Murder and replace [target.name], the [!target_role_type ? target.assigned_role.title : english_list(target.get_special_roles())]. Remember, your mission is to blend in, do not kill anyone else unless you have to!" - ///Static bluespace stream used in its ghost poll icon. /obj/effect/bluespace_stream name = "bluespace stream"