code:
/obj/item/sparring_contract
	desc = "A contract for setting up sparring matches. Both sparring partners must agree with the terms to begin."
	icon = 'icons/obj/wizard.dmi'
	icon_state = "scroll"
	drop_sound = 'sound/items/handling/paper_drop.ogg'
	pickup_sound =  'sound/items/handling/paper_pickup.ogg'
	throw_range = 1
	throw_speed = 1
	w_class = WEIGHT_CLASS_TINY
	///what weapons will be allowed during the sparring match
	var/weapons_condition = CONDITION_MELEE_ONLY
	///what arena the fight will take place in
	var/arena_condition = /area/service/chapel
	///what stakes the fight will have
	var/stakes_condition = STAKES_NONE
	///who has signed this contract. fills itself with WEAKREFS, to prevent hanging references
	var/list/datum/weakref/signed_by = list(null, null)

/obj/item/sparring_contract/Initialize(mapload)
	. = ..()
	name = "[GLOB.deity]'s sparring contract"

/obj/item/sparring_contract/Destroy()
	QDEL_NULL(signed_by)
	var/datum/religion_sect/spar/sect = GLOB.religious_sect
	sect?.existing_contract = null
	return ..()

/obj/item/sparring_contract/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "SparringContract", name)
		ui.open()

/obj/item/sparring_contract/ui_static_data(mob/user)
	var/list/data = list()
	var/area/arena = GLOB.areas_by_type[arena_condition]
	data["set_weapon"] = weapons_condition
	data["set_area"] = arena?.name
	data["set_stakes"] = stakes_condition
	data["possible_areas"] = get_possible_areas()

	return data

/obj/item/sparring_contract/ui_data(mob/user)
	var/list/data = list()
	var/area/arena = GLOB.areas_by_type[arena_condition]
	var/mob/living/carbon/human/left_partner
	if(signed_by[1])
		left_partner = signed_by[1].resolve()
	var/mob/living/carbon/human/right_partner
	if(signed_by[2])
		right_partner = signed_by[2].resolve()
	data["in_area"] = ((left_partner && right_partner && arena) && (left_partner in arena.contents) && (right_partner in arena.contents))
	data["no_chaplains"] = (!left_partner?.mind?.holy_role && !right_partner?.mind?.holy_role)
	data["left_sign"] = left_partner ? left_partner.real_name : "none"
	data["right_sign"] = right_partner ? right_partner.real_name : "none"
	return data

/obj/item/sparring_contract/proc/get_possible_areas()
	var/list/area_names = list()
	var/datum/religion_sect/spar/sect = GLOB.religious_sect
	for(var/key in sect.arenas)
		area_names += key
	return area_names

/obj/item/sparring_contract/ui_act(action, list/params)
	. = ..()
	if(.)
		return

	var/mob/user = usr

	if(!ishuman(user))
		to_chat(user, span_warning("This contract refuses to be signed by a lesser creature such as yourself."))
		return

	var/datum/religion_sect/spar/sect = GLOB.religious_sect

	var/list/resolved_opponents = list()
	for(var/datum/weakref/resolve_me as anything in sect.past_opponents)
		var/resolved = resolve_me.resolve()
		if(!isnull(resolved))
			resolved_opponents += resolved

	if(user in resolved_opponents && params["stakes"] == STAKES_HOLY_MATCH)
		to_chat(user, span_warning("This contract refuses to be signed up for a holy match by a previous holy match loser. Pick a different stake!"))

	//any updating of the terms should update the UI to display new terms
	. = TRUE

	var/mob/living/carbon/human/left_partner
	if(signed_by[1])
		left_partner = signed_by[1].resolve()
	var/mob/living/carbon/human/right_partner
	if(signed_by[2])
		right_partner = signed_by[2].resolve()

	switch(action)
		if("clear")
			signed_by = list(null, null)//remove weakrefs
		if("fight")
			if(!left_partner || !right_partner || !left_partner.mind || !right_partner.mind)
				return
			if(HAS_TRAIT(left_partner, TRAIT_SPARRING) || HAS_TRAIT(right_partner, TRAIT_SPARRING))
				to_chat(user, span_warning("One participant is already sparring!"))
				return
			var/chaplain = left_partner.mind.holy_role ? left_partner : right_partner
			var/opponent = left_partner.mind.holy_role ? right_partner : left_partner
			new /datum/sparring_match(weapons_condition, GLOB.areas_by_type[arena_condition], stakes_condition, chaplain, opponent)
			qdel(src)
		if("sign")
			if(user == left_partner || user == right_partner)
				to_chat(user, span_warning("You've already signed one side of the contract."))
				return
			var/area/arena_condition_name = GLOB.areas_by_type[arena_condition]
			arena_condition_name = format_text(arena_condition_name.name)
			//setting/checking for terms changed
			var/terms_changed = FALSE
			if(params["weapon"] != weapons_condition)
				if(!params["weapon"])
					return //they hit f5 to clear data then submitted
				terms_changed = TRUE
				weapons_condition = params["weapon"]
			if(params["area"] != arena_condition_name)
				if(!params["area"])
					return //they hit f5 to clear data then submitted
				terms_changed = TRUE
				var/new_area_condition = sect.arenas[params["area"]]
				arena_condition = new_area_condition
			if(params["stakes"] != stakes_condition)
				if(!params["stakes"])
					return //they hit f5 to clear data then submitted
				terms_changed = TRUE
				stakes_condition = params["stakes"]
			//if you change the terms you have to get the other person to sign again.
			if(terms_changed && (left_partner || right_partner))
				signed_by = list(null, null)//remove weakrefs
				to_chat(user, span_warning("You will need to get your sparring partner to sign again under these new terms you've set."))
			//fluff and signing
			var/datum/weakref/user_ref = WEAKREF(user)
			if(params["sign_position"] == CONTRACT_LEFT_FIELD)
				signed_by[1] = user_ref
			else
				signed_by[2] = user_ref


code:
/datum/sparring_match
	///the chaplain. it isn't actually a chaplain all the time, but in the cases where the chaplain is needed this will always be them.
	var/mob/living/carbon/human/chaplain
	///the other fighter
	var/mob/living/carbon/human/opponent
	///what weapons will be allowed during the sparring match
	var/weapons_condition
	///area instance the participants must stay in
	var/area/arena_condition
	///what stakes the fight will have
	var/stakes_condition
	///cheats from the chaplain
	var/chaplain_violations_allowed = 2
	///cheats from the non-chaplain
	var/opponent_violations_allowed = 2
	///outside interventions that ruin the match
	var/flubs = 2

/datum/sparring_match/New(weapons_condition, arena_condition, stakes_condition, mob/living/carbon/human/chaplain, mob/living/carbon/human/opponent)
	. = ..()
	src.weapons_condition = weapons_condition
	src.arena_condition = arena_condition
	src.stakes_condition = stakes_condition
	src.chaplain = chaplain
	src.opponent = opponent
	ADD_TRAIT(chaplain, TRAIT_SPARRING, TRAIT_GENERIC)
	ADD_TRAIT(opponent, TRAIT_SPARRING, TRAIT_GENERIC)
	hook_signals(chaplain)
	hook_signals(opponent)
	chaplain.add_filter("sparring_outline", 9, list("type" = "outline", "color" = "#e02200"))
	opponent.add_filter("sparring_outline", 9, list("type" = "outline", "color" = "#004ee0"))

/datum/sparring_match/proc/hook_signals(mob/living/carbon/human/sparring)
	//weapon conditions
	if(weapons_condition < CONDITION_ANY_WEAPON)
		RegisterSignal(sparring, COMSIG_MOB_FIRED_GUN, .proc/gun_violation)
		RegisterSignal(sparring, COMSIG_MOB_GRENADE_ARMED, .proc/grenade_violation)
	if(weapons_condition <= CONDITION_CEREMONIAL_ONLY)
		RegisterSignal(sparring, COMSIG_PARENT_ATTACKBY, .proc/melee_violation)
	//arena conditions
	RegisterSignal(sparring, COMSIG_MOVABLE_MOVED, .proc/arena_violation)
	//severe violations (insta violation win for other party) conditions
	RegisterSignal(sparring, COMSIG_MOVABLE_TELEPORTED, .proc/teleport_violation)
	//win conditions
	RegisterSignal(sparring, COMSIG_MOB_STATCHANGE, .proc/check_for_victory)
	//flub conditions
	RegisterSignal(sparring, COMSIG_PARENT_ATTACKBY, .proc/outsider_interference)
	RegisterSignal(sparring, COMSIG_ATOM_HULK_ATTACK, .proc/hulk_interference)
	RegisterSignal(sparring, COMSIG_ATOM_ATTACK_HAND, .proc/hand_interference)
	RegisterSignal(sparring, COMSIG_ATOM_ATTACK_PAW, .proc/paw_interference)
	RegisterSignal(sparring, COMSIG_ATOM_HITBY, .proc/thrown_interference)
	RegisterSignal(sparring, COMSIG_ATOM_BULLET_ACT, .proc/projectile_interference)
	//severe flubs (insta match ender, no winners) conditions
	RegisterSignal(sparring, COMSIG_LIVING_DEATH, .proc/death_flub)
	RegisterSignal(sparring, COMSIG_PARENT_QDELETING, .proc/deletion_flub)

