code:
/datum/round_event_control/wizard/deprevolt //stationwide!
	name = "Departmental Uprising"
	weight = 0 //An order that requires order in a round of chaos was maybe not the best idea. Requiescat in pace departmental uprising August 2014 - March 2015 //hello motherfucker i fixed your shit in 2021
	typepath = /datum/round_event/wizard/deprevolt
	max_occurrences = 1
	earliest_start = 0 MINUTES

	var/picked_department
	var/announce = FALSE
	var/dangerous_nation = TRUE

/datum/round_event_control/wizard/deprevolt/admin_setup()
	if(!check_rights(R_FUN))
		return
	var/list/options = list("Random", "Uprising of Assistants", "Medical", "Engineering", "Science", "Supply", "Service", "Security")
	picked_department = input(usr,"Which department should revolt?","Select a department") as null|anything in options

	var/announce_question = tgui_alert(usr, "Announce This New Independent State?", "Secession", list("Announce", "No Announcement"))
	if(announce_question == "Announce")
		announce = TRUE

	var/dangerous_question = tgui_alert(usr, "Dangerous Nation? This means they will fight other nations.", "Conquest", list("Yes", "No"))
	if(dangerous_question == "No")
		dangerous_nation = FALSE

	//this is down here to allow the random system to pick a department whilst considering other independent departments
	if(!picked_department || picked_department == "Random")
		picked_department = null
		return

/datum/round_event/wizard/deprevolt/start()

	var/datum/round_event_control/wizard/deprevolt/event_control = control

	var/list/independent_departments = list() ///departments that are already independent, these will be disallowed to be randomly picked
	var/list/cannot_pick = list() ///departments that are already independent, these will be disallowed to be randomly picked
	for(var/datum/antagonist/separatist/separatist_datum in GLOB.antagonists)
		if(!separatist_datum.nation)
			continue
		independent_departments |= separatist_datum.nation
		cannot_pick |= separatist_datum.nation.nation_department

	var/announcement = event_control.announce
	var/dangerous = event_control.dangerous_nation
	var/department
	if(event_control.picked_department)
		department = event_control.picked_department
		event_control.picked_department = null
	else
		department = pick(list("Uprising of Assistants", "Medical", "Engineering", "Science", "Supply", "Service", "Security") - cannot_pick)
		if(!department)
			message_admins("Department Revolt could not create a nation, as all the departments are independent! You have created nations, you madman!")
	var/list/jobs_to_revolt = list()
	var/nation_name
	var/list/citizens = list()

	switch(department)
		if("Uprising of Assistants") //God help you
			jobs_to_revolt += "Assistant"
			nation_name = pick("Assa", "Mainte", "Tunnel", "Gris", "Grey", "Liath", "Grigio", "Ass", "Assi")
		if("Medical")
			var/datum/job_department/job_department = SSjob.get_department_type(/datum/job_department/medical)
			for(var/datum/job/job as anything in job_department.department_jobs)
				jobs_to_revolt += job.title
			nation_name = pick("Mede", "Healtha", "Recova", "Chemi", "Viro", "Psych")
		if("Engineering")
			var/datum/job_department/job_department = SSjob.get_department_type(/datum/job_department/engineering)
			for(var/datum/job/job as anything in job_department.department_jobs)
				jobs_to_revolt += job.title
			nation_name = pick("Atomo", "Engino", "Power", "Teleco")
		if("Science")
			var/datum/job_department/job_department = SSjob.get_department_type(/datum/job_department/science)
			for(var/datum/job/job as anything in job_department.department_jobs)
				jobs_to_revolt += job.title
			nation_name = pick("Sci", "Griffa", "Geneti", "Explosi", "Mecha", "Xeno", "Nani", "Cyto")
		if("Supply")
			var/datum/job_department/job_department = SSjob.get_department_type(/datum/job_department/cargo)
			for(var/datum/job/job as anything in job_department.department_jobs)
				jobs_to_revolt += job.title
			nation_name = pick("Cargo", "Guna", "Suppli", "Mule", "Crate", "Ore", "Mini", "Shaf")
		if("Service") //the few, the proud, the technically aligned
			var/datum/job_department/job_department = SSjob.get_department_type(/datum/job_department/service)
			for(var/datum/job/job as anything in job_department.department_jobs)
				jobs_to_revolt += job.title
			nation_name = pick("Honka", "Boozo", "Fatu", "Danka", "Mimi", "Libra", "Jani", "Religi")
		if("Security")
			var/datum/job_department/job_department = SSjob.get_department_type(/datum/job_department/security)
			for(var/datum/job/job as anything in job_department.department_jobs)
				jobs_to_revolt += job.title
			nation_name = pick("Securi", "Beepski", "Shitcuri", "Red", "Stunba", "Flashbango", "Flasha", "Stanfordi")

	nation_name += pick("stan", "topia", "land", "nia", "ca", "tova", "dor", "ador", "tia", "sia", "ano", "tica", "tide", "cis", "marea", "co", "taoide", "slavia", "stotzka")
	if(department == "Uprising of Assistants")
		var/prefix = pick("roving clans", "barbaric tribes", "tides", "bandit kingdom", "tribal society", "marauder clans", "horde")
		nation_name = "The [prefix] of [nation_name]"

	var/datum/team/nation/nation = new(null, jobs_to_revolt, department)
	nation.name = nation_name
	var/datum/team/department_target //dodges unfortunate runtime
	if(independent_departments.len)
		department_target = pick(independent_departments)
	nation.generate_nation_objectives(dangerous, department_target)

	for(var/mob/living/carbon/human/possible_separatist as anything in GLOB.human_list)
		if(!possible_separatist.mind)
			continue
		var/datum/mind/separatist_mind = possible_separatist.mind
		if(!(separatist_mind.assigned_role.title in jobs_to_revolt))
			continue
		citizens += possible_separatist
		separatist_mind.add_antag_datum(/datum/antagonist/separatist, nation, department)
		nation.add_member(separatist_mind)
		possible_separatist.log_message("Was made into a separatist, long live [nation_name]!", LOG_ATTACK, color="red")

	if(citizens.len)
		var/jobs_english_list = english_list(jobs_to_revolt)
		message_admins("The nation of [nation_name] has been formed. Affected jobs are [jobs_english_list]. Any new crewmembers with these jobs will join the secession.")
		if(announcement)
			var/announce_text = "The new independent state of [nation_name] has formed from the ashes of the [department] department!"
			if(department == "Uprising of Assistants") //the text didn't really work otherwise
				announce_text = "The assistants of the station have risen to form the new independent state of [nation_name]!"
			priority_announce(announce_text, "Secession from [GLOB.station_name]",  has_important_message = TRUE)
	else
		message_admins("The nation of [nation_name] did not have enough potential members to be created.")
		qdel(nation)


