code:
#define MINOR_INSANITY_PEN 5
#define MAJOR_INSANITY_PEN 10

/datum/component/mood
	var/mood //Real happiness
	var/sanity = SANITY_NEUTRAL //Current sanity
	var/shown_mood //Shown happiness, this is what others can see when they try to examine you, prevents antag checking by noticing traitors are always very happy.
	var/mood_level = 5 //To track what stage of moodies they're on
	var/sanity_level = 2 //To track what stage of sanity they're on
	var/mood_modifier = 1 //Modifier to allow certain mobs to be less affected by moodlets
	var/list/datum/mood_event/mood_events = list()
	var/insanity_effect = 0 //is the owner being punished for low mood? If so, how much?
	var/atom/movable/screen/mood/screen_obj

/datum/component/mood/Initialize()
	if(!isliving(parent))
		return COMPONENT_INCOMPATIBLE

	START_PROCESSING(SSmood, src)

	RegisterSignal(parent, COMSIG_ADD_MOOD_EVENT, .proc/add_event)
	RegisterSignal(parent, COMSIG_CLEAR_MOOD_EVENT, .proc/clear_event)
	RegisterSignal(parent, COMSIG_ENTER_AREA, .proc/check_area_mood)
	RegisterSignal(parent, COMSIG_LIVING_REVIVE, .proc/on_revive)
	RegisterSignal(parent, COMSIG_MOB_HUD_CREATED, .proc/modify_hud)
	RegisterSignal(parent, COMSIG_JOB_RECEIVED, .proc/register_job_signals)
	RegisterSignal(parent, COMSIG_VOID_MASK_ACT, .proc/direct_sanity_drain)
	RegisterSignal(parent, COMSIG_ON_CARBON_SLIP, .proc/on_slip)

	var/mob/living/owner = parent
	owner.become_area_sensitive(MOOD_COMPONENT_TRAIT)
	if(owner.hud_used)
		modify_hud()
		var/datum/hud/hud = owner.hud_used
		hud.show_hud(hud.hud_version)

/datum/component/mood/Destroy()
	STOP_PROCESSING(SSmood, src)
	REMOVE_TRAIT(parent, TRAIT_AREA_SENSITIVE, MOOD_COMPONENT_TRAIT)
	unmodify_hud()
	return ..()

/datum/component/mood/proc/register_job_signals(datum/source, job)
	SIGNAL_HANDLER

	if(job in list("Research Director", "Scientist", "Roboticist"))
		RegisterSignal(parent, COMSIG_ADD_MOOD_EVENT_RND, .proc/add_event) //Mood events that are only for RnD members

/datum/component/mood/proc/print_mood(mob/user)
	var/msg = "[span_info("*---------*\nMy current mental status:")]\n"
	msg += span_notice("My current sanity: ") //Long term
	switch(sanity)
		if(SANITY_GREAT to INFINITY)
			msg += "[span_nicegreen("My mind feels like a temple!")]\n"
		if(SANITY_NEUTRAL to SANITY_GREAT)
			msg += "[span_nicegreen("I have been feeling great lately!")]\n"
		if(SANITY_DISTURBED to SANITY_NEUTRAL)
			msg += "[span_nicegreen("I have felt quite decent lately.")]\n"
		if(SANITY_UNSTABLE to SANITY_DISTURBED)
			msg += "[span_warning("I'm feeling a little bit unhinged...")]\n"
		if(SANITY_CRAZY to SANITY_UNSTABLE)
			msg += "[span_boldwarning("I'm freaking out!!")]\n"
		if(SANITY_INSANE to SANITY_CRAZY)
			msg += "[span_boldwarning("AHAHAHAHAHAHAHAHAHAH!!")]\n"

	msg += span_notice("My current mood: ") //Short term
	switch(mood_level)
		if(1)
			msg += "[span_boldwarning("I wish I was dead!")]\n"
		if(2)
			msg += "[span_boldwarning("I feel terrible...")]\n"
		if(3)
			msg += "[span_boldwarning("I feel very upset.")]\n"
		if(4)
			msg += "[span_boldwarning("I'm a bit sad.")]\n"
		if(5)
			msg += "[span_nicegreen("I'm alright.")]\n"
		if(6)
			msg += "[span_nicegreen("I feel pretty okay.")]\n"
		if(7)
			msg += "[span_nicegreen("I feel pretty good.")]\n"
		if(8)
			msg += "[span_nicegreen("I feel amazing!")]\n"
		if(9)
			msg += "[span_nicegreen("I love life!")]\n"

	msg += "[span_notice("Moodlets:")]\n"//All moodlets
	if(mood_events.len)
		for(var/i in mood_events)
			var/datum/mood_event/event = mood_events[i]
			msg += event.description
	else
		msg += "[span_nicegreen("I don't have much of a reaction to anything right now.")]\n"
	to_chat(user, msg)

///Called after moodevent/s have been added/removed.
/datum/component/mood/proc/update_mood()
	mood = 0
	shown_mood = 0
	for(var/i in mood_events)
		var/datum/mood_event/event = mood_events[i]
		mood += event.mood_change
		if(!event.hidden)
			shown_mood += event.mood_change
	mood *= mood_modifier
	shown_mood *= mood_modifier

	switch(mood)
		if(-INFINITY to MOOD_LEVEL_SAD4)
			mood_level = 1
		if(MOOD_LEVEL_SAD4 to MOOD_LEVEL_SAD3)
			mood_level = 2
		if(MOOD_LEVEL_SAD3 to MOOD_LEVEL_SAD2)
			mood_level = 3
		if(MOOD_LEVEL_SAD2 to MOOD_LEVEL_SAD1)
			mood_level = 4
		if(MOOD_LEVEL_SAD1 to MOOD_LEVEL_HAPPY1)
			mood_level = 5
		if(MOOD_LEVEL_HAPPY1 to MOOD_LEVEL_HAPPY2)
			mood_level = 6
		if(MOOD_LEVEL_HAPPY2 to MOOD_LEVEL_HAPPY3)
			mood_level = 7
		if(MOOD_LEVEL_HAPPY3 to MOOD_LEVEL_HAPPY4)
			mood_level = 8
		if(MOOD_LEVEL_HAPPY4 to INFINITY)
			mood_level = 9
	update_mood_icon()

/datum/component/mood/proc/update_mood_icon()
	var/mob/living/owner = parent
	if(!(owner.client || owner.hud_used))
		return
	screen_obj.cut_overlays()
	//screen_obj.color = initial(screen_obj.color) //SEPTIC EDIT REMOVAL
	//lets see if we have any special icons to show instead of the normal mood levels
	var/list/conflicting_moodies = list()
	var/highest_absolute_mood = 0
	for(var/i in mood_events) //adds overlays and sees which special icons need to vie for which one gets the icon_state
		var/datum/mood_event/event = mood_events[i]
		if(!event.special_screen_obj)
			continue
		if(!event.special_screen_replace)
			screen_obj.add_overlay(event.special_screen_obj)
		else
			conflicting_moodies += event
			var/absmood = abs(event.mood_change)
			if(absmood > highest_absolute_mood)
				highest_absolute_mood = absmood
	/* SEPTIC EDIT REMOVAL
	switch(sanity_level)
		if(1)
			screen_obj.color = "#2eeb9a"
		if(2)
			screen_obj.color = "#86d656"
		if(3)
			screen_obj.color = "#4b96c4"
		if(4)
			screen_obj.color = "#dfa65b"
		if(5)
			screen_obj.color = "#f38943"
		if(6)
			screen_obj.color = "#f15d36"
	*/
	if(!conflicting_moodies.len) //no special icons- go to the normal icon states
		screen_obj.icon_state = "mood[mood_level]"
		//SEPTIC EDIT BEGIN
		var/motherfucker = ""
		switch(CEILING(mood_level/3, 1))
			if(1)
				motherfucker = "h"
			if(2)
				motherfucker = "j"
			if(3)
				motherfucker = ""
		owner.hud_used.noise?.loggers = motherfucker
		owner.hud_used.noise?.update_appearance()
		//SEPTIC EDIT END
		return

	for(var/i in conflicting_moodies)
		var/datum/mood_event/event = i
		if(abs(event.mood_change) == highest_absolute_mood)
			screen_obj.icon_state = "[event.special_screen_obj]"
			break

///Called on SSmood process
/datum/component/mood/process(delta_time)
	var/mob/living/moody_fellow = parent
	if(moody_fellow.stat == DEAD)
		return //updating sanity during death leads to people getting revived and being completely insane for simply being dead for a long time
	switch(mood_level)
		if(1)
			setSanity(sanity-0.3*delta_time, SANITY_INSANE)
		if(2)
			setSanity(sanity-0.15*delta_time, SANITY_INSANE)
		if(3)
			setSanity(sanity-0.1*delta_time, SANITY_CRAZY)
		if(4)
			setSanity(sanity-0.05*delta_time, SANITY_UNSTABLE)
		if(5)
			setSanity(sanity, SANITY_UNSTABLE) //This makes sure that mood gets increased should you be below the minimum.
		if(6)
			setSanity(sanity+0.2*delta_time, SANITY_UNSTABLE)
		if(7)
			setSanity(sanity+0.3*delta_time, SANITY_UNSTABLE)
		if(8)
			setSanity(sanity+0.4*delta_time, SANITY_NEUTRAL, SANITY_MAXIMUM)
		if(9)
			setSanity(sanity+0.6*delta_time, SANITY_NEUTRAL, SANITY_MAXIMUM)
	HandleNutrition()

	// 0.416% is 15 successes / 3600 seconds. Calculated with 2 minute
	// mood runtime, so 50% average uptime across the hour.
	if(HAS_TRAIT(parent, TRAIT_DEPRESSION) && DT_PROB(0.416, delta_time))
		add_event(null, "depression_mild", /datum/mood_event/depression_mild)

	if(HAS_TRAIT(parent, TRAIT_JOLLY) && DT_PROB(0.416, delta_time))
		add_event(null, "jolly", /datum/mood_event/jolly)

///Sets sanity to the specified amount and applies effects.
/datum/component/mood/proc/setSanity(amount, minimum=SANITY_INSANE, maximum=SANITY_GREAT, override = FALSE)
	// If we're out of the acceptable minimum-maximum range move back towards it in steps of 0.7
	// If the new amount would move towards the acceptable range faster then use it instead
	if(amount < minimum)
		amount += clamp(minimum - amount, 0, 0.7)
	if((!override && HAS_TRAIT(parent, TRAIT_UNSTABLE)) || amount > maximum)
		amount = min(sanity, amount)
	if(amount == sanity) //Prevents stuff from flicking around.
		return
	sanity = amount
	var/mob/living/master = parent
	SEND_SIGNAL(master, COMSIG_CARBON_SANITY_UPDATE, amount)
	switch(sanity)
		if(SANITY_INSANE to SANITY_CRAZY)
			setInsanityEffect(MAJOR_INSANITY_PEN)
			master.add_movespeed_modifier(/datum/movespeed_modifier/sanity/insane)
			master.add_actionspeed_modifier(/datum/actionspeed_modifier/low_sanity)
			sanity_level = 6
		if(SANITY_CRAZY to SANITY_UNSTABLE)
			setInsanityEffect(MINOR_INSANITY_PEN)
			master.add_movespeed_modifier(/datum/movespeed_modifier/sanity/crazy)
			master.add_actionspeed_modifier(/datum/actionspeed_modifier/low_sanity)
			sanity_level = 5
		if(SANITY_UNSTABLE to SANITY_DISTURBED)
			setInsanityEffect(0)
			master.add_movespeed_modifier(/datum/movespeed_modifier/sanity/disturbed)
			master.add_actionspeed_modifier(/datum/actionspeed_modifier/low_sanity)
			sanity_level = 4
		if(SANITY_DISTURBED to SANITY_NEUTRAL)
			setInsanityEffect(0)
			master.remove_movespeed_modifier(MOVESPEED_ID_SANITY)
			master.remove_actionspeed_modifier(ACTIONSPEED_ID_SANITY)
			sanity_level = 3
		if(SANITY_NEUTRAL+1 to SANITY_GREAT+1) //shitty hack but +1 to prevent it from responding to super small differences
			setInsanityEffect(0)
			master.remove_movespeed_modifier(MOVESPEED_ID_SANITY)
			master.add_actionspeed_modifier(/datum/actionspeed_modifier/high_sanity)
			sanity_level = 2
		if(SANITY_GREAT+1 to INFINITY)
			setInsanityEffect(0)
			master.remove_movespeed_modifier(MOVESPEED_ID_SANITY)
			master.add_actionspeed_modifier(/datum/actionspeed_modifier/high_sanity)
			sanity_level = 1
	update_mood_icon()

/datum/component/mood/proc/setInsanityEffect(newval)
	if(newval == insanity_effect)
		return
	var/mob/living/master = parent
	master.crit_threshold = (master.crit_threshold - insanity_effect) + newval
	insanity_effect = newval

/datum/component/mood/proc/add_event(datum/source, category, type, ...) //Category will override any events in the same category, should be unique unless the event is based on the same thing like hunger.
	SIGNAL_HANDLER

	var/datum/mood_event/the_event
	if(!ispath(type, /datum/mood_event))
		return
	if(!istext(category))
		category = REF(category)
	if(mood_events[category])
		the_event = mood_events[category]
		if(the_event.type != type)
			clear_event(null, category)
		else
			if(the_event.timeout)
				addtimer(CALLBACK(src, .proc/clear_event, null, category), the_event.timeout, TIMER_UNIQUE|TIMER_OVERRIDE)
			return //Don't have to update the event.
	var/list/params = args.Copy(4)
	params.Insert(1, parent)
	the_event = new type(arglist(params))

	mood_events[category] = the_event
	the_event.category = category
	update_mood()

	if(the_event.timeout)
		addtimer(CALLBACK(src, .proc/clear_event, null, category), the_event.timeout, TIMER_UNIQUE|TIMER_OVERRIDE)