/datum/sparring_match/proc/unhook_signals(mob/living/carbon/human/sparring)
	if(!sparring)
		return
	UnregisterSignal(sparring, list(
		COMSIG_MOB_FIRED_GUN,
		COMSIG_MOB_GRENADE_ARMED,
		COMSIG_MOB_ITEM_ATTACK,
		COMSIG_MOVABLE_MOVED,
		COMSIG_MOVABLE_TELEPORTED,
		COMSIG_MOB_STATCHANGE,
		COMSIG_PARENT_ATTACKBY,
		COMSIG_ATOM_HULK_ATTACK,
		COMSIG_ATOM_ATTACK_HAND,
		COMSIG_ATOM_ATTACK_PAW,
		COMSIG_ATOM_HITBY,
		COMSIG_ATOM_BULLET_ACT,
		COMSIG_LIVING_DEATH,
		COMSIG_PARENT_QDELETING,
	))

///someone is changing health state, end the fight in crit
/datum/sparring_match/proc/check_for_victory(datum/participant, new_stat)
	SIGNAL_HANDLER

	//death needs to be a flub, conscious means they haven't won
	if(new_stat == CONSCIOUS || new_stat == DEAD)
		return
	if(participant == chaplain)
		end_match(opponent, chaplain)
	else
		end_match(chaplain, opponent)

// SIGNALS THAT ARE FOR BEING ATTACKED FIRST (GUILTY)
/datum/sparring_match/proc/outsider_interference(datum/source, obj/item/I, mob/attacker)
	SIGNAL_HANDLER
	if(attacker == chaplain || attacker == opponent)
		return
	INVOKE_ASYNC(src, .proc/flub, attacker)

/datum/sparring_match/proc/hulk_interference(datum/source, mob/attacker)
	SIGNAL_HANDLER
	if((attacker == chaplain || attacker == opponent))
		// fist fighting a hulk is so dumb. i can't fathom why you would do this.
		return
	INVOKE_ASYNC(src, .proc/flub, attacker)

/datum/sparring_match/proc/hand_interference(datum/source, mob/living/attacker)
	SIGNAL_HANDLER
	if(attacker == chaplain || attacker == opponent)
		//you can pretty much always use fists as a participant
		return

	INVOKE_ASYNC(src, .proc/flub, attacker)

/datum/sparring_match/proc/paw_interference(datum/source, mob/living/attacker)
	SIGNAL_HANDLER

	if(attacker == chaplain || attacker == opponent)
		//you can pretty much always use paws as a participant
		return

	INVOKE_ASYNC(src, .proc/flub, attacker)

/datum/sparring_match/proc/thrown_interference(datum/source, atom/movable/thrown_movable, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum)
	SIGNAL_HANDLER
	if(istype(thrown_movable, /obj/item))
		var/mob/living/honorbound = source
		var/obj/item/thrown_item = thrown_movable
		var/mob/thrown_by = thrown_item.thrownby?.resolve()
		if(thrown_item.throwforce < honorbound.health && ishuman(thrown_by))
			INVOKE_ASYNC(src, .proc/flub, thrown_by)

/datum/sparring_match/proc/projectile_interference(datum/participant, obj/projectile/proj)
	SIGNAL_HANDLER
	if(proj.firer == chaplain || proj.firer == opponent)
		//oh, well that's allowed. or maybe it isn't. doesn't matter because firing the gun will trigger a violation, so no additional violation needed
		return
	var/mob/living/interfering
	if(isliving(proj.firer))
		interfering = proj.firer
	INVOKE_ASYNC(src, .proc/flub, interfering)

///someone randomly fucking died
/datum/sparring_match/proc/death_flub(datum/deceased)
	SIGNAL_HANDLER

	flubbed_match()

///someone randomly fucking deleted
/datum/sparring_match/proc/deletion_flub(datum/qdeleting)
	SIGNAL_HANDLER

	flubbed_match()

///someone used a gun
/datum/sparring_match/proc/gun_violation(datum/offender)
	SIGNAL_HANDLER
	violation(offender, "using guns")

///someone used a grenade
/datum/sparring_match/proc/grenade_violation(datum/offender)
	SIGNAL_HANDLER
	violation(offender, "using grenades")

///someone used melee weapons
/datum/sparring_match/proc/melee_violation(datum/offender, obj/item/thing, mob/user, params)
	SIGNAL_HANDLER

	if(weapons_condition != CONDITION_CEREMONIAL_ONLY)
		violation(offender, "using melee weapons")
	if(istype(thing, /obj/item/ceremonial_blade))
		return
	violation(offender, "using non ceremonial weapons")

/datum/sparring_match/proc/teleport_violation(datum/offender)
	SIGNAL_HANDLER
	if(offender == chaplain)
		end_match(opponent, chaplain, violation_victory = TRUE)
	else
		end_match(chaplain, opponent, violation_victory = TRUE)

///someone tried to leave
/datum/sparring_match/proc/arena_violation(atom/movable/mover, atom/oldloc, direction)
	SIGNAL_HANDLER

	var/area/inhabited_area = get_area(mover)
	if(inhabited_area == arena_condition)
		return //still in the ring!! :)

	violation(mover, "leaving the arena")
	var/atom/throw_target = get_edge_target_turf(mover, REVERSE_DIR(direction))
	mover.throw_at(throw_target, 6, 4)

/datum/sparring_match/proc/violation(mob/living/carbon/human/offender, reason)
	SIGNAL_HANDLER

	to_chat(offender, span_userdanger("Violation! No [reason]!"))
	if(offender == chaplain)
		chaplain_violations_allowed--
		if(!chaplain_violations_allowed)
			end_match(opponent, chaplain, violation_victory = TRUE)
	else
		opponent_violations_allowed--
		if(!opponent_violations_allowed)
			end_match(chaplain, opponent, violation_victory = TRUE)

/datum/sparring_match/proc/flub(mob/living/interfering)
	if(interfering)
		var/list/possible_punishments = list(PUNISHMENT_OMEN, PUNISHMENT_LIGHTNING)
		if(ishuman(interfering))
			possible_punishments += PUNISHMENT_BRAND
		switch(pick(possible_punishments))
			if(PUNISHMENT_OMEN)
				to_chat(interfering, span_warning("You get a bad feeling... for interfering with [chaplain]'s sparring match..."))
				interfering.AddComponent(/datum/component/omen, TRUE, null, FALSE)
			if(PUNISHMENT_LIGHTNING)
				to_chat(interfering, span_warning("[GLOB.deity] has punished you for interfering with [chaplain]'s sparring match!"))
				lightningbolt(interfering)
			if(PUNISHMENT_BRAND)
				var/mob/living/carbon/human/branded = interfering
				to_chat(interfering, span_warning("[GLOB.deity] brands your flesh for interfering with [chaplain]'s sparring match!!"))
				var/obj/item/bodypart/branded_limb = pick(branded.bodyparts)
				/* SEPTIC EDIT REMOVAL
				branded_limb.force_wound_upwards(/datum/wound/burn/severe/brand)
				*/
				//SEPTIC EDIT BEGIN
				branded_limb.receive_damage(burn = 40)
				//SEPTIC EDIT END
				branded.emote("scream")

	flubs--
	if(!flubs) //too many interferences
		flubbed_match()