code:
/datum/round_event_control/wizard/embedpocalypse
	name = "Make Everything Embeddable"
	weight = 2
	typepath = /datum/round_event/wizard/embedpocalypse
	max_occurrences = 1
	earliest_start = 0 MINUTES

///behold... the only reason sticky is a subtype...
/datum/round_event_control/wizard/embedpocalypse/canSpawnEvent(players_amt, gamemode)
	if(GLOB.global_funny_embedding)
		return FALSE

/datum/round_event/wizard/embedpocalypse/start()
	GLOB.global_funny_embedding = new /datum/global_funny_embedding/pointy

/datum/round_event_control/wizard/embedpocalypse/sticky
	name = "Make Everything Sticky"
	weight = 6
	typepath = /datum/round_event/wizard/embedpocalypse/sticky
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event/wizard/embedpocalypse/sticky/start()
	GLOB.global_funny_embedding = new /datum/global_funny_embedding/sticky

///set this to a new instance of a SUBTYPE of global_funny_embedding. The main type is a prototype and will runtime really hard
GLOBAL_DATUM(global_funny_embedding, /datum/global_funny_embedding)

/**
 * ## global_funny_embedding!
 *
 * Stored in a global datum, and created when it is turned on via event or VV'ing the GLOB.embedpocalypse_controller to be a new /datum/global_funny_embedding.
 * Gives every item in the world a prefix to their name, and...
 * Makes every item in the world embed when thrown, but also hooks into global signals for new items created to also bless them with embed-ability(??).
 */
/datum/global_funny_embedding
	var/embed_type = EMBED_POINTY
	var/prefix = "error"

/datum/global_funny_embedding/New()
	. = ..()
	//second operation takes MUCH longer, so lets set up signals first.
	RegisterSignal(SSdcs, COMSIG_GLOB_NEW_ITEM, .proc/on_new_item_in_existence)
	handle_current_items()

/datum/global_funny_embedding/Destroy(force)
	. = ..()
	UnregisterSignal(SSdcs, COMSIG_GLOB_NEW_ITEM)

///signal sent by a new item being created.
/datum/global_funny_embedding/proc/on_new_item_in_existence(datum/source, obj/item/created_item)
	SIGNAL_HANDLER

	// this proc says it's for initializing components, but we're initializing elements too because it's you and me against the world >:)
	if(LAZYLEN(created_item.embedding))
		return //already embeds to some degree, so whatever 🐀
	created_item.embedding = embed_type
	created_item.name = "[prefix] [created_item.name]"
	created_item.updateEmbedding()

/**
 * ### handle_current_items
 *
 * Gives every viable item in the world the embed_type, and the prefix prefixed to the name.
 */
/datum/global_funny_embedding/proc/handle_current_items()
	for(var/obj/item/embed_item in world)
		CHECK_TICK
		if(!(embed_item.flags_1 & INITIALIZED_1))
			continue
		if(!embed_item.embedding)
			embed_item.embedding = embed_type
			embed_item.updateEmbedding()
			embed_item.name = "[prefix] [embed_item.name]"

///everything will be... POINTY!!!!
/datum/global_funny_embedding/pointy
	embed_type = EMBED_POINTY
	prefix = "pointy"

///everything will be... sticky? sure, why not
/datum/global_funny_embedding/sticky
	embed_type = EMBED_HARMLESS
	prefix = "sticky"

repo_name: AnalWerewolf/sundown

license: mit


code:
/datum/round_event_control/wizard/fake_explosion //Oh no the station is gone!
	name = "Fake Nuclear Explosion"
	weight = 0 //Badmin exclusive now because once it's expected its not funny
	typepath = /datum/round_event/wizard/fake_explosion
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event/wizard/fake_explosion/start()
	sound_to_playing_players('sound/machines/alarm.ogg')
	sleep(100)
	Cinematic(CINEMATIC_NUKE_FAKE,world)


code:
/datum/round_event_control/wizard/ghost //The spook is real
	name = "G-G-G-Ghosts!"
	weight = 3
	typepath = /datum/round_event/wizard/ghost
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event/wizard/ghost/start()
	var/msg = span_warning("You suddenly feel extremely obvious...")
	set_observer_default_invisibility(0, msg)

//--//

/datum/round_event_control/wizard/possession //Oh shit
	name = "Possessing G-G-G-Ghosts!"
	weight = 2
	typepath = /datum/round_event/wizard/possession
	max_occurrences = 5
	earliest_start = 0 MINUTES

/datum/round_event/wizard/possession/start()
	for(var/mob/dead/observer/G in GLOB.player_list)
		add_verb(G, /mob/dead/observer/verb/boo)
		add_verb(G, /mob/dead/observer/verb/possess)
		to_chat(G, "You suddenly feel a welling of new spooky powers...")


code:
/datum/round_event_control/wizard/greentext //Gotta have it!
	name = "Greentext"
	weight = 4
	typepath = /datum/round_event/wizard/greentext
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event/wizard/greentext/start()

	var/list/holder_canadates = GLOB.player_list.Copy()
	for(var/mob/M in holder_canadates)
		if(!ishuman(M))
			holder_canadates -= M
	if(!holder_canadates) //Very unlikely, but just in case
		return FALSE

	var/mob/living/carbon/human/H = pick(holder_canadates)
	new /obj/item/greentext(H.loc)
	to_chat(H, "The mythical greentext appear at your feet! Pick it up if you dare...")

/obj/item/greentext
	name = "greentext"
	desc = "No one knows what this massive tome does, but it feels desirable all the same..."
	w_class = WEIGHT_CLASS_BULKY
	icon = 'icons/obj/wizard.dmi'
	icon_state = "greentext"
	var/mob/living/last_holder
	var/mob/living/new_holder
	var/list/color_altered_mobs = list()
	var/datum/callback/roundend_callback
	resistance_flags = FIRE_PROOF | ACID_PROOF
	var/quiet = FALSE

/obj/item/greentext/Initialize(mapload)
	. = ..()
	SSpoints_of_interest.make_point_of_interest(src)
	roundend_callback = CALLBACK(src,.proc/check_winner)
	SSticker.OnRoundend(roundend_callback)

/obj/item/greentext/equipped(mob/living/user as mob)
	to_chat(user, "So long as you leave this place with greentext in hand you know will be happy...")
	var/list/other_objectives = user.mind.get_all_objectives()
	if(user.mind && other_objectives.len > 0)
		to_chat(user, span_warning("... so long as you still perform your other objectives that is!"))
	new_holder = user
	if(!last_holder)
		last_holder = user
	if(!(user in color_altered_mobs))
		color_altered_mobs += user
	user.add_atom_colour("#00FF00", ADMIN_COLOUR_PRIORITY)
	START_PROCESSING(SSobj, src)
	..()