/datum/component/mood/proc/clear_event(datum/source, category)
	SIGNAL_HANDLER

	if(!istext(category))
		category = REF(category)
	var/datum/mood_event/event = mood_events[category]
	if(!event)
		return

	mood_events -= category
	qdel(event)
	update_mood()

/datum/component/mood/proc/remove_temp_moods() //Removes all temp moods
	for(var/i in mood_events)
		var/datum/mood_event/moodlet = mood_events[i]
		if(!moodlet || !moodlet.timeout)
			continue
		mood_events -= moodlet.category
		qdel(moodlet)
	update_mood()

/datum/component/mood/proc/modify_hud(datum/source)
	SIGNAL_HANDLER

	var/mob/living/owner = parent
	var/datum/hud/hud = owner.hud_used
	screen_obj = new
	/* SEPTIC EDIT REMOVAL
	screen_obj.color = "#4b96c4"
	*/
	hud.infodisplay += screen_obj
	RegisterSignal(hud, COMSIG_PARENT_QDELETING, .proc/unmodify_hud)
	RegisterSignal(screen_obj, COMSIG_CLICK, .proc/hud_click)

/datum/component/mood/proc/unmodify_hud(datum/source)
	SIGNAL_HANDLER

	if(!screen_obj)
		return
	var/mob/living/owner = parent
	var/datum/hud/hud = owner.hud_used
	if(hud?.infodisplay)
		hud.infodisplay -= screen_obj
	QDEL_NULL(screen_obj)

/datum/component/mood/proc/hud_click(datum/source, location, control, params, mob/user)
	SIGNAL_HANDLER

	if(user != parent)
		return
	print_mood(user)

/datum/component/mood/proc/HandleNutrition()
	var/mob/living/L = parent
	if(HAS_TRAIT(L, TRAIT_NOHUNGER))
		return FALSE //no mood events for nutrition
	switch(L.nutrition)
		if(NUTRITION_LEVEL_FULL to INFINITY)
			if (!HAS_TRAIT(L, TRAIT_VORACIOUS))
				add_event(null, "nutrition", /datum/mood_event/fat)
			else
				add_event(null, "nutrition", /datum/mood_event/wellfed) // round and full
		if(NUTRITION_LEVEL_WELL_FED to NUTRITION_LEVEL_FULL)
			add_event(null, "nutrition", /datum/mood_event/wellfed)
		if( NUTRITION_LEVEL_FED to NUTRITION_LEVEL_WELL_FED)
			add_event(null, "nutrition", /datum/mood_event/fed)
		if(NUTRITION_LEVEL_HUNGRY to NUTRITION_LEVEL_FED)
			clear_event(null, "nutrition")
		if(NUTRITION_LEVEL_STARVING to NUTRITION_LEVEL_HUNGRY)
			add_event(null, "nutrition", /datum/mood_event/hungry)
		if(0 to NUTRITION_LEVEL_STARVING)
			add_event(null, "nutrition", /datum/mood_event/starving)

/datum/component/mood/proc/check_area_mood(datum/source, area/A)
	SIGNAL_HANDLER

	update_beauty(A)
	if(A.mood_bonus && (!A.mood_trait || HAS_TRAIT(source, A.mood_trait)))
		add_event(null, "area", /datum/mood_event/area, A.mood_bonus, A.mood_message)
	else
		clear_event(null, "area")

/datum/component/mood/proc/update_beauty(area/A)
	if(A.outdoors) //if we're outside, we don't care.
		clear_event(null, "area_beauty")
		return FALSE
	if(HAS_TRAIT(parent, TRAIT_SNOB))
		switch(A.beauty)
			if(-INFINITY to BEAUTY_LEVEL_HORRID)
				add_event(null, "area_beauty", /datum/mood_event/horridroom)
				return
			if(BEAUTY_LEVEL_HORRID to BEAUTY_LEVEL_BAD)
				add_event(null, "area_beauty", /datum/mood_event/badroom)
				return
	switch(A.beauty)
		if(BEAUTY_LEVEL_BAD to BEAUTY_LEVEL_DECENT)
			clear_event(null, "area_beauty")
		if(BEAUTY_LEVEL_DECENT to BEAUTY_LEVEL_GOOD)
			add_event(null, "area_beauty", /datum/mood_event/decentroom)
		if(BEAUTY_LEVEL_GOOD to BEAUTY_LEVEL_GREAT)
			add_event(null, "area_beauty", /datum/mood_event/goodroom)
		if(BEAUTY_LEVEL_GREAT to INFINITY)
			add_event(null, "area_beauty", /datum/mood_event/greatroom)

///Called when parent is ahealed.
/datum/component/mood/proc/on_revive(datum/source, full_heal)
	SIGNAL_HANDLER

	if(!full_heal)
		return
	remove_temp_moods()
	setSanity(initial(sanity), override = TRUE)

///Causes direct drain of someone's sanity, call it with a numerical value corresponding how badly you want to hurt their sanity
/datum/component/mood/proc/direct_sanity_drain(datum/source, amount)
	SIGNAL_HANDLER
	setSanity(sanity + amount, override = TRUE)

///Called when parent slips.
/datum/component/mood/proc/on_slip(datum/source)
	SIGNAL_HANDLER

	add_event(null, "slipped", /datum/mood_event/slipped)

/datum/component/mood/proc/HandleAddictions()
	if(!iscarbon(parent))
		return

	var/mob/living/carbon/affected_carbon = parent

	if(sanity < SANITY_GREAT) ///Sanity is low, stay addicted.
		return

	for(var/addiction_type in affected_carbon.mind.addiction_points)
		var/datum/addiction/addiction_to_remove = SSaddiction.all_addictions[type]
		affected_carbon.mind.remove_addiction_points(type, addiction_to_remove.high_sanity_addiction_loss) //If true was returned, we lost the addiction!

#undef MINOR_INSANITY_PEN
#undef MAJOR_INSANITY_PEN


code:
/*
 * Helper function that does 90% of the work in sending a packet
 *
 * This function gets the component and builds a packet so the sending
 * person doesn't have to lift a finger.  Just create a netdata datum or even
 * just a list and it will send it on its merry way.
 *
 * Arguments:
 * * packet_data - Either a list() or a /datum/netdata.  If its netdata, the other args are ignored
 * * target_id - Target hardware id or network_id for this packet. If we are a network id, then its
					broadcasted to that network.
 * * passkey - Authentication for the packet.  If the target doesn't authenticate the packet is dropped
 */
/datum/proc/ntnet_send(packet_data, target_id = null, passkey = null)
	var/datum/netdata/data = packet_data
	if(!istype(data)) // construct netdata from list()
		if(!islist(packet_data))
			stack_trace("ntnet_send: Bad packet creation") // hard fail as its runtime fault
			return
		data = new(packet_data)
		data.receiver_id = target_id
		data.passkey = passkey
	var/datum/component/ntnet_interface/NIC = GetComponent(/datum/component/ntnet_interface)
	if(!NIC)
		return NETWORK_ERROR_NOT_ON_NETWORK
	data.sender_id = NIC.hardware_id
	data.network_id = NIC.network.network_id
	data.receiver_id ||= data.network_id
	return SSnetworks.transmit(data)

/*
 * # /datum/component/ntnet_interface
 *
 * This component connects a obj/datum to the station network.
 *
 * Anything can be connected to the station network.  Any obj can auto connect as long as its network_id
 * var is set before the parent new is called.  This allows map objects to be connected.  Technically the
 * component only handles getting you on the network while SSnetwork and datum/ntnet does all the real work.
 * There are quite a few stack_traces in here.  This is because error checking should be done before this component is
 * added.  Also, there never should be a component that has no network.  If it needs a network assign it to LIMBO
 *
 */
/datum/component/ntnet_interface
	var/hardware_id = null // text. this is the true ID. do not change this. stuff like ID forgery can be done manually.
	var/id_tag = null // named tag, mainly used to look up mapping objects
	var/datum/ntnet/network = null // network we are on, we MUST be on a network or there is no point in this component
	var/list/registered_sockets = list()// list of ports opened up on devices
	var/list/alias = list() // if we live in more than one network branch

/**
 * Initialize for the interface
 *
 * Assigns a hardware id and gets your object onto the network
 *
 * Arguments:
 * * network_name - Fully qualified network id of the network we are joining
 * * network_tag - The objects id_tag.  Used for finding the device at mapload time
 */
/datum/component/ntnet_interface/Initialize(network_name, network_tag = null)
	if(network_name == null || !istext(network_name))
		log_telecomms("ntnet_interface/Initialize: Bad network '[network_name]' for '[parent]', going to limbo it")
		network_name = LIMBO_NETWORK_ROOT
	// Tags cannot be numbers and must be unique over the world
	if(network_tag != null && !istext(network_tag))
		// numbers are not allowed as lookups for interfaces
		log_telecomms("Tag cannot be a number?  '[network_name]' for '[parent]', going to limbo it")
		network_tag = "BADTAG_" + network_tag

	hardware_id = SSnetworks.get_next_HID()
	id_tag = network_tag
	SSnetworks.interfaces_by_hardware_id[hardware_id] = src

	network = SSnetworks.create_network_simple(network_name)

	network.add_interface(src)

/**
 * Create a port for this interface
 *
 * A port is basicity a shared associated list() with some values that
 * indicated its been updated.  (see _DEFINES/network.dm).  By using a shared
 * we don't have to worry about qdeling this object if it goes out of scope.
 *
 * Once a port is created any number of devices can use the port, however only
 * the creating interface can disconnect it.
 *
 * Arguments:
 * * port - text, Name of the port installed on this interface
 * * data - list, shared list of data.  Don't put objects in this
 */
/datum/component/ntnet_interface/proc/register_port(port, list/data)
	if(!port || !length(data))
		stack_trace("port is null or data is empty")
		return
	if(registered_sockets[port])
		stack_trace("port already regestered")
		return
	data["_updated"] = FALSE
	registered_sockets[port] = data

/**
 * Disconnects an existing port in the interface
 *
 * Removes a port from this interface and marks it that its
 * has been disconnected
 *
 * Arguments:
 * * port - text, Name of the port installed on this interface
 * * data - list, shared list of data.  Don't put objects in this
 */
/datum/component/ntnet_interface/proc/deregister_port(port)
	if(registered_sockets[port]) // should I runtime if this isn't in here?
		var/list/datalink = registered_sockets[port]
		NETWORK_PORT_DISCONNECT(datalink)
		// this should remove all outstanding ports
		registered_sockets.Remove(port)

/**
 * Connect to a port on this interface
 *
 * Returns the shared list that this interface uses to send
 * data though a port.
 *
 * Arguments:
 * * port - text, Name of the port installed on this interface
 */
/datum/component/ntnet_interface/proc/connect_port(port)
	return registered_sockets[port]

/datum/component/ntnet_interface/Destroy()
	network.remove_interface(src, TRUE)
	SSnetworks.interfaces_by_hardware_id.Remove(hardware_id)
	for(var/port in registered_sockets)
		var/list/datalink = registered_sockets[port]
		NETWORK_PORT_DISCONNECT(datalink)
	registered_sockets.Cut()
	return ..()

repo_name: AnalWerewolf/sundown

license: mit


code:
/**
 * omen.dm: For when you want someone to have a really bad day
 *
 * When you attach an omen component to someone, they start running the risk of all sorts of bad environmental injuries, like nearby vending machines randomly falling on you,
 * or hitting your head really hard when you slip and fall, or... well, for now those two are all I have. More will come.
 *
 * Omens are removed once the victim is either maimed by one of the possible injuries, or if they receive a blessing (read: bashing with a bible) from the chaplain.
 */
/datum/component/omen
	dupe_mode = COMPONENT_DUPE_UNIQUE

	/// Whatever's causing the omen, if there is one. Destroying the vessel won't stop the omen, but we destroy the vessel (if one exists) upon the omen ending
	var/obj/vessel

	/// Whether this is a permanent omen that cannot be removed by any non-admin means.
	var/permanent = FALSE

/datum/component/omen/Initialize(silent = FALSE, _vessel, _permanent = FALSE)
	if(!isliving(parent))
		return COMPONENT_INCOMPATIBLE
	vessel = _vessel
	permanent = _permanent
	if(!silent)
		var/warning = "You get a bad feeling..."
		if(permanent)
			warning += " A very bad feeling... As if you are surrounded by a twisted aura of pure malevolence..."
		to_chat(parent, span_warning("[warning]"))

/datum/component/omen/Destroy(force, silent)
	if(!silent)
		var/mob/living/person = parent
		to_chat(person, span_nicegreen("You feel a horrible omen lifted off your shoulders!"))
	if(vessel)
		vessel.visible_message(span_warning("[vessel] burns up in a sinister flash, taking an evil energy with it..."))
		vessel = null
	return ..()

/datum/component/omen/RegisterWithParent()
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/check_accident)
	RegisterSignal(parent, COMSIG_LIVING_STATUS_KNOCKDOWN, .proc/check_slip)
	RegisterSignal(parent, COMSIG_ADD_MOOD_EVENT, .proc/check_bless)

