diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm index 2e99af52c3f..dd0ed105efd 100644 --- a/_maps/map_files/Deltastation/DeltaStation2.dmm +++ b/_maps/map_files/Deltastation/DeltaStation2.dmm @@ -33807,10 +33807,11 @@ /turf/open/floor/iron/white, /area/science/research) "cPd" = ( -/obj/structure/chair, /obj/effect/turf_decal/tile/purple{ dir = 4 }, +/obj/machinery/destructive_scanner, +/obj/effect/turf_decal/stripes/end, /turf/open/floor/iron/white, /area/science/research) "cPe" = ( @@ -35295,15 +35296,6 @@ }, /turf/open/floor/iron, /area/security/checkpoint/science/research) -"cSp" = ( -/obj/item/kirbyplants{ - icon_state = "plant-22" - }, -/obj/effect/turf_decal/tile/purple{ - dir = 1 - }, -/turf/open/floor/iron/white, -/area/science/research) "cSq" = ( /obj/structure/disposalpipe/segment{ dir = 5 @@ -36200,6 +36192,10 @@ /obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2{ dir = 6 }, +/obj/effect/turf_decal/arrows/white{ + color = "#800080"; + dir = 8 + }, /turf/open/floor/iron, /area/science/research) "cUh" = ( @@ -41271,6 +41267,13 @@ dir = 6 }, /obj/machinery/power/apc/auto_name/west, +/obj/item/experi_scanner, +/obj/item/experi_scanner{ + pixel_x = 4 + }, +/obj/item/experi_scanner{ + pixel_x = -4 + }, /turf/open/floor/iron, /area/science/lab) "dfr" = ( @@ -149835,7 +149838,7 @@ cmP cNp cOW cQH -cSp +dgv cUe cWb cXr diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm index d5deba4f665..ca185029ab0 100644 --- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm +++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm @@ -12604,6 +12604,7 @@ /obj/effect/turf_decal/tile/purple{ dir = 8 }, +/obj/machinery/lapvend, /turf/open/floor/iron, /area/hallway/primary/starboard) "bfZ" = ( @@ -12615,14 +12616,14 @@ dir = 8 }, /obj/machinery/light, +/obj/effect/turf_decal/tile/purple{ + dir = 4 + }, /turf/open/floor/iron, /area/hallway/primary/starboard) "bga" = ( -/obj/effect/turf_decal/tile/purple, -/obj/effect/turf_decal/tile/purple{ - dir = 8 - }, -/obj/machinery/lapvend, +/obj/machinery/destructive_scanner, +/obj/effect/turf_decal/stripes/end, /turf/open/floor/iron, /area/hallway/primary/starboard) "bgb" = ( @@ -14998,6 +14999,14 @@ /obj/effect/turf_decal/stripes/line{ dir = 1 }, +/obj/item/experi_scanner{ + pixel_x = 4 + }, +/obj/item/experi_scanner, +/obj/item/experi_scanner{ + pixel_x = -4 + }, +/obj/structure/table, /turf/open/floor/iron/white, /area/science/lab) "boB" = ( @@ -31448,6 +31457,13 @@ }, /turf/open/floor/iron, /area/cargo/storage) +"gdd" = ( +/obj/effect/turf_decal/tile/purple{ + dir = 8 + }, +/obj/effect/turf_decal/tile/purple, +/turf/open/floor/iron, +/area/hallway/primary/starboard) "gdg" = ( /obj/structure/mirror{ pixel_y = 28 @@ -56782,6 +56798,16 @@ /obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2, /turf/open/floor/iron/dark, /area/medical/medbay/central) +"xXm" = ( +/obj/effect/turf_decal/tile/purple, +/obj/effect/turf_decal/tile/purple{ + dir = 8 + }, +/obj/effect/turf_decal/tile/purple{ + dir = 1 + }, +/turf/open/floor/iron, +/area/hallway/primary/starboard) "xXp" = ( /turf/closed/wall, /area/medical/break_room) @@ -102378,7 +102404,7 @@ nhQ iMW csr pXx -aYV +bga qXW aYV bfX @@ -103922,8 +103948,8 @@ iwd vVm aYV qXW -aYV -bga +gdd +bfX bgc bgc bgc @@ -104180,7 +104206,7 @@ vVm aYV qXW aYV -bfX +xXm bhC biF bks diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm index a1b813c745f..e34db77aa55 100644 --- a/_maps/map_files/KiloStation/KiloStation.dmm +++ b/_maps/map_files/KiloStation/KiloStation.dmm @@ -11439,18 +11439,12 @@ /obj/effect/turf_decal/tile/neutral{ dir = 1 }, -/obj/structure/table, -/obj/item/paper_bin{ - pixel_x = -4; - pixel_y = 4 - }, -/obj/item/pen, /obj/effect/turf_decal/stripes/corner{ dir = 1 }, -/obj/machinery/camera{ - c_tag = "Starboard Hallway Research Desk"; - name = "starboard camera" +/obj/machinery/destructive_scanner, +/obj/effect/turf_decal/stripes/line{ + dir = 10 }, /turf/open/floor/iron/dark, /area/hallway/primary/starboard) @@ -21774,6 +21768,13 @@ /obj/structure/extinguisher_cabinet{ pixel_y = -30 }, +/obj/item/experi_scanner{ + pixel_x = -4 + }, +/obj/item/experi_scanner, +/obj/item/experi_scanner{ + pixel_x = 4 + }, /turf/open/floor/iron/dark, /area/science/lab) "aZl" = ( @@ -23477,6 +23478,11 @@ }, /obj/machinery/atmospherics/pipe/simple/supply/hidden/layer4, /obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer2, +/obj/machinery/camera{ + c_tag = "Starboard Hallway Research Desk"; + dir = 4; + name = "starboard camera" + }, /turf/open/floor/iron, /area/hallway/primary/starboard) "bcn" = ( diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm index 75013e4b495..767840a4266 100644 --- a/_maps/map_files/MetaStation/MetaStation.dmm +++ b/_maps/map_files/MetaStation/MetaStation.dmm @@ -33699,7 +33699,6 @@ /obj/effect/turf_decal/tile/purple{ dir = 4 }, -/obj/item/kirbyplants/random, /turf/open/floor/iron/white, /area/science/research) "euv" = ( @@ -34973,6 +34972,7 @@ dir = 1 }, /obj/effect/turf_decal/siding/purple, +/obj/item/kirbyplants/random, /turf/open/floor/iron/white, /area/science/research) "eRR" = ( @@ -43063,16 +43063,16 @@ /turf/open/floor/wood, /area/command/heads_quarters/captain/private) "ikF" = ( -/obj/structure/window/reinforced{ - dir = 4 - }, /obj/effect/turf_decal/trimline/purple/warning{ dir = 6 }, /obj/effect/turf_decal/trimline/purple/warning{ dir = 6 }, -/obj/effect/turf_decal/siding/purple, +/obj/machinery/destructive_scanner, +/obj/effect/turf_decal/siding/purple{ + dir = 6 + }, /turf/open/floor/iron/white, /area/science/research) "ikK" = ( @@ -43467,6 +43467,9 @@ /area/commons/vacant_room/commissary) "isH" = ( /obj/structure/table/glass, +/obj/item/experi_scanner{ + pixel_x = -4 + }, /turf/open/floor/iron/white, /area/science/research) "isV" = ( @@ -51975,6 +51978,16 @@ }, /turf/open/floor/engine/vacuum, /area/engineering/atmos) +"lXi" = ( +/obj/structure/table/glass, +/obj/item/experi_scanner{ + pixel_x = 4 + }, +/obj/item/experi_scanner{ + pixel_x = -15 + }, +/turf/open/floor/iron/white, +/area/science/research) "lXu" = ( /obj/item/radio/intercom{ pixel_y = -29 @@ -114559,7 +114572,7 @@ glk vou uwX jeM -isH +lXi vLG dvC hSZ diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index a9b37740060..33157167745 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -49,6 +49,9 @@ /// handler for vv_do_topic (usr, href_list) #define COMSIG_VV_TOPIC "vv_topic" #define COMPONENT_VV_HANDLED (1<<0) +/// from datum ui_act (usr, action) +#define COMSIG_UI_ACT "COMSIG_UI_ACT" + /// fires on the target datum when an element is attached to it (/datum/element) #define COMSIG_ELEMENT_ATTACH "element_attach" @@ -147,6 +150,9 @@ ///from obj/machinery/bsa/full/proc/fire(): () #define COMSIG_ATOM_BSA_BEAM "atom_bsa_beam_pass" #define COMSIG_ATOM_BLOCKS_BSA_BEAM (1<<0) +///from /obj/machinery/doppler_array/proc/sense_explosion(...): Runs when an explosion is succesfully detected by a doppler array(turf/epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range) +#define COMSIG_DOPPLER_ARRAY_EXPLOSION_DETECTED "atom_dopplerarray_explosion_detected" + ///from base of atom/setDir(): (old_dir, new_dir). Called before the direction changes. #define COMSIG_ATOM_DIR_CHANGE "atom_dir_change" ///from base of atom/handle_atom_del(): (atom/deleted) @@ -270,6 +276,8 @@ #define COMSIG_LIVING_START_PULL "living_start_pull" ///called on /living when someone is pulled (mob/living/puller) #define COMSIG_LIVING_GET_PULLED "living_start_pulled" +///from base of [/atom/proc/interact]: (mob/user) +#define COMSIG_ATOM_UI_INTERACT "atom_ui_interact" /// from /datum/component/singularity/proc/can_move(), as well as /obj/energy_ball/proc/can_move() /// if a callback returns `SINGULARITY_TRY_MOVE_BLOCK`, then the singularity will not move to that turf @@ -588,6 +596,10 @@ #define COMSIG_MACHINERY_POWER_RESTORED "machinery_power_restored" ///from /obj/machinery/set_occupant(atom/movable/O): (new_occupant) #define COMSIG_MACHINERY_SET_OCCUPANT "machinery_set_occupant" +///from /obj/machinery/destructive_scanner/proc/open(aggressive): Runs when the destructive scanner scans a group of objects. (list/scanned_atoms) +#define COMSIG_MACHINERY_DESTRUCTIVE_SCAN "machinery_destructive_scan" +///from /obj/machinery/computer/arcade/prizevend(mob/user, prizes = 1) +#define COMSIG_ARCADE_PRIZEVEND "arcade_prizevend" ///from obj/machinery/iv_drip/IV_attach(target, usr) : (attachee) #define COMSIG_IV_ATTACH "iv_attach" diff --git a/code/__DEFINES/experisci.dm b/code/__DEFINES/experisci.dm new file mode 100644 index 00000000000..9130e0ae565 --- /dev/null +++ b/code/__DEFINES/experisci.dm @@ -0,0 +1,35 @@ +#define EXPERIMENT_CONFIG_ATTACKSELF "experiment_config_attackself" +#define EXPERIMENT_CONFIG_ALTCLICK "experiment_config_altclick" +#define EXPERIMENT_CONFIG_CLICK "experiment_config_click" +#define EXPERIMENT_CONFIG_UI "experiment_config_ui" + +/// Boolean stage, complete/incomplete. No specific progress to report. +#define EXPERIMENT_BOOL_STAGE "bool" +/// Integer stages, should be whole numbers with total being included +/// to support rendering ``value of total``, or something akin to it. +#define EXPERIMENT_INT_STAGE "integer" +/// Float stages, the value should be between 0 and 1 representing percent completion +#define EXPERIMENT_FLOAT_STAGE "float" +/// Detail stages, only provide more textual information and have no inherent progress +#define EXPERIMENT_DETAIL_STAGE "detail" + +/// Macro for defining a progress stage +#define EXPERIMENT_PROGRESS(type, desc, values...) list(list(type, desc, values)) +/// Macro for boolean stages +#define EXPERIMENT_PROG_BOOL(desc, complete) EXPERIMENT_PROGRESS(EXPERIMENT_BOOL_STAGE, desc, complete) +/// Macro for integer stages +#define EXPERIMENT_PROG_INT(desc, complete, total) EXPERIMENT_PROGRESS(EXPERIMENT_INT_STAGE, desc, complete, total) +/// Macro for float stages +#define EXPERIMENT_PROG_FLOAT(desc, complete) EXPERIMENT_PROGRESS(EXPERIMENT_FLOAT_STAGE, desc, complete) +/// Macro for non-valued stages, details for exp stages +#define EXPERIMENT_PROG_DETAIL(desc, complete) EXPERIMENT_PROGRESS(EXPERIMENT_DETAIL_STAGE, desc, complete) + +/// Destructive experiments which will destroy the sample +#define EXPERIMENT_TRAIT_DESTRUCTIVE (1 << 0) + +/// Will always attempt to action every experiment eligible with a single input, +/// no experiment selection required +#define EXPERIMENT_CONFIG_ALWAYS_ACTIVE (1 << 0) +/// Experiment handlers with this flag will not automatically connect to the first techweb they find +/// on initialization +#define EXPERIMENT_CONFIG_NO_AUTOCONNECT (1 << 1) diff --git a/code/__DEFINES/research.dm b/code/__DEFINES/research.dm index 218a66a3a31..d8db678bc29 100644 --- a/code/__DEFINES/research.dm +++ b/code/__DEFINES/research.dm @@ -1,36 +1,5 @@ -//! Defines for the R&D console, see: [/obj/machinery/computer/rdconsole] -#define RDCONSOLE_UI_MODE_NORMAL 1 -#define RDCONSOLE_UI_MODE_EXPERT 2 -#define RDCONSOLE_UI_MODE_LIST 3 - -#define RDSCREEN_MENU 0 -#define RDSCREEN_TECHDISK 1 -#define RDSCREEN_DESIGNDISK 20 -#define RDSCREEN_DESIGNDISK_UPLOAD 21 -#define RDSCREEN_SETTINGS 61 -#define RDSCREEN_TECHWEB 70 -#define RDSCREEN_TECHWEB_NODEVIEW 71 -#define RDSCREEN_TECHWEB_DESIGNVIEW 72 - #define RDSCREEN_NOBREAK "" -//! Sanity check defines for when these devices aren't connected or no disk is inserted -#define RDSCREEN_TEXT_NO_PROTOLATHE "

No Protolathe Linked!


" -#define RDSCREEN_TEXT_NO_IMPRINTER "

No Circuit Imprinter Linked!


" -#define RDSCREEN_TEXT_NO_DECONSTRUCT "

No Destructive Analyzer Linked!


" -#define RDSCREEN_TEXT_NO_TDISK "

No Technology Disk Inserted!


" -#define RDSCREEN_TEXT_NO_DDISK "

No Design Disk Inserted!


" -#define RDSCREEN_TEXT_NO_SNODE "

No Technology Node Selected!


" -#define RDSCREEN_TEXT_NO_SDESIGN "

No Design Selected!


" - -#define RDSCREEN_UI_LATHE_CHECK if(QDELETED(linked_lathe)) { return RDSCREEN_TEXT_NO_PROTOLATHE } -#define RDSCREEN_UI_IMPRINTER_CHECK if(QDELETED(linked_imprinter)) { return RDSCREEN_TEXT_NO_IMPRINTER } -#define RDSCREEN_UI_DECONSTRUCT_CHECK if(QDELETED(linked_destroy)) { return RDSCREEN_TEXT_NO_DECONSTRUCT } -#define RDSCREEN_UI_TDISK_CHECK if(QDELETED(t_disk)) { return RDSCREEN_TEXT_NO_TDISK } -#define RDSCREEN_UI_DDISK_CHECK if(QDELETED(d_disk)) { return RDSCREEN_TEXT_NO_DDISK } -#define RDSCREEN_UI_SNODE_CHECK if(!selected_node) { return RDSCREEN_TEXT_NO_SNODE } -#define RDSCREEN_UI_SDESIGN_CHECK if(!selected_design) { return RDSCREEN_TEXT_NO_SDESIGN } - //! Defines for the Protolathe screens, see: [/obj/machinery/rnd/production/protolathe] #define RESEARCH_FABRICATOR_SCREEN_MAIN 1 #define RESEARCH_FABRICATOR_SCREEN_CHEMICALS 2 @@ -63,17 +32,8 @@ TECHWEB_POINT_TYPE_NANITES = "Nanite Research"\ ) -/// R&D point value for a maxcap bomb. Can be adjusted if need be. Current Value Cap Radius: 100 -#define TECHWEB_BOMB_POINTCAP 50000 - -//! Research point values for slime extracts, see: [xenobio_camera][code/modules/research/xenobiology/xenobio_camera.dm] -#define SLIME_RESEARCH_TIER_0 100 -#define SLIME_RESEARCH_TIER_1 500 -#define SLIME_RESEARCH_TIER_2 1000 -#define SLIME_RESEARCH_TIER_3 1500 -#define SLIME_RESEARCH_TIER_4 2000 -#define SLIME_RESEARCH_TIER_5 2500 -#define SLIME_RESEARCH_TIER_RAINBOW 5000 +/// Amount of cash you can get from a maxcap +#define TECHWEB_BOMB_CASHCAP 50000 //! Amount of points gained per second by a single R&D server, see: [research][code/controllers/subsystem/research.dm] #define TECHWEB_SINGLE_SERVER_INCOME 52.3 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 2e750703a8f..bb13f919cfb 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -149,6 +149,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_TESLA_SHOCKIMMUNE "tesla_shock_immunity" #define TRAIT_STABLEHEART "stable_heart" #define TRAIT_STABLELIVER "stable_liver" +#define TRAIT_VATGROWN "vatgrown" #define TRAIT_RESISTHEAT "resist_heat" #define TRAIT_RESISTHEATHANDS "resist_heat_handsonly" //For when you want to be able to touch hot things, but still want fire to be an issue. #define TRAIT_RESISTCOLD "resist_cold" diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm index c7551c30044..d16673f008a 100644 --- a/code/_globalvars/lists/objects.dm +++ b/code/_globalvars/lists/objects.dm @@ -35,6 +35,7 @@ GLOBAL_LIST_EMPTY(meteor_list) // List of all meteors. GLOBAL_LIST_EMPTY(active_jammers) // List of active radio jammers GLOBAL_LIST_EMPTY(ladders) GLOBAL_LIST_EMPTY(trophy_cases) +GLOBAL_LIST_EMPTY(experiment_handlers) ///This is a global list of all signs you can change an existing sign or new sign backing to, when using a pen on them. GLOBAL_LIST_EMPTY(editable_sign_types) diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index 9045e4077d8..b58cfcff91b 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -93,6 +93,7 @@ else add_fingerprint(user) if(interaction_flags_atom & INTERACT_ATOM_UI_INTERACT) + SEND_SIGNAL(src, COMSIG_ATOM_UI_INTERACT, user) return ui_interact(user) return FALSE diff --git a/code/controllers/subsystem/research.dm b/code/controllers/subsystem/research.dm index bce3168049e..368503a091e 100644 --- a/code/controllers/subsystem/research.dm +++ b/code/controllers/subsystem/research.dm @@ -30,10 +30,8 @@ SUBSYSTEM_DEF(research) ) var/list/errored_datums = list() var/list/point_types = list() //typecache style type = TRUE list - var/list/slime_already_researched = list() //Slime cores that have already been researched //---------------------------------------------- var/list/single_server_income = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_SINGLE_SERVER_INCOME) - var/multiserver_calculation = FALSE var/last_income //^^^^^^^^ ALL OF THESE ARE PER SECOND! ^^^^^^^^ @@ -65,18 +63,10 @@ SUBSYSTEM_DEF(research) /datum/controller/subsystem/research/fire() var/list/bitcoins = list() - if(multiserver_calculation) - var/eff = calculate_server_coefficient() - for(var/obj/machinery/rnd/server/miner in servers) - var/list/result = (miner.mine()) //SLAVE AWAY, SLAVE. - for(var/i in result) - result[i] *= eff - bitcoins[i] = bitcoins[i]? bitcoins[i] + result[i] : result[i] - else - for(var/obj/machinery/rnd/server/miner in servers) - if(miner.working) - bitcoins = single_server_income.Copy() - break //Just need one to work. + for(var/obj/machinery/rnd/server/miner in servers) + if(miner.working) + bitcoins = single_server_income.Copy() + break //Just need one to work. if (!isnull(last_income)) var/income_time_difference = world.time - last_income science_tech.last_bitcoins = bitcoins // Doesn't take tick drift into account diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm index 186b656da99..819078e5ad5 100644 --- a/code/datums/materials/_material.dm +++ b/code/datums/materials/_material.dm @@ -16,7 +16,7 @@ Simple datum which is instanced once per type and is used for every object of sa ///Base color of the material, is used for greyscale. Item isn't changed in color if this is null. var/color ///Base alpha of the material, is used for greyscale icons. - var/alpha + var/alpha = 255 ///Bitflags that influence how SSmaterials handles this material. var/init_flags = MATERIAL_INIT_MAPLOAD ///Materials "Traits". its a map of key = category | Value = Bool. Used to define what it can be used for @@ -163,8 +163,8 @@ Simple datum which is instanced once per type and is used for every object of sa o.throwforce = initial(o.throwforce) /datum/material/proc/on_removed_turf(turf/T, amount, material_flags) - if(alpha) - RemoveElement(/datum/element/turf_z_transparency, FALSE) + if(alpha < 255) + T.RemoveElement(/datum/element/turf_z_transparency, FALSE) /** * This proc is called when the mat is found in an item that's consumed by accident. see /obj/item/proc/on_accidental_consumption. diff --git a/code/game/machinery/computer/arcade.dm b/code/game/machinery/computer/arcade.dm index 89ca674d420..bb6188ad3a1 100644 --- a/code/game/machinery/computer/arcade.dm +++ b/code/game/machinery/computer/arcade.dm @@ -77,6 +77,7 @@ GLOBAL_LIST_INIT(arcade_prize_pool, list( Reset() /obj/machinery/computer/arcade/proc/prizevend(mob/user, prizes = 1) + SEND_SIGNAL(user, COMSIG_ARCADE_PRIZEVEND, user, prizes) if(user.mind?.get_skill_level(/datum/skill/gaming) >= SKILL_LEVEL_LEGENDARY && HAS_TRAIT(user, TRAIT_GAMERGOD)) visible_message("[user] inputs an intense cheat code!",\ "You hear a flurry of buttons being pressed.") diff --git a/code/game/machinery/doppler_array.dm b/code/game/machinery/doppler_array.dm index df609d0edea..e6f0c098474 100644 --- a/code/game/machinery/doppler_array.dm +++ b/code/game/machinery/doppler_array.dm @@ -144,12 +144,7 @@ return FALSE next_announce = world.time + cooldown - var/distance = get_dist(epicenter, zone) - var/direct = get_dir(zone, epicenter) - - if(distance > max_dist) - return FALSE - if(!(direct & dir)) + if((get_dist(epicenter, zone) > max_dist) || !(get_dir(zone, epicenter) & dir)) return FALSE var/datum/data/tachyon_record/R = new /datum/data/tachyon_record() @@ -178,12 +173,13 @@ record_number++ records += R + + SEND_SIGNAL(src, COMSIG_DOPPLER_ARRAY_EXPLOSION_DETECTED, epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range) + return TRUE /obj/machinery/doppler_array/powered() - if(!anchored) - return FALSE - return ..() + return anchored && ..() /obj/machinery/doppler_array/update_icon_state() if(machine_stat & BROKEN) @@ -194,44 +190,64 @@ /obj/machinery/doppler_array/research name = "tachyon-doppler research array" - desc = "A specialized tachyon-doppler bomb detection array that uses the results of the highest yield of explosions for research." + desc = "A specialized tachyon-doppler bomb detection array that uses complex on-board software to record data for experiments." + circuit = /obj/item/circuitboard/machine/doppler_array var/datum/techweb/linked_techweb +/obj/machinery/doppler_array/research/Initialize() + ..() + linked_techweb = SSresearch.science_tech + return INITIALIZE_HINT_LATELOAD + +// Late initialize to allow the server machinery to initialize first +/obj/machinery/doppler_array/research/LateInitialize() + . = ..() + AddComponent(/datum/component/experiment_handler, \ + allowed_experiments = list(/datum/experiment/explosion), \ + config_mode = EXPERIMENT_CONFIG_UI, \ + config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE) + +/obj/machinery/doppler_array/research/attackby(obj/item/I, mob/user, params) + if (default_deconstruction_screwdriver(user, "tdoppler", "tdoppler", I) || default_deconstruction_crowbar(I)) + update_icon() + return + return ..() + /obj/machinery/doppler_array/research/sense_explosion(datum/source, turf/epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range) //probably needs a way to ignore admin explosives later on . = ..() if(!.) return + if(!istype(linked_techweb)) - say("Warning: No linked research system!") + say("Warning: no linked research system!") return - var/point_gain = 0 + var/cash_gain = 0 + /*****The Point Calculator*****/ if(orig_light_range < 10) - say("Explosion not large enough for research calculations.") + say("Explosion not large enough for profitability.") return else if(orig_light_range < 4500) - point_gain = (83300 * orig_light_range) / (orig_light_range + 3000) + cash_gain = (83300 * orig_light_range) / (orig_light_range + 3000) else - point_gain = TECHWEB_BOMB_POINTCAP + cash_gain = TECHWEB_BOMB_CASHCAP /*****The Point Capper*****/ - if(point_gain > linked_techweb.largest_bomb_value) - if(point_gain <= TECHWEB_BOMB_POINTCAP || linked_techweb.largest_bomb_value < TECHWEB_BOMB_POINTCAP) + if(cash_gain > linked_techweb.largest_bomb_value) + if(cash_gain <= TECHWEB_BOMB_CASHCAP || linked_techweb.largest_bomb_value < TECHWEB_BOMB_CASHCAP) var/old_tech_largest_bomb_value = linked_techweb.largest_bomb_value //held so we can pull old before we do math - linked_techweb.largest_bomb_value = point_gain - point_gain -= old_tech_largest_bomb_value - point_gain = min(point_gain,TECHWEB_BOMB_POINTCAP) + linked_techweb.largest_bomb_value = cash_gain + cash_gain -= old_tech_largest_bomb_value + cash_gain = min(cash_gain,TECHWEB_BOMB_CASHCAP) else - linked_techweb.largest_bomb_value = TECHWEB_BOMB_POINTCAP - point_gain = 1000 + linked_techweb.largest_bomb_value = TECHWEB_BOMB_CASHCAP + cash_gain = 1000 var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_SCI) if(D) - D.adjust_money(point_gain) - linked_techweb.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, point_gain) - say("Explosion details and mixture analyzed and sold to the highest bidder for [point_gain] cr, with a reward of [point_gain] points.") - + D.adjust_money(cash_gain) + say("Explosion details and mixture analyzed and sold to the highest bidder for [cash_gain] cr.") else //you've made smaller bombs say("Data already captured. Aborting.") return @@ -240,4 +256,13 @@ . = ..() linked_techweb = SSresearch.science_tech +/obj/machinery/doppler_array/research/ui_data(mob/user) + . = ..() + .["is_research"] = TRUE + +/obj/machinery/doppler_array/research/ui_act(action, list/params) + . = ..() + if (.) + return + #undef PRINTER_TIMEOUT diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm index 8ef417d6ea9..96985a18816 100644 --- a/code/game/objects/items/circuitboards/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm @@ -1236,3 +1236,19 @@ /obj/item/stock_parts/scanning_module = 2 ) +/obj/item/circuitboard/machine/destructive_scanner + name = "Experimental Destructive Scanner (Machine Board)" + icon_state = "science" + build_path = /obj/machinery/destructive_scanner + req_components = list( + /obj/item/stock_parts/micro_laser = 2, + /obj/item/stock_parts/matter_bin = 1, + /obj/item/stock_parts/manipulator = 2) + +/obj/item/circuitboard/machine/doppler_array + name = "Tachyon-Doppler Research Array (Machine Board)" + icon_state = "science" + build_path = /obj/machinery/doppler_array/research + req_components = list( + /obj/item/stock_parts/micro_laser = 2, + /obj/item/stock_parts/scanning_module = 4) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 7c03dfab1e6..721bfff1c60 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -186,6 +186,7 @@ . = ..() if(.) return + SEND_SIGNAL(src, COMSIG_ATOM_UI_INTERACT, user) ui_interact(user) /mob/proc/unset_machine() diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 9f66c42fd18..8549656943f 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -309,6 +309,7 @@ icon = 'icons/obj/smooth_structures/glass_table.dmi' icon_state = "glass_table-0" base_icon_state = "glass_table" + custom_materials = list(/datum/material/glass = 2000) buildstack = /obj/item/stack/sheet/glass smoothing_groups = list(SMOOTH_GROUP_GLASS_TABLES) canSmoothWith = list(SMOOTH_GROUP_GLASS_TABLES) @@ -558,6 +559,7 @@ can_buckle = 1 buckle_lying = NO_BUCKLE_LYING buckle_requires_restraints = TRUE + custom_materials = list(/datum/material/silver = 2000) var/mob/living/carbon/human/patient = null var/obj/machinery/computer/operating/computer = null diff --git a/code/game/turfs/closed/wall/mineral_walls.dm b/code/game/turfs/closed/wall/mineral_walls.dm index d54fe013525..f1bc8bf8e32 100644 --- a/code/game/turfs/closed/wall/mineral_walls.dm +++ b/code/game/turfs/closed/wall/mineral_walls.dm @@ -17,6 +17,7 @@ explosion_block = 0 //gold is a soft metal you dingus. smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_GOLD_WALLS) canSmoothWith = list(SMOOTH_GROUP_GOLD_WALLS) + custom_materials = list(/datum/material/gold = 4000) /turf/closed/wall/mineral/silver name = "silver wall" @@ -28,6 +29,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_SILVER_WALLS) canSmoothWith = list(SMOOTH_GROUP_SILVER_WALLS) + custom_materials = list(/datum/material/silver = 4000) /turf/closed/wall/mineral/diamond name = "diamond wall" @@ -41,6 +43,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_DIAMOND_WALLS) canSmoothWith = list(SMOOTH_GROUP_DIAMOND_WALLS) + custom_materials = list(/datum/material/diamond = 4000) /turf/closed/wall/mineral/diamond/hulk_recoil(obj/item/bodypart/arm, mob/living/carbon/human/hulkman, damage = 41) return ..() @@ -55,6 +58,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_BANANIUM_WALLS) canSmoothWith = list(SMOOTH_GROUP_BANANIUM_WALLS) + custom_materials = list(/datum/material/bananium = 4000) /turf/closed/wall/mineral/sandstone name = "sandstone wall" @@ -67,6 +71,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_SANDSTONE_WALLS) canSmoothWith = list(SMOOTH_GROUP_SANDSTONE_WALLS) + custom_materials = list(/datum/material/sandstone = 4000) /turf/closed/wall/mineral/uranium article = "a" @@ -79,6 +84,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_URANIUM_WALLS) canSmoothWith = list(SMOOTH_GROUP_URANIUM_WALLS) + custom_materials = list(/datum/material/uranium = 4000) /turf/closed/wall/mineral/uranium/proc/radiate() if(!active) @@ -118,6 +124,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_PLASMA_WALLS) canSmoothWith = list(SMOOTH_GROUP_PLASMA_WALLS) + custom_materials = list(/datum/material/plasma = 4000) /turf/closed/wall/mineral/plasma/attackby(obj/item/W, mob/user, params) if(W.get_temperature() > 300)//If the temperature of the object is over 300, then ignite @@ -160,6 +167,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_WOOD_WALLS) canSmoothWith = list(SMOOTH_GROUP_WOOD_WALLS) + custom_materials = list(/datum/material/wood = 4000) /turf/closed/wall/mineral/wood/attackby(obj/item/W, mob/user) if(W.get_sharpness() && W.force) @@ -189,6 +197,7 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_IRON_WALLS) canSmoothWith = list(SMOOTH_GROUP_IRON_WALLS) + custom_materials = list(/datum/material/iron = 4000) /turf/closed/wall/mineral/snow name = "packed snow wall" @@ -205,6 +214,7 @@ girder_type = null bullet_sizzle = TRUE bullet_bounce_sound = null + custom_materials = list(/datum/material/snow = 4000) /turf/closed/wall/mineral/snow/hulk_recoil(obj/item/bodypart/arm, mob/living/carbon/human/hulkman, damage = 0) return ..() //No recoil damage, snow is weak @@ -221,6 +231,7 @@ smoothing_flags = SMOOTH_BITMASK | SMOOTH_DIAGONAL_CORNERS smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_ABDUCTOR_WALLS) canSmoothWith = list(SMOOTH_GROUP_ABDUCTOR_WALLS) + custom_materials = list(/datum/material/alloy/alien = 4000) /////////////////////Titanium walls///////////////////// @@ -237,6 +248,7 @@ smoothing_flags = SMOOTH_BITMASK | SMOOTH_DIAGONAL_CORNERS smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_TITANIUM_WALLS) canSmoothWith = list(SMOOTH_GROUP_TITANIUM_WALLS, SMOOTH_GROUP_AIRLOCK, SMOOTH_GROUP_SHUTTLE_PARTS) + custom_materials = list(/datum/material/titanium = 4000) /turf/closed/wall/mineral/titanium/rust_heretic_act() return // titanium does not rust @@ -312,6 +324,7 @@ smoothing_flags = SMOOTH_BITMASK | SMOOTH_DIAGONAL_CORNERS smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS, SMOOTH_GROUP_PLASTITANIUM_WALLS) canSmoothWith = list(SMOOTH_GROUP_PLASTITANIUM_WALLS, SMOOTH_GROUP_SYNDICATE_WALLS, SMOOTH_GROUP_AIRLOCK, SMOOTH_GROUP_SHUTTLE_PARTS) + custom_materials = list(/datum/material/alloy/plastitanium = 4000) /turf/closed/wall/mineral/plastitanium/rust_heretic_act() return // plastitanium does not rust diff --git a/code/game/turfs/open/floor/mineral_floor.dm b/code/game/turfs/open/floor/mineral_floor.dm index 00fd3e7c62b..5e0f2d96afc 100644 --- a/code/game/turfs/open/floor/mineral_floor.dm +++ b/code/game/turfs/open/floor/mineral_floor.dm @@ -35,6 +35,7 @@ icon_state = "plasma" floor_tile = /obj/item/stack/tile/mineral/plasma icons = list("plasma","plasma_dam") + custom_materials = list(/datum/material/plasma = 500) /turf/open/floor/mineral/plasma/temperature_expose(datum/gas_mixture/air, exposed_temperature) if(exposed_temperature > 300) @@ -69,6 +70,7 @@ icon_state = "gold" floor_tile = /obj/item/stack/tile/mineral/gold icons = list("gold","gold_dam") + custom_materials = list(/datum/material/gold = 500) //SILVER @@ -77,6 +79,7 @@ icon_state = "silver" floor_tile = /obj/item/stack/tile/mineral/silver icons = list("silver","silver_dam") + custom_materials = list(/datum/material/silver = 500) //TITANIUM (shuttle) @@ -84,6 +87,7 @@ name = "shuttle floor" icon_state = "titanium" floor_tile = /obj/item/stack/tile/mineral/titanium + custom_materials = list(/datum/material/titanium = 500) /turf/open/floor/mineral/titanium/setup_broken_states() return list("titanium_dam1","titanium_dam2","titanium_dam3","titanium_dam4","titanium_dam5") @@ -167,6 +171,7 @@ name = "shuttle floor" icon_state = "plastitanium" floor_tile = /obj/item/stack/tile/mineral/plastitanium + custom_materials = list(/datum/material/alloy/plastitanium = 500) /turf/open/floor/mineral/plastitanium/setup_broken_states() return list("plastitanium_dam1","plastitanium_dam2","plastitanium_dam3","plastitanium_dam4","plastitanium_dam5") @@ -193,6 +198,7 @@ icon_state = "bananium" floor_tile = /obj/item/stack/tile/mineral/bananium icons = list("bananium","bananium_dam") + custom_materials = list(/datum/material/bananium = 500) var/sound_cooldown = 0 /turf/open/floor/mineral/bananium/Entered(atom/movable/AM) @@ -236,6 +242,7 @@ icon_state = "diamond" floor_tile = /obj/item/stack/tile/mineral/diamond icons = list("diamond","diamond_dam") + custom_materials = list(/datum/material/diamond = 500) //URANIUM @@ -245,6 +252,7 @@ icon_state = "uranium" floor_tile = /obj/item/stack/tile/mineral/uranium icons = list("uranium","uranium_dam") + custom_materials = list(/datum/material/uranium = 500) var/last_event = 0 var/active = null @@ -287,6 +295,7 @@ floor_tile = /obj/item/stack/tile/mineral/abductor icons = list("alienpod1", "alienpod2", "alienpod3", "alienpod4", "alienpod5", "alienpod6", "alienpod7", "alienpod8", "alienpod9") baseturfs = /turf/open/floor/plating/abductor2 + custom_materials = list(/datum/material/alloy/alien = 500) /turf/open/floor/mineral/abductor/Initialize() . = ..() diff --git a/code/modules/antagonists/abductor/equipment/abduction_gear.dm b/code/modules/antagonists/abductor/equipment/abduction_gear.dm index f6863fa78fd..b602beaf73c 100644 --- a/code/modules/antagonists/abductor/equipment/abduction_gear.dm +++ b/code/modules/antagonists/abductor/equipment/abduction_gear.dm @@ -822,6 +822,7 @@ Congratulations! You are now trained for invasive xenobiology research!"} smoothing_groups = list(SMOOTH_GROUP_ABDUCTOR_TABLES) canSmoothWith = list(SMOOTH_GROUP_ABDUCTOR_TABLES) frame = /obj/structure/table_frame/abductor + custom_materials = list(/datum/material/silver = 2000) /obj/structure/table/optable/abductor name = "alien operating table" diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index 221febbe14d..94ffbb318cc 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -217,6 +217,19 @@ GLOBAL_LIST_EMPTY(asset_datums) var/size_id = sprite[SPR_SIZE] return {"[name][size_id] [sprite_name]"} +/** + * Returns the size class (ex design32x32) for a given sprite's icon + * + * Arguments: + * * sprite_name - The sprite to get the size of + */ +/datum/asset/spritesheet/proc/icon_size_id(sprite_name) + var/sprite = sprites[sprite_name] + if (!sprite) + return null + var/size_id = sprite[SPR_SIZE] + return "[name][size_id]" + #undef SPR_SIZE #undef SPR_IDX #undef SPRSZ_COUNT diff --git a/code/modules/atmospherics/gasmixtures/reactions.dm b/code/modules/atmospherics/gasmixtures/reactions.dm index ca6957156a1..3e10add9dc7 100644 --- a/code/modules/atmospherics/gasmixtures/reactions.dm +++ b/code/modules/atmospherics/gasmixtures/reactions.dm @@ -170,13 +170,13 @@ nobiliumsuppression = INFINITY energy_released += (FIRE_HYDROGEN_ENERGY_WEAK * burned_fuel) cached_results["fire"] += burned_fuel - + else burned_fuel = cached_gases[/datum/gas/tritium][MOLES] cached_gases[/datum/gas/tritium][MOLES] -= burned_fuel / TRITIUM_BURN_TRIT_FACTOR cached_gases[/datum/gas/oxygen][MOLES] -= burned_fuel - + ASSERT_GAS(/datum/gas/water_vapor, air) //oxygen+more-or-less hydrogen=H2O cached_gases[/datum/gas/water_vapor][MOLES] += burned_fuel / TRITIUM_BURN_TRIT_FACTOR @@ -514,8 +514,6 @@ nobiliumsuppression = INFINITY cached_gases[/datum/gas/nitrous_oxide][MOLES] -= reaction_efficency cached_gases[/datum/gas/plasma][MOLES] -= 2 * reaction_efficency - SSresearch.science_tech.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, min((reaction_efficency**2) * BZ_RESEARCH_SCALE, BZ_RESEARCH_MAX_AMOUNT)) - if(energy_released > 0) var/new_heat_capacity = air.heat_capacity() if(new_heat_capacity > MINIMUM_HEAT_CAPACITY) @@ -554,7 +552,6 @@ nobiliumsuppression = INFINITY cached_gases[/datum/gas/hydrogen][MOLES] -= heat_efficency * 3.5 if (prob(100 / increase_factor)) new /obj/item/stack/sheet/mineral/metal_hydrogen(location) - SSresearch.science_tech.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, min((heat_efficency * increase_factor * 0.5), METAL_HYDROGEN_RESEARCH_MAX_AMOUNT)) if(energy_used > 0) var/new_heat_capacity = air.heat_capacity() @@ -619,7 +616,6 @@ nobiliumsuppression = INFINITY cached_gases[/datum/gas/stimulum][MOLES]+= heat_scale * 0.75 cached_gases[/datum/gas/tritium][MOLES] -= heat_scale cached_gases[/datum/gas/nitryl][MOLES] -= heat_scale - SSresearch.science_tech.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, STIMULUM_RESEARCH_AMOUNT * max(stim_energy_change, 0)) if(stim_energy_change) var/new_heat_capacity = air.heat_capacity() if(new_heat_capacity > MINIMUM_HEAT_CAPACITY) @@ -647,10 +643,9 @@ nobiliumsuppression = INFINITY var/energy_produced = nob_formed * (NOBLIUM_FORMATION_ENERGY / (max(cached_gases[/datum/gas/bz][MOLES], 1))) if ((cached_gases[/datum/gas/tritium][MOLES] - 5 * nob_formed < 0) || (cached_gases[/datum/gas/nitrogen][MOLES] - 10 * nob_formed < 0)) return NO_REACTION - cached_gases[/datum/gas/tritium][MOLES] -= nob_formed * 5 - cached_gases[/datum/gas/nitrogen][MOLES] -= nob_formed * 10 + cached_gases[/datum/gas/tritium][MOLES] -= 5 * nob_formed + cached_gases[/datum/gas/nitrogen][MOLES] -= 10 * nob_formed cached_gases[/datum/gas/hypernoblium][MOLES] += nob_formed - SSresearch.science_tech.add_point_type(TECHWEB_POINT_TYPE_DEFAULT, nob_formed * NOBLIUM_RESEARCH_AMOUNT) if (nob_formed) var/new_heat_capacity = air.heat_capacity() diff --git a/code/modules/experisci/destructive_scanner.dm b/code/modules/experisci/destructive_scanner.dm new file mode 100644 index 00000000000..b572193cb42 --- /dev/null +++ b/code/modules/experisci/destructive_scanner.dm @@ -0,0 +1,101 @@ +/** + * # Destructive scanner + * + * Placed machine that handles destructive experiments (but can also do the normal ones) + */ +/obj/machinery/destructive_scanner + name = "Experimental Destructive Scanner" + desc = "A much larger version of the hand-held scanner, a charred label warns about its destructive capabilities." + icon = 'icons/obj/machines/experisci.dmi' + icon_state = "tube_open" + circuit = /obj/item/circuitboard/machine/destructive_scanner + layer = MOB_LAYER + var/scanning = FALSE + +/obj/machinery/destructive_scanner/Initialize() + ..() + return INITIALIZE_HINT_LATELOAD + +// Late load to ensure the component initialization occurs after the machines are initialized +/obj/machinery/destructive_scanner/LateInitialize() + . = ..() + AddComponent(/datum/component/experiment_handler, \ + allowed_experiments = list(/datum/experiment/scanning),\ + config_mode = EXPERIMENT_CONFIG_CLICK, \ + start_experiment_callback = CALLBACK(src, .proc/activate)) + +///Activates the machine; checks if it can actually scan, then starts. +/obj/machinery/destructive_scanner/proc/activate() + var/atom/pickup_zone = drop_location() + var/aggressive = FALSE + for(var/mob/living/living_mob in pickup_zone) + if(!(obj_flags & EMAGGED) && ishuman(living_mob)) //Can only kill humans when emagged. + playsound(src, 'sound/machines/buzz-sigh.ogg', 25) + say("Cannot scan with humans inside.") + return + aggressive = TRUE + start_closing(aggressive) + +///Closes the machine to kidnap everything in the turf into it. +/obj/machinery/destructive_scanner/proc/start_closing(aggressive) + if(scanning) + return + var/atom/pickup_zone = drop_location() + for(var/atom/movable/to_pickup in pickup_zone) + to_pickup.forceMove(src) + flick("tube_down", src) + scanning = TRUE + update_icon() + playsound(src, 'sound/machines/destructive_scanner/TubeDown.ogg', 100) + addtimer(CALLBACK(src, .proc/start_scanning, aggressive), 1.2 SECONDS) + +///Starts scanning the fancy scanning effects +/obj/machinery/destructive_scanner/proc/start_scanning(aggressive) + if(aggressive) + playsound(src, 'sound/machines/destructive_scanner/ScanDangerous.ogg', 100, extrarange = 5) + else + playsound(src, 'sound/machines/destructive_scanner/ScanSafe.ogg', 100) + addtimer(CALLBACK(src, .proc/finish_scanning, aggressive), 6 SECONDS) + + +///Performs the actual scan, happens once the tube effects are done +/obj/machinery/destructive_scanner/proc/finish_scanning(aggressive) + flick("tube_up", src) + scanning = FALSE + update_icon() + playsound(src, 'sound/machines/destructive_scanner/TubeUp.ogg', 100) + addtimer(CALLBACK(src, .proc/open, aggressive), 1.2 SECONDS) + +///Opens the machine to let out any contents. If the scan had mobs it'll gib them. +/obj/machinery/destructive_scanner/proc/open(aggressive) + var/turf/this_turf = get_turf(src) + var/list/scanned_atoms = list() + + for(var/atom/movable/movable_atom in contents) + if(movable_atom in component_parts) + continue + scanned_atoms += movable_atom + movable_atom.forceMove(this_turf) + if(isliving(movable_atom)) + var/mob/living/fucked_up_thing = movable_atom + fucked_up_thing.gib() + + SEND_SIGNAL(src, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, scanned_atoms) + + +/obj/machinery/destructive_scanner/emag_act(mob/user) + if(obj_flags & EMAGGED) + return + obj_flags |= EMAGGED + playsound(src, "sparks", 75, TRUE, SILENCED_SOUND_EXTRARANGE) + to_chat(user, "You disable the safety sensor BIOS on [src].") + +/obj/machinery/destructive_scanner/update_icon_state() + . = ..() + icon_state = scanning ? "tube_on" : "tube_open" + +/obj/machinery/destructive_scanner/attackby(obj/item/object, mob/user, params) + if (!scanning && default_deconstruction_screwdriver(user, "tube_open", "tube_open", object) || default_deconstruction_crowbar(object)) + update_icon() + return + return ..() diff --git a/code/modules/experisci/experiment/experiments.dm b/code/modules/experisci/experiment/experiments.dm new file mode 100644 index 00000000000..73a4d7fb886 --- /dev/null +++ b/code/modules/experisci/experiment/experiments.dm @@ -0,0 +1,140 @@ +/datum/experiment/scanning/points/slime + name = "Base Slime Experiment" + required_points = 1 + +/datum/experiment/scanning/points/slime/calibration + name = "Slime Sample Test" + description = "Let's see if our scanners can pick up the genetic data from a simple slime extract." + required_atoms = list(/obj/item/slime_extract/grey = 1) + +/datum/experiment/scanning/points/slime/easy + name = "Easy Slime Survey" + description = "A wealthy client has requested that we provide samples of data from several basic slime cores." + required_points = 3 + required_atoms = list(/obj/item/slime_extract/orange = 1, + /obj/item/slime_extract/purple = 1, + /obj/item/slime_extract/blue = 1, + /obj/item/slime_extract/metal = 1) + +/datum/experiment/scanning/points/slime/moderate + name = "Moderate Slime Survey" + description = "Central Command has asked that you collect data from several common slime cores." + required_points = 5 + required_atoms = list(/obj/item/slime_extract/yellow = 1, + /obj/item/slime_extract/darkpurple = 1, + /obj/item/slime_extract/darkblue = 1, + /obj/item/slime_extract/silver = 1) + +/datum/experiment/scanning/points/slime/hard + name = "Challenging Slime Survey" + description = "Another station has challenged your research team to collect several challenging slime cores, \ + are you up to the task?" + required_points = 10 + required_atoms = list(/obj/item/slime_extract/bluespace = 1, + /obj/item/slime_extract/sepia = 1, + /obj/item/slime_extract/cerulean = 1, + /obj/item/slime_extract/pyrite = 1, + /obj/item/slime_extract/red = 2, + /obj/item/slime_extract/green = 2, + /obj/item/slime_extract/pink = 2, + /obj/item/slime_extract/gold = 2) + +/datum/experiment/scanning/points/slime/expert + name = "Expert Slime Survey" + description = "The intergalactic society of xenobiologists are currently looking for samples of the most complex \ + slime cores, we are tasking your station with providing them with everything they need." + required_points = 10 + required_atoms = list(/obj/item/slime_extract/adamantine = 1, + /obj/item/slime_extract/oil = 1, + /obj/item/slime_extract/black = 1, + /obj/item/slime_extract/lightpink = 1, + /obj/item/slime_extract/rainbow = 10) + +/datum/experiment/scanning/random/cytology/easy + name = "Basic Cytology Scanning Experiment" + description = "A scientist needs vermin to test on, use the cytology equipment to grow some of these simple critters!" + total_requirement = 3 + max_requirement_per_type = 2 + possible_types = list(/mob/living/simple_animal/hostile/cockroach, /datum/micro_organism/cell_line/mouse) + +/datum/experiment/scanning/random/cytology/medium + name = "Advanced Cytology Scanning Experiment" + description = "We need to see how the body functions from the earliest moments. Some cytology experiments will help us gain this understanding." + total_requirement = 3 + max_requirement_per_type = 2 + possible_types = list(/mob/living/simple_animal/hostile/carp, /mob/living/simple_animal/hostile/retaliate/poison/snake, /mob/living/simple_animal/pet/cat, /mob/living/simple_animal/pet/dog/corgi, /mob/living/simple_animal/cow, /mob/living/simple_animal/chicken) + +/datum/experiment/scanning/random/cytology/medium/one + name = "Advanced Cytology Scanning Experiment One" + +/datum/experiment/scanning/random/cytology/medium/two + name = "Advanced Cytology Scanning Experiment Two" + +/datum/experiment/scanning/random/janitor_trash + name = "Station Hygiene Inspection" + description = "To learn how to clean, we must first learn what it is to have filth. We need you to scan some filth around the station." + possible_types = list(/obj/effect/decal/cleanable/vomit, + /obj/effect/decal/cleanable/blood) + total_requirement = 3 + +/datum/experiment/explosion/calibration + name = "Is This Thing On?" + description = "The engineers from last shift left a notice for us that the doppler array seemed to be malfunctioning. \ + Could you check that it is still working? Any explosion will do!" + required_light = 1 + +/datum/experiment/explosion/maxcap + name = "Mother of God" + description = "A recent outbreak of a blood-cult in a nearby sector necessitates the development of a large explosive. \ + Create a large enough explosion to prove your bomb, we'll be watching." + +/datum/experiment/explosion/medium + name = "Explosive Ordinance Experiment" + description = "Alright, can we really call ourselves professionals if we can't make shit blow up?" + required_heavy = 2 + required_light = 6 + +/datum/experiment/explosion/maxcap/New() + required_devastation = GLOB.MAX_EX_DEVESTATION_RANGE + required_heavy = GLOB.MAX_EX_HEAVY_RANGE + required_light = GLOB.MAX_EX_LIGHT_RANGE + +/datum/experiment/scanning/random/material/meat + name = "Biological Material Scanning Experiment" + description = "They told us we couldn't make chairs out of every material in the world. You're here to prove those nay-sayers wrong." + possible_material_types = list(/datum/material/meat) + +/datum/experiment/scanning/random/material/easy + name = "Low Grade Material Scanning Experiment" + description = "Material science is all about a basic understanding of the universe, and how it's built. To explain this, build something basic and we'll show you how to break it." + total_requirement = 6 + possible_types = list(/obj/structure/chair, /obj/structure/toilet, /obj/structure/table) + possible_material_types = list(/datum/material/iron, /datum/material/glass) + +/datum/experiment/scanning/random/material/medium + name = "Medium Grade Material Scanning Experiment" + description = "Not all materials are strong enough to hold together a space station. Look at these materials for example, and see what makes them useful for our electronics and equipment." + possible_material_types = list(/datum/material/silver, /datum/material/gold, /datum/material/plastic, /datum/material/titanium) + +/datum/experiment/scanning/random/material/medium/one + name = "Medium Grade Material Scanning Experiment One" + +/datum/experiment/scanning/random/material/medium/two + name = "Medium Grade Material Scanning Experiment Two" + +/datum/experiment/scanning/random/material/medium/three + name = "Medium Grade Material Scanning Experiment Three" + +/datum/experiment/scanning/random/material/hard + name = "High Grade Material Scanning Experiment" + description = "NT spares no expense to test even the most valuable of materials for their qualities as construction materials. Go build us some of these exotic creations and collect the data." + possible_material_types = list(/datum/material/diamond, /datum/material/plasma, /datum/material/uranium) + +/datum/experiment/scanning/random/material/hard/one + name = "High Grade Material Scanning Experiment One" + +/datum/experiment/scanning/random/material/hard/two + name = "High Grade Material Scanning Experiment Two" + +/datum/experiment/scanning/random/material/hard/three + name = "High Grade Material Scanning Experiment Three" diff --git a/code/modules/experisci/experiment/handlers/experiment_handler.dm b/code/modules/experisci/experiment/handlers/experiment_handler.dm new file mode 100644 index 00000000000..430dff2fa0d --- /dev/null +++ b/code/modules/experisci/experiment/handlers/experiment_handler.dm @@ -0,0 +1,405 @@ +/** + * # Experiment Handler + * + * This is the component for interacting with experiments from a connected techweb. It is generic + * and should be set-up to automatically work on any class it is attached to without outside code + * (Excluding potential callbacks) + */ +/datum/component/experiment_handler + /// Holds the currently linked techweb to get experiments from + var/datum/techweb/linked_web + /// Holds the currently selected experiment + var/datum/experiment/selected_experiment + /// Holds the list of types of experiments that this experiment_handler can interact with + var/list/allowed_experiments + /// Holds the list of types of experiments that this experimennt_handler should NOT interact with + var/list/blacklisted_experiments + /// A set of optional experiment traits (see defines) that are disallowed for any experiments + var/disallowed_traits + /// Additional configuration flags for how the experiment_handler operates + var/config_flags + /// Callback that, when supplied, can be called from the UI + var/datum/callback/start_experiment_callback + +/** + * Initializes a new instance of the experiment_handler component + * + * Arguments: + * * allowed_experiments - The list of /datum/experiment types that can be performed with this component + * * blacklisted_experiments - The list of /datum/experiment types that explicitly cannot be performed with this component + * * config_mode - The define that determines how the experiment_handler should display the configuration UI + * * disallowed_traits - Flags that control what experiment traits are blacklisted by this experiment handler + * * config_flags - Flags that control the operational behaviour of the experiment handler, see experiment defines + * * start_experiment_callback - When provided adds a UI button to use this callback to the start the experiment + */ +/datum/component/experiment_handler/Initialize(allowed_experiments = list(), + blacklisted_experiments = list(), + config_mode = EXPERIMENT_CONFIG_ATTACKSELF, + disallowed_traits = null, + config_flags = null, + datum/callback/start_experiment_callback = null, +) + . = ..() + if(!ismovable(parent)) + return COMPONENT_INCOMPATIBLE + + src.allowed_experiments = allowed_experiments + src.blacklisted_experiments = blacklisted_experiments + src.disallowed_traits = disallowed_traits + src.config_flags = config_flags + src.start_experiment_callback = start_experiment_callback + + if(isitem(parent)) + RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, .proc/try_run_handheld_experiment) + RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, .proc/ignored_handheld_experiment_attempt) + if(istype(parent, /obj/machinery/doppler_array)) + RegisterSignal(parent, COMSIG_DOPPLER_ARRAY_EXPLOSION_DETECTED, .proc/try_run_doppler_experiment) + if(istype(parent, /obj/machinery/destructive_scanner)) + RegisterSignal(parent, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, .proc/try_run_destructive_experiment) + + // Determine UI display mode + switch(config_mode) + if(EXPERIMENT_CONFIG_ATTACKSELF) + RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, .proc/configure_experiment) + if(EXPERIMENT_CONFIG_ALTCLICK) + RegisterSignal(parent, COMSIG_CLICK_ALT, .proc/configure_experiment) + if(EXPERIMENT_CONFIG_CLICK) + RegisterSignal(parent, COMSIG_ATOM_UI_INTERACT, .proc/configure_experiment_click) + if(EXPERIMENT_CONFIG_UI) + RegisterSignal(parent, COMSIG_UI_ACT, .proc/ui_handle_experiment) + + // Auto connect to the first visible techweb (useful for always active handlers) + // Note this won't work at the moment for non-machines that have been included + // on the map as the servers aren't initialized when the non-machines are initializing + if (!(config_flags & EXPERIMENT_CONFIG_NO_AUTOCONNECT)) + var/list/found_servers = get_available_servers(parent) + var/obj/machinery/rnd/server/selected_server = found_servers.len ? found_servers[1] : null + if (selected_server) + link_techweb(selected_server.stored_research) + + GLOB.experiment_handlers += src + +/datum/component/experiment_handler/Destroy(force, silent) + . = ..() + GLOB.experiment_handlers -= src + +/** + * Hooks on attack to try and run an experiment (When using a handheld handler) + */ +/datum/component/experiment_handler/proc/try_run_handheld_experiment(datum/source, atom/target, mob/user, params) + SIGNAL_HANDLER + if (!should_run_handheld_experiment(source, target, user, params)) + return + INVOKE_ASYNC(src, .proc/try_run_handheld_experiment_async, source, target, user, params) + return COMPONENT_CANCEL_ATTACK_CHAIN + +/** + * Provides feedback when an item isn't related to an experiment, and has fully passed the attack chain + */ +/datum/component/experiment_handler/proc/ignored_handheld_experiment_attempt(datum/source, atom/target, mob/user, proximity_flag, params) + SIGNAL_HANDLER + if (!proximity_flag || (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE))) + return + playsound(user, 'sound/machines/buzz-sigh.ogg', 25) + to_chat(user, "[target] is not related to your currently selected experiment.") + +/** + * Checks that an experiment can be run using the provided target, used for preventing the cancellation of the attack chain inappropriately + */ +/datum/component/experiment_handler/proc/should_run_handheld_experiment(datum/source, atom/target, mob/user, params) + // Check that there is actually an experiment selected + if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE)) + return + + // Determine if this experiment is actionable with this target + var/list/arguments = list(src) + arguments = args.len > 1 ? arguments + args.Copy(2) : arguments + if (config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE) + for (var/datum/experiment/experiment in linked_web.available_experiments) + if (experiment.actionable(arglist(arguments))) + return TRUE + else + return selected_experiment.actionable(arglist(arguments)) + +/** + * This proc exists because Jared Fogle really likes async + */ +/datum/component/experiment_handler/proc/try_run_handheld_experiment_async(datum/source, atom/target, mob/user, params) + if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE)) + to_chat(user, "You do not have an experiment selected!") + return + if(!do_after(user, 1 SECONDS, target = target)) + return + if(action_experiment(source, target)) + playsound(user, 'sound/machines/ping.ogg', 25) + to_chat(user, "You scan [target].") + else + playsound(user, 'sound/machines/buzz-sigh.ogg', 25) + to_chat(user, "[target] is not related to your currently selected experiment.") + + +/** + * Hooks on destructive scans to try and run an experiment (When using a handheld handler) + */ +/datum/component/experiment_handler/proc/try_run_destructive_experiment(datum/source, list/scanned_atoms) + SIGNAL_HANDLER + var/atom/movable/our_scanner = parent + if (selected_experiment == null) + playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25) + to_chat(our_scanner, "No experiment selected!") + return + var/successful_scan + for(var/scan_target in scanned_atoms) + if(action_experiment(source, scan_target)) + successful_scan = TRUE + break + if(successful_scan) + playsound(our_scanner, 'sound/machines/ping.ogg', 25) + to_chat(our_scanner, "The scan succeeds.") + else + playsound(src, 'sound/machines/buzz-sigh.ogg', 25) + our_scanner.say("The scan did not result in anything.") +/** + * Hooks on successful explosions on the doppler array this is attached to + */ +/datum/component/experiment_handler/proc/try_run_doppler_experiment(datum/source, turf/epicenter, devastation_range, + heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range +) + SIGNAL_HANDLER + var/atom/movable/our_array = parent + if(action_experiment(source, devastation_range, heavy_impact_range, light_impact_range)) + playsound(src, 'sound/machines/ping.ogg', 25) + else + playsound(src, 'sound/machines/buzz-sigh.ogg', 25) + our_array.say("Insufficient explosion to contribute to current experiment.") + +/** + * Announces a message to all experiment handlers + * + * Arguments: + * * message - The message to announce + */ +/datum/component/experiment_handler/proc/announce_message_to_all(message) + for(var/experiment in GLOB.experiment_handlers) + var/datum/component/experiment_handler/experi_handler = experiment + var/atom/movable/experi_parent = experi_handler.parent + experi_parent.say(message) + +/** + * Announces a message to this experiment handler + * + * Arguments: + * * message - The message to announce + */ +/datum/component/experiment_handler/proc/announce_message(message) + var/atom/movable/experi_parent = parent + experi_parent.say(message) + +/** + * Attempts to perform the selected experiment given some arguments + */ +/datum/component/experiment_handler/proc/action_experiment(datum/source, ...) + // Check if an experiment is selected + if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE)) + return FALSE + + // Get arguments for passing to the experiment[s] + var/list/arguments = list(src) + arguments = args.len > 1 ? arguments + args.Copy(2) : arguments + + // Check if this handler is configured to be always active, in which case we + // attempt to action every experiment that is available to this handler. + if (config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE) + var/any_success + for (var/datum/experiment/experiment in linked_web.available_experiments) + // Because this checks any experiment, we have to ensure it is allowable to be selected with can_select_experiment(...) + // this handles the handler's blacklist, whitelist, etc (potentially refactor this in the future if possible because this could be expensive) + if (can_select_experiment(experiment) && experiment.actionable(arglist(arguments)) && experiment.perform_experiment(arglist(arguments))) + any_success = TRUE + return any_success + else + // Returns true if the experiment was succesfuly handled + return selected_experiment.actionable(arglist(arguments)) && selected_experiment.perform_experiment(arglist(arguments)) + +/** + * Hook for handling UI interaction via signals + */ +/datum/component/experiment_handler/proc/ui_handle_experiment(datum/source, mob/user, action) + SIGNAL_HANDLER + switch(action) + if("open_experiments") + INVOKE_ASYNC(src, .proc/configure_experiment, null, usr) + +/** + * Attempts to show the user the experiment configuration panel + * + * Arguments: + * * user - The user to show the experiment configuration panel to + */ +/datum/component/experiment_handler/proc/configure_experiment(datum/source, mob/user) + ui_interact(user) + +/** + * Attempts to show the user the experiment configuration panel + * + * Arguments: + * * user - The user to show the experiment configuration panel to + */ +/datum/component/experiment_handler/proc/configure_experiment_click(datum/source, mob/user) + ui_interact(user) + +/** + * Attempts to link this experiment_handler to a provided techweb + * + * This proc attempts to link the handler to a provided techweb, overriding the existing techweb if relevant + * + * Arguments: + * * new_web - The new techweb to link to + */ +/datum/component/experiment_handler/proc/link_techweb(datum/techweb/new_web) + if (new_web == linked_web) + return + selected_experiment = null + linked_web = new_web + +/** + * Unlinks this handler from the selected techweb + */ +/datum/component/experiment_handler/proc/unlink_techweb() + selected_experiment = null + linked_web = null + +/** + * Attempts to link this experiment_handler to a provided experiment + * + * Arguments: + * * experiment - The experiment to attempt to link to + */ +/datum/component/experiment_handler/proc/link_experiment(datum/experiment/experiment) + if (experiment && can_select_experiment(experiment)) + selected_experiment = experiment + +/** + * Unlinks this handler from the selected experiment + */ +/datum/component/experiment_handler/proc/unlink_experiment() + selected_experiment = null + +/** + * Attempts to get rnd servers on the same z-level as a provided turf + * + * Arguments: + * * turf_to_check_for_servers - The turf to get servers on the same z-level of + */ +/datum/component/experiment_handler/proc/get_available_servers(turf/turf_to_check_for_servers = null) + if (!turf_to_check_for_servers) + turf_to_check_for_servers = get_turf(parent) + var/list/local_servers = list() + for (var/obj/machinery/rnd/server/server in SSresearch.servers) + var/turf/position_of_this_server_machine = get_turf(server) + if (turf_to_check_for_servers && position_of_this_server_machine && position_of_this_server_machine.z == turf_to_check_for_servers.z) + local_servers += server + return local_servers + +/** + * Checks if an experiment is valid to be selected by this handler + * + * Arguments: + * * experiment - The experiment to check + */ +/datum/component/experiment_handler/proc/can_select_experiment(datum/experiment/experiment) + // Check that this experiments has no disallowed traits + if (experiment.traits & disallowed_traits) + return FALSE + + // Check against the list of allowed experimentors + if (experiment.allowed_experimentors && experiment.allowed_experimentors.len) + var/matched = FALSE + for (var/experimentor in experiment.allowed_experimentors) + if (istype(parent, experimentor)) + matched = TRUE + break + if (!matched) + return FALSE + + // Check that this experiment is visible currently + if (!linked_web || !(experiment in linked_web.available_experiments)) + return FALSE + + // Check that this experiment type isn't blacklisted + for (var/badsci in blacklisted_experiments) + if (istype(experiment, badsci)) + return FALSE + + // Check against the allowed experiment types + for (var/goodsci in allowed_experiments) + if (istype(experiment, goodsci)) + return TRUE + + // If we haven't returned yet then this shouldn't be allowed + return FALSE + +/datum/component/experiment_handler/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + var/atom/parent_atom = parent + ui = new(user, src, "ExperimentConfigure", "[parent_atom ? "[parent_atom.name] | " : ""]Experiment Configuration") + ui.open() + +/datum/component/experiment_handler/ui_data(mob/user) + . = list( + "always_active" = config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE, + "has_start_callback" = !isnull(start_experiment_callback)) + .["servers"] = list() + for (var/obj/machinery/rnd/server/server in get_available_servers()) + var/list/data = list( + name = server.name, + web_id = server.stored_research?.id, + web_org = server.stored_research?.organization, + location = get_area(server), + selected = !isnull(linked_web) && server.stored_research == linked_web, + ref = REF(server) + ) + .["servers"] += list(data) + .["experiments"] = list() + if (linked_web) + for (var/datum/experiment/experiment in linked_web.available_experiments) + var/list/data = list( + name = experiment.name, + description = experiment.description, + tag = experiment.exp_tag, + selectable = can_select_experiment(experiment), + selected = selected_experiment == experiment, + progress = experiment.check_progress(), + performance_hint = experiment.performance_hint, + ref = REF(experiment) + ) + .["experiments"] += list(data) + +/datum/component/experiment_handler/ui_act(action, params) + . = ..() + if (.) + return + switch (action) + if ("select_server") + . = TRUE + var/obj/machinery/rnd/server/server = locate(params["ref"]) + if (server) + link_techweb(server.stored_research) + return + if ("clear_server") + . = TRUE + unlink_techweb() + if ("select_experiment") + . = TRUE + // Don't allow selection for always actives (no concept of active) + if (config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE) + return + var/datum/experiment/experiment = locate(params["ref"]) + if (experiment) + link_experiment(experiment) + if ("clear_experiment") + . = TRUE + unlink_experiment() + if("start_experiment_callback") + start_experiment_callback.Invoke(selected_experiment) diff --git a/code/modules/experisci/experiment/physical_experiments.dm b/code/modules/experisci/experiment/physical_experiments.dm new file mode 100644 index 00000000000..d163e5f0b35 --- /dev/null +++ b/code/modules/experisci/experiment/physical_experiments.dm @@ -0,0 +1,56 @@ +/datum/experiment/physical/meat_wall_explosion + name = "Extreme Cooking Experiment" + description = "There has been interest in using our engineering equipment to see what kind of new cooking appliances we can create" + +/datum/experiment/physical/meat_wall_explosion/register_events() + if(!istype(currently_scanned_atom, /turf/closed/wall)) + linked_experiment_handler.announce_message("Incorrect object for experiment.") + return FALSE + + if(!currently_scanned_atom.has_material_type(/datum/material/meat)) + linked_experiment_handler.announce_message("Object is not made out of the correct materials.") + return FALSE + + RegisterSignal(currently_scanned_atom, COMSIG_ATOM_BULLET_ACT, .proc/check_experiment) + linked_experiment_handler.announce_message("Experiment ready to start.") + return TRUE + +/datum/experiment/physical/meat_wall_explosion/unregister_events() + UnregisterSignal(currently_scanned_atom, COMSIG_ATOM_BULLET_ACT) + +/datum/experiment/physical/meat_wall_explosion/check_progress() + . += EXPERIMENT_PROG_BOOL("Fire an emitter at a tracked meat wall", is_complete()) + +/datum/experiment/physical/meat_wall_explosion/proc/check_experiment(datum/source, obj/projectile/Proj) + if(istype(Proj, /obj/projectile/beam/emitter)) + finish_experiment(linked_experiment_handler) + +/datum/experiment/physical/meat_wall_explosion/finish_experiment(datum/component/experiment_handler/experiment_handler) + . = ..() + new /obj/effect/gibspawner/generic(currently_scanned_atom) + var/turf/meat_wall = currently_scanned_atom + var/turf/new_turf = meat_wall.ScrapeAway() + new /obj/effect/gibspawner/generic(new_turf) + new /obj/item/food/meat/steak/plain(new_turf) + +/datum/experiment/physical/arcade_winner + name = "Playtesting Experiences" + description = "How do they make these arcade games so fun? Let's play one and win it to find out." + +/datum/experiment/physical/arcade_winner/register_events() + if(!istype(currently_scanned_atom, /obj/machinery/computer/arcade)) + linked_experiment_handler.announce_message("Incorrect object for experiment.") + return FALSE + + RegisterSignal(currently_scanned_atom, COMSIG_ARCADE_PRIZEVEND, .proc/win_arcade) + linked_experiment_handler.announce_message("Experiment ready to start.") + return TRUE + +/datum/experiment/physical/arcade_winner/unregister_events() + UnregisterSignal(currently_scanned_atom, COMSIG_ARCADE_PRIZEVEND) + +/datum/experiment/physical/arcade_winner/check_progress() + . += EXPERIMENT_PROG_BOOL("Win an arcade game at a tracked arcade cabinet.", is_complete()) + +/datum/experiment/physical/arcade_winner/proc/win_arcade(datum/source) + finish_experiment(linked_experiment_handler) diff --git a/code/modules/experisci/experiment/types/experiment.dm b/code/modules/experisci/experiment/types/experiment.dm new file mode 100644 index 00000000000..efbf488426c --- /dev/null +++ b/code/modules/experisci/experiment/types/experiment.dm @@ -0,0 +1,88 @@ +/** + * # Experiment + * + * This is the base datum for experiments, storing the base definition. + * + * This class should be subclassed for producing actual experiments. The + * proc stubs should be implemented in whole. + */ +/datum/experiment + /// Name that distinguishes the experiment + var/name = "Experiment" + /// A brief description of the experiment to be shown as details + var/description = "Base experiment" + /// A descriptive tag used on UI elements to denote 'types' of experiments + var/exp_tag = "Base" + /// A list of types that are allowed to experiment with this dastum + var/list/allowed_experimentors + /// Whether the experiment has been completed + var/completed = FALSE + /// Traits related to the experiment + var/traits + /// A textual hint shown on the UI in a tooltip to help a user determine how to perform + /// the experiment + var/performance_hint + +/** + * Performs any necessary initialization of tags and other variables + */ +/datum/experiment/New() + if (traits & EXPERIMENT_TRAIT_DESTRUCTIVE) + exp_tag = "Destructive [exp_tag]" + +/** + * Checks if the experiment is complete + * + * This proc should be overridden such that it returns TRUE/FALSE to + * state if the experiment is complete or not. + */ +/datum/experiment/proc/is_complete() + return + +/** + * Gets the current progress towards the goal of the experiment + * + * This proc should be overridden such that the return value is a + * list of lists, wherein each inner list represents a stage. Stages have + * types, see _DEFINES/experisci.dm. Each stage should be constructed using + * one of several available macros in that file. + */ +/datum/experiment/proc/check_progress() + . = list() + +/** + * Gets if the experiment is actionable provided some arguments + * + * This proc should be overridden such that the return value is a + * boolean value representing if the experiment could be actioned with + * the provided arguments. + */ +/datum/experiment/proc/actionable(...) + return !is_complete() + +/** + * Proc that tries to perform the experiment, and then checks if its completed. + */ +/datum/experiment/proc/perform_experiment(datum/component/experiment_handler/experiment_handler, ...) + var/action_succesful = perform_experiment_actions(arglist(args)) + if(is_complete()) + finish_experiment(experiment_handler) + return action_succesful + +/** + * Attempts to perform the experiment provided some arguments + * + * This proc should be overridden such that the experiment will be actioned + * with some defined arguments + */ +/datum/experiment/proc/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, ...) + return + +/** + * Called when you complete an experiment, makes sure the techwebs knows the experiment was finished, and tells everyone it happend, yay! + */ +/datum/experiment/proc/finish_experiment(datum/component/experiment_handler/experiment_handler) + completed = TRUE + experiment_handler.announce_message_to_all("The [name] has been completed!") + experiment_handler.selected_experiment = null + experiment_handler.linked_web.complete_experiment(src) diff --git a/code/modules/experisci/experiment/types/explosive.dm b/code/modules/experisci/experiment/types/explosive.dm new file mode 100644 index 00000000000..62db6e963cd --- /dev/null +++ b/code/modules/experisci/experiment/types/explosive.dm @@ -0,0 +1,39 @@ +/datum/experiment/explosion + name = "Explosive Experiment" + description = "An experiment requiring an explosion to progress" + exp_tag = "Explosion" + performance_hint = "Perform explosive experiments using the research doppler array in the toxins lab." + /// The required devastation range to complete the experiment + var/required_devastation = 0 + /// The required heavy impact range to complete the experiment + var/required_heavy = 0 + /// The required light impact range to complete the experiment + var/required_light = 0 + /// The last measured devastation range + var/last_devastation + /// The last measured heavy range + var/last_heavy + /// The last measured light range + var/last_light + +/datum/experiment/explosion/is_complete() + return required_devastation <= last_devastation \ + && required_heavy <= last_heavy \ + && required_light <= last_light + +/datum/experiment/explosion/check_progress() + var/status_message = "You must record an explosion with ranges of at least \ + [required_devastation] devastation, [required_heavy] heavy, and [required_light] light." + if (last_devastation || last_heavy || last_light) + status_message += " The last attempt had ranges of [last_devastation]D/[last_heavy]H/[last_light]L." + . += EXPERIMENT_PROG_BOOL(status_message, is_complete()) + +/datum/experiment/explosion/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, devastation, heavy, light) + last_devastation = devastation + last_heavy = heavy + last_light = light + return is_complete() + +/datum/experiment/explosion/actionable(datum/component/experiment_handler/experiment_handler, devastation, heavy, light) + return !is_complete() + diff --git a/code/modules/experisci/experiment/types/physical_experiment.dm b/code/modules/experisci/experiment/types/physical_experiment.dm new file mode 100644 index 00000000000..5e692931c5b --- /dev/null +++ b/code/modules/experisci/experiment/types/physical_experiment.dm @@ -0,0 +1,35 @@ +/datum/experiment/physical + name = "Physical Experiment" + description = "An experiment requiring a physical reaction to continue" + exp_tag = "Physical Experiment" + performance_hint = "To perform physical experiments you must use a hand-held scanner unit to track objects in our world relevant to \ + your experiment. Activate the experiment on your scanner, scan the object to track, and then complete the objective." + /// The atom that is currently being watched by this experiment + var/atom/currently_scanned_atom + /// Linked experiment handler + var/datum/component/experiment_handler/linked_experiment_handler + +/datum/experiment/physical/is_complete() + return completed + +/datum/experiment/physical/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, atom/target) + unregister_events() + currently_scanned_atom = target + linked_experiment_handler = experiment_handler + if(register_events()) + return TRUE + currently_scanned_atom = null + linked_experiment_handler = null + return FALSE + +/** + * Handles registering to events relevant to the experiment + */ +/datum/experiment/physical/proc/register_events() + return FALSE + +/** + * Handles unregistering to events relevant to the experiment + */ +/datum/experiment/physical/proc/unregister_events() + return diff --git a/code/modules/experisci/experiment/types/random_scanning.dm b/code/modules/experisci/experiment/types/random_scanning.dm new file mode 100644 index 00000000000..e80a80b5a0e --- /dev/null +++ b/code/modules/experisci/experiment/types/random_scanning.dm @@ -0,0 +1,21 @@ +/datum/experiment/scanning/random + name = "Base random scanning experiment" + description = "This experiment's contents will be randomized. Good luck!" + ///list of types which that can be included in the experiment. Randomly picked from on New + var/list/possible_types = list() + /// The total desired number of atoms to have scanned + var/total_requirement = 0 + /// Max amount of a requirement per type + var/max_requirement_per_type = 100 + +/datum/experiment/scanning/random/New() + // Generate random contents + if (possible_types.len) + var/picked = 0 + while (picked < total_requirement) + var/randum = min(rand(1, total_requirement - picked), max_requirement_per_type) //Short for "random num" and not "this is dum that I have to rename 100 different variable names" + required_atoms[pick(possible_types)] += randum + picked += randum + + // Fill the experiment as per usual + ..() diff --git a/code/modules/experisci/experiment/types/scanning.dm b/code/modules/experisci/experiment/types/scanning.dm new file mode 100644 index 00000000000..ab2b11d9de5 --- /dev/null +++ b/code/modules/experisci/experiment/types/scanning.dm @@ -0,0 +1,140 @@ +/** + * # Scanning Experiment + * + * This is the base implementation of scanning experiments. + * + * This class should be subclassed for producing actual experiments. The + * procs should be extended where necessary. + */ +/datum/experiment/scanning + name = "Scanning Experiment" + description = "Base experiment for scanning atoms" + exp_tag = "Scan" + allowed_experimentors = list(/obj/item/experi_scanner, /obj/machinery/destructive_scanner) + performance_hint = "Perform scanning experiments using a handheld experi-scanner, or the stationary experimental destructive scanner. \ + Destructive scans can only be performed with the experimental destructive scanner." + /// The typepaths and number of atoms that must be scanned + var/list/required_atoms = list() + /// The list of atoms with sub-lists of atom references for scanned atoms contributing to the experiment + var/list/scanned = list() + +/** + * Initializes the scanned atoms lists + * + * Initializes the internal scanned atoms list to keep track of which atoms have already been scanned + */ +/datum/experiment/scanning/New() + . = ..() + for (var/req_atom in required_atoms) + scanned[req_atom] = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 0 : list() + +/** + * Checks if the scanning experiment is complete + * + * Returns TRUE/FALSE as to if the necessary number of atoms have been + * scanned. + */ +/datum/experiment/scanning/is_complete() + . = TRUE + var/destructive = traits & EXPERIMENT_TRAIT_DESTRUCTIVE + for (var/req_atom in required_atoms) + var/list/seen = scanned[req_atom] + if (destructive && (!(req_atom in scanned) || scanned[req_atom] != required_atoms[req_atom])) + return FALSE + if (!destructive && (!seen || seen.len != required_atoms[req_atom])) + return FALSE + +/** + * Gets the number of atoms that have been scanned and the goal + * + * This proc returns a string describing the number of atoms that + * have been scanned as well as the target number of atoms. + */ +/datum/experiment/scanning/check_progress() + . = list() + for (var/atom_type in required_atoms) + var/atom/required_atom = atom_type + var/list/seen_instances = scanned[required_atom] + . += serialize_progress_stage(required_atom, seen_instances) + +/** + * Serializes a progress stage into a list to be sent to the UI + * + * Arguments: + * * target - The targeted atom for this progress stage + * * seen_instances - The number of instances seen of this atom + */ +/datum/experiment/scanning/proc/serialize_progress_stage(atom/target, list/seen_instances) + var/scanned_total = traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len + return EXPERIMENT_PROG_INT("Scan samples of \a [initial(target.name)]", scanned_total, required_atoms[target]) + +/** + * Attempts to scan an atom towards the experiment's goal + * + * This proc attempts to scan an atom towards the experiment's goal, + * and returns TRUE/FALSE based on success. + * Arguments: + * * target - The atom to attempt to scan + */ +/datum/experiment/scanning/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, atom/target) + var/contributing_index_value = get_contributing_index(target) + if (contributing_index_value) + scanned[contributing_index_value] += traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? 1 : target + if(traits & EXPERIMENT_TRAIT_DESTRUCTIVE && !isliving(target))//only qdel things when destructive scanning and they're not living (living things get gibbed) + qdel(target) + do_after_experiment(target, contributing_index_value) + return TRUE + +/datum/experiment/scanning/actionable(datum/component/experiment_handler/experiment_handler, atom/target) + return ..() && get_contributing_index(target) + +/** + * Attempts to get the typepath for an atom that would contribute to the experiment + * + * This proc checks the required atoms for a typepath that this target atom can contribute to + * and if found returns that typepath, otherwise returns null + * Arguments: + * * target - The atom to attempt to scan + */ +/datum/experiment/scanning/proc/get_contributing_index(atom/target) + var/destructive = traits & EXPERIMENT_TRAIT_DESTRUCTIVE + for (var/req_atom in required_atoms) + if (!istype(target, req_atom)) + continue + + // Try to select a required atom that this scanned atom would contribute towards + var/selected + var/list/seen = scanned[req_atom] + if (destructive && (req_atom in scanned) && scanned[req_atom] < required_atoms[req_atom]) + selected = req_atom + else if (!destructive && seen.len < required_atoms[req_atom] && !(target in seen)) + selected = req_atom + + // Run any additonal checks if necessary + if (selected && final_contributing_index_checks(target, selected)) + return selected + +/** + * Performs any additional checks against the atom being considered for selection as a contributing index + * + * This proc is intended to be used to add additional functionality to contributing index checks + * without having to duplicate the iteration structure of get_contributing_index() + * Arguments: + * * target - The atom being scanned + * * typepath - The typepath (selected index) of the target atom + */ +/datum/experiment/scanning/proc/final_contributing_index_checks(atom/target, typepath) + return TRUE + +/** + * Performs actions following a successful experiment action + * + * This proc is intended to be used to add additional functionality to follow experiment + * actions without having to change the perform_experiment_actions proc to get access to the + * selected typepath index + * Arguments: + * * target - The atom being scanned + * * typepath - The typepath (selected index) of the target atom + */ +/datum/experiment/scanning/proc/do_after_experiment(atom/target, typepath) + return TRUE diff --git a/code/modules/experisci/experiment/types/scanning_material.dm b/code/modules/experisci/experiment/types/scanning_material.dm new file mode 100644 index 00000000000..714205289de --- /dev/null +++ b/code/modules/experisci/experiment/types/scanning_material.dm @@ -0,0 +1,24 @@ +/datum/experiment/scanning/random/material + name = "Material Scanning Experiment" + description = "Base experiment for scanning atoms with materials" + exp_tag = "Material Scan" + total_requirement = 8 + possible_types = list(/obj/structure/chair, /obj/structure/toilet, /obj/structure/table, /turf/closed/wall, /turf/open/floor) + ///List of materials that can be required. + var/possible_material_types = list() + ///List of materials actually required, indexed by the atom that is required. + var/required_materials = list() + +/datum/experiment/scanning/random/material/New() + . = ..() + for(var/req_atom in required_atoms) + var/chosen_material = pick(possible_material_types) + required_materials[req_atom] = chosen_material + +/datum/experiment/scanning/random/material/final_contributing_index_checks(atom/target, typepath) + return ..() && target.custom_materials && target.has_material_type(required_materials[typepath]) + +/datum/experiment/scanning/random/material/serialize_progress_stage(atom/target, list/seen_instances) + var/datum/material/required_material = GET_MATERIAL_REF(required_materials[target]) + return EXPERIMENT_PROG_INT("Scan samples of \a [required_material.name] [initial(target.name)]", \ + traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len, required_atoms[target]) diff --git a/code/modules/experisci/experiment/types/scanning_points.dm b/code/modules/experisci/experiment/types/scanning_points.dm new file mode 100644 index 00000000000..7b8c7b07d4b --- /dev/null +++ b/code/modules/experisci/experiment/types/scanning_points.dm @@ -0,0 +1,46 @@ +/datum/experiment/scanning/points + name = "Point Scanning Experiment" + description = "Base experiment for scanning experiments tracked by points" + /// The current points gained on this experiment + var/points = 0 + /// The total required points for this experiment + var/required_points + +/datum/experiment/scanning/points/is_complete() + return points >= required_points + +/datum/experiment/scanning/points/check_progress() + . = EXPERIMENT_PROG_INT("Scan samples of the following objects to accumulate enough points to complete this experiment.", points, required_points) + var/complete = is_complete() + var/point_val_cache = list() + for (var/a_type in required_atoms) + var/atom/req_atom = a_type + if (!point_val_cache["[required_atoms[a_type]]"]) + point_val_cache["[required_atoms[a_type]]"] = list() + point_val_cache["[required_atoms[a_type]]"] += initial(req_atom.name) + + for (var/point_amt in point_val_cache) + var/list/types = point_val_cache[point_amt] + var/types_joined = types.Join(", ") + . += EXPERIMENT_PROG_DETAIL("[text2num(point_amt)] point\s: [types_joined]", complete) + +/datum/experiment/scanning/points/get_contributing_index(atom/target) + var/destructive = traits & EXPERIMENT_TRAIT_DESTRUCTIVE + for (var/req_atom in required_atoms) + if (!istype(target, req_atom)) + continue + + // Try to select a required atom that this scanned atom would contribute towards + var/selected + if (destructive && (req_atom in scanned)) + selected = req_atom + else if (!destructive && !(target in scanned[req_atom])) + selected = req_atom + + // Run any additonal checks if necessary + if (selected && final_contributing_index_checks(target, selected)) + return selected + +/datum/experiment/scanning/points/do_after_experiment(atom/target, typepath) + . = ..() + points = min(required_points, points + required_atoms[typepath]) diff --git a/code/modules/experisci/experiment/types/scanning_vatgrown.dm b/code/modules/experisci/experiment/types/scanning_vatgrown.dm new file mode 100644 index 00000000000..63bdf171a8a --- /dev/null +++ b/code/modules/experisci/experiment/types/scanning_vatgrown.dm @@ -0,0 +1,14 @@ +/datum/experiment/scanning/random/cytology + name = "Cytology Scanning Experiment" + description = "Base experiment for scanning atoms that were vatgrown" + exp_tag = "Cytology Scan" + total_requirement = 1 + possible_types = list(/mob/living/simple_animal/hostile/cockroach) + traits = EXPERIMENT_TRAIT_DESTRUCTIVE + +/datum/experiment/scanning/random/cytology/final_contributing_index_checks(atom/target, typepath) + return ..() && HAS_TRAIT(target, TRAIT_VATGROWN) + +/datum/experiment/scanning/random/cytology/serialize_progress_stage(atom/target, list/seen_instances) + return EXPERIMENT_PROG_INT("Scan samples of \a vat-grown [initial(target.name)]", \ + traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len, required_atoms[target]) diff --git a/code/modules/experisci/handheld_scanner.dm b/code/modules/experisci/handheld_scanner.dm new file mode 100644 index 00000000000..153958c3c38 --- /dev/null +++ b/code/modules/experisci/handheld_scanner.dm @@ -0,0 +1,24 @@ +/** + * # Experi-Scanner + * + * Handheld scanning unit to perform scanning experiments + */ +/obj/item/experi_scanner + name = "Experi-Scanner" + desc = "A handheld scanner used for completing the many experiments of modern science." + w_class = WEIGHT_CLASS_SMALL + icon = 'icons/obj/device.dmi' + icon_state = "experiscanner" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + +/obj/item/experi_scanner/Initialize() + ..() + return INITIALIZE_HINT_LATELOAD + +// Late initialize to allow for the rnd servers to initialize first +/obj/item/experi_scanner/LateInitialize() + . = ..() + AddComponent(/datum/component/experiment_handler, \ + allowed_experiments = list(/datum/experiment/scanning, /datum/experiment/physical),\ + disallowed_traits = EXPERIMENT_TRAIT_DESTRUCTIVE) diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index af92e73d61b..9d3c4819e7f 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -527,17 +527,10 @@ All effects don't start immediately, but rather get worse over time; the rate is if(SSresearch.science_tech) if(drunkenness >= 12.9 && drunkenness <= 13.8) drunkenness = round(drunkenness, 0.01) - var/ballmer_percent = 0 - if(drunkenness == 13.35) // why run math if I dont have to - ballmer_percent = 1 - else - ballmer_percent = (-abs(drunkenness - 13.35) / 0.9) + 1 if(DT_PROB(2.5, delta_time)) say(pick_list_replacements(VISTA_FILE, "ballmer_good_msg"), forced = "ballmer") - SSresearch.science_tech.add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = BALLMER_POINTS * ballmer_percent)) if(drunkenness > 26) // by this point you're into windows ME territory if(DT_PROB(2.5, delta_time)) - SSresearch.science_tech.remove_point_list(list(TECHWEB_POINT_TYPE_GENERIC = BALLMER_POINTS)) say(pick_list_replacements(VISTA_FILE, "ballmer_windows_me_msg"), forced = "ballmer") if(drunkenness >= 41) diff --git a/code/modules/modular_computers/computers/item/tablet_presets.dm b/code/modules/modular_computers/computers/item/tablet_presets.dm index a812c62bc55..7dac5dc416c 100644 --- a/code/modules/modular_computers/computers/item/tablet_presets.dm +++ b/code/modules/modular_computers/computers/item/tablet_presets.dm @@ -41,6 +41,7 @@ install_component(new /obj/item/computer_hardware/sensorpackage) install_component(new /obj/item/computer_hardware/card_slot/secondary) hard_drive.store_file(new /datum/computer_file/program/budgetorders) + hard_drive.store_file(new /datum/computer_file/program/science) /// Given by the syndicate as part of the contract uplink bundle - loads in the Contractor Uplink. /obj/item/modular_computer/tablet/syndicate_contract_uplink/preset/uplink/Initialize() diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm new file mode 100644 index 00000000000..c4801fb8cad --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/techweb.dm @@ -0,0 +1,217 @@ +/datum/computer_file/program/science + filename = "experi_track" + filedesc = "Nanotrasen Science Hub" + program_icon_state = "research" + extended_desc = "Connect to the internal science server in order to assist in station research efforts." + requires_ntnet = TRUE + size = 16 + tgui_id = "NtosTechweb" + program_icon = "atom" + required_access = ACCESS_RND + transfer_access = ACCESS_RD + /// Reference to global science techweb + var/datum/techweb/stored_research + /// Determines if the console is locked, and consequently if actions can be performed with it + var/locked = FALSE + /// Used for compressing data sent to the UI via static_data as payload size is of concern + var/id_cache = list() + /// Sequence var for the id cache + var/id_cache_seq = 1 + +/datum/computer_file/program/science/run_program(mob/living/user) + . = ..() + stored_research = SSresearch.science_tech + + +/datum/computer_file/program/science/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/research_designs) + ) + +// heavy data from this proc should be moved to static data when possible +/datum/computer_file/program/science/ui_data(mob/user) + var/list/data = get_header_data() + data += list( + "nodes" = list(), + "experiments" = list(), + "researched_designs" = stored_research.researched_designs, + "points" = stored_research.research_points, + "points_last_tick" = stored_research.last_bitcoins, + "web_org" = stored_research.organization, + "sec_protocols" = !(computer.obj_flags & EMAGGED), + "t_disk" = null, //Not doing disk operations on the app, use the console for that. + "d_disk" = null, //See above. + "locked" = locked + ) + + // Serialize all nodes to display + for(var/tier in stored_research.tiers) + var/datum/techweb_node/node = SSresearch.techweb_node_by_id(tier) + + // Ensure node is supposed to be visible + if (stored_research.hidden_nodes[tier]) + continue + + data["nodes"] += list(list( + "id" = node.id, + "can_unlock" = stored_research.can_unlock_node(node), + "tier" = stored_research.tiers[node.id] + )) + + // Get experiments and serialize them + var/list/exp_to_process = stored_research.available_experiments.Copy() + for (var/comp_experi in stored_research.completed_experiments) + exp_to_process += stored_research.completed_experiments[comp_experi] + for (var/process_experi in exp_to_process) + var/datum/experiment/unf_experi = process_experi + data["experiments"][unf_experi.type] = list( + "name" = unf_experi.name, + "description" = unf_experi.description, + "tag" = unf_experi.exp_tag, + "progress" = unf_experi.check_progress(), + "completed" = unf_experi.completed, + "performance_hint" = unf_experi.performance_hint + ) + return data + +/datum/computer_file/program/science/ui_act(action, list/params) + . = ..() + if (.) + return + var/obj/item/computer_hardware/card_slot/card_slot + if(computer) + card_slot = computer.all_components[MC_CARD] + var/obj/item/card/id/user_id_card = card_slot.stored_card + + // Check if the console is locked to block any actions occuring + if (locked && action != "toggleLock") + computer.say("Console is locked, cannot perform further actions.") + return TRUE + + switch (action) + if ("toggleLock") + if(computer.obj_flags & EMAGGED) + to_chat(usr, "Security protocol error: Unable to access locking protocols.") + return TRUE + if(ACCESS_RND in user_id_card?.access) + locked = !locked + else + to_chat(usr, "Unauthorized Access. Please insert research ID card.") + return TRUE + if ("researchNode") + if(!SSresearch.science_tech.available_nodes[params["node_id"]]) + return TRUE + research_node(params["node_id"], usr) + return TRUE + +/datum/computer_file/program/science/ui_static_data(mob/user) + . = list( + "static_data" = list() + ) + + // Build node cache... + // Note this looks a bit ugly but its to reduce the size of the JSON payload + // by the greatest amount that we can, as larger JSON payloads result in + // hanging when the user opens the UI + var/node_cache = list() + for (var/node_id in SSresearch.techweb_nodes) + var/datum/techweb_node/node = SSresearch.techweb_nodes[node_id] || SSresearch.error_node + var/compressed_id = "[compress_id(node.id)]" + node_cache[compressed_id] = list( + "name" = node.display_name, + "description" = node.description + ) + if (LAZYLEN(node.research_costs)) + node_cache[compressed_id]["costs"] = list() + for (var/node_cost in node.research_costs) + node_cache[compressed_id]["costs"]["[compress_id(node_cost)]"] = node.research_costs[node_cost] + if (LAZYLEN(node.prereq_ids)) + node_cache[compressed_id]["prereq_ids"] = list() + for (var/prerequisite_node in node.prereq_ids) + node_cache[compressed_id]["prereq_ids"] += compress_id(prerequisite_node) + if (LAZYLEN(node.design_ids)) + node_cache[compressed_id]["design_ids"] = list() + for (var/unlocked_design in node.design_ids) + node_cache[compressed_id]["design_ids"] += compress_id(unlocked_design) + if (LAZYLEN(node.unlock_ids)) + node_cache[compressed_id]["unlock_ids"] = list() + for (var/unlocked_node in node.unlock_ids) + node_cache[compressed_id]["unlock_ids"] += compress_id(unlocked_node) + if (LAZYLEN(node.required_experiments)) + node_cache[compressed_id]["required_experiments"] = node.required_experiments + if (LAZYLEN(node.discount_experiments)) + node_cache[compressed_id]["discount_experiments"] = node.discount_experiments + + // Build design cache + var/design_cache = list() + var/datum/asset/spritesheet/research_designs/spritesheet = get_asset_datum(/datum/asset/spritesheet/research_designs) + var/size32x32 = "[spritesheet.name]32x32" + for (var/design_id in SSresearch.techweb_designs) + var/datum/design/design = SSresearch.techweb_designs[design_id] || SSresearch.error_design + var/compressed_id = "[compress_id(design.id)]" + var/size = spritesheet.icon_size_id(design.id) + design_cache[compressed_id] = list( + design.name, + "[size == size32x32 ? "" : "[size] "][design.id]" + ) + + // Ensure id cache is included for decompression + var/flat_id_cache = list() + for (var/id in id_cache) + flat_id_cache += id + + .["static_data"] = list( + "node_cache" = node_cache, + "design_cache" = design_cache, + "id_cache" = flat_id_cache + ) + +/** + * Compresses an ID to an integer representation using the id_cache, used for deduplication + * in sent JSON payloads + * + * Arguments: + * * id - the ID to compress + */ +/datum/computer_file/program/science/proc/compress_id(id) + if (!id_cache[id]) + id_cache[id] = id_cache_seq + id_cache_seq += 1 + return id_cache[id] + +/datum/computer_file/program/science/proc/research_node(id, mob/user) + if(!stored_research.available_nodes[id] || stored_research.researched_nodes[id]) + computer.say("Node unlock failed: Either already researched or not available!") + return FALSE + var/datum/techweb_node/tech_node = SSresearch.techweb_node_by_id(id) + if(!istype(tech_node)) + computer.say("Node unlock failed: Unknown error.") + return FALSE + var/list/price = tech_node.get_price(stored_research) + if(stored_research.can_afford(price)) + computer.investigate_log("[key_name(user)] researched [id]([json_encode(price)]) on techweb id [stored_research.id] via [computer].", INVESTIGATE_RESEARCH) + if(stored_research == SSresearch.science_tech) + SSblackbox.record_feedback("associative", "science_techweb_unlock", 1, list("id" = "[id]", "name" = tech_node.display_name, "price" = "[json_encode(price)]", "time" = SQLtime())) + if(stored_research.research_node_id(id)) + computer.say("Successfully researched [tech_node.display_name].") + var/logname = "Unknown" + if(isAI(user)) + logname = "AI: [user.name]" + if(iscarbon(user)) + var/obj/item/card/id/idcard = user.get_active_held_item() + if(istype(idcard)) + logname = "User: [idcard.registered_name]" + if(ishuman(user)) + var/mob/living/carbon/human/human_user = user + var/obj/item/worn = human_user.wear_id + if(istype(worn)) + var/obj/item/card/id/id_card_of_human_user = worn.GetID() + if(istype(id_card_of_human_user)) + logname = "User: [id_card_of_human_user.registered_name]" + stored_research.research_logs = list(list(tech_node.display_name, price["General Research"], logname, "[get_area(src)] ([computer.x],[computer.y],[computer.z])")) + return TRUE + else + computer.say("Failed to research node: Internal database error!") + return FALSE + computer.say("Not enough research points...") + return FALSE diff --git a/code/modules/research/designs/experisci_designs.dm b/code/modules/research/designs/experisci_designs.dm new file mode 100644 index 00000000000..fb74bc827d7 --- /dev/null +++ b/code/modules/research/designs/experisci_designs.dm @@ -0,0 +1,9 @@ +/datum/design/experi_scanner + name = "Experimental Scanner" + desc = "Experimental scanning unit used for performing scanning experiments." + id = "experi_scanner" + build_type = PROTOLATHE + materials = list(/datum/material/glass = 500, /datum/material/iron = 500) + build_path = /obj/item/experi_scanner + category = list("Equipment") + departmental_flags = DEPARTMENTAL_FLAG_SCIENCE diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm index 7f2d48ca7fc..3bb3f2d6335 100644 --- a/code/modules/research/designs/machine_designs.dm +++ b/code/modules/research/designs/machine_designs.dm @@ -312,6 +312,22 @@ build_path = /obj/item/circuitboard/machine/dnascanner category = list("Research Machinery") +/datum/design/board/destructive_scanner + name = "Machine Design (Destructive Scanner Board)" + desc = "The circuit board for an experimental destructive scanner." + id = "destructive_scanner" + build_path = /obj/item/circuitboard/machine/destructive_scanner + category = list("Research Machinery") + departmental_flags = DEPARTMENTAL_FLAG_SCIENCE + +/datum/design/board/doppler_array + name = "Machine Design (Tachyon-Doppler Research Array Board)" + desc = "The circuit board for a tachyon-doppler research array" + id = "doppler_array" + build_path = /obj/item/circuitboard/machine/doppler_array + category = list("Research Machinery") + departmental_flags = DEPARTMENTAL_FLAG_SCIENCE + /datum/design/board/microwave name = "Machine Design (Microwave Board)" desc = "The circuit board for a microwave." diff --git a/code/modules/research/designs/nanite_designs.dm b/code/modules/research/designs/nanite_designs.dm index df7854ff701..d744bdc5b85 100644 --- a/code/modules/research/designs/nanite_designs.dm +++ b/code/modules/research/designs/nanite_designs.dm @@ -25,20 +25,6 @@ program_type = /datum/nanite_program/viral category = list("Utility Nanites") -/datum/design/nanites/research - name = "Distributed Computing" - desc = "The nanites aid the research servers by performing a portion of its calculations, increasing research point generation." - id = "research_nanites" - program_type = /datum/nanite_program/research - category = list("Utility Nanites") - -/datum/design/nanites/researchplus - name = "Neural Network" - desc = "The nanites link the host's brains together forming a neural research network, that becomes more efficient with the amount of total hosts. Can be overloaded to increase research output." - id = "researchplus_nanites" - program_type = /datum/nanite_program/researchplus - category = list("Utility Nanites") - /datum/design/nanites/monitoring name = "Monitoring" desc = "The nanites monitor the host's vitals and location, sending them to the suit sensor network." diff --git a/code/modules/research/destructive_analyzer.dm b/code/modules/research/destructive_analyzer.dm index 33625135619..87fb2732359 100644 --- a/code/modules/research/destructive_analyzer.dm +++ b/code/modules/research/destructive_analyzer.dm @@ -111,11 +111,7 @@ Note: Must be placed within 3 tiles of the R&D Console return FALSE if(QDELETED(loaded_item) || QDELETED(src)) return FALSE - var/loaded_type = loaded_item.type - if(destroy_item(loaded_item)) - stored_research.add_point_list(point_value) - stored_research.deconstructed_items[loaded_type] = point_value - + destroy_item(loaded_item) return TRUE /obj/machinery/rnd/destructive_analyzer/proc/unload_item() diff --git a/code/modules/research/experimentor.dm b/code/modules/research/experimentor.dm index 5d40dbb6b1a..5d7db7a4e78 100644 --- a/code/modules/research/experimentor.dm +++ b/code/modules/research/experimentor.dm @@ -464,10 +464,6 @@ visible_message("[src] scans the [exp_on], revealing its true nature!") playsound(src, 'sound/effects/supermatter.ogg', 50, 3, -1) var/obj/item/relic/R = loaded_item - if (!R.revealed) - var/points = rand(3500,3750) // discovery reward - new /obj/item/research_notes(drop_location(src), points, "experimentation") - visible_message(" This discovery netted [points] points for research.") R.reveal() investigate_log("Experimentor has revealed a relic with [R.realProc] effect.", INVESTIGATE_EXPERIMENTOR) ejectItem() diff --git a/code/modules/research/nanites/nanite_programs/utility.dm b/code/modules/research/nanites/nanite_programs/utility.dm index b33b390f670..6e84a291204 100644 --- a/code/modules/research/nanites/nanite_programs/utility.dm +++ b/code/modules/research/nanites/nanite_programs/utility.dm @@ -154,53 +154,6 @@ /datum/nanite_program/metabolic_synthesis/active_effect() host_mob.adjust_nutrition(-0.5) -/datum/nanite_program/research - name = "Distributed Computing" - desc = "The nanites aid the research servers by performing a portion of its calculations, increasing research point generation." - use_rate = 0.2 - rogue_types = list(/datum/nanite_program/toxic) - -/datum/nanite_program/research/active_effect() - if(!iscarbon(host_mob)) - return - var/points = 1 - if(!host_mob.client) //less brainpower - points *= 0.25 - SSresearch.science_tech.add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = points)) - -/datum/nanite_program/researchplus - name = "Neural Network" - desc = "The nanites link the host's brains together forming a neural research network, that becomes more efficient with the amount of total hosts." - use_rate = 0.3 - rogue_types = list(/datum/nanite_program/brain_decay) - -/datum/nanite_program/researchplus/enable_passive_effect() - . = ..() - if(!iscarbon(host_mob)) - return - if(host_mob.client) - SSnanites.neural_network_count++ - else - SSnanites.neural_network_count += 0.25 - -/datum/nanite_program/researchplus/disable_passive_effect() - . = ..() - if(!iscarbon(host_mob)) - return - if(host_mob.client) - SSnanites.neural_network_count-- - else - SSnanites.neural_network_count -= 0.25 - -/datum/nanite_program/researchplus/active_effect() - if(!iscarbon(host_mob)) - return - var/mob/living/carbon/C = host_mob - var/points = round(SSnanites.neural_network_count / 12, 0.1) - if(!C.client) //less brainpower - points *= 0.25 - SSresearch.science_tech.add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = points)) - /datum/nanite_program/access name = "Subdermal ID" desc = "The nanites store the host's ID access rights in a subdermal magnetic strip. Updates when triggered, copying the host's current access." diff --git a/code/modules/research/rdconsole.dm b/code/modules/research/rdconsole.dm index 163cf949b33..ecf325ff854 100644 --- a/code/modules/research/rdconsole.dm +++ b/code/modules/research/rdconsole.dm @@ -1,3 +1,5 @@ +#define RND_TECH_DISK "tech" +#define RND_DESIGN_DISK "design" /* Research and Development (R&D) Console @@ -19,34 +21,25 @@ Nothing else in the console has ID requirements. desc = "A console used to interface with R&D tools." icon_screen = "rdcomp" icon_keyboard = "rd_key" - var/datum/techweb/stored_research //Reference to global science techweb. - var/obj/item/disk/tech_disk/t_disk //Stores the technology disk. - var/obj/item/disk/design_disk/d_disk //Stores the design disk. circuit = /obj/item/circuitboard/computer/rdconsole - - req_access = list(ACCESS_RND) //lA AND SETTING MANIPULATION REQUIRES SCIENTIST ACCESS. - - //UI VARS - var/screen = RDSCREEN_MENU - var/back = RDSCREEN_MENU + req_access = list(ACCESS_RND) // Locking and unlocking the console requires science access + /// Reference to global science techweb + var/datum/techweb/stored_research + /// The stored technology disk, if present + var/obj/item/disk/tech_disk/t_disk + /// The stored design disk, if present + var/obj/item/disk/design_disk/d_disk + /// Determines if the console is locked, and consequently if actions can be performed with it var/locked = FALSE - var/tdisk_uple = FALSE - var/ddisk_uple = FALSE - var/datum/selected_node_id - var/datum/selected_design_id - var/selected_category - var/disk_slot_selected - var/searchstring = "" - var/searchtype = "" - var/ui_mode = RDCONSOLE_UI_MODE_NORMAL - - var/research_control = TRUE + /// Used for compressing data sent to the UI via static_data as payload size is of concern + var/id_cache = list() + /// Sequence var for the id cache + var/id_cache_seq = 1 /proc/CallMaterialName(ID) if (istype(ID, /datum/material)) var/datum/material/material = ID return material.name - else if(GLOB.chemical_reagents_list[ID]) var/datum/reagent/reagent = GLOB.chemical_reagents_list[ID] return reagent.name @@ -69,12 +62,6 @@ Nothing else in the console has ID requirements. return ..() /obj/machinery/computer/rdconsole/attackby(obj/item/D, mob/user, params) - if(istype(D, /obj/item/research_notes)) - var/obj/item/research_notes/R = D - SSresearch.science_tech.add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = R.value)) - playsound(src,'sound/machines/copier.ogg', 100, TRUE) - qdel(R) - return TRUE //Loading a disk into it. if(istype(D, /obj/item/disk)) if(istype(D, /obj/item/disk/tech_disk)) @@ -147,417 +134,267 @@ Nothing else in the console has ID requirements. locked = FALSE return ..() -/obj/machinery/computer/rdconsole/proc/list_categories(list/categories, menu_num as num) - if(!categories) - return +/obj/machinery/computer/rdconsole/ui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "Techweb") + ui.open() - var/line_length = 1 - var/list/l = "" +/obj/machinery/computer/rdconsole/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/research_designs) + ) - for(var/C in categories) - if(line_length > 2) - l += "" - line_length = 1 +// heavy data from this proc should be moved to static data when possible +/obj/machinery/computer/rdconsole/ui_data(mob/user) + . = list( + "nodes" = list(), + "experiments" = list(), + "researched_designs" = stored_research.researched_designs, + "points" = stored_research.research_points, + "points_last_tick" = stored_research.last_bitcoins, + "web_org" = stored_research.organization, + "sec_protocols" = !(obj_flags & EMAGGED), + "t_disk" = null, + "d_disk" = null, + "locked" = locked + ) - l += "" - line_length++ - - l += "
[C]
" - return l - -/obj/machinery/computer/rdconsole/proc/ui_header() - var/list/l = list() - var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/research_designs) - l += "[sheet.css_tag()][RDSCREEN_NOBREAK]" - l += "
[stored_research.organization] Research and Development Network" - l += "Available points:
[techweb_point_display_rdconsole(stored_research.research_points, stored_research.last_bitcoins)]" - l += "Security protocols: [obj_flags & EMAGGED ? "Disabled" : "Enabled"]" - l += "Main Menu | Back
[RDSCREEN_NOBREAK]" - l += "[ui_mode == 1? "Normal View" : "Normal View"] | [ui_mode == 2? "Expert View" : "Expert View"] | [ui_mode == 3? "List View" : "List View"]" - return l - -/obj/machinery/computer/rdconsole/proc/ui_main_menu() - var/list/l = list() - if(research_control) - l += "

Technology" - if(d_disk) - l += "
Design Disk" - if(t_disk) - l += "
Tech Disk" - l += "
Settings

" - return l - -/obj/machinery/computer/rdconsole/proc/ui_locked() - return list("

SYSTEM LOCKED


") - -/obj/machinery/computer/rdconsole/proc/ui_settings() - var/list/l = list() - l += "

R&D Console Settings:

" - l += "Lock Console
" - return l - -/obj/machinery/computer/rdconsole/proc/ui_techdisk() //Legacy code - RDSCREEN_UI_TDISK_CHECK - var/list/l = list() - l += "
Disk Operations: Clear Disk" - l += "Eject Disk" - l += "Upload All" - l += "Load Technology to Disk
" - l += "

Stored Technology Nodes:

" - for(var/i in t_disk.stored_research.researched_nodes) - var/datum/techweb_node/N = SSresearch.techweb_node_by_id(i) - l += "[N.display_name]" - l += "
" - return l - -/obj/machinery/computer/rdconsole/proc/ui_designdisk() //Legacy code - RDSCREEN_UI_DDISK_CHECK - var/list/l = list() - l += "Disk Operations: Clear DiskUpload AllEject Disk" - for(var/i in 1 to d_disk.max_blueprints) - l += "
" - if(d_disk.blueprints[i]) - var/datum/design/D = d_disk.blueprints[i] - l += "[D.name]" - l += "Operations: Upload to database Clear Slot" - else - l += "Empty Slot Operations: Load Design to Slot" - l += "
" - return l - -/obj/machinery/computer/rdconsole/proc/ui_designdisk_upload() //Legacy code - RDSCREEN_UI_DDISK_CHECK - var/list/l = list() - l += "Return to Disk Operations
" - l += "

Load Design to Disk:

" - for(var/v in stored_research.researched_designs) - var/datum/design/D = SSresearch.techweb_design_by_id(v) - l += "[D.name] " - l += "Copy to Disk" - l += "
" - return l - -/obj/machinery/computer/rdconsole/proc/ui_techweb() - var/list/l = list() - if(ui_mode != RDCONSOLE_UI_MODE_LIST) - var/list/columns = list() - var/max_tier = 0 - for (var/node_ in stored_research.tiers) - var/datum/techweb_node/node = SSresearch.techweb_node_by_id(node_) - var/tier = stored_research.tiers[node.id] - LAZYINITLIST(columns["[tier]"]) // String hackery to make the numbers associative - columns["[tier]"] += ui_techweb_single_node(node, minimal=(tier != 1)) - max_tier = max(max_tier, tier) - - l += "[RDSCREEN_NOBREAK]" - if(max_tier) - for(var/tier in 0 to max_tier) - l += "[RDSCREEN_NOBREAK]" - l += "
ResearchedAvailableFuture
[RDSCREEN_NOBREAK]" - l += columns["[tier]"] - l += "
[RDSCREEN_NOBREAK]" - else - var/list/avail = list() //This could probably be optimized a bit later. - var/list/unavail = list() - var/list/res = list() - for(var/v in stored_research.researched_nodes) - res += SSresearch.techweb_node_by_id(v) - for(var/v in stored_research.available_nodes) - if(stored_research.researched_nodes[v] || stored_research.hidden_nodes[v]) - continue - avail += SSresearch.techweb_node_by_id(v) - for(var/v in stored_research.visible_nodes) - if(stored_research.available_nodes[v]) - continue - unavail += SSresearch.techweb_node_by_id(v) - l += "

Technology Nodes:

[RDSCREEN_NOBREAK]" - l += "

Available for Research:

" - for(var/datum/techweb_node/N in avail) - var/not_unlocked = (stored_research.available_nodes[N.id] && !stored_research.researched_nodes[N.id]) - var/has_points = (stored_research.can_afford(N.get_price(stored_research))) - var/research_href = not_unlocked? (has_points? "Research" : "Not Enough Points") : null - l += "[N.display_name][research_href]" - l += "

Locked Nodes:

" - for(var/datum/techweb_node/N in unavail) - l += "[N.display_name]" - l += "

Researched Nodes:

" - for(var/datum/techweb_node/N in res) - l += "[N.display_name]" - l += "
[RDSCREEN_NOBREAK]" - return l - -/obj/machinery/computer/rdconsole/proc/machine_icon(atom/item) - return icon2html(initial(item.icon), usr, initial(item.icon_state), SOUTH) - -/obj/machinery/computer/rdconsole/proc/ui_techweb_single_node(datum/techweb_node/node, selflink=TRUE, minimal=FALSE) - var/list/l = list() - if (stored_research.hidden_nodes[node.id]) - return l - var/display_name = node.display_name - if (selflink) - display_name = "[display_name]" - l += "
[display_name] [RDSCREEN_NOBREAK]" - if(minimal) - l += "
[node.description]" - else - if(stored_research.researched_nodes[node.id]) - l += "Researched" - else if(stored_research.available_nodes[node.id]) - if(stored_research.can_afford(node.get_price(stored_research))) - l += "
[node.price_display(stored_research)]" + if (t_disk) + .["t_disk"] = list ( + "stored_research" = t_disk.stored_research.researched_nodes + ) + if (d_disk) + .["d_disk"] = list ( + "max_blueprints" = d_disk.max_blueprints, + "blueprints" = list() + ) + for (var/i in 1 to d_disk.max_blueprints) + if (d_disk.blueprints[i]) + var/datum/design/D = d_disk.blueprints[i] + .["d_disk"]["blueprints"] += D.id else - l += "
[node.price_display(stored_research)]" // gray - too expensive - else - l += "
[node.price_display(stored_research)]" // red - missing prereqs - if(ui_mode == RDCONSOLE_UI_MODE_NORMAL) - l += "[node.description]" - for(var/i in node.design_ids) - var/datum/design/D = SSresearch.techweb_design_by_id(i) - l += "[D.icon_html(usr)][RDSCREEN_NOBREAK]" - l += "
[RDSCREEN_NOBREAK]" - return l + .["d_disk"]["blueprints"] += null -/obj/machinery/computer/rdconsole/proc/ui_techweb_nodeview() - var/datum/techweb_node/selected_node = SSresearch.techweb_node_by_id(selected_node_id) - RDSCREEN_UI_SNODE_CHECK - var/list/l = list() - if(stored_research.hidden_nodes[selected_node.id]) - l += "

ERROR: RESEARCH NODE UNKNOWN.

" + + // Serialize all nodes to display + for(var/v in stored_research.tiers) + var/datum/techweb_node/n = SSresearch.techweb_node_by_id(v) + + // Ensure node is supposed to be visible + if (stored_research.hidden_nodes[v]) + continue + + .["nodes"] += list(list( + "id" = n.id, + "can_unlock" = stored_research.can_unlock_node(n), + "tier" = stored_research.tiers[n.id] + )) + + // Get experiments and serialize them + var/list/exp_to_process = stored_research.available_experiments.Copy() + for (var/e in stored_research.completed_experiments) + exp_to_process += stored_research.completed_experiments[e] + for (var/e in exp_to_process) + var/datum/experiment/ex = e + .["experiments"][ex.type] = list( + "name" = ex.name, + "description" = ex.description, + "tag" = ex.exp_tag, + "progress" = ex.check_progress(), + "completed" = ex.completed, + "performance_hint" = ex.performance_hint + ) + +/** + * Compresses an ID to an integer representation using the id_cache, used for deduplication + * in sent JSON payloads + * + * Arguments: + * * id - the ID to compress + */ +/obj/machinery/computer/rdconsole/proc/compress_id(id) + if (!id_cache[id]) + id_cache[id] = id_cache_seq + id_cache_seq += 1 + return id_cache[id] + +/obj/machinery/computer/rdconsole/ui_static_data(mob/user) + . = list( + "static_data" = list() + ) + + // Build node cache... + // Note this looks a bit ugly but its to reduce the size of the JSON payload + // by the greatest amount that we can, as larger JSON payloads result in + // hanging when the user opens the UI + var/node_cache = list() + for (var/node_id in SSresearch.techweb_nodes) + var/datum/techweb_node/node = SSresearch.techweb_nodes[node_id] || SSresearch.error_node + var/compressed_id = "[compress_id(node.id)]" + node_cache[compressed_id] = list( + "name" = node.display_name, + "description" = node.description + ) + if (LAZYLEN(node.research_costs)) + node_cache[compressed_id]["costs"] = list() + for (var/node_cost in node.research_costs) + node_cache[compressed_id]["costs"]["[compress_id(node_cost)]"] = node.research_costs[node_cost] + if (LAZYLEN(node.prereq_ids)) + node_cache[compressed_id]["prereq_ids"] = list() + for (var/prerequisite_node in node.prereq_ids) + node_cache[compressed_id]["prereq_ids"] += compress_id(prerequisite_node) + if (LAZYLEN(node.design_ids)) + node_cache[compressed_id]["design_ids"] = list() + for (var/unlocked_design in node.design_ids) + node_cache[compressed_id]["design_ids"] += compress_id(unlocked_design) + if (LAZYLEN(node.unlock_ids)) + node_cache[compressed_id]["unlock_ids"] = list() + for (var/unlocked_node in node.unlock_ids) + node_cache[compressed_id]["unlock_ids"] += compress_id(unlocked_node) + if (LAZYLEN(node.required_experiments)) + node_cache[compressed_id]["required_experiments"] = node.required_experiments + if (LAZYLEN(node.discount_experiments)) + node_cache[compressed_id]["discount_experiments"] = node.discount_experiments + + // Build design cache + var/design_cache = list() + var/datum/asset/spritesheet/research_designs/spritesheet = get_asset_datum(/datum/asset/spritesheet/research_designs) + var/size32x32 = "[spritesheet.name]32x32" + for (var/design_id in SSresearch.techweb_designs) + var/datum/design/design = SSresearch.techweb_designs[design_id] || SSresearch.error_design + var/compressed_id = "[compress_id(design.id)]" + var/size = spritesheet.icon_size_id(design.id) + design_cache[compressed_id] = list( + design.name, + "[size == size32x32 ? "" : "[size] "][design.id]" + ) + + // Ensure id cache is included for decompression + var/flat_id_cache = list() + for (var/id in id_cache) + flat_id_cache += id + + .["static_data"] = list( + "node_cache" = node_cache, + "design_cache" = design_cache, + "id_cache" = flat_id_cache + ) + +/obj/machinery/computer/rdconsole/ui_act(action, list/params) + . = ..() + if (.) return - l += "[RDSCREEN_NOBREAK]" - if (length(selected_node.prereq_ids)) - l += "[RDSCREEN_NOBREAK]" - l += "[RDSCREEN_NOBREAK]" - if (length(selected_node.unlock_ids)) - l += "[RDSCREEN_NOBREAK]" - - l += "[RDSCREEN_NOBREAK]" - if (length(selected_node.prereq_ids)) - l += "[RDSCREEN_NOBREAK]" - l += "[RDSCREEN_NOBREAK]" - if (length(selected_node.unlock_ids)) - l += "[RDSCREEN_NOBREAK]" - - l += "
RequiresCurrent NodeUnlocks
[RDSCREEN_NOBREAK]" - for (var/i in selected_node.prereq_ids) - l += ui_techweb_single_node(SSresearch.techweb_node_by_id(i)) - l += "[RDSCREEN_NOBREAK]" - l += ui_techweb_single_node(selected_node, selflink=FALSE) - l += "[RDSCREEN_NOBREAK]" - for (var/i in selected_node.unlock_ids) - l += ui_techweb_single_node(SSresearch.techweb_node_by_id(i)) - l += "
[RDSCREEN_NOBREAK]" - return l - -/obj/machinery/computer/rdconsole/proc/ui_techweb_designview() //Legacy code - var/datum/design/selected_design = SSresearch.techweb_design_by_id(selected_design_id) - RDSCREEN_UI_SDESIGN_CHECK - var/list/l = list() - l += "
[selected_design.icon_html(usr)][selected_design.name]
[RDSCREEN_NOBREAK]" - if(selected_design.build_type) - var/lathes = list() - if(selected_design.build_type & IMPRINTER) - lathes += "[machine_icon(/obj/machinery/rnd/production/circuit_imprinter)][RDSCREEN_NOBREAK]" - if(selected_design.build_type & PROTOLATHE) - lathes += "[machine_icon(/obj/machinery/rnd/production/protolathe)][RDSCREEN_NOBREAK]" - if(selected_design.build_type & AUTOLATHE) - lathes += "[machine_icon(/obj/machinery/autolathe)][RDSCREEN_NOBREAK]" - if(selected_design.build_type & MECHFAB) - lathes += "[machine_icon(/obj/machinery/mecha_part_fabricator)][RDSCREEN_NOBREAK]" - if(selected_design.build_type & BIOGENERATOR) - lathes += "[machine_icon(/obj/machinery/biogenerator)][RDSCREEN_NOBREAK]" - if(selected_design.build_type & LIMBGROWER) - lathes += "[machine_icon(/obj/machinery/limbgrower)][RDSCREEN_NOBREAK]" - if(selected_design.build_type & SMELTER) - lathes += "[machine_icon(/obj/machinery/mineral/processing_unit)][RDSCREEN_NOBREAK]" - l += "Construction types:" - l += lathes - l += "" - l += "Required materials:" - var/all_mats = selected_design.materials + selected_design.reagents_list - for(var/M in all_mats) - l += "* [CallMaterialName(M)] x [all_mats[M]]" - l += "Unlocked by:" - for (var/i in selected_design.unlocked_by) - l += ui_techweb_single_node(SSresearch.techweb_node_by_id(i)) - l += "[RDSCREEN_NOBREAK]
" - return l - -//Fuck TGUI. -/obj/machinery/computer/rdconsole/proc/generate_ui() - var/list/ui = list() - ui += ui_header() - if(locked) - ui += ui_locked() - else - switch(screen) - if(RDSCREEN_MENU) - ui += ui_main_menu() - if(RDSCREEN_TECHWEB) - ui += ui_techweb() - if(RDSCREEN_TECHWEB_NODEVIEW) - ui += ui_techweb_nodeview() - if(RDSCREEN_TECHWEB_DESIGNVIEW) - ui += ui_techweb_designview() - if(RDSCREEN_DESIGNDISK) - ui += ui_designdisk() - if(RDSCREEN_DESIGNDISK_UPLOAD) - ui += ui_designdisk_upload() - if(RDSCREEN_TECHDISK) - ui += ui_techdisk() - if(RDSCREEN_SETTINGS) - ui += ui_settings() - - for(var/i in 1 to length(ui)) - if(!findtextEx(ui[i], RDSCREEN_NOBREAK)) - ui[i] += "
" - . = ui.Join("") - return replacetextEx(., RDSCREEN_NOBREAK, "") - -/obj/machinery/computer/rdconsole/Topic(raw, ls) - if(..()) - return add_fingerprint(usr) - usr.set_machine(src) - if(ls["switch_screen"]) - back = screen - screen = text2num(ls["switch_screen"]) - if(ls["ui_mode"]) - ui_mode = text2num(ls["ui_mode"]) - if(ls["lock_console"]) - if(obj_flags & EMAGGED) - to_chat(usr, "Security protocol error: Unable to lock.") - return - if(allowed(usr)) - lock_console(usr) - else - to_chat(usr, "Unauthorized Access.") - if(ls["unlock_console"]) - if(allowed(usr)) - unlock_console(usr) - else - to_chat(usr, "Unauthorized Access.") - if(ls["back_screen"]) - back = text2num(ls["back_screen"]) - if(ls["category"]) - selected_category = ls["category"] - if(ls["eject_design"]) //Eject the design disk. - eject_disk("design") - screen = RDSCREEN_MENU - say("Ejecting Design Disk") - if(ls["eject_tech"]) //Eject the technology disk. - eject_disk("tech") - screen = RDSCREEN_MENU - say("Ejecting Technology Disk") - if(ls["disk_slot"]) - disk_slot_selected = text2num(ls["disk_slot"]) - if(ls["research_node"]) - if(!research_control) - return //honestly should call them out for href exploiting :^) - if(!SSresearch.science_tech.available_nodes[ls["research_node"]]) - return //Nope! - research_node(ls["research_node"], usr) - if(ls["clear_tech"]) //Erase la on the technology disk. - if(QDELETED(t_disk)) - say("No Technology Disk Inserted!") - return - qdel(t_disk.stored_research) - t_disk.stored_research = new - say("Wiping technology disk.") - if(ls["copy_tech"]) //Copy some technology la from the research holder to the disk. - if(QDELETED(t_disk)) - say("No Technology Disk Inserted!") - return - stored_research.copy_research_to(t_disk.stored_research) - screen = RDSCREEN_TECHDISK - say("Downloading to technology disk.") - if(ls["clear_design"]) //Erases la on the design disk. - if(QDELETED(d_disk)) - say("No Design Disk Inserted!") - return - var/n = text2num(ls["clear_design"]) - if(!n) - for(var/i in 1 to d_disk.max_blueprints) - d_disk.blueprints[i] = null - say("Wiping design disk.") - else + + // Check if the console is locked to block any actions occuring + if (locked && action != "toggleLock") + say("Console is locked, cannot perform further actions.") + return TRUE + + switch (action) + if ("toggleLock") + if(obj_flags & EMAGGED) + to_chat(usr, "Security protocol error: Unable to access locking protocols.") + return TRUE + if(allowed(usr)) + locked = !locked + else + to_chat(usr, "Unauthorized Access.") + return TRUE + if ("researchNode") + if(!SSresearch.science_tech.available_nodes[params["node_id"]]) + return TRUE + research_node(params["node_id"], usr) + return TRUE + if ("ejectDisk") + eject_disk(params["type"]) + return TRUE + if ("writeDesign") + if(QDELETED(d_disk)) + say("No Design Disk Inserted!") + return TRUE + var/slot = text2num(params["slot"]) + var/datum/design/design = SSresearch.techweb_design_by_id(params["selectedDesign"]) + if(design) + var/autolathe_friendly = TRUE + if(design.reagents_list.len) + autolathe_friendly = FALSE + design.category -= "Imported" + else + for(var/material in design.materials) + if( !(material in list(/datum/material/iron, /datum/material/glass))) + autolathe_friendly = FALSE + design.category -= "Imported" + + if(design.build_type & (AUTOLATHE|PROTOLATHE)) // Specifically excludes circuit imprinter and mechfab + design.build_type = autolathe_friendly ? (design.build_type | AUTOLATHE) : design.build_type + design.category |= "Imported" + d_disk.blueprints[slot] = design + return TRUE + if ("uploadDesignSlot") + if(QDELETED(d_disk)) + say("No design disk found.") + return TRUE + var/n = text2num(params["slot"]) + stored_research.add_design(d_disk.blueprints[n], TRUE) + return TRUE + if ("clearDesignSlot") + if(QDELETED(d_disk)) + say("No design disk inserted!") + return TRUE + var/n = text2num(params["slot"]) var/datum/design/D = d_disk.blueprints[n] say("Wiping design [D.name] from design disk.") d_disk.blueprints[n] = null - if(ls["updt_tech"]) //Uple the research holder with information from the technology disk. - if(QDELETED(t_disk)) - say("No Technology Disk Inserted!") - return - say("Uploading technology disk.") - t_disk.stored_research.copy_research_to(stored_research) - if(ls["copy_design"]) //Copy design from the research holder to the design disk. - if(QDELETED(d_disk)) - say("No Design Disk Inserted!") - return - var/slot = text2num(ls["copy_design"]) - var/datum/design/D = SSresearch.techweb_design_by_id(ls["copy_design_ID"]) - if(D) - var/autolathe_friendly = TRUE - if(D.reagents_list.len) - autolathe_friendly = FALSE - D.category -= "Imported" - else - for(var/x in D.materials) - if( !(x in list(/datum/material/iron, /datum/material/glass))) - autolathe_friendly = FALSE - D.category -= "Imported" - - if(D.build_type & (AUTOLATHE|PROTOLATHE)) // Specifically excludes circuit imprinter and mechfab - D.build_type = autolathe_friendly ? (D.build_type | AUTOLATHE) : D.build_type - D.category |= "Imported" - d_disk.blueprints[slot] = D - screen = RDSCREEN_DESIGNDISK - if(ls["view_node"]) - selected_node_id = ls["view_node"] - screen = RDSCREEN_TECHWEB_NODEVIEW - if(ls["view_design"]) - selected_design_id = ls["view_design"] - screen = RDSCREEN_TECHWEB_DESIGNVIEW - if(ls["updt_design"]) //Uploads a design from disk to the techweb. - if(QDELETED(d_disk)) - say("No design disk found.") - return - var/n = text2num(ls["updt_design"]) - if(!n) - for(var/D in d_disk.blueprints) - if(D) - stored_research.add_design(D, TRUE) - else - stored_research.add_design(d_disk.blueprints[n], TRUE) - - updateUsrDialog() - -/obj/machinery/computer/rdconsole/ui_interact(mob/user) - . = ..() - var/datum/browser/popup = new(user, "rndconsole", name, 900, 600) - popup.add_stylesheet("techwebs", 'html/browser/techwebs.css') - popup.set_content(generate_ui()) - popup.open() - -/obj/machinery/computer/rdconsole/proc/tdisk_uple_complete() - tdisk_uple = FALSE - updateUsrDialog() - -/obj/machinery/computer/rdconsole/proc/ddisk_uple_complete() - ddisk_uple = FALSE - updateUsrDialog() + return TRUE + if ("eraseDisk") + if (params["type"] == RND_DESIGN_DISK) + if(QDELETED(d_disk)) + say("No design disk inserted!") + return TRUE + say("Wiping design disk.") + for(var/i in 1 to d_disk.max_blueprints) + d_disk.blueprints[i] = null + if (params["type"] == RND_TECH_DISK) + if(QDELETED(t_disk)) + say("No tech disk inserted!") + return TRUE + qdel(t_disk.stored_research) + t_disk.stored_research = new + say("Wiping technology disk.") + return TRUE + if ("uploadDisk") + if (params["type"] == RND_DESIGN_DISK) + if(QDELETED(d_disk)) + say("No design disk inserted!") + return TRUE + for(var/D in d_disk.blueprints) + if(D) + stored_research.add_design(D, TRUE) + if (params["type"] == RND_TECH_DISK) + if (QDELETED(t_disk)) + say("No tech disk inserted!") + return TRUE + say("Uploading technology disk.") + t_disk.stored_research.copy_research_to(stored_research) + return TRUE + if ("loadTech") + if(QDELETED(t_disk)) + say("No tech disk inserted!") + return + stored_research.copy_research_to(t_disk.stored_research) + say("Downloading to technology disk.") + return TRUE /obj/machinery/computer/rdconsole/proc/eject_disk(type) - if(type == "design") + if(type == RND_DESIGN_DISK && d_disk) d_disk.forceMove(get_turf(src)) d_disk = null - if(type == "tech") + if(type == RND_TECH_DISK && t_disk) t_disk.forceMove(get_turf(src)) t_disk = null -/obj/machinery/computer/rdconsole/proc/lock_console(mob/user) - locked = TRUE - -/obj/machinery/computer/rdconsole/proc/unlock_console(mob/user) - locked = FALSE +#undef RND_TECH_DISK +#undef RND_DESIGN_DISK diff --git a/code/modules/research/research_disk.dm b/code/modules/research/research_disk.dm index 62eca2aedf7..3b1e3e928e2 100644 --- a/code/modules/research/research_disk.dm +++ b/code/modules/research/research_disk.dm @@ -40,66 +40,3 @@ /obj/item/disk/tech_disk/spaceloot/Initialize() . = ..() stored_research = new /datum/techweb/bepis(remove_tech = FALSE) - -/obj/item/research_notes - name = "research notes" - desc = "Valuable scientific data. Use it in a research console to scan it." - icon = 'icons/obj/bureaucracy.dmi' - icon_state = "paper" - inhand_icon_state = "paper" - w_class = WEIGHT_CLASS_SMALL - ///research points it holds - var/value = 69 - ///origin of the research - var/origin_type = "debug" - ///if it ws merged with different origins to apply a bonus - var/mixed = FALSE - -/obj/item/research_notes/Initialize(mapload, _value, _origin_type) - . = ..() - if(_value) - value = _value - if(_origin_type) - origin_type = _origin_type - change_vol() - -/obj/item/research_notes/examine(mob/user) - . = ..() - . += "It is worth [value] research points." - -/// proc that changes name and icon depending on value -/obj/item/research_notes/proc/change_vol() - if(value >= 10000) - name = "revolutionary discovery in the field of [origin_type]" - icon_state = "docs_verified" - return - else if(value >= 2500) - name = "essay about [origin_type]" - icon_state = "paper_words" - return - else if(value >= 100) - name = "notes of [origin_type]" - icon_state = "paperslip_words" - return - else - name = "fragmentary data of [origin_type]" - icon_state = "scrap" - return - -///proc when you slap research notes into another one, it applies a bonus if they are of different origin (only applied once) -/obj/item/research_notes/proc/merge(obj/item/research_notes/new_paper) - var/bonus = min(value , new_paper.value) - value = value + new_paper.value - if(origin_type != new_paper.origin_type && !mixed) - value += bonus * 0.3 - origin_type = "[origin_type] and [new_paper.origin_type]" - mixed = TRUE - change_vol() - qdel(new_paper) - -/obj/item/research_notes/attackby(obj/item/I, mob/user, params) - . = ..() - if(istype(I, /obj/item/research_notes)) - var/obj/item/research_notes/R = I - merge(R) - return TRUE diff --git a/code/modules/research/server.dm b/code/modules/research/server.dm index 3c3bb3cc7ff..e8bf8f3ddcc 100644 --- a/code/modules/research/server.dm +++ b/code/modules/research/server.dm @@ -69,12 +69,6 @@ research_disabled = !research_disabled refresh_working() -/obj/machinery/rnd/server/proc/mine() - . = base_mining_income - var/penalty = max((get_env_temp() - temp_tolerance_high), 0) * temp_penalty_coefficient - current_temp = get_env_temp() - . = max(. - penalty, 0) - /obj/machinery/rnd/server/proc/get_env_temp() var/turf/open/L = loc if(isturf(L)) diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm index dfba6fa3408..6982395aed1 100644 --- a/code/modules/research/techweb/_techweb.dm +++ b/code/modules/research/techweb/_techweb.dm @@ -1,27 +1,47 @@ - -//Used \n[\s]*origin_tech[\s]*=[\s]*"[\S]+" to delete all origin techs. -//Or \n[\s]*origin_tech[\s]*=[\s]list\([A-Z_\s=0-9,]*\) -//Used \n[\s]*req_tech[\s]*=[\s]*list\(["a-z\s=0-9,]*\) to delete all req_techs. - -//Techweb datums are meant to store unlocked research, being able to be stored on research consoles, servers, and disks. They are NOT global. +/** + * # Techweb + * + * A datum representing a research techweb + * + * Techweb datums are meant to store unlocked research, being able to be stored + * on research consoles, servers, and disks. They are NOT global. + */ /datum/techweb - var/list/researched_nodes = list() //Already unlocked and all designs are now available. Assoc list, id = TRUE - var/list/visible_nodes = list() //Visible nodes, doesn't mean it can be researched. Assoc list, id = TRUE - var/list/available_nodes = list() //Nodes that can immediately be researched, all reqs met. assoc list, id = TRUE - var/list/researched_designs = list() //Designs that are available for use. Assoc list, id = TRUE - var/list/custom_designs = list() //Custom inserted designs like from disks that should survive recalculation. - var/list/boosted_nodes = list() //Already boosted nodes that can't be boosted again. node id = path of boost object. - var/list/hidden_nodes = list() //Hidden nodes. id = TRUE. Used for unhiding nodes when requirements are met by removing the entry of the node. - var/list/deconstructed_items = list() //items already deconstructed for a generic point boost. path = list(point_type = points) - var/list/research_points = list() //Available research points. type = number + /// Already unlocked and all designs are now available. Assoc list, id = TRUE + var/list/researched_nodes = list() + /// Visible nodes, doesn't mean it can be researched. Assoc list, id = TRUE + var/list/visible_nodes = list() + /// Nodes that can immediately be researched, all reqs met. assoc list, id = TRUE + var/list/available_nodes = list() + /// Designs that are available for use. Assoc list, id = TRUE + var/list/researched_designs = list() + /// Custom inserted designs like from disks that should survive recalculation. + var/list/custom_designs = list() + /// Already boosted nodes that can't be boosted again. node id = path of boost object. + var/list/boosted_nodes = list() + /// Hidden nodes. id = TRUE. Used for unhiding nodes when requirements are met by removing the entry of the node. + var/list/hidden_nodes = list() + /// Items already deconstructed for a generic point boost, path = list(point_type = points) + var/list/deconstructed_items = list() + /// Available research points, type = number + var/list/research_points = list() var/list/obj/machinery/computer/rdconsole/consoles_accessing = list() var/id = "generic" - var/list/research_logs = list() //IC logs. + /// IC logs + var/list/research_logs = list() var/largest_bomb_value = 0 - var/organization = "Third-Party" //Organization name, used for display. - var/list/last_bitcoins = list() //Current per-second production, used for display only. - var/list/discovered_mutations = list() //Mutations discovered by genetics, this way they are shared and cant be destroyed by destroying a single console - var/list/tiers = list() //Assoc list, id = number, 1 is available, 2 is all reqs are 1, so on + /// Organization name, used for display + var/organization = "Third-Party" + /// Current per-second production, used for display only. + var/list/last_bitcoins = list() + /// Mutations discovered by genetics, this way they are shared and cant be destroyed by destroying a single console + var/list/discovered_mutations = list() + /// Assoc list, id = number, 1 is available, 2 is all reqs are 1, so on + var/list/tiers = list() + /// Available experiments + var/list/available_experiments = list() + /// Completed experiments + var/list/completed_experiments = list() /datum/techweb/New() SSresearch.techwebs += src @@ -206,6 +226,69 @@ return FALSE return TRUE +/** + * Checks if all experiments have been completed for a given node on this techweb + * + * Arguments: + * * node - the node to check + */ +/datum/techweb/proc/have_experiments_for_node(datum/techweb_node/node) + . = TRUE + for (var/experiment_type in node.required_experiments) + if (!completed_experiments[experiment_type]) + return FALSE + +/** + * Checks if a node can be unlocked on this techweb, having the required points and experiments + * + * Arguments: + * * node - the node to check + */ +/datum/techweb/proc/can_unlock_node(datum/techweb_node/node) + return can_afford(node.get_price(src)) && have_experiments_for_node(node) + +/** + * Adds an experiment to this techweb by its type, ensures that no duplicates are added. + * + * Arguments: + * * experiment_type - the type of the experiment to add + */ +/datum/techweb/proc/add_experiment(experiment_type) + . = TRUE + // check active experiments for experiment of this type + for (var/available_experiment in available_experiments) + var/datum/experiment/experiment = available_experiment + if (experiment.type == experiment_type) + return FALSE + // check completed experiments for experiments of this type + for (var/completed_experiment in completed_experiments) + var/datum/experiment/experiment = completed_experiment + if (experiment == experiment_type) + return FALSE + available_experiments += new experiment_type() + +/** + * Adds a list of experiments to this techweb by their types, ensures that no duplicates are added. + * + * Arguments: + * * experiment_list - the list of types of experiments to add + */ +/datum/techweb/proc/add_experiments(list/experiment_list) + . = TRUE + for (var/experiment_type in experiment_list) + var/datum/experiment/experiment = experiment_type + . = . && add_experiment(experiment) + +/** + * Notifies the techweb that an experiment has been completed, updating internal state of the techweb to reflect this. + * + * Arguments: + * * completed_experiment - the experiment which was completed + */ +/datum/techweb/proc/complete_experiment(datum/experiment/completed_experiment) + available_experiments -= completed_experiment + completed_experiments[completed_experiment.type] = completed_experiment + /datum/techweb/proc/printout_points() return techweb_point_display_generic(research_points) @@ -217,21 +300,25 @@ return FALSE update_node_status(node) if(!force) - if(!available_nodes[node.id] || (auto_adjust_cost && (!can_afford(node.get_price(src))))) + if(!available_nodes[node.id] || (auto_adjust_cost && (!can_afford(node.get_price(src)))) || !have_experiments_for_node(node)) return FALSE if(auto_adjust_cost) remove_point_list(node.get_price(src)) researched_nodes[node.id] = TRUE //Add to our researched list for(var/id in node.unlock_ids) visible_nodes[id] = TRUE - update_node_status(SSresearch.techweb_node_by_id(id)) + var/datum/techweb_node/unlocked_node = SSresearch.techweb_node_by_id(id) + if (unlocked_node.required_experiments.len > 0) + add_experiments(unlocked_node.required_experiments) + if (unlocked_node.discount_experiments.len > 0) + add_experiments(unlocked_node.discount_experiments) + update_node_status(unlocked_node) for(var/id in node.design_ids) add_design_by_id(id) update_node_status(node) if(get_that_dosh) - var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_SCI) - if(D) - D.adjust_money(SSeconomy.techweb_bounty) + var/datum/bank_account/science_department_bank_account = SSeconomy.get_dep_account(ACCOUNT_SCI) + science_department_bank_account?.adjust_money(SSeconomy.techweb_bounty) return TRUE /datum/techweb/science/research_node(datum/techweb_node/node, force = FALSE, auto_adjust_cost = TRUE, get_that_dosh = TRUE) //When something is researched, triggers the proc for this techweb only diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm index 04f66bcdcde..0dcac24bb57 100644 --- a/code/modules/research/techweb/_techweb_node.dm +++ b/code/modules/research/techweb/_techweb_node.dm @@ -1,21 +1,41 @@ - -//Techweb nodes are GLOBAL, there should only be one instance of them in the game. Persistant changes should never be made to them in-game. -//USE SSRESEARCH PROCS TO OBTAIN REFERENCES. DO NOT REFERENCE OUTSIDE OF SSRESEARCH OR YOU WILL FUCK UP GC. - +/** + * # Techweb Node + * + * A datum representing a researchable node in the techweb. + * + * Techweb nodes are GLOBAL, there should only be one instance of them in the game. Persistant + * changes should never be made to them in-game. USE SSRESEARCH PROCS TO OBTAIN REFERENCES. + * DO NOT REFERENCE OUTSIDE OF SSRESEARCH OR YOU WILL FUCK UP GC. + */ /datum/techweb_node + /// Internal ID of the node var/id + /// The name of the node as it is shown on UIs var/display_name = "Errored Node" + /// A description of the node to show on UIs var/description = "Why are you seeing this?" - var/hidden = FALSE //Whether it starts off hidden. - var/experimental = FALSE //If the tech can be randomly granted by the BEPIS as a reward. Meant to be fully given in tech disks, not researched. - var/starting_node = FALSE //Whether it's available without any research. + /// Whether it starts off hidden + var/hidden = FALSE + /// If the tech can be randomly generated by the BEPIS as a reward. MEant to be fully given in tech disks, not researched + var/experimental = FALSE + /// Whether it's available without any research + var/starting_node = FALSE var/list/prereq_ids = list() var/list/design_ids = list() - var/list/unlock_ids = list() //CALCULATED FROM OTHER NODE'S PREREQUISITES. Assoc list id = TRUE. - var/list/boost_item_paths = list() //Associative list, path = list(point type = point_value). - var/autounlock_by_boost = TRUE //boosting this will autounlock this node. - var/list/research_costs = list() //Point cost to research. type = amount - var/category = "Misc" //Category + /// CALCULATED FROM OTHER NODE'S PREREQUISITIES. Associated list id = TRUE + var/list/unlock_ids = list() + /// Associative list, path = list(point type = point_value) + var/list/boost_item_paths = list() + /// Boosting this will autounlock this node + var/autounlock_by_boost = TRUE + /// The points cost to research the node, type = amount + var/list/research_costs = list() + /// The category of the node + var/category = "Misc" + /// The list of experiments required to research the node + var/list/required_experiments = list() + /// If completed, these experiments give a specific point amount discount to the node.area + var/list/discount_experiments = list() /datum/techweb_node/error_node id = "ERROR" @@ -48,6 +68,7 @@ VARSET_TO_LIST(., autounlock_by_boost) VARSET_TO_LIST(., research_costs) VARSET_TO_LIST(., category) + VARSET_TO_LIST(., required_experiments) /datum/techweb_node/deserialize_list(list/input, list/options) if(!input["id"]) @@ -63,6 +84,7 @@ VARSET_FROM_LIST(input, autounlock_by_boost) VARSET_FROM_LIST(input, research_costs) VARSET_FROM_LIST(input, category) + VARSET_FROM_LIST(input, required_experiments) Initialize() return src @@ -83,10 +105,14 @@ if(host) var/list/actual_costs = research_costs if(host.boosted_nodes[id]) - var/list/L = host.boosted_nodes[id] - for(var/i in L) - if(actual_costs[i]) - actual_costs[i] -= L[i] + var/list/boostlist = host.boosted_nodes[id] + for(var/booster in boostlist) + if(actual_costs[booster]) + actual_costs[booster] -= boostlist[booster] + for(var/cost_type in actual_costs) + for(var/experiment_type in discount_experiments) + if(host.completed_experiments[experiment_type]) //do we have this discount_experiment unlocked? + actual_costs[cost_type] -= discount_experiments[experiment_type] return actual_costs else return research_costs diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm index 860164f8617..7c92f140d71 100644 --- a/code/modules/research/techweb/all_nodes.dm +++ b/code/modules/research/techweb/all_nodes.dm @@ -10,7 +10,7 @@ // Default research tech, prevents bricking design_ids = list("basic_matter_bin", "basic_cell", "basic_scanning", "basic_capacitor", "basic_micro_laser", "micro_mani", "basic_electrolite", "c-reader", "desttagger", "salestagger", "handlabel", "packagewrap", "destructive_analyzer", "circuit_imprinter", "experimentor", "rdconsole", "bepis", "design_disk", "tech_disk", "rdserver", "rdservercontrol", "mechfab", - "paystand", "space_heater", "bucket", "sec_rshot", "sec_beanbag_slug", "sec_Islug", "sec_dart", "sec_38", "rglass", "plasteel", + "paystand", "space_heater", "bucket", "sec_rshot", "sec_beanbag_slug", "sec_Islug", "sec_dart", "sec_38", "rglass", "plasteel", "experi_scanner", "destructive_scanner", "doppler_array", "plastitanium", "plasmaglass", "plasmareinforcedglass", "titaniumglass", "plastitaniumglass", "plastic_knife", "plastic_fork", "plastic_spoon", "conveyor_belt", "conveyor_switch") /datum/techweb_node/mmi @@ -62,6 +62,7 @@ "dropper", "defibmountdefault", "surgical_tape", "portable_chem_mixer") /////////////////////////Biotech///////////////////////// + /datum/techweb_node/biotech id = "biotech" display_name = "Biological Technology" @@ -69,6 +70,7 @@ prereq_ids = list("base") design_ids = list("chem_heater", "chem_master", "chem_dispenser", "pandemic", "defibrillator", "defibmount", "operating", "soda_dispenser", "beer_dispenser", "healthanalyzer", "medigel","genescanner", "med_spray_bottle", "chem_pack", "blood_pack", "medical_kiosk", "crewpinpointerprox", "medipen_refiller", "biopsy_tool", "plumbing_rcd_sci") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + required_experiments = list(/datum/experiment/scanning/points/slime/calibration) /datum/techweb_node/adv_biotech id = "adv_biotech" @@ -76,7 +78,9 @@ description = "Advanced Biotechnology" prereq_ids = list("biotech") design_ids = list("piercesyringe", "crewpinpointer", "smoke_machine", "plasmarefiller", "limbgrower", "meta_beaker", "healthanalyzer_advanced", "harvester", "holobarrier_med", "detective_scanner", "defibrillator_compact", "ph_meter") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000) + required_experiments = list(/datum/experiment/scanning/points/slime/easy) + discount_experiments = list(/datum/experiment/scanning/random/material/meat = 4000) //Big discount to reinforce doing it. /datum/techweb_node/bio_process id = "bio_process" @@ -84,9 +88,11 @@ description = "From slimes to kitchens." prereq_ids = list("biotech") design_ids = list("smartfridge", "gibber", "deepfryer", "monkey_recycler", "processor", "gibber", "microwave", "reagentgrinder", "dish_drive", "fat_sucker", "griddle") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 4000) + discount_experiments = list(/datum/experiment/scanning/random/cytology = 3000) //Big discount to reinforce doing it. /////////////////////////Advanced Surgery///////////////////////// + /datum/techweb_node/imp_wt_surgery id = "imp_wt_surgery" display_name = "Improved Wound-Tending Surgery" @@ -121,6 +127,7 @@ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000) /////////////////////////data theory tech///////////////////////// + /datum/techweb_node/datatheory //Computer science id = "datatheory" display_name = "Data Theory" @@ -131,6 +138,7 @@ /////////////////////////engineering tech///////////////////////// + /datum/techweb_node/engineering id = "engineering" display_name = "Industrial Engineering" @@ -140,7 +148,8 @@ "atmosalerts", "atmos_control", "recycler", "autolathe", "high_micro_laser", "nano_mani", "mesons", "welding_goggles", "thermomachine", "rad_collector", "tesla_coil", "grounding_rod", "apc_control", "cell_charger", "power control", "airlock_board", "firelock_board", "airalarm_electronics", "firealarm_electronics", "cell_charger", "stack_console", "stack_machine", "oxygen_tank", "plasma_tank", "emergency_oxygen", "emergency_oxygen_engi", "plasmaman_tank_belt", "electrolyzer", "adv_electrolite", "pneumatic_seal") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 7500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 12500) + discount_experiments = list(/datum/experiment/scanning/random/material/easy = 7500) /datum/techweb_node/adv_engi id = "adv_engi" @@ -149,7 +158,8 @@ prereq_ids = list("engineering", "emp_basic") design_ids = list("engine_goggles", "magboots", "forcefield_projector", "weldingmask", "rcd_loaded", "rpd_loaded", "sheetifier", "HFR_core", "HFR_fuel_input", "HFR_waste_output", "HFR_moderator_input", "HFR_corner", "HFR_interface") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000) + discount_experiments = list(/datum/experiment/scanning/random/material/medium/one = 4000) /datum/techweb_node/anomaly id = "anomaly_research" @@ -377,7 +387,8 @@ description = "For the slackers on the station." prereq_ids = list("comptech") design_ids = list("arcade_battle", "arcade_orion", "slotmachine") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1000) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 3250) + discount_experiments = list(/datum/experiment/physical/arcade_winner = 3000) /datum/techweb_node/comp_recordkeeping id = "comp_recordkeeping" @@ -493,7 +504,8 @@ description = "Efficiency Level 127" //dumb mc references prereq_ids = list("basic_mining", "adv_engi", "adv_power", "adv_plasma") design_ids = list("drill_diamond", "jackhammer", "hypermod", "plasmacutter_adv") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 7500) + discount_experiments = list(/datum/experiment/scanning/random/material/hard/one = 5000) /datum/techweb_node/janitor id = "janitor" @@ -501,7 +513,8 @@ description = "Clean things better, faster, stronger, and harder!" prereq_ids = list("adv_engi") design_ids = list("holobarrier_jani", "advmop", "buffer", "blutrash", "light_replacer", "spraybottle", "beartrap", "paint_remover") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 4000) + discount_experiments = list(/datum/experiment/scanning/random/janitor_trash = 3000) //75% discount for scanning some trash, seems fair right? /datum/techweb_node/botany id = "botany" @@ -517,7 +530,8 @@ description = "Highly advanced tools." design_ids = list("exwelder", "jawsoflife", "handdrill", "laserscalpel", "mechanicalpinches", "searingtool", "gene_shears") prereq_ids = list("adv_engi") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 7500) + discount_experiments = list(/datum/experiment/scanning/random/material/hard/one = 5000) /datum/techweb_node/sec_basic id = "sec_basic" @@ -541,7 +555,8 @@ description = "Unlocks new RCD designs." design_ids = list("rcd_upgrade_silo_link") prereq_ids = list("rcd_upgrade", "bluespace_travel") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000) + discount_experiments = list(/datum/experiment/scanning/random/material/hard/two = 5000) /////////////////////////weaponry tech///////////////////////// /datum/techweb_node/weaponry @@ -551,6 +566,7 @@ prereq_ids = list("engineering") design_ids = list("pin_testing", "tele_shield") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000) + required_experiments = list(/datum/experiment/explosion/calibration) /datum/techweb_node/adv_weaponry id = "adv_weaponry" @@ -559,6 +575,7 @@ prereq_ids = list("adv_engi", "weaponry") design_ids = list("pin_loyalty") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 10000) + required_experiments = list(/datum/experiment/explosion/medium) /datum/techweb_node/electric_weapons id = "electronic_weapons" @@ -599,6 +616,7 @@ prereq_ids = list("adv_weaponry") design_ids = list("large_Grenade", "pyro_Grenade", "adv_Grenade") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + required_experiments = list(/datum/experiment/explosion/maxcap) /datum/techweb_node/ballistic_weapons id = "ballistic_weapons" @@ -631,7 +649,8 @@ description = "For when you just aren't Gundam enough." prereq_ids = list("adv_robotics") design_ids = list("mech_repair_droid") - research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 7500) + discount_experiments = list(/datum/experiment/scanning/random/material/medium/three = 5000) /datum/techweb_node/odysseus id = "mecha_odysseus" @@ -821,7 +840,7 @@ prereq_ids = list("datatheory") design_ids = list("nanite_disk","nanite_remote","nanite_comm_remote","nanite_scanner",\ "nanite_chamber","public_nanite_chamber","nanite_chamber_control","nanite_programmer","nanite_program_hub","nanite_cloud_control",\ - "relay_nanites", "monitoring_nanites", "research_nanites" ,"researchplus_nanites", "access_nanites", "repairing_nanites","sensor_nanite_volume", "repeater_nanites", "relay_repeater_nanites","debugging_nanites") + "relay_nanites", "monitoring_nanites", "access_nanites", "repairing_nanites","sensor_nanite_volume", "repeater_nanites", "relay_repeater_nanites","debugging_nanites") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1000) /datum/techweb_node/nanite_smart @@ -935,6 +954,7 @@ /obj/item/cautery/alien, /obj/item/surgicaldrill/alien, /obj/item/screwdriver/abductor, /obj/item/wrench/abductor, /obj/item/crowbar/abductor, /obj/item/multitool/abductor, /obj/item/weldingtool/abductor, /obj/item/wirecutters/abductor, /obj/item/circuitboard/machine/abductor, /obj/item/melee/baton/abductor, /obj/item/abductor, /obj/item/gun/energy/shrink_ray) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + required_experiments = list(/datum/experiment/scanning/points/slime/hard) hidden = TRUE /datum/techweb_node/alien_engi diff --git a/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm b/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm index d0ad4963046..2a4573938f9 100644 --- a/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm +++ b/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm @@ -99,6 +99,7 @@ for(var/created_thing in resulting_atoms) for(var/x in 1 to resulting_atoms[created_thing]) var/atom/thing = new created_thing(get_turf(vat)) + ADD_TRAIT(thing, TRAIT_VATGROWN, "vatgrowing") vat.visible_message("[thing] pops out of [vat]!") QDEL_NULL(vat.biological_sample) //Kill off the sample, we're done diff --git a/code/modules/research/xenobiology/xenobio_camera.dm b/code/modules/research/xenobiology/xenobio_camera.dm index 2e8b3152fb0..fa2b5d579ad 100644 --- a/code/modules/research/xenobiology/xenobio_camera.dm +++ b/code/modules/research/xenobiology/xenobio_camera.dm @@ -165,28 +165,6 @@ current_potion = O to_chat(user, "You load [O] in the console's potion slot[replaced ? ", replacing the one that was there before" : ""].") return - else if(istype(O, /obj/item/slime_extract)) - var/obj/item/slime_extract/E = O - if(!SSresearch.slime_already_researched[E.type]) - if(!E.research) - playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 3, -1) - visible_message("[src] buzzes and displays a message: Invalid extract! (You shouldn't be seeing this. If you are, tell someone.)") - return - if(E.Uses <= 0) - playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 3, -1) - visible_message("[src] buzzes and displays a message: Extract consumed - no research available.") - return - else - playsound(src, 'sound/machines/ping.ogg', 50, 3, -1) - visible_message("You insert [E] into a slot on the [src]. It pings and prints out some research notes worth [E.research] points!") - new /obj/item/research_notes(drop_location(), E.research, "xenobiology") - SSresearch.slime_already_researched[E.type] = TRUE - qdel(O) - return - else - visible_message("[src] buzzes and displays a message: Slime extract already researched!") - playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 3, -1) - return ..() /obj/machinery/computer/camera_advanced/xenobio/multitool_act(mob/living/user, obj/item/multitool/I) diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index 428d5fcae28..1789609da15 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -16,8 +16,6 @@ var/effectmod ///Which type of crossbred var/list/activate_reagents = list() ///Reagents required for activation var/recurring = FALSE - /// Research point value for slime cores. These are defines stored in [code/__DEFINES/research.dm] - the actual values are updated there. - var/research /obj/item/slime_extract/examine(mob/user) . = ..() @@ -94,7 +92,6 @@ icon_state = "grey slime extract" effectmod = "reproductive" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_0 /obj/item/slime_extract/grey/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -120,7 +117,8 @@ icon_state = "gold slime extract" effectmod = "symbiont" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_4 + + /obj/item/slime_extract/gold/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -150,7 +148,8 @@ icon_state = "silver slime extract" effectmod = "consuming" activate_reagents = list(/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_2 + + /obj/item/slime_extract/silver/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -176,7 +175,6 @@ icon_state = "metal slime extract" effectmod = "industrial" activate_reagents = list(/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_1 /obj/item/slime_extract/metal/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -201,7 +199,6 @@ icon_state = "purple slime extract" effectmod = "regenerative" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_1 /obj/item/slime_extract/purple/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -221,7 +218,6 @@ icon_state = "dark purple slime extract" effectmod = "self-sustaining" activate_reagents = list(/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_2 /obj/item/slime_extract/darkpurple/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -245,7 +241,6 @@ icon_state = "orange slime extract" effectmod = "burning" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_1 /obj/item/slime_extract/orange/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -266,7 +261,6 @@ icon_state = "yellow slime extract" effectmod = "charged" activate_reagents = list(/datum/reagent/blood,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_2 /obj/item/slime_extract/yellow/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -290,7 +284,6 @@ icon_state = "red slime extract" effectmod = "sanguine" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_4 /obj/item/slime_extract/red/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -311,7 +304,6 @@ icon_state = "blue slime extract" effectmod = "stabilized" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_1 /obj/item/slime_extract/blue/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -332,7 +324,6 @@ icon_state = "dark blue slime extract" effectmod = "chilling" activate_reagents = list(/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_2 /obj/item/slime_extract/darkblue/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -356,7 +347,6 @@ icon_state = "pink slime extract" effectmod = "gentle" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_4 /obj/item/slime_extract/pink/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -385,7 +375,6 @@ icon_state = "green slime extract" effectmod = "mutative" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,/datum/reagent/uranium/radium) - research = SLIME_RESEARCH_TIER_4 /obj/item/slime_extract/green/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -410,7 +399,6 @@ icon_state = "light pink slime extract" effectmod = "loyal" activate_reagents = list(/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_5 /obj/item/slime_extract/lightpink/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -435,7 +423,6 @@ icon_state = "black slime extract" effectmod = "transformative" activate_reagents = list(/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_5 /obj/item/slime_extract/black/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -457,7 +444,6 @@ icon_state = "oil slime extract" effectmod = "detonating" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_5 /obj/item/slime_extract/oil/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -481,7 +467,6 @@ icon_state = "adamantine slime extract" effectmod = "crystalline" activate_reagents = list(/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_5 /obj/item/slime_extract/adamantine/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -515,7 +500,6 @@ var/teleport_x = 0 var/teleport_y = 0 var/teleport_z = 0 - research = SLIME_RESEARCH_TIER_3 /obj/item/slime_extract/bluespace/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -548,7 +532,6 @@ icon_state = "pyrite slime extract" effectmod = "prismatic" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_3 /obj/item/slime_extract/pyrite/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -576,7 +559,6 @@ icon_state = "cerulean slime extract" effectmod = "recurring" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma) - research = SLIME_RESEARCH_TIER_3 /obj/item/slime_extract/cerulean/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -597,7 +579,6 @@ icon_state = "sepia slime extract" effectmod = "lengthened" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,/datum/reagent/water) - research = SLIME_RESEARCH_TIER_3 /obj/item/slime_extract/sepia/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) @@ -620,7 +601,6 @@ icon_state = "rainbow slime extract" effectmod = "hyperchromatic" activate_reagents = list(/datum/reagent/blood,/datum/reagent/toxin/plasma,"lesser plasma",/datum/reagent/toxin/slimejelly,"holy water and uranium") //Curse this snowflake reagent list. - research = SLIME_RESEARCH_TIER_RAINBOW /obj/item/slime_extract/rainbow/activate(mob/living/carbon/human/user, datum/species/jelly/luminescent/species, activation_type) switch(activation_type) diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index fc1db4510fe..e3ca964256e 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -75,6 +75,7 @@ */ /datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action) // If UI is not interactive or usr calling Topic is not the UI user, bail. if(!ui || ui.status != UI_INTERACTIVE) return TRUE diff --git a/icons/mob/inhands/misc/devices_lefthand.dmi b/icons/mob/inhands/misc/devices_lefthand.dmi index b84d8a8978c..3c52a437283 100644 Binary files a/icons/mob/inhands/misc/devices_lefthand.dmi and b/icons/mob/inhands/misc/devices_lefthand.dmi differ diff --git a/icons/mob/inhands/misc/devices_righthand.dmi b/icons/mob/inhands/misc/devices_righthand.dmi index 47e3260c6ef..e4c57d5ec55 100644 Binary files a/icons/mob/inhands/misc/devices_righthand.dmi and b/icons/mob/inhands/misc/devices_righthand.dmi differ diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi index f9b9286c062..46007b1adaf 100644 Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ diff --git a/icons/obj/machines/experisci.dmi b/icons/obj/machines/experisci.dmi new file mode 100644 index 00000000000..9db58dbfbf7 Binary files /dev/null and b/icons/obj/machines/experisci.dmi differ diff --git a/icons/obj/modular_tablet.dmi b/icons/obj/modular_tablet.dmi index 9ccbef928b9..6b3dc82dda1 100644 Binary files a/icons/obj/modular_tablet.dmi and b/icons/obj/modular_tablet.dmi differ diff --git a/sound/machines/destructive_scanner/ScanDangerous.ogg b/sound/machines/destructive_scanner/ScanDangerous.ogg new file mode 100644 index 00000000000..8f625a2b0f1 Binary files /dev/null and b/sound/machines/destructive_scanner/ScanDangerous.ogg differ diff --git a/sound/machines/destructive_scanner/ScanSafe.ogg b/sound/machines/destructive_scanner/ScanSafe.ogg new file mode 100644 index 00000000000..afc7c91f3ac Binary files /dev/null and b/sound/machines/destructive_scanner/ScanSafe.ogg differ diff --git a/sound/machines/destructive_scanner/TubeDown.ogg b/sound/machines/destructive_scanner/TubeDown.ogg new file mode 100644 index 00000000000..18690dd4422 Binary files /dev/null and b/sound/machines/destructive_scanner/TubeDown.ogg differ diff --git a/sound/machines/destructive_scanner/TubeUp.ogg b/sound/machines/destructive_scanner/TubeUp.ogg new file mode 100644 index 00000000000..411e2f1d279 Binary files /dev/null and b/sound/machines/destructive_scanner/TubeUp.ogg differ diff --git a/tgstation.dme b/tgstation.dme index f13ce032382..da87f450d9a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -55,6 +55,7 @@ #include "code\__DEFINES\economy.dm" #include "code\__DEFINES\events.dm" #include "code\__DEFINES\exosuit_fab.dm" +#include "code\__DEFINES\experisci.dm" #include "code\__DEFINES\exports.dm" #include "code\__DEFINES\fantasy_affixes.dm" #include "code\__DEFINES\flags.dm" @@ -2049,6 +2050,19 @@ #include "code\modules\events\wizard\rpgloot.dm" #include "code\modules\events\wizard\shuffle.dm" #include "code\modules\events\wizard\summons.dm" +#include "code\modules\experisci\destructive_scanner.dm" +#include "code\modules\experisci\handheld_scanner.dm" +#include "code\modules\experisci\experiment\experiments.dm" +#include "code\modules\experisci\experiment\physical_experiments.dm" +#include "code\modules\experisci\experiment\handlers\experiment_handler.dm" +#include "code\modules\experisci\experiment\types\experiment.dm" +#include "code\modules\experisci\experiment\types\explosive.dm" +#include "code\modules\experisci\experiment\types\physical_experiment.dm" +#include "code\modules\experisci\experiment\types\random_scanning.dm" +#include "code\modules\experisci\experiment\types\scanning.dm" +#include "code\modules\experisci\experiment\types\scanning_material.dm" +#include "code\modules\experisci\experiment\types\scanning_points.dm" +#include "code\modules\experisci\experiment\types\scanning_vatgrown.dm" #include "code\modules\fields\fields.dm" #include "code\modules\fields\gravity.dm" #include "code\modules\fields\peaceborg_dampener.dm" @@ -2688,6 +2702,7 @@ #include "code\modules\modular_computers\file_system\programs\robotact.dm" #include "code\modules\modular_computers\file_system\programs\secureye.dm" #include "code\modules\modular_computers\file_system\programs\sm_monitor.dm" +#include "code\modules\modular_computers\file_system\programs\techweb.dm" #include "code\modules\modular_computers\file_system\programs\antagonist\contract_uplink.dm" #include "code\modules\modular_computers\file_system\programs\antagonist\dos.dm" #include "code\modules\modular_computers\file_system\programs\antagonist\revelation.dm" @@ -3016,6 +3031,7 @@ #include "code\modules\research\designs\comp_board_designs.dm" #include "code\modules\research\designs\computer_part_designs.dm" #include "code\modules\research\designs\electronics_designs.dm" +#include "code\modules\research\designs\experisci_designs.dm" #include "code\modules\research\designs\limbgrower_designs.dm" #include "code\modules\research\designs\machine_designs.dm" #include "code\modules\research\designs\mecha_designs.dm" diff --git a/tgui/packages/tgui/interfaces/ExperimentConfigure.js b/tgui/packages/tgui/interfaces/ExperimentConfigure.js new file mode 100644 index 00000000000..22993558c1f --- /dev/null +++ b/tgui/packages/tgui/interfaces/ExperimentConfigure.js @@ -0,0 +1,237 @@ +import { Window } from '../layouts'; +import { useBackend } from '../backend'; +import { Section, Box, Button, Flex, Icon, LabeledList, Table, Tooltip } from '../components'; +import { sortBy } from 'common/collections'; + +const ExperimentStages = props => { + return ( + + {props.children.map((stage, idx) => + ())} +
+ ); +}; + +const ExperimentStageRow = props => { + const [type, description, value, altValue] = props; + + // Determine completion based on type of stage + let completion = false; + switch (type) { + case "bool": + case "detail": + completion = value; + break; + case "integer": + completion = value === altValue; + break; + case "float": + completion = value >= 1; + break; + } + + return ( + + + {(type === "bool" && ) + || (type === "integer" && `${value}/${altValue}`) + || (type === "float" && `${value * 100}%`) + || (type === "detail" && "⤷")} + + + {description} + + + ); +}; + +export const TechwebServer = (props, context) => { + const { act, data } = useBackend(context); + const { servers } = props; + + return ( + + + + {servers[0].web_id} / {servers[0].web_org} + + + + + )} + + + + ); +}; + +export const Experiment = (props, context) => { + const { act, data } = useBackend(context); + const { + exp, + controllable, + } = props; + const { + name, + description, + tag, + selectable, + selected, + progress, + performance_hint, + ref, + } = exp; + + return ( + + + + + {description} + + {props.children} + + {progress} + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/NtosTechweb.js b/tgui/packages/tgui/interfaces/NtosTechweb.js new file mode 100644 index 00000000000..a7ff19c8f02 --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosTechweb.js @@ -0,0 +1,13 @@ +import { AppTechweb } from './Techweb.js'; +import { useBackend, useLocalState } from '../backend'; +import { createLogger } from '../logging'; + +const logger = createLogger('backend'); + +export const NtosTechweb = (props, context) => { + const { config, data, act } = useBackend(context); + logger.log(config.AppTechweb); + return ( + + ); +}; diff --git a/tgui/packages/tgui/interfaces/TachyonArray.js b/tgui/packages/tgui/interfaces/TachyonArray.js index 7dde7e98990..87c3395edfa 100644 --- a/tgui/packages/tgui/interfaces/TachyonArray.js +++ b/tgui/packages/tgui/interfaces/TachyonArray.js @@ -6,19 +6,34 @@ export const TachyonArray = (props, context) => { const { act, data } = useBackend(context); const { records = [], + is_research, } = data; return ( + width={550} + height={250} + resizable> - {!records.length ? ( - - No Records - - ) : ( - - )} + + + {!records.length ? ( + + No Records + + ) : ( + + )} + + {!!is_research && ( + + + + )} + ); @@ -37,7 +52,7 @@ export const TachyonArrayContent = (props, context) => { return record.name === activeRecordName; }); return ( -
+
@@ -53,7 +68,7 @@ export const TachyonArrayContent = (props, context) => { {activeRecord ? ( - +
remappingIdCache[id]; + +const selectRemappedStaticData = data => { + // Handle reshaping of node cache to fill in unsent fields, and + // decompress the node IDs + const node_cache = {}; + for (let id of Object.keys(data.static_data.node_cache)) { + const node = data.static_data.node_cache[id]; + const costs = Object.keys(node.costs || {}).map(x => ({ + type: remapId(x), + value: node.costs[x], + })); + node_cache[remapId(id)] = { + ...node, + id: remapId(id), + costs, + prereq_ids: map(remapId)(node.prereq_ids || []), + design_ids: map(remapId)(node.design_ids || []), + unlock_ids: map(remapId)(node.unlock_ids || []), + required_experiments: node.required_experiments || [], + discount_experiments: node.discount_experiments || [], + }; + } + + // Do the same as the above for the design cache + const design_cache = {}; + for (let id of Object.keys(data.static_data.design_cache)) { + const [name, classes] = data.static_data.design_cache[id]; + design_cache[remapId(id)] = { + name: name, + class: classes.startsWith("design") ? classes : `design32x32 ${classes}`, + }; + } + + return { + node_cache, + design_cache, + }; +}; + +let remappedStaticData; + +const useRemappedBackend = context => { + const { data, ...rest } = useBackend(context); + // Only remap the static data once, cache for future use + if (!remappedStaticData) { + const id_cache = data.static_data.id_cache; + for (let i = 0; i < id_cache.length; i++) { + remappingIdCache[i + 1] = id_cache[i]; + } + remappedStaticData = selectRemappedStaticData(data); + } + return { + data: { + ...data, + ...remappedStaticData, + }, + ...rest, + }; +}; + +// Utility Functions + +const abbreviations = { + "General Research": "Gen. Res.", + "Nanite Research": "Nanite Res.", +}; +const abbreviateName = name => abbreviations[name] ?? name; + +// Actual Components + +export const Techweb = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { + locked, + } = data; + return ( + + + {!!locked && ( + +
Console Locked
+ +
+ )} + +
+
+ ); +}; + +export const AppTechweb = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { + locked, + } = data; + return ( + + + {!!locked && ( + +
Console Locked
+ +
+ )} + +
+
+ ); +}; + +export const TechwebContent = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { + points, + points_last_tick, + web_org, + sec_protocols, + t_disk, + d_disk, + locked, + } = data; + const [ + techwebRoute, + setTechwebRoute, + ] = useLocalState(context, 'techwebRoute', null); + const [ + lastPoints, + setLastPoints, + ] = useLocalState(context, 'lastPoints', {}); + + return ( + + + + + + Available points: +
    + {Object.keys(points).map(k => ( +
  • + {k}: {points[k]} + {!!points_last_tick[k] && ( + ` (+${points_last_tick[k]}/sec)` + )} +
  • + ))} +
+
+ + Security protocols: + + {sec_protocols ? "Engaged" : "Disengaged"} + + +
+ + + + {d_disk && ( + + + + )} + {t_disk && ( + + + + )} + +
+
+ + + +
+ ); +}; + +const TechwebRouter = (props, context) => { + const [ + techwebRoute, + ] = useLocalState(context, 'techwebRoute', null); + + const route = techwebRoute?.route; + const RoutedComponent = ( + route === "details" && TechwebNodeDetail + || route === "disk" && TechwebDiskMenu + || TechwebOverview + ); + + return ( + + ); +}; + +const TechwebOverview = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { nodes, node_cache, design_cache } = data; + const [ + tabIndex, + setTabIndex, + ] = useLocalState(context, 'overviewTabIndex', 1); + const [ + searchText, + setSearchText, + ] = useLocalState(context, 'searchText'); + + // Only search when 3 or more characters have been input + const searching = searchText && searchText.trim().length > 1; + + let displayedNodes = nodes; + if (searching) { + displayedNodes = displayedNodes.filter(x => { + const n = node_cache[x.id]; + return n.name.toLowerCase().includes(searchText) + || n.description.toLowerCase().includes(searchText) + || n.design_ids.some(e => + design_cache[e].name.toLowerCase().includes(searchText)); + }); + } else { + displayedNodes = sortBy(x => node_cache[x.id].name)(tabIndex < 2 + ? nodes.filter(x => x.tier === tabIndex) + : nodes.filter(x => x.tier >= tabIndex)); + } + + const switchTab = tab => { + setTabIndex(tab); + setSearchText(null); + }; + + return ( + + + + + Web View + + + + switchTab(0)}> + Researched + + switchTab(1)}> + Available + + switchTab(2)}> + Future + + {!!searching && ( + + Search Results + + )} + + + + setSearchText(value)} + placeholder={"Search..."} /> + + + + + {displayedNodes.map(n => { + return ( + + ); + })} + + + ); +}; + +const TechwebNodeDetail = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { nodes } = data; + const { selectedNode } = props; + + const selectedNodeData = selectedNode + && nodes.find(x => x.id === selectedNode); + return ( + + ); +}; + +const TechwebDiskMenu = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { diskType } = props; + const { t_disk, d_disk } = data; + const [ + techwebRoute, + setTechwebRoute, + ] = useLocalState(context, 'techwebRoute', null); + + // Check for the disk actually being inserted + if ((diskType === "design" && !d_disk) || (diskType === "tech" && !t_disk)) { + return null; + } + + const DiskContent = diskType === "design" && TechwebDesignDisk + || TechwebTechDisk; + return ( + + + + + {diskType.charAt(0).toUpperCase() + diskType.slice(1)} Disk + + + + + Stored Data + + + + + {diskType === "tech" && ( + + )} + + + + + + + + + + + + ); +}; + +const TechwebDesignDisk = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { + design_cache, + researched_designs, + d_disk, + } = data; + const { blueprints } = d_disk; + const [ + selectedDesign, + setSelectedDesign, + ] = useLocalState(context, "designDiskSelect", null); + const [ + showModal, + setShowModal, + ] = useLocalState(context, 'showDesignModal', -1); + + const designIdByIdx = Object.keys(researched_designs); + const designOptions = flow([ + filter(x => x.toLowerCase() !== "error"), + map((id, idx) => `${design_cache[id].name} [${idx}]`), + sortBy(x => x), + ])(designIdByIdx); + + return ( + <> + {showModal >= 0 && ( + + + + Select a design to save... + + + { + const idx = parseInt(val.split('[').pop().split(']')[0], 10); + setSelectedDesign(designIdByIdx[idx]); + }} /> + + + + + + + + )} + {blueprints.map((x, i) => ( +
+ {x !== null && ( + + )} + + {x !== null && ( + + )} + + }> + {x === null && 'Empty' || ( + <> + Contains the design for {design_cache[x].name}:
+ + + )} +
+ ))} + + ); +}; + +const TechwebTechDisk = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { t_disk } = data; + const { stored_research } = t_disk; + + return Object.keys(stored_research).map(x => ({ id: x })).map(n => ( + + )); +}; + +const TechNodeDetail = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { + nodes, + node_cache, + } = data; + const { node } = props; + const { id } = node; + const { prereq_ids, unlock_ids } = node_cache[id]; + const [ + tabIndex, + setTabIndex, + ] = useLocalState(context, 'nodeDetailTabIndex', 0); + const [ + techwebRoute, + setTechwebRoute, + ] = useLocalState(context, 'techwebRoute', null); + + const prereqNodes = nodes.filter(x => prereq_ids.includes(x.id)); + const complPrereq = prereq_ids + .filter(x => nodes.find(y => y.id === x)?.tier === 0).length; + const unlockedNodes = nodes.filter(x => unlock_ids.includes(x.id)); + + return ( + + + + + Node + + + + setTabIndex(0)}> + Required ({complPrereq}/{prereqNodes.length}) + + setTabIndex(1)}> + Unlocks ({unlockedNodes.length}) + + + + + + + + + + + + + {tabIndex === 0 && ( + + {prereqNodes.map(n => ( + + ))} + + )} + {tabIndex === 1 && ( + + {unlockedNodes.map(n => ( + + ))} + + )} + + ); +}; + +const TechNode = (props, context) => { + const { act, data } = useRemappedBackend(context); + const { + node_cache, + design_cache, + experiments, + points, + nodes, + } = data; + const { node, nodetails, nocontrols } = props; + const { id, can_unlock, tier } = node; + const { + name, + description, + costs, + design_ids, + prereq_ids, + required_experiments, + discount_experiments, + } = node_cache[id]; + const [ + techwebRoute, + setTechwebRoute, + ] = useLocalState(context, 'techwebRoute', null); + const [ + tabIndex, + setTabIndex, + ] = useLocalState(context, 'nodeDetailTabIndex', 0); + + const expcompl = required_experiments + .filter(x => experiments[x]?.completed) + .length; + const experimentProgress = ( + + Experiments ({expcompl}/{required_experiments.length}) + + ); + + const techcompl = prereq_ids + .filter(x => nodes.find(y => y.id === x)?.tier === 0) + .length; + const techProgress = ( + + Tech ({techcompl}/{prereq_ids.length}) + + ); + + // Notice this logic will have te be changed if we make the discounts + // pool-specific + const nodeDiscount = Object.keys(discount_experiments) + .filter(x => experiments[x]?.completed) + .reduce((tot, curr) => { + return tot + discount_experiments[curr]; + }, 0); + + return ( +
+ {!nodetails && ( + + )} + {tier > 0 && ( + + )} + + )}> + {tier !== 0 && ( + + {costs.map(k => { + const reqPts = Math.max(0, k.value - nodeDiscount); + const nodeProg = Math.min(reqPts, points[k.type]) || 0; + return ( + + + {abbreviateName(k.type)} ({nodeProg}/{reqPts}) + + + ); + })} + {prereq_ids.length > 0 && ( + + {techProgress} + + )} + {required_experiments?.length > 0 && ( + + {experimentProgress} + + )} + + )} + + {description} + + + {design_ids.map((k, i) => ( +
+ ); +}; + +const LockedExperiment = (props, context) => { + return ( + + + + This experiment has not been discovered yet, continue researching + nodes in the tree to discover the contents of this experiment. + + + ); +}; diff --git a/tgui/packages/tgui/styles/interfaces/ExperimentConfigure.scss b/tgui/packages/tgui/styles/interfaces/ExperimentConfigure.scss new file mode 100644 index 00000000000..b4265b1fba5 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/ExperimentConfigure.scss @@ -0,0 +1,85 @@ +.ExperimentTechwebServer__Web, +.ExperimentConfigure__ExperimentPanel { + background: #000000; + border: 1px solid #40628a; + margin: 3px 0; +} + +.ExperimentTechwebServer__WebHeader { + background: #40628a; + padding: 2px; +} + +.ExperimentTechwebServer__WebName { + font-size: 18px; +} + +.ExperimentTechwebServer__WebContent { + padding: 4px; +} + +.ExperimentTechwebServer__WebContent > .LabeledList { + margin: 0.25rem 0.25rem 0.25rem 1rem; +} + +.ExperimentConfigure__ExperimentName { + font-weight: bold; + border-radius: 0; +} + +.ExperimentConfigure__ExperimentContent { + padding: 0.25rem 1.5rem 0.25rem 0.25rem; +} + +.ExperimentStage__Indicator { + font-weight: bold; + margin-right: 1rem; + text-align: center; +} + +.ExperimentStage__StageContainer.complete .ExperimentStage__Description { + opacity: 0.4; + text-decoration: line-through; +} + +.ExperimentStage__StageContainer { + margin-bottom: 5px; +} + +.ExperimentStage__Table { + border-collapse: separate; + border-spacing: 0.25rem 0.25rem; +} + +.ExperimentConfigure__PerformExperiment { + text-align: center; + padding: 0.75rem 0; +} + +.ExperimentConfigure__ExperimentsContainer { + height: 100%; + display: flex; + flex-direction: column; +} + +.ExperimentConfigure__ExperimentsContainer > :last-child { + flex: 1; + overflow-y: auto; +} + +// Required for the time being otherwise we'll end up coloring the tooltip +// by accident when we color the tag. +.ExperimentConfigure__TagContainer { + position: absolute; + right: 0; + top: 0; +} + +.ExperimentConfigure__PerformanceHint * { + position: absolute; + width: 100%; + height: 100%; + right: 0; + top: 0; + color: rgba(255, 255, 255, 0.5); +} diff --git a/tgui/packages/tgui/styles/interfaces/TachyonArray.scss b/tgui/packages/tgui/styles/interfaces/TachyonArray.scss new file mode 100644 index 00000000000..2aed9f94dcf --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/TachyonArray.scss @@ -0,0 +1,13 @@ +.TachyonArray__ActiveRecord { + margin: 0 0.5em 0 0.8em; +} + +.TachyonArray__Content { + overflow-x: hidden; + overflow-y: auto; +} + +.TachyonArray__ResearchFooter > * { + width: 100%; + text-align: center; +} diff --git a/tgui/packages/tgui/styles/interfaces/Techweb.scss b/tgui/packages/tgui/styles/interfaces/Techweb.scss new file mode 100644 index 00000000000..684267185d1 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/Techweb.scss @@ -0,0 +1,78 @@ +@use '../colors.scss'; + +.Techweb__NodeProgress { + margin-bottom: 1rem; +} + +.Techweb__NodeProgress > *:not(:last-child) { + margin-right: 0.4rem; +} + +.Techweb__DesignIcon { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + +.Techweb__OverviewNodes { + overflow-y: auto; + overflow-x: hidden; + padding-right: 6px; + padding-top: 4px; +} + +.Techweb__HeaderContent { + background-color: black; + padding: 6px; + border: 1px solid #40628a; +} + +.Techweb__HeaderContent > * > :not(:last-child) { + margin-bottom: 5px; +} + +.Techweb__HeaderSectionTabs { + margin-top: 8px; + background-color: black; + border: 1px solid #40628a; + padding-left: 5px; + padding-right: 5px; +} + +.Techweb__HeaderTabTitle { + border-right: 1px solid #40628a; + padding-right: 0.5em; + margin-right: 0.5em; + font-weight: bold; +} + +.Techweb__HeaderSectionTabs input { + background-color: rgba(255, 255, 255, 0.05); +} + +.Techweb__PointSummary { + list-style: none; + margin: 0.4em 0 0 1em; + padding: 0; +} + +.Techweb__SecProtocol { + color: colors.$bad; + margin-left: 0.2em; +} + +.Techweb__SecProtocol.engaged { + color: colors.$good; +} + +.Techweb__DesignModal > :not(:last-child) { + margin-bottom: 0.5em; +} + +.Techweb__LockedModal > :not(:last-child) { + margin-bottom: 0.5em; +} + +.Techweb__ExperimentDiscount { + color: colors.$label; + margin: 0.5em 0; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 03fd832d574..6124d863dee 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -45,10 +45,13 @@ @include meta.load-css('./interfaces/ListInput.scss'); @include meta.load-css('./interfaces/AlertModal.scss'); @include meta.load-css('./interfaces/CameraConsole.scss'); +@include meta.load-css('./interfaces/ExperimentConfigure.scss'); @include meta.load-css('./interfaces/NuclearBomb.scss'); @include meta.load-css('./interfaces/Paper.scss'); @include meta.load-css('./interfaces/Roulette.scss'); @include meta.load-css('./interfaces/Safe.scss'); +@include meta.load-css('./interfaces/TachyonArray.scss'); +@include meta.load-css('./interfaces/Techweb.scss'); // Layouts @include meta.load-css('./layouts/Layout.scss'); diff --git a/tools/ci/build_spaceman_dmm.sh b/tools/ci/build_spaceman_dmm.sh old mode 100644 new mode 100755 diff --git a/tools/ci/check_changelogs.sh b/tools/ci/check_changelogs.sh old mode 100644 new mode 100755 diff --git a/tools/ci/check_filedirs.sh b/tools/ci/check_filedirs.sh old mode 100644 new mode 100755 diff --git a/tools/ci/check_grep.sh b/tools/ci/check_grep.sh old mode 100644 new mode 100755 diff --git a/tools/ci/dm.sh b/tools/ci/dm.sh old mode 100644 new mode 100755 diff --git a/tools/ci/download_byond.sh b/tools/ci/download_byond.sh old mode 100644 new mode 100755 diff --git a/tools/ci/install_byond.sh b/tools/ci/install_byond.sh old mode 100644 new mode 100755 diff --git a/tools/ci/install_rust_g.sh b/tools/ci/install_rust_g.sh old mode 100644 new mode 100755 diff --git a/tools/ci/install_spaceman_dmm.sh b/tools/ci/install_spaceman_dmm.sh old mode 100644 new mode 100755 diff --git a/tools/ci/template_dm_generator.py b/tools/ci/template_dm_generator.py old mode 100644 new mode 100755