/obj/item/greentext/dropped(mob/living/user as mob)
	if(user in color_altered_mobs)
		to_chat(user, span_warning("A sudden wave of failure washes over you..."))
		user.add_atom_colour("#FF0000", ADMIN_COLOUR_PRIORITY) //ya blew it
	last_holder = null
	new_holder = null
	STOP_PROCESSING(SSobj, src)
	..()

/obj/item/greentext/proc/check_winner()
	if(!new_holder)
		return

	if(is_centcom_level(new_holder.z))//you're winner!
		to_chat(new_holder, "At last it feels like victory is assured!")
		new_holder.mind.add_antag_datum(/datum/antagonist/greentext)
		new_holder.log_message("won with greentext!!!", LOG_ATTACK, color="green")
		color_altered_mobs -= new_holder
		resistance_flags |= ON_FIRE
		qdel(src)

/obj/item/greentext/process()
	if(last_holder && last_holder != new_holder) //Somehow it was swiped without ever getting dropped
		to_chat(last_holder, span_warning("A sudden wave of failure washes over you..."))
		last_holder.add_atom_colour("#FF0000", ADMIN_COLOUR_PRIORITY)
		last_holder = new_holder //long live the king

/obj/item/greentext/Destroy(force)
	if(!(resistance_flags & ON_FIRE) && !force)
		return QDEL_HINT_LETMELIVE

	LAZYREMOVE(SSticker.round_end_events, roundend_callback)
	roundend_callback = null //This ought to free the callback datum, and prevent us from harddeling
	for(var/i in GLOB.player_list)
		var/mob/M = i
		var/message = "A dark temptation has passed from this world"
		if(M in color_altered_mobs)
			message += " and you're finally able to forgive yourself"
			if(M.color == "#FF0000" || M.color == "#00FF00")
				M.remove_atom_colour(ADMIN_COLOUR_PRIORITY)
		message += "..."
		// can't skip the mob check as it also does the decolouring
		if(!quiet)
			to_chat(M, message)
	. = ..()

/obj/item/greentext/quiet
	quiet = TRUE


code:
/datum/round_event_control/wizard/identity_spoof //now EVERYONE is the wizard!
	name = "Mass Identity Spoof"
	weight = 5
	typepath = /datum/round_event/wizard/identity_spoof
	max_occurrences = 1

/datum/round_event_control/wizard/identity_spoof/canSpawnEvent(players_amt)
	. = ..()
	if(.)
		return FALSE
	if(GLOB.current_anonymous_theme) //already anonymous, ABORT ABORT
		return FALSE

/datum/round_event/wizard/identity_spoof/start()
	if(GLOB.current_anonymous_theme)
		QDEL_NULL(GLOB.current_anonymous_theme)
	GLOB.current_anonymous_theme = new /datum/anonymous_theme/wizards(extras_enabled = TRUE, alert_players = TRUE)


code:
/datum/round_event_control/wizard/imposter //Mirror Mania
	name = "Imposter Wizard"
	weight = 1
	typepath = /datum/round_event/wizard/imposter
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event/wizard/imposter/start()
	for(var/datum/mind/M as anything in get_antag_minds(/datum/antagonist/wizard))
		if(!ishuman(M.current))
			continue
		var/mob/living/carbon/human/W = M.current
		var/list/candidates = poll_ghost_candidates("Would you like to be an imposter wizard?", ROLE_WIZARD)
		if(!candidates)
			return //Sad Trombone
		var/mob/dead/observer/C = pick(candidates)

		new /obj/effect/particle_effect/smoke(W.loc)

		var/mob/living/carbon/human/I = new /mob/living/carbon/human(W.loc)
		W.dna.transfer_identity(I, transfer_SE=1)
		I.real_name = I.dna.real_name
		I.name = I.dna.real_name
		I.updateappearance(mutcolor_update=1)
		I.domutcheck()
		I.key = C.key
		var/datum/antagonist/wizard/master = M.has_antag_datum(/datum/antagonist/wizard)
		if(!master.wiz_team)
			master.create_wiz_team()
		var/datum/antagonist/wizard/apprentice/imposter/imposter = new()
		imposter.master = M
		imposter.wiz_team = master.wiz_team
		master.wiz_team.add_member(imposter)
		I.mind.add_antag_datum(imposter)
		I.mind.special_role = "imposter"
		I.log_message("is an imposter!", LOG_ATTACK, color="red") //?
		SEND_SOUND(I, sound('sound/effects/magic.ogg'))


code:
/datum/round_event_control/wizard/invincible //Boolet Proof
	name = "Invincibility"
	weight = 3
	typepath = /datum/round_event/wizard/invincible
	max_occurrences = 5
	earliest_start = 0 MINUTES

/datum/round_event/wizard/invincible/start()

	for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
		H.reagents.add_reagent(/datum/reagent/medicine/adminordrazine, 40) //100 ticks of absolute invinciblity (barring gibs)
		to_chat(H, span_notice("You feel invincible, nothing can hurt you!"))


code:
/datum/round_event_control/wizard/lava //THE LEGEND NEVER DIES
	name = "The Floor Is LAVA!"
	weight = 2
	typepath = /datum/round_event/wizard/lava
	max_occurrences = 3
	earliest_start = 0 MINUTES

/datum/round_event/wizard/lava
	endWhen = 0
	var/started = FALSE

/datum/round_event/wizard/lava/start()
	if(!started)
		started = TRUE
		SSweather.run_weather(/datum/weather/floor_is_lava)


code:
/datum/round_event_control/wizard/madness
	name = "Curse of Madness"
	weight = 1
	typepath = /datum/round_event/wizard/madness
	earliest_start = 0 MINUTES

	var/forced_secret

/datum/round_event_control/wizard/madness/admin_setup()
	if(!check_rights(R_FUN))
		return

	var/suggested = pick(strings(REDPILL_FILE, "redpill_questions"))

	forced_secret = (input(usr, "What horrifying truth will you reveal?", "Curse of Madness", sort_list(suggested)) as text|null) || suggested

/datum/round_event/wizard/madness/start()
	var/datum/round_event_control/wizard/madness/C = control

	var/horrifying_truth

	if(C.forced_secret)
		horrifying_truth = C.forced_secret
		C.forced_secret = null
	else
		horrifying_truth = pick(strings(REDPILL_FILE, "redpill_questions"))

	curse_of_madness(null, horrifying_truth)


code:
/datum/round_event_control/wizard/magicarp //these fish is loaded
	name = "Magicarp"
	weight = 1
	typepath = /datum/round_event/wizard/magicarp
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event/wizard/magicarp
	announceWhen = 3
	startWhen = 50

/datum/round_event/wizard/magicarp/setup()
	startWhen = rand(40, 60)

/datum/round_event/wizard/magicarp/announce(fake)
	priority_announce("Unknown magical entities have been detected near [station_name()], please stand-by.", "Lifesign Alert")