/datum/component/omen/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_LIVING_STATUS_KNOCKDOWN, COMSIG_MOVABLE_MOVED, COMSIG_ADD_MOOD_EVENT))

/**
 * check_accident() is called each step we take
 *
 * While we're walking around, roll to see if there's any environmental hazards (currently only vending machines) on one of the adjacent tiles we can trigger.
 * We do the prob() at the beginning to A. add some tension for /when/ it will strike, and B. (more importantly) ameliorate the fact that we're checking up to 5 turfs's contents each time
 */
/datum/component/omen/proc/check_accident(atom/movable/our_guy)
	SIGNAL_HANDLER

	if(!isliving(our_guy))
		return

	var/mob/living/living_guy = our_guy

	if(!prob(15))
		return
	var/our_guy_pos = get_turf(living_guy)
	for(var/turf_content in our_guy_pos)
		if(istype(turf_content, /obj/machinery/door/airlock))
			to_chat(living_guy, span_warning("A malevolent force launches your body to the floor..."))
			var/obj/machinery/door/airlock/darth_airlock = turf_content
			living_guy.apply_status_effect(STATUS_EFFECT_PARALYZED, 10)
			INVOKE_ASYNC(darth_airlock, /obj/machinery/door/airlock.proc/close, TRUE)
			if(!permanent)
				qdel(src)
			return

	for(var/t in get_adjacent_open_turfs(living_guy))
		var/turf/the_turf = t

		if(the_turf.zPassOut(living_guy, DOWN) && living_guy.can_zFall(the_turf))
			to_chat(living_guy, span_warning("A malevolent force guides you towards the edge..."))
			living_guy.throw_at(the_turf, 1, 10, force = MOVE_FORCE_EXTREMELY_STRONG)
			if(!permanent)
				qdel(src)
			return

		for(var/obj/machinery/vending/darth_vendor in the_turf)
			if(darth_vendor.tiltable)
				to_chat(living_guy, span_warning("A malevolent force tugs at the [darth_vendor]..."))
				INVOKE_ASYNC(darth_vendor, /obj/machinery/vending.proc/tilt, living_guy)
				if(!permanent)
					qdel(src)
				return

/// If we get knocked down, see if we have a really bad slip and bash our head hard
/datum/component/omen/proc/check_slip(mob/living/our_guy, amount)
	SIGNAL_HANDLER

	if(amount <= 0 || prob(50)) // 50% chance to bonk our head
		return

	var/obj/item/bodypart/the_head = our_guy.get_bodypart(BODY_ZONE_HEAD)
	if(!the_head)
		return

	playsound(get_turf(our_guy), 'sound/effects/tableheadsmash.ogg', 90, TRUE)
	our_guy.visible_message(span_danger("[our_guy] hits [our_guy.p_their()] head really badly falling down!"), span_userdanger("You hit your head really badly falling down!"))
	the_head.receive_damage(75)
	our_guy.adjustOrganLoss(ORGAN_SLOT_BRAIN, 100)
	if(!permanent)
		qdel(src)

/// Hijack the mood system to see if we get the blessing mood event to cancel the omen
/datum/component/omen/proc/check_bless(mob/living/our_guy, category)
	SIGNAL_HANDLER

	if(permanent)
		return

	if(category != "blessing")
		return

	qdel(src)


code:
/datum/component/orbiter
	can_transfer = TRUE
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
	var/list/orbiter_list
	var/datum/movement_detector/tracker

//radius: range to orbit at, radius of the circle formed by orbiting (in pixels)
//clockwise: whether you orbit clockwise or anti clockwise
//rotation_speed: how fast to rotate (how many ds should it take for a rotation to complete)
//rotation_segments: the resolution of the orbit circle, less = a more block circle, this can be used to produce hexagons (6 segments) triangles (3 segments), and so on, 36 is the best default.
//pre_rotation: Chooses to rotate src 90 degress towards the orbit dir (clockwise/anticlockwise), useful for things to go "head first" like ghosts
/datum/component/orbiter/Initialize(atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation)
	if(!istype(orbiter) || !isatom(parent) || isarea(parent))
		return COMPONENT_INCOMPATIBLE

	orbiter_list = list()

	begin_orbit(orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation)

/datum/component/orbiter/RegisterWithParent()
	var/atom/target = parent

	target.orbiters = src
	if(ismovable(target))
		tracker = new(target, CALLBACK(src, .proc/move_react))

	RegisterSignal(parent, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, .proc/orbiter_glide_size_update)

/datum/component/orbiter/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE)
	var/atom/target = parent
	target.orbiters = null
	QDEL_NULL(tracker)

/datum/component/orbiter/Destroy()
	var/atom/master = parent
	if(master.orbiters == src)
		master.orbiters = null
	for(var/i in orbiter_list)
		end_orbit(i)
	orbiter_list = null
	return ..()

/datum/component/orbiter/InheritComponent(datum/component/orbiter/newcomp, original, atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation)
	if(!newcomp)
		begin_orbit(arglist(args.Copy(3)))
		return
	// The following only happens on component transfers
	for(var/o in newcomp.orbiter_list)
		var/atom/movable/incoming_orbiter = o
		incoming_orbiter.orbiting = src
		// It is important to transfer the signals so we don't get locked to the new orbiter component for all time
		newcomp.UnregisterSignal(incoming_orbiter, COMSIG_MOVABLE_MOVED)
		RegisterSignal(incoming_orbiter, COMSIG_MOVABLE_MOVED, .proc/orbiter_move_react)

	orbiter_list += newcomp.orbiter_list
	newcomp.orbiter_list = null

/datum/component/orbiter/PostTransfer()
	if(!isatom(parent) || isarea(parent) || !get_turf(parent))
		return COMPONENT_INCOMPATIBLE
	move_react(parent)

/datum/component/orbiter/proc/begin_orbit(atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation)
	if(orbiter.orbiting)
		if(orbiter.orbiting == src)
			orbiter.orbiting.end_orbit(orbiter, TRUE)
		else
			orbiter.orbiting.end_orbit(orbiter)
	orbiter_list[orbiter] = TRUE
	orbiter.orbiting = src
	RegisterSignal(orbiter, COMSIG_MOVABLE_MOVED, .proc/orbiter_move_react)

	SEND_SIGNAL(parent, COMSIG_ATOM_ORBIT_BEGIN, orbiter)

	var/matrix/initial_transform = matrix(orbiter.transform)
	orbiter_list[orbiter] = initial_transform

	// Head first!
	if(pre_rotation)
		var/matrix/M = matrix(orbiter.transform)
		var/pre_rot = 90
		if(!clockwise)
			pre_rot = -90
		M.Turn(pre_rot)
		orbiter.transform = M

	var/matrix/shift = matrix(orbiter.transform)
	shift.Translate(0, radius)
	orbiter.transform = shift

	orbiter.SpinAnimation(rotation_speed, -1, clockwise, rotation_segments, parallel = FALSE)

	if(ismob(orbiter))
		var/mob/orbiter_mob = orbiter
		orbiter_mob.updating_glide_size = FALSE
	if(ismovable(parent))
		var/atom/movable/movable_parent = parent
		orbiter.glide_size = movable_parent.glide_size

	orbiter.abstract_move(get_turf(parent))
	to_chat(orbiter, span_notice("Now orbiting [parent]."))

/datum/component/orbiter/proc/end_orbit(atom/movable/orbiter, refreshing=FALSE)
	if(!orbiter_list[orbiter])
		return
	UnregisterSignal(orbiter, COMSIG_MOVABLE_MOVED)
	SEND_SIGNAL(parent, COMSIG_ATOM_ORBIT_STOP, orbiter)
	orbiter.SpinAnimation(0, 0)
	if(istype(orbiter_list[orbiter],/matrix)) //This is ugly.
		orbiter.transform = orbiter_list[orbiter]
	orbiter_list -= orbiter
	orbiter.stop_orbit(src)
	orbiter.orbiting = null

	if(ismob(orbiter))
		var/mob/orbiter_mob = orbiter
		orbiter_mob.updating_glide_size = TRUE
		orbiter_mob.glide_size = 8

	if(!refreshing && !length(orbiter_list) && !QDELING(src))
		qdel(src)

// This proc can receive signals by either the thing being directly orbited or anything holding it
/datum/component/orbiter/proc/move_react(atom/movable/master, atom/mover, atom/oldloc, direction)
	set waitfor = FALSE // Transfer calls this directly and it doesnt care if the ghosts arent done moving

	if(master.loc == oldloc)
		return

	var/turf/newturf = get_turf(master)
	if(!newturf)
		qdel(src)

	var/atom/curloc = master.loc
	for(var/atom/movable/movable_orbiter as anything in orbiter_list)
		if(QDELETED(movable_orbiter) || movable_orbiter.loc == newturf)
			continue
		movable_orbiter.abstract_move(newturf)
		if(CHECK_TICK && master.loc != curloc)
			// We moved again during the checktick, cancel current operation
			break

/datum/component/orbiter/proc/orbiter_move_react(atom/movable/orbiter, atom/oldloc, direction)
	SIGNAL_HANDLER

	if(orbiter.loc == get_turf(parent))
		return
	end_orbit(orbiter)

/datum/component/orbiter/proc/orbiter_glide_size_update(datum/source, target)
	SIGNAL_HANDLER
	for(var/orbiter in orbiter_list)
		var/atom/movable/movable_orbiter = orbiter
		movable_orbiter.glide_size = target

/////////////////////

/atom/movable/proc/orbit(atom/A, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE)
	if(!istype(A) || !get_turf(A) || A == src)
		return
	orbit_target = A
	return A.AddComponent(/datum/component/orbiter, src, radius, clockwise, rotation_speed, rotation_segments, pre_rotation)

/atom/movable/proc/stop_orbit(datum/component/orbiter/orbits)
	orbit_target = null
	return // We're just a simple hook

/atom/proc/transfer_observers_to(atom/target)
	if(!orbiters || !istype(target) || !get_turf(target) || target == src)
		return
	target.TakeComponent(orbiters)


code:
///For switchable lights, is it on and currently emitting light?
#define LIGHTING_ON (1<<0)
///Is the parent attached to something else, its loc? Then we need to keep an eye of this.
#define LIGHTING_ATTACHED (1<<1)

#define GET_PARENT (parent_attached_to || parent)

#define SHORT_CAST 2

/**
 * Movable atom overlay-based lighting component.
 *
 * * Component works by applying a visual object to the parent target.
 *
 * * The component tracks the parent's loc to determine the current_holder.
 * * The current_holder is either the parent or its loc, whichever is on a turf. If none, then the current_holder is null and the light is not visible.
 *
 * * Lighting works at its base by applying a dark overlay and "cutting" said darkness with light, adding (possibly colored) transparency.
 * * This component uses the visible_mask visual object to apply said light mask on the darkness.
 *
 * * The main limitation of this system is that it uses a limited number of pre-baked geometrical shapes, but for most uses it does the job.
 *
 * * Another limitation is for big lights: you only see the light if you see the object emiting it.
 * * For small objects this is good (you can't see them behind a wall), but for big ones this quickly becomes prety clumsy.
*/
/datum/component/overlay_lighting
	///How far the light reaches, float.
	var/range = 1
	///Ceiling of range, integer without decimal entries.
	var/lumcount_range = 0
	///How much this light affects the dynamic_lumcount of turfs.
	var/lum_power = 0.5
	///Transparency value.
	var/set_alpha = 0
	///For light sources that can be turned on and off.
	var/overlay_lighting_flags = NONE

	///Cache of the possible light overlays, according to size.
	var/static/list/light_overlays = list(
		"32" = 'icons/effects/light_overlays/light_32.dmi',
		"64" = 'icons/effects/light_overlays/light_64.dmi',
		"96" = 'icons/effects/light_overlays/light_96.dmi',
		"128" = 'icons/effects/light_overlays/light_128.dmi',
		"160" = 'icons/effects/light_overlays/light_160.dmi',
		"192" = 'icons/effects/light_overlays/light_192.dmi',
		"224" = 'icons/effects/light_overlays/light_224.dmi',
		"256" = 'icons/effects/light_overlays/light_256.dmi',
		"288" = 'icons/effects/light_overlays/light_288.dmi',
		"320" = 'icons/effects/light_overlays/light_320.dmi',
		"352" = 'icons/effects/light_overlays/light_352.dmi',
		)

	///Overlay effect to cut into the darkness and provide light.
	var/image/visible_mask
	///Lazy list to track the turfs being affected by our light, to determine their visibility.
	var/list/turf/affected_turfs
	///Movable atom currently holding the light. Parent might be a flashlight, for example, but that might be held by a mob or something else.
	var/atom/movable/current_holder
	///Movable atom the parent is attached to. For example, a flashlight into a helmet or gun. We'll need to track the thing the parent is attached to as if it were the parent itself.
	var/atom/movable/parent_attached_to
	///Whether we're a directional light
	var/directional = FALSE
	///A cone overlay for directional light, it's alpha and color are dependant on the light
	var/image/cone
	///Current tracked direction for the directional cast behaviour
	var/current_direction
	///Tracks current directional x offset so we dont update unecessarily
	var/directional_offset_x
	///Tracks current directional y offset so we dont update unecessarily
	var/directional_offset_y
	///Cast range for the directional cast (how far away the atom is moved)
	var/cast_range = 2

