From bf6039215ea021098687bf85ca8373ac65866816 Mon Sep 17 00:00:00 2001 From: Timberpoes Date: Mon, 6 Apr 2020 17:00:19 +0100 Subject: [PATCH] tgui: DNA Console Overhaul Project (#50225) * Initial work on tgui conversion for DNA Consoles * Some initial button logic complete TODO: Scanner Open button should disable while scanner is locked TODO: Scramble DNA should disable while on cooldown * Tabs for the tab God * Dropdown changes, genetic sequencer modifications * Mutation and genetic sequencer functionality Almost complete. Need to implement Advanced Injectors still. * Mostly done, converted most of genetic makeup code * Tactical pocket linting * More changes * Everything except advanced injectors complete * First complete alpha version * Fixed debug/testing change * Removal of more debugging code * Fix pulse bug * Bugbusters * Fix for accidental changes * Fix broken genetics disk changes, fix list of lists * Squash tgui tab-key bugs and clean up attackby * Code documenting, refactoring, bugfixing, spellcheck * Documentation and misc bug squashes and runtime error fixes * Fixes, features and tweaks * Special check for those who have shuffled off this mortal coil * New interface concept for console storage * Mini code refactor * Massive refactoring of DnaConsole interface (WIP) - De-spaghettifying of LawyerCode - Fixed the flex-basis bug, and removed ForcedBox since it's no longer needed. - Added a Divider component. * Implemented requested changes. Removed list() versions of strings. Removed used TGUI-exclusive constants from data object. Currently unused ones still remain, can be removed if they're no longer necessary. Fixed other DM data structures to serialise as JS Arrays instead of Objects. Minor TGUI logic fixes for various edge cases. Added some comments in DnaConsole.js outlining some minor notes and TODOs. * Fixes "Save to X" being disabled. * Included 2px outline CSS * Additional minor logic patches Don't want certain buttons active unless the mutation relation to them is also active * Makes these transforms use a timer * Fixed transformation race condition and stuff. * Mutation source defined in DM. Conditional formatting improvements. * Custom cache directory with BYOND_CACHE env var * Fix gene cycler on index of -1 * Framework and TODO for next interface element. Cleared TODO list a bit. * Fix 1px shift in gene cycle buttons * Pass raw event with GeneCycler * More robust combining logic * Fix some cycler bugs, start working on enzymes * Conditional highlighting for unsolved mutations and X'd genes. * Lint for the Lint God * Enzyme UI and more refactoring incoming * Finish tgui refactoring, enzymes injectors done * Whack-a-Bug * Unlinted. Advanced injectors moved and improved. Implemented disk genetic data readout. * Partial linting * Assorted bug fixes * Remove debugging code * DNA Consoles are now more conversational. Initial state set. * Final tweaks, implemented mutation combining, complete? * Fixes tooltop, re-enables delayed enzyme transfer, cleans up some data params * 10 Fix a bug, 20 make a bug, 30 goto 10 * The definition of irony * Don't drink and derive, kids || How I learned to stop coding while drunk and rely on stylemistake to Flex on my formatting. * uniqBy * Add support for dropdowns with disabilities. * Cleanup * Remove current mutation from combinations * Dividing * Document BYOND_CACHE env var * Outline cleanup * Declare radiation constants since they were removed in DM * Combine mutations only after checking for null * Advanced Injectors actually work now. * Comment cleanup, DMDOC, stard and end processing at appropriate times * Pressing Ctrl-S occasionally helps things * Fix enzyme mutator timeout bug, added injector timeout display. * Rebuild tgui * Rebuild tgui Co-authored-by: Aleksej Komarov Co-authored-by: spookydonut --- _maps/RandomZLevels/research.dmm | 4 +- code/datums/dna.dm | 15 +- code/datums/mutations/_mutations.dm | 18 +- code/game/machinery/computer/dna_console.dm | 2814 +++++++++++------ code/game/machinery/dna_scanner.dm | 11 +- code/game/objects/items/chromosome.dm | 10 +- code/modules/admin/verbs/randomverbs.dm | 2 +- code/modules/hydroponics/grown/replicapod.dm | 2 +- .../mob/living/carbon/carbon_defines.dm | 3 + .../mob/living/carbon/human/species.dm | 2 + code/modules/mob/transform_procs.dm | 99 +- code/modules/spells/spell_types/lichdom.dm | 2 +- tgui/README.md | 29 + tgui/packages/common/collections.js | 87 + tgui/packages/tgui-dev-server/reloader.js | 5 + tgui/packages/tgui/components/Box.js | 19 - tgui/packages/tgui/components/Dimmer.js | 23 +- tgui/packages/tgui/components/Divider.js | 18 + tgui/packages/tgui/components/Dropdown.js | 23 +- tgui/packages/tgui/components/Flex.js | 8 +- tgui/packages/tgui/components/LabeledList.js | 7 +- tgui/packages/tgui/components/index.js | 1 + .../tgui/interfaces/BlackmarketUplink.js | 6 +- tgui/packages/tgui/interfaces/DnaConsole.js | 1429 +++++++++ .../tgui/interfaces/NtosJobManager.js | 3 +- .../tgui/interfaces/NtosSupermatterMonitor.js | 34 +- .../tgui/interfaces/PersonalCrafting.js | 10 +- .../tgui/interfaces/SyndContractor.js | 5 +- tgui/packages/tgui/public/tgui.bundle.css | 2 +- tgui/packages/tgui/public/tgui.bundle.js | 4 +- tgui/packages/tgui/routes.js | 5 + tgui/packages/tgui/styles/atomic/outline.scss | 50 + .../tgui/styles/components/Dimmer.scss | 16 + .../tgui/styles/components/Divider.scss | 20 + .../tgui/styles/components/Dropdown.scss | 11 + .../packages/tgui/styles/components/Flex.scss | 5 +- tgui/packages/tgui/styles/main.scss | 3 + 37 files changed, 3718 insertions(+), 1087 deletions(-) create mode 100644 tgui/packages/tgui/components/Divider.js create mode 100644 tgui/packages/tgui/interfaces/DnaConsole.js create mode 100644 tgui/packages/tgui/styles/atomic/outline.scss create mode 100644 tgui/packages/tgui/styles/components/Dimmer.scss create mode 100644 tgui/packages/tgui/styles/components/Divider.scss diff --git a/_maps/RandomZLevels/research.dmm b/_maps/RandomZLevels/research.dmm index 7991d86d0596d..53efc47ff0fcc 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 e7b6f40e09ad7..ddf53eea4c677 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 5222de9e04260..1ef7f15551721 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 0535ea8aa3463..da5e9f887adce 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,38 +47,114 @@ 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 + /// 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) + stored_chromosomes += I + to_chat(user, "You insert [I].") + 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 @@ -71,16 +163,10 @@ diskette = null diskette = I to_chat(user, "You insert [I].") - updateUsrDialog() - return - if (istype(I, /obj/item/chromosome)) - if(LAZYLEN(stored_chromosomes) < max_chromosomes) - I.forceMove(src) - stored_chromosomes += I - to_chat(user, "You insert [I].") - else - to_chat(user, "You cannot store any more chromosomes!") 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 : "----"]
" - else - viable_occupant = null - occupant_status += "Invalid DNA structure" - 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)" + // 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 - occupant_status += "----" - scanner_status += "Error: No scanner detected" - - var/list/status = list("
") - status += "
Scanner:
[scanner_status]
" - status += occupant_status + can_use_scanner = FALSE + connected_scanner = null + is_viable_occupant = FALSE - - 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)) + // Check for a viable occupant in the scanner. + if(can_modify_occ) + is_viable_occupant = TRUE 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" + 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 + 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 - buttons += "[connected.locked ? "Unlock" : "Lock"] Scanner" + 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 - buttons += "Open Scanner Lock Scanner" - if(viable_occupant && (scrambleready < world.time)) - buttons += "Scramble DNA" + data["subjectName"] = null + data["subjectStatus"] = null + data["subjectHealth"] = null + data["subjectRads"] = null + data["subjectEnzymes"] = null + //data["subjectMutations"] = null + data["storage"]["occupant"] = null + + 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 + + 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 - 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 + + 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 + + //data["advInjectors"] = tgui_advinjector_mutations + data["storage"]["injector"] = tgui_advinjector_mutations + data["maxAdvInjectors"] = max_injector_selections + + return data + +/obj/machinery/computer/scan_consolenew/ui_act(action, var/list/params) + if(..()) + return TRUE - 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

" - - 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 += "" - 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 += "
" + . = TRUE - 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 += "----------" + add_fingerprint(usr) + usr.set_machine(src) - popup.set_content(temp_html.Join()) - popup.open() + 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 -/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 + // Toggle the door open/closed status on attached DNA Scanner + if("toggle_door") + // GUARD CHECK - Scanner still connected and operational? + if(!scanner_operational()) + 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 + connected_scanner.toggle_open(usr) + return -/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 + // Toggle the door bolts on the attached DNA Scanner + if("toggle_lock") + // GUARD CHECK - Scanner still connected and operational? + if(!scanner_operational()) + return -/obj/machinery/computer/scan_consolenew/Topic(href, href_list) - if(..()) - return - if(current_screen == "working") - return + connected_scanner.locked = !connected_scanner.locked + return - add_fingerprint(usr) - usr.set_machine(src) + // 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 + + 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 + + // 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 + + // GUARD CHECK - Have we somehow cheekily swapped occupants? This is + // unexpected. + if(!(scanner_occupant == connected_scanner.occupant)) + return + + check_discovery(params["alias"]) + return - var/mob/living/carbon/viable_occupant = get_viable_occupant() - - //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) - - 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) - - var/locked_state = connected.locked - connected.locked = TRUE - - current_screen = "working" - ui_interact(usr) - - sleep(radduration*10) - current_screen = "ui" - - 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]; " - - var/hex = copytext_char(viable_occupant.dna.uni_identity, num, num+1) - last_change += "[hex]" - hex = scramble(hex, radstrength, radduration) - last_change += "->[hex]" - - 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) + // 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] - - 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] + 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(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 - 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 - - if("remove_advinjector") - var/selection = href_list["injector"] - for(selection in injector_selection) - if(selection == selection) - injector_selection.Remove(selection) - - 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() - - - - ui_interact(usr,last_change) - -/obj/machinery/computer/scan_consolenew/proc/scramble(input,rs,rd) //hexadecimal genetics. dont confuse with scramble button + return + + // 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 + + // 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 + + 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 + + // Ejects a stored chromosome from the DNA Console + // ---------------------------------------------------------------------- // + // params["chromo"] - Text string of the chromosome name + if("eject_chromo") + var/chromname = params["chromo"] + + 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 + + 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) + + 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) + + rad_pulse_index = 0 + end_processing() + return + +/** + * 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_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 - -/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"] -/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 ? - -/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 - - -/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 8525b2063d04d..49b6463b1c123 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 d3a77080bf34b..3acf3cfe5cb38 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 0c83d4e9d1555..79ae060a984c9 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 b1581c9dcee21..020d554cf5349 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 75c635d71de8c..0eeae528bad4a 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 46ed7fcaee5df..650207ad4c698 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 62e66038a0a8a..0e41840f6ee6f 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -1,11 +1,32 @@ +#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 + if(tr_flags & TR_KEEPITEMS) + var/Itemlist = get_equipped_items(TRUE) + Itemlist += held_items + for(var/obj/item/W in Itemlist) + dropItemToGround(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(loc) + + 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() - var/list/int_organs = list() if (tr_flags & TR_KEEPIMPLANTS) for(var/X in implants) @@ -13,8 +34,7 @@ stored_implants += IMP IMP.removed(src, 1, 1) - var/list/missing_bodyparts_zones = get_missing_limbs() - + var/list/int_organs = list() var/obj/item/cavity_object var/obj/item/bodypart/chest/CH = get_bodypart(BODY_ZONE_CHEST) @@ -22,21 +42,6 @@ cavity_object = CH.cavity_item CH.cavity_item = null - if(tr_flags & TR_KEEPITEMS) - var/Itemlist = get_equipped_items(TRUE) - Itemlist += held_items - for(var/obj/item/W in Itemlist) - dropItemToGround(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(loc) - sleep(22) 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 77c7a8e0b7a97..1845fdfadf46b 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 5dabbb2f07abd..5fd6434c1ea9a 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 db0086867d87d..1e5c978942e0f 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 d5541c2a992eb..394e55afdb339 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 c3828450cf25f..b0f56e67e855a 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 9d3ead05493cf..f38d0a026a637 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 0000000000000..5c807c290b8e1 --- /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 ( +