/datum/round_event/wizard/magicarp/start()
	for(var/obj/effect/landmark/carpspawn/C in GLOB.landmarks_list)
		if(prob(5))
			new /mob/living/simple_animal/hostile/carp/ranged/chaos(C.loc)
		else
			new /mob/living/simple_animal/hostile/carp/ranged(C.loc)

/mob/living/simple_animal/hostile/carp/ranged
	name = "magicarp"
	desc = "50% magic, 50% carp, 100% horrible."
	icon_state = "magicarp"
	icon_living = "magicarp"
	icon_dead = "magicarp_dead"
	icon_gib = "magicarp_gib"
	ranged = 1
	retreat_distance = 2
	minimum_distance = 0 //Between shots they can and will close in to nash
	projectiletype = /obj/projectile/magic
	projectilesound = 'sound/weapons/emitter.ogg'
	maxHealth = 50
	health = 50
	gold_core_spawnable = NO_SPAWN
	random_color = FALSE

	var/allowed_projectile_types = list(/obj/projectile/magic/change, /obj/projectile/magic/animate, /obj/projectile/magic/resurrection,
	/obj/projectile/magic/death, /obj/projectile/magic/teleport, /obj/projectile/magic/door, /obj/projectile/magic/aoe/fireball,
	/obj/projectile/magic/spellblade, /obj/projectile/magic/arcane_barrage)

/mob/living/simple_animal/hostile/carp/ranged/Initialize(mapload)
	projectiletype = pick(allowed_projectile_types)
	. = ..()

/mob/living/simple_animal/hostile/carp/ranged/chaos
	name = "chaos magicarp"
	desc = "50% carp, 100% magic, 150% horrible."
	color = "#00FFFF"
	maxHealth = 75
	health = 75
	gold_core_spawnable = NO_SPAWN

/mob/living/simple_animal/hostile/carp/ranged/chaos/Shoot()
	projectiletype = pick(allowed_projectile_types)
	..()

/mob/living/simple_animal/hostile/carp/ranged/xenobiology // these are for the xenobio gold slime pool
	gold_core_spawnable = HOSTILE_SPAWN
	allowed_projectile_types = list(/obj/projectile/magic/animate, /obj/projectile/magic/teleport,
	/obj/projectile/magic/door, /obj/projectile/magic/aoe/fireball, /obj/projectile/magic/spellblade, /obj/projectile/magic/arcane_barrage) //thanks Lett1

/mob/living/simple_animal/hostile/carp/ranged/chaos/xenobiology
	gold_core_spawnable = HOSTILE_SPAWN
	allowed_projectile_types = list(/obj/projectile/magic/animate, /obj/projectile/magic/teleport,
	/obj/projectile/magic/door, /obj/projectile/magic/aoe/fireball, /obj/projectile/magic/spellblade, /obj/projectile/magic/arcane_barrage)


code:
/datum/round_event_control/wizard/petsplosion //the horror
	name = "Petsplosion"
	weight = 2
	typepath = /datum/round_event/wizard/petsplosion
	max_occurrences = 1 //Exponential growth is nothing to sneeze at!
	earliest_start = 0 MINUTES
	var/mobs_to_dupe = 0

/datum/round_event_control/wizard/petsplosion/preRunEvent()
	for(var/mob/living/simple_animal/F in GLOB.alive_mob_list)
		if(!ishostile(F) && is_station_level(F.z))
			mobs_to_dupe++
	if(mobs_to_dupe > 100 || !mobs_to_dupe)
		return EVENT_CANT_RUN

	..()

/datum/round_event/wizard/petsplosion
	endWhen = 61 //1 minute (+1 tick for endWhen not to interfere with tick)
	var/countdown = 0
	var/mobs_duped = 0

/datum/round_event/wizard/petsplosion/tick()
	if(activeFor >= 30 * countdown) // 0 seconds : 2 animals | 30 seconds : 4 animals | 1 minute : 8 animals
		countdown += 1
		for(var/mob/living/simple_animal/F in GLOB.alive_mob_list) //If you cull the heard before the next replication, things will be easier for you
			if(!ishostile(F) && is_station_level(F.z))
				new F.type(F.loc)
				mobs_duped++
				if(mobs_duped > 400)
					kill()


code:
/datum/round_event_control/wizard/race //Lizard Wizard? Lizard Wizard.
	name = "Race Swap"
	weight = 2
	typepath = /datum/round_event/wizard/race
	max_occurrences = 5
	earliest_start = 0 MINUTES

/datum/round_event/wizard/race/start()

	var/all_the_same = 0
	var/all_species = list()

	for(var/stype in subtypesof(/datum/species))
		var/datum/species/S = stype
		if(initial(S.changesource_flags) & RACE_SWAP)
			all_species += stype

	var/datum/species/new_species = pick(all_species)

	if(prob(50))
		all_the_same = 1

	for(var/i in GLOB.human_list) //yes, even the dead
		var/mob/living/carbon/human/H = i
		H.set_species(new_species)
		H.dna.unique_enzymes = H.dna.generate_unique_enzymes()
		to_chat(H, span_notice("You feel somehow... different?"))
		if(!all_the_same)
			new_species = pick(all_species)


code:
/datum/round_event_control/wizard/rpgloot //its time to minmax your shit
	name = "RPG Loot"
	weight = 3
	typepath = /datum/round_event/wizard/rpgloot
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event/wizard/rpgloot/start()
	GLOB.rpgloot_controller = new /datum/rpgloot_controller

/obj/item/upgradescroll
	name = "item fortification scroll"
	desc = "Somehow, this piece of paper can be applied to items to make them \"better\". Apparently there's a risk of losing the item if it's already \"too good\". <i>This all feels so arbitrary...</i>"
	icon = 'icons/obj/wizard.dmi'
	icon_state = "scroll"
	worn_icon_state = "scroll"
	w_class = WEIGHT_CLASS_TINY

	var/upgrade_amount = 1
	var/can_backfire = TRUE
	var/uses = 1

/obj/item/upgradescroll/afterattack(obj/item/target, mob/user, proximity)
	. = ..()
	if(!proximity || !istype(target))
		return

	target.AddComponent(/datum/component/fantasy, upgrade_amount, null, null, can_backfire, TRUE)

	uses -= 1
	if(!uses)
		visible_message(span_warning("[src] vanishes, its magic completely consumed from the fortification."))
		qdel(src)

/obj/item/upgradescroll/unlimited
	name = "unlimited foolproof item fortification scroll"
	desc = "Somehow, this piece of paper can be applied to items to make them \"better\". This scroll is made from the tongues of dead paper wizards, and can be used an unlimited number of times, with no drawbacks."
	uses = INFINITY
	can_backfire = FALSE