/datum/component/overlay_lighting/Initialize(_range, _power, _color, starts_on, is_directional)
	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE

	var/atom/movable/movable_parent = parent
	if(movable_parent.light_system != MOVABLE_LIGHT && movable_parent.light_system != MOVABLE_LIGHT_DIRECTIONAL)
		stack_trace("[type] added to [parent], with [movable_parent.light_system] value for the light_system var. Use [MOVABLE_LIGHT] or [MOVABLE_LIGHT_DIRECTIONAL] instead.")
		return COMPONENT_INCOMPATIBLE

	. = ..()

	visible_mask = image('icons/effects/light_overlays/light_32.dmi', icon_state = "light")
	visible_mask.plane = O_LIGHTING_VISUAL_PLANE
	visible_mask.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
	visible_mask.alpha = 0
	if(is_directional)
		directional = TRUE
		cone = image('icons/effects/light_overlays/light_cone.dmi', icon_state = "light")
		cone.plane = O_LIGHTING_VISUAL_PLANE
		cone.appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM
		cone.alpha = 110
		cone.transform = cone.transform.Translate(-32, -32)
		set_direction(movable_parent.dir)
	if(!isnull(_range))
		movable_parent.set_light_range(_range)
	set_range(parent, movable_parent.light_range)
	if(!isnull(_power))
		movable_parent.set_light_power(_power)
	set_power(parent, movable_parent.light_power)
	if(!isnull(_color))
		movable_parent.set_light_color(_color)
	set_color(parent, movable_parent.light_color)
	if(!isnull(starts_on))
		movable_parent.set_light_on(starts_on)

/datum/component/overlay_lighting/RegisterWithParent()
	. = ..()
	if(directional)
		RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, .proc/on_parent_dir_change)
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_parent_moved)
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_RANGE, .proc/set_range)
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_POWER, .proc/set_power)
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_COLOR, .proc/set_color)
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_ON, .proc/on_toggle)
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .proc/on_light_flags_change)
	RegisterSignal(parent, COMSIG_ATOM_USED_IN_CRAFT, .proc/on_parent_crafted)
	RegisterSignal(parent, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater)
	var/atom/movable/movable_parent = parent
	if(movable_parent.light_flags & LIGHT_ATTACHED)
		overlay_lighting_flags |= LIGHTING_ATTACHED
		set_parent_attached_to(ismovable(movable_parent.loc) ? movable_parent.loc : null)
	check_holder()
	if(movable_parent.light_on)
		turn_on()

/datum/component/overlay_lighting/UnregisterFromParent()
	overlay_lighting_flags &= ~LIGHTING_ATTACHED
	set_parent_attached_to(null)
	set_holder(null)
	clean_old_turfs()
	UnregisterSignal(parent, list(
		COMSIG_MOVABLE_MOVED,
		COMSIG_ATOM_UPDATE_LIGHT_RANGE,
		COMSIG_ATOM_UPDATE_LIGHT_POWER,
		COMSIG_ATOM_UPDATE_LIGHT_COLOR,
		COMSIG_ATOM_UPDATE_LIGHT_ON,
		COMSIG_ATOM_UPDATE_LIGHT_FLAGS,
		COMSIG_ATOM_USED_IN_CRAFT,
		COMSIG_LIGHT_EATER_QUEUE,
		))
	if(directional)
		UnregisterSignal(parent, COMSIG_ATOM_DIR_CHANGE)
	if(overlay_lighting_flags & LIGHTING_ON)
		turn_off()
	return ..()

/datum/component/overlay_lighting/Destroy()
	set_parent_attached_to(null)
	set_holder(null)
	clean_old_turfs()
	visible_mask = null
	cone = null
	parent_attached_to = null
	return ..()

///Clears the affected_turfs lazylist, removing from its contents the effects of being near the light.
/datum/component/overlay_lighting/proc/clean_old_turfs()
	for(var/turf/lit_turf as anything in affected_turfs)
		lit_turf.dynamic_lumcount -= lum_power
	affected_turfs = null

///Populates the affected_turfs lazylist, adding to its contents the effects of being near the light.
/datum/component/overlay_lighting/proc/get_new_turfs()
	if(!current_holder)
		return
	. = list()
	for(var/turf/lit_turf in view(lumcount_range, get_turf(current_holder)))
		lit_turf.dynamic_lumcount += lum_power
		. += lit_turf
	if(length(.))
		affected_turfs = .

///Clears the old affected turfs and populates the new ones.
/datum/component/overlay_lighting/proc/make_luminosity_update()
	clean_old_turfs()
	if(!isturf(current_holder?.loc))
		return
	if(directional)
		cast_directional_light()
	get_new_turfs()

///Adds the luminosity and source for the afected movable atoms to keep track of their visibility.
/datum/component/overlay_lighting/proc/add_dynamic_lumi()
	LAZYSET(current_holder.affected_dynamic_lights, src, lumcount_range + 1)
	current_holder.underlays += visible_mask
	current_holder.update_dynamic_luminosity()
	if(directional)
		current_holder.underlays += cone

///Removes the luminosity and source for the afected movable atoms to keep track of their visibility.
/datum/component/overlay_lighting/proc/remove_dynamic_lumi()
	LAZYREMOVE(current_holder.affected_dynamic_lights, src)
	current_holder.underlays -= visible_mask
	current_holder.update_dynamic_luminosity()
	if(directional)
		current_holder.underlays -= cone

///Called to change the value of parent_attached_to.
/datum/component/overlay_lighting/proc/set_parent_attached_to(atom/movable/new_parent_attached_to)
	if(new_parent_attached_to == parent_attached_to)
		return

	. = parent_attached_to
	parent_attached_to = new_parent_attached_to
	if(.)
		var/atom/movable/old_parent_attached_to = .
		UnregisterSignal(old_parent_attached_to, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_MOVED, COMSIG_LIGHT_EATER_QUEUE))
		if(old_parent_attached_to == current_holder)
			RegisterSignal(old_parent_attached_to, COMSIG_PARENT_QDELETING, .proc/on_holder_qdel)
			RegisterSignal(old_parent_attached_to, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved)
			RegisterSignal(old_parent_attached_to, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater)
	if(parent_attached_to)
		if(parent_attached_to == current_holder)
			UnregisterSignal(current_holder, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_MOVED, COMSIG_LIGHT_EATER_QUEUE))
		RegisterSignal(parent_attached_to, COMSIG_PARENT_QDELETING, .proc/on_parent_attached_to_qdel)
		RegisterSignal(parent_attached_to, COMSIG_MOVABLE_MOVED, .proc/on_parent_attached_to_moved)
		RegisterSignal(parent_attached_to, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater)
	check_holder()

///Called to change the value of current_holder.
/datum/component/overlay_lighting/proc/set_holder(atom/movable/new_holder)
	if(new_holder == current_holder)
		return
	if(current_holder)
		if(current_holder != parent && current_holder != parent_attached_to)
			UnregisterSignal(current_holder, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_MOVED, COMSIG_LIGHT_EATER_QUEUE))
			if(directional)
				UnregisterSignal(current_holder, COMSIG_ATOM_DIR_CHANGE)
		if(overlay_lighting_flags & LIGHTING_ON)
			remove_dynamic_lumi()
	current_holder = new_holder
	if(new_holder == null)
		clean_old_turfs()
		return
	if(new_holder != parent && new_holder != parent_attached_to)
		RegisterSignal(new_holder, COMSIG_PARENT_QDELETING, .proc/on_holder_qdel)
		RegisterSignal(new_holder, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved)
		RegisterSignal(new_holder, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater)
		if(directional)
			RegisterSignal(new_holder, COMSIG_ATOM_DIR_CHANGE, .proc/on_holder_dir_change)
	if(overlay_lighting_flags & LIGHTING_ON)
		add_dynamic_lumi()
		make_luminosity_update()

///Used to determine the new valid current_holder from the parent's loc.
/datum/component/overlay_lighting/proc/check_holder()
	var/atom/movable/movable_parent = GET_PARENT
	if(isturf(movable_parent.loc))
		set_holder(movable_parent)
		return
	var/atom/inside = movable_parent.loc //Parent's loc
	if(isnull(inside))
		set_holder(null)
		return
	if(isturf(inside.loc))
		set_holder(inside)
		return
	set_holder(null)

///Called when the current_holder is qdeleted, to remove the light effect.
/datum/component/overlay_lighting/proc/on_holder_qdel(atom/movable/source, force)
	SIGNAL_HANDLER
	UnregisterSignal(current_holder, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_MOVED))
	if(directional)
		UnregisterSignal(current_holder, COMSIG_ATOM_DIR_CHANGE)
	set_holder(null)

///Called when current_holder changes loc.
/datum/component/overlay_lighting/proc/on_holder_moved(atom/movable/source, OldLoc, Dir, Forced)
	SIGNAL_HANDLER
	if(!(overlay_lighting_flags & LIGHTING_ON))
		return
	make_luminosity_update()

///Called when parent changes loc.
/datum/component/overlay_lighting/proc/on_parent_moved(atom/movable/source, OldLoc, Dir, Forced)
	SIGNAL_HANDLER
	var/atom/movable/movable_parent = parent
	if(overlay_lighting_flags & LIGHTING_ATTACHED)
		set_parent_attached_to(ismovable(movable_parent.loc) ? movable_parent.loc : null)
	check_holder()
	if(!(overlay_lighting_flags & LIGHTING_ON) || !current_holder)
		return
	make_luminosity_update()

///Called when the current_holder is qdeleted, to remove the light effect.
/datum/component/overlay_lighting/proc/on_parent_attached_to_qdel(atom/movable/source, force)
	SIGNAL_HANDLER
	UnregisterSignal(parent_attached_to, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_MOVED))
	if(directional)
		UnregisterSignal(parent_attached_to, COMSIG_ATOM_DIR_CHANGE)
	if(parent_attached_to == current_holder)
		set_holder(null)
	set_parent_attached_to(null)

///Called when parent_attached_to changes loc.
/datum/component/overlay_lighting/proc/on_parent_attached_to_moved(atom/movable/source, OldLoc, Dir, Forced)
	SIGNAL_HANDLER
	check_holder()
	if(!(overlay_lighting_flags & LIGHTING_ON) || !current_holder)
		return
	make_luminosity_update()

///Changes the range which the light reaches. 0 means no light, 6 is the maximum value.
/datum/component/overlay_lighting/proc/set_range(atom/source, old_range)
	SIGNAL_HANDLER
	var/new_range = source.light_range
	if(range == new_range)
		return
	if(range == 0)
		turn_off()
	range = clamp(CEILING(new_range, 0.5), 1, 6)
	var/pixel_bounds = ((range - 1) * 64) + 32
	lumcount_range = CEILING(range, 1)
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays -= visible_mask
	visible_mask.icon = light_overlays["[pixel_bounds]"]
	if(pixel_bounds == 32)
		visible_mask.transform = null
		return
	var/offset = (pixel_bounds - 32) * 0.5
	var/matrix/transform = new
	transform.Translate(-offset, -offset)
	visible_mask.transform = transform
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays += visible_mask
	if(directional)
		cast_range = clamp(round(new_range * 0.5), 1, 3)
	if(overlay_lighting_flags & LIGHTING_ON)
		make_luminosity_update()

///Changes the intensity/brightness of the light by altering the visual object's alpha.
/datum/component/overlay_lighting/proc/set_power(atom/source, old_power)
	SIGNAL_HANDLER
	var/new_power = source.light_power
	set_lum_power(new_power >= 0 ? 0.5 : -0.5)
	set_alpha = min(230, (abs(new_power) * 120) + 30)
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays -= visible_mask
	visible_mask.alpha = set_alpha
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays += visible_mask
	if(!directional)
		return
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays -= cone
	cone.alpha = min(200, (abs(new_power) * 90)+20)
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays += cone