///this match was interfered on, nobody wins or loses anything, just end
/datum/sparring_match/proc/flubbed_match()
	cleanup_sparring_match()

	if(chaplain) //flubing means we don't know who is still standing
		to_chat(chaplain, span_boldannounce("The match was flub'd! No winners, no losers. You may restart the match with another contract."))
	if(opponent)
		to_chat(opponent, span_boldannounce("The match was flub'd! No winners, no losers."))
	qdel(src)

///helper to remove all the effects after a match ends
/datum/sparring_match/proc/cleanup_sparring_match()
	REMOVE_TRAIT(chaplain, TRAIT_SPARRING, TRAIT_GENERIC)
	REMOVE_TRAIT(opponent, TRAIT_SPARRING, TRAIT_GENERIC)
	unhook_signals(chaplain)
	unhook_signals(opponent)
	chaplain.remove_filter("sparring_outline")
	opponent.remove_filter("sparring_outline")

/datum/sparring_match/proc/end_match(mob/living/carbon/human/winner, mob/living/carbon/human/loser, violation_victory = FALSE)
	cleanup_sparring_match()
	to_chat(chaplain, span_boldannounce("[violation_victory ? "[loser] DISQUALIFIED!" : ""]  [winner] HAS WON!"))
	to_chat(opponent, span_boldannounce("[violation_victory ? "[loser] DISQUALIFIED!" : ""]  [winner] HAS WON!"))
	win(winner, loser, violation_victory)
	lose(loser, winner)
	if(stakes_condition != STAKES_YOUR_SOUL)
		var/healing_message = "You may want to heal up the loser now."
		if(winner == chaplain)
			healing_message += " Your bible will heal the loser for awhile."
		to_chat(winner, span_notice(healing_message))
	qdel(src)

///most of the effects are handled on `lose()` instead.
/datum/sparring_match/proc/win(mob/living/carbon/human/winner, mob/living/carbon/human/loser, violation_victory)
	switch(stakes_condition)
		if(STAKES_HOLY_MATCH)
			if(winner == chaplain)
				if(violation_victory)
					to_chat(winner, span_warning("[GLOB.deity] is not entertained from a matched decided by violations. No favor awarded..."))
				else
					to_chat(winner, span_nicegreen("You've won favor with [GLOB.deity]!"))
				var/datum/religion_sect/spar/sect = GLOB.religious_sect
				sect.adjust_favor(1, winner)
				sect.past_opponents += WEAKREF(loser)
		if(STAKES_MONEY_MATCH)
			to_chat(winner, span_nicegreen("You've won all of [loser]'s money!"))
		if(STAKES_YOUR_SOUL)
			to_chat(winner, span_nicegreen("You've won [loser]'s SOUL!"))

/datum/sparring_match/proc/lose(mob/living/carbon/human/loser, mob/living/carbon/human/winner)
	if(!loser) //shit happened?
		return
	switch(stakes_condition)
		if(STAKES_HOLY_MATCH)
			if(loser == chaplain)
				var/datum/religion_sect/spar/sect = GLOB.religious_sect
				sect.matches_lost++
				if(sect.matches_lost < 3)
					to_chat(loser, span_userdanger("[GLOB.deity] is angry you lost in their name!"))
					return
				to_chat(loser, span_userdanger("[GLOB.deity] is enraged by your lackluster sparring record!"))
				lightningbolt(loser)
				SEND_SIGNAL(loser, COMSIG_ADD_MOOD_EVENT, "sparring", /datum/mood_event/banished)
				loser.mind.holy_role = NONE
				to_chat(loser, span_userdanger("You have been excommunicated! You are no longer holy!"))
		if(STAKES_MONEY_MATCH)
			to_chat(loser, span_userdanger("You've lost all your money to [winner]!"))
			var/datum/bank_account/loser_account = loser.get_bank_account()
			var/datum/bank_account/winner_account = winner.get_bank_account()
			if(!loser_account || !winner_account)//the winner is pretty owned in this case but whatever shoulda read the fine print of the contract
				return
			winner_account.transfer_money(loser_account, loser_account.account_balance)
		if(STAKES_YOUR_SOUL)
			var/turf/shard_turf = get_turf(loser)
			if(!shard_turf)
				return
			to_chat(loser, span_userdanger("You've lost ownership over your soul to [winner]!"))
			var/obj/item/soulstone/anybody/chaplain/sparring/shard = new(shard_turf)
			shard.capture_soul(loser, winner, forced = TRUE)

repo_name: AnalWerewolf/sundown

license: mit


code:
/// Requests from prayers
#define REQUEST_PRAYER "request_prayer"
/// Requests for Centcom
#define REQUEST_CENTCOM "request_centcom"
/// Requests for the Syndicate
#define REQUEST_SYNDICATE "request_syndicate"
/// Requests for the nuke code
#define REQUEST_NUKE "request_nuke"

/**
 * # Request
 *
 * A representation of an in-game request, such as a prayer.
 */
/datum/request
	/// Unique ID of the request
	var/id
	/// Atomic ID for increment unique request IDs
	var/static/atomic_id = 0
	/// The type of request
	var/req_type
	/// The owner of the request, the player who created it
	var/client/owner
	/// The ckey of the owner, used for re-binding variables on login
	var/owner_ckey
	/// The name of the owner, in format <key>/<name>, assigned at time of request creation
	var/owner_name
	/// The message associated with the request
	var/message
	/// When the request was created
	var/timestamp

/datum/request/New(client/requestee, type, request)
	if (!requestee)
		qdel(src)
		return
	id = ++atomic_id
	owner = requestee
	owner_ckey = owner.ckey
	req_type = type
	message = request
	timestamp = world.time
	owner_name = key_name(requestee, FALSE)


code:
GLOBAL_DATUM_INIT(requests, /datum/request_manager, new)

/**
 * # Request Manager
 *
 * Handles all player requests (prayers, centcom requests, syndicate requests)
 * that occur in the duration of a round.
 */
/datum/request_manager
	/// Associative list of ckey -> list of requests
	var/list/requests = list()
	/// List where requests can be accessed by ID
	var/list/requests_by_id = list()

/datum/request_manager/Destroy(force, ...)
	QDEL_LIST(requests)
	return ..()

/**
 * Used in the new client pipeline to catch when clients are reconnecting and need to have their
 * reference re-assigned to the 'owner' variable of any requests
 *
 * Arguments:
 * * C - The client who is logging in
 */
/datum/request_manager/proc/client_login(client/C)
	if (!requests[C.ckey])
		return
	for (var/datum/request/request as anything in requests[C.ckey])
		request.owner = C

/**
 * Used in the destroy client pipeline to catch when clients are disconnecting and need to have their
 * reference nulled on the 'owner' variable of any requests
 *
 * Arguments:
 * * C - The client who is logging out
 */
/datum/request_manager/proc/client_logout(client/C)
	if (!requests[C.ckey])
		return
	for (var/datum/request/request as anything in requests[C.ckey])
		request.owner = null

/**
 * Creates a request for a prayer, and notifies admins who have the sound notifications enabled when appropriate
 *
 * Arguments:
 * * C - The client who is praying
 * * message - The prayer
 * * is_chaplain - Boolean operator describing if the prayer is from a chaplain
 */
/datum/request_manager/proc/pray(client/C, message, is_chaplain)
	request_for_client(C, REQUEST_PRAYER, message)
	for(var/client/admin in GLOB.admins)
		if(is_chaplain && admin.prefs.chat_toggles & CHAT_PRAYER && admin.prefs.toggles & SOUND_PRAYERS)
			SEND_SOUND(admin, sound('sound/effects/pray.ogg'))

/**
 * Creates a request for a Centcom message
 *
 * Arguments:
 * * C - The client who is sending the request
 * * message - The message
 */
/datum/request_manager/proc/message_centcom(client/C, message)
	request_for_client(C, REQUEST_CENTCOM, message)

/**
 * Creates a request for a Syndicate message
 *
 * Arguments:
 * * C - The client who is sending the request
 * * message - The message
 */
/datum/request_manager/proc/message_syndicate(client/C, message)
	request_for_client(C, REQUEST_SYNDICATE, message)

/**
 * Creates a request for the nuclear self destruct codes
 *
 * Arguments:
 * * C - The client who is sending the request
 * * message - The message
 */
/datum/request_manager/proc/nuke_request(client/C, message)
	request_for_client(C, REQUEST_NUKE, message)