///Holds the global datum for rpgloot, so anywhere may check for its existence (it signals into whatever it needs to modify, so it shouldn't require fetching)
GLOBAL_DATUM(rpgloot_controller, /datum/rpgloot_controller)

/**
 * ## rpgloot controller!
 *
 * Stored in a global datum, and created when rpgloot is turned on via event or VV'ing the GLOB.rpgloot_controller to be a new /datum/rpgloot_controller.
 * Makes every item in the world fantasy, but also hooks into global signals for new items created to also bless them with fantasy.
 *
 * What do I mean by fantasy?
 * * Items will have random qualities assigned to them
 * * Good quality items will have positive buffs/special powers applied to them
 * * Bad quality items will get the opposite!
 * * All of this is reflected in a fitting name for the item
 * * See fantasy.dm and read the component for more information :)
 */
/datum/rpgloot_controller

/datum/rpgloot_controller/New()
	. = ..()
	//second operation takes MUCH longer, so lets set up signals first.
	RegisterSignal(SSdcs, COMSIG_GLOB_NEW_ITEM, .proc/on_new_item_in_existence)
	handle_current_items()

///signal sent by a new item being created.
/datum/rpgloot_controller/proc/on_new_item_in_existence(datum/source, obj/item/created_item)
	SIGNAL_HANDLER

	created_item.AddComponent(/datum/component/fantasy)

/**
 * ### handle_current_items
 *
 * Gives every viable item in the world the fantasy component.
 * If the item it is giving fantasy to is a storage item, there's a chance it'll drop in an item fortification scroll. neat!
 */
/datum/rpgloot_controller/proc/handle_current_items()
	var/upgrade_scroll_chance = 0
	for(var/obj/item/fantasy_item in world)
		CHECK_TICK

		if(!(fantasy_item.flags_1 & INITIALIZED_1) || QDELETED(fantasy_item))
			continue

		fantasy_item.AddComponent(/datum/component/fantasy)

		if(istype(fantasy_item, /obj/item/storage))
			var/obj/item/storage/storage_item = fantasy_item
			var/datum/component/storage/storage_component = storage_item.GetComponent(/datum/component/storage)
			if(prob(upgrade_scroll_chance) && storage_item.contents.len < storage_component.max_items && !storage_item.invisibility)
				var/obj/item/upgradescroll/scroll = new(get_turf(storage_item))
				SEND_SIGNAL(storage_item, COMSIG_TRY_STORAGE_INSERT, scroll, null, TRUE, TRUE)
				upgrade_scroll_chance = max(0,upgrade_scroll_chance-100)
				if(isturf(scroll.loc))
					qdel(scroll)

			upgrade_scroll_chance += 25


code:
/datum/round_event/wizard/shuffle/start()

/datum/round_event_control/wizard/shuffleloc //Somewhere an AI is crying
	name = "Change Places!"
	weight = 2
	typepath = /datum/round_event/wizard/shuffleloc
	max_occurrences = 5
	earliest_start = 0 MINUTES

/datum/round_event/wizard/shuffleloc/start()
	var/list/moblocs = list()
	var/list/mobs  = list()

	for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
		if(!is_station_level(H.z))
			continue //lets not try to strand people in space or stuck in the wizards den
		moblocs += H.loc
		mobs += H

	if(!mobs)
		return

	shuffle_inplace(moblocs)
	shuffle_inplace(mobs)

	for(var/mob/living/carbon/human/H in mobs)
		if(!moblocs)
			break //locs aren't always unique, so this may come into play
		do_teleport(H, moblocs[moblocs.len], channel = TELEPORT_CHANNEL_MAGIC)
		moblocs.len -= 1

	for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
		var/datum/effect_system/smoke_spread/smoke = new
		smoke.set_up(0, H.loc)
		smoke.start()

//---//

/datum/round_event_control/wizard/shufflenames //Face/off joke
	name = "Change Faces!"
	weight = 4
	typepath = /datum/round_event/wizard/shufflenames
	max_occurrences = 5
	earliest_start = 0 MINUTES

/datum/round_event/wizard/shufflenames/start()
	var/list/mobnames = list()
	var/list/mobs  = list()

	for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
		mobnames += H.real_name
		mobs += H

	if(!mobs)
		return

	shuffle_inplace(mobnames)
	shuffle_inplace(mobs)

	for(var/mob/living/carbon/human/H in mobs)
		if(!mobnames)
			break
		H.real_name = mobnames[mobnames.len]
		mobnames.len -= 1

	for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
		var/datum/effect_system/smoke_spread/smoke = new
		smoke.set_up(0, H.loc)
		smoke.start()

//---//

/datum/round_event_control/wizard/shuffleminds //Basically Mass Ranged Mindswap
	name = "Change Minds!"
	weight = 1
	typepath = /datum/round_event/wizard/shuffleminds
	max_occurrences = 3
	earliest_start = 0 MINUTES

/datum/round_event/wizard/shuffleminds/start()
	var/list/mobs  = list()

	for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
		if(H.stat || !H.mind || IS_WIZARD(H))
			continue //the wizard(s) are spared on this one
		mobs += H

	if(!mobs)
		return

	shuffle_inplace(mobs)

	var/obj/effect/proc_holder/spell/pointed/mind_transfer/swapper = new
	while(mobs.len > 1)
		var/mob/living/carbon/human/H = pick(mobs)
		mobs -= H
		swapper.cast(list(H), mobs[mobs.len], TRUE)
		mobs -= mobs[mobs.len]

	for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
		var/datum/effect_system/smoke_spread/smoke = new
		smoke.set_up(0, H.loc)
		smoke.start()


code:
/datum/round_event_control/wizard/summonguns //The Classic
	name = "Summon Guns"
	weight = 1
	typepath = /datum/round_event/wizard/summonguns
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event_control/wizard/summonguns/New()
	if(CONFIG_GET(flag/no_summon_guns))
		weight = 0
	..()

/datum/round_event/wizard/summonguns/start()
	rightandwrong(SUMMON_GUNS, null, 10)

/datum/round_event_control/wizard/summonmagic //The Somewhat Less Classic
	name = "Summon Magic"
	weight = 1
	typepath = /datum/round_event/wizard/summonmagic
	max_occurrences = 1
	earliest_start = 0 MINUTES

/datum/round_event_control/wizard/summonmagic/New()
	if(CONFIG_GET(flag/no_summon_magic))
		weight = 0
	..()

/datum/round_event/wizard/summonmagic/start()
	rightandwrong(SUMMON_MAGIC, null, 10)


code:
GLOBAL_LIST_EMPTY(all_wormholes) // So we can pick wormholes to teleport to

/datum/round_event_control/wormholes
	name = "Wormholes"
	typepath = /datum/round_event/wormholes
	max_occurrences = 3
	weight = 2
	min_players = 2

