diff --git a/_maps/RandomZLevels/research.dmm b/_maps/RandomZLevels/research.dmm index 7991d86d059..53efc47ff0f 100644 --- a/_maps/RandomZLevels/research.dmm +++ b/_maps/RandomZLevels/research.dmm @@ -1235,7 +1235,7 @@ /obj/structure/closet/crate, /obj/item/disk/data{ desc = "A data disk used to store cloning and genetic records. The name on the label appears to be scratched off."; - fields = list("label" = "Buffer1:Kr-$$@##", "UI" = "f8f603857000f930127c4", "SE" = "414401462231053131010241514651403453121613263463440351136366", "UE" = "340008485c321e542aed4df7032ac04d", "name" = "Krystal Symers", "blood_type" = "A+"); + genetic_makeup_buffer = list("label" = "Buffer1:Kr-$$@##", "UI" = "f8f603857000f930127c4", "SE" = "414401462231053131010241514651403453121613263463440351136366", "UE" = "340008485c321e542aed4df7032ac04d", "name" = "Krystal Symers", "blood_type" = "A+"); name = "dusty genetics data disk"; read_only = 1 }, @@ -1611,7 +1611,7 @@ /obj/structure/closet/crate, /obj/item/disk/data{ desc = "A data disk used to store cloning and genetic records. The name on the label appears to be scratched off with the words 'DO NOT CLONE' hastily written over it."; - fields = list("label" = "Buffer1:George Melons", "UI" = "3c300f11b5421ca7014d8", "SE" = "430431205660551642142504334461413202111310233445620533134255", "UE" = "6893e6a0b0076a41897776b10cc2b324", "name" = "George Melons", "blood_type" = "B+"); + genetic_makeup_buffer = list("label" = "Buffer1:George Melons", "UI" = "3c300f11b5421ca7014d8", "SE" = "430431205660551642142504334461413202111310233445620533134255", "UE" = "6893e6a0b0076a41897776b10cc2b324", "name" = "George Melons", "blood_type" = "B+"); name = "old genetics data disk" }, /obj/item/disk/data{ diff --git a/code/datums/dna.dm b/code/datums/dna.dm index e7b6f40e09a..ddf53eea4c6 100644 --- a/code/datums/dna.dm +++ b/code/datums/dna.dm @@ -12,6 +12,7 @@ var/list/previous = list() //For temporary name/ui/ue/blood_type modifications var/mob/living/holder var/mutation_index[DNA_MUTATION_BLOCKS] //List of which mutations this carbon has and its assigned block + var/default_mutation_genes[DNA_MUTATION_BLOCKS] //List of the default genes from this mutation to allow DNA Scanner highlighting var/stability = 100 var/scrambled = FALSE //Did we take something like mutagen? In that case we cant get our genes scanned to instantly cheese all the powers. @@ -46,10 +47,12 @@ destination.dna.temporary_mutations = temporary_mutations.Copy() if(transfer_SE) destination.dna.mutation_index = mutation_index + destination.dna.default_mutation_genes = default_mutation_genes /datum/dna/proc/copy_dna(datum/dna/new_dna) new_dna.unique_enzymes = unique_enzymes new_dna.mutation_index = mutation_index + new_dna.default_mutation_genes = default_mutation_genes new_dna.uni_identity = uni_identity new_dna.blood_type = blood_type new_dna.features = features.Copy() @@ -123,15 +126,18 @@ if(!LAZYLEN(mutations_temp)) return mutation_index.Cut() + default_mutation_genes.Cut() shuffle_inplace(mutations_temp) if(ismonkey(holder)) mutations |= new RACEMUT(MUT_NORMAL) mutation_index[RACEMUT] = GET_SEQUENCE(RACEMUT) else mutation_index[RACEMUT] = create_sequence(RACEMUT, FALSE) + default_mutation_genes[RACEMUT] = mutation_index[RACEMUT] for(var/i in 2 to DNA_MUTATION_BLOCKS) var/datum/mutation/human/M = mutations_temp[i] mutation_index[M.type] = create_sequence(M.type, FALSE, M.difficulty) + default_mutation_genes[M.type] = mutation_index[M.type] shuffle_inplace(mutation_index) //Used to generate original gene sequences for every mutation @@ -325,7 +331,7 @@ return dna -/mob/living/carbon/human/proc/hardset_dna(ui, list/mutation_index, newreal_name, newblood_type, datum/species/mrace, newfeatures, list/mutations, force_transfer_mutations) +/mob/living/carbon/human/proc/hardset_dna(ui, list/mutation_index, list/default_mutation_genes, newreal_name, newblood_type, datum/species/mrace, newfeatures, list/mutations, force_transfer_mutations) //Do not use force_transfer_mutations for stuff like cloners without some precautions, otherwise some conditional mutations could break (timers, drill hat etc) if(newfeatures) dna.features = newfeatures @@ -348,6 +354,10 @@ if(LAZYLEN(mutation_index)) dna.mutation_index = mutation_index.Copy() + if(LAZYLEN(default_mutation_genes)) + dna.default_mutation_genes = default_mutation_genes.Copy() + else + dna.default_mutation_genes = mutation_index.Copy() domutcheck() if(mrace || newfeatures || ui) @@ -440,8 +450,11 @@ . = TRUE if(on) mutation_index[HM.type] = GET_SEQUENCE(HM.type) + default_mutation_genes[HM.type] = mutation_index[HM.type] else if(GET_SEQUENCE(HM.type) == mutation_index[HM.type]) mutation_index[HM.type] = create_sequence(HM.type, FALSE, HM.difficulty) + default_mutation_genes[HM.type] = mutation_index[HM.type] + /datum/dna/proc/activate_mutation(mutation) //note that this returns a boolean and not a new mob if(!mutation) diff --git a/code/datums/mutations/_mutations.dm b/code/datums/mutations/_mutations.dm index 5222de9e042..1ef7f155517 100644 --- a/code/datums/mutations/_mutations.dm +++ b/code/datums/mutations/_mutations.dm @@ -176,23 +176,23 @@ power.panel = "Genetic" owner.AddSpell(power) return TRUE - + // Runs through all the coefficients and uses this to determine which chromosomes the // mutation can take. Stores these as text strings in a list. /datum/mutation/human/proc/update_valid_chromosome_list() valid_chrom_list.Cut() - + if(can_chromosome == CHROMOSOME_NEVER) valid_chrom_list += "none" return - - valid_chrom_list += "reinforcement" - + + valid_chrom_list += "Reinforcement" + if(stabilizer_coeff != -1) - valid_chrom_list += "stabilizer" + valid_chrom_list += "Stabilizer" if(synchronizer_coeff != -1) - valid_chrom_list += "synchronizer" + valid_chrom_list += "Synchronizer" if(power_coeff != -1) - valid_chrom_list += "power" + valid_chrom_list += "Power" if(energy_coeff != -1) - valid_chrom_list += "energetic" + valid_chrom_list += "Energetic" diff --git a/code/game/machinery/computer/dna_console.dm b/code/game/machinery/computer/dna_console.dm index 0535ea8aa34..da5e9f887ad 100644 --- a/code/game/machinery/computer/dna_console.dm +++ b/code/game/machinery/computer/dna_console.dm @@ -1,25 +1,41 @@ +/// Base timeout for creating mutation activators and other injectors #define INJECTOR_TIMEOUT 100 +/// Maximum number of genetic makeup storage slots in DNA Console #define NUMBER_OF_BUFFERS 3 +/// Timeout for DNA Scramble in DNA Consoles #define SCRAMBLE_TIMEOUT 600 -#define JOKER_TIMEOUT 12000 //20 minutes +/// Timeout for using the Joker feature to solve a gene in DNA Console +#define JOKER_TIMEOUT 12000 +/// How much time DNA Scanner upgrade tiers remove from JOKER_TIMEOUT #define JOKER_UPGRADE 3000 +/// Maximum value for radiaton strength when pulsing enzymes #define RADIATION_STRENGTH_MAX 15 -#define RADIATION_STRENGTH_MULTIPLIER 1 //larger has more range +/// Larger multipliers will affect the range of values when pulsing enzymes +#define RADIATION_STRENGTH_MULTIPLIER 1 +/// Maximum value for the radiation pulse duration when pulsing enzymes #define RADIATION_DURATION_MAX 30 -#define RADIATION_ACCURACY_MULTIPLIER 3 //larger is less accurate +/// Large values reduce pulse accuracy and may pulse other enzymes than selected +#define RADIATION_ACCURACY_MULTIPLIER 3 +/// Special status indicating a scanner occupant is transforming eg. from monkey to human +#define STATUS_TRANSFORMING 4 -#define RADIATION_IRRADIATION_MULTIPLIER 1 //multiplier for how much radiation a test subject receives +/// Multiplier for how much radiation received from DNA Console functionality +#define RADIATION_IRRADIATION_MULTIPLIER 1 -#define SCANNER_ACTION_SE 1 -#define SCANNER_ACTION_UI 2 -#define SCANNER_ACTION_UE 3 -#define SCANNER_ACTION_MIXED 4 +/// Flag for the mutation ref search system. Search will include scanner occupant +#define SEARCH_OCCUPANT 1 +/// Flag for the mutation ref search system. Search will include console storage +#define SEARCH_STORED 2 +/// Flag for the mutation ref search system. Search will include diskette storage +#define SEARCH_DISKETTE 4 +/// Flag for the mutation ref search system. Search will include advanced injector mutations +#define SEARCH_ADV_INJ 8 /obj/machinery/computer/scan_consolenew - name = "\improper DNA scanner access console" + name = "DNA Console" desc = "Scan DNA." icon_screen = "dna" icon_keyboard = "med_key" @@ -31,48 +47,103 @@ active_power_usage = 400 light_color = LIGHT_COLOR_BLUE + /// Link to the techweb's stored research. Used to retrieve stored mutations var/datum/techweb/stored_research + /// Maximum number of mutations that DNA Consoles are able to store var/max_storage = 6 - var/combine + /// Duration for enzyme radiation pulses var/radduration = 2 + /// Strength for enzyme radiation pulses var/radstrength = 1 + /// Maximum number of chromosomes that DNA Consoles are able to store. var/max_chromosomes = 6 - ///Amount of mutations we can store - var/list/buffer[NUMBER_OF_BUFFERS] - ///mutations we have stored + /// Maximum number of enzymes we can store + var/list/genetic_makeup_buffer[NUMBER_OF_BUFFERS] + /// List of all mutations stored on the DNA Console var/list/stored_mutations = list() - ///chromosomes we have stored + /// List of all chromosomes stored in the DNA Console var/list/stored_chromosomes = list() - ///combinations of injectors for the 'injector selection'. format is list("Elsa" = list(Cryokinesis, Geladikinesis), "The Hulk" = list(Hulk, Gigantism), etc) Glowy and the gang being an initialized datum - var/list/injector_selection = list() - ///max amount of selections you can make + /// Assoc list of all advanced injectors. Keys are injector names. Values are lists of mutations. + var/list/list/injector_selection = list() + /// Maximum number of advanced injectors that DNA Consoles store var/max_injector_selections = 2 - ///hard-cap on the advanced dna injector + /// Maximum number of mutation that an advanced injector can store var/max_injector_mutations = 10 - ///the max instability of the advanced injector. + /// Maximum total instability of all combined mutations allowed on an advanced injector var/max_injector_instability = 50 - var/injectorready = 0 //world timer cooldown var + /// World time when injectors are ready to be printed + var/injectorready = 0 + /// World time when JOKER algorithm can be used in DNA Consoles var/jokerready = 0 + /// World time when Scramble can be used in DNA Consoles var/scrambleready = 0 - var/current_screen = "mainmenu" - var/current_mutation //what block are we inspecting? only used when screen = "info" - var/current_storage //what storage block are we looking at? - var/obj/machinery/dna_scannernew/connected = null + + /// Currently stored genetic data diskette var/obj/item/disk/data/diskette = null + + /// Current delayed action, used for delayed enzyme transfer on scanner door close var/list/delayed_action = null -/obj/machinery/computer/scan_consolenew/attackby(obj/item/I, mob/user, params) - if (istype(I, /obj/item/disk/data)) //INSERT SOME DISKETTES - if (!user.transferItemToLoc(I,src)) - return - if(diskette) - diskette.forceMove(drop_location()) - diskette = null - diskette = I - to_chat(user, "You insert [I].") - updateUsrDialog() + /// Index of the enzyme being modified during delayed enzyme pulse operations + var/rad_pulse_index = 0 + /// World time when the enzyme pulse should complete + var/rad_pulse_timer = 0 + + /// Used for setting tgui data - Whether the connected DNA Scanner is usable + var/can_use_scanner = FALSE + /// Used for setting tgui data - Whether the current DNA Scanner occupant is viable for genetic modification + var/is_viable_occupant = FALSE + /// Used for setting tgui data - Whether Scramble DNA is ready + var/is_scramble_ready = FALSE + /// Used for setting tgui data - Whether JOKER algorithm is ready + var/is_joker_ready = FALSE + /// Used for setting tgui data - Whether injectors are ready to be printed + var/is_injector_ready = FALSE + /// Used for setting tgui data - Wheher an enzyme pulse operation is ongoing + var/is_pulsing_rads = FALSE + /// Used for setting tgui data - Time until scramble is ready + var/time_to_scramble = 0 + /// Used for setting tgui data - Time until joker is ready + var/time_to_joker = 0 + /// Used for setting tgui data - Time until injectors are ready + var/time_to_injector = 0 + /// Used for setting tgui data - Time until the enzyme pulse is complete + var/time_to_pulse = 0 + + /// Currently connected DNA Scanner + var/obj/machinery/dna_scannernew/connected_scanner = null + /// Current DNA Scanner occupant + var/mob/living/carbon/scanner_occupant = null + + /// Used for setting tgui data - List of occupant mutations + var/list/tgui_occupant_mutations = list() + /// Used for setting tgui data - List of DNA Console stored mutations + var/list/tgui_console_mutations = list() + /// Used for setting tgui data - List of diskette stored mutations + var/list/tgui_diskette_mutations = list() + /// Used for setting tgui data - List of DNA Console chromosomes + var/list/tgui_console_chromosomes = list() + /// Used for setting tgui data - List of occupant mutations + var/list/tgui_genetic_makeup = list() + /// Used for setting tgui data - List of occupant mutations + var/list/tgui_advinjector_mutations = list() + + + /// State of tgui view, i.e. which tab is currently active, or which genome we're currently looking at. + var/list/list/tgui_view_state = list() + +/obj/machinery/computer/scan_consolenew/process() + . = ..() + + // This is for pulsing the UI element with radiation as part of genetic makeup + // If rad_pulse_index > 0 then it means we're attempting a rad pulse + if((rad_pulse_index > 0) && (rad_pulse_timer <= world.time)) + rad_pulse() return + +/obj/machinery/computer/scan_consolenew/attackby(obj/item/I, mob/user, params) + // Store chromosomes in the console if there's room if (istype(I, /obj/item/chromosome)) if(LAZYLEN(stored_chromosomes) < max_chromosomes) I.forceMove(src) @@ -81,6 +152,21 @@ else to_chat(user, "You cannot store any more chromosomes!") return + + // Insert data disk if console disk slot is empty + // Swap data disk if there is one already a disk in the console + if (istype(I, /obj/item/disk/data)) //INSERT SOME DISKETTES + if (!user.transferItemToLoc(I,src)) + return + if(diskette) + diskette.forceMove(drop_location()) + diskette = null + diskette = I + to_chat(user, "You insert [I].") + return + + // Recycle non-activator used injectors + // Turn activator used injectors (aka research injectors) to chromosomes if(istype(I, /obj/item/dnainjector/activator)) var/obj/item/dnainjector/activator/A = I if(A.used) @@ -101,851 +187,1759 @@ qdel(I) return - else - return ..() + return ..() /obj/machinery/computer/scan_consolenew/Initialize() . = ..() - for(var/direction in GLOB.cardinals) - connected = locate(/obj/machinery/dna_scannernew, get_step(src, direction)) - if(!isnull(connected)) - break + + // Connect with a nearby DNA Scanner on init + connect_to_scanner() + + // Set appropriate ready timers and limits for machines functions injectorready = world.time + INJECTOR_TIMEOUT scrambleready = world.time + SCRAMBLE_TIMEOUT jokerready = world.time + JOKER_TIMEOUT + // Set the default tgui state + set_default_state() + + // Link machine with research techweb. Used for discovering and accessing + // already discovered mutations stored_research = SSresearch.science_tech /obj/machinery/computer/scan_consolenew/examine(mob/user) . = ..() - if(jokerready < world.time) - . += "JOKER algorithm available." - else - . += "JOKER algorithm available in about [round(0.00166666667 * (jokerready - world.time))] minutes." -/obj/machinery/computer/scan_consolenew/ui_interact(mob/user, last_change) +/obj/machinery/computer/scan_consolenew/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) . = ..() - if(!user) - return - var/datum/browser/popup = new(user, "scannernew", "DNA Modifier Console", 800, 630) // Set up the popup browser window - if(user.client) - var/datum/asset/simple/assets = get_asset_datum(/datum/asset/simple/genetics) - assets.send(user.client) - popup.add_stylesheet("scannernew", 'html/browser/scannernew.css') - var/mob/living/carbon/viable_occupant - var/list/occupant_status = list("
Subject Status:
") - var/scanner_status - var/list/temp_html = list() - if(connected && connected.is_operational()) - if(connected.occupant) //set occupant_status message - viable_occupant = connected.occupant - if(viable_occupant.has_dna() && !HAS_TRAIT(viable_occupant, TRAIT_RADIMMUNE) && !HAS_TRAIT(viable_occupant, TRAIT_BADDNA) || (connected.scan_level == 3)) //occupant is viable for dna modification - occupant_status += "[viable_occupant.name] => " - switch(viable_occupant.stat) - if(CONSCIOUS) - occupant_status += "Conscious" - if(UNCONSCIOUS) - occupant_status += "Unconscious" - else - occupant_status += "DEAD" - occupant_status += "
" - occupant_status += "
Health:
[viable_occupant.health] %
" - occupant_status += "
Radiation Level:
[viable_occupant.radiation/(RAD_MOB_SAFE/100)] %
" - occupant_status += "
Unique Enzymes :
[viable_occupant.dna.unique_enzymes]
" - occupant_status += "
Last Operation:
[last_change ? last_change : "----"]
" + // Most of ui_interact is spent setting variables for passing to the tgui + // interface. + // We can also do some general state processing here too as it's a good + // indication that a player is using the console. + + var/scanner_op = scanner_operational() + var/can_modify_occ = can_modify_occupant() + + // Check for connected AND operational scanner. + if(scanner_op) + can_use_scanner = TRUE + else + can_use_scanner = FALSE + connected_scanner = null + is_viable_occupant = FALSE + + // Check for a viable occupant in the scanner. + if(can_modify_occ) + is_viable_occupant = TRUE + else + is_viable_occupant = FALSE + + + // Populates various buffers for passing to tgui + build_mutation_list(can_modify_occ) + build_genetic_makeup_list() + + // Populate variables for passing to tgui interface + is_scramble_ready = (scrambleready < world.time) + time_to_scramble = round((scrambleready - world.time)/10) + + is_joker_ready = (jokerready < world.time) + time_to_joker = round((jokerready - world.time)/10) + + is_injector_ready = (injectorready < world.time) + time_to_injector = round((injectorready - world.time)/10) + + is_pulsing_rads = ((rad_pulse_index > 0) && (rad_pulse_timer > world.time)) + time_to_pulse = round((rad_pulse_timer - world.time)/10) + + // Attempt to update tgui ui, open and update if needed. + ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) + + if(!ui) + ui = new(user, src, ui_key, "scan_consolenew", name, 515, 710, master_ui, state) + ui.open() + +/obj/machinery/computer/scan_consolenew/ui_data(mob/user) + var/list/data = list() + + data["view"] = tgui_view_state + data["storage"] = list() + + // This block of code generates the huge data structure passed to the tgui + // interface for displaying all the various bits of console/scanner data + // Should all be very self-explanatory + data["isScannerConnected"] = can_use_scanner + if(can_use_scanner) + data["scannerOpen"] = connected_scanner.state_open + data["scannerLocked"] = connected_scanner.locked + data["radStrength"] = radstrength + data["radDuration"] = radduration + data["stdDevStr"] = radstrength * RADIATION_STRENGTH_MULTIPLIER + switch(RADIATION_ACCURACY_MULTIPLIER / (radduration + (connected_scanner.precision_coeff ** 2))) //hardcoded values from a z-table for a normal distribution + if(0 to 0.25) + data["stdDevAcc"] = ">95 %" + if(0.25 to 0.5) + data["stdDevAcc"] = "68-95 %" + if(0.5 to 0.75) + data["stdDevAcc"] = "55-68 %" else - viable_occupant = null - occupant_status += "Invalid DNA structure" + data["stdDevAcc"] = "<38 %" + + data["isViableSubject"] = is_viable_occupant + if(is_viable_occupant) + data["subjectName"] = scanner_occupant.name + if(scanner_occupant.transformation_timer) + data["subjectStatus"] = STATUS_TRANSFORMING else - occupant_status += "No subject detected" - - if(connected.state_open) - scanner_status = "Open" - else - scanner_status = "Closed" - if(connected.locked) - scanner_status += "(Locked)" - else - scanner_status += "(Unlocked)" - - + data["subjectStatus"] = scanner_occupant.stat + data["subjectHealth"] = scanner_occupant.health + data["subjectRads"] = scanner_occupant.radiation/(RAD_MOB_SAFE/100) + data["subjectEnzymes"] = scanner_occupant.dna.unique_enzymes + data["isMonkey"] = ismonkey(scanner_occupant) + data["subjectUNI"] = scanner_occupant.dna.uni_identity + data["storage"]["occupant"] = tgui_occupant_mutations + //data["subjectMutations"] = tgui_occupant_mutations else - occupant_status += "----" - scanner_status += "Error: No scanner detected" + data["subjectName"] = null + data["subjectStatus"] = null + data["subjectHealth"] = null + data["subjectRads"] = null + data["subjectEnzymes"] = null + //data["subjectMutations"] = null + data["storage"]["occupant"] = null - var/list/status = list("
") - status += "
Scanner:
[scanner_status]
" - status += occupant_status + data["hasDelayedAction"] = (delayed_action != null) + data["isScrambleReady"] = is_scramble_ready + data["isJokerReady"] = is_joker_ready + data["isInjectorReady"] = is_injector_ready + data["scrambleSeconds"] = time_to_scramble + data["jokerSeconds"] = time_to_joker + data["injectorSeconds"] = time_to_injector + data["isPulsingRads"] = is_pulsing_rads + data["radPulseSeconds"] = time_to_pulse - - status += "

Radiation Emitter Status

" - var/stddev = radstrength*RADIATION_STRENGTH_MULTIPLIER - status += "
Output Level:
[radstrength]
" - status += "
  \> Mutation:
(-[stddev] to +[stddev] = 68 %) (-[2*stddev] to +[2*stddev] = 95 %)
" - if(connected) - stddev = RADIATION_ACCURACY_MULTIPLIER/(radduration + (connected.precision_coeff ** 2)) + if(diskette != null) + data["hasDisk"] = TRUE + data["diskCapacity"] = diskette.max_mutations - LAZYLEN(diskette.mutations) + data["diskReadOnly"] = diskette.read_only + //data["diskMutations"] = tgui_diskette_mutations + data["storage"]["disk"] = tgui_diskette_mutations + data["diskHasMakeup"] = (LAZYLEN(diskette.genetic_makeup_buffer) > 0) + data["diskMakeupBuffer"] = diskette.genetic_makeup_buffer.Copy() else - stddev = RADIATION_ACCURACY_MULTIPLIER/radduration - var/chance_to_hit - switch(stddev) //hardcoded values from a z-table for a normal distribution - if(0 to 0.25) - chance_to_hit = ">95 %" - if(0.25 to 0.5) - chance_to_hit = "68-95 %" - if(0.5 to 0.75) - chance_to_hit = "55-68 %" - else - chance_to_hit = "<38 %" - status += "
Pulse Duration:
[radduration]
" - status += "
  \> Accuracy:
[chance_to_hit]
" - status += "
" // Close statusDisplay div - var/list/buttons = list("Scan") - if(connected) - buttons += "[connected.state_open ? "Close" : "Open"] Scanner" - if (connected.state_open) - buttons += "[connected.locked ? "Unlock" : "Lock"] Scanner" - else - buttons += "[connected.locked ? "Unlock" : "Lock"] Scanner" - else - buttons += "Open Scanner Lock Scanner" - if(viable_occupant && (scrambleready < world.time)) - buttons += "Scramble DNA" - else - buttons += "Scramble DNA" - if(diskette) - buttons += "Disk" - else - buttons += "Disk" - if(current_screen == "mutations") - buttons += "
Mutations" - else - buttons += "
Mutations" - if((current_screen == "mainmenu") || !current_screen) - buttons += "Genetic Sequencer" - else - buttons += "Genetic Sequencer" - if(current_screen == "ui") - buttons += "Unique Identifiers" - else - buttons += "Unique Identifiers" - if(current_screen == "advinjector") - buttons += "Adv. Injectors" - else - buttons += "Adv. Injectors" + data["hasDisk"] = FALSE + data["diskCapacity"] = 0 + data["diskReadOnly"] = TRUE + //data["diskMutations"] = null + data["storage"]["disk"] = null + data["diskHasMakeup"] = FALSE + data["diskMakeupBuffer"] = null - switch(current_screen) - if("working") - temp_html += status - temp_html += "

System Busy

" - temp_html += "Working ... Please wait ([DisplayTimeText(radduration*10)])" - if("ui") - temp_html += status - temp_html += buttons - temp_html += "

Unique Identifiers

" - temp_html += "-- Output Level ++" - temp_html += "
-- Pulse Duration ++" - temp_html += "

Irradiate Subject

" - temp_html += "
Unique Identifier:
" - var/max_line_len = 7*DNA_BLOCK_SIZE - if(viable_occupant) - temp_html += "
1
" - var/char = "" - var/ui_text = viable_occupant.dna.uni_identity - var/len_byte = length(ui_text) - var/char_it = 0 - for(var/byte_it = 1, byte_it <= len_byte, byte_it += length(char)) - char_it++ - char = ui_text[byte_it] - temp_html += "[char]" - if((char_it % max_line_len) == 0) - temp_html += "
" - if((char_it % DNA_BLOCK_SIZE) == 0 && byte_it < len_byte) - temp_html += "
[(char_it / DNA_BLOCK_SIZE) + 1]
" - else - temp_html += "---------" - temp_html += "

Buffer Menu

" + data["mutationCapacity"] = max_storage - LAZYLEN(stored_mutations) + //data["mutationStorage"] = tgui_console_mutations + data["storage"]["console"] = tgui_console_mutations + data["chromoCapacity"] = max_chromosomes - LAZYLEN(stored_chromosomes) + data["chromoStorage"] = tgui_console_chromosomes + data["makeupCapacity"] = NUMBER_OF_BUFFERS + data["makeupStorage"] = tgui_genetic_makeup - if(istype(buffer)) - for(var/i=1, i<=buffer.len, i++) - temp_html += "
Slot [i]: " - var/list/buffer_slot = buffer[i] - if( !buffer_slot || !buffer_slot.len || !buffer_slot["name"] || !((buffer_slot["UI"] && buffer_slot["UE"]) || buffer_slot["SE"]) ) - temp_html += "
\tNo Data" - if(viable_occupant) - temp_html += "
Save to Buffer" - else - temp_html += "
Save to Buffer" - temp_html += "Clear Buffer" - if(diskette) - temp_html += "Load from Disk" - else - temp_html += "Load from Disk" - temp_html += "Save to Disk" - else - var/ui = buffer_slot["UI"] - var/ue = buffer_slot["UE"] - var/name = buffer_slot["name"] - var/label = buffer_slot["label"] - var/blood_type = buffer_slot["blood_type"] - temp_html += "
\tLabel: [label ? label : name]" - temp_html += "
\tSubject: [name]" - if(ue && name && blood_type) - temp_html += "
\tBlood Type: [blood_type]" - temp_html += "
\tUE: [ue] " - if(viable_occupant) - temp_html += "Occupant" - else - temp_html += "Occupant" - temp_html += "Occupant:Delayed" - if(injectorready < world.time) - temp_html += "Injector" - else - temp_html += "Injector" - else - temp_html += "
\tBlood Type: No Data" - temp_html += "
\tUE: No Data" - if(ui) - temp_html += "
\tUI: [ui] " - if(viable_occupant) - temp_html += "Occupant" - else - temp_html += "Occupant" - temp_html += "Occupant:Delayed" - if(injectorready < world.time) - temp_html += "Injector" - else - temp_html += "Injector" - else - temp_html += "
\tUI: No Data" - if(ue && name && blood_type && ui) - temp_html += "
\tUI+UE: [ui]/[ue] " - if(viable_occupant) - temp_html += "Occupant" - else - temp_html += "Occupant" - temp_html += "Occupant:Delayed" - if(injectorready < world.time) - temp_html += "UI+UE Injector" - else - temp_html += "UI+UE Injector" - if(viable_occupant) - temp_html += "
Save to Buffer" - else - temp_html += "
Save to Buffer" - temp_html += "Clear Buffer" - if(diskette) - temp_html += "Load from Disk" - else - temp_html += "Load from Disk" - if(diskette && !diskette.read_only) - temp_html += "Save to Disk" - else - temp_html += "Save to Disk" - if("disk") - temp_html += status - temp_html += buttons - if(diskette) - temp_html += "

[diskette.name]


" - temp_html += "Eject Disk
" - if(LAZYLEN(diskette.mutations)) - temp_html += "" - for(var/datum/mutation/human/A in diskette.mutations) - temp_html += "" - temp_html += "" - if(LAZYLEN(stored_mutations) < max_storage) - temp_html += "" - else - temp_html += "" - temp_html += "" - temp_html += "
[A.name]DeleteImportImport
" - else - temp_html += "
Load diskette to start ----------" - if("info") - if(LAZYLEN(stored_mutations)) - if(LAZYLEN(stored_mutations) >= current_storage) - var/datum/mutation/human/HM = stored_mutations[current_storage] - if(HM) - temp_html += display_sequence(HM.type, current_storage) - else - current_screen = "mainmenu" - if("mutations") - temp_html += status - temp_html += buttons - temp_html += "

Mutation Storage:

" - temp_html += "" - for(var/datum/mutation/human/HM in stored_mutations) - var/i = stored_mutations.Find(HM) - temp_html += "" - if(diskette) - temp_html += "" - else - temp_html += "" - temp_html += "" - if(combine == HM.type) - temp_html += "" - else - temp_html += "" - temp_html += "
[HM.name]ExportExportDeleteCombine
Combine

" - temp_html += "

Chromosome Storage:

" - temp_html += "" - for(var/i in 1 to stored_chromosomes.len) - var/obj/item/chromosome/CM = stored_chromosomes[i] - temp_html += "
" - temp_html += "
[CM.name]
" - if("advinjector") - temp_html += status - temp_html += buttons - temp_html += "
Advanced Injectors:

" - temp_html += "
New Selection
" - for(var/A in injector_selection) - temp_html += "
[A]" - var/list/true_selection = injector_selection[A] - temp_html += "
" - for(var/B in true_selection) - var/datum/mutation/human/HM = B - var/mutcolor - switch(HM.quality) - if(POSITIVE) - mutcolor = "good" - if(MINOR_NEGATIVE) - mutcolor = "average" - if(NEGATIVE) - mutcolor = "bad" - temp_html += "
[HM.name] " - temp_html += "Remove
" - if(injectorready < world.time) - temp_html += "
Print Advanced Injector" - else - temp_html += "
Printer ready in [DisplayTimeText(injectorready - world.time, 1)]" - temp_html += "Remove Injector
" - temp_html += "
" + //data["advInjectors"] = tgui_advinjector_mutations + data["storage"]["injector"] = tgui_advinjector_mutations + data["maxAdvInjectors"] = max_injector_selections - else - temp_html += status - temp_html += buttons - temp_html += "
Genetic Sequence:

" - if(viable_occupant) - if(viable_occupant) - for(var/A in get_mutation_list()) - temp_html += display_inactive_sequence(A) - temp_html += "
" - else - temp_html += "----" - if(viable_occupant && (current_mutation in get_mutation_list(viable_occupant))) - temp_html += display_sequence(current_mutation) - temp_html += "

" - else - temp_html += "----------" + return data - popup.set_content(temp_html.Join()) - popup.open() - -/obj/machinery/computer/scan_consolenew/proc/display_inactive_sequence(mutation) - var/temp_html = "" - var/class = "unselected" - var/mob/living/carbon/viable_occupant = get_viable_occupant() - if(!viable_occupant) - return - - var/location = viable_occupant.dna.mutation_index.Find(mutation) //We do this because we dont want people using sysexp or similair tools to just read the mutations. - - if(!location) //Do this only when needed, dont make a list with mutations for every iteration if you dont need to - var/list/mutations = get_mutation_list(TRUE) - if(mutation in mutations) - location = mutations.Find(mutation) - if(mutation == current_mutation) - class = "selected" - if(location > DNA_MUTATION_BLOCKS) - temp_html += "Extra Mutation" - else if(mutation in stored_research.discovered_mutations) - temp_html += "Discovered Mutation" - else - temp_html += "Undiscovered" - return temp_html - -/obj/machinery/computer/scan_consolenew/proc/display_sequence(mutation, storage_slot) //Storage slot is for when viewing from the stored mutations - var/temp_html = "" - if(!mutation) - temp_html += "ERR-" - return - var/mut_name = "Unknown gene" - var/mut_desc = "No information available." - var/alias - var/discovered = FALSE - var/active = FALSE - var/scrambled = FALSE - var/instability - var/mob/living/carbon/viable_occupant = get_viable_occupant() - var/datum/mutation/human/HM = get_valid_mutation(mutation) - - if(viable_occupant) - var/datum/mutation/human/M = viable_occupant.dna.get_mutation(mutation) - if(M) - scrambled = M.scrambled - active = TRUE - var/datum/mutation/human/A = GET_INITIALIZED_MUTATION(mutation) - alias = A.alias - if(active && !scrambled) - discover(mutation) - if(stored_research && (mutation in stored_research.discovered_mutations)) - mut_name = A.name - mut_desc = A.desc - discovered = TRUE - instability = A.instability - var/extra - if(viable_occupant && !(storage_slot || viable_occupant.dna.mutation_in_sequence(mutation))) - extra = TRUE - if(discovered && !scrambled) - var/mutcolor - switch(A.quality) - if(POSITIVE) - mutcolor = "good" - if(MINOR_NEGATIVE) - mutcolor = "average" - if(NEGATIVE) - mutcolor = "bad" - if(HM) - instability *= GET_MUTATION_STABILIZER(HM) - temp_html += "
[mut_name] ([alias])
" - temp_html += "
Instability : [round(instability)]
" - else - temp_html += "
[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,"DNA scrambled.") - 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,"DNA scrambled.") + 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,"Gene pulse failed: The scanner occupant undergoing a transformation.") + 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,"Mutation storage is full.") + 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,"Mutation successfully stored.") + 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,"Disk storage is full.") + 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,"Disk is set to read only mode.") + 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,"Mutation successfully stored to disk.") + 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,"Mutation succesfully stored.") - if(!succes) //we can exactly return here - to_chat(usr,"Mutation storage is full.") - 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, "Successfully wrote [A.name] to [diskette.name].") - 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,"Successfully wrote [A.name] to storage.") - 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, "Success! New mutation has been added to storage") - discover(result_path) - combine = null - else - to_chat(usr, "Failed. No mutation could be created.") - combine = null - else - combine = path - to_chat(usr,"Selected [A.name] for combining") - else - to_chat(usr, "Not enough space to store potential mutation.") - 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, "You apply [CM] to [HM.name].") - 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, "Adding more mutations would make the advanced injector too unstable!") - 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,"Disk is set to read only mode.") + 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,"Mutation storage is full.") + 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, "Success! New mutation has been added to console storage.") + + // 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,"Disk storage is full.") + 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,"Disk is set to read only mode.") + 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, "Success! New mutation has been added to the disk.") + + // 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,"Disk is set to read only mode.") + 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,"Disk is set to read only mode.") + 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,"Genetic data corrupted, unable to create injector.") + 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,"Genetic data corrupted, unable to create injector.") + 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,"Genetic data corrupted, unable to create injector.") + 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,"Advanced injector mutation storage is full.") + 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,"Extra mutation would make the advanced injector too instable.") + 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,"Mutation successfully added to advanced injector.") + 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,"Genetic data corrupted, unable to apply genetic data.") + 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,"Genetic data corrupted, unable to apply genetic data.") + 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,"Genetic data corrupted, unable to apply genetic data.") + 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, "[src] activates!") + 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, "[src] activates!") - 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, "Your bones clatter and shudder as you are pulled back into this world!") 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 ( - - - {children} - - - ); -}; - -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}> +
+ {children} +
+
); }; 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 ( +