diff --git a/_maps/RandomRuins/SpaceRuins/cloning_facility.dmm b/_maps/RandomRuins/SpaceRuins/cloning_facility.dmm new file mode 100644 index 00000000000..b2d0e007685 --- /dev/null +++ b/_maps/RandomRuins/SpaceRuins/cloning_facility.dmm @@ -0,0 +1,586 @@ +//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE +"a" = ( +/turf/template_noop, +/area/template_noop) +"b" = ( +/turf/closed/wall/r_wall, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"c" = ( +/obj/effect/spawner/structure/window/reinforced, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"d" = ( +/turf/closed/wall/r_wall/rust, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"e" = ( +/obj/machinery/defibrillator_mount/loaded, +/turf/closed/wall/r_wall, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"f" = ( +/obj/effect/spawner/structure/window/reinforced, +/turf/open/floor/iron/airless, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"g" = ( +/obj/structure/table, +/obj/item/flashlight/lamp, +/obj/effect/decal/cleanable/cobweb, +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"h" = ( +/obj/structure/table, +/obj/item/pen, +/obj/item/paper/fluff/ruins/exp_cloning/log, +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"i" = ( +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/obj/machinery/computer/experimental_cloner, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"j" = ( +/obj/item/paper/fluff/ruins/exp_cloning/manual, +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/obj/structure/table, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"k" = ( +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/obj/machinery/experimental_cloner, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"l" = ( +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/obj/structure/showcase/horrific_experiment, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"m" = ( +/obj/effect/decal/cleanable/cobweb/cobweb2, +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/obj/machinery/light/small/directional/north, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"n" = ( +/obj/structure/sign/nanotrasen, +/turf/closed/wall, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"o" = ( +/obj/machinery/vending/snack/teal, +/obj/effect/decal/cleanable/dirt, +/turf/open/floor/iron/airless, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"p" = ( +/obj/effect/decal/cleanable/dirt, +/turf/open/floor/iron/airless, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"q" = ( +/obj/structure/sign/directions/science{ + dir = 8 + }, +/turf/closed/wall/r_wall, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"r" = ( +/obj/effect/decal/remains/human, +/obj/effect/decal/cleanable/blood/old, +/obj/item/organ/ears/cat, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"s" = ( +/obj/machinery/iv_drip, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"t" = ( +/obj/structure/bed, +/obj/item/bedsheet/medical, +/mob/living/basic/living_limb_flesh, +/obj/machinery/light/small/directional/north, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"v" = ( +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"w" = ( +/obj/structure/chair{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"x" = ( +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"y" = ( +/obj/structure/chair{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 8 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"z" = ( +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"A" = ( +/obj/structure/sign/departments/science, +/turf/closed/wall, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"B" = ( +/turf/open/floor/iron/airless, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"C" = ( +/turf/open/floor/plating/airless, +/area/space/nearstation) +"D" = ( +/obj/structure/chair/comfy{ + dir = 8 + }, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"E" = ( +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"F" = ( +/obj/machinery/door/airlock/research/glass, +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/obj/effect/turf_decal/stripes/line, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"G" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/obj/effect/turf_decal/stripes/line, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"H" = ( +/obj/machinery/door/airlock/research/glass, +/obj/effect/turf_decal/stripes/line, +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"I" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 8 + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"J" = ( +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"K" = ( +/obj/machinery/door/airlock/research/glass, +/turf/open/floor/iron/airless, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"M" = ( +/obj/structure/broken_flooring{ + dir = 8; + icon_state = "plating"; + name = "broken plating" + }, +/turf/template_noop, +/area/space/nearstation) +"N" = ( +/obj/item/book/random, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"O" = ( +/obj/structure/bookcase/random/fiction, +/obj/machinery/light/small/directional/south, +/turf/open/floor/iron, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"P" = ( +/obj/structure/table/glass, +/obj/structure/bedsheetbin, +/obj/machinery/light/cold/directional/south, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"Q" = ( +/obj/structure/table/glass, +/obj/item/disk/design_disk/limbs/felinid, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"R" = ( +/obj/structure/table/glass, +/obj/item/storage/medkit/regular, +/obj/item/healthanalyzer{ + desc = "A prototype hand-held body scanner able to distinguish vital signs of the subject."; + name = "prototype health analyzer" + }, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"S" = ( +/obj/structure/table/glass, +/obj/item/storage/box/syringes, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"T" = ( +/turf/closed/wall, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"U" = ( +/obj/effect/turf_decal/tile/blue{ + dir = 1 + }, +/obj/effect/turf_decal/tile/blue{ + dir = 4 + }, +/obj/effect/decal/cleanable/vomit/old, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"V" = ( +/turf/closed/wall/r_wall/rust, +/area/space/nearstation) +"W" = ( +/obj/machinery/experimental_cloner_scanner, +/turf/open/floor/iron/white, +/area/ruin/space/has_grav/powered/ancient_shuttle) +"X" = ( +/obj/item/gps/spaceruin, +/turf/open/floor/plating/airless, +/area/space/nearstation) + +(1,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(2,1,1) = {" +a +a +a +a +a +a +a +a +a +"} +(3,1,1) = {" +a +a +c +c +c +c +c +a +a +"} +(4,1,1) = {" +a +a +c +r +D +N +c +a +a +"} +(5,1,1) = {" +a +a +c +s +E +E +c +a +a +"} +(6,1,1) = {" +a +a +d +t +E +O +d +a +a +"} +(7,1,1) = {" +a +a +d +b +F +d +d +a +a +"} +(8,1,1) = {" +a +a +a +b +G +b +a +a +a +"} +(9,1,1) = {" +a +a +d +d +H +b +b +a +a +"} +(10,1,1) = {" +a +b +b +I +I +I +c +a +a +"} +(11,1,1) = {" +a +c +g +v +J +J +c +a +a +"} +(12,1,1) = {" +a +c +h +w +J +W +d +a +a +"} +(13,1,1) = {" +a +d +i +x +J +P +b +a +a +"} +(14,1,1) = {" +a +b +j +y +J +Q +c +a +a +"} +(15,1,1) = {" +a +b +k +U +J +R +c +a +a +"} +(16,1,1) = {" +a +e +l +x +J +S +d +a +a +"} +(17,1,1) = {" +a +d +m +z +J +J +d +a +a +"} +(18,1,1) = {" +a +d +n +A +K +T +b +a +a +"} +(19,1,1) = {" +a +f +o +B +p +X +V +a +a +"} +(20,1,1) = {" +a +f +p +p +C +C +M +a +a +"} +(21,1,1) = {" +a +d +q +C +C +M +a +a +a +"} +(22,1,1) = {" +a +a +b +B +M +a +a +a +a +"} +(23,1,1) = {" +a +a +a +a +a +a +a +a +a +"} diff --git a/code/__DEFINES/dcs/signals/signals_cloner.dm b/code/__DEFINES/dcs/signals/signals_cloner.dm new file mode 100644 index 00000000000..5474570d5c2 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_cloner.dm @@ -0,0 +1,2 @@ +/// Sent from [/obj/machinery/experimental_cloner_scanner/proc/complete_scan] : (/datum/experimental_cloning_record/new_record) +#define COMSIG_CLONER_SCAN_SUCCESSFUL "cloner_scan_successful" diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 2b96c668de2..da975f90ea5 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -60,6 +60,7 @@ #define ROLE_BRAINWASHED "Brainwashed Victim" #define ROLE_DEATHSQUAD "Deathsquad" #define ROLE_DRONE "Drone" +#define ROLE_EVIL_CLONE "Evil Clone" #define ROLE_EMAGGED_BOT "Malfunctioning Bot" #define ROLE_HIVE "Hivemind Host" //Role removed, left here for safety. #define ROLE_HYPNOTIZED "Hypnotized Victim" @@ -77,6 +78,7 @@ #define ROLE_SENTIENCE "Sentience Potion Spawn" #define ROLE_SOULTRAPPED_HERETIC "Soultrapped Heretic" #define ROLE_SYNDICATE "Syndicate" +#define ROLE_EXPERIMENTAL_CLONER "Experimental Cloner" #define ROLE_CLOWN_OPERATIVE "Clown Operative" #define ROLE_FREE_GOLEM "Free Golem" diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm index 7663bfa2a52..f4d21f95c0b 100644 --- a/code/_globalvars/lists/poll_ignore.dm +++ b/code/_globalvars/lists/poll_ignore.dm @@ -38,6 +38,7 @@ #define POLL_IGNORE_SYNDICATE "syndicate" #define POLL_IGNORE_VENUSHUMANTRAP "venus_human_trap" #define POLL_IGNORE_RECOVERED_CREW "recovered_crew" +#define POLL_IGNORE_EXPERIMENTAL_CLONER "experimental_cloner" GLOBAL_LIST_INIT(poll_ignore_desc, list( POLL_IGNORE_ACADEMY_WIZARD = "Academy Wizard Defender", diff --git a/code/datums/components/ghost_direct_control.dm b/code/datums/components/ghost_direct_control.dm index 8d9e632b059..b43ad734974 100644 --- a/code/datums/components/ghost_direct_control.dm +++ b/code/datums/components/ghost_direct_control.dm @@ -80,7 +80,6 @@ var/mob/chosen_one = SSpolling.poll_ghosts_for_target( question = poll_question, check_jobban = ban_type, - role = ban_type, poll_time = poll_length, checked_target = parent, ignore_category = poll_ignore_key, diff --git a/code/datums/quirks/_quirk.dm b/code/datums/quirks/_quirk.dm index fc01f7ca3dc..9529cf2befe 100644 --- a/code/datums/quirks/_quirk.dm +++ b/code/datums/quirks/_quirk.dm @@ -66,7 +66,7 @@ * * new_holder - The mob to add this quirk to. * * quirk_transfer - If this is being added to the holder as part of a quirk transfer. Quirks can use this to decide not to spawn new items or apply any other one-time effects. */ -/datum/quirk/proc/add_to_holder(mob/living/new_holder, quirk_transfer = FALSE, client/client_source) +/datum/quirk/proc/add_to_holder(mob/living/new_holder, quirk_transfer = FALSE, client/client_source, unique = TRUE) if(!new_holder) CRASH("Quirk attempted to be added to null mob.") @@ -100,7 +100,8 @@ if(!quirk_transfer) if(gain_text) to_chat(quirk_holder, gain_text) - add_unique(client_source) + if (unique) + add_unique(client_source) if(quirk_holder.client) post_add() diff --git a/code/datums/quirks/neutral_quirks/gamer.dm b/code/datums/quirks/neutral_quirks/gamer.dm index 9055b09ce7f..20aca2f3de5 100644 --- a/code/datums/quirks/neutral_quirks/gamer.dm +++ b/code/datums/quirks/neutral_quirks/gamer.dm @@ -21,7 +21,7 @@ RegisterSignal(quirk_holder, COMSIG_MOB_LOST_VIDEOGAME, PROC_REF(lost_game)) RegisterSignal(quirk_holder, COMSIG_MOB_PLAYED_VIDEOGAME, PROC_REF(gamed)) -/datum/quirk/gamer/add_unique(client/client_source) +/datum/quirk/gamer/post_add() // The gamer starts off quelled gaming_withdrawal_timer = addtimer(CALLBACK(src, PROC_REF(enter_withdrawal)), GAMING_WITHDRAWAL_TIME, TIMER_STOPPABLE) diff --git a/code/datums/quirks/positive_quirks/chipped.dm b/code/datums/quirks/positive_quirks/chipped.dm index 26b4883f9b3..a1af49f9bc5 100644 --- a/code/datums/quirks/positive_quirks/chipped.dm +++ b/code/datums/quirks/positive_quirks/chipped.dm @@ -21,7 +21,7 @@ associated_typepath = /datum/quirk/chipped customization_options = list(/datum/preference/choiced/chipped) -/datum/quirk/chipped/add_to_holder(mob/living/new_holder, quirk_transfer, client/client_source) +/datum/quirk/chipped/add_to_holder(mob/living/new_holder, quirk_transfer, client/client_source, unique = TRUE) var/chip_pref = client_source?.prefs?.read_preference(/datum/preference/choiced/chipped) if(isnull(chip_pref)) diff --git a/code/datums/ruins/space.dm b/code/datums/ruins/space.dm index 1af8c04997c..d2b81b413d0 100644 --- a/code/datums/ruins/space.dm +++ b/code/datums/ruins/space.dm @@ -528,21 +528,21 @@ BUBBERSTATION REMOVAL END */ /datum/map_template/ruin/space/commsbuoy id = "commsbuoy" suffix = "commsbuoy_lowtech.dmm" - name = "Kosmokomm Communications Buoy" + name = "Space-Ruin Kosmokomm Communications Buoy" description = "One of the SSC's many Comms Buoys, acting as a broadcaster, receiver and relay for interstellar communications. Due to the \ shoddy tech available, it does not enable local communications." /datum/map_template/ruin/space/commsbuoy_pirate id = "commsbuoy_pirate" suffix = "commsbuoy_pirate.dmm" - name = "Pirated Communications Buoy" + name = "Space-Ruin Pirated Communications Buoy" description = "A Comms Buoy satellite that has been hijacked by local criminal elements, acting as a broadcaster, receiver and relay for \ evil interstellar communications. Due to the shoddy tech available, it does not enable local communications." /datum/map_template/ruin/space/commsbuoy_nt id = "commsbuoy_nt" suffix = "commsbuoy_nt.dmm" - name = "Nanotrasen Model-7 Communications Buoy" + name = "Space-Ruin Nanotrasen Model-7 Communications Buoy" description = "One of Nanotrasen's highly advanced Communication Buoys. Besides acting as a broadcaster, receiver and relay for interstellar \ communications, the satellite also includes a Local-Network array and two multi-function satellite dishes, providing the local sector with \ connectivity - as long as you have your Employee ID handy. Though, this one has been reported to have some recent malfunctions." @@ -552,3 +552,9 @@ BUBBERSTATION REMOVAL END */ suffix = "film_studio.dmm" name = "Space-Ruin Space Sector 13 Local Channel" description = "A local TV station serving areas surrounding Space Sector 13, shows including but not limited to: talk shows, operas, stage plays and more!" + +/datum/map_template/ruin/space/experimental_cloning + id = "cloning_facility" + suffix = "cloning_facility.dmm" + name = "Space-Ruin Experimental Cloning Facility" + description = "A fragment of a research station containing a somewhat functional cloning device." diff --git a/code/game/machinery/experimental_cloner/experimental_cloner_computer.dm b/code/game/machinery/experimental_cloner/experimental_cloner_computer.dm new file mode 100644 index 00000000000..6e1b0542080 --- /dev/null +++ b/code/game/machinery/experimental_cloner/experimental_cloner_computer.dm @@ -0,0 +1,170 @@ +/// Computer which starts the experimental cloning process +/obj/machinery/computer/experimental_cloner + name = "experimental cloner control console" + desc = "It scans DNA structures." + circuit = /obj/item/circuitboard/computer/experimental_cloner + icon_screen = "crew" + icon_keyboard = "med_key" + light_color = LIGHT_COLOR_GREEN + interaction_flags_click = ALLOW_SILICON_REACH + /// Our current stored cloning record + var/datum/experimental_cloning_record/stored_record + /// Scanner we save a test subject from + var/obj/machinery/experimental_cloner_scanner/input + /// Pod we print someone into + var/obj/machinery/experimental_cloner/output + /// Whether we should automatically try to connect to nearby machines + var/find_connections = FALSE + +/obj/machinery/computer/experimental_cloner/Initialize(mapload, obj/item/circuitboard/circuit) + . = ..() + find_connections = mapload + +/obj/machinery/computer/experimental_cloner/post_machine_initialize() + . = ..() + if (find_connections) + connect_nearby_machines() + +/// Find nearby associated machinery and link up +/obj/machinery/computer/experimental_cloner/proc/connect_nearby_machines() + var/list/stuff_in_range = range(5, src) + + var/obj/machinery/experimental_cloner_scanner/scanner = locate() in stuff_in_range + if (!isnull(scanner)) + link_scanner(scanner) + var/obj/machinery/experimental_cloner/pod = locate() in stuff_in_range + if (!isnull(pod)) + link_pod(pod) + +/obj/machinery/computer/experimental_cloner/multitool_act(mob/living/user, obj/item/multitool/multi_tool) + . = NONE + if (machine_stat & BROKEN || isnull(multi_tool.buffer)) + return + + if (istype(multi_tool.buffer, /obj/machinery/experimental_cloner_scanner)) + unlink_scanner() + link_scanner(multi_tool.buffer) + balloon_alert(user, "scanner linked") + to_chat(user, span_notice("You link \the [multi_tool.buffer] with \the [src].")) + return ITEM_INTERACT_SUCCESS + if (istype(multi_tool.buffer, /obj/machinery/experimental_cloner)) + unlink_pod() + link_pod(multi_tool.buffer) + balloon_alert(user, "pod linked") + to_chat(user, span_notice("You link \the [multi_tool.buffer] with \the [src].")) + return ITEM_INTERACT_SUCCESS + +/obj/machinery/computer/experimental_cloner/ui_interact(mob/user, datum/tgui/ui) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ExperimentalCloner") + ui.open() + +/// Link up with a scanner to scan people +/obj/machinery/computer/experimental_cloner/proc/link_scanner(obj/machinery/experimental_cloner_scanner/scanner) + RegisterSignal(scanner, COMSIG_QDELETING, PROC_REF(unlink_scanner)) + RegisterSignal(scanner, COMSIG_CLONER_SCAN_SUCCESSFUL, PROC_REF(on_scan_complete)) + input = scanner + +/// Store the record made by scanning someone +/obj/machinery/computer/experimental_cloner/proc/on_scan_complete(obj/machinery/experimental_cloner_scanner/scanner, datum/experimental_cloning_record/record) + SIGNAL_HANDLER + stored_record = record + +/// Release held references on deletion +/obj/machinery/computer/experimental_cloner/proc/unlink_scanner() + SIGNAL_HANDLER + if (!input) + return + UnregisterSignal(input, list(COMSIG_CLONER_SCAN_SUCCESSFUL, COMSIG_QDELETING)) + input = null + +/// Link up with a pod to print people +/obj/machinery/computer/experimental_cloner/proc/link_pod(obj/machinery/experimental_cloner/pod) + RegisterSignal(pod, COMSIG_QDELETING, PROC_REF(unlink_pod)) + output = pod + +/// Release held references on deletion +/obj/machinery/computer/experimental_cloner/proc/unlink_pod() + SIGNAL_HANDLER + if (!output) + return + UnregisterSignal(output, COMSIG_QDELETING) + output = null + +/obj/machinery/computer/experimental_cloner/ui_data(mob/user) + var/list/data = list() + + var/mob/living/carbon/human/scanner_occupant = input?.occupant + var/cloning_time_left = !output || output.awaiting_ghost ? 0 : timeleft(output.running_timer) + + data["record_name"] = stored_record?.name + data["record_species"] = stored_record?.dna.species?.name + data["linked_scanner"] = !!input + data["scanner_occupant"] = scanner_occupant?.name + data["scanner_species"] = scanner_occupant?.dna?.species?.name + data["is_scanning"] = !!input?.scanning + data["linked_pod"] = !!output + data["is_cloning"] = !!output?.running + data["cloning_name"] = output?.loaded_record?.name + data["cloning_species"] = output?.loaded_record?.dna?.species?.name + data["cloning_progress"] = output? ((output.cloning_time - cloning_time_left) / output.cloning_time) * 100 : 0 + + return data + +/obj/machinery/computer/experimental_cloner/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if ("clear_record") + if (isnull(stored_record)) + return TRUE + stored_record = null + playsound(src, 'sound/machines/ping.ogg', vol = 100) + return TRUE + if ("start_scan") + if (isnull(input)) + balloon_alert(ui.user, "no linked scanner!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + if (isnull(input.occupant)) + balloon_alert(ui.user, "scanner empty!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + if (!iscarbon(input.occupant)) + balloon_alert(ui.user, "invalid subject!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + var/mob/living/carbon/carbon_occupant = input.occupant + if (!carbon_occupant.has_dna()) + balloon_alert(ui.user, "invalid subject!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + if (input.scanning) + balloon_alert(ui.user, "scanner busy!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + + log_combat(ui.user, input.occupant, "irradiated via experimental clone scan") + input.start_scan() + return TRUE + if ("start_clone") + if (isnull(output)) + balloon_alert(ui.user, "no linked pod!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + if (output.running) + balloon_alert(ui.user, "pod busy!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + if (isnull(stored_record)) + balloon_alert(ui.user, "no stored DNA!") + playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, TRUE) + return TRUE + + ui.user.log_message("began growing an experimental clone of [stored_record.name]", LOG_GAME, log_globally = TRUE) + output.start_cloning(stored_record) + return TRUE diff --git a/code/game/machinery/experimental_cloner/experimental_cloner_fuckup.dm b/code/game/machinery/experimental_cloner/experimental_cloner_fuckup.dm new file mode 100644 index 00000000000..6002ee0476f --- /dev/null +++ b/code/game/machinery/experimental_cloner/experimental_cloner_fuckup.dm @@ -0,0 +1,164 @@ +#define CLONER_FAILURE_COMMON 3 +#define CLONER_FAILURE_RARE 1 + +/// Things we can do to mess up the clones +/datum/experimental_cloner_fuckup + /// How likely is it? + var/weight + +/// Can we actually mutate this thing? +/datum/experimental_cloner_fuckup/proc/is_valid(species_type) + return TRUE + +/// What do we do to it +/datum/experimental_cloner_fuckup/proc/apply_to_mob(mob/living/carbon/human/victim) + return + +/// What do we do after someone has taken over the mob +/datum/experimental_cloner_fuckup/proc/post_emerged(mob/living/victim) + return + +/// Become cat +/datum/experimental_cloner_fuckup/felinise + weight = CLONER_FAILURE_COMMON + +/datum/experimental_cloner_fuckup/felinise/is_valid(species_type) + return !ispath(species_type, /datum/species/human/felinid) + +/datum/experimental_cloner_fuckup/felinise/apply_to_mob(mob/living/carbon/human/victim) + if (is_species(victim, /datum/species/human)) + victim.set_species(/datum/species/human/felinid) + else // I think this is funnier + var/obj/item/organ/tail/cat/new_tail = new(victim) + var/obj/item/organ/ears/cat/new_ears = new(victim) + var/obj/item/organ/tongue/cat/new_tongue = new(victim) + + new_tail.replace_into(victim) + new_ears.replace_into(victim) + new_tongue.replace_into(victim) + +/// Bald +/datum/experimental_cloner_fuckup/bald + weight = CLONER_FAILURE_COMMON + +/datum/experimental_cloner_fuckup/bald/apply_to_mob(mob/living/carbon/human/victim) + victim.set_facial_hairstyle("Shaved", update = FALSE) + victim.set_hairstyle("Bald", update = TRUE) + +/datum/experimental_cloner_fuckup/bald/is_valid(species_type) + return !is_path_in_list(/datum/species/human, /datum/species/ethereal) + +/// Give a brain trauma or two +/datum/experimental_cloner_fuckup/brain_trauma + weight = CLONER_FAILURE_COMMON + +/datum/experimental_cloner_fuckup/brain_trauma/apply_to_mob(mob/living/carbon/human/victim) + victim.gain_trauma_type() + if (prob(50)) + return + victim.gain_trauma_type() + +/// Roll the limb and organ dice a couple times +/datum/experimental_cloner_fuckup/scramble + weight = CLONER_FAILURE_COMMON + +/datum/experimental_cloner_fuckup/scramble/apply_to_mob(mob/living/carbon/human/victim) + victim.bioscramble("your fucked up genes") + if (prob(50)) + return + victim.bioscramble("your fucked up genes") + if (prob(75)) + return + victim.bioscramble("your fucked up genes") + +/// Mess with the genes +/datum/experimental_cloner_fuckup/mutate + weight = CLONER_FAILURE_COMMON + +/datum/experimental_cloner_fuckup/mutate/apply_to_mob(mob/living/carbon/human/victim) + victim.easy_random_mutate(NEGATIVE + MINOR_NEGATIVE) + if (prob(50)) + return + victim.easy_random_mutate(NEGATIVE + MINOR_NEGATIVE) + if (prob(75)) + return + victim.easy_random_mutate(NEGATIVE + MINOR_NEGATIVE) + +/// Makes your limbs mad at you +/datum/experimental_cloner_fuckup/lively_flesh + weight = CLONER_FAILURE_COMMON + +/datum/experimental_cloner_fuckup/lively_flesh/apply_to_mob(mob/living/carbon/human/victim) + var/list/valid_zones = GLOB.limb_zones.Copy() + for (var/i in 1 to (rand(1, 3))) + var/target_zone = pick_n_take(valid_zones) + + var/part_type + switch(target_zone) + if(BODY_ZONE_L_ARM) + part_type = /obj/item/bodypart/arm/left/flesh + if(BODY_ZONE_R_ARM) + part_type = /obj/item/bodypart/arm/right/flesh + if(BODY_ZONE_L_LEG) + part_type = /obj/item/bodypart/leg/left/flesh + if(BODY_ZONE_R_LEG) + part_type = /obj/item/bodypart/leg/right/flesh + + var/obj/item/bodypart/old_bodypart = victim.get_bodypart(target_zone) + var/obj/item/bodypart/new_bodypart = new part_type() + new_bodypart.replace_limb(victim, TRUE) + qdel(old_bodypart) + +/// Contaminated sample +/datum/experimental_cloner_fuckup/fly_mishap + weight = CLONER_FAILURE_COMMON + +/datum/experimental_cloner_fuckup/fly_mishap/apply_to_mob(mob/living/carbon/human/victim) + victim.set_species(/datum/species/fly) + +/// Return to monkey +/datum/experimental_cloner_fuckup/monkey + weight = CLONER_FAILURE_RARE + +/datum/experimental_cloner_fuckup/monkey/post_emerged(mob/living/carbon/victim) + victim.visible_message(\ + span_boldwarning("[victim]'s hair begins to grow rapidly!"),\ + span_boldwarning("As you emerge from the pod, all the hair on your body starts to grow!")) + victim.monkeyize() + +/// No skin +/datum/experimental_cloner_fuckup/skeletised + weight = CLONER_FAILURE_RARE + +/datum/experimental_cloner_fuckup/skeletised/post_emerged(mob/living/victim) + victim.emote("scream") + victim.visible_message(\ + span_boldwarning("[victim]'s flesh slithers off in a disgusting heap!"),\ + span_boldwarning("As you emerge from the pod, your skin slithers off onto the ground!")) + victim.set_species(/datum/species/skeleton) + new /obj/effect/gibspawner/human/bodypartless(victim.drop_location()) + +/// Become a psyker, possibly the worst fate on this list +/datum/experimental_cloner_fuckup/psykerize + weight = CLONER_FAILURE_RARE + +/datum/experimental_cloner_fuckup/total_failure/post_emerged(mob/living/carbon/human/victim) + victim.slow_psykerize() + +/// Just fuck me up +/datum/experimental_cloner_fuckup/total_failure + weight = CLONER_FAILURE_RARE + +/datum/experimental_cloner_fuckup/total_failure/post_emerged(mob/living/victim) + victim.emote("scream") + victim.visible_message(\ + span_boldwarning("[victim] collapses bonelessly into a writhing heap of flesh!"),\ + span_boldwarning("As you emerge from the pod, your boneless flesh collapses into a writhing heap!")) + var/mob/living/basic/fleshblob/blob = new(victim.drop_location()) + blob.name = victim.real_name + blob.real_name = victim.real_name + victim.mind?.transfer_to(blob, TRUE) + victim.gib() + +#undef CLONER_FAILURE_COMMON +#undef CLONER_FAILURE_RARE diff --git a/code/game/machinery/experimental_cloner/experimental_cloner_pod.dm b/code/game/machinery/experimental_cloner/experimental_cloner_pod.dm new file mode 100644 index 00000000000..5866b9d1824 --- /dev/null +++ b/code/game/machinery/experimental_cloner/experimental_cloner_pod.dm @@ -0,0 +1,177 @@ +/// How likely are we to do something weird to the clone? Persists between construct/deconstruct +GLOBAL_VAR_INIT(experimental_cloner_fuckup_chance, 50) + +/// Machine which experimentally clones people you scanned with the experimental cloner scanner +/obj/machinery/experimental_cloner + name = "experimental cloning pod" + desc = "An early prototype of the currently-outlawed cloning pods used by Nanotrasen executives. I wonder if it still works?" + icon = 'icons/obj/machines/cloning.dmi' + icon_state = "pod_0" + base_icon_state = "pod" + density = TRUE + circuit = /obj/item/circuitboard/machine/experimental_cloner + use_power = NO_POWER_USE + processing_flags = START_PROCESSING_MANUALLY + /// How likely are we to produce evil clones? + var/evil_chance = 2 + /// Are we cooking? + var/running = FALSE + /// Are we waiting for candidates? + var/awaiting_ghost = FALSE + /// Data for mob we're about to produce + var/datum/experimental_cloning_record/loaded_record + /// Sound to play while cooking + var/datum/looping_sound/oven/sound_loop + /// How long it takes to bake a new man + var/cloning_time = 1 MINUTES + /// Time until we're done cooking + var/running_timer + +/obj/machinery/experimental_cloner/Initialize(mapload) + . = ..() + sound_loop = new(src, FALSE) + +/obj/machinery/experimental_cloner/power_change() + . = ..() + if (machine_stat & NOPOWER && running) + fail_growing() + +/obj/machinery/experimental_cloner/examine(mob/user) + . = ..() + if (running) + . += span_notice("You can see a shape forming in the murky liquid.") + +/obj/machinery/experimental_cloner/update_icon_state() + . = ..() + icon_state = "[base_icon_state]_[running]" + +/obj/machinery/experimental_cloner/on_deconstruction(disassembled) + if (running) + new /obj/effect/gibspawner/human(drop_location()) + +/obj/machinery/experimental_cloner/welder_act(mob/living/user, obj/item/tool) + if (user.combat_mode) + return NONE + + if (!tool.tool_start_check(user, amount = 5)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You start slicing \the [src] apart.")) + if(!tool.use_tool(src, user, 6 SECONDS, amount = 5, volume = 50)) + return ITEM_INTERACT_BLOCKING + deconstruct(disassembled = TRUE) + to_chat(user, span_notice("You slice \the [src] apart.")) + return ITEM_INTERACT_SUCCESS + +/obj/machinery/experimental_cloner/multitool_act(mob/living/user, obj/item/multitool/tool) + tool.set_buffer(src) + balloon_alert(user, "frequency stored") + return ITEM_INTERACT_SUCCESS + +/// Start growing a guy +/obj/machinery/experimental_cloner/proc/start_cloning(datum/experimental_cloning_record/to_create) + if (!to_create || running) + return + running = TRUE + loaded_record = to_create // We'll need this later + update_use_power(ACTIVE_POWER_USE) + update_appearance(UPDATE_ICON_STATE) + sound_loop.start() + running_timer = addtimer(CALLBACK(src, PROC_REF(finish_cloning)), cloning_time, TIMER_STOPPABLE | TIMER_DELETE_ME) + +/// This one didn't make it +/obj/machinery/experimental_cloner/proc/fail_growing() + new /obj/effect/gibspawner/human(drop_location()) + playsound(src, 'sound/machines/toilet_flush.ogg', vol = 40, vary = TRUE) + deltimer(running_timer) + on_finished() + +/// Stuff to do when we stop processing +/obj/machinery/experimental_cloner/proc/on_finished() + loaded_record = null + running = FALSE + update_use_power(NO_POWER_USE) + update_appearance(UPDATE_ICON_STATE) + sound_loop.stop() + +/// Produce a man and poll for ghosts +/obj/machinery/experimental_cloner/proc/finish_cloning() + var/datum/experimental_cloner_fuckup/mistake = get_cloning_mistake() + + var/mob/living/result = create_result_mob() + result.mind_initialize() + mistake?.apply_to_mob(result) + playsound(src, 'sound/machines/microwave/microwave-end.ogg', vol = 100) + + RegisterSignals(result, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_clone_failed)) + + awaiting_ghost = TRUE + var/mob/chosen_one = SSpolling.poll_ghosts_for_target( + check_jobban = ROLE_RECOVERED_CREW, + poll_time = 10 SECONDS, + checked_target = result, + ignore_category = POLL_IGNORE_EXPERIMENTAL_CLONER, + alert_pic = result, + role_name_text = "Clone of [loaded_record.name]", + announce_chosen = TRUE, + ) + + awaiting_ghost = FALSE + if(QDELETED(result)) + return // We'll assume the signal will handle ending the cloning process + if(isnull(chosen_one)) + on_clone_failed(result) + return + + GLOB.experimental_cloner_fuckup_chance += rand(1, 5) // Each success gets more unstable + + chosen_one.log_message("took control of experimental clone of [result].", LOG_GAME) + result.PossessByPlayer(chosen_one.ckey) + to_chat(chosen_one, span_boldnotice("You are [loaded_record.name]! You aren't quite sure where you are or how you got here, though.")) + var/policy = get_policy(ROLE_EXPERIMENTAL_CLONER) + if (policy) + to_chat(chosen_one, span_notice(policy)) + + UnregisterSignal(result, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING, COMSIG_LIVING_DEATH)) + result.forceMove(drop_location()) + if (prob(evil_chance)) + message_admins("[ADMIN_LOOKUPFLW(result)] has become an evil clone tasked to kill everyone else called '[result.real_name]'.") + result.mind?.add_antag_datum(/datum/antagonist/evil_clone) + + mistake?.post_emerged(result) + on_finished() + +/// Return either nothing or something weird to happen to our clone +/obj/machinery/experimental_cloner/proc/get_cloning_mistake() + if (!prob(GLOB.experimental_cloner_fuckup_chance)) + return + + var/list/all_types = subtypesof(/datum/experimental_cloner_fuckup) + var/list/weighted_types = list() + for (var/type in all_types) + var/datum/experimental_cloner_fuckup/new_type = new type() + if (!new_type.is_valid(loaded_record.dna.species.type)) + qdel(type) + continue + weighted_types[new_type] = new_type.weight + + return pick_weight(weighted_types) + +/// Return a mob for our tube to produce +/obj/machinery/experimental_cloner/proc/create_result_mob() + var/mob/living/carbon/human/new_clone = new(src) + loaded_record.apply_profile(new_clone) + + if (prob(75)) + var/static/list/permitted_heights = list(HUMAN_HEIGHT_SHORTEST, HUMAN_HEIGHT_SHORT, HUMAN_HEIGHT_MEDIUM, HUMAN_HEIGHT_TALL, HUMAN_HEIGHT_TALLER, HUMAN_HEIGHT_TALLEST) + new_clone.dna.remove_mutation(/datum/mutation/human/dwarfism) + new_clone.set_mob_height(pick(permitted_heights - loaded_record.height)) // To differentiate the clones + + return new_clone + +/// Somehow our clone exited the cloner or was deleted before we were ready or we just didn't find any ghosts +/obj/machinery/experimental_cloner/proc/on_clone_failed(mob/living/clone) + SIGNAL_HANDLER + UnregisterSignal(clone, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING, COMSIG_LIVING_DEATH)) + if (!QDELETED(clone)) + qdel(clone) // Fuck you then + fail_growing() diff --git a/code/game/machinery/experimental_cloner/experimental_cloner_scanner.dm b/code/game/machinery/experimental_cloner/experimental_cloner_scanner.dm new file mode 100644 index 00000000000..e1e541e19f9 --- /dev/null +++ b/code/game/machinery/experimental_cloner/experimental_cloner_scanner.dm @@ -0,0 +1,166 @@ +/// Machine that you put someone in to scan them for the experimental cloner +/obj/machinery/experimental_cloner_scanner + name = "experimental cloning scanner" + desc = "An old prototype DNA scanner, compatible with an experimental cloning setup." + icon = 'icons/obj/machines/cloning.dmi' + icon_state = "scanner" + base_icon_state = "scanner" + density = TRUE + obj_flags = BLOCKS_CONSTRUCTION + interaction_flags_mouse_drop = NEED_DEXTERITY + occupant_typecache = list(/mob/living, /obj/item/bodypart/head, /obj/item/organ/brain) + circuit = /obj/item/circuitboard/machine/experimental_cloner_scanner + use_power = NO_POWER_USE + processing_flags = START_PROCESSING_MANUALLY + /// No exit while it's running + var/locked = FALSE + /// Are we trying to scan someone? + var/scanning = FALSE + /// How long does scanning take? + var/scan_time = 10 SECONDS + /// How long does breaking out take? + var/breakout_time = 3 SECONDS + /// Sound to play while scanning + var/datum/looping_sound/microwave/soundloop + /// Timer storing our scanning progress + var/scan_timer + /// Time to wait between telling people they can't get out + COOLDOWN_DECLARE(message_cooldown) + +/obj/machinery/experimental_cloner_scanner/Initialize(mapload) + . = ..() + soundloop = new (src) + +/// Scan the occupant, eventually producing a [/datum/experimental_cloning_record]. Returns FALSE if unsuccessful. +/obj/machinery/experimental_cloner_scanner/proc/start_scan() + if (machine_stat & BROKEN || machine_stat & NOPOWER || isnull(occupant)) + playsound(src, 'sound/machines/scanner/scanbuzz.ogg', vol = 100) + return FALSE + + SSradiation.irradiate(occupant) + scanning = TRUE + locked = TRUE + update_use_power(ACTIVE_POWER_USE) + playsound(src, 'sound/machines/closet/closet_unlock.ogg', vol = 100) + soundloop.start() + scan_timer = addtimer(CALLBACK(src, PROC_REF(complete_scan)), scan_time, TIMER_STOPPABLE | TIMER_DELETE_ME) + +/// Successfully produce a scan record +/obj/machinery/experimental_cloner_scanner/proc/complete_scan() + if (isnull(occupant)) + fail_scan() + return + var/datum/experimental_cloning_record/new_record = new() + new_record.create_profile(occupant) + SEND_SIGNAL(src, COMSIG_CLONER_SCAN_SUCCESSFUL, new_record) + + on_scan_stopped() + +/// There's nobody in the tank, so nothing to scan +/obj/machinery/experimental_cloner_scanner/proc/fail_scan() + playsound(src, 'sound/machines/scanner/scanbuzz.ogg', vol = 100) + on_scan_stopped() + +/// Generic stuff to do when we're done scanning +/obj/machinery/experimental_cloner_scanner/proc/on_scan_stopped() + update_use_power(NO_POWER_USE) + scanning = FALSE + if (locked) + playsound(src, 'sound/machines/closet/closet_unlock.ogg', vol = 100) + locked = FALSE + soundloop.stop() + deltimer(scan_timer) + +/obj/machinery/experimental_cloner_scanner/power_change() + . = ..() + if (machine_stat & NOPOWER && scanning) + fail_scan() + +/obj/machinery/experimental_cloner_scanner/open_machine(drop, density_to_set) + . = ..() + if (scanning) + fail_scan() + +/obj/machinery/experimental_cloner_scanner/update_icon_state() + //no power or maintenance + if (machine_stat & (NOPOWER|BROKEN)) + icon_state = "[base_icon_state][state_open ? "_open" : null]_unpowered" + return ..() + + //running and someone in there + if (occupant) + icon_state = "[base_icon_state]_occupied" + return ..() + + //running + icon_state = "[base_icon_state][state_open ? "_open" : null]" + return ..() + +/obj/machinery/experimental_cloner_scanner/container_resist_act(mob/living/user) + if (!locked) + open_machine() + return + + user.changeNext_move(CLICK_CD_BREAKOUT) + user.last_special = world.time + CLICK_CD_BREAKOUT + user.visible_message(span_notice("You see [user] kicking against the door of [src]!"), \ + span_notice("You lean on the back of [src] and start pushing the door open..."), \ + span_hear("You hear a metallic creaking from [src].")) + + balloon_alert(user, "breaking out...") + if (!do_after(user,(breakout_time), target = src)) + return + if (!user || user.stat != CONSCIOUS || user.loc != src || state_open || !locked) + return + + locked = FALSE + user.visible_message(span_warning("[user] successfully broke out of [src]!"), \ + span_notice("You successfully break out of [src]!")) + open_machine() + +/obj/machinery/experimental_cloner_scanner/relaymove(mob/living/user, direction) + if (user.stat || locked) + if (COOLDOWN_FINISHED(src, message_cooldown)) + COOLDOWN_START(src, message_cooldown, breakout_time) + balloon_alert(user, "door locked!") + container_resist_act(user) + return + open_machine() + +/obj/machinery/experimental_cloner_scanner/interact(mob/user) + toggle_open(user) + +/obj/machinery/experimental_cloner_scanner/mouse_drop_receive(atom/target, mob/user, params) + if (!iscarbon(target)) + return + close_machine(target) + +/// Try opening the machine if it's not locked +/obj/machinery/experimental_cloner_scanner/proc/toggle_open(mob/user) + if (state_open) + close_machine() + return + + if (locked) + balloon_alert(user, "it's locked!") + return + + open_machine() + +/obj/machinery/experimental_cloner_scanner/welder_act(mob/living/user, obj/item/tool) + if (user.combat_mode) + return NONE + + if (!tool.tool_start_check(user, amount = 5)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You start slicing \the [src] apart.")) + if(!tool.use_tool(src, user, 6 SECONDS, amount = 5, volume = 50)) + return ITEM_INTERACT_BLOCKING + deconstruct(disassembled = TRUE) + to_chat(user, span_notice("You slice \the [src] apart.")) + return ITEM_INTERACT_SUCCESS + +/obj/machinery/experimental_cloner_scanner/multitool_act(mob/living/user, obj/item/multitool/tool) + tool.set_buffer(src) + balloon_alert(user, "frequency stored") + return ITEM_INTERACT_SUCCESS diff --git a/code/game/machinery/experimental_cloner/experimental_cloning_record.dm b/code/game/machinery/experimental_cloner/experimental_cloning_record.dm new file mode 100644 index 00000000000..71a000059e6 --- /dev/null +++ b/code/game/machinery/experimental_cloner/experimental_cloning_record.dm @@ -0,0 +1,83 @@ +/// Data we store to clone someone +/datum/experimental_cloning_record + /// The name of the cloned subject + var/name + /// The DNA of the cloned subject + var/datum/dna/dna + /// The types of the quirks of the cloned subject + var/list/quirks + /// The types of the brain traumas of the cloned subject at time of scanning + var/list/brain_traumas + /// The age of the cloned subject + var/age + /// The bodytype of the cloned subject + var/physique + /// The athletics level of the cloned subject + var/athletics_level + /// The TTS voice of the cloned subject + var/voice + /// The TTS voice filter of the cloned subject + var/voice_filter + /// The height of the cloned subject + var/height + +/// Record data from a human subject +/datum/experimental_cloning_record/proc/create_profile(mob/living/carbon/human/subject) + name = subject.real_name + dna = new subject.dna.type() + subject.dna.copy_dna(dna) + age = subject.age + physique = subject.physique + athletics_level = subject.mind?.get_skill_level(/datum/skill/athletics) || SKILL_LEVEL_NONE + height = subject.mob_height + + quirks = list() + for(var/datum/quirk/quirk as anything in subject.quirks) + LAZYADD(quirks, quirk.type) + + brain_traumas = list() + for(var/datum/brain_trauma/trauma as anything in subject.get_traumas()) + LAZYADD(brain_traumas, trauma.type) + + voice = subject.voice + voice_filter = subject.voice_filter + +/// Duplicates our data +/datum/experimental_cloning_record/proc/create_copy() + var/datum/experimental_cloning_record/new_record = new() + new_record.name = name + new_record.dna = new dna.type() + dna.copy_dna(new_record.dna) + new_record.age = age + new_record.physique = physique + new_record.athletics_level = athletics_level + new_record.quirks = quirks.Copy() + new_record.brain_traumas = brain_traumas.Copy() + new_record.voice = voice + new_record.voice_filter = voice_filter + return new_record + +/// Apply record data to a clone +/datum/experimental_cloning_record/proc/apply_profile(mob/living/carbon/human/subject) + subject.name = name + subject.real_name = name + subject.age = age + subject.physique = physique + subject.mind?.set_level(/datum/skill/athletics, athletics_level, silent = TRUE) + subject.voice = voice + subject.voice_filter = voice_filter + + dna.transfer_identity(subject, TRUE) + + for (var/quirk_type as anything in quirks) + subject.add_quirk(quirk_type, add_unique = FALSE) + + for (var/trauma_type as anything in brain_traumas) + subject.gain_trauma(trauma_type) + + for (var/obj/item/bodypart/limb as anything in subject.bodyparts) + limb.update_limb(is_creating = TRUE) + + subject.updateappearance(mutcolor_update = TRUE) + subject.domutcheck() + subject.regenerate_icons() diff --git a/code/game/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm index e77e36a0dac..98d738235bd 100644 --- a/code/game/objects/items/circuitboards/computer_circuitboards.dm +++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm @@ -405,6 +405,11 @@ greyscale_colors = CIRCUIT_COLOR_MEDICAL build_path = /obj/machinery/computer/pandemic +/obj/item/circuitboard/computer/experimental_cloner + name = "Experimental Cloner Control Console" + greyscale_colors = CIRCUIT_COLOR_MEDICAL + build_path = /obj/machinery/computer/experimental_cloner + //Science /obj/item/circuitboard/computer/aifixer diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm index 976daeb1a34..108d320e53d 100644 --- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm @@ -1158,6 +1158,28 @@ /obj/item/stack/cable_coil = 2, ) +/obj/item/circuitboard/machine/experimental_cloner_scanner + name = "Experimental Cloning Scanner" + greyscale_colors = CIRCUIT_COLOR_MEDICAL + build_path = /obj/machinery/experimental_cloner_scanner + req_components = list( + /datum/stock_part/scanning_module = 1, + /datum/stock_part/matter_bin = 1, + /datum/stock_part/micro_laser = 1, + /obj/item/stack/sheet/glass = 1, + /obj/item/stack/cable_coil = 2 + ) + +/obj/item/circuitboard/machine/experimental_cloner + name = "Experimental Cloning Pod" + greyscale_colors = CIRCUIT_COLOR_MEDICAL + build_path = /obj/machinery/experimental_cloner + req_components = list( + /datum/stock_part/matter_bin = 1, + /obj/item/stack/cable_coil = 1, + /obj/item/stack/sheet/glass = 4 + ) + /obj/item/circuitboard/machine/mechpad name = "Mecha Orbital Pad" greyscale_colors = CIRCUIT_COLOR_SCIENCE diff --git a/code/modules/antagonists/evil_clone/evil_clone.dm b/code/modules/antagonists/evil_clone/evil_clone.dm new file mode 100644 index 00000000000..3d6521d97da --- /dev/null +++ b/code/modules/antagonists/evil_clone/evil_clone.dm @@ -0,0 +1,67 @@ +/// Antag datum associated with the experimental cloner +/datum/antagonist/evil_clone + name = "\improper Evil Clone" + stinger_sound = 'sound/music/antag/hypnotized.ogg' + job_rank = ROLE_EVIL_CLONE + roundend_category = "evil clones" + show_in_antagpanel = TRUE + antagpanel_category = ANTAG_GROUP_CREW + show_name_in_check_antagonists = TRUE + count_against_dynamic_roll_chance = FALSE + +/datum/antagonist/evil_clone/on_gain() + if (owner.current) + name = "[owner.current.real_name] Prime" + forge_objectives() + return ..() + +/datum/antagonist/evil_clone/greet() + if(silent) + return + play_stinger() + var/mob/living/current_mob = owner.current + if (current_mob) + to_chat(current_mob, span_big("You are [current_mob.real_name].")) + to_chat(current_mob, span_hypnophrase("You are the only [current_mob.real_name].")) + to_chat(current_mob, span_boldwarning("Anyone else pretending to be [current_mob.real_name] must be punished.")) + owner.announce_objectives() + +/datum/antagonist/evil_clone/forge_objectives() + var/datum/objective/accept_no_substitutes/objective = new + objective.owner = owner + objective.set_target_name(owner.current?.real_name) + objectives += objective + +/// Kill everyone with the same name as you +/datum/objective/accept_no_substitutes + name = "kill all clones" + explanation_text = "Ensure that nobody with a particular name that you don't remember remains alive." + admin_grantable = TRUE + /// What name do we want to expunge? + var/target_name + +/// Set the name to check for +/datum/objective/accept_no_substitutes/proc/set_target_name(new_name) + target_name = new_name + explanation_text = "Ensure that nobody with the name [target_name] remains alive." + +/datum/objective/accept_no_substitutes/check_completion() + if (!target_name) + return FALSE // Well we forgot to check for a name + + for (var/mob/living/someone as anything in GLOB.player_list) // We will generously not include people who logged out or ghosted + if (!istype(someone)) + continue + if (someone.stat == DEAD) + continue + if (someone == owner.current) + continue + if (someone.real_name == target_name) + return FALSE + + return TRUE + +/datum/objective/accept_no_substitutes/admin_edit(mob/admin) + admin_simple_target_pick(admin) + if (target.current) + set_target_name(target.current.real_name) diff --git a/code/modules/mapfluff/ruins/spaceruin_code/cloning_lab.dm b/code/modules/mapfluff/ruins/spaceruin_code/cloning_lab.dm new file mode 100644 index 00000000000..02451a25e4b --- /dev/null +++ b/code/modules/mapfluff/ruins/spaceruin_code/cloning_lab.dm @@ -0,0 +1,35 @@ +/obj/item/paper/fluff/ruins/exp_cloning/manual + name = "paper - 'H-11 Cloning Apparatus Manual" + default_raw_text = "
The provided CLONEPOD SYSTEM will produce the desired clone. Standard clone maturation times are roughly 120 seconds. \ + The cloning pod will automatically dispose of subjects whose brains fail to develop properly.