/datum/round_event/wormholes
	announceWhen = 10
	endWhen = 60

	var/list/pick_turfs = list()
	var/list/wormholes = list()
	var/shift_frequency = 3
	var/number_of_wormholes = 400

/datum/round_event/wormholes/setup()
	announceWhen = rand(0, 20)
	endWhen = rand(40, 80)

/datum/round_event/wormholes/start()
	for(var/turf/open/floor/T in world)
		if(is_station_level(T.z))
			pick_turfs += T

	for(var/i = 1, i <= number_of_wormholes, i++)
		var/turf/T = pick(pick_turfs)
		wormholes += new /obj/effect/portal/wormhole(T, 0, null, FALSE)

/datum/round_event/wormholes/announce(fake)
	priority_announce("Space-time anomalies detected on the station. There is no additional data.", "Anomaly Alert", ANNOUNCER_SPANOMALIES)

/datum/round_event/wormholes/tick()
	if(activeFor % shift_frequency == 0)
		for(var/obj/effect/portal/wormhole/O in wormholes)
			var/turf/T = pick(pick_turfs)
			if(T)
				O.forceMove(T)

/datum/round_event/wormholes/end()
	QDEL_LIST(wormholes)
	wormholes = null

/obj/effect/portal/wormhole
	name = "wormhole"
	desc = "It looks highly unstable; It could close at any moment."
	icon = 'icons/obj/objects.dmi'
	icon_state = "anom"
	mech_sized = TRUE

/obj/effect/portal/wormhole/Initialize(mapload, _creator, _lifespan = 0, obj/effect/portal/_linked, automatic_link = FALSE, turf/hard_target_override, atmos_link_override)
	. = ..()
	GLOB.all_wormholes += src

/obj/effect/portal/wormhole/Destroy()
	. = ..()
	GLOB.all_wormholes -= src

/obj/effect/portal/wormhole/teleport(atom/movable/M)
	if(iseffect(M)) //sparks don't teleport
		return
	if(M.anchored)
		if(!(ismecha(M) && mech_sized))
			return

	if(ismovable(M))
		if(GLOB.all_wormholes.len)
			var/obj/effect/portal/wormhole/P = pick(GLOB.all_wormholes)
			if(P && isturf(P.loc))
				hard_target = P.loc
		if(!hard_target)
			return
		do_teleport(M, hard_target, 1, null, null, channel = TELEPORT_CHANNEL_WORMHOLE) ///You will appear adjacent to the beacon


code:
/**
 * # 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(mapload)
	..()
	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_notice("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 ..()


code:
/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/basic/cockroach, /mob/living/simple_animal/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/snake, /mob/living/simple_animal/pet/cat, /mob/living/simple_animal/pet/dog/corgi, /mob/living/basic/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"

/datum/experiment/scanning/random/plants/wild
	name = "Wild Biomatter Mutation Sample"
	description = "Due to a number of reasons, (Solar Rays, a diet consisting only of unstable mutagen, entropy) plants with lower levels of instability may occasionally mutate with little reason. Scan one of these samples for us."
	performance_hint = "\"Wild\" mutations have been recorded to occur above 30 points of instability, while species mutations occur above 60 points of instability."
	total_requirement = 1

/datum/experiment/scanning/random/plants/traits
	name = "Unique Biomatter Mutation Sample"
	description = "We here at centcom are on the look out for rare and exotic plants with unique properties to brag about to our shareholders. We're looking for a sample with a very specific genes currently."
	performance_hint = "The wide varities of plants on station each carry various traits, some unique to them. Look for plants that may mutate into what we're looking for."
	total_requirement = 3
	possible_plant_genes = list(/datum/plant_gene/trait/squash, /datum/plant_gene/trait/cell_charge, /datum/plant_gene/trait/glow/shadow, /datum/plant_gene/trait/teleport, /datum/plant_gene/trait/brewing, /datum/plant_gene/trait/juicing, /datum/plant_gene/trait/eyes, /datum/plant_gene/trait/sticky)

/datum/experiment/scanning/points/machinery_tiered_scan/tier2_lathes
	name = "Advanced Stock Parts Benchmark"
	description = "Our newly-designed advanced machinery components require practical application tests for hints at possible further advancements, as well as a general confirmation that we didn't actually design worse parts somehow."
	required_points = 6
	required_atoms = list(
		/obj/machinery/rnd/production/protolathe/department/science = 1,
		/obj/machinery/rnd/production/protolathe/department/engineering = 1,
		/obj/machinery/rnd/production/techfab/department/cargo = 1,
		/obj/machinery/rnd/production/techfab/department/medical = 1,
		/obj/machinery/rnd/production/techfab/department/security = 1,
		/obj/machinery/rnd/production/techfab/department/service = 1
	)
	required_tier = 2

/datum/experiment/scanning/points/machinery_tiered_scan/tier3_bluespacemachines
	name = "Bluespace Machinery Attunement"
	description = "Teleportation technology using bluespace capabilities is a high selling point for our company, but the threat of a critical malfunction in calibration procedures wasn't something we predicted to emerge. Since our RnD department has started a flyperson race riot, maybe your advancements in stock parts could help mitigate the buzzing problem."
	required_points = 4
	required_atoms = list(
		/obj/machinery/teleport/hub = 1,
		/obj/machinery/teleport/station = 1
	)
	required_tier = 3

/datum/experiment/scanning/points/machinery_tiered_scan/tier3_variety
	name = "High Efficiency Parts Applications Test"
	description = "We require further testing of the stock part designs to push their efficiency and market price even further."
	required_points = 15
	required_atoms = list(
		/obj/machinery/autolathe = 1,
		/obj/machinery/rnd/production/circuit_imprinter/department/science = 1,
		/obj/machinery/monkey_recycler = 1,
		/obj/machinery/processor/slime = 1,
		/obj/machinery/processor = 2,
		/obj/machinery/reagentgrinder = 2,
		/obj/machinery/hydroponics = 2,
		/obj/machinery/biogenerator = 3,
		/obj/machinery/gibber = 3,
		/obj/machinery/chem_master = 3,
		/obj/machinery/atmospherics/components/unary/cryo_cell = 3,
		/obj/machinery/harvester = 5,
		/obj/machinery/quantumpad = 5
	)
	required_tier = 3

/datum/experiment/scanning/points/machinery_tiered_scan/tier3_mechbay
	name = "Military-grade Mech Bay Setup"
	description = "Constructing combat-oriented exosuits is a pricy endeavour. Make sure you have an efficient setup for production, and we'll send over some of our design documents."
	required_points = 6
	required_atoms = list(
		/obj/machinery/mecha_part_fabricator = 1,
		/obj/machinery/mech_bay_recharge_port = 1,
		/obj/machinery/recharge_station = 1
	)
	required_tier = 3

/datum/experiment/scanning/points/machinery_pinpoint_scan/tier2_microlaser
	name = "High-power Micro-lasers Calibration"
	description = "Our Nanotrasen High-Power Office-Ready Laser Pointer ™ isn't powerful enough to strike airborne Syndidrones out of the sky yet. Find us some diode applications for hints on how to improve them!"
	required_points = 10
	required_atoms = list(
		/obj/machinery/mecha_part_fabricator = 1,
		/obj/machinery/rnd/experimentor = 1,
		/obj/machinery/dna_scannernew = 1,
		/obj/machinery/microwave = 2,
		/obj/machinery/deepfryer = 2,
		/obj/machinery/chem_heater = 3,
		/obj/machinery/power/emitter = 3
	)
	required_stock_part = /obj/item/stock_parts/micro_laser/high

/datum/experiment/scanning/points/machinery_pinpoint_scan/tier2_capacitors
	name = "Advanced Capacitors Benchmark"
	description = "Further improving the power capacity of devices station-wide is the next step towards the important project marked as CRITICAL: motorised wheelchairs that run on bluespace-contained nuclear power."
	required_points = 12
	required_atoms = list(
		/obj/machinery/recharge_station = 1,
		/obj/machinery/cell_charger = 1,
		/obj/machinery/mech_bay_recharge_port = 1,
		/obj/machinery/recharger = 2,
		/obj/machinery/power/smes = 2,
		/obj/machinery/chem_dispenser = 3,
		/obj/machinery/chem_dispenser/drinks = 3, /*actually having only the chem dispenser works for scanning soda/booze dispensers but im not quite sure how would i go about actually pointing that out w/o these two lines*/
		/obj/machinery/chem_dispenser/drinks/beer = 3
	)
	required_stock_part = /obj/item/stock_parts/capacitor/adv