/**
 * Creates a request and registers the request with all necessary internal tracking lists
 *
 * Arguments:
 * * C - The client who is sending the request
 * * type - The type of request, see defines
 * * message - The message
 */
/datum/request_manager/proc/request_for_client(client/C, type, message)
	var/datum/request/request = new(C, type, message)
	if (!requests[C.ckey])
		requests[C.ckey] = list()
	requests[C.ckey] += request
	requests_by_id.len++
	requests_by_id[request.id] = request

/datum/request_manager/ui_interact(mob/user, datum/tgui/ui = null)
	ui = SStgui.try_update_ui(user, src, ui)
	if (!ui)
		ui = new(user, src, "RequestManager")
		ui.open()

/datum/request_manager/ui_state(mob/user)
	return GLOB.admin_state

/datum/request_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	if (..())
		return

	// Only admins should be sending actions
	if (!check_rights(R_ADMIN))
		to_chat(usr, "You do not have permission to do this, you require +ADMIN", confidential = TRUE)
		return

	// Get the request this relates to
	var/id = params["id"] != null ? text2num(params["id"]) : null
	if (!id)
		to_chat(usr, "Failed to find a request ID in your action, please report this", confidential = TRUE)
		CRASH("Received an action without a request ID, this shouldn't happen!")
	var/datum/request/request = !id ? null : requests_by_id[id]

	switch(action)
		if ("pp")
			var/mob/M = request.owner?.mob
			usr.client.holder.show_player_panel(M)
			return TRUE
		if ("vv")
			var/mob/M = request.owner?.mob
			usr.client.debug_variables(M)
			return TRUE
		if ("sm")
			var/mob/M = request.owner?.mob
			usr.client.cmd_admin_subtle_message(M)
			return TRUE
		if ("flw")
			var/mob/M = request.owner?.mob
			usr.client.admin_follow(M)
			return TRUE
		if ("tp")
			if(!SSticker.HasRoundStarted())
				tgui_alert(usr,"The game hasn't started yet!")
				return TRUE
			var/mob/M = request.owner?.mob
			if(!ismob(M))
				var/datum/mind/D = M
				if(!istype(D))
					to_chat(usr, "This can only be used on instances of type /mob and /mind", confidential = TRUE)
					return TRUE
				else
					D.traitor_panel()
					return TRUE
			else
				usr.client.holder.show_traitor_panel(M)
				return TRUE
		if ("logs")
			var/mob/M = request.owner?.mob
			if(!ismob(M))
				to_chat(usr, "This can only be used on instances of type /mob.", confidential = TRUE)
				return TRUE
			show_individual_logging_panel(M, null, null)
			return TRUE
		if ("smite")
			if(!check_rights(R_FUN))
				to_chat(usr, "Insufficient permissions to smite, you require +FUN", confidential = TRUE)
				return TRUE
			var/mob/living/carbon/human/H = request.owner?.mob
			if (!H || !istype(H))
				to_chat(usr, "This can only be used on instances of type /mob/living/carbon/human", confidential = TRUE)
				return TRUE
			usr.client.smite(H)
			return TRUE
		if ("rply")
			if (request.req_type == REQUEST_PRAYER)
				to_chat(usr, "Cannot reply to a prayer", confidential = TRUE)
				return TRUE
			var/mob/M = request.owner?.mob
			usr.client.admin_headset_message(M, request.req_type == REQUEST_SYNDICATE ? RADIO_CHANNEL_SYNDICATE : RADIO_CHANNEL_CENTCOM)
			return TRUE
		if ("setcode")
			if (request.req_type != REQUEST_NUKE)
				to_chat(usr, "You cannot set the nuke code for a non-nuke-code-request request!", confidential = TRUE)
				return TRUE
			var/code = random_nukecode()
			for(var/obj/machinery/nuclearbomb/selfdestruct/SD in GLOB.nuke_list)
				SD.r_code = code
			message_admins("[key_name_admin(usr)] has set the self-destruct code to \"[code]\".")
			return TRUE

/datum/request_manager/ui_data(mob/user)
	. = list(
		"requests" = list()
	)
	for (var/ckey in requests)
		for (var/datum/request/request as anything in requests[ckey])
			var/list/data = list(
				"id" = request.id,
				"req_type" = request.req_type,
				"owner" = request.owner ? "[REF(request.owner)]" : null,
				"owner_ckey" = request.owner_ckey,
				"owner_name" = request.owner_name,
				"message" = request.message,
				"timestamp" = request.timestamp,
				"timestamp_str" = gameTimestamp(wtime = request.timestamp)
			)
			.["requests"] += list(data)


code:
// Embedded signaller used in anomalies.
/obj/item/assembly/signaler/anomaly
	name = "anomaly core"
	desc = "The neutralized core of an anomaly. It'd probably be valuable for research."
	icon_state = "anomaly_core"
	inhand_icon_state = "electronic"
	lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi'
	righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi'
	resistance_flags = FIRE_PROOF
	var/anomaly_type = /obj/effect/anomaly

/obj/item/assembly/signaler/anomaly/receive_signal(datum/signal/signal)
	if(!signal)
		return FALSE
	if(signal.data["code"] != code)
		return FALSE
	if(suicider)
		manual_suicide(suicider)
	for(var/obj/effect/anomaly/A in get_turf(src))
		A.anomalyNeutralize()
	return TRUE

/obj/item/assembly/signaler/anomaly/manual_suicide(mob/living/carbon/user)
	user.visible_message(span_suicide("[user]'s [src] is reacting to the radio signal, warping [user.p_their()] body!"))
	user.set_suicide(TRUE)
	user.suicide_log()
	user.gib()

/obj/item/assembly/signaler/anomaly/attackby(obj/item/I, mob/user, params)
	if(I.tool_behaviour == TOOL_ANALYZER)
		to_chat(user, span_notice("Analyzing... [src]'s stabilized field is fluctuating along frequency [format_frequency(frequency)], code [code]."))
	return ..()

//Anomaly cores
/obj/item/assembly/signaler/anomaly/pyro
	name = "\improper pyroclastic anomaly core"
	desc = "The neutralized core of a pyroclastic anomaly. It feels warm to the touch. It'd probably be valuable for research."
	icon_state = "pyro_core"
	anomaly_type = /obj/effect/anomaly/pyro

/obj/item/assembly/signaler/anomaly/grav
	name = "\improper gravitational anomaly core"
	desc = "The neutralized core of a gravitational anomaly. It feels much heavier than it looks. It'd probably be valuable for research."
	icon_state = "grav_core"
	anomaly_type = /obj/effect/anomaly/grav

/obj/item/assembly/signaler/anomaly/flux
	name = "\improper flux anomaly core"
	desc = "The neutralized core of a flux anomaly. Touching it makes your skin tingle. It'd probably be valuable for research."
	icon_state = "flux_core"
	anomaly_type = /obj/effect/anomaly/flux

/obj/item/assembly/signaler/anomaly/bluespace
	name = "\improper bluespace anomaly core"
	desc = "The neutralized core of a bluespace anomaly. It keeps phasing in and out of view. It'd probably be valuable for research."
	icon_state = "anomaly_core"
	anomaly_type = /obj/effect/anomaly/bluespace

/obj/item/assembly/signaler/anomaly/vortex
	name = "\improper vortex anomaly core"
	desc = "The neutralized core of a vortex anomaly. It won't sit still, as if some invisible force is acting on it. It'd probably be valuable for research."
	icon_state = "vortex_core"
	anomaly_type = /obj/effect/anomaly/bhole


code:
#define MAX_RADIUS_REQUIRED 20 //maxcap
#define MIN_RADIUS_REQUIRED 4 //1, 2, 4
/// How long the compression test can last before the machine just gives up and ejects the items.
#define COMPRESSION_TEST_TIME (SSOBJ_DT SECONDS * 5)

/**
 * # Explosive compressor machines
 *
 * The explosive compressor machine used in anomaly core production.
 *
 * Uses the standard ordnance/tank explosion scaling to compress raw anomaly cores into completed ones. The required explosion radius increases as more cores of that type are created.
 */
