Experi-Sci: Techweb nodes may now require you to perform "scientific" experiments (#54093)

Co-authored-by: Brett Williams <bobbahbrown@gmail.com>
Co-authored-by: Jordan Brown <Cyberboss@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: Aleksej Komarov <stylemistake@gmail.com>
Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
This commit is contained in:
Qustinnus
2021-02-23 13:30:24 -08:00
committed by GitHub
co-authored by Brett Williams Jordan Brown ArcaneMusic Aleksej Komarov Mothblocks
parent 381b09fbd6
commit 931a32ffb3
82 changed files with 3349 additions and 808 deletions
@@ -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, "<span class='notice'>You disable the safety sensor BIOS on [src].</span>")
/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 ..()
@@ -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"
@@ -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, "<span class='notice'>[target] is not related to your currently selected experiment.</span>")
/**
* 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, "<span class='notice'>You do not have an experiment selected!</span>")
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, "<span class='notice'>You scan [target].</span>")
else
playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
to_chat(user, "<span class='notice'>[target] is not related to your currently selected experiment.</span>")
/**
* 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, "<span class='notice'>No experiment selected!</span>")
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, "<span class='notice'>The scan succeeds.</span>")
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)
@@ -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)
@@ -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)
@@ -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()
@@ -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
@@ -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
..()
@@ -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
@@ -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])
@@ -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])
@@ -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])
@@ -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)