/datum/experiment/scanning/points/machinery_pinpoint_scan/tier2_scanmodules
	name = "Advanced Scanning Modules Calibration"
	description = "Despite the apparent lack of use of the scanning modules on our stations, we still expect you to run performance tests on them, just in case we come up with a ground-breaking way to fit 6 scanning modules in an exosuit."
	required_points = 6
	required_atoms = list(
		/obj/machinery/dna_scannernew = 1,
		/obj/machinery/rnd/experimentor = 1,
		/obj/machinery/medical_kiosk = 2,
		/obj/machinery/piratepad/civilian = 2,
		/obj/machinery/rnd/bepis = 3
	)
	required_stock_part = /obj/item/stock_parts/scanning_module/adv

/datum/experiment/scanning/points/machinery_pinpoint_scan/tier3_cells
	name = "Power Cells Capacity Test"
	description = "Nanotrasen has two major problems with their new Hamster-powered Generator Array: excess of power produced and violent protests of Animal Rights Consortium activists over genetically modifying hamsters with the Hulk gene. We place dibs on dealing with the latter!"
	required_points = 8
	required_atoms = list(
		/obj/machinery/recharge_station = 1,
		/obj/machinery/chem_dispenser = 1,
		/obj/machinery/chem_dispenser/drinks = 1,
		/obj/machinery/chem_dispenser/drinks/beer = 1,
		/obj/machinery/power/smes = 2
	)
	required_stock_part = /obj/item/stock_parts/cell/hyper

/datum/experiment/scanning/points/machinery_pinpoint_scan/tier3_microlaser
	name = "Ultra-high-power Micro-lasers Calibration"
	description = "We're very close to outperforming the surgeons of the past by inventing laser tools precise enough to perform surgeries on grapes. Help us fine-tune the diodes to perfection!"
	required_points = 10
	required_atoms = list(
		/obj/machinery/mecha_part_fabricator = 1,
		/obj/machinery/microwave = 1,
		/obj/machinery/rnd/experimentor = 1,
		/obj/machinery/atmospherics/components/binary/thermomachine/freezer = 2,
		/obj/machinery/power/emitter = 2,
		/obj/machinery/chem_heater = 2,
		/obj/machinery/chem_mass_spec = 3
	)
	required_stock_part = /obj/item/stock_parts/micro_laser/ultra


code:
/**
 * # 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_notice("[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, span_notice("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, span_notice("You scan [target]."))
	else
		playsound(user, 'sound/machines/buzz-sigh.ogg', 25)
		to_chat(user, span_notice("[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, span_notice("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, span_notice("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)
	SIGNAL_HANDLER
	INVOKE_ASYNC(src, .proc/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)
	SIGNAL_HANDLER
	INVOKE_ASYNC(src, /datum/proc/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 that are on the station z-level, also checks if provided turf is on the station z-level
 *
 * Arguments:
 * * turf_to_check_for_servers - The turf to check if its on the station z-level
 */
/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()
	if (!SSmapping.level_trait(turf_to_check_for_servers.z,ZTRAIT_STATION))
		return local_servers
	for (var/obj/machinery/rnd/server/server in SSresearch.servers)
		var/turf/position_of_this_server_machine = get_turf(server)
		if (position_of_this_server_machine && SSmapping.level_trait(position_of_this_server_machine.z,ZTRAIT_STATION))
			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)


code:
/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)
	SIGNAL_HANDLER
	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)
	SIGNAL_HANDLER
	finish_experiment(linked_experiment_handler)