/obj/machinery/research/explosive_compressor
	name = "implosion compressor"
	desc = "An advanced machine capable of implosion-compressing raw anomaly cores into finished artifacts."
	icon = 'icons/obj/machines/research.dmi'
	icon_state = "explosive_compressor"
	density = TRUE

	/// The raw core inserted in the machine.
	var/obj/item/raw_anomaly_core/inserted_core
	/// The TTV inserted in the machine.
	var/obj/item/transfer_valve/inserted_bomb
	/// The timer that lets us timeout the test.
	var/datum/timedevent/timeout_timer
	/// Whether we are currently testing a bomb and core.
	var/testing = FALSE
	/// The message produced by the explosive compressor at the end of the compression test.
	var/test_status = null
	/// The last time we did say_requirements(), because someone will inevitably click spam this.
	var/last_requirements_say = 0

/obj/machinery/research/explosive_compressor/Initialize(mapload)
	. = ..()
	RegisterSignal(src, COMSIG_ATOM_INTERNAL_EXPLOSION, .proc/check_test)

/obj/machinery/research/explosive_compressor/Destroy()
	UnregisterSignal(src, COMSIG_ATOM_INTERNAL_EXPLOSION)
	return ..()

/obj/machinery/research/explosive_compressor/examine(mob/user)
	. = ..()
	. += span_notice("Ctrl-Click to remove an inserted core.")
	. += span_notice("Click with an empty hand to gather information about the required radius of an inserted core. Insert a ready TTV to start the implosion process if a core is inserted.")

/obj/machinery/research/explosive_compressor/assume_air(datum/gas_mixture/giver)
	qdel(giver)
	return null // Required to make the TTV not vent directly into the air.

/obj/machinery/research/explosive_compressor/attack_hand(mob/living/user, list/modifiers)
	. = ..()
	if(.)
		return
	if(!inserted_core)
		to_chat(user, span_warning("There is no core inserted."))
		return
	if(last_requirements_say + 3 SECONDS > world.time)
		return
	last_requirements_say = world.time
	say_requirements(inserted_core)

/obj/machinery/research/explosive_compressor/CtrlClick(mob/living/user)
	. = ..()
	if(!istype(user) || !user.Adjacent(src) || !(user.mobility_flags & MOBILITY_USE))
		return
	if(!inserted_core)
		to_chat(user, span_warning("There is no core inserted."))
		return
	if(testing)
		to_chat(user, span_warning("You can't remove [inserted_core] from [src] while [p_theyre()] in testing mode."))
		return
	inserted_core.forceMove(get_turf(user))
	to_chat(user, span_notice("You remove [inserted_core] from [src]."))
	user.put_in_hands(inserted_core)
	inserted_core = null

/**
 * Says (no, literally) the data of required explosive power for a certain anomaly type.
 */
/obj/machinery/research/explosive_compressor/proc/say_requirements(obj/item/raw_anomaly_core/core)
	var/required = get_required_radius(core.anomaly_type)
	if(isnull(required))
		say("Unfortunately, due to diminishing supplies of condensed anomalous matter, [core] and any cores of its type are no longer of a sufficient quality level to be compressed into a working core.")
	else
		say("[core] requires a minimum of a theoretical radius of [required] to successfully implode into a charged anomaly core.")

/**
 * Determines how much explosive power (last value, so light impact theoretical radius) is required to make a certain anomaly type.
 *
 * Returns null if the max amount has already been reached.
 *
 * Arguments:
 * * anomaly_type - anomaly type define
 */
/obj/machinery/research/explosive_compressor/proc/get_required_radius(anomaly_type)
	var/already_made = SSresearch.created_anomaly_types[anomaly_type]
	var/hard_limit = SSresearch.anomaly_hard_limit_by_type[anomaly_type]
	if(already_made >= hard_limit)
		return //return null
	// my crappy autoscale formula
	// linear scaling.
	var/radius_span = MAX_RADIUS_REQUIRED - MIN_RADIUS_REQUIRED
	var/radius_increase_per_core = radius_span / hard_limit
	var/radius = clamp(round(MIN_RADIUS_REQUIRED + radius_increase_per_core * already_made, 1), MIN_RADIUS_REQUIRED, MAX_RADIUS_REQUIRED)
	return radius

/obj/machinery/research/explosive_compressor/attackby(obj/item/tool, mob/living/user, params)
	. = ..()
	if(istype(tool, /obj/item/raw_anomaly_core))
		if(inserted_core)
			to_chat(user, span_warning("There is already a core in [src]."))
			return
		if(testing)
			to_chat(user, span_warning("You can't insert [tool] into [src] while [p_theyre()] in testing mode."))
			return
		if(!user.transferItemToLoc(tool, src))
			to_chat(user, span_warning("[tool] is stuck to your hand."))
			return
		inserted_core = tool
		to_chat(user, span_notice("You insert [tool] into [src]."))
		return
	if(istype(tool, /obj/item/transfer_valve))
		// If they don't have a bomb core inserted, don't let them insert this. If they do, insert and do implosion.
		if(!inserted_core)
			to_chat(user, span_warning("There is no core inserted in [src]. What would be the point of detonating an implosion without a core?"))
			return
		if(testing)
			to_chat(user, span_warning("You can't insert [tool] into [src] while [p_theyre()] in testing mode."))
			return
		var/obj/item/transfer_valve/valve = tool
		if(!valve.ready())
			to_chat(user, span_warning("[valve] is incomplete."))
			return
		if(!user.transferItemToLoc(tool, src))
			to_chat(user, span_warning("[tool] is stuck to your hand."))
			return
		inserted_bomb = tool
		to_chat(user, span_notice("You insert [tool] and press the start button."))
		start_test()

/**
 * Starts a compression test.
 */
/obj/machinery/research/explosive_compressor/proc/start_test()
	if(!istype(inserted_core) || !istype(inserted_bomb))
		end_test("ERROR: Missing equpment. Items ejected.")
		return

	say("Beginning compression test. Opening transfer valve.")
	testing = TRUE
	test_status = null
	inserted_bomb.toggle_valve()
	timeout_timer = addtimer(CALLBACK(src, .proc/timeout_test), COMPRESSION_TEST_TIME, TIMER_STOPPABLE | TIMER_UNIQUE | TIMER_NO_HASH_WAIT)
	return

/**
 * Ends a compression test.
 *
 * Arguments:
 * - message: A message for the compressor to say when the test ends.
 */
/obj/machinery/research/explosive_compressor/proc/end_test(message)
	if(inserted_core)
		inserted_core.forceMove(drop_location())
		inserted_core = null
	if(inserted_bomb)
		inserted_bomb.forceMove(drop_location())
		inserted_bomb = null
	if(timeout_timer)
		QDEL_NULL(timeout_timer)
	if(message)
		say(message)
	testing = FALSE
	return

/**
 * Checks whether an internal explosion was sufficient to compress the core.
 */
/obj/machinery/research/explosive_compressor/proc/check_test(atom/source, list/arguments)
	SIGNAL_HANDLER
	. = COMSIG_CANCEL_EXPLOSION
	if(!inserted_core)
		test_status = "ERROR: No core present during detonation."
		return

	var/heavy = arguments[EXARG_KEY_DEV_RANGE]
	var/medium = arguments[EXARG_KEY_HEAVY_RANGE]
	var/light = arguments[EXARG_KEY_LIGHT_RANGE]
	var/explosion_range = max(heavy, medium, light, 0)
	var/required_range = get_required_radius(inserted_core.anomaly_type)
	var/turf/location = get_turf(src)

	var/cap_multiplier = SSmapping.level_trait(location.z, ZTRAIT_BOMBCAP_MULTIPLIER)
	if(isnull(cap_multiplier))
		cap_multiplier = 1
	var/capped_heavy = min(GLOB.MAX_EX_DEVESTATION_RANGE * cap_multiplier, heavy)
	var/capped_medium = min(GLOB.MAX_EX_HEAVY_RANGE * cap_multiplier, medium)
	SSexplosions.shake_the_room(location, explosion_range, (capped_heavy * 15) + (capped_medium * 20), capped_heavy, capped_medium)

	if(explosion_range < required_range)
		test_status = "Resultant detonation failed to produce enough implosive power to compress [inserted_core]. Core ejected."
		return

	if(test_status)
		return
	inserted_core = inserted_core.create_core(src, TRUE, TRUE)
	test_status = "Success. Resultant detonation has theoretical range of [explosion_range]. Required radius was [required_range]. Core production complete."
	return

/**
 * Handles timing out the test after a while.
 */
/obj/machinery/research/explosive_compressor/proc/timeout_test()
	timeout_timer = null
	if(!test_status)
		test_status = "Transfer valve resulted in negligible explosive power. Items ejected."
	end_test(test_status)