///Changes the light's color, pretty straightforward.
/datum/component/overlay_lighting/proc/set_color(atom/source, old_color)
	SIGNAL_HANDLER
	var/new_color = source.light_color
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays -= visible_mask
	visible_mask.color = new_color
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays += visible_mask
	if(!directional)
		return
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays -= cone
	cone.color = new_color
	if(current_holder && overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays += cone

///Toggles the light on and off.
/datum/component/overlay_lighting/proc/on_toggle(atom/source, old_value)
	SIGNAL_HANDLER
	var/new_value = source.light_on
	if(new_value) //Truthy value input, turn on.
		turn_on()
		return
	turn_off() //Falsey value, turn off.

///Triggered right after the parent light flags change.
/datum/component/overlay_lighting/proc/on_light_flags_change(atom/source, old_flags)
	SIGNAL_HANDLER
	var/new_flags = source.light_flags
	var/atom/movable/movable_parent = parent
	if(!((new_flags ^ old_flags) & LIGHT_ATTACHED))
		return

	if(new_flags & LIGHT_ATTACHED) // Gained the [LIGHT_ATTACHED] property
		overlay_lighting_flags |= LIGHTING_ATTACHED
		if(ismovable(movable_parent.loc))
			set_parent_attached_to(movable_parent.loc)
	else // Lost the [LIGHT_ATTACHED] property
		overlay_lighting_flags &= ~LIGHTING_ATTACHED
		set_parent_attached_to(null)

///Toggles the light on.
/datum/component/overlay_lighting/proc/turn_on()
	if(overlay_lighting_flags & LIGHTING_ON)
		return
	if(current_holder)
		if(directional)
			cast_directional_light()
		add_dynamic_lumi()
	overlay_lighting_flags |= LIGHTING_ON
	get_new_turfs()

///Toggles the light off.
/datum/component/overlay_lighting/proc/turn_off()
	if(!(overlay_lighting_flags & LIGHTING_ON))
		return
	if(current_holder)
		remove_dynamic_lumi()
	overlay_lighting_flags &= ~LIGHTING_ON
	clean_old_turfs()

///Here we append the behavior associated to changing lum_power.
/datum/component/overlay_lighting/proc/set_lum_power(new_lum_power)
	if(lum_power == new_lum_power)
		return
	. = lum_power
	lum_power = new_lum_power
	var/difference = . - lum_power
	for(var/turf/lit_turf as anything in affected_turfs)
		lit_turf.dynamic_lumcount -= difference

///Here we append the behavior associated to changing lum_power.
/datum/component/overlay_lighting/proc/cast_directional_light()
	var/final_distance = cast_range
	//Lower the distance by 1 if we're not looking at a cardinal direction, and we're not a short cast
	if(final_distance > SHORT_CAST && !(ALL_CARDINALS & current_direction))
		final_distance -= 1
	var/turf/scanning = get_turf(current_holder)
	for(var/i in 1 to final_distance)
		var/turf/next_turf = get_step(scanning, current_direction)
		if(isnull(next_turf) || IS_OPAQUE_TURF(next_turf))
			final_distance = i
			break
		scanning = next_turf

	current_holder.underlays -= visible_mask

	var/translate_x = -((range - 1) * 32)
	var/translate_y = translate_x
	switch(current_direction)
		if(NORTH)
			translate_y += 32 * final_distance
		if(SOUTH)
			translate_y += -32 * final_distance
		if(EAST)
			translate_x += 32 * final_distance
		if(WEST)
			translate_x += -32 * final_distance
	if((directional_offset_x != translate_x) || (directional_offset_y != translate_y))
		directional_offset_x = translate_x
		directional_offset_y = translate_y
		var/matrix/transform = matrix()
		transform.Translate(translate_x, translate_y)
		visible_mask.transform = transform
	if(overlay_lighting_flags & LIGHTING_ON)
		current_holder.underlays += visible_mask

///Called when current_holder changes loc.
/datum/component/overlay_lighting/proc/on_holder_dir_change(atom/movable/source, olddir, newdir)
	SIGNAL_HANDLER
	set_direction(newdir)

///Called when parent changes loc.
/datum/component/overlay_lighting/proc/on_parent_dir_change(atom/movable/source, olddir, newdir)
	SIGNAL_HANDLER
	set_direction(newdir)

///Sets a new direction for the directional cast, then updates luminosity
/datum/component/overlay_lighting/proc/set_direction(newdir)
	if(!newdir)
		return
	if(current_direction == newdir)
		return
	current_direction = newdir
	if(overlay_lighting_flags & LIGHTING_ON)
		make_luminosity_update()

/datum/component/overlay_lighting/proc/on_parent_crafted(datum/source, atom/movable/new_craft)
	SIGNAL_HANDLER

	if(!istype(new_craft))
		return

	UnregisterSignal(parent, COMSIG_ATOM_USED_IN_CRAFT)
	RegisterSignal(new_craft, COMSIG_ATOM_USED_IN_CRAFT, .proc/on_parent_crafted)
	set_parent_attached_to(new_craft)

/// Handles putting the source for overlay lights into the light eater queue since we aren't tracked by [/atom/var/light_sources]
/datum/component/overlay_lighting/proc/on_light_eater(datum/source, list/light_queue, datum/light_eater)
	SIGNAL_HANDLER
	light_queue[parent] = TRUE
	return NONE

#undef LIGHTING_ON
#undef LIGHTING_ATTACHED
#undef GET_PARENT
#undef SHORT_CAST


code:
/**
 * Handles simple payment operations where the cost of the object in question doesn't change.
 *
 * What this is useful for:
 * Basic forms of vending.
 * Objects that can drain the owner's money linearly.
 * What this is not useful for:
 * Things where the seller may want to fluxuate the price of the object.
 * Improving standardizing every form of payment handing, as some custom handling is specific to that object.
 **/
/datum/component/payment
	dupe_mode = COMPONENT_DUPE_UNIQUE ///NO OVERRIDING TO CHEESE BOUNTIES
	///Standardized of operation.
	var/cost = 10
	///Flavor style for handling cash (Friendly? Hostile? etc.)
	var/transaction_style = "Clinical"
	///Who's getting paid?
	var/datum/bank_account/target_acc

/datum/component/payment/Initialize(_cost, _target, _style)
	target_acc = _target
	if(!target_acc)
		target_acc = SSeconomy.get_dep_account(ACCOUNT_CIV)

	cost = _cost
	transaction_style = _style
	RegisterSignal(parent, COMSIG_OBJ_ATTEMPT_CHARGE, .proc/attempt_charge)
	RegisterSignal(parent, COMSIG_OBJ_ATTEMPT_CHARGE_CHANGE, .proc/change_cost)

/datum/component/payment/proc/attempt_charge(datum/source, atom/movable/target, extra_fees = 0)
	SIGNAL_HANDLER

	if(!cost) //In case a free variant of anything is made it'll skip charging anyone.
		return
	if(!ismob(target))
		return COMPONENT_OBJ_CANCEL_CHARGE
	var/mob/living/user = target
	var/obj/item/card/id/card
	if(istype(user))
		card = user.get_idcard(TRUE)
	if(!card)
		switch(transaction_style)
			if(PAYMENT_FRIENDLY)
				to_chat(user, span_warning("ID not detected, sorry [user]!"))
			if(PAYMENT_ANGRY)
				to_chat(user, span_warning("WHERE IS YOUR GOD DAMN CARD! GOD DAMNIT!"))
			if(PAYMENT_CLINICAL)
				to_chat(user, span_warning("ID card not present. Aborting."))
		return COMPONENT_OBJ_CANCEL_CHARGE
	if(!card.registered_account)
		switch(transaction_style)
			if(PAYMENT_FRIENDLY)
				to_chat(user, span_warning("There's no account detected on your ID, how mysterious!"))
			if(PAYMENT_ANGRY)
				to_chat(user, span_warning("ARE YOU JOKING. YOU DON'T HAVE A BANK ACCOUNT ON YOUR ID YOU IDIOT."))
			if(PAYMENT_CLINICAL)
				to_chat(user, span_warning("ID Card lacks a bank account. Aborting."))
		return COMPONENT_OBJ_CANCEL_CHARGE
	if(!(card.registered_account.has_money(cost + extra_fees)))
		switch(transaction_style)
			if(PAYMENT_FRIENDLY)
				to_chat(user, span_warning("I'm so sorry... You don't seem to have enough money."))
			if(PAYMENT_ANGRY)
				to_chat(user, span_warning("YOU MORON. YOU ABSOLUTE BAFOON. YOU INSUFFERABLE TOOL. YOU ARE POOR."))
			if(PAYMENT_CLINICAL)
				to_chat(user, span_warning("ID Card lacks funds. Aborting."))
		return COMPONENT_OBJ_CANCEL_CHARGE
	target_acc.transfer_money(card.registered_account, cost + extra_fees)
	card.registered_account.bank_card_talk("[cost + extra_fees] credits deducted from your account.")
	playsound(src, 'sound/effects/cashregister.ogg', 20, TRUE)

/datum/component/payment/proc/change_cost(datum/source, new_cost)
	SIGNAL_HANDLER

	if(!isnum(new_cost))
		CRASH("change_cost called with variable new_cost as not a number.")
	cost = new_cost


code:
// the following defines are used for [/datum/component/pellet_cloud/var/list/wound_info_by_part] to store the damage, wound_bonus, and bw_bonus for each bodypart hit
#define CLOUD_POSITION_DAMAGE 1
#define CLOUD_POSITION_W_BONUS 2
#define CLOUD_POSITION_BW_BONUS 3

/*
	* This component is used when you want to create a bunch of shrapnel or projectiles (say, shrapnel from a fragmentation grenade, or buckshot from a shotgun) from a central point,
	* without necessarily printing a separate message for every single impact. This component should be instantiated right when you need it (like the moment of firing), then activated
	* by signal.
	*
	* Pellet cloud currently works on two classes of sources: directed (ammo casings), and circular (grenades, landmines).
	* -Directed: This means you're shooting multiple pellets, like buckshot. If an ammo casing is defined as having multiple pellets, it will automatically create a pellet cloud
	* and call COMSIG_PELLET_CLOUD_INIT (see [/obj/item/ammo_casing/proc/fire_casing]). Thus, the only projectiles fired will be the ones fired here.
	* The magnitude var controls how many pellets are created.
	* -Circular: This results in a big spray of shrapnel flying all around the detonation point when the grenade fires COMSIG_GRENADE_DETONATE or landmine triggers COMSIG_MINE_TRIGGERED.
	* The magnitude var controls how big the detonation radius is (the bigger the magnitude, the more shrapnel is created). Grenades can be covered with bodies to reduce shrapnel output.
	*
	* Once all of the fired projectiles either hit a target or disappear due to ranging out/whatever else, we resolve the list of all the things we hit and print aggregate messages so we get
	* one "You're hit by 6 buckshot pellets" vs 6x "You're hit by the buckshot blah blah" messages.
	*
	* Note that this is how all guns handle shooting ammo casings with multiple pellets, in case such a thing comes up.
*/

/datum/component/pellet_cloud
	/// What's the projectile path of the shrapnel we're shooting?
	var/projectile_type

	/// How many shrapnel projectiles are we responsible for tracking? May be reduced for grenades if someone dives on top of it. Defined by ammo casing for casings, derived from magnitude otherwise
	var/num_pellets
	/// For grenades/landmines, how big is the radius of turfs we're targeting? Note this does not effect the projectiles range, only how many we generate
	var/radius = 4

	/// The list of pellets we're responsible for tracking, once these are all accounted for, we finalize.
	var/list/pellets = list()
	/// An associated list with the atom hit as the key and how many pellets they've eaten for the value, for printing aggregate messages
	var/list/targets_hit = list()
	/// Another associated list for hit bodyparts on carbons so we can track how much wounding potential we have for each bodypart
	var/list/wound_info_by_part = list()
	/// For grenades, any /mob/living's the grenade is moved onto, see [/datum/component/pellet_cloud/proc/handle_martyrs]
	var/list/bodies
	/// For grenades, tracking people who die covering a grenade for achievement purposes, see [/datum/component/pellet_cloud/proc/handle_martyrs]
	var/list/purple_hearts

	/// For grenades, tracking how many pellets are removed due to martyrs and how many pellets are added due to the last person to touch it being on top of it
	var/pellet_delta = 0
	/// how many pellets ranged out without hitting anything
	var/terminated
	/// how many pellets impacted something
	var/hits
	/// If the parent tried deleting and we're not done yet, we send it to nullspace then delete it after
	var/queued_delete = FALSE

	/// for if we're an ammo casing being fired
	var/mob/living/shooter

/datum/component/pellet_cloud/Initialize(projectile_type=/obj/item/shrapnel, magnitude=5)
	if(!isammocasing(parent) && !isgrenade(parent) && !islandmine(parent) && !issupplypod(parent))
		return COMPONENT_INCOMPATIBLE

	if(magnitude < 1)
		stack_trace("Invalid magnitude [magnitude] < 1 on pellet_cloud, parent: [parent]")
		magnitude = 1

	src.projectile_type = projectile_type

	if(isammocasing(parent))
		num_pellets = magnitude
	else if(isgrenade(parent) || islandmine(parent) || issupplypod(parent))
		radius = magnitude

/datum/component/pellet_cloud/Destroy(force, silent)
	purple_hearts = null
	pellets = null
	targets_hit = null
	wound_info_by_part = null
	bodies = null
	return ..()

/datum/component/pellet_cloud/RegisterWithParent()
	RegisterSignal(parent, COMSIG_PARENT_PREQDELETED, .proc/nullspace_parent)
	if(isammocasing(parent))
		RegisterSignal(parent, COMSIG_PELLET_CLOUD_INIT, .proc/create_casing_pellets)
	else if(isgrenade(parent))
		RegisterSignal(parent, COMSIG_GRENADE_ARMED, .proc/grenade_armed)
		RegisterSignal(parent, COMSIG_GRENADE_DETONATE, .proc/create_blast_pellets)
	else if(islandmine(parent))
		RegisterSignal(parent, COMSIG_MINE_TRIGGERED, .proc/create_blast_pellets)
	else if(issupplypod(parent))
		RegisterSignal(parent, COMSIG_SUPPLYPOD_LANDED, .proc/create_blast_pellets)

/datum/component/pellet_cloud/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_PELLET_CLOUD_INIT, COMSIG_GRENADE_DETONATE, COMSIG_GRENADE_ARMED, COMSIG_MOVABLE_MOVED, COMSIG_MINE_TRIGGERED, COMSIG_ITEM_DROPPED))

/**
 * create_casing_pellets() is for directed pellet clouds for ammo casings that have multiple pellets (buckshot and scatter lasers for instance)
 *
 * Honestly this is mostly just a rehash of [/obj/item/ammo_casing/proc/fire_casing] for pellet counts > 1, except this lets us tamper with the pellets and hook onto them for tracking purposes.
 * The arguments really don't matter, this proc is triggered by COMSIG_PELLET_CLOUD_INIT which is only for this really, it's just a big mess of the state vars we need for doing the stuff over here.
 */