code:
/**
 * # 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)


code:
/datum/experiment/exploration_scan
	name = "Exploration Experiment"
	description = "An experiment requiring drone exploration to progress"
	exp_tag = "Exploration"
	performance_hint = "Find a site with specific characteristics and perform the scan."
	// Site type, scan needs to be of that type
	var/required_site_type
	// Condition type, scan needs to be of site with this condition
	var/required_condition
	// Required scan type
	var/required_scan_type = EXOSCAN_POINT

/datum/experiment/exploration_scan/is_complete()
	for(var/datum/exploration_site/site in GLOB.exploration_sites)
		switch(required_scan_type)
			if(EXOSCAN_DEEP)
				if(!site.deep_scan_complete)
					continue
			if(EXOSCAN_POINT)
				if(!site.point_scan_complete)
					continue
		if(required_site_type && !istype(site,required_site_type))
			continue
		if(required_condition && !(locate(required_condition) in site.scan_conditions))
			continue
		return TRUE
	return FALSE

/datum/experiment/exploration_scan/perform_experiment_actions(datum/component/experiment_handler/experiment_handler)
	return is_complete()

/datum/experiment/exploration_scan/actionable(datum/component/experiment_handler/experiment_handler)
	return !is_complete()

/datum/experiment/exploration_scan/asteroid_belt
	name = "Scan Asteroid Belt"
	description = "We're looking for a site to test our asteroid blasting caps. Perform point scan of one."
	required_site_type = /datum/exploration_site/asteroid_belt
	required_scan_type = EXOSCAN_POINT

/datum/experiment/exploration_scan/black_hole
	name = "Deep scan a black hole"
	description = "We require more research data on black holes, perform deep scan of a system affected by one."
	required_condition = /datum/scan_condition/black_hole
	required_scan_type = EXOSCAN_DEEP

/datum/experiment/exploration_scan/random
	name = "Random Exoscan Experiment"
	description = "We need scan data of specific site type"
	/// If not null the required_site_type will be picked from this list
	var/list/possible_random_conditions
	/// If not null the required_condition will be picked from this list
	var/list/possible_random_site_types

/datum/experiment/exploration_scan/random/New()
	. = ..()
	if(length(possible_random_site_types))
		required_site_type = pick(possible_random_site_types)
	if(length(possible_random_conditions))
		required_condition = pick(possible_random_conditions)
	var/list/name_parts = list()
	name_parts += "[required_scan_type] scan of"
	if(required_site_type)
		var/datum/exploration_site/site = required_site_type
		name_parts += initial(site.name)
	else
		name_parts += "site"
	if(required_condition)
		var/datum/scan_condition/condition = required_condition
		name_parts += "affected by \the [initial(condition.name)]"
	name = capitalize(name_parts.Join(""))
	description = name

/datum/experiment/exploration_scan/random/condition
	possible_random_conditions = list(/datum/scan_condition/asteroid_belt,/datum/scan_condition/black_hole,/datum/scan_condition/nebula,/datum/scan_condition/pulsar)

/datum/experiment/exploration_scan/random/site_type
	possible_random_site_types = list(/datum/exploration_site/asteroid_belt,/datum/exploration_site/uncharted_planet,/datum/exploration_site/junkyard)


code:
/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 ordnance 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 || required_devastation <= last_devastation) \
		&& (!required_heavy || required_heavy <= last_heavy) \
		&& (!required_light || 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()


code:
/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


code:
/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
	..()


code:
/**
 * # 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 (Or a count of atoms destoryed for destructive expiriments)
	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 : WEAKREF(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] && !(WEAKREF(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


code:
///This experiment type will turn up TRUE if at least one of the stock parts in the scanned machine is of the required_tier.
///Pretend to upgrade security's techfab but in reality apply only one better matter bin!

/datum/experiment/scanning/points/machinery_tiered_scan
	name = "Upgraded Machinery Scanning Experiment"
	description = "Base experiment for scanning machinery with upgraded parts"
	exp_tag = "Scan"
	///What tier of parts is required for the experiment
	var/required_tier = 1

/datum/experiment/scanning/points/machinery_tiered_scan/check_progress()
	. = ..()
	.[1] = EXPERIMENT_PROG_INT("Scan samples of the following machines built with parts of tier [required_tier] or better.", points, required_points)[1]

/datum/experiment/scanning/points/machinery_tiered_scan/final_contributing_index_checks(atom/target, typepath)
	. = ..()
	if(!.)
		return FALSE

	var/obj/machinery/machine = target
	for(var/obj/item/stock_parts/stock_part in machine.component_parts)
		if(stock_part.rating >= required_tier) //>= for backwards research cases when you want the discount done after you did the node
			return TRUE
	return FALSE

//This experiment type will turn up TRUE if there is a specific part in the scanned machine
/datum/experiment/scanning/points/machinery_pinpoint_scan
	name = "Machinery Pinpoint Stock Parts Scanning Experiment"
	description = "Base experiment for scanning machinery with specific parts"
	exp_tag = "Scan"
	///Which stock part are we looking for in the machine
	var/obj/item/stock_parts/required_stock_part = /obj/item/stock_parts

/datum/experiment/scanning/points/machinery_pinpoint_scan/check_progress()
	. = ..()
	.[1] = EXPERIMENT_PROG_INT("Scan samples of the following machines upgraded with \a [initial(required_stock_part.name)] to accumulate enough points to complete this experiment.", points, required_points)[1]

/datum/experiment/scanning/points/machinery_pinpoint_scan/final_contributing_index_checks(atom/target, typepath)
	. = ..()
	if(!.)
		return FALSE

	var/obj/machinery/machine = target
	for(var/obj/stock_part in machine.component_parts)
		if(istype(stock_part, required_stock_part))
			return TRUE
	return FALSE


code:
/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])


code:
/datum/experiment/scanning/random/plants
	name = "Botanical Scanning Experiment"
	description = "Base experiment for scanning edible plant biomass."
	exp_tag = "Plant Biomatter Scan"
	total_requirement = 1
	possible_types = list(/obj/item/food/grown)
	traits = EXPERIMENT_TRAIT_DESTRUCTIVE
	///List of possible plant genes the experiment may ask for.
	var/list/possible_plant_genes = list()
	///List of plant genes actually required, indexed by the atom that is required.
	var/list/required_genes = list()

/datum/experiment/scanning/random/plants/New()
	. = ..()
	if(possible_plant_genes.len)
		for(var/req_atom in required_atoms)
			var/chosen_gene = pick(possible_plant_genes)
			required_genes[req_atom] = chosen_gene

/datum/experiment/scanning/random/plants/serialize_progress_stage(atom/target, list/seen_instances)
	return EXPERIMENT_PROG_INT("Scan samples of \a harvested plant.", \
		traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len, required_atoms[target])

/datum/experiment/scanning/random/plants/traits/final_contributing_index_checks(atom/target, typepath)
	if(!istype(target, /obj/item/food/grown))
		return FALSE
	var/obj/item/food/grown/crop = target
	if(possible_plant_genes.len)
		return ..() && crop.seed.get_gene(required_genes[typepath])
	return ..()

/datum/experiment/scanning/random/plants/traits/serialize_progress_stage(atom/target, list/seen_instances)
	if(possible_plant_genes.len)
		var/datum/plant_gene/gene = required_genes[target]
		return EXPERIMENT_PROG_INT("Scan samples of harvested plants with the trait: [initial(gene.name)].", \
			traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len, required_atoms[target])

/datum/experiment/scanning/random/plants/wild/final_contributing_index_checks(atom/target, typepath)
	return ..() && HAS_TRAIT(target, TRAIT_PLANT_WILDMUTATE)


code:
/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 && !(WEAKREF(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])


code:
/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/basic/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])


code:
/**
 * # 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(mapload)
	..()
	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)


code:
Future improvements:
	Better names/descriptions for ships/trading/planets
	More fluff lines for each site type.
	Adventure-Maker : styling aaaaah.
	Adventure-Maker : Consider parsing define hard-configured dm file for constants
	Typelists for event/site stuff to reduce costs
	Figure out some way to cache adventure prototypes / defer loading somehow so it scales better with a lot of adventure files.
	Unit test for json loading?
	Mk II drones with more tool slots/other buffs researchable at R&D ?
	Could use more scan conditions
	Exit Node - Random Node