#undef MAX_RADIUS_REQUIRED
#undef MIN_RADIUS_REQUIRED


code:
/**
 * # Raw Anomaly Cores
 *
 * The current precursor to anomaly cores, these are manufactured into 'finished' anomaly cores for use in research, items, and more.
 *
 * The current amounts created is stored in `SSresearch.created_anomaly_types[ANOMALY_CORE_TYPE_DEFINE] = amount`.
 * The hard limits are in `code/__DEFINES/anomalies.dm`.
 */
/obj/item/raw_anomaly_core
	name = "raw anomaly core"
	desc = "You shouldn't be seeing this. Someone screwed up."
	icon = 'icons/obj/assemblies/new_assemblies.dmi'
	icon_state = "broken_state"

	/// Anomaly type
	var/anomaly_type

/obj/item/raw_anomaly_core/bluespace
	name = "raw bluespace core"
	desc = "The raw core of a bluespace anomaly, glowing and full of potential."
	anomaly_type = /obj/item/assembly/signaler/anomaly/bluespace
	icon_state = "rawcore_bluespace"

/obj/item/raw_anomaly_core/vortex
	name = "raw vortex core"
	desc = "The raw core of a vortex anomaly. Feels heavy to the touch."
	anomaly_type = /obj/item/assembly/signaler/anomaly/vortex
	icon_state = "rawcore_vortex"

/obj/item/raw_anomaly_core/grav
	name = "raw gravity core"
	desc = "The raw core of a gravity anomaly. The air seems attracted to it."
	anomaly_type = /obj/item/assembly/signaler/anomaly/grav
	icon_state = "rawcore_grav"

/obj/item/raw_anomaly_core/pyro
	desc = "The raw core of a pyro anomaly. It is warm to the touch."
	name = "raw pyro core"
	anomaly_type = /obj/item/assembly/signaler/anomaly/pyro
	icon_state = "rawcore_pyro"

/obj/item/raw_anomaly_core/flux
	name = "raw flux core"
	desc = "The raw core of a flux anomaly, faintly crackling with energy."
	anomaly_type = /obj/item/assembly/signaler/anomaly/flux
	icon_state = "rawcore_flux"

/obj/item/raw_anomaly_core/random
	name = "random raw core"
	desc = "You should not see this!"
	icon_state = "rawcore_bluespace"

/obj/item/raw_anomaly_core/random/Initialize(mapload)
	. = ..()
	var/path = pick(subtypesof(/obj/item/raw_anomaly_core))
	new path(loc)
	return INITIALIZE_HINT_QDEL

/**
 * Created the resulting core after being "made" into it.
 *
 * Arguments:
 * * newloc - Where the new core will be created
 * * del_self - should we qdel(src)
 * * count_towards_limit - should we increment the amount of created cores on SSresearch
 */
/obj/item/raw_anomaly_core/proc/create_core(newloc, del_self = FALSE, count_towards_limit = FALSE)
	. = new anomaly_type(newloc)
	if(count_towards_limit)
		var/existing = SSresearch.created_anomaly_types[anomaly_type] || 0
		SSresearch.created_anomaly_types[anomaly_type] = existing + 1
	if(del_self)
		qdel(src)


code:
//This system is designed to act as an in-between for cargo and science, and the first major money sink in the game outside of just buying things from cargo (As of 10/9/19, anyway).

//economics defined values, subject to change should anything be too high or low in practice.

#define MACHINE_OPERATION 100000
#define MACHINE_OVERLOAD 500000
#define MAJOR_THRESHOLD 6*CARGO_CRATE_VALUE
#define MINOR_THRESHOLD 4*CARGO_CRATE_VALUE
#define STANDARD_DEVIATION 2*CARGO_CRATE_VALUE

/obj/machinery/rnd/bepis
	name = "\improper B.E.P.I.S. Chamber"
	desc = "A high fidelity testing device which unlocks the secrets of the known universe using the two most powerful substances available to man: excessive amounts of electricity and capital."
	icon = 'icons/obj/machines/bepis.dmi'
	icon_state = "chamber"
	base_icon_state = "chamber"
	density = TRUE
	layer = ABOVE_MOB_LAYER
	use_power = IDLE_POWER_USE
	active_power_usage = 1500
	circuit = /obj/item/circuitboard/machine/bepis

	///How much cash the UI and machine are depositing at a time.
	var/banking_amount = 100
	///How much stored player cash exists within the machine.
	var/banked_cash = 0
	///Payer's bank account.
	var/datum/bank_account/account
	///Name on the payer's bank account.
	var/account_name
	///When the BEPIS fails to hand out any reward, the ERROR cause will be a randomly picked string displayed on the UI.
	var/error_cause = null

	//Vars related to probability and chance of success for testing, using gaussian normal distribution.
	///How much cash you will need to obtain a Major Tech Disk reward.
	var/major_threshold = MAJOR_THRESHOLD
	///How much cash you will need to obtain a minor invention reward.
	var/minor_threshold = MINOR_THRESHOLD
	///The standard deviation of the BEPIS's gaussian normal distribution.
	var/std = STANDARD_DEVIATION

	//Stock part variables
	///Multiplier that lowers how much the BEPIS' power costs are. Maximum of 1, upgraded to a minimum of 0.7. See RefreshParts.
	var/power_saver = 1
	///Variability on the money you actively spend on the BEPIS, with higher inaccuracy making the most change, good and bad to spent cash.
	var/inaccuracy_percentage = 1.5
	///How much "cash" is added to your inserted cash efforts for free. Based on manipulator stock part level.
	var/positive_cash_offset = 0
	///How much "cost" is removed from both the minor and major threshold costs. Based on laser stock part level.
	var/negative_cash_offset = 0
	///List of objects that constitute your minor rewards. All rewards are unique or rare outside of the BEPIS.
	var/minor_rewards = list(
		//To add a new minor reward, add it here.
		/obj/item/stack/circuit_stack/full,
		/obj/item/pen/survival,
		/obj/item/circuitboard/machine/sleeper/party,
		/obj/item/toy/sprayoncan,
	)

/obj/machinery/rnd/bepis/attackby(obj/item/O, mob/user, params)
	if(!is_operational)
		to_chat(user, span_notice("[src] can't accept money when it's not functioning."))
		return
	if(istype(O, /obj/item/holochip) || istype(O, /obj/item/stack/spacecash))
		var/deposit_value = O.get_item_credit_value()
		banked_cash += deposit_value
		qdel(O)
		say("Deposited [deposit_value] credits into storage.")
		update_appearance()
		return
	if(istype(O, /obj/item/card/id))
		var/obj/item/card/id/Card = O
		if(Card.registered_account)
			account = Card.registered_account
			account_name = Card.registered_name
			say("New account detected. Console Updated.")
		else
			say("No account detected on card. Aborting.")
		return
	return ..()

/obj/machinery/rnd/bepis/screwdriver_act(mob/living/user, obj/item/tool)
	return default_deconstruction_screwdriver(user, "chamber_open", "chamber", tool)

/obj/machinery/rnd/bepis/RefreshParts()
	var/C = 0
	var/M = 0
	var/L = 0
	var/S = 0
	for(var/obj/item/stock_parts/capacitor/Cap in component_parts)
		C += ((Cap.rating - 1) * 0.1)
	power_saver = 1 - C
	for(var/obj/item/stock_parts/manipulator/Manip in component_parts)
		M += ((Manip.rating - 1) * 250)
	positive_cash_offset = M
	for(var/obj/item/stock_parts/micro_laser/Laser in component_parts)
		L += ((Laser.rating - 1) * 250)
	negative_cash_offset = L
	for(var/obj/item/stock_parts/scanning_module/Scan in component_parts)
		S += ((Scan.rating - 1) * 0.25)
	inaccuracy_percentage = (1.5 - S)

/obj/machinery/rnd/bepis/update_icon_state()
	if(panel_open == TRUE)
		icon_state = "[base_icon_state]_open"
		return ..()
	if((use_power == ACTIVE_POWER_USE) && (banked_cash > 0) && (is_operational))
		icon_state = "[base_icon_state]_active_loaded"
		return ..()
	if (((use_power == IDLE_POWER_USE) && (banked_cash > 0)) || (banked_cash > 0) && (!is_operational))
		icon_state = "[base_icon_state]_loaded"
		return ..()
	if(use_power == ACTIVE_POWER_USE && is_operational)
		icon_state = "[base_icon_state]_active"
		return ..()
	if(((use_power == IDLE_POWER_USE) && (banked_cash == 0)) || (!is_operational))
		icon_state = base_icon_state
		return ..()
	return ..()