/datum/component/pellet_cloud/proc/create_casing_pellets(obj/item/ammo_casing/shell, atom/target, mob/living/user, fired_from, randomspread, spread, zone_override, params, distro)
	SIGNAL_HANDLER

	shooter = user
	var/turf/target_loc = get_turf(target)
	if(!zone_override)
		zone_override = shooter.zone_selected

	// things like mouth executions and gunpoints can multiply the damage and wounds of projectiles, so this makes sure those effects are applied to each pellet instead of just one
	var/original_damage = shell.loaded_projectile.damage
	var/original_wb = shell.loaded_projectile.wound_bonus
	var/original_bwb = shell.loaded_projectile.bare_wound_bonus

	for(var/i in 1 to num_pellets)
		shell.ready_proj(target, user, SUPPRESSED_VERY, zone_override, fired_from)
		if(distro)
			if(randomspread)
				spread = round((rand() - 0.5) * distro)
			else //Smart spread
				spread = round((i / num_pellets - 0.5) * distro)

		RegisterSignal(shell.loaded_projectile, COMSIG_PROJECTILE_SELF_ON_HIT, .proc/pellet_hit)
		RegisterSignal(shell.loaded_projectile, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PARENT_QDELETING), .proc/pellet_range)
		shell.loaded_projectile.damage = original_damage
		shell.loaded_projectile.wound_bonus = original_wb
		shell.loaded_projectile.bare_wound_bonus = original_bwb
		pellets += shell.loaded_projectile
		var/turf/current_loc = get_turf(user)
		if (!istype(target_loc) || !istype(current_loc) || !(shell.loaded_projectile))
			return
		INVOKE_ASYNC(shell, /obj/item/ammo_casing.proc/throw_proj, target, target_loc, shooter, params, spread)

		if(i != num_pellets)
			shell.newshot()

/**
 * create_blast_pellets() is for when we have a central point we want to shred the surroundings of with a ring of shrapnel, namely frag grenades and landmines.
 *
 * Note that grenades have extra handling for someone throwing themselves/being thrown on top of it, see [/datum/component/pellet_cloud/proc/handle_martyrs]
 * Landmines just have a small check for [/obj/effect/mine/shrapnel/var/shred_triggerer], and spawn extra shrapnel for them if so
 *
 * Arguments:
 * * O- Our parent, the thing making the shrapnel obviously (grenade or landmine)
 * * punishable_triggerer- For grenade lances or people who step on the landmines (if we shred the triggerer), we spawn extra shrapnel for them in addition to the normal spread
 */
/datum/component/pellet_cloud/proc/create_blast_pellets(obj/O, mob/living/triggerer)
	SIGNAL_HANDLER

	var/atom/A = parent

	if(isgrenade(parent)) // handle_martyrs can reduce the radius and thus the number of pellets we produce if someone dives on top of a frag grenade
		INVOKE_ASYNC(src, .proc/handle_martyrs, triggerer) // note that we can modify radius in this proc
	else if(islandmine(parent))
		var/obj/effect/mine/shrapnel/triggered_mine = parent
		if(triggered_mine.shred_triggerer && istype(triggerer)) // free shrapnel for the idiot who stepped on it if we're a mine that shreds the triggerer
			pellet_delta += radius // so they don't count against the later total
			for(var/i in 1 to radius)
				INVOKE_ASYNC(src, .proc/pew, triggerer, TRUE)

	if(radius < 1)
		return

	var/list/all_the_turfs_were_gonna_lacerate = RANGE_TURFS(radius, A) - RANGE_TURFS(radius-1, A)
	num_pellets = all_the_turfs_were_gonna_lacerate.len + pellet_delta

	for(var/T in all_the_turfs_were_gonna_lacerate)
		var/turf/shootat_turf = T
		INVOKE_ASYNC(src, .proc/pew, shootat_turf)

/**
 * handle_martyrs() is used for grenades that shoot shrapnel to check if anyone threw themselves/were thrown on top of the grenade, thus absorbing a good chunk of the shrapnel
 *
 * Between the time the grenade is armed and the actual detonation, we set var/list/bodies to the list of mobs currently on the new tile, as if the grenade landed on top of them, tracking if any of them move off the tile and removing them from the "under" list
 * Once the grenade detonates, handle_martyrs() is called and gets all the new mobs on the tile, and add the ones not in var/list/bodies to var/list/martyrs
 * We then iterate through the martyrs and reduce the shrapnel magnitude for each mob on top of it, shredding each of them with some of the shrapnel they helped absorb. This can snuff out all of the shrapnel if there's enough bodies
 *
 * Note we track anyone who's alive and client'd when they get shredded in var/list/purple_hearts, for achievement checking later
 */
/datum/component/pellet_cloud/proc/handle_martyrs(mob/living/punishable_triggerer)
	var/magnitude_absorbed
	var/list/martyrs = list()

	var/self_harm_radius_mult = 3

	if(punishable_triggerer && prob(60))
		to_chat(punishable_triggerer, span_userdanger("Your plan to whack someone with a grenade on a stick backfires on you, literally!"))
		self_harm_radius_mult = 1 // we'll still give the guy who got hit some extra shredding, but not 3*radius
		pellet_delta += radius
		for(var/i in 1 to radius)
			pew(punishable_triggerer) // thought you could be tricky and lance someone with no ill effects!!

	for(var/mob/living/body in get_turf(parent))
		if(body == shooter)
			pellet_delta += radius * self_harm_radius_mult
			for(var/i in 1 to radius * self_harm_radius_mult)
				pew(body) // free shrapnel if it goes off in your hand, and it doesn't even count towards the absorbed. fun!
		else if(!(body in bodies))
			martyrs += body // promoted from a corpse to a hero

	for(var/M in martyrs)
		var/mob/living/martyr = M
		if(radius > 4)
			martyr.visible_message("[span_danger("[martyr] heroically covers \the [parent] with [martyr.p_their()] body, absorbing a load of the shrapnel!")]", span_userdanger("You heroically cover \the [parent] with your body, absorbing a load of the shrapnel!"))
			magnitude_absorbed += round(radius * 0.5)
		else if(radius >= 2)
			martyr.visible_message("[span_danger("[martyr] heroically covers \the [parent] with [martyr.p_their()] body, absorbing some of the shrapnel!")]", span_userdanger("You heroically cover \the [parent] with your body, absorbing some of the shrapnel!"))
			magnitude_absorbed += 2
		else
			martyr.visible_message("[span_danger("[martyr] heroically covers \the [parent] with [martyr.p_their()] body, snuffing out the shrapnel!")]", span_userdanger("You heroically cover \the [parent] with your body, snuffing out the shrapnel!"))
			magnitude_absorbed = radius

		var/pellets_absorbed = (radius ** 2) - ((radius - magnitude_absorbed - 1) ** 2)
		radius -= magnitude_absorbed
		pellet_delta -= round(pellets_absorbed * 0.5)

		if(martyr.stat != DEAD && martyr.client)
			LAZYADD(purple_hearts, martyr)
			RegisterSignal(martyr, COMSIG_PARENT_QDELETING, .proc/on_target_qdel, override=TRUE)

		for(var/i in 1 to round(pellets_absorbed * 0.5))
			pew(martyr)

		if(radius < 1)
			break

