[alias]"
- temp_html += "
[mut_desc]
"
- if(active && !storage_slot)
- if(HM?.can_chromosome && (HM in viable_occupant.dna.mutations))
- var/i = viable_occupant.dna.mutations.Find(HM)
- var/chromosome_name = "
----"
- if(HM.chromosome_name)
- chromosome_name = HM.chromosome_name
- temp_html += "
Chromosome status: [chromosome_name]
"
- temp_html += "
Compatible chromosomes: [jointext(HM.valid_chrom_list, ", ")]
"
-
- temp_html += "
Sequence:
"
- if(!scrambled)
- for(var/block in 1 to A.blocks)
- var/whole_sequence = get_valid_gene_string(mutation)
- var/sequence = copytext_char(whole_sequence, 1+(block-1)*(DNA_SEQUENCE_LENGTH*2),(DNA_SEQUENCE_LENGTH*2*block+1))
- temp_html += "
"
- for(var/i in 1 to DNA_SEQUENCE_LENGTH)
- var/num = 1+(i-1)*2
- var/genenum = num+(DNA_SEQUENCE_LENGTH*2*(block-1))
- if(sequence[num] == "X")
- temp_html += " | "
- else
- temp_html += " | "
- temp_html += "
"
- for(var/i in 1 to DNA_SEQUENCE_LENGTH)
- temp_html += "| | "
- temp_html += "
"
- for(var/i in 1 to DNA_SEQUENCE_LENGTH)
- var/num = i*2
- var/genenum = num+(DNA_SEQUENCE_LENGTH*2*(block-1))
-
- if(sequence[num] == "X")
- temp_html += " | "
- else
- temp_html += " | "
- temp_html += "
"
- temp_html += "
"
- else
- temp_html = "
Sequence unreadable due to unpredictable mutation.
"
- if((active || storage_slot) && (injectorready < world.time) && !scrambled)
- temp_html += "
Print Activator"
- temp_html += "
Print Mutator"
- else
- temp_html += "
Print Activator"
- temp_html += "
Print Mutator"
- temp_html += "
"
- if(storage_slot)
- temp_html += "
Delete"
- if((LAZYLEN(stored_mutations) < max_storage) && diskette && !diskette.read_only)
- temp_html += "
Export"
- else
- temp_html += "
Export"
- temp_html += "
Back"
- else if(active && !scrambled)
- temp_html += "
Store"
- temp_html += "
Adv. Injector"
- if(extra || scrambled)
- temp_html += "
Nullify"
- else
- temp_html += "
Nullify"
- temp_html += "
"
- return temp_html
-
-/obj/machinery/computer/scan_consolenew/Topic(href, href_list)
+/obj/machinery/computer/scan_consolenew/ui_act(action, var/list/params)
if(..())
- return
- if(current_screen == "working")
- return
+ return TRUE
+
+ . = TRUE
add_fingerprint(usr)
usr.set_machine(src)
- var/mob/living/carbon/viable_occupant = get_viable_occupant()
+ switch(action)
+ // Connect this DNA Console to a nearby DNA Scanner
+ // Usually only activate as an option if there is no connected scanner
+ if("connect_scanner")
+ connect_to_scanner()
+ return
- //Basic Tasks///////////////////////////////////////////
- var/num = round(text2num(href_list["num"]))
- var/last_change
- switch(href_list["task"])
- if("togglelock")
- if(connected)
- connected.locked = !connected.locked
- if("toggleopen")
- if(connected)
- connected.toggle_open(usr)
- if("setduration")
- if(!num)
- num = round(input(usr, "Choose pulse duration:", "Input an Integer", null) as num|null)
- if(num)
- radduration = WRAP(num, 1, RADIATION_DURATION_MAX+1)
- if("setstrength")
- if(!num)
- num = round(input(usr, "Choose pulse strength:", "Input an Integer", null) as num|null)
- if(num)
- radstrength = WRAP(num, 1, RADIATION_STRENGTH_MAX+1)
- if("screen")
- current_screen = href_list["text"]
- if("scramble")
- if(viable_occupant && (scrambleready < world.time))
- viable_occupant.dna.remove_all_mutations(list(MUT_NORMAL, MUT_EXTRA))
- viable_occupant.dna.generate_dna_blocks()
- scrambleready = world.time + SCRAMBLE_TIMEOUT
- to_chat(usr,"
")
- viable_occupant.radiation += RADIATION_STRENGTH_MULTIPLIER*50/(connected.damage_coeff ** 2)
+ // Toggle the door open/closed status on attached DNA Scanner
+ if("toggle_door")
+ // GUARD CHECK - Scanner still connected and operational?
+ if(!scanner_operational())
+ return
- if("setbufferlabel")
- var/text = sanitize(input(usr, "Input a new label:", "Input a Text", null) as text|null)
- if(num && text)
- num = clamp(num, 1, NUMBER_OF_BUFFERS)
- var/list/buffer_slot = buffer[num]
- if(istype(buffer_slot))
- buffer_slot["label"] = text
- if("setbuffer")
- if(num && viable_occupant)
- num = clamp(num, 1, NUMBER_OF_BUFFERS)
- buffer[num] = list(
- "label"="Buffer[num]:[viable_occupant.real_name]",
- "UI"=viable_occupant.dna.uni_identity,
- "UE"=viable_occupant.dna.unique_enzymes,
- "name"=viable_occupant.real_name,
- "blood_type"=viable_occupant.dna.blood_type
- )
- if("clearbuffer")
- if(num)
- num = clamp(num, 1, NUMBER_OF_BUFFERS)
- var/list/buffer_slot = buffer[num]
- if(istype(buffer_slot))
- buffer_slot.Cut()
- if("transferbuffer")
- if(num && viable_occupant)
- switch(href_list["text"]) //Numbers are this high because other way upgrading laser is just not worth the hassle, and i cant think of anything better to inmrove
- if("ui")
- apply_buffer(SCANNER_ACTION_UI,num)
- if("ue")
- apply_buffer(SCANNER_ACTION_UE,num)
- if("mixed")
- apply_buffer(SCANNER_ACTION_MIXED,num)
- if("injector")
- if(num && injectorready < world.time)
- num = clamp(num, 1, NUMBER_OF_BUFFERS)
- var/list/buffer_slot = buffer[num]
- if(istype(buffer_slot))
- var/obj/item/dnainjector/timed/I
- switch(href_list["text"])
- if("ui")
- if(buffer_slot["UI"])
- I = new /obj/item/dnainjector/timed(loc)
- I.fields = list("UI"=buffer_slot["UI"])
- if(connected)
- I.damage_coeff = connected.damage_coeff
- if("ue")
- if(buffer_slot["name"] && buffer_slot["UE"] && buffer_slot["blood_type"])
- I = new /obj/item/dnainjector/timed(loc)
- I.fields = list("name"=buffer_slot["name"], "UE"=buffer_slot["UE"], "blood_type"=buffer_slot["blood_type"])
- if(connected)
- I.damage_coeff = connected.damage_coeff
- if("mixed")
- if(buffer_slot["UI"] && buffer_slot["name"] && buffer_slot["UE"] && buffer_slot["blood_type"])
- I = new /obj/item/dnainjector/timed(loc)
- I.fields = list("UI"=buffer_slot["UI"],"name"=buffer_slot["name"], "UE"=buffer_slot["UE"], "blood_type"=buffer_slot["blood_type"])
- if(connected)
- I.damage_coeff = connected.damage_coeff
- if(I)
- injectorready = world.time + INJECTOR_TIMEOUT
- if("loaddisk")
- if(num && diskette && diskette.fields)
- num = clamp(num, 1, NUMBER_OF_BUFFERS)
- buffer[num] = diskette.fields.Copy()
- if("savedisk")
- if(num && diskette && !diskette.read_only)
- num = clamp(num, 1, NUMBER_OF_BUFFERS)
- var/list/buffer_slot = buffer[num]
- if(istype(buffer_slot))
- diskette.name = "data disk \[[buffer_slot["label"]]\]"
- diskette.fields = buffer_slot.Copy()
- if("ejectdisk")
- if(diskette)
- diskette.forceMove(drop_location())
- diskette = null
- if("setdelayed")
- if(num)
- delayed_action = list("action"=text2num(href_list["delayaction"]),"buffer"=num)
- if("pulseui")
- if(num && viable_occupant && connected)
- radduration = WRAP(radduration, 1, RADIATION_DURATION_MAX+1)
- radstrength = WRAP(radstrength, 1, RADIATION_STRENGTH_MAX+1)
+ connected_scanner.toggle_open(usr)
+ return
- var/locked_state = connected.locked
- connected.locked = TRUE
+ // Toggle the door bolts on the attached DNA Scanner
+ if("toggle_lock")
+ // GUARD CHECK - Scanner still connected and operational?
+ if(!scanner_operational())
+ return
- current_screen = "working"
- ui_interact(usr)
+ connected_scanner.locked = !connected_scanner.locked
+ return
- sleep(radduration*10)
- current_screen = "ui"
+ // Scramble scanner occupant's DNA
+ if("scramble_dna")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ // GUARD CHECK - Is scramble DNA actually ready?
+ if(!can_modify_occupant() || !(scrambleready < world.time))
+ return
- if(viable_occupant && connected && connected.occupant==viable_occupant)
- viable_occupant.radiation += (RADIATION_IRRADIATION_MULTIPLIER*radduration*radstrength)/(connected.damage_coeff ** 2) //Read comment in "transferbuffer" section above for explanation
- switch(href_list["task"]) //Same thing as there but values are even lower, on best part they are about 0.0*, effectively no damage
- if("pulseui")
- var/len = length_char(viable_occupant.dna.uni_identity)
- num = WRAP(num, 1, len+1)
- num = randomize_radiation_accuracy(num, radduration + (connected.precision_coeff ** 2), len) //Each manipulator level above 1 makes randomization as accurate as selected time + manipulator lvl^2
- //Value is this high for the same reason as with laser - not worth the hassle of upgrading if the bonus is low
- var/block = round((num-1)/DNA_BLOCK_SIZE)+1
- var/subblock = num - block*DNA_BLOCK_SIZE
- last_change = "UI #[block]-[subblock]; "
+ scanner_occupant.dna.remove_all_mutations(list(MUT_NORMAL, MUT_EXTRA))
+ scanner_occupant.dna.generate_dna_blocks()
+ scrambleready = world.time + SCRAMBLE_TIMEOUT
+ to_chat(usr,"
")
+ scanner_occupant.radiation += RADIATION_STRENGTH_MULTIPLIER*50/(connected_scanner.damage_coeff ** 2)
+ return
- var/hex = copytext_char(viable_occupant.dna.uni_identity, num, num+1)
- last_change += "[hex]"
- hex = scramble(hex, radstrength, radduration)
- last_change += "->[hex]"
+ // Check whether a specific mutation is eligible for discovery within the
+ // scanner occupant
+ // This is additionally done when a mutation's tab is selected in the tgui
+ // interface. This is because some mutations, such as Monkified on monkeys,
+ // are infact completed by default but not yet discovered. Likewise, all
+ // mutations can have their sequence completed while Monkified is still an
+ // active mutation and thus won't immediately be discovered but could be
+ // discovered when Monkified is removed
+ // ---------------------------------------------------------------------- //
+ // params["alias"] - Alias of a mutation. The alias is the "hidden" name of
+ // the mutation, for example "Mutation 5" or "Mutation 33"
+ if("check_discovery")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
- viable_occupant.dna.uni_identity = copytext_char(viable_occupant.dna.uni_identity, 1, num) + hex + copytext_char(viable_occupant.dna.uni_identity, num + 1)
- viable_occupant.updateappearance(mutations_overlay_update=1)
+ // GUARD CHECK - Have we somehow cheekily swapped occupants? This is
+ // unexpected.
+ if(!(scanner_occupant == connected_scanner.occupant))
+ return
+
+ check_discovery(params["alias"])
+ return
+
+ // Check all mutations of the occupant and check if any are discovered.
+ // This is called when the Genetic Sequencer is selected. It'll do things
+ // like immediately discover Monkified without needing to click through
+ // the mutation tabs and handle cases where mutations are solved but not
+ // discovered due to the Monkified mutation being active then removed.
+ if("all_check_discovery")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
+
+ // GUARD CHECK - Have we somehow cheekily swapped occupants? This is
+ // unexpected.
+ if(!(scanner_occupant == connected_scanner.occupant))
+ return
+
+ // Go over all standard mutations and check if they've been discovered.
+ for(var/mutation_type in scanner_occupant.dna.mutation_index)
+ var/datum/mutation/human/HM = GET_INITIALIZED_MUTATION(mutation_type)
+ check_discovery(HM.alias)
+
+ return
+
+ // Set a gene in a mutation's genetic sequence. Will also check for mutations
+ // discovery as part of the process.
+ // ---------------------------------------------------------------------- //
+ // params["alias"] - Alias of a mutation. The alias is the "hidden" name of
+ // the mutation, for example "Mutation 5" or "Mutation 33"
+ // params["gene"] - The letter of the new gene
+ // params["pos"] - The BYOND index of the letter in the gene sequence to be
+ // changed. Expects a text string from TGUI and will convert to a number
+ if("pulse_gene")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
+
+ // GUARD CHECK - Have we somehow cheekily swapped occupants? This is
+ // unexpected.
+ if(!(scanner_occupant == connected_scanner.occupant))
+ return
+
+ // GUARD CHECK - Is the occupant currently undergoing some form of
+ // transformation? If so, we don't want to be pulsing genes.
+ if(scanner_occupant.transformation_timer)
+ to_chat(usr,"
")
+ return
+
+ // Resolve mutation's BYOND path from the alias
+ var/alias = params["alias"]
+ var/path = GET_MUTATION_TYPE_FROM_ALIAS(alias)
+
+ // Make sure the occupant still has this mutation
+ if(!(path in scanner_occupant.dna.mutation_index))
+ return
+
+ // Resolve BYOND path to genome sequence of scanner occupant
+ var/sequence = GET_GENE_STRING(path, scanner_occupant.dna)
+
+ var/newgene = params["gene"]
+ var/genepos = text2num(params["pos"])
+
+ // If the new gene is J, this means we're dealing with a JOKER
+ // GUARD CHECK - Is JOKER actually ready?
+ if((newgene == "J") && (jokerready < world.time))
+ var/truegenes = GET_SEQUENCE(path)
+ newgene = truegenes[genepos]
+ jokerready = world.time + JOKER_TIMEOUT - (JOKER_UPGRADE * (connected_scanner.precision_coeff-1))
+
+ // If the gene is an X, we want to update the default genes with the new
+ // X to allow highlighting logic to work on the tgui interface.
+ if(newgene == "X")
+ var/defaultseq = scanner_occupant.dna.default_mutation_genes[path]
+ defaultseq = copytext_char(defaultseq, 1, genepos) + newgene + copytext_char(defaultseq, genepos + 1)
+ scanner_occupant.dna.default_mutation_genes[path] = defaultseq
+
+ // Copy genome to scanner occupant and do some basic mutation checks as
+ // we've increased the occupant rads
+ sequence = copytext_char(sequence, 1, genepos) + newgene + copytext_char(sequence, genepos + 1)
+ scanner_occupant.dna.mutation_index[path] = sequence
+ scanner_occupant.radiation += RADIATION_STRENGTH_MULTIPLIER/connected_scanner.damage_coeff
+ scanner_occupant.domutcheck()
+
+ // GUARD CHECK - Modifying genetics can lead to edge cases where the
+ // scanner occupant is qdel'd and replaced with a different entity.
+ // Examples of this include adding/removing the Monkified mutation which
+ // qdels the previous entity and creates a brand new one in its place.
+ // We should redo all of our occupant modification checks again, although
+ // it is less than ideal.
+ if(!can_modify_occupant())
+ return
+
+ // Check if we cracked a mutation
+ check_discovery(alias)
+
+ return
+
+ // Apply a chromosome to a specific mutation.
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to apply the chromo to
+ // params["chromo"] - Name of the chromosome to apply to the mutation
+ if("apply_chromo")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
+
+ // GUARD CHECK - Have we somehow cheekily swapped occupants? This is
+ // unexpected.
+ if(!(scanner_occupant == connected_scanner.occupant))
+ return
+
+ var/bref = params["mutref"]
+
+ // GUARD CHECK - Only search occupant for this specific ref, since your
+ // can only apply chromosomes to mutations occupants.
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, SEARCH_OCCUPANT)
+
+ // GUARD CHECK - This should not be possible. Unexpected result
+ if(!HM)
+ return
+
+ // Look through our stored chromos and compare names to find a
+ // stored chromo we can apply.
+ for(var/obj/item/chromosome/CM in stored_chromosomes)
+ if(CM.can_apply(HM) && (CM.name == params["chromo"]))
+ stored_chromosomes -= CM
+ CM.apply(HM)
+
+ return
+
+ // Print any type of standard injector, limited right now to activators that
+ // activate a dormant mutation and mutators that forcibly create a new
+ // MUT_EXTRA mutation
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to create an injector of
+ // params["is_activator"] - Is this an "Activator" style injector, also
+ // referred to as a "Research" type. Expects a string with 0 or 1, which
+ // then gets converted to a number.
+ // params["source"] - The source the request came from.
+ // Expected results:
+ // "occupant" - From genetic sequencer
+ // "console" - From DNA Console storage
+ // "disk" - From inserted diskette
+ if("print_injector")
+ // Because printing mutators and activators share a bunch of code,
+ // it makes sense to keep them both together and set unique vars
+ // later in the code
+
+ // As a side note, because mutations can contain unique metadata,
+ // this system uses BYOND Atom Refs to safely and accurately
+ // identify mutations from big ol' lists
+
+ // GUARD CHECK - Is the injector actually ready?
+ if(world.time < injectorready)
+ return
+
+ var/search_flags = 0
+
+ switch(params["source"])
+ if("occupant")
+ // GUARD CHECK - Make sure we can modify the occupant before we
+ // attempt to search them for any given mutation refs. This could
+ // lead to no search flags being passed to get_mut_by_ref and this
+ // is intended functionality to prevent any cheese or abuse
+ if(can_modify_occupant())
+ search_flags |= SEARCH_OCCUPANT
+ if("console")
+ search_flags |= SEARCH_STORED
+ if("disk")
+ search_flags |= SEARCH_DISKETTE
+
+ var/bref = params["mutref"]
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, search_flags)
+
+ // GUARD CHECK - This should not be possible. Unexpected result
+ if(!HM)
+ return
+
+ // Create a new DNA Injector and add the appropriate mutations to it
+ var/obj/item/dnainjector/activator/I = new /obj/item/dnainjector/activator(loc)
+ I.add_mutations += new HM.type(copymut = HM)
+
+ var/is_activator = text2num(params["is_activator"])
+
+ // Activators are also called "research" injectors and are used to create
+ // chromosomes by recycling at the DNA Console
+ if(is_activator)
+ I.name = "[HM.name] activator"
+ I.research = TRUE
+ // If there's an operational connected scanner, we can use its upgrades
+ // to improve our injector's radiation generation
+ if(scanner_operational())
+ I.damage_coeff = connected_scanner.damage_coeff*4
+ injectorready = world.time + INJECTOR_TIMEOUT * (1 - 0.1 * connected_scanner.precision_coeff)
else
- current_screen = "mainmenu"
-
- if(connected)
- connected.locked = locked_state
- if("inspect")
- if(viable_occupant)
- var/list/mutations = get_mutation_list(TRUE)
- if(current_mutation == mutations[num])
- current_mutation = null
+ injectorready = world.time + INJECTOR_TIMEOUT
+ else
+ I.name = "[HM.name] mutator"
+ I.doitanyway = TRUE
+ // If there's an operational connected scanner, we can use its upgrades
+ // to improve our injector's radiation generation
+ if(scanner_operational())
+ I.damage_coeff = connected_scanner.damage_coeff
+ injectorready = world.time + INJECTOR_TIMEOUT * 5 * (1 - 0.1 * connected_scanner.precision_coeff)
else
- current_mutation = mutations[num]
+ injectorready = world.time + INJECTOR_TIMEOUT * 5
+
+ return
+
+ // Save a mutation to the console's storage buffer.
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to store
+ // params["source"] - The source the request came from.
+ // Expected results:
+ // "occupant" - From genetic sequencer
+ // "disk" - From inserted diskette
+ if("save_console")
+ var/search_flags = 0
+
+ switch(params["source"])
+ if("occupant")
+ // GUARD CHECK - Make sure we can modify the occupant before we
+ // attempt to search them for any given mutation refs. This could
+ // lead to no search flags being passed to get_mut_by_ref and this
+ // is intended functionality to prevent any cheese or abuse
+ if(can_modify_occupant())
+ search_flags |= SEARCH_OCCUPANT
+ if("disk")
+ search_flags |= SEARCH_DISKETTE
+
+ // GUARD CHECK - Is mutation storage full?
+ if(LAZYLEN(stored_mutations) >= max_storage)
+ to_chat(usr,"
")
+ return
+
+ var/bref = params["mutref"]
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, search_flags)
+
+ // GUARD CHECK - This should not be possible. Unexpected result
+ if(!HM)
+ return
+
+ var/datum/mutation/human/A = new HM.type()
+ A.copy_mutation(HM)
+ stored_mutations += A
+ to_chat(usr,"
")
+ return
+
+ // Save a mutation to the diskette's storage buffer.
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to store
+ // params["source"] - The source the request came from
+ // Expected results:
+ // "occupant" - From genetic sequencer
+ // "console" - From DNA Console storage
+ if("save_disk")
+ // GUARD CHECK - This code shouldn't even be callable without a diskette
+ // inserted. Unexpected result
+ if(!diskette)
+ return
+
+ // GUARD CHECK - Make sure the disk is not full
+ if(LAZYLEN(diskette.mutations) >= diskette.max_mutations)
+ to_chat(usr,"
")
+ return
+
+ // GUARD CHECK - Make sure the disk isn't set to read only, as we're
+ // attempting to write to it
+ if(diskette.read_only)
+ to_chat(usr,"
")
+ return
+
+ var/search_flags = 0
+
+ switch(params["source"])
+ if("occupant")
+ // GUARD CHECK - Make sure we can modify the occupant before we
+ // attempt to search them for any given mutation refs. This could
+ // lead to no search flags being passed to get_mut_by_ref and this
+ // is intended functionality to prevent any cheese or abuse
+ if(can_modify_occupant())
+ search_flags |= SEARCH_OCCUPANT
+ if("console")
+ search_flags |= SEARCH_STORED
+
+ var/bref = params["mutref"]
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, search_flags)
+
+ // GUARD CHECK - This should not be possible. Unexpected result
+ if(!HM)
+ return
+
+ var/datum/mutation/human/A = new HM.type()
+ A.copy_mutation(HM)
+ diskette.mutations += A
+ to_chat(usr,"
")
+ return
+
+ // Completely removes a MUT_EXTRA mutation or mutation with corrupt gene
+ // sequence from the scanner occupant
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to nullify
+ if("nullify")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
+
+ var/bref = params["mutref"]
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, SEARCH_OCCUPANT)
+
+ // GUARD CHECK - This should not be possible. Unexpected result
+ if(!HM)
+ return
+
+ // GUARD CHECK - Nullify should only be used on scrambled or "extra"
+ // mutations.
+ if(!HM.scrambled && !(HM.class == MUT_EXTRA))
+ return
+
+ scanner_occupant.dna.remove_mutation(HM.type)
+ return
+
+ // Deletes saved mutation from console buffer.
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to delete
+ if("delete_console_mut")
+ var/bref = params["mutref"]
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, SEARCH_STORED)
- if("inspectstorage")
- current_storage = num
- current_screen = "info"
- if("savemut")
- if(viable_occupant)
- var/succes
- if(LAZYLEN(stored_mutations) < max_storage)
- var/mutation = text2path(href_list["path"])
- if(ispath(mutation, /datum/mutation/human)) //sanity checks
- var/datum/mutation/human/HM = viable_occupant.dna.get_mutation(mutation)
- if(HM)
- var/datum/mutation/human/A = new HM.type()
- A.copy_mutation(HM)
- succes = TRUE
- stored_mutations += A
- to_chat(usr,"
")
- if("deletemut")
- var/datum/mutation/human/HM = stored_mutations[num]
if(HM)
stored_mutations.Remove(HM)
qdel(HM)
- current_screen = "mutations"
- if("activator")
- if(injectorready < world.time)
- var/mutation = text2path(href_list["path"])
- if(ispath(mutation, /datum/mutation/human))
- var/datum/mutation/human/HM = get_valid_mutation(mutation)
- if(HM)
- var/obj/item/dnainjector/activator/I = new /obj/item/dnainjector/activator(loc)
- I.add_mutations += new HM.type (copymut = HM)
- I.name = "[HM.name] activator"
- I.research = TRUE
- if(connected)
- I.damage_coeff = connected.damage_coeff*4
- injectorready = world.time + INJECTOR_TIMEOUT * (1 - 0.1 * connected.precision_coeff) //precision_coeff being the matter bin rating
- else
- injectorready = world.time + INJECTOR_TIMEOUT
- if("mutator")
- if(injectorready < world.time)
- var/mutation = text2path(href_list["path"])
- if(ispath(mutation, /datum/mutation/human))
- var/datum/mutation/human/HM = get_valid_mutation(mutation)
- if(HM)
- var/obj/item/dnainjector/activator/I = new /obj/item/dnainjector/activator(loc)
- I.add_mutations += new HM.type (copymut = HM)
- I.doitanyway = TRUE
- I.name = "[HM.name] injector"
- if(connected)
- I.damage_coeff = connected.damage_coeff
- injectorready = world.time + INJECTOR_TIMEOUT * 5 * (1 - 0.1 * connected.precision_coeff)
- else
- injectorready = world.time + INJECTOR_TIMEOUT * 5
- if("advinjector")
- var/selection = href_list["injector"]
- if(injectorready < world.time)
- if(injector_selection.Find(selection))
- var/list/true_selection = injector_selection[selection]
- if(LAZYLEN(injector_selection))
- var/obj/item/dnainjector/activator/I = new /obj/item/dnainjector/activator(loc)
- for(var/A in true_selection)
- var/datum/mutation/human/HM = A
- I.add_mutations += new HM.type (copymut = HM)
- I.doitanyway = TRUE
- I.name = "Advanced [selection] injector"
- if(connected)
- I.damage_coeff = connected.damage_coeff
- injectorready = world.time + INJECTOR_TIMEOUT * 8 * (1 - 0.1 * connected.precision_coeff)
- else
- injectorready = world.time + INJECTOR_TIMEOUT * 8
+ return
- if("nullify")
- if(viable_occupant)
- var/datum/mutation/human/A = viable_occupant.dna.get_mutation(current_mutation)
- if(A && (!viable_occupant.dna.mutation_in_sequence(current_mutation) || A.scrambled))
- viable_occupant.dna.remove_mutation(current_mutation)
- current_screen = "mainmenu"
- current_mutation = null
- if("pulsegene")
- if(current_screen != "info")
- var/path = GET_MUTATION_TYPE_FROM_ALIAS(href_list["alias"])
- if(viable_occupant && num && (path in viable_occupant.dna.mutation_index))
- var/list/genes = list("A","T","G","C","X")
- if(jokerready < world.time)
- genes += "JOKER"
- var/sequence = GET_GENE_STRING(path, viable_occupant.dna)
- var/original = sequence[num]
- var/new_gene = input("From [original] to-", "New block", original) as null|anything in genes
- if(!new_gene)
- new_gene = original
- if(viable_occupant == get_viable_occupant()) //No cheesing
- if((new_gene == "JOKER") && (jokerready < world.time))
- var/true_genes = GET_SEQUENCE(current_mutation)
- new_gene = true_genes[num]
- jokerready = world.time + JOKER_TIMEOUT - (JOKER_UPGRADE * (connected.precision_coeff-1))
- sequence = copytext_char(sequence, 1, num) + new_gene + copytext_char(sequence, num + 1)
- viable_occupant.dna.mutation_index[path] = sequence
- viable_occupant.radiation += RADIATION_STRENGTH_MULTIPLIER/connected.damage_coeff
- viable_occupant.domutcheck()
- if("exportdiskmut")
- if(diskette && !diskette.read_only)
- var/path = text2path(href_list["path"])
- if(ispath(path, /datum/mutation/human))
- var/datum/mutation/human/A = get_valid_mutation(path)
- if(A && diskette && (LAZYLEN(diskette.mutations) < diskette.max_mutations))
- var/datum/mutation/human/HM = new A.type()
- diskette.mutations += HM
- HM.copy_mutation(A)
- to_chat(usr, "
")
- if("deletediskmut")
- if(diskette && !diskette.read_only)
- if(num && (LAZYLEN(diskette.mutations) >= num))
- var/datum/mutation/human/A = diskette.mutations[num]
- diskette.mutations.Remove(A)
- qdel(A)
- if("importdiskmut")
- if(diskette && (LAZYLEN(diskette.mutations) >= num))
- if(LAZYLEN(stored_mutations) < max_storage)
- var/datum/mutation/human/A = diskette.mutations[num]
- var/datum/mutation/human/HM = new A.type()
- HM.copy_mutation(A)
- stored_mutations += HM
- to_chat(usr,"
")
- if("combine")
- if(num && (LAZYLEN(stored_mutations) >= num))
- if(LAZYLEN(stored_mutations) < max_storage)
- var/datum/mutation/human/A = stored_mutations[num]
- var/path = A.type
- if(combine)
- var/result_path = get_mixed_mutation(combine, path)
- if(result_path)
- stored_mutations += new result_path()
- to_chat(usr, "
")
- discover(result_path)
- combine = null
- else
- to_chat(usr, "
")
- if("ejectchromosome")
- if(LAZYLEN(stored_chromosomes) >= num)
- var/obj/item/chromosome/CM = stored_chromosomes[num]
- CM.forceMove(drop_location())
- adjust_item_drop_location(CM)
- stored_chromosomes -= CM
- if("applychromosome")
- if(viable_occupant && (LAZYLEN(viable_occupant.dna.mutations) >= num))
- var/datum/mutation/human/HM = viable_occupant.dna.mutations[num]
- var/list/chromosomes = list()
- for(var/obj/item/chromosome/CM in stored_chromosomes)
- if(CM.can_apply(HM))
- chromosomes += CM
- if(chromosomes.len)
- var/obj/item/chromosome/CM = input("Select a chromosome to apply", "Apply Chromosome") as null|anything in sortNames(chromosomes)
- if(CM)
- to_chat(usr, "
")
- stored_chromosomes -= CM
- CM.apply(HM)
- if("expand_advinjector")
- var/mutation = text2path(href_list["path"])
- var/datum/mutation/human/HM = get_valid_mutation(mutation)
- if(HM && LAZYLEN(injector_selection))
- var/which_injector = input(usr, "Select Adv. Injector", "Advanced Injectors") as null|anything in injector_selection
- if(injector_selection.Find(which_injector))
- var/list/true_selection = injector_selection[which_injector]
- var/total_instability
- for(var/B in true_selection)
- var/datum/mutation/human/mootacion = B
- total_instability += mootacion.instability
- total_instability += HM.instability
- if((total_instability > max_injector_instability) || (true_selection.len + 1) > max_injector_mutations)
- to_chat(usr, "
")
- else
- true_selection += HM //reminder that this works. because I keep forgetting this works
- if("remove_from_advinjector")
- var/mutation = text2path(href_list["path"])
- var/selection = href_list["injector"]
- if(injector_selection.Find(selection))
- var/list/true_selection = injector_selection[selection]
- for(var/B in true_selection)
- var/datum/mutation/human/HM = B
- if(HM.type == mutation)
- true_selection -= HM
- break
+ // Deletes saved mutation from disk buffer.
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to delete
+ if("delete_disk_mut")
+ // GUARD CHECK - This code shouldn't even be callable without a diskette
+ // inserted. Unexpected result
+ if(!diskette)
+ return
- if("remove_advinjector")
- var/selection = href_list["injector"]
- for(selection in injector_selection)
- if(selection == selection)
- injector_selection.Remove(selection)
+ // GUARD CHECK - Make sure the disk isn't set to read only, as we're
+ // attempting to write to it (via deletion)
+ if(diskette.read_only)
+ to_chat(usr,"
")
+ return
- if("add_advinjector")
- if(LAZYLEN(injector_selection) < max_injector_selections)
- var/new_selection = stripped_input(usr, "Enter Adv. Injector name", "Advanced Injectors")
- if(new_selection && !(new_selection in injector_selection))
- injector_selection[new_selection] = list()
+ var/bref = params["mutref"]
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, SEARCH_DISKETTE)
+ if(HM)
+ diskette.mutations.Remove(HM)
+ qdel(HM)
+ return
- ui_interact(usr,last_change)
+ // Ejects a stored chromosome from the DNA Console
+ // ---------------------------------------------------------------------- //
+ // params["chromo"] - Text string of the chromosome name
+ if("eject_chromo")
+ var/chromname = params["chromo"]
-/obj/machinery/computer/scan_consolenew/proc/scramble(input,rs,rd) //hexadecimal genetics. dont confuse with scramble button
+ for(var/obj/item/chromosome/CM in stored_chromosomes)
+ if(chromname == CM.name)
+ CM.forceMove(drop_location())
+ adjust_item_drop_location(CM)
+ stored_chromosomes -= CM
+ return
+
+ return
+
+ // Combines two mutations from the console to try and create a new mutation
+ // ---------------------------------------------------------------------- //
+ // params["firstref"] - ATOM Ref of first mutation for combination
+ // params["secondref"] - ATOM Ref of second mutation for combination
+ // mutation
+ if("combine_console")
+ // GUaRD CHECK - Make sure mutation storage isn't full. If it is, we won't
+ // be able to store the new combo mutation
+ if(LAZYLEN(stored_mutations) >= max_storage)
+ to_chat(usr,"
")
+ return
+
+ // GUARD CHECK - We're running a research-type operation. If, for some
+ // reason, somehow the DNA Console has been disconnected from the research
+ // network - Or was never in it to begin with - don't proceed
+ if(!stored_research)
+ return
+
+ var/first_bref = params["firstref"]
+ var/second_bref = params["secondref"]
+
+ // GUARD CHECK - Find the source and destination mutations on the console
+ // and make sure they actually exist.
+ var/datum/mutation/human/source_mut = get_mut_by_ref(first_bref, SEARCH_STORED | SEARCH_DISKETTE)
+ if(!source_mut)
+ return
+
+ var/datum/mutation/human/dest_mut = get_mut_by_ref(second_bref, SEARCH_STORED | SEARCH_DISKETTE)
+ if(!dest_mut)
+ return
+
+ // Attempt to mix the two mutations to get a new type
+ var/result_path = get_mixed_mutation(source_mut.type, dest_mut.type)
+
+ if(!result_path)
+ return
+
+ // If we got a new type, add it to our storage
+ stored_mutations += new result_path()
+ to_chat(usr, "
")
+
+ // If it's already discovered, end here. Otherwise, add it to the list of
+ // discovered mutations.
+ // We've already checked for stored_research earlier
+ if(result_path in stored_research.discovered_mutations)
+ return
+
+ var/datum/mutation/human/HM = GET_INITIALIZED_MUTATION(result_path)
+ stored_research.discovered_mutations += result_path
+ say("Successfully mutated [HM.name].")
+ return
+
+ // Combines two mutations from the disk to try and create a new mutation
+ // ---------------------------------------------------------------------- //
+ // params["firstref"] - ATOM Ref of first mutation for combination
+ // params["secondref"] - ATOM Ref of second mutation for combination
+ // mutation
+ if("combine_disk")
+ // GUARD CHECK - This code shouldn't even be callable without a diskette
+ // inserted. Unexpected result
+ if(!diskette)
+ return
+
+ // GUARD CHECK - Make sure the disk is not full.
+ if(LAZYLEN(diskette.mutations) >= diskette.max_mutations)
+ to_chat(usr,"
")
+ return
+
+ // GUARD CHECK - Make sure the disk isn't set to read only, as we're
+ // attempting to write to it
+ if(diskette.read_only)
+ to_chat(usr,"
")
+ return
+
+ // GUARD CHECK - We're running a research-type operation. If, for some
+ // reason, somehow the DNA Console has been disconnected from the research
+ // network - Or was never in it to begin with - don't proceed
+ if(!stored_research)
+ return
+
+ var/first_bref = params["firstref"]
+ var/second_bref = params["secondref"]
+
+ // GUARD CHECK - Find the source and destination mutations on the console
+ // and make sure they actually exist.
+ var/datum/mutation/human/source_mut = get_mut_by_ref(first_bref, SEARCH_STORED | SEARCH_DISKETTE)
+ if(!source_mut)
+ return
+
+ var/datum/mutation/human/dest_mut = get_mut_by_ref(second_bref, SEARCH_STORED | SEARCH_DISKETTE)
+ if(!dest_mut)
+ return
+
+ // Attempt to mix the two mutations to get a new type
+ var/result_path = get_mixed_mutation(source_mut.type, dest_mut.type)
+
+ if(!result_path)
+ return
+
+ // If we got a new type, add it to our storage
+ diskette.mutations += new result_path()
+ to_chat(usr, "
")
+
+ // If it's already discovered, end here. Otherwise, add it to the list of
+ // discovered mutations
+ // We've already checked for stored_research earlier
+ if(result_path in stored_research.discovered_mutations)
+ return
+
+ var/datum/mutation/human/HM = GET_INITIALIZED_MUTATION(result_path)
+ stored_research.discovered_mutations += result_path
+ say("Successfully mutated [HM.name].")
+ return
+
+ // Sets the Genetic Makeup pulse strength.
+ // ---------------------------------------------------------------------- //
+ // params["val"] - New strength value as text string, converted to number
+ // later on in code
+ if("set_pulse_strength")
+ var/value = round(text2num(params["val"]))
+ radstrength = WRAP(value, 1, RADIATION_STRENGTH_MAX+1)
+ return
+
+ // Sets the Genetic Makeup pulse duration
+ // ---------------------------------------------------------------------- //
+ // params["val"] - New strength value as text string, converted to number
+ // later on in code
+ if("set_pulse_duration")
+ var/value = round(text2num(params["val"]))
+ radduration = WRAP(value, 1, RADIATION_DURATION_MAX+1)
+ return
+
+ // Saves Genetic Makeup information to disk
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the console genetic makeup buffer to
+ // copy to disk
+ if("save_makeup_disk")
+ // GUARD CHECK - This code shouldn't even be callable without a diskette
+ // inserted. Unexpected result
+ if(!diskette)
+ return
+
+ // GUARD CHECK - Make sure the disk isn't set to read only, as we're
+ // attempting to write to it
+ if(diskette.read_only)
+ to_chat(usr,"
")
+ return
+
+ // Convert the index to a number and clamp within the array range
+ var/buffer_index = text2num(params["index"])
+ buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
+
+ var/list/buffer_slot = genetic_makeup_buffer[buffer_index]
+
+ // GUARD CHECK - This should not be possible to activate on a buffer slot
+ // that doesn't have any genetic data. Unexpected result
+ if(!istype(buffer_slot))
+ return
+
+ diskette.genetic_makeup_buffer = buffer_slot.Copy()
+ return
+
+ // Loads Genetic Makeup from disk to a console buffer
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the console genetic makeup buffer to
+ // copy to. Expected as text string, converted to number later
+ if("load_makeup_disk")
+ // GUARD CHECK - This code shouldn't even be callable without a diskette
+ // inserted. Unexpected result
+ if(!diskette)
+ return
+
+ // GUARD CHECK - This should not be possible to activate on a diskette
+ // that doesn't have any genetic data. Unexpected result
+ if(LAZYLEN(diskette.genetic_makeup_buffer) == 0)
+ return
+
+ // Convert the index to a number and clamp within the array range, then
+ // copy the data from the disk to that buffer
+ var/buffer_index = text2num(params["index"])
+ buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
+ genetic_makeup_buffer[buffer_index] = diskette.genetic_makeup_buffer.Copy()
+ return
+
+ // Deletes genetic makeup buffer from the inserted diskette
+ if("del_makeup_disk")
+ // GUARD CHECK - This code shouldn't even be callable without a diskette
+ // inserted. Unexpected result
+ if(!diskette)
+ return
+
+ // GUARD CHECK - Make sure the disk isn't set to read only, as we're
+ // attempting to write (via deletion) to it
+ if(diskette.read_only)
+ to_chat(usr,"
")
+ return
+
+ diskette.genetic_makeup_buffer.Cut()
+ return
+
+ // Saves the scanner occupant's genetic makeup to a given console buffer
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the console genetic makeup buffer to
+ // save the new genetic data to. Expected as text string, converted to
+ // number later
+ if("save_makeup_console")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
+
+ // Convert the index to a number and clamp within the array range, then
+ // copy the data from the disk to that buffer
+ var/buffer_index = text2num(params["index"])
+ buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
+
+ // Set the new information
+ genetic_makeup_buffer[buffer_index] = list(
+ "label"="Slot [buffer_index]:[scanner_occupant.real_name]",
+ "UI"=scanner_occupant.dna.uni_identity,
+ "UE"=scanner_occupant.dna.unique_enzymes,
+ "name"=scanner_occupant.real_name,
+ "blood_type"=scanner_occupant.dna.blood_type)
+
+ return
+
+ // Deleted genetic makeup data from a console buffer slot
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the console genetic makeup buffer to
+ // delete the genetic data from. Expected as text string, converted to
+ // number later
+ if("del_makeup_console")
+ // Convert the index to a number and clamp within the array range, then
+ // copy the data from the disk to that buffer
+ var/buffer_index = text2num(params["index"])
+ buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
+ var/list/buffer_slot = genetic_makeup_buffer[buffer_index]
+
+ // GUARD CHECK - This shouldn't be possible to execute this on a null
+ // buffer. Unexpected resut
+ if(!istype(buffer_slot))
+ return
+
+ genetic_makeup_buffer[buffer_index] = null
+ return
+
+ // Eject stored diskette from console
+ if("eject_disk")
+ // GUARD CHECK - This code shouldn't even be callable without a diskette
+ // inserted. Unexpected result
+ if(!diskette)
+ return
+
+ diskette.forceMove(drop_location())
+ diskette = null
+ return
+
+ // Create a Genetic Makeup injector. These injectors are timed and thus are
+ // only temporary
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the console genetic makeup buffer to
+ // create the makeup injector from. Expected as text string, converted to
+ // number later
+ // params["type"] - Type of injector to create
+ // Expected results:
+ // "ue" - Unique Enzyme, changes name and blood type
+ // "ui" - Unique Identity, changes looks
+ // "mixed" - Combination of both ue and ui
+ if("makeup_injector")
+ // Convert the index to a number and clamp within the array range, then
+ // copy the data from the disk to that buffer
+ var/buffer_index = text2num(params["index"])
+ buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
+ var/list/buffer_slot = genetic_makeup_buffer[buffer_index]
+
+ // GUARD CHECK - This shouldn't be possible to execute this on a null
+ // buffer. Unexpected resut
+ if(!istype(buffer_slot))
+ return
+
+ var/type = params["type"]
+ var/obj/item/dnainjector/timed/I
+
+ switch(type)
+ if("ui")
+ // GUARD CHECK - There's currently no way to save partial genetic data.
+ // However, if this is the case, we can't make a complete injector and
+ // this catches that edge case
+ if(!buffer_slot["UI"])
+ to_chat(usr,"
")
+ return
+
+ I = new /obj/item/dnainjector/timed(loc)
+ I.fields = list("UI"=buffer_slot["UI"])
+
+ // If there is a connected scanner, we can use its upgrades to reduce
+ // the radiation generated by this injector
+ if(scanner_operational())
+ I.damage_coeff = connected_scanner.damage_coeff
+ if("ue")
+ // GUARD CHECK - There's currently no way to save partial genetic data.
+ // However, if this is the case, we can't make a complete injector and
+ // this catches that edge case
+ if(!buffer_slot["name"] || !buffer_slot["UE"] || !buffer_slot["blood_type"])
+ to_chat(usr,"
")
+ return
+
+ I = new /obj/item/dnainjector/timed(loc)
+ I.fields = list("name"=buffer_slot["name"], "UE"=buffer_slot["UE"], "blood_type"=buffer_slot["blood_type"])
+
+ // If there is a connected scanner, we can use its upgrades to reduce
+ // the radiation generated by this injector
+ if(scanner_operational())
+ I.damage_coeff = connected_scanner.damage_coeff
+ if("mixed")
+ // GUARD CHECK - There's currently no way to save partial genetic data.
+ // However, if this is the case, we can't make a complete injector and
+ // this catches that edge case
+ if(!buffer_slot["UI"] || !buffer_slot["name"] || !buffer_slot["UE"] || !buffer_slot["blood_type"])
+ to_chat(usr,"
")
+ return
+
+ I = new /obj/item/dnainjector/timed(loc)
+ I.fields = list("UI"=buffer_slot["UI"],"name"=buffer_slot["name"], "UE"=buffer_slot["UE"], "blood_type"=buffer_slot["blood_type"])
+
+ // If there is a connected scanner, we can use its upgrades to reduce
+ // the radiation generated by this injector
+ if(scanner_operational())
+ I.damage_coeff = connected_scanner.damage_coeff
+
+ // If we successfully created an injector, don't forget to set the new
+ // ready timer.
+ if(I)
+ injectorready = world.time + INJECTOR_TIMEOUT
+
+ return
+
+ // Applies a genetic makeup buffer to the scanner occupant
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the console genetic makeup buffer to
+ // apply to the scanner occupant. Expected as text string, converted to
+ // number later
+ // params["type"] - Type of genetic makeup copy to implement
+ // Expected results:
+ // "ue" - Unique Enzyme, changes name and blood type
+ // "ui" - Unique Identity, changes looks
+ // "mixed" - Combination of both ue and ui
+ if("makeup_apply")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
+
+ // Convert the index to a number and clamp within the array range, then
+ // copy the data from the disk to that buffer
+ var/buffer_index = text2num(params["index"])
+ buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
+ var/list/buffer_slot = genetic_makeup_buffer[buffer_index]
+
+ // GUARD CHECK - This shouldn't be possible to execute this on a null
+ // buffer. Unexpected resut
+ if(!istype(buffer_slot))
+ return
+
+ var/type = params["type"]
+
+ apply_genetic_makeup(type, buffer_slot)
+ return
+
+ // Applies a genetic makeup buffer to the next scanner occupant. This sets
+ // some code that will run when the connected DNA Scanner door is next
+ // closed
+ // This allows people to self-modify their genetic makeup, as tgui
+ // interfaces can not be accessed while inside the DNA Scanner and genetic
+ // makeup injectors are only temporary
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the console genetic makeup buffer to
+ // apply to the scanner occupant. Expected as text string, converted to
+ // number later
+ // params["type"] - Type of genetic makeup copy to implement
+ // Expected results:
+ // "ue" - Unique Enzyme, changes name and blood type
+ // "ui" - Unique Identity, changes looks
+ // "mixed" - Combination of both ue and ui
+ if("makeup_delay")
+ // Convert the index to a number and clamp within the array range, then
+ // copy the data from the disk to that buffer
+ var/buffer_index = text2num(params["index"])
+ buffer_index = clamp(buffer_index, 1, NUMBER_OF_BUFFERS)
+ var/list/buffer_slot = genetic_makeup_buffer[buffer_index]
+
+ // GUARD CHECK - This shouldn't be possible to execute this on a null
+ // buffer. Unexpected resut
+ if(!istype(buffer_slot))
+ return
+
+ var/type = params["type"]
+
+ // Set the delayed action. The next time the scanner door is closed,
+ // unless this is cancelled in the UI, the action will happen
+ delayed_action = list("type" = type, "buffer_slot" = buffer_slot)
+ return
+
+ // Attempts to modify the indexed element of the Unique Identity string
+ // This is a time delayed action that is handled in process()
+ // ---------------------------------------------------------------------- //
+ // params["index"] - The BYOND index of the Unique Identity string to
+ // attempt to modify
+ if("makeup_pulse")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ if(!can_modify_occupant())
+ return
+
+ // Set the appropriate timer and index to pulse. This is then managed
+ // later on in process()
+ var/len = length_char(scanner_occupant.dna.uni_identity)
+ rad_pulse_timer = world.time + (radduration*10)
+ rad_pulse_index = WRAP(text2num(params["index"]), 1, len+1)
+ begin_processing()
+ return
+
+ // Cancels the delayed action - In this context it is not the radiation
+ // pulse from "makeup_pulse", which can not be cancelled. It is instead
+ // the delayed genetic transfer from "makeup_delay"
+ if("cancel_delay")
+ delayed_action = null
+ return
+
+ // Creates a new advanced injector storage buffer in the console
+ // ---------------------------------------------------------------------- //
+ // params["name"] - The name to apply to the new injector
+ if("new_adv_inj")
+ // GUARD CHECK - Make sure we can make a new injector. This code should
+ // not be called if we're already maxed out and this is an Unexpected
+ // result
+ if(!(LAZYLEN(injector_selection) < max_injector_selections))
+ return
+
+ // GUARD CHECK - Sanitise and trim the proposed name. This prevents HTML
+ // injection and equivalent as tgui input is not stripped
+ var/inj_name = params["name"]
+ inj_name = trim(sanitize(inj_name))
+
+ // GUARD CHECK - If the name is null or blank, or the name is already in
+ // the list of advanced injectors, we want to reject it as we can't have
+ // duplicate named advanced injectors
+ if(!inj_name || (inj_name in injector_selection))
+ return
+
+ injector_selection[inj_name] = list()
+ return
+
+ // Deleted an advanced injector storage buffer from the console
+ // ---------------------------------------------------------------------- //
+ // params["name"] - The name of the injector to delete
+ if("del_adv_inj")
+ var/inj_name = params["name"]
+
+ // GUARD CHECK - If the name is null or blank, reject.
+ // GUARD CHECK - If the name isn't in the list of advanced injectors, we
+ // want to reject this as it shouldn't be possible ever do this.
+ // Unexpected result
+ if(!inj_name || !(inj_name in injector_selection))
+ return
+
+ injector_selection.Remove(inj_name)
+ return
+
+ // Creates an injector from an advanced injector buffer
+ // ---------------------------------------------------------------------- //
+ // params["name"] - The name of the injector to print
+ if("print_adv_inj")
+ // As a side note, because mutations can contain unique metadata,
+ // this system uses BYOND Atom Refs to safely and accurately
+ // identify mutations from big ol' lists.
+
+ // GUARD CHECK - Is the injector actually ready?
+ if(world.time < injectorready)
+ return
+
+ var/inj_name = params["name"]
+
+ // GUARD CHECK - If the name is null or blank, reject.
+ // GUARD CHECK - If the name isn't in the list of advanced injectors, we
+ // want to reject this as it shouldn't be possible ever do this.
+ // Unexpected result
+ if(!inj_name || !(inj_name in injector_selection))
+ return
+
+ var/list/injector = injector_selection[inj_name]
+ var/obj/item/dnainjector/activator/I = new /obj/item/dnainjector/activator(loc)
+
+ // Run through each mutation in our Advanced Injector and add them to a
+ // new injector
+ for(var/A in injector)
+ var/datum/mutation/human/HM = A
+ I.add_mutations += new HM.type(copymut=HM)
+
+ // Force apply any mutations, this is functionality similar to mutators
+ I.doitanyway = TRUE
+ I.name = "Advanced [inj_name] injector"
+
+ // If there's an operational connected scanner, we can use its upgrades
+ // to improve our injector's radiation generation
+ if(scanner_operational())
+ I.damage_coeff = connected_scanner.damage_coeff
+ injectorready = world.time + INJECTOR_TIMEOUT * 8 * (1 - 0.1 * connected_scanner.precision_coeff)
+ else
+ injectorready = world.time + INJECTOR_TIMEOUT * 8
+
+ return
+
+ // Adds a mutation to an advanced injector
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to add to the injector
+ // params["advinj"] - Name of the advanced injector to add the mutation to
+ if("add_advinj_mut")
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ // This is needed because this operation can only be completed from the
+ // genetic sequencer.
+ if(!can_modify_occupant())
+ return
+
+ var/adv_inj = params["advinj"]
+
+ // GUARD CHECK - Make sure our advanced injector actually exists. This
+ // should not be possible. Unexpected result
+ if(!(adv_inj in injector_selection))
+ return
+
+ // GUARD CHECK - Make sure we limit the number of mutations appropriately
+ if(LAZYLEN(injector_selection[adv_inj]) >= max_injector_mutations)
+ to_chat(usr,"
")
+ return
+
+ var/mut_source = params["source"]
+ var/search_flag = 0
+
+ switch(mut_source)
+ if("disk")
+ search_flag = SEARCH_DISKETTE
+ if("occupant")
+ search_flag = SEARCH_OCCUPANT
+ if("console")
+ search_flag = SEARCH_STORED
+
+ if(!search_flag)
+ return
+
+ var/bref = params["mutref"]
+ // We've already made sure we can modify the occupant, so this is safe to
+ // call
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, search_flag)
+
+ // GUARD CHECK - This should not be possible. Unexpected result
+ if(!HM)
+ return
+
+ // We want to make sure we stick within the instability limit.
+ // We start with the instability of the mutation we're intending to add.
+ var/instability_total = HM.instability
+
+ // We then add the instabilities of all other mutations in the injector,
+ // remembering to apply the Stabilizer chromosome modifiers
+ for(var/datum/mutation/human/I in injector_selection[adv_inj])
+ instability_total += I.instability * GET_MUTATION_STABILIZER(I)
+
+ // If this would take us over the max instability, we inform the user.
+ if(instability_total > max_injector_instability)
+ to_chat(usr,"
")
+ return
+
+ // If we've got here, all our checks are passed and we can successfully
+ // add the mutation to the advanced injector.
+ var/datum/mutation/human/A = new HM.type()
+ A.copy_mutation(HM)
+ injector_selection[adv_inj] += A
+ to_chat(usr,"
")
+ return
+
+ // Deletes a mutation from an advanced injector
+ // ---------------------------------------------------------------------- //
+ // params["mutref"] - ATOM Ref of specific mutation to del from the injector
+ if("delete_injector_mut")
+ var/bref = params["mutref"]
+
+ var/datum/mutation/human/HM = get_mut_by_ref(bref, SEARCH_ADV_INJ)
+
+ // GUARD CHECK - This should not be possible. Unexpected result
+ if(!HM)
+ return
+
+ // Check Advanced Injectors to find and remove the mutation
+ for(var/I in injector_selection)
+ if(injector_selection["[I]"].Remove(HM))
+ qdel(HM)
+ return
+
+ return
+
+ // Sets a new tgui view state
+ // ---------------------------------------------------------------------- //
+ // params["id"] - Key for the state to set
+ // params[...] - Every other element is used to set state variables
+ if("set_view")
+ for (var/key in params)
+ if(key == "src")
+ continue
+ tgui_view_state[key] = params[key]
+ return TRUE
+ return FALSE
+
+/**
+ * Applies the enzyme buffer to the current scanner occupant
+ *
+ * Applies the type of a specific genetic makeup buffer to the current scanner
+ * occupant
+ *
+ * Arguments:
+ * * type - "ui"/"ue"/"mixed" - Which part of the enzyme buffer to apply
+ * * buffer_slot - Index of the enzyme buffer to apply
+ */
+/obj/machinery/computer/scan_consolenew/proc/apply_genetic_makeup(type, buffer_slot)
+ // Note - This proc is only called from code that has already performed the
+ // necessary occupant guard checks. If you call this code yourself, please
+ // apply can_modify_occupant() or equivalent checks first.
+
+ // Pre-calc the rad increase since we'll be using it in all the possible
+ // operations
+ var/rad_increase = rand(100/(connected_scanner.damage_coeff ** 2),250/(connected_scanner.damage_coeff ** 2))
+
+ switch(type)
+ if("ui")
+ // GUARD CHECK - There's currently no way to save partial genetic data.
+ // However, if this is the case, we can't make a complete injector and
+ // this catches that edge case
+ if(!buffer_slot["UI"])
+ to_chat(usr,"
")
+ return FALSE
+ scanner_occupant.dna.uni_identity = buffer_slot["UI"]
+ scanner_occupant.updateappearance(mutations_overlay_update=1)
+ scanner_occupant.radiation += rad_increase
+ scanner_occupant.domutcheck()
+ return TRUE
+ if("ue")
+ // GUARD CHECK - There's currently no way to save partial genetic data.
+ // However, if this is the case, we can't make a complete injector and
+ // this catches that edge case
+ if(!buffer_slot["name"] || !buffer_slot["UE"] || !buffer_slot["blood_type"])
+ to_chat(usr,"
")
+ return FALSE
+ scanner_occupant.real_name = buffer_slot["name"]
+ scanner_occupant.name = buffer_slot["name"]
+ scanner_occupant.dna.unique_enzymes = buffer_slot["UE"]
+ scanner_occupant.dna.blood_type = buffer_slot["blood_type"]
+ scanner_occupant.radiation += rad_increase
+ scanner_occupant.domutcheck()
+ return TRUE
+ if("mixed")
+ // GUARD CHECK - There's currently no way to save partial genetic data.
+ // However, if this is the case, we can't make a complete injector and
+ // this catches that edge case
+ if(!buffer_slot["UI"] || !buffer_slot["name"] || !buffer_slot["UE"] || !buffer_slot["blood_type"])
+ to_chat(usr,"
")
+ return FALSE
+ scanner_occupant.dna.uni_identity = buffer_slot["UI"]
+ scanner_occupant.updateappearance(mutations_overlay_update=1)
+ scanner_occupant.real_name = buffer_slot["name"]
+ scanner_occupant.name = buffer_slot["name"]
+ scanner_occupant.dna.unique_enzymes = buffer_slot["UE"]
+ scanner_occupant.dna.blood_type = buffer_slot["blood_type"]
+ scanner_occupant.radiation += rad_increase
+ scanner_occupant.domutcheck()
+ return TRUE
+
+ return FALSE
+/**
+ * Checks if there is a connected DNA Scanner that is operational
+ */
+/obj/machinery/computer/scan_consolenew/proc/scanner_operational()
+ if(!connected_scanner)
+ return FALSE
+
+ return (connected_scanner && connected_scanner.is_operational())
+
+/**
+ * Checks if there is a valid DNA Scanner occupant for genetic modification
+ *
+ * Checks if there is a valid subject in the DNA Scanner that can be genetically
+ * modified. Will set the scanner occupant var as part of this check.
+ * Requires that the scanner can be operated and will return early if it can't
+ */
+/obj/machinery/computer/scan_consolenew/proc/can_modify_occupant()
+ // GUARD CHECK - We always want to perform the scanner operational check as
+ // part of checking if we can modify the occupant.
+ // We can never modify the occupant of a broken scanner.
+ if(!scanner_operational())
+ return FALSE
+
+ if(!connected_scanner.occupant)
+ return FALSE
+
+ scanner_occupant = connected_scanner.occupant
+
+ // Check validity of occupent for DNA Modification
+ // DNA Modification:
+ // requires DNA
+ // this DNA can not be bad
+ // is done via radiation bursts, so radiation immune carbons are not viable
+ // And the DNA Scanner itself must have a valid scan level
+ if(scanner_occupant.has_dna() && !HAS_TRAIT(scanner_occupant, TRAIT_RADIMMUNE) && !HAS_TRAIT(scanner_occupant, TRAIT_BADDNA) || (connected_scanner.scan_level == 3))
+ return TRUE
+
+ return FALSE
+
+/**
+ * Checks for adjacent DNA scanners and connects when it finds a viable one
+ *
+ * Seearches cardinal directions in order. Stops when it finds a viable DNA Scanner.
+ * Will connect to a broken scanner if no functional scanner is available.
+ * Links itself to the DNA Scanner to receive door open and close events.
+ */
+/obj/machinery/computer/scan_consolenew/proc/connect_to_scanner()
+ var/obj/machinery/dna_scannernew/test_scanner = null
+ var/obj/machinery/dna_scannernew/broken_scanner = null
+
+ // Look in each cardinal direction and try and find a DNA Scanner
+ // If you find a DNA Scanner, check to see if it broken or working
+ // If it's working, set the current scanner and return early
+ // If it's not working, remember it anyway as a broken scanner
+ for(var/direction in GLOB.cardinals)
+ test_scanner = locate(/obj/machinery/dna_scannernew, get_step(src, direction))
+ if(!isnull(test_scanner))
+ if(test_scanner.is_operational())
+ connected_scanner = test_scanner
+ connected_scanner.linked_console = src
+ return
+ else
+ broken_scanner = test_scanner
+
+ // Ultimately, if we have a broken scanner, we'll attempt to connect to it as
+ // a fallback case, but the code above will prefer a working scanner
+ if(!isnull(broken_scanner))
+ connected_scanner = broken_scanner
+ connected_scanner.linked_console = src
+
+/**
+ * Called by connected DNA Scanners when their doors close.
+ *
+ * Sets the new scanner occupant and completes delayed enzyme transfer if one
+ * is queued.
+ */
+/obj/machinery/computer/scan_consolenew/proc/on_scanner_close()
+ // Set the appropriate occupant now the scanner is closed
+ if(connected_scanner.occupant)
+ scanner_occupant = connected_scanner.occupant
+ else
+ scanner_occupant = null
+
+ // If we have a delayed action - In this case the only delayed action is
+ // applying a genetic makeup buffer the next time the DNA Scanner is closed -
+ // we want to perform it.
+ // GUARD CHECK - Make sure we can modify the occupant, apply_genetic_makeup()
+ // assumes we've already done this.
+ if(delayed_action && can_modify_occupant())
+ var/type = delayed_action["type"]
+ var/buffer_slot = delayed_action["buffer_slot"]
+ if(apply_genetic_makeup(type, buffer_slot))
+ to_chat(connected_scanner.occupant, "
")
+ delayed_action = null
+
+/**
+ * Called by connected DNA Scanners when their doors open.
+ *
+ * Clears enzyme pulse operations, stops processing and nulls the current
+ * scanner occupant var.
+ */
+/obj/machinery/computer/scan_consolenew/proc/on_scanner_open()
+ // If we had a radiation pulse action ongoing, we want to stop this.
+ // Imagine it being like a microwave stopping when you open the door.
+ rad_pulse_index = 0
+ rad_pulse_timer = 0
+ end_processing()
+ scanner_occupant = null
+
+/**
+ * Builds the genetic makeup list which will be sent to tgui interface.
+ */
+/obj/machinery/computer/scan_consolenew/proc/build_genetic_makeup_list()
+ // No code will ever null this list, we can safely Cut it.
+ tgui_genetic_makeup.Cut()
+
+ for(var/i=1, i <= NUMBER_OF_BUFFERS, i++)
+ if(genetic_makeup_buffer[i])
+ tgui_genetic_makeup["[i]"] = genetic_makeup_buffer[i].Copy()
+ else
+ tgui_genetic_makeup["[i]"] = null
+
+/**
+ * Builds the genetic makeup list which will be sent to tgui interface.
+ *
+ * Will iterate over the connected scanner occupant, DNA Console, inserted
+ * diskette and chromosomes and any advanced injectors, building the main data
+ * structures which get passed to the tgui interface.
+ */
+/obj/machinery/computer/scan_consolenew/proc/build_mutation_list(can_modify_occ)
+ // No code will ever null these lists. We can safely Cut them.
+ tgui_occupant_mutations.Cut()
+ tgui_diskette_mutations.Cut()
+ tgui_console_mutations.Cut()
+ tgui_console_chromosomes.Cut()
+ tgui_advinjector_mutations.Cut()
+
+ // ------------------------------------------------------------------------ //
+ // GUARD CHECK - Can we genetically modify the occupant? This check will have
+ // previously included checks to make sure the DNA Scanner is still
+ // operational
+ if(can_modify_occ)
+ // ---------------------------------------------------------------------- //
+ // Start cataloguing all mutations that the occupant has by default
+ for(var/mutation_type in scanner_occupant.dna.mutation_index)
+ var/datum/mutation/human/HM = GET_INITIALIZED_MUTATION(mutation_type)
+
+ var/list/mutation_data = list()
+ var/text_sequence = scanner_occupant.dna.mutation_index[mutation_type]
+ var/default_sequence = scanner_occupant.dna.default_mutation_genes[mutation_type]
+ var/discovered = (stored_research && (mutation_type in stored_research.discovered_mutations))
+
+ mutation_data["Alias"] = HM.alias
+ mutation_data["Sequence"] = text_sequence
+ mutation_data["DefaultSeq"] = default_sequence
+ mutation_data["Discovered"] = discovered
+ mutation_data["Source"] = "occupant"
+
+ // We only want to pass this information along to the tgui interface if
+ // the mutation has been discovered. Prevents people being able to cheese
+ // or "hack" their way to figuring out what undiscovered mutations are
+ if(discovered)
+ mutation_data["Name"] = HM.name
+ mutation_data["Description"] = HM.desc
+ mutation_data["Instability"] = HM.instability * GET_MUTATION_STABILIZER(HM)
+ mutation_data["Quality"] = HM.quality
+
+ // Assume the mutation is normal unless assigned otherwise.
+ var/mut_class = MUT_NORMAL
+
+ // Check if the mutation is currently activated. If it is, we can add even
+ // MORE information to send to tgui.
+ var/datum/mutation/human/A = scanner_occupant.dna.get_mutation(mutation_type)
+ if(A)
+ mutation_data["Active"] = TRUE
+ mutation_data["Scrambled"] = A.scrambled
+ mutation_data["Class"] = A.class
+ mut_class = A.class
+ mutation_data["CanChromo"] = A.can_chromosome
+ mutation_data["ByondRef"] = REF(A)
+ mutation_data["Type"] = A.type
+ if(A.can_chromosome)
+ mutation_data["ValidChromos"] = jointext(A.valid_chrom_list, ", ")
+ mutation_data["AppliedChromo"] = A.chromosome_name
+ mutation_data["ValidStoredChromos"] = build_chrom_list(A)
+ else
+ mutation_data["Active"] = FALSE
+ mutation_data["Scrambled"] = FALSE
+ mutation_data["Class"] = MUT_NORMAL
+
+ // Technically NONE of these mutations should be MUT_EXTRA but this will
+ // catch any weird edge cases
+ // Assign icons by priority - MUT_EXTRA will ALSO be discovered, so it
+ // has a higher priority for icon/image assignment
+ if (mut_class == MUT_EXTRA)
+ mutation_data["Image"] = "dna_extra.gif"
+ else if(discovered)
+ mutation_data["Image"] = "dna_discovered.gif"
+ else
+ mutation_data["Image"] = "dna_undiscovered.gif"
+
+ tgui_occupant_mutations += list(mutation_data)
+
+ // ---------------------------------------------------------------------- //
+ // Now get additional/"extra" mutations that they shouldn't have by default
+ for(var/datum/mutation/human/HM in scanner_occupant.dna.mutations)
+ // If it's in the mutation index array, we've already catalogued this
+ // mutation and can safely skip over it. It really shouldn't be, but this
+ // will catch any weird edge cases
+ if(HM.type in scanner_occupant.dna.mutation_index)
+ continue
+
+ var/list/mutation_data = list()
+ var/text_sequence = GET_SEQUENCE(HM.type)
+
+ // These will all be active mutations. They're added by injector and their
+ // sequencing code can't be changed. They can only be nullified, which
+ // completely removes them.
+ var/datum/mutation/human/A = GET_INITIALIZED_MUTATION(HM.type)
+
+ mutation_data["Alias"] = A.alias
+ mutation_data["Sequence"] = text_sequence
+ mutation_data["Discovered"] = TRUE
+ mutation_data["Quality"] = HM.quality
+ mutation_data["Source"] = "occupant"
+
+ mutation_data["Name"] = HM.name
+ mutation_data["Description"] = HM.desc
+ mutation_data["Instability"] = HM.instability * GET_MUTATION_STABILIZER(HM)
+
+ mutation_data["Active"] = TRUE
+ mutation_data["Scrambled"] = HM.scrambled
+ mutation_data["Class"] = HM.class
+ mutation_data["CanChromo"] = HM.can_chromosome
+ mutation_data["ByondRef"] = REF(HM)
+ mutation_data["Type"] = HM.type
+
+ if(HM.can_chromosome)
+ mutation_data["ValidChromos"] = jointext(HM.valid_chrom_list, ", ")
+ mutation_data["AppliedChromo"] = HM.chromosome_name
+ mutation_data["ValidStoredChromos"] = build_chrom_list(HM)
+
+ // Nothing in this list should be undiscovered. Technically nothing
+ // should be anything but EXTRA. But we're just handling some edge cases.
+ if (HM.class == MUT_EXTRA)
+ mutation_data["Image"] = "dna_extra.gif"
+ else
+ mutation_data["Image"] = "dna_discovered.gif"
+
+ tgui_occupant_mutations += list(mutation_data)
+
+ // ------------------------------------------------------------------------ //
+ // Build the list of mutations stored within the DNA Console
+ for(var/datum/mutation/human/HM in stored_mutations)
+ var/list/mutation_data = list()
+
+ var/datum/mutation/human/A = GET_INITIALIZED_MUTATION(HM.type)
+
+ mutation_data["Alias"] = A.alias
+ mutation_data["Name"] = HM.name
+ mutation_data["Source"] = "console"
+ mutation_data["Active"] = TRUE
+ mutation_data["Description"] = HM.desc
+ mutation_data["Instability"] = HM.instability * GET_MUTATION_STABILIZER(HM)
+ mutation_data["ByondRef"] = REF(HM)
+ mutation_data["Type"] = HM.type
+
+ mutation_data["CanChromo"] = HM.can_chromosome
+ if(HM.can_chromosome)
+ mutation_data["ValidChromos"] = jointext(HM.valid_chrom_list, ", ")
+ mutation_data["AppliedChromo"] = HM.chromosome_name
+ mutation_data["ValidStoredChromos"] = build_chrom_list(HM)
+
+ tgui_console_mutations += list(mutation_data)
+
+ // ------------------------------------------------------------------------ //
+ // Build the list of chromosomes stored within the DNA Console
+ var/chrom_index = 1
+ for(var/obj/item/chromosome/CM in stored_chromosomes)
+ var/list/chromo_data = list()
+
+ chromo_data["Name"] = CM.name
+ chromo_data["Description"] = CM.desc
+ chromo_data["Index"] = chrom_index
+
+ tgui_console_chromosomes += list(chromo_data)
+ ++chrom_index
+
+ // ------------------------------------------------------------------------ //
+ // Build the list of mutations stored on any inserted diskettes
+ if(diskette)
+ for(var/datum/mutation/human/HM in diskette.mutations)
+ var/list/mutation_data = list()
+
+ var/datum/mutation/human/A = GET_INITIALIZED_MUTATION(HM.type)
+
+ mutation_data["Alias"] = A.alias
+ mutation_data["Name"] = HM.name
+ mutation_data["Active"] = TRUE
+ //mutation_data["Sequence"] = GET_SEQUENCE(HM.type)
+ mutation_data["Source"] = "disk"
+ mutation_data["Description"] = HM.desc
+ mutation_data["Instability"] = HM.instability * GET_MUTATION_STABILIZER(HM)
+ mutation_data["ByondRef"] = REF(HM)
+ mutation_data["Type"] = HM.type
+
+ mutation_data["CanChromo"] = HM.can_chromosome
+ if(HM.can_chromosome)
+ mutation_data["ValidChromos"] = jointext(HM.valid_chrom_list, ", ")
+ mutation_data["AppliedChromo"] = HM.chromosome_name
+ mutation_data["ValidStoredChromos"] = build_chrom_list(HM)
+
+ tgui_diskette_mutations += list(mutation_data)
+
+ // ------------------------------------------------------------------------ //
+ // Build the list of mutations stored within any Advanced Injectors
+ if(LAZYLEN(injector_selection))
+ for(var/I in injector_selection)
+ var/list/mutations = list()
+ for(var/datum/mutation/human/HM in injector_selection[I])
+ var/list/mutation_data = list()
+
+ var/datum/mutation/human/A = GET_INITIALIZED_MUTATION(HM.type)
+
+ mutation_data["Alias"] = A.alias
+ mutation_data["Name"] = HM.name
+ mutation_data["Active"] = TRUE
+ //mutation_data["Sequence"] = GET_SEQUENCE(HM.type)
+ mutation_data["Source"] = "injector"
+ mutation_data["Description"] = HM.desc
+ mutation_data["Instability"] = HM.instability * GET_MUTATION_STABILIZER(HM)
+ mutation_data["ByondRef"] = REF(HM)
+ mutation_data["Type"] = HM.type
+
+ if(HM.can_chromosome)
+ mutation_data["AppliedChromo"] = HM.chromosome_name
+
+ mutations += list(mutation_data)
+ tgui_advinjector_mutations += list(list(
+ "name" = "[I]",
+ "mutations" = mutations,
+ ))
+
+/**
+ * Takes any given chromosome and calculates chromosome compatibility
+ *
+ * Will iterate over the stored chromosomes in the DNA Console and will check
+ * whether it can be applied to the supplied mutation. Then returns a list of
+ * names of chromosomes that were compatible.
+ *
+ * Arguments:
+ * * mutation - The mutation to check chromosome compatibility with
+ */
+/obj/machinery/computer/scan_consolenew/proc/build_chrom_list(mutation)
+ var/list/chromosomes = list()
+
+ for(var/obj/item/chromosome/CM in stored_chromosomes)
+ if(CM.can_apply(mutation))
+ chromosomes += CM.name
+
+ return chromosomes
+
+/**
+ * Checks whether a mutation alias has been discovered
+ *
+ * Checks whether a given mutation's genetic sequence has been completed and
+ * discovers it if appropriate
+ *
+ * Arguments:
+ * * alias - Alias of the mutation to check (ie "Mutation 51" or "Mutation 12")
+ */
+/obj/machinery/computer/scan_consolenew/proc/check_discovery(alias)
+ // Note - All code paths that call this have already done checks on the
+ // current occupant to prevent cheese and other abuses. If you call this
+ // proc please also do the following checks first:
+ // if(!can_modify_occupant())
+ // return
+ // if(!(scanner_occupant == connected_scanner.occupant))
+ // return
+
+ // Turn the alias ("Mutation 1", "Mutation 35") into a mutation path
+ var/path = GET_MUTATION_TYPE_FROM_ALIAS(alias)
+
+ // Check to see if this mutation is in the active mutation list. If it isn't,
+ // then the mutation isn't eligible for discovery. If it is but is scrambled,
+ // then the mutation isn't eligible for discovery. Finally, check if the
+ // mutation is in discovered mutations - If it isn't, add it to discover.
+ var/datum/mutation/human/M = scanner_occupant.dna.get_mutation(path)
+ if(!M)
+ return FALSE
+ if(M.scrambled)
+ return FALSE
+ if(stored_research && !(path in stored_research.discovered_mutations))
+ var/datum/mutation/human/HM = GET_INITIALIZED_MUTATION(path)
+ stored_research.discovered_mutations += path
+ say("Successfully discovered [HM.name].")
+ return TRUE
+
+ return FALSE
+
+/**
+ * Find a mutation from various storage locations via ATOM ref
+ *
+ * Takes an ATOM Ref and searches the appropriate mutation buffers and storage
+ * vars to try and find the associated mutation.
+ *
+ * Arguments:
+ * * ref - ATOM ref of the mutation to locate
+ * * target_flags - Flags for storage mediums to search, see #defines
+ */
+/obj/machinery/computer/scan_consolenew/proc/get_mut_by_ref(ref, target_flags)
+ var/mutation
+
+ // Assume the occupant is valid and the check has been carried out before
+ // calling this proc with the relevant flags.
+ if(target_flags & SEARCH_OCCUPANT)
+ mutation = (locate(ref) in scanner_occupant.dna.mutations)
+ if(mutation)
+ return mutation
+
+ if(target_flags & SEARCH_STORED)
+ mutation = (locate(ref) in stored_mutations)
+ if(mutation)
+ return mutation
+
+ if(diskette && (target_flags & SEARCH_DISKETTE))
+ mutation = (locate(ref) in diskette.mutations)
+ if(mutation)
+ return mutation
+
+ if(injector_selection && (target_flags & SEARCH_ADV_INJ))
+ for(var/I in injector_selection)
+ mutation = (locate(ref) in injector_selection["[I]"])
+ if(mutation)
+ return mutation
+
+ return null
+
+/**
+ * Creates a randomised accuracy value for the enzyme pulse functionality.
+ *
+ * Donor code from previous DNA Console iteration.
+ *
+ * Arguments:
+ * * position - Index of the intended enzyme element to pulse
+ * * radduration - Duration of intended radiation pulse
+ * * number_of_blocks - Number of individual data blocks in the pulsed enzyme
+ */
+/obj/machinery/computer/scan_consolenew/proc/randomize_radiation_accuracy(position, radduration, number_of_blocks)
+ var/val = round(gaussian(0, RADIATION_ACCURACY_MULTIPLIER/radduration) + position, 1)
+ return WRAP(val, 1, number_of_blocks+1)
+
+/**
+ * Scrambles an enzyme element value for the enzyme pulse functionality.
+ *
+ * Donor code from previous DNA Console iteration.
+ *
+ * Arguments:
+ * * input - Enzyme identity element to scramble, expected hex value
+ * * rs - Strength of radiation pulse, increases the range of possible outcomes
+ */
+/obj/machinery/computer/scan_consolenew/proc/scramble(input,rs)
var/length = length(input)
var/ran = gaussian(0, rs*RADIATION_STRENGTH_MULTIPLIER)
if(ran == 0)
@@ -956,98 +1950,48 @@
ran = -round(-ran) //positive, so ceiling it
return num2hex(WRAP(hex2num(input)+ran, 0, 16**length), length)
-/obj/machinery/computer/scan_consolenew/proc/randomize_radiation_accuracy(position, radduration, number_of_blocks)
- var/val = round(gaussian(0, RADIATION_ACCURACY_MULTIPLIER/radduration) + position, 1)
- return WRAP(val, 1, number_of_blocks+1)
+ /**
+ * Performs the enzyme radiation pulse.
+ *
+ * Donor code from previous DNA Console iteration. Called from process() when
+ * there is a radiation pulse in progress. Ends processing.
+ */
+/obj/machinery/computer/scan_consolenew/proc/rad_pulse()
+ // GUARD CHECK - Can we genetically modify the occupant? Includes scanner
+ // operational guard checks.
+ // If we can't, abort the procedure.
+ if(!can_modify_occupant())
+ rad_pulse_index = 0
+ end_processing()
+ return
-/obj/machinery/computer/scan_consolenew/proc/get_viable_occupant()
- var/mob/living/carbon/viable_occupant = null
- if(connected)
- viable_occupant = connected.occupant
- if(!istype(viable_occupant) || !viable_occupant.dna || HAS_TRAIT(viable_occupant, TRAIT_RADIMMUNE) || HAS_TRAIT(viable_occupant, TRAIT_BADDNA))
- viable_occupant = null
- return viable_occupant
+ var/len = length_char(scanner_occupant.dna.uni_identity)
+ var/num = randomize_radiation_accuracy(rad_pulse_index, radduration + (connected_scanner.precision_coeff ** 2), len) //Each manipulator level above 1 makes randomization as accurate as selected time + manipulator lvl^2 //Value is this high for the same reason as with laser - not worth the hassle of upgrading if the bonus is low
+ var/hex = copytext_char(scanner_occupant.dna.uni_identity, num, num+1)
+ hex = scramble(hex, radstrength, radduration)
-/obj/machinery/computer/scan_consolenew/proc/apply_buffer(action,buffer_num)
- buffer_num = clamp(buffer_num, 1, NUMBER_OF_BUFFERS)
- var/list/buffer_slot = buffer[buffer_num]
- var/mob/living/carbon/viable_occupant = get_viable_occupant()
- if(istype(buffer_slot))
- viable_occupant.radiation += rand(100/(connected.damage_coeff ** 2),250/(connected.damage_coeff ** 2))
- //15 and 40 are just magic numbers that were here before so i didnt touch them, they are initial boundaries of damage
- //Each laser level reduces damage by lvl^2, so no effect on 1 lvl, 4 times less damage on 2 and 9 times less damage on 3
- //Numbers are this high because other way upgrading laser is just not worth the hassle, and i cant think of anything better to inmrove
- switch(action)
- if(SCANNER_ACTION_UI)
- if(buffer_slot["UI"])
- viable_occupant.dna.uni_identity = buffer_slot["UI"]
- viable_occupant.updateappearance(mutations_overlay_update=1)
- if(SCANNER_ACTION_UE)
- if(buffer_slot["name"] && buffer_slot["UE"] && buffer_slot["blood_type"])
- viable_occupant.real_name = buffer_slot["name"]
- viable_occupant.name = buffer_slot["name"]
- viable_occupant.dna.unique_enzymes = buffer_slot["UE"]
- viable_occupant.dna.blood_type = buffer_slot["blood_type"]
- if(SCANNER_ACTION_MIXED)
- if(buffer_slot["UI"])
- viable_occupant.dna.uni_identity = buffer_slot["UI"]
- viable_occupant.updateappearance(mutations_overlay_update=1)
- if(buffer_slot["name"] && buffer_slot["UE"] && buffer_slot["blood_type"])
- viable_occupant.real_name = buffer_slot["name"]
- viable_occupant.name = buffer_slot["name"]
- viable_occupant.dna.unique_enzymes = buffer_slot["UE"]
- viable_occupant.dna.blood_type = buffer_slot["blood_type"]
+ scanner_occupant.dna.uni_identity = copytext_char(scanner_occupant.dna.uni_identity, 1, num) + hex + copytext_char(scanner_occupant.dna.uni_identity, num + 1)
+ scanner_occupant.updateappearance(mutations_overlay_update=1)
-/obj/machinery/computer/scan_consolenew/proc/on_scanner_close()
- if(delayed_action && get_viable_occupant())
- to_chat(connected.occupant, "
")
- apply_buffer(delayed_action["action"],delayed_action["buffer"])
- delayed_action = null //or make it stick + reset button ?
+ rad_pulse_index = 0
+ end_processing()
+ return
-/obj/machinery/computer/scan_consolenew/proc/get_valid_mutation(mutation)
- var/mob/living/carbon/C = get_viable_occupant()
- if(C)
- var/datum/mutation/human/HM = C.dna.get_mutation(mutation)
- if(HM)
- return HM
- for(var/datum/mutation/human/A in stored_mutations)
- if(A.type == mutation)
- return A
+/**
+ * Sets the default state for the tgui interface.
+ */
+/obj/machinery/computer/scan_consolenew/proc/set_default_state()
+ tgui_view_state["consoleMode"] = "storage"
+ tgui_view_state["storageMode"] = "console"
+ tgui_view_state["storageConsSubMode"] = "mutations"
+ tgui_view_state["storageDiskSubMode"] = "mutations"
-/obj/machinery/computer/scan_consolenew/proc/get_mutation_list(include_storage) //Returns a list of the mutation index types and any extra mutations
- var/mob/living/carbon/viable_occupant = get_viable_occupant()
- var/list/paths = list()
- if(viable_occupant)
- for(var/A in viable_occupant.dna.mutation_index)
- paths += A
- for(var/datum/mutation/human/A in viable_occupant.dna.mutations)
- if(A.class == MUT_EXTRA)
- paths += A.type
- if(include_storage)
- for(var/datum/mutation/human/A in stored_mutations)
- paths += A.type
- return paths
-
-/obj/machinery/computer/scan_consolenew/proc/get_valid_gene_string(mutation)
- var/mob/living/carbon/C = get_viable_occupant()
- if(C && (mutation in C.dna.mutation_index))
- return GET_GENE_STRING(mutation, C.dna)
- else if(C && (LAZYLEN(C.dna.mutations)))
- for(var/datum/mutation/human/A in C.dna.mutations)
- if(A.type == mutation)
- return GET_SEQUENCE(mutation)
- for(var/datum/mutation/human/A in stored_mutations)
- if(A.type == mutation)
- return GET_SEQUENCE(mutation)
-
-/obj/machinery/computer/scan_consolenew/proc/discover(mutation)
- if(stored_research && !(mutation in stored_research.discovered_mutations))
- stored_research.discovered_mutations += mutation
- return TRUE
-/////////////////////////// DNA MACHINES
#undef INJECTOR_TIMEOUT
#undef NUMBER_OF_BUFFERS
+#undef SCRAMBLE_TIMEOUT
+#undef JOKER_TIMEOUT
+#undef JOKER_UPGRADE
#undef RADIATION_STRENGTH_MAX
#undef RADIATION_STRENGTH_MULTIPLIER
@@ -1057,11 +2001,9 @@
#undef RADIATION_IRRADIATION_MULTIPLIER
-#undef SCANNER_ACTION_SE
-#undef SCANNER_ACTION_UI
-#undef SCANNER_ACTION_UE
-#undef SCANNER_ACTION_MIXED
+#undef STATUS_TRANSFORMING
-//#undef BAD_MUTATION_DIFFICULTY
-//#undef GOOD_MUTATION_DIFFICULTY
-//#undef OP_MUTATION_DIFFICULTY
+#undef SEARCH_OCCUPANT
+#undef SEARCH_STORED
+#undef SEARCH_DISKETTE
+#undef SEARCH_ADV_INJ
diff --git a/code/game/machinery/dna_scanner.dm b/code/game/machinery/dna_scanner.dm
index 8525b2063d0..49b6463b1c1 100644
--- a/code/game/machinery/dna_scanner.dm
+++ b/code/game/machinery/dna_scanner.dm
@@ -15,6 +15,7 @@
var/precision_coeff
var/message_cooldown
var/breakout_time = 1200
+ var/obj/machinery/computer/scan_consolenew/linked_console = null
/obj/machinery/dna_scannernew/RefreshParts()
scan_level = 0
@@ -97,9 +98,8 @@
// DNA manipulators cannot operate on severed heads or brains
if(iscarbon(occupant))
- var/obj/machinery/computer/scan_consolenew/console = locate_computer(/obj/machinery/computer/scan_consolenew)
- if(console)
- console.on_scanner_close()
+ if(linked_console)
+ linked_console.on_scanner_close()
return TRUE
@@ -109,6 +109,9 @@
..()
+ if(linked_console)
+ linked_console.on_scanner_open()
+
return TRUE
/obj/machinery/dna_scannernew/relaymove(mob/user as mob)
@@ -147,7 +150,7 @@
/obj/item/disk/data
name = "DNA data disk"
icon_state = "datadisk0" //Gosh I hope syndies don't mistake them for the nuke disk.
- var/list/fields = list()
+ var/list/genetic_makeup_buffer = list()
var/list/mutations = list()
var/max_mutations = 6
var/read_only = FALSE //Well,it's still a floppy disk
diff --git a/code/game/objects/items/chromosome.dm b/code/game/objects/items/chromosome.dm
index d3a77080bf3..3acf3cfe5cb 100644
--- a/code/game/objects/items/chromosome.dm
+++ b/code/game/objects/items/chromosome.dm
@@ -53,32 +53,32 @@
/obj/item/chromosome/stabilizer
name = "stabilizer chromosome"
- desc = "A chromosome that adjusts to the body to reduce genetic damage by 20%."
+ desc = "A chromosome that reduces mutation instability by 20%."
icon_state = "stabilizer"
stabilizer_coeff = 0.8
weight = 1
/obj/item/chromosome/synchronizer
name = "synchronizer chromosome"
- desc = "A chromosome that gives the mind more controle over the mutation, reducing knockback and downsides by 50%."
+ desc = "A chromosome that reduces mutation knockback and downsides by 50%."
icon_state = "synchronizer"
synchronizer_coeff = 0.5
/obj/item/chromosome/power
name = "power chromosome"
- desc = "A power chromosome for boosting certain mutation's power by 50%."
+ desc = "A chromosome that increases mutation power by 50%."
icon_state = "power"
power_coeff = 1.5
/obj/item/chromosome/energy
name = "energetic chromosome"
- desc = "A chromosome that reduces cooldown on action based mutations by 50%."
+ desc = "A chromosome that reduces action based mutation cooldowns by by 50%."
icon_state = "energy"
energy_coeff = 0.5
/obj/item/chromosome/reinforcer
name = "reinforcement chromosome"
- desc = "Renders the mutation immune to mutadone."
+ desc = "A chromosome that renders mutations immune to mutadone."
icon_state = "reinforcer"
weight = 3
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index 0c83d4e9d15..79ae060a984 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -421,7 +421,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
new_character.real_name = record_found.fields["name"]
new_character.gender = record_found.fields["gender"]
new_character.age = record_found.fields["age"]
- new_character.hardset_dna(record_found.fields["identity"], record_found.fields["enzymes"], record_found.fields["name"], record_found.fields["blood_type"], new record_found.fields["species"], record_found.fields["features"])
+ new_character.hardset_dna(record_found.fields["identity"], record_found.fields["enzymes"], null, record_found.fields["name"], record_found.fields["blood_type"], new record_found.fields["species"], record_found.fields["features"])
else
var/datum/preferences/A = new()
A.copy_to(new_character)
diff --git a/code/modules/hydroponics/grown/replicapod.dm b/code/modules/hydroponics/grown/replicapod.dm
index b1581c9dcee..020d554cf53 100644
--- a/code/modules/hydroponics/grown/replicapod.dm
+++ b/code/modules/hydroponics/grown/replicapod.dm
@@ -118,7 +118,7 @@
features["mcolor"] = "#59CE00"
for(var/V in quirks)
new V(podman)
- podman.hardset_dna(null,null,podman.real_name,blood_type, new /datum/species/pod,features)//Discard SE's and UI's, podman cloning is inaccurate, and always make them a podman
+ podman.hardset_dna(null,null,null,podman.real_name,blood_type, new /datum/species/pod,features)//Discard SE's and UI's, podman cloning is inaccurate, and always make them a podman
podman.set_cloned_appearance()
log_cloning("[key_name(mind)] cloned as a podman via [src] in [parent] at [AREACOORD(parent)].")
diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm
index 75c635d71de..0eeae528bad 100644
--- a/code/modules/mob/living/carbon/carbon_defines.dm
+++ b/code/modules/mob/living/carbon/carbon_defines.dm
@@ -69,3 +69,6 @@
var/heat_protection = 0 // No heat protection
/// Protection (insulation) from the cold, Value 0-1 corresponding to the percentage of protection
var/cold_protection = 0 // No cold protection
+
+ /// Timer id of any transformation
+ var/transformation_timer
diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm
index 46ed7fcaee5..650207ad4c6 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -407,7 +407,9 @@ GLOBAL_LIST_EMPTY(roundstart_races)
//keep it at the right spot, so we can't have people taking shortcuts
var/location = C.dna.mutation_index.Find(inert_mutation)
C.dna.mutation_index[location] = new_species.inert_mutation
+ C.dna.default_mutation_genes[location] = C.dna.mutation_index[location]
C.dna.mutation_index[new_species.inert_mutation] = create_sequence(new_species.inert_mutation)
+ C.dna.default_mutation_genes[new_species.inert_mutation] = C.dna.mutation_index[new_species.inert_mutation]
if(inherent_factions)
for(var/i in inherent_factions)
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 62e66038a0a..0e41840f6ee 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -1,26 +1,8 @@
+#define TRANSFORMATION_DURATION 22
+
/mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG))
- if (notransform)
+ if (notransform || transformation_timer)
return
- //Handle items on mob
-
- //first implants & organs
- var/list/stored_implants = list()
- var/list/int_organs = list()
-
- if (tr_flags & TR_KEEPIMPLANTS)
- for(var/X in implants)
- var/obj/item/implant/IMP = X
- stored_implants += IMP
- IMP.removed(src, 1, 1)
-
- var/list/missing_bodyparts_zones = get_missing_limbs()
-
- var/obj/item/cavity_object
-
- var/obj/item/bodypart/chest/CH = get_bodypart(BODY_ZONE_CHEST)
- if(CH.cavity_item)
- cavity_object = CH.cavity_item
- CH.cavity_item = null
if(tr_flags & TR_KEEPITEMS)
var/Itemlist = get_equipped_items(TRUE)
@@ -30,13 +12,36 @@
//Make mob invisible and spawn animation
notransform = TRUE
- Paralyze(22, ignore_canstun = TRUE)
+ Paralyze(TRANSFORMATION_DURATION, ignore_canstun = TRUE)
icon = null
cut_overlays()
invisibility = INVISIBILITY_MAXIMUM
new /obj/effect/temp_visual/monkeyify(loc)
- sleep(22)
+
+ transformation_timer = addtimer(CALLBACK(src, .proc/finish_monkeyize, tr_flags), TRANSFORMATION_DURATION, TIMER_UNIQUE)
+
+/mob/living/carbon/proc/finish_monkeyize(tr_flags)
+ transformation_timer = null
+
+ var/list/missing_bodyparts_zones = get_missing_limbs()
+
+ var/list/stored_implants = list()
+
+ if (tr_flags & TR_KEEPIMPLANTS)
+ for(var/X in implants)
+ var/obj/item/implant/IMP = X
+ stored_implants += IMP
+ IMP.removed(src, 1, 1)
+
+ var/list/int_organs = list()
+ var/obj/item/cavity_object
+
+ var/obj/item/bodypart/chest/CH = get_bodypart(BODY_ZONE_CHEST)
+ if(CH.cavity_item)
+ cavity_object = CH.cavity_item
+ CH.cavity_item = null
+
var/mob/living/carbon/monkey/O = new /mob/living/carbon/monkey( loc )
// hash the original name?
@@ -50,6 +55,7 @@
if(tr_flags & TR_KEEPSE)
O.dna.mutation_index = dna.mutation_index
+ O.dna.default_mutation_genes = dna.default_mutation_genes
O.dna.set_se(1, GET_INITIALIZED_MUTATION(RACEMUT))
if(suiciding)
@@ -163,11 +169,32 @@
//Could probably be merged with monkeyize but other transformations got their own procs, too
/mob/living/carbon/proc/humanize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG))
- if (notransform)
+ if (notransform || transformation_timer)
return
- //Handle items on mob
- //first implants & organs
+ //now the rest
+ if (tr_flags & TR_KEEPITEMS)
+ var/Itemlist = get_equipped_items(TRUE)
+ Itemlist += held_items
+ for(var/obj/item/W in Itemlist)
+ dropItemToGround(W, TRUE)
+ if (client)
+ client.screen -= W
+
+ //Make mob invisible and spawn animation
+ notransform = TRUE
+ Paralyze(TRANSFORMATION_DURATION, ignore_canstun = TRUE)
+
+ icon = null
+ cut_overlays()
+ invisibility = INVISIBILITY_MAXIMUM
+ new /obj/effect/temp_visual/monkeyify/humanify(loc)
+
+ transformation_timer = addtimer(CALLBACK(src, .proc/finish_humanize, tr_flags), TRANSFORMATION_DURATION, TIMER_UNIQUE)
+
+/mob/living/carbon/proc/finish_humanize(tr_flags)
+ transformation_timer = null
+
var/list/stored_implants = list()
var/list/int_organs = list()
@@ -186,27 +213,6 @@
cavity_object = CH.cavity_item
CH.cavity_item = null
- //now the rest
- if (tr_flags & TR_KEEPITEMS)
- var/Itemlist = get_equipped_items(TRUE)
- Itemlist += held_items
- for(var/obj/item/W in Itemlist)
- dropItemToGround(W, TRUE)
- if (client)
- client.screen -= W
-
-
-
- //Make mob invisible and spawn animation
- notransform = TRUE
- Paralyze(22, ignore_canstun = TRUE)
-
- icon = null
- cut_overlays()
- invisibility = INVISIBILITY_MAXIMUM
- new /obj/effect/temp_visual/monkeyify/humanify(loc)
- sleep(22)
-
var/mob/living/carbon/human/O = new( loc )
for(var/obj/item/C in O.loc)
if(C.anchored)
@@ -225,6 +231,7 @@
if(tr_flags & TR_KEEPSE)
O.dna.mutation_index = dna.mutation_index
+ O.dna.default_mutation_genes = dna.default_mutation_genes
O.dna.set_se(0, GET_INITIALIZED_MUTATION(RACEMUT))
O.domutcheck()
@@ -640,3 +647,5 @@
//Not in here? Must be untested!
return 0
+
+#undef TRANSFORMATION_DURATION
diff --git a/code/modules/spells/spell_types/lichdom.dm b/code/modules/spells/spell_types/lichdom.dm
index 77c7a8e0b7a..1845fdfadf4 100644
--- a/code/modules/spells/spell_types/lichdom.dm
+++ b/code/modules/spells/spell_types/lichdom.dm
@@ -136,7 +136,7 @@
lich.real_name = mind.name
mind.transfer_to(lich)
mind.grab_ghost(force=TRUE)
- lich.hardset_dna(null,null,lich.real_name,null, new /datum/species/skeleton)
+ lich.hardset_dna(null,null,null,lich.real_name,null, new /datum/species/skeleton)
to_chat(lich, "
")
var/turf/body_turf = get_turf(old_body)
lich.Paralyze(200 + 200*resurrections)
diff --git a/tgui/README.md b/tgui/README.md
index 5dabbb2f07a..5fd6434c1ea 100644
--- a/tgui/README.md
+++ b/tgui/README.md
@@ -105,6 +105,24 @@ with the console:
> We prefer to keep it version controlled, so that people could build the
> game just by using Dream Maker.
+## Troubleshooting
+
+**Development server doesn't find my BYOND cache!**
+
+This happens if your Documents folder in Windows has a custom location, for
+example in `E:\Libraries\Documents`. Development server has no knowledge
+of these non-standard locations, therefore you have to run the dev server
+with an additional environmental variable, with a full path to BYOND cache.
+
+```
+export BYOND_CACHE="E:/Libraries/Documents/BYOND/cache"
+bin/tgui --dev
+```
+
+Note that in Windows, you have to go through Advanced System Settings,
+System Properties and then open Environment Variables window to do the
+same thing. You may need to reboot after this.
+
## Project structure
- `/packages` - Each folder here represents a self-contained Node module.
@@ -416,6 +434,17 @@ Props:
- See inherited props: [Box](#box)
+### `Divider`
+
+Draws a horizontal or vertical line, dividing a section into groups.
+Works like the good old `
` element, but it's fancier.
+
+Props:
+
+- `vertical: boolean` - Divide content vertically.
+- `hidden: boolean` - Divider can divide content without creating a dividing
+line.
+
### `Dropdown`
A simple dropdown box component. Lets the user select from a list of options
diff --git a/tgui/packages/common/collections.js b/tgui/packages/common/collections.js
index db0086867d8..1e5c978942e 100644
--- a/tgui/packages/common/collections.js
+++ b/tgui/packages/common/collections.js
@@ -4,6 +4,8 @@
* - Arrays are returned unmodified;
* - If object was provided, keys will be discarded;
* - Everything else will result in an empty array.
+ *
+ * @returns {any[]}
*/
export const toArray = collection => {
if (Array.isArray(collection)) {
@@ -22,6 +24,38 @@ export const toArray = collection => {
return [];
};
+/**
+ * Converts a given object to an array, and appends a key to every
+ * object inside of that array.
+ *
+ * Example input (object):
+ * ```
+ * {
+ * 'Foo': { info: 'Hello world!' },
+ * 'Bar': { info: 'Hello world!' },
+ * }
+ * ```
+ *
+ * Example output (array):
+ * ```
+ * [
+ * { key: 'Foo', info: 'Hello world!' },
+ * { key: 'Bar', info: 'Hello world!' },
+ * ]
+ * ```
+ *
+ * @template T
+ * @param {{ [key: string]: T }} obj Object, or in DM terms, an assoc array
+ * @param {string} keyProp Property, to which key will be assigned
+ * @returns {T[]} Array of keyed objects
+ */
+export const toKeyedArray = (obj, keyProp = 'key') => {
+ return map((item, key) => ({
+ [keyProp]: key,
+ ...item,
+ }))(obj);
+};
+
/**
* Iterates over elements of collection, returning an array of all elements
* iteratee returns truthy for. The predicate is invoked with three
@@ -29,6 +63,8 @@ export const toArray = collection => {
*
* If collection is 'null' or 'undefined', it will be returned "as is"
* without emitting any errors (which can be useful in some cases).
+ *
+ * @returns {any[]}
*/
export const filter = iterateeFn => collection => {
if (collection === null && collection === undefined) {
@@ -54,6 +90,8 @@ export const filter = iterateeFn => collection => {
*
* If collection is 'null' or 'undefined', it will be returned "as is"
* without emitting any errors (which can be useful in some cases).
+ *
+ * @returns {any[]}
*/
export const map = iterateeFn => collection => {
if (collection === null && collection === undefined) {
@@ -101,6 +139,8 @@ const COMPARATOR = (objA, objB) => {
* of running each element in a collection thru each iteratee.
*
* Iteratees are called with one argument (value).
+ *
+ * @returns {any[]}
*/
export const sortBy = (...iterateeFns) => array => {
if (!Array.isArray(array)) {
@@ -146,10 +186,55 @@ export const reduce = (reducerFn, initialValue) => array => {
return result;
};
+/**
+ * Creates a duplicate-free version of an array, using SameValueZero for
+ * equality comparisons, in which only the first occurrence of each element
+ * is kept. The order of result values is determined by the order they occur
+ * in the array.
+ *
+ * It accepts iteratee which is invoked for each element in array to generate
+ * the criterion by which uniqueness is computed. The order of result values
+ * is determined by the order they occur in the array. The iteratee is
+ * invoked with one argument: value.
+ */
+export const uniqBy = iterateeFn => array => {
+ const { length } = array;
+ const result = [];
+ const seen = iterateeFn ? [] : result;
+ let index = -1;
+ outer:
+ while (++index < length) {
+ let value = array[index];
+ const computed = iterateeFn ? iterateeFn(value) : value;
+ value = value !== 0 ? value : 0;
+ if (computed === computed) {
+ let seenIndex = seen.length;
+ while (seenIndex--) {
+ if (seen[seenIndex] === computed) {
+ continue outer;
+ }
+ }
+ if (iterateeFn) {
+ seen.push(computed);
+ }
+ result.push(value);
+ }
+ else if (!seen.includes(computed)) {
+ if (seen !== result) {
+ seen.push(computed);
+ }
+ result.push(value);
+ }
+ }
+ return result;
+};
+
/**
* Creates an array of grouped elements, the first of which contains
* the first elements of the given arrays, the second of which contains
* the second elements of the given arrays, and so on.
+ *
+ * @returns {any[]}
*/
export const zip = (...arrays) => {
if (arrays.length === 0) {
@@ -172,6 +257,8 @@ export const zip = (...arrays) => {
* This method is like "zip" except that it accepts iteratee to
* specify how grouped values should be combined. The iteratee is
* invoked with the elements of each group.
+ *
+ * @returns {any[]}
*/
export const zipWith = iterateeFn => (...arrays) => {
return map(values => iterateeFn(...values))(zip(...arrays));
diff --git a/tgui/packages/tgui-dev-server/reloader.js b/tgui/packages/tgui-dev-server/reloader.js
index d5541c2a992..394e55afdb3 100644
--- a/tgui/packages/tgui-dev-server/reloader.js
+++ b/tgui/packages/tgui-dev-server/reloader.js
@@ -9,6 +9,8 @@ const logger = createLogger('reloader');
const HOME = os.homedir();
const SEARCH_LOCATIONS = [
+ // Custom location
+ process.env.BYOND_CACHE,
// Windows
`${HOME}/*/BYOND/cache`,
// Wine
@@ -28,6 +30,9 @@ export const findCacheRoot = async () => {
logger.log('looking for byond cache');
// Find BYOND cache folders
for (let pattern of SEARCH_LOCATIONS) {
+ if (!pattern) {
+ continue;
+ }
const paths = await resolveGlob(pattern);
if (paths.length > 0) {
cacheRoot = paths[0];
diff --git a/tgui/packages/tgui/components/Box.js b/tgui/packages/tgui/components/Box.js
index c3828450cf2..b0f56e67e85 100644
--- a/tgui/packages/tgui/components/Box.js
+++ b/tgui/packages/tgui/components/Box.js
@@ -165,22 +165,3 @@ export const Box = props => {
};
Box.defaultHooks = pureComponentHooks;
-
-/**
- * A hack to force certain things (like tables) to position correctly
- * inside bugged things, like Flex in Internet Explorer.
- */
-const ForcedBox = props => {
- const { children, ...rest } = props;
- return (
-
- );
-};
-
-ForcedBox.defaultHooks = pureComponentHooks;
-
-Box.Forced = ForcedBox;
diff --git a/tgui/packages/tgui/components/Dimmer.js b/tgui/packages/tgui/components/Dimmer.js
index 9d3ead05493..f38d0a026a6 100644
--- a/tgui/packages/tgui/components/Dimmer.js
+++ b/tgui/packages/tgui/components/Dimmer.js
@@ -1,19 +1,18 @@
+import { classes } from 'common/react';
import { Box } from './Box';
export const Dimmer = props => {
- const { style, ...rest } = props;
+ const { className, children, ...rest } = props;
return (
+ className={classes([
+ 'Dimmer',
+ ...className,
+ ])}
+ {...rest}>
+
+
);
};
diff --git a/tgui/packages/tgui/components/Divider.js b/tgui/packages/tgui/components/Divider.js
new file mode 100644
index 00000000000..5c807c290b8
--- /dev/null
+++ b/tgui/packages/tgui/components/Divider.js
@@ -0,0 +1,18 @@
+import { classes } from 'common/react';
+
+export const Divider = props => {
+ const {
+ vertical,
+ hidden,
+ } = props;
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/components/Dropdown.js b/tgui/packages/tgui/components/Dropdown.js
index 462b8a5fe56..3064f40c2cf 100644
--- a/tgui/packages/tgui/components/Dropdown.js
+++ b/tgui/packages/tgui/components/Dropdown.js
@@ -1,5 +1,5 @@
import { classes } from 'common/react';
-import { Component, createRef } from 'inferno';
+import { Component } from 'inferno';
import { Box } from './Box';
import { Icon } from './Icon';
@@ -46,7 +46,7 @@ export class Dropdown extends Component {
{
+ onClick={() => {
this.setSelected(option);
}}>
{option}
@@ -60,9 +60,12 @@ export class Dropdown extends Component {
const {
color = 'default',
over,
+ noscroll,
+ nochevron,
width,
onClick,
selected,
+ disabled,
...boxProps
} = props;
const {
@@ -80,7 +83,7 @@ export class Dropdown extends Component {
'width': width,
}}
className={classes([
- 'Dropdown__menu',
+ noscroll && 'Dropdown__menu-noscroll' || 'Dropdown__menu',
over && 'Dropdown__over',
])}>
{this.buildMenu()}
@@ -95,18 +98,24 @@ export class Dropdown extends Component {
'Dropdown__control',
'Button',
'Button--color--' + color,
+ disabled && 'Button--disabled',
className,
])}
{...rest}
- onClick={e => {
+ onClick={() => {
+ if (disabled && !this.state.open) {
+ return;
+ }
this.setOpen(!this.state.open);
}}>
{this.state.selected}
-
-
-
+ {!!nochevron || (
+
+
+
+ )}
{menu}
diff --git a/tgui/packages/tgui/components/Flex.js b/tgui/packages/tgui/components/Flex.js
index 447c9e13eb9..4b5cb71115c 100644
--- a/tgui/packages/tgui/components/Flex.js
+++ b/tgui/packages/tgui/components/Flex.js
@@ -1,5 +1,5 @@
import { classes, pureComponentHooks } from 'common/react';
-import { Box } from './Box';
+import { Box, unit } from './Box';
export const computeFlexProps = props => {
const {
@@ -39,6 +39,10 @@ export const computeFlexItemProps = props => {
className,
grow,
order,
+ shrink,
+ // IE11: Always set basis to specified width, which fixes certain
+ // bugs when rendering tables inside the flex.
+ basis = props.width,
align,
...rest
} = props;
@@ -50,6 +54,8 @@ export const computeFlexItemProps = props => {
style: {
...rest.style,
'flex-grow': grow,
+ 'flex-shrink': shrink,
+ 'flex-basis': unit(basis),
'order': order,
'align-self': align,
},
diff --git a/tgui/packages/tgui/components/LabeledList.js b/tgui/packages/tgui/components/LabeledList.js
index 2364f9798a4..a7b68ddec06 100644
--- a/tgui/packages/tgui/components/LabeledList.js
+++ b/tgui/packages/tgui/components/LabeledList.js
@@ -18,6 +18,7 @@ export const LabeledListItem = props => {
label,
labelColor = 'label',
color,
+ textAlign,
buttons,
content,
children,
@@ -34,11 +35,13 @@ export const LabeledListItem = props => {
className={classes([
'LabeledList__cell',
'LabeledList__label',
- ])}
- content={label + ':'} />
+ ])}>
+ {label ? label + ':' : null}
+
+
+ );
+};
+
+const StorageButtons = props => {
+ const { data, act } = useBackend(props);
+ const { hasDisk } = data;
+ const { storageMode, storageConsSubMode, storageDiskSubMode } = data.view;
+ return (
+
+ );
+};
+
+const DnaConsoleStorage = props => {
+ const { state } = props;
+ const { data, act } = useBackend(props);
+ const { storageMode, storageConsSubMode, storageDiskSubMode } = data.view;
+ const { diskMakeupBuffer, diskHasMakeup } = data;
+ const mutations = data.storage[storageMode];
+ return (
+
+ )}>
+ {storageMode === STORAGE_MODE_CONSOLE
+ && storageConsSubMode === STORAGE_CONS_SUBMODE_MUTATIONS && (
+
+ )}
+ {storageMode === STORAGE_MODE_CONSOLE
+ && storageConsSubMode === STORAGE_CONS_SUBMODE_CHROMOSOMES && (
+
+ )}
+ {storageMode === STORAGE_MODE_DISK
+ && storageDiskSubMode === STORAGE_DISK_SUBMODE_MUTATIONS && (
+
+ )}
+ {storageMode === STORAGE_MODE_DISK
+ && storageDiskSubMode === STORAGE_DISK_SUBMODE_ENZYMES && (
+
+ )}
+
+ );
+};
+
+const StorageMutations = props => {
+ const { state, mutations, customMode = '' } = props;
+ const { data, act } = useBackend(props);
+ const mode = data.view.storageMode + customMode;
+
+ let mutationRef = data.view[`storage${mode}MutationRef`];
+ let mutation = mutations
+ .find(mutation => mutation.ByondRef === mutationRef);
+
+ // If no mutation is selected but there are stored mutations, pick the first
+ // mutation and set that as the currently showed one.
+ if (!mutation && mutations.length > 0) {
+ mutation = mutations[0];
+ mutationRef = mutation.ByondRef;
+ }
+
+ return (
+
+ );
+};
+
+const StorageChromosomes = props => {
+ const { data, act } = useBackend(props);
+ const chromos = data.chromoStorage ?? [];
+ const uniqueChromos = uniqBy(chromo => chromo.Name)(chromos);
+ const chromoName = data.view.storageChromoName;
+ const chromo = chromos.find(chromo => chromo.Name === chromoName);
+ return (
+
+ );
+};
+
+const MutationInfo = props => {
+ const { state, mutation } = props;
+ const { data, act } = useBackend(props);
+ const {
+ diskCapacity,
+ diskReadOnly,
+ hasDisk,
+ isInjectorReady,
+ mutationCapacity,
+ } = data;
+ const diskMutations = data.storage.disk ?? [];
+ const mutationStorage = data.storage.console ?? [];
+ const advInjectors = data.storage.injector ?? [];
+ if (!mutation) {
+ return (
+
+ );
+ }
+ if (mutation.Source === 'occupant' && !mutation.Discovered) {
+ return (
+
+ );
+ }
+ const savedToConsole = mutationStorage
+ .find(x => isSameMutation(x, mutation));
+ const savedToDisk = diskMutations
+ .find(x => isSameMutation(x, mutation));
+ const combinedMutations = flow([
+ uniqBy(mutation => mutation.Name),
+ filter(x => x.Name !== mutation.Name),
+ ])([
+ ...diskMutations,
+ ...mutationStorage,
+ ]);
+ return (
+
+ );
+};
+
+const ChromosomeInfo = props => {
+ const { mutation, disabled } = props;
+ const { data, act } = useBackend(props);
+ if (mutation.CanChromo === CHROMOSOME_NEVER) {
+ return (
+
+ );
+ }
+ if (mutation.CanChromo === CHROMOSOME_NONE) {
+ if (disabled) {
+ return (
+
+ );
+ }
+ if (mutation.CanChromo === CHROMOSOME_USED) {
+ return (
+
+ );
+ }
+ return null;
+};
+
+const DnaConsoleSequencer = props => {
+ const { state } = props;
+ const { data, act } = useBackend(props);
+ const mutations = data.storage?.occupant ?? [];
+ const {
+ isJokerReady,
+ isMonkey,
+ jokerSeconds,
+ subjectStatus,
+ } = data;
+ const { sequencerMutation, jokerActive } = data.view;
+ const mutation = mutations.find(mutation => (
+ mutation.Alias === sequencerMutation
+ ));
+ return (
+
+ );
+};
+
+const GenomeImage = props => {
+ const { url, selected, onClick } = props;
+ let outline;
+ if (selected) {
+ outline = '2px solid #22aa00';
+ }
+ return (
+
+ );
+};
+
+const GeneCycler = props => {
+ const { gene, onChange, disabled, ...rest } = props;
+ const length = GENES.length;
+ const index = GENES.indexOf(gene);
+ const color = (disabled && GENE_COLORS['X']) || GENE_COLORS[gene];
+ return (
+
+ );
+};
+
+const GenomeSequencer = props => {
+ const { state, mutation } = props;
+ const { data, act } = useBackend(props);
+ const { jokerActive } = data.view;
+ if (!mutation) {
+ return (
+
+ );
+ }
+ // Create gene cycler buttons
+ const sequence = mutation.Sequence;
+ const defaultSeq = mutation.DefaultSeq;
+ const buttons = [];
+ for (let i = 0; i < sequence.length; i++) {
+ const gene = sequence.charAt(i);
+ const button = (
+