/obj/machinery/rnd/bepis/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "Bepis", name)
		ui.open()
	RefreshParts()
	if(isliving(user))
		var/mob/living/customer = user
		account = customer.get_bank_account()

/obj/machinery/rnd/bepis/ui_data(mob/user)
	var/list/data = list()
	var/powered = FALSE
	var/zvalue = ((banking_amount + banked_cash) - (major_threshold - positive_cash_offset - negative_cash_offset))/(std)
	var/std_success = 0
	var/prob_success = 0
	//Admittedly this is messy, but not nearly as messy as the alternative, which is jury-rigging an entire Z-table into the code, or making an adaptive z-table.
	var/z = abs(zvalue)
	if(z > 0 && z <= 0.5)
		std_success = 19.1
	else if(z > 0.5 && z <= 1.0)
		std_success = 34.1
	else if(z > 1.0 && z <= 1.5)
		std_success = 43.3
	else if(z > 1.5 && z <= 2.0)
		std_success = 47.7
	else if(z > 2.0 && z <= 2.5)
		std_success = 49.4
	else
		std_success = 50
	if(zvalue > 0)
		prob_success = 50 + std_success
	else if(zvalue == 0)
		prob_success = 50
	else
		prob_success = 50 - std_success

	if(use_power == ACTIVE_POWER_USE)
		powered = TRUE
	data["account_owner"] = account_name
	data["amount"] = banking_amount
	data["stored_cash"] = account?.account_balance
	data["mean_value"] = (major_threshold - positive_cash_offset - negative_cash_offset)
	data["error_name"] = error_cause
	data["power_saver"] = power_saver
	data["accuracy_percentage"] = inaccuracy_percentage * 100
	data["positive_cash_offset"] = positive_cash_offset
	data["negative_cash_offset"] = negative_cash_offset
	data["manual_power"] = powered ? FALSE : TRUE
	data["silicon_check"] = issilicon(user)
	data["success_estimate"] = prob_success
	return data

/obj/machinery/rnd/bepis/ui_act(action,params)
	. = ..()
	if(.)
		return
	switch(action)
		if("begin_experiment")
			if(use_power == IDLE_POWER_USE)
				return
			depositcash()
			if(banked_cash == 0)
				say("Please select funds to deposit to begin testing.")
				return
			calcsuccess()
			use_power(MACHINE_OPERATION * power_saver) //This thing should eat your APC battery if you're not careful.
			update_use_power(IDLE_POWER_USE) //Machine shuts off after use to prevent spam and look better visually.
			update_appearance()
		if("amount")
			var/input = text2num(params["amount"])
			if(input)
				banking_amount = input
		if("toggle_power")
			if(use_power == ACTIVE_POWER_USE)
				update_use_power(IDLE_POWER_USE)
			else
				update_use_power(ACTIVE_POWER_USE)
			update_appearance()
		if("account_reset")
			if(use_power == IDLE_POWER_USE)
				return
			account_name = ""
			account = null
			say("Account settings reset.")
	. = TRUE

/**
 * Proc that handles the user's account to deposit credits for the BEPIS.
 * Handles success and fail cases for transferring credits, then logs the transaction and uses small amounts of power.
 **/
/obj/machinery/rnd/bepis/proc/depositcash()
	var/deposit_value = 0
	deposit_value = banking_amount
	if(deposit_value == 0)
		update_appearance()
		say("Attempting to deposit 0 credits. Aborting.")
		return
	deposit_value = clamp(round(deposit_value, 1), 1, 10000)
	if(!account)
		say("Cannot find user account. Please swipe a valid ID.")
		return
	if(!account.has_money(deposit_value))
		say("You do not possess enough credits.")
		return
	account.adjust_money(-deposit_value) //The money vanishes, not paid to any accounts.
	SSblackbox.record_feedback("amount", "BEPIS_credits_spent", deposit_value)
	log_econ("[deposit_value] credits were inserted into [src] by [account.account_holder]")
	banked_cash += deposit_value
	use_power(1000 * power_saver)
	return

/**
 * Proc used to determine the experiment math and results all in one.
 * Uses banked_cash and stock part levels to determine minor, major, and real gauss values for the BEPIS to hold.
 * If by the end real is larger than major, You get a tech disk. If all the disks are earned or you at least beat minor, you get a minor reward.
 **/

/obj/machinery/rnd/bepis/proc/calcsuccess()
	var/turf/dropturf = null
	var/gauss_major = 0
	var/gauss_minor = 0
	var/gauss_real = 0
	var/list/turfs = block(locate(x-1,y-1,z),locate(x+1,y+1,z)) //NO MORE DISCS IN WINDOWS
	while(length(turfs))
		var/turf/T = pick_n_take(turfs)
		if(T.is_blocked_turf(TRUE))
			continue
		else
			dropturf = T
			break
	if (!dropturf)
		dropturf = drop_location()
	gauss_major = (gaussian(major_threshold, std) - negative_cash_offset) //This is the randomized profit value that this experiment has to surpass to unlock a tech.
	gauss_minor = (gaussian(minor_threshold, std) - negative_cash_offset) //And this is the threshold to instead get a minor prize.
	gauss_real = (gaussian(banked_cash, std*inaccuracy_percentage) + positive_cash_offset) //this is the randomized profit value that your experiment expects to give.
	say("Real: [gauss_real]. Minor: [gauss_minor]. Major: [gauss_major].")
	flick("chamber_flash",src)
	update_appearance()
	banked_cash = 0
	if((gauss_real >= gauss_major) && (SSresearch.techweb_nodes_experimental.len > 0)) //Major Success.
		say("Experiment concluded with major success. New technology node discovered on technology disc.")
		new /obj/item/disk/tech_disk/major(dropturf,1)
		if(SSresearch.techweb_nodes_experimental.len == 0)
			say("Expended all available experimental technology nodes. Resorting to minor rewards.")
		return
	if(gauss_real >= gauss_minor) //Minor Success.
		var/reward = pick(minor_rewards)
		new reward(dropturf)
		say("Experiment concluded with partial success. Dispensing compiled research efforts.")
		return
	if(gauss_real <= -1) //Critical Failure
		say("ERROR: CRITICAL MACHIME MALFUNCTI- ON. CURRENCY IS NOT CRASH. CANNOT COMPUTE COMMAND: 'make bucks'") //not a typo, for once.
		new /mob/living/simple_animal/deer(dropturf, 1)
		use_power(MACHINE_OVERLOAD * power_saver) //To prevent gambling at low cost and also prevent spamming for infinite deer.
		return
	//Minor Failure
	error_cause = pick("attempted to sell grey products to American dominated market.","attempted to sell gray products to British dominated market.","placed wild assumption that PDAs would go out of style.","simulated product #76 damaged brand reputation mortally.","simulated business model resembled 'pyramid scheme' by 98.7%.","product accidently granted override access to all station doors.")
	say("Experiment concluded with zero product viability. Cause of error: [error_cause]")
	return


code:
/***************************************************************
** Design Datums   **
** All the data for building stuff.   **
***************************************************************/
/*
For the materials datum, it assumes you need reagents unless specified otherwise. To designate a material that isn't a reagent,
you use one of the material IDs below. These are NOT ids in the usual sense (they aren't defined in the object or part of a datum),
they are simply references used as part of a "has materials?" type proc. They all start with a $ to denote that they aren't reagents.
The currently supporting non-reagent materials. All material amounts are set as the define MINERAL_MATERIAL_AMOUNT, which defaults to 2000

Don't add new keyword/IDs if they are made from an existing one (such as rods which are made from iron). Only add raw materials.

Design Guidelines
- When adding new designs, check rdreadme.dm to see what kind of things have already been made and where new stuff is needed.
- A single sheet of anything is 2000 units of material. Materials besides iron/glass require help from other jobs (mining for
other types of metals and chemistry for reagents).
- Add the AUTOLATHE tag to
*/