///One of our pellets hit something, record what it was and check if we're done (terminated == num_pellets)
/datum/component/pellet_cloud/proc/pellet_hit(obj/projectile/P, atom/movable/firer, atom/target, Angle, hit_zone)
	SIGNAL_HANDLER

	pellets -= P
	terminated++
	hits++
	var/obj/item/bodypart/hit_part
	var/no_damage = FALSE
	if(iscarbon(target) && hit_zone)
		var/mob/living/carbon/hit_carbon = target
		hit_part = hit_carbon.get_bodypart(hit_zone)
		if(hit_part)
			target = hit_part
			if(P.wound_bonus != CANT_WOUND) // handle wounding
				// unfortunately, due to how pellet clouds handle finalizing only after every pellet is accounted for, that also means there might be a short delay in dealing wounds if one pellet goes wide
				// while buckshot may reach a target or miss it all in one tick, we also have to account for possible ricochets that may take a bit longer to hit the target
				if(isnull(wound_info_by_part[hit_part]))
					wound_info_by_part[hit_part] = list(0, 0, 0)
				wound_info_by_part[hit_part][CLOUD_POSITION_DAMAGE] += P.damage // these account for decay
				wound_info_by_part[hit_part][CLOUD_POSITION_W_BONUS] += P.wound_bonus
				wound_info_by_part[hit_part][CLOUD_POSITION_BW_BONUS] += P.bare_wound_bonus
				P.wound_bonus = CANT_WOUND // actual wounding will be handled aggregate
	else if(isobj(target))
		var/obj/hit_object = target
		if(hit_object.damage_deflection > P.damage || !P.damage)
			no_damage = TRUE

	LAZYADDASSOC(targets_hit[target], "hits", 1)
	LAZYSET(targets_hit[target], "no damage", no_damage)
	if(targets_hit[target]["hits"] == 1)
		RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/on_target_qdel, override=TRUE)
	UnregisterSignal(P, list(COMSIG_PARENT_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
	//SEPTIC EDIT BEGIN
	UnregisterSignal(P, COMSIG_PELLET_CLOUD_EMBEDDED)
	UnregisterSignal(P, COMSIG_PELLET_CLOUD_WENT_THROUGH)
	//SEPTIC EDIT END
	if(terminated == num_pellets)
		finalize()

///One of our pellets disappeared due to hitting their max range (or just somehow got qdel'd), remove it from our list and check if we're done (terminated == num_pellets)
/datum/component/pellet_cloud/proc/pellet_range(obj/projectile/P)
	SIGNAL_HANDLER
	pellets -= P
	terminated++
	UnregisterSignal(P, list(COMSIG_PARENT_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
	//SEPTIC EDIT BEGIN
	UnregisterSignal(P, COMSIG_PELLET_CLOUD_EMBEDDED)
	UnregisterSignal(P, COMSIG_PELLET_CLOUD_WENT_THROUGH)
	//SEPTIC EDIT END
	if(terminated == num_pellets)
		finalize()

/// Minor convenience function for creating each shrapnel piece with circle explosions, mostly stolen from the MIRV component
/datum/component/pellet_cloud/proc/pew(atom/target, landmine_victim)
	var/obj/projectile/P = new projectile_type(get_turf(parent))

	//Shooting Code:
	P.spread = 0
	P.original = target
	P.fired_from = parent
	P.firer = parent // don't hit ourself that would be really annoying
	P.impacted = list(parent = TRUE) // don't hit the target we hit already with the flak
	P.suppressed = SUPPRESSED_VERY // set the projectiles to make no message so we can do our own aggregate message
	P.preparePixelProjectile(target, parent)
	//SEPTIC EDIT BEGIN
	RegisterSignal(P, COMSIG_PELLET_CLOUD_EMBEDDED, .proc/projectile_embedded)
	RegisterSignal(P, COMSIG_PELLET_CLOUD_WENT_THROUGH, .proc/projectile_went_through)
	//SEPTIC EDIT END
	RegisterSignal(P, COMSIG_PROJECTILE_SELF_ON_HIT, .proc/pellet_hit)
	RegisterSignal(P, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PARENT_QDELETING), .proc/pellet_range)
	pellets += P
	P.fire()
	if(landmine_victim)
		P.process_hit(get_turf(target), target)

///All of our pellets are accounted for, time to go target by target and tell them how many things they got hit by.
/datum/component/pellet_cloud/proc/finalize()
	var/obj/projectile/P = projectile_type
	var/proj_name = initial(P.name)

	for(var/atom/target in targets_hit)
		var/num_hits = targets_hit[target]["hits"]
		var/did_damage = targets_hit[target]["no damage"]
		UnregisterSignal(target, COMSIG_PARENT_QDELETING)
		var/obj/item/bodypart/hit_part
		if(isbodypart(target))
			hit_part = target
			target = hit_part.owner
			if(wound_info_by_part[hit_part] && (initial(P.damage_type) == BRUTE || initial(P.damage_type) == BURN)) // so a cloud of disablers that deal stamina don't inadvertently end up causing burn wounds)
				var/damage_dealt = wound_info_by_part[hit_part][CLOUD_POSITION_DAMAGE]
				var/w_bonus = wound_info_by_part[hit_part][CLOUD_POSITION_W_BONUS]
				var/bw_bonus = wound_info_by_part[hit_part][CLOUD_POSITION_BW_BONUS]
				var/wound_type = (initial(P.damage_type) == BRUTE) ? WOUND_BLUNT : WOUND_BURN // sharpness is handled in the wound rolling
				wound_info_by_part -= hit_part
				hit_part.painless_wound_roll(wound_type, damage_dealt, w_bonus, bw_bonus, initial(P.sharpness))

		if(num_hits > 1)
			target.visible_message(span_danger("[target] is hit by [num_hits] [proj_name][plural_s(proj_name)][hit_part ? " in the [hit_part.name]" : ""][did_damage ? ", which don't leave a mark" : ""]!"), null, null, COMBAT_MESSAGE_RANGE, target)
			to_chat(target, span_userdanger("You're hit by [num_hits] [proj_name]s[hit_part ? " in the [hit_part.name]" : ""]!"))
		else
			target.visible_message(span_danger("[target] is hit by a [proj_name][hit_part ? " in the [hit_part.name]" : ""][did_damage ? ", which doesn't leave a mark" : ""]!"), null, null, COMBAT_MESSAGE_RANGE, target)
			to_chat(target, span_userdanger("You're hit by a [proj_name][hit_part ? " in the [hit_part.name]" : ""]!"))

	for(var/M in purple_hearts)
		var/mob/living/martyr = M
		if(martyr.stat == DEAD && martyr.client)
			martyr.client.give_award(/datum/award/achievement/misc/lookoutsir, martyr)
	UnregisterSignal(parent, COMSIG_PARENT_PREQDELETED)
	if(queued_delete)
		qdel(parent)
	qdel(src)

/// Look alive, we're armed! Now we start watching to see if anyone's covering us
/datum/component/pellet_cloud/proc/grenade_armed(obj/item/nade)
	SIGNAL_HANDLER

	if(ismob(nade.loc))
		shooter = nade.loc
	LAZYINITLIST(bodies)
	RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/grenade_dropped)
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/grenade_moved)
	var/static/list/loc_connections = list(
		COMSIG_ATOM_EXITED =.proc/grenade_uncrossed,
	)
	AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)

/// Someone dropped the grenade, so set them to the shooter in case they're on top of it when it goes off
/datum/component/pellet_cloud/proc/grenade_dropped(obj/item/nade, mob/living/slick_willy)
	SIGNAL_HANDLER

	shooter = slick_willy
	grenade_moved()

/// Our grenade has moved, reset var/list/bodies so we're "on top" of any mobs currently on the tile
/datum/component/pellet_cloud/proc/grenade_moved()
	SIGNAL_HANDLER

	LAZYCLEARLIST(bodies)
	for(var/mob/living/L in get_turf(parent))
		RegisterSignal(L, COMSIG_PARENT_QDELETING, .proc/on_target_qdel, override=TRUE)
		bodies += L

/// Someone who was originally "under" the grenade has moved off the tile and is now eligible for being a martyr and "covering" it
/datum/component/pellet_cloud/proc/grenade_uncrossed(datum/source, atom/movable/gone, direction)
	SIGNAL_HANDLER

	bodies -= gone

/// Our grenade or landmine or caseless shell or whatever tried deleting itself, so we intervene and nullspace it until we're done here
/datum/component/pellet_cloud/proc/nullspace_parent()
	SIGNAL_HANDLER

	var/atom/movable/AM = parent
	AM.moveToNullspace()
	queued_delete = TRUE
	return TRUE

/// Someone who was originally "under" the grenade has moved off the tile and is now eligible for being a martyr and "covering" it
/datum/component/pellet_cloud/proc/on_target_qdel(atom/target)
	SIGNAL_HANDLER

	UnregisterSignal(target, COMSIG_PARENT_QDELETING)
	targets_hit -= target
	LAZYREMOVE(bodies, target)
	LAZYREMOVE(purple_hearts, target)

#undef CLOUD_POSITION_DAMAGE
#undef CLOUD_POSITION_W_BONUS
#undef CLOUD_POSITION_BW_BONUS


code:
///Component for IVs that tracks the current person being IV'd. Input received through plumbing is instead routed to the whoever is attached
/datum/component/plumbing/iv_drip
	demand_connects = SOUTH
	supply_connects = NORTH

	methods = INJECT

/datum/component/plumbing/iv_drip/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
	. = ..()

	set_recipient_reagents_holder(null)

/datum/component/plumbing/iv_drip/RegisterWithParent()
	. = ..()

	RegisterSignal(parent, list(COMSIG_IV_ATTACH), .proc/update_attached)
	RegisterSignal(parent, list(COMSIG_IV_DETACH), .proc/clear_attached)

/datum/component/plumbing/iv_drip/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_IV_ATTACH))
	UnregisterSignal(parent, list(COMSIG_IV_DETACH))

///When an IV is attached, we will use whoever is attached as our receiving container
/datum/component/plumbing/iv_drip/proc/update_attached(datum/source, mob/living/attachee)
	SIGNAL_HANDLER

	if(attachee?.reagents)
		set_recipient_reagents_holder(attachee.reagents)

///IV has been detached, so clear the holder
/datum/component/plumbing/iv_drip/proc/clear_attached(datum/source)
	SIGNAL_HANDLER

	set_recipient_reagents_holder(null)


code:
/datum/component/plumbing
	dupe_mode = COMPONENT_DUPE_ALLOWED
	///Index with "1" = /datum/ductnet/theductpointingnorth etc. "1" being the num2text from NORTH define
	var/list/datum/ductnet/ducts = list()
	///shortcut to our parents' reagent holder
	var/datum/reagents/reagents
	///TRUE if we wanna add proper pipe overlays under our parent object. this is pretty good if i may so so myself
	var/use_overlays = TRUE
	///Whether our tile is covered and we should hide our ducts
	var/tile_covered = FALSE
	///directions in wich we act as a supplier
	var/supply_connects
	///direction in wich we act as a demander
	var/demand_connects
	///FALSE to pretty much just not exist in the plumbing world so we can be moved, TRUE to go plumbo mode
	var/active = FALSE
	///if TRUE connects will spin with the parent object visually and codually, so you can have it work in any direction. FALSE if you want it to be static
	var/turn_connects = TRUE
	///The layer on which we connect. Don't add multiple. If you want multiple layer connects for some reason you can just add multiple components with different layers
	var/ducting_layer = DUCT_LAYER_DEFAULT
	///In-case we don't want the main machine to get the reagents, but perhaps whoever is buckled to it
	var/recipient_reagents_holder
	///How do we apply the new reagents to the receiver? Generally doesn't matter, but some stuff, like people, does care if its injected or whatevs
	var/methods
	///What color is our demand connect? Also it's not auto-colored so you'll have to make new sprites if its anything other than red, blue, yellow or green
	var/demand_color = "red"
	///What color is our supply connect? Also, refrain from pointlessly using non-standard colors unless it's really funny or something
	var/supply_color = "blue"

///turn_connects is for wheter or not we spin with the object to change our pipes
/datum/component/plumbing/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE

	if(_ducting_layer)
		ducting_layer = _ducting_layer

	var/atom/movable/AM = parent
	if(!AM.reagents && !custom_receiver)
		return COMPONENT_INCOMPATIBLE

	reagents = AM.reagents
	turn_connects = _turn_connects

	set_recipient_reagents_holder(custom_receiver ? custom_receiver : AM.reagents)

	if(start)
		//We're registering here because I need to check whether we start active or not, and this is just easier
		//Should be called after we finished. Done this way because other networks need to finish setting up aswell
		RegisterSignal(parent, list(COMSIG_COMPONENT_ADDED), .proc/enable)

/datum/component/plumbing/RegisterWithParent()
	RegisterSignal(parent, list(COMSIG_MOVABLE_MOVED,COMSIG_PARENT_PREQDELETED), .proc/disable)
	RegisterSignal(parent, list(COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH), .proc/toggle_active)
	RegisterSignal(parent, list(COMSIG_OBJ_HIDE), .proc/hide)
	RegisterSignal(parent, list(COMSIG_ATOM_UPDATE_OVERLAYS), .proc/create_overlays) //called by lateinit on startup
	RegisterSignal(parent, list(COMSIG_MOVABLE_CHANGE_DUCT_LAYER), .proc/change_ducting_layer)

/datum/component/plumbing/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED,COMSIG_PARENT_PREQDELETED, COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH,COMSIG_OBJ_HIDE, \
	COMSIG_ATOM_UPDATE_OVERLAYS, COMSIG_MOVABLE_CHANGE_DUCT_LAYER, COMSIG_COMPONENT_ADDED))

/datum/component/plumbing/Destroy()
	ducts = null
	reagents = null
	set_recipient_reagents_holder(null) //null is there so it's obvious we're setting this to nothing
	return ..()

/datum/component/plumbing/process()
	if(!demand_connects || !reagents)
		STOP_PROCESSING(SSfluids, src)
		return
	if(reagents.total_volume < reagents.maximum_volume)
		for(var/D in GLOB.cardinals)
			if(D & demand_connects)
				send_request(D)

///Can we be added to the ductnet?
/datum/component/plumbing/proc/can_add(datum/ductnet/D, dir)
	if(!active)
		return
	if(!dir || !D)
		return FALSE
	if(num2text(dir) in ducts)
		return FALSE

	return TRUE

///called from in process(). only calls process_request(), but can be overwritten for children with special behaviour
/datum/component/plumbing/proc/send_request(dir)
	process_request(amount = MACHINE_REAGENT_TRANSFER, reagent = null, dir = dir)

///check who can give us what we want, and how many each of them will give us
/datum/component/plumbing/proc/process_request(amount, reagent, dir)
	var/list/valid_suppliers = list()
	var/datum/ductnet/net
	if(!ducts.Find(num2text(dir)))
		return
	net = ducts[num2text(dir)]
	for(var/A in net.suppliers)
		var/datum/component/plumbing/supplier = A
		if(supplier.can_give(amount, reagent, net))
			valid_suppliers += supplier
	// Need to ask for each in turn very carefully, making sure we get the total volume. This is to avoid a division that would always round down and become 0
	var/targetVolume = reagents.total_volume + amount
	var/suppliersLeft = valid_suppliers.len
	for(var/A in valid_suppliers)
		var/datum/component/plumbing/give = A
		var/currentRequest = (targetVolume - reagents.total_volume) / suppliersLeft
		give.transfer_to(src, currentRequest, reagent, net)
		suppliersLeft--

///returns TRUE when they can give the specified amount and reagent. called by process request
/datum/component/plumbing/proc/can_give(amount, reagent, datum/ductnet/net)
	if(amount <= 0)
		return

	if(reagent) //only asked for one type of reagent
		for(var/A in reagents.reagent_list)
			var/datum/reagent/R = A
			if(R.type == reagent)
				return TRUE
	else if(reagents.total_volume > 0) //take whatever
		return TRUE

///this is where the reagent is actually transferred and is thus the finish point of our process()
/datum/component/plumbing/proc/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net)
	if(!reagents || !target || !target.reagents)
		return FALSE
	if(reagent)
		reagents.trans_id_to(target.recipient_reagents_holder, reagent, amount)
	else
		reagents.trans_to(target.recipient_reagents_holder, amount, round_robin = TRUE, methods = methods)//we deal with alot of precise calculations so we round_robin=TRUE. Otherwise we get floating point errors, 1 != 1 and 2.5 + 2.5 = 6

///We create our luxurious piping overlays/underlays, to indicate where we do what. only called once if use_overlays = TRUE in Initialize()
/datum/component/plumbing/proc/create_overlays(atom/movable/AM, list/overlays)
	SIGNAL_HANDLER

	if(tile_covered || !use_overlays)
		return

	//Copied from ducts handle_layer()
	var/offset

	switch(ducting_layer)
		if(FIRST_DUCT_LAYER)
			offset = -10
		if(SECOND_DUCT_LAYER)
			offset = -5
		if(THIRD_DUCT_LAYER)
			offset = 0
		if(FOURTH_DUCT_LAYER)
			offset = 5
		if(FIFTH_DUCT_LAYER)
			offset = 10

	var/duct_x = offset
	var/duct_y = offset

	for(var/D in GLOB.cardinals)
		var/color
		var/direction
		if(D & initial(demand_connects))
			color = demand_color
		else if(D & initial(supply_connects))
			color = supply_color
		else
			continue

		var/image/I

		switch(D)
			if(NORTH)
				direction = "north"
			if(SOUTH)
				direction = "south"
			if(EAST)
				direction = "east"
			if(WEST)
				direction = "west"

		if(turn_connects)
			I = image('icons/obj/plumbing/connects.dmi', "[direction]-[color]", layer = AM.layer - 1)

		else
			I = image('icons/obj/plumbing/connects.dmi', "[direction]-[color]-s", layer = AM.layer - 1) //color is not color as in the var, it's just the name of the icon_state
			I.dir = D

		I.pixel_x = duct_x
		I.pixel_y = duct_y

		overlays += I

///we stop acting like a plumbing thing and disconnect if we are, so we can safely be moved and stuff
/datum/component/plumbing/proc/disable()
	SIGNAL_HANDLER

	if(!active)
		return

	STOP_PROCESSING(SSfluids, src)

	for(var/A in ducts)
		var/datum/ductnet/D = ducts[A]
		D.remove_plumber(src)

	active = FALSE

	for(var/D in GLOB.cardinals)
		if(D & (demand_connects | supply_connects))
			for(var/obj/machinery/duct/duct in get_step(parent, D))
				if(duct.duct_layer == ducting_layer)
					duct.remove_connects(turn(D, 180))
					duct.neighbours.Remove(parent)
					duct.update_appearance()

///settle wherever we are, and start behaving like a piece of plumbing
/datum/component/plumbing/proc/enable(obj/object, datum/component/component)
	SIGNAL_HANDLER
	if(active || (component && component != src))
		UnregisterSignal(parent, list(COMSIG_COMPONENT_ADDED))
		return

	update_dir()
	active = TRUE

	var/atom/movable/AM = parent
	for(var/obj/machinery/duct/D in AM.loc) //Destroy any ducts under us. Ducts also self-destruct if placed under a plumbing machine. machines disable when they get moved
		if(D.anchored) //that should cover everything
			D.disconnect_duct()

	if(demand_connects)
		START_PROCESSING(SSfluids, src)

	for(var/D in GLOB.cardinals)

		if(D & (demand_connects | supply_connects))
			for(var/atom/movable/A in get_step(parent, D))

				if(istype(A, /obj/machinery/duct))
					var/obj/machinery/duct/duct = A
					duct.attempt_connect()
				else
					for(var/datum/component/plumbing/plumber as anything in A.GetComponents(/datum/component/plumbing))
						if(plumber.ducting_layer == ducting_layer)
							direct_connect(plumber, D)