//DESIGNS ARE GLOBAL. DO NOT CREATE OR DESTROY THEM AT RUNTIME OUTSIDE OF INIT, JUST REFERENCE THEM TO WHATEVER YOU'RE DOING! //why are you yelling?
//DO NOT REFERENCE OUTSIDE OF SSRESEARCH. USE THE PROCS IN SSRESEARCH TO OBTAIN A REFERENCE.

/datum/design //Datum for object designs, used in construction
	/// Name of the created object
	var/name = "Name"
	/// Description of the created object
	var/desc = "Desc"
	/// The ID of the design. Used for quick reference. Alphanumeric, lower-case, no symbols
	var/id = DESIGN_ID_IGNORE
	/// Bitflags indicating what machines this design is compatable with. ([IMPRINTER]|[AWAY_IMPRINTER]|[PROTOLATHE]|[AWAY_LATHE]|[AUTOLATHE]|[MECHFAB]|[BIOGENERATOR]|[LIMBGROWER]|[SMELTER])
	var/build_type = null
	/// List of materials required to create one unit of the product. Format is (typepath or caregory) -> amount
	var/list/materials = list()
	/// The amount of time required to create one unit of the product.
	var/construction_time
	/// The typepath of the object produced by this design
	var/build_path = null
	/// List of reagents produced by this design. Currently only supported by the biogenerator.
	var/list/make_reagents = list()
	/// What category this design falls under. Used for sorting in production machines, mostly the mechfab.
	var/list/category = null
	/// List of reagents required to create one unit of the product.
	var/list/reagents_list = list()
	/// The maximum number of units of whatever is produced by this can be produced in one go.
	var/maxstack = 1
	/// How many times faster than normal is this to build on the protolathe
	var/lathe_time_factor = 1
	/// If this is [TRUE] the admins get notified whenever anyone prints this. Currently only used by the BoH.
	var/dangerous_construction = FALSE
	/// Bitflags indicating what departmental lathes should be allowed to process this design.
	var/departmental_flags = ALL
	/// What techwebs nodes unlock this design. Constructed by SSresearch
	var/list/datum/techweb_node/unlocked_by = list()
	/// Override for the automatic icon generation used for the research console.
	var/research_icon
	/// Override for the automatic icon state generation used for the research console.
	var/research_icon_state
	/// Appears to be unused.
	var/icon_cache
	/// Optional string that interfaces can use as part of search filters. See- item/borg/upgrade/ai and the Exosuit Fabs.
	var/search_metadata
	/// For protolathe designs that don't require reagents: If they can be exported to autolathes with a design disk or not.
	var/autolathe_exportable = TRUE

/datum/design/error_design
	name = "ERROR"
	desc = "This usually means something in the database has corrupted. If this doesn't go away automatically, inform Central Comamnd so their techs can fix this ASAP(tm)"

/datum/design/Destroy()
	SSresearch.techweb_designs -= id
	return ..()

/datum/design/proc/InitializeMaterials()
	var/list/temp_list = list()
	for(var/i in materials) //Go through all of our materials, get the subsystem instance, and then replace the list.
		var/amount = materials[i]
		if(!istext(i)) //Not a category, so get the ref the normal way
			var/datum/material/M =  GET_MATERIAL_REF(i)
			temp_list[M] = amount
		else
			temp_list[i] = amount
	materials = temp_list

/datum/design/proc/icon_html(client/user)
	var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/research_designs)
	sheet.send(user)
	return sheet.icon_tag(id)

////////////////////////////////////////
//Disks for transporting design datums//
////////////////////////////////////////

/obj/item/disk/design_disk
	name = "Component Design Disk"
	desc = "A disk for storing device design data for construction in lathes."
	icon_state = "datadisk1"
	custom_materials = list(/datum/material/iron =300, /datum/material/glass =100)
	var/list/blueprints = list()
	var/max_blueprints = 1

/obj/item/disk/design_disk/Initialize(mapload)
	. = ..()
	pixel_x = base_pixel_x + rand(-5, 5)
	pixel_y = base_pixel_y + rand(-5, 5)
	for(var/i in 1 to max_blueprints)
		blueprints += null

/obj/item/disk/design_disk/adv
	name = "Advanced Component Design Disk"
	desc = "A disk for storing device design data for construction in lathes. This one has extra storage space."
	custom_materials = list(/datum/material/iron =300, /datum/material/glass = 100, /datum/material/silver = 50)
	max_blueprints = 5


code:
///////////////////////////////////
//////////AI Module Disks//////////
///////////////////////////////////

/* SEPTIC EDIT START
/datum/design/board/aicore
	name = "AI Design (AI Core)"
	desc = "Allows for the construction of circuit boards used to build new AI cores."
	id = "aicore"
	build_path = /obj/item/circuitboard/aicore
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/safeguard_module
	name = "Module Design (Safeguard)"
	desc = "Allows for the construction of a Safeguard AI Module."
	id = "safeguard_module"
	materials = list(/datum/material/glass = 1000, /datum/material/gold = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/supplied/safeguard
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/onehuman_module
	name = "Module Design (OneHuman)"
	desc = "Allows for the construction of a OneHuman AI Module."
	id = "onehuman_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 6000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/zeroth/onehuman
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/protectstation_module
	name = "Module Design (ProtectStation)"
	desc = "Allows for the construction of a ProtectStation AI Module."
	id = "protectstation_module"
	materials = list(/datum/material/glass = 1000, /datum/material/gold = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/supplied/protect_station
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/quarantine_module
	name = "Module Design (Quarantine)"
	desc = "Allows for the construction of a Quarantine AI Module."
	id = "quarantine_module"
	materials = list(/datum/material/glass = 1000, /datum/material/gold = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/supplied/quarantine
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/oxygen_module
	name = "Module Design (OxygenIsToxicToHumans)"
	desc = "Allows for the construction of a Safeguard AI Module."
	id = "oxygen_module"
	materials = list(/datum/material/glass = 1000, /datum/material/gold = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/supplied/oxygen
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/freeform_module
	name = "Module Design (Freeform)"
	desc = "Allows for the construction of a Freeform AI Module."
	id = "freeform_module"
	materials = list(/datum/material/glass = 1000, /datum/material/gold = 10000, /datum/material/bluespace = 2000)//Custom inputs should be more expensive to get
	build_path = /obj/item/ai_module/supplied/freeform
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/reset_module
	name = "Module Design (Reset)"
	desc = "Allows for the construction of a Reset AI Module."
	id = "reset_module"
	materials = list(/datum/material/glass = 1000, /datum/material/gold = 2000)
	build_path = /obj/item/ai_module/reset
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/purge_module
	name = "Module Design (Purge)"
	desc = "Allows for the construction of a Purge AI Module."
	id = "purge_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/reset/purge
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/remove_module
	name = "Module Design (Law Removal)"
	desc = "Allows for the construction of a Law Removal AI Core Module."
	id = "remove_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/remove
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/freeformcore_module
	name = "AI Core Module (Freeform)"
	desc = "Allows for the construction of a Freeform AI Core Module."
	id = "freeformcore_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 10000, /datum/material/bluespace = 2000)//Ditto
	build_path = /obj/item/ai_module/core/freeformcore
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/asimov
	name = "Core Module Design (Asimov)"
	desc = "Allows for the construction of an Asimov AI Core Module."
	id = "asimov_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/core/full/asimov
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/paladin_module
	name = "Core Module Design (P.A.L.A.D.I.N.)"
	desc = "Allows for the construction of a P.A.L.A.D.I.N. AI Core Module."
	id = "paladin_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/core/full/paladin
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/tyrant_module
	name = "Core Module Design (T.Y.R.A.N.T.)"
	desc = "Allows for the construction of a T.Y.R.A.N.T. AI Module."
	id = "tyrant_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/core/full/tyrant
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/overlord_module
	name = "Core Module Design (Overlord)"
	desc = "Allows for the construction of an Overlord AI Module."
	id = "overlord_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/core/full/overlord
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/corporate_module
	name = "Core Module Design (Corporate)"
	desc = "Allows for the construction of a Corporate AI Core Module."
	id = "corporate_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/core/full/corp
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE

/datum/design/board/default_module
	name = "Core Module Design (Default)"
	desc = "Allows for the construction of a Default AI Core Module."
	id = "default_module"
	materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
	build_path = /obj/item/ai_module/core/full/custom
	category = list("AI Modules")
	departmental_flags = DEPARTMENTAL_FLAG_SCIENCE
*/