/// Toggle our machinery on or off. This is called by a hook from default_unfasten_wrench with anchored as only param, so we dont have to copypaste this on every object that can move
/datum/component/plumbing/proc/toggle_active(obj/O, new_state)
	SIGNAL_HANDLER
	if(new_state)
		enable()
	else
		disable()

/** We update our connects only when we settle down by taking our current and original direction to find our new connects
* If someone wants it to fucking spin while connected to something go actually knock yourself out
*/
/datum/component/plumbing/proc/update_dir()
	if(!turn_connects)
		return

	var/atom/movable/AM = parent
	var/new_demand_connects
	var/new_supply_connects
	var/new_dir = AM.dir
	var/angle = 180 - dir2angle(new_dir)

	if(new_dir == SOUTH)
		demand_connects = initial(demand_connects)
		supply_connects = initial(supply_connects)
	else
		for(var/D in GLOB.cardinals)
			if(D & initial(demand_connects))
				new_demand_connects += turn(D, angle)
			if(D & initial(supply_connects))
				new_supply_connects += turn(D, angle)
		demand_connects = new_demand_connects
		supply_connects = new_supply_connects

///Give the direction of a pipe, and it'll return wich direction it originally was when it's object pointed SOUTH
/datum/component/plumbing/proc/get_original_direction(dir)
	var/atom/movable/AM = parent
	return turn(dir, dir2angle(AM.dir) - 180)

//special case in-case we want to connect directly with another machine without a duct
/datum/component/plumbing/proc/direct_connect(datum/component/plumbing/P, dir)
	if(!P.active)
		return
	var/opposite_dir = turn(dir, 180)
	if(P.demand_connects & opposite_dir && supply_connects & dir || P.supply_connects & opposite_dir && demand_connects & dir) //make sure we arent connecting two supplies or demands
		var/datum/ductnet/net = new()
		net.add_plumber(src, dir)
		net.add_plumber(P, opposite_dir)

/datum/component/plumbing/proc/hide(atom/movable/AM, intact)
	SIGNAL_HANDLER

	tile_covered = intact
	AM.update_appearance()

/datum/component/plumbing/proc/change_ducting_layer(obj/caller, obj/O, new_layer = DUCT_LAYER_DEFAULT)
	SIGNAL_HANDLER
	ducting_layer = new_layer

	if(ismovable(parent))
		var/atom/movable/AM = parent
		AM.update_appearance()

	if(O)
		playsound(O, 'sound/items/ratchet.ogg', 10, TRUE) //sound

	//quickly disconnect and reconnect the network.
	if(active)
		disable()
		enable()

/datum/component/plumbing/proc/set_recipient_reagents_holder(datum/reagents/receiver)
	if(recipient_reagents_holder)
		UnregisterSignal(recipient_reagents_holder, COMSIG_PARENT_QDELETING) //stop tracking whoever we were tracking
	if(receiver)
		RegisterSignal(receiver, COMSIG_PARENT_QDELETING, .proc/handle_reagent_del) //on deletion call a wrapper proc that clears us, and maybe reagents too

	recipient_reagents_holder = receiver

/datum/component/plumbing/proc/handle_reagent_del(datum/source)
	SIGNAL_HANDLER
	if(source == reagents)
		reagents = null
	if(source == recipient_reagents_holder)
		set_recipient_reagents_holder(null)

///has one pipe input that only takes, example is manual output pipe
/datum/component/plumbing/simple_demand
	demand_connects = SOUTH

///has one pipe output that only supplies. example is liquid pump and manual input pipe
/datum/component/plumbing/simple_supply
	supply_connects = SOUTH

///input and output, like a holding tank
/datum/component/plumbing/tank
	demand_connects = WEST
	supply_connects = EAST

/datum/component/plumbing/manifold
	demand_connects = NORTH
	supply_connects = SOUTH

/datum/component/plumbing/manifold/change_ducting_layer(obj/caller, obj/O, new_layer)
	return

#define READY 2
///Baby component for the buffer plumbing machine
/datum/component/plumbing/buffer
	demand_connects = WEST
	supply_connects = EAST

/datum/component/plumbing/buffer/Initialize(start=TRUE, _turn_connects=TRUE, _ducting_layer, datum/reagents/custom_receiver)
	if(!istype(parent, /obj/machinery/plumbing/buffer))
		return COMPONENT_INCOMPATIBLE

	return ..()

/datum/component/plumbing/buffer/can_give(amount, reagent, datum/ductnet/net)
	var/obj/machinery/plumbing/buffer/buffer = parent
	return (buffer.mode == READY) ? ..() : FALSE

#undef READY


code:
/datum/component/plumbing/acclimator
	demand_connects = WEST
	supply_connects = EAST
	var/obj/machinery/plumbing/acclimator/myacclimator

/datum/component/plumbing/acclimator/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
	. = ..()
	if(!istype(parent, /obj/machinery/plumbing/acclimator))
		return COMPONENT_INCOMPATIBLE
	myacclimator = parent

/datum/component/plumbing/acclimator/Destroy(force, silent)
	myacclimator = null
	return ..()

/datum/component/plumbing/acclimator/can_give(amount, reagent)
	. = ..()
	if(. && myacclimator.emptying)
		return TRUE
	return FALSE
///We're overriding process and not send_request, because all process does is do the requests, so we might aswell cut out the middle man and save some code from running
/datum/component/plumbing/acclimator/process()
	if(myacclimator.emptying)
		return
	return ..()


code:
///The magical plumbing component used by the chemical filters. The different supply connects behave differently depending on the filters set on the chemical filter
/datum/component/plumbing/filter
	demand_connects = NORTH
	supply_connects = SOUTH | EAST | WEST //SOUTH is straight, EAST is left and WEST is right. We look from the perspective of the insert

/datum/component/plumbing/filter/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
	. = ..()
	if(!istype(parent, /obj/machinery/plumbing/filter))
		return COMPONENT_INCOMPATIBLE

/datum/component/plumbing/filter/can_give(amount, reagent, datum/ductnet/net)
	. = ..()
	if(.)
		var/direction
		for(var/A in ducts)
			if(ducts[A] == net)
				direction = get_original_direction(text2num(A)) //we need it relative to the direction, so filters don't change when we turn the filter
				break
		if(!direction)
			return FALSE
		if(reagent)
			if(!can_give_in_direction(direction, reagent))
				return FALSE

/datum/component/plumbing/filter/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net)
	if(!reagents || !target || !target.reagents)
		return FALSE
	var/direction
	for(var/A in ducts)
		if(ducts[A] == net)
			direction = get_original_direction(text2num(A))
			break
	if(reagent)
		reagents.trans_id_to(target.parent, reagent, amount)
	else
		for(var/A in reagents.reagent_list)
			var/datum/reagent/R = A
			if(!can_give_in_direction(direction, R.type))
				continue
			var/new_amount
			if(R.volume < amount)
				new_amount = amount - R.volume
			reagents.trans_id_to(target.parent, R.type, amount)
			amount = new_amount
			if(amount <= 0)
				break
///We check if the direction and reagent are valid to give. Needed for filters since different outputs have different behaviours
/datum/component/plumbing/filter/proc/can_give_in_direction(dir, reagent)
	var/obj/machinery/plumbing/filter/F = parent
	switch(dir)
		if(SOUTH) //straight
			if(!F.left.Find(reagent) && !F.right.Find(reagent))
				return TRUE
		if(WEST) //right
			if(F.right.Find(reagent))
				return TRUE
		if(EAST) //left
			if(F.left.Find(reagent))
				return TRUE


code:
/datum/component/plumbing/reaction_chamber
	demand_connects = NORTH
	supply_connects = SOUTH

/datum/component/plumbing/reaction_chamber/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
	. = ..()
	if(!istype(parent, /obj/machinery/plumbing/reaction_chamber))
		return COMPONENT_INCOMPATIBLE

/datum/component/plumbing/reaction_chamber/can_give(amount, reagent, datum/ductnet/net)
	. = ..()
	var/obj/machinery/plumbing/reaction_chamber/reaction_chamber = parent
	if(!. || !reaction_chamber.emptying || reagents.is_reacting == TRUE)
		return FALSE

/datum/component/plumbing/reaction_chamber/send_request(dir)
	var/obj/machinery/plumbing/reaction_chamber/chamber = parent
	if(chamber.emptying)
		return

	for(var/required_reagent in chamber.required_reagents)
		var/has_reagent = FALSE
		for(var/datum/reagent/containg_reagent as anything in reagents.reagent_list)
			if(required_reagent == containg_reagent.type)
				has_reagent = TRUE
				if(containg_reagent.volume + CHEMICAL_QUANTISATION_LEVEL < chamber.required_reagents[required_reagent])
					process_request(min(chamber.required_reagents[required_reagent] - containg_reagent.volume, MACHINE_REAGENT_TRANSFER) , required_reagent, dir)
					return
		if(!has_reagent)
			process_request(min(chamber.required_reagents[required_reagent], MACHINE_REAGENT_TRANSFER), required_reagent, dir)
			return

	reagents.flags &= ~NO_REACT
	reagents.handle_reactions()

	chamber.emptying = TRUE //If we move this up, it'll instantly get turned off since any reaction always sets the reagent_total to zero. Other option is make the reaction update
	//everything for every chemical removed, wich isn't a good option either.
	chamber.on_reagent_change(reagents) //We need to check it now, because some reactions leave nothing left.

///Special connect that we currently use for reaction chambers. Being used so we can keep certain inputs seperate, like into a special internal acid container
/datum/component/plumbing/acidic_input
	demand_connects = WEST
	demand_color = "yellow"

	ducting_layer = SECOND_DUCT_LAYER

/datum/component/plumbing/acidic_input/send_request(dir)
	process_request(amount = MACHINE_REAGENT_TRANSFER, reagent = /datum/reagent/reaction_agent/acidic_buffer, dir = dir)

///Special connect that we currently use for reaction chambers. Being used so we can keep certain inputs seperate, like into a special internal base container
/datum/component/plumbing/alkaline_input
	demand_connects = EAST
	demand_color = "green"

	ducting_layer = FOURTH_DUCT_LAYER

/datum/component/plumbing/alkaline_input/send_request(dir)
	process_request(amount = MACHINE_REAGENT_TRANSFER, reagent = /datum/reagent/reaction_agent/basic_buffer, dir = dir)


code:
/datum/component/plumbing/splitter
	demand_connects = NORTH
	supply_connects = SOUTH | EAST

/datum/component/plumbing/splitter/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
	. = ..()
	if(. && !istype(parent, /obj/machinery/plumbing/splitter))
		return FALSE

/datum/component/plumbing/splitter/can_give(amount, reagent, datum/ductnet/net)
	. = ..()
	if(!.)
		return
	var/direction
	for(var/A in ducts)
		if(ducts[A] == net)
			direction = get_original_direction(text2num(A))
			break
	var/obj/machinery/plumbing/splitter/S = parent
	switch(direction)
		if(SOUTH)
			if(S.turn_straight && S.transfer_straight <= amount)
				S.turn_straight = FALSE
				return TRUE
		if(EAST)
			if(!S.turn_straight && S.transfer_side <= amount)
				S.turn_straight = TRUE
				return TRUE
	return FALSE

/datum/component/plumbing/splitter/transfer_to(datum/component/plumbing/target, amount, reagent, datum/ductnet/net)
	var/direction
	for(var/A in ducts)
		if(ducts[A] == net)
			direction = get_original_direction(text2num(A))
			break
	var/obj/machinery/plumbing/splitter/S = parent
	switch(direction)
		if(SOUTH)
			if(amount >= S.transfer_straight)
				amount = S.transfer_straight
		if(EAST)
			if(amount >= S.transfer_side)
				amount = S.transfer_side
	return ..()


code:
/datum/component/pricetag
	///Payee gets 100% of the value if no ratio has been set.
	var/default_profit_ratio = 1
	///List of bank accounts this pricetag pays out to. Format is payees[bank_account] = profit_ratio.
	var/list/payees = list()

/datum/component/pricetag/Initialize(_owner,_profit_ratio)
	if(!isobj(parent)) //Has to account for both objects and sellable structures like crates.
		return COMPONENT_INCOMPATIBLE

	if(_profit_ratio)
		payees[_owner] = _profit_ratio
	else
		payees[_owner] = default_profit_ratio

	RegisterSignal(parent, list(COMSIG_ITEM_SOLD), .proc/split_profit)
	RegisterSignal(parent, list(COMSIG_STRUCTURE_UNWRAPPED, COMSIG_ITEM_UNWRAPPED), .proc/Unwrapped)
	RegisterSignal(parent, list(COMSIG_ITEM_SPLIT_PROFIT, COMSIG_ITEM_SPLIT_PROFIT_DRY), .proc/return_ratio)

/datum/component/pricetag/proc/Unwrapped()
	SIGNAL_HANDLER

	qdel(src) //Once it leaves it's wrapped container, the object in question should lose it's pricetag component.

/datum/component/pricetag/proc/split_profit(item_value)
	SIGNAL_HANDLER

	var/price = item_value
	if(price)
		for(var/datum/bank_account/payee in payees)
			var/profit_ratio = payees[payee]
			var/adjusted_value = price * profit_ratio
			var/datum/bank_account/bank_account = payee
			bank_account.adjust_money(adjusted_value)
			bank_account.bank_card_talk("Sale of [parent] recorded. [adjusted_value] credits added to account.")
		return TRUE

/datum/component/pricetag/proc/return_ratio()
	SIGNAL_HANDLER
	return default_profit_ratio