code:
/** Component representing acid applied to an object.
 *
 * Must be attached to an atom.
 * Processes, repeatedly damaging whatever it is attached to.
 * If the parent atom is a turf it applies acid to the contents of the turf.
 */
/datum/component/acid
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
	/// The strength of the acid on the parent [/atom].
	var/acid_power
	/// The volume of acid on the parent [/atom].
	var/acid_volume
	/// The maximum volume of acid on the parent [/atom].
	var/max_volume = INFINITY
	/// The ambiant sound of acid eating away at the parent [/atom].
	var/datum/looping_sound/acid/sizzle
	/// Used exclusively for melting turfs. TODO: Move integrity to the atom level so that this can be dealt with there.
	var/parent_integrity = 30
	/// How far the acid melting of turfs has progressed
	var/stage = 0
	/// The proc used to handle the parent [/atom] when processing. TODO: Unify damage and resistance flags so that this doesn't need to exist!
	var/datum/callback/process_effect

/datum/component/acid/Initialize(_acid_power, _acid_volume, _max_volume=null)
	if((_acid_power) <= 0 || (_acid_volume <= 0))
		stack_trace("Acid component added with insufficient acid power ([_acid_power]) or acid volume ([_acid_power]).")
		return COMPONENT_INCOMPATIBLE // Not enough acid or the acid's too weak, either one.
	if(!isatom(parent))
		stack_trace("Acid component added to [parent] ([parent?.type]) which is not a /atom subtype.")
		return COMPONENT_INCOMPATIBLE // Incompatible type. TODO: Rework take_damage to the atom level and move this there.

	if(isobj(parent))
		var/obj/parent_object = parent
		if(parent_object.resistance_flags & UNACIDABLE) // The parent object cannot have acid. Should never happen, will happen.
			stack_trace("Acid component added to unacidable object [parent].")
			return COMPONENT_INCOMPATIBLE

		max_volume = OBJ_ACID_VOLUME_MAX
		process_effect = CALLBACK(src, .proc/process_obj, parent)
	else if(isliving(parent))
		max_volume = MOB_ACID_VOLUME_MAX
		process_effect = CALLBACK(src, .proc/process_mob, parent)
	else if(isturf(parent))
		max_volume = TURF_ACID_VOLUME_MAX
		process_effect = CALLBACK(src, .proc/process_turf, parent)

	acid_power = _acid_power
	set_volume(_acid_volume)

	var/atom/parent_atom = parent
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, .proc/on_update_overlays)
	parent_atom.update_appearance()
	sizzle = new(parent, TRUE)
	START_PROCESSING(SSacid, src)

/datum/component/acid/Destroy(force, silent)
	STOP_PROCESSING(SSacid, src)
	QDEL_NULL(sizzle)
	if(process_effect)
		QDEL_NULL(process_effect)
	UnregisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS)
	if(parent && !QDELING(parent))
		var/atom/parent_atom = parent
		parent_atom.update_appearance()
	return ..()

/datum/component/acid/RegisterWithParent()
	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
	RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, .proc/on_clean)
	RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand)
	RegisterSignal(parent, COMSIG_ATOM_EXPOSE_REAGENT, .proc/on_expose_reagent)
	if(isturf(parent))
		RegisterSignal(parent, COMSIG_ATOM_ENTERED, .proc/on_entered)

/datum/component/acid/UnregisterFromParent()
	UnregisterSignal(parent, list(
		COMSIG_PARENT_EXAMINE,
		COMSIG_COMPONENT_CLEAN_ACT,
		COMSIG_ATOM_ATTACK_HAND,
		COMSIG_ATOM_EXPOSE_REAGENT))

	if(isturf(parent))
		UnregisterSignal(parent, COMSIG_ATOM_ENTERED)

/// Averages corrosive power and sums volume.
/datum/component/acid/InheritComponent(datum/component/C, i_am_original, _acid_power, _acid_volume)
	acid_power = ((acid_power * acid_volume) + (_acid_power * _acid_volume)) / (acid_volume + _acid_volume)
	set_volume(acid_volume + _acid_volume)

/// Sets the acid volume to a new value. Limits the acid volume by the amount allowed to exist on the parent atom.
/datum/component/acid/proc/set_volume(new_volume)
	acid_volume = clamp(new_volume, 0, max_volume)
	if(!acid_volume)
		qdel(src)

/// Handles the slow corrosion of the parent [/atom].
/datum/component/acid/process(delta_time)
	process_effect?.InvokeAsync(delta_time)
	if(QDELING(src)) //The process effect deals damage, and on turfs diminishes the acid volume, potentially destroying the component. Let's not destroy it twice.
		return
	set_volume(acid_volume - (ACID_DECAY_BASE + (ACID_DECAY_SCALING*round(sqrt(acid_volume)))) * delta_time)

/// Handles processing on a [/obj].
/datum/component/acid/proc/process_obj(obj/target, delta_time)
	if(target.resistance_flags & ACID_PROOF)
		return
	target.take_damage(min(1 + round(sqrt(acid_power * acid_volume)*0.3), OBJ_ACID_DAMAGE_MAX) * delta_time, BURN, ACID, 0)

/// Handles processing on a [/mob/living].
/datum/component/acid/proc/process_mob(mob/living/target, delta_time)
	target.acid_act(acid_power, acid_volume * delta_time)

/// Handles processing on a [/turf].
/datum/component/acid/proc/process_turf(turf/target_turf, delta_time)
	var/acid_used = min(acid_volume * 0.05, 20) * delta_time
	var/applied_targets = 0
	for(var/am in target_turf)
		var/atom/movable/target_movable = am
		if(target_movable.acid_act(acid_power, acid_used))
			applied_targets++

	if(applied_targets)
		set_volume(acid_volume - (acid_used * applied_targets))

	// Snowflake code for handling acid melting walls. TODO: Move integrity handling to the atom level so this can be desnowflaked.
	if(acid_power < ACID_POWER_MELT_TURF)
		return

	parent_integrity -= delta_time
	if(parent_integrity <= 0)
		target_turf.visible_message(span_warning("[target_turf] collapses under its own weight into a puddle of goop and undigested debris!"))
		target_turf.acid_melt()
	else if(parent_integrity <= 4 && stage <= 3)
		target_turf.visible_message(span_warning("[target_turf] begins to crumble under the acid!"))
		stage = 4
	else if(parent_integrity <= 8 && stage <= 2)
		target_turf.visible_message(span_warning("[target_turf] is struggling to withstand the acid!"))
		stage = 3
	else if(parent_integrity <= 16 && stage <= 1)
		target_turf.visible_message(span_warning("[target_turf] is being melted by the acid!"))
		stage = 2
	else if(parent_integrity <= 24 && stage == 0)
		target_turf.visible_message(span_warning("[target_turf] is holding up against the acid!"))
		stage = 1

/// Used to maintain the acid overlay on the parent [/atom].
/datum/component/acid/proc/on_update_overlays(atom/parent_atom, list/overlays)
	SIGNAL_HANDLER

	overlays += mutable_appearance('icons/effects/acid.dmi', parent_atom.custom_acid_overlay || ACID_OVERLAY_DEFAULT)

/// Alerts any examiners to the acid on the parent atom.
/datum/component/acid/proc/on_examine(atom/A, mob/user, list/examine_list)
	SIGNAL_HANDLER

	examine_list += span_danger("[A.p_theyre()] covered in corrosive liquid!")

/// Makes it possible to clean acid off of objects.
/datum/component/acid/proc/on_clean(atom/A, clean_types)
	SIGNAL_HANDLER

	if(!(clean_types & CLEAN_TYPE_ACID))
		return NONE
	qdel(src)
	return COMPONENT_CLEANED

/// Handles water diluting the acid on the object.
/datum/component/acid/proc/on_expose_reagent(atom/parent_atom, datum/reagent/exposing_reagent, reac_volume)
	SIGNAL_HANDLER

	if(!istype(exposing_reagent, /datum/reagent/water))
		return NONE

	acid_power /= (acid_volume / (acid_volume + reac_volume))
	set_volume(acid_volume + reac_volume)
	return NONE

/// Handles searing the hand of anyone who tries to touch this without protection.
/datum/component/acid/proc/on_attack_hand(atom/parent_atom, mob/living/carbon/user)
	SIGNAL_HANDLER

	if(!istype(user))
		return NONE
	if((parent_atom == user) || (parent_atom.loc == user))
		return NONE // So people can take their own clothes off.
	if((acid_power * acid_volume) < ACID_LEVEL_HANDBURN)
		return NONE
	if(user.gloves?.resistance_flags & (UNACIDABLE|ACID_PROOF))
		return NONE

	var/obj/item/bodypart/affecting = user.get_bodypart("[(user.active_hand_index % 2 == 0) ? "r" : "l" ]_arm")
	if(!affecting?.receive_damage(0, 5))
		return NONE

	to_chat(user, span_warning("The acid on \the [parent_atom] burns your hand!"))
	playsound(parent_atom, 'sound/weapons/sear.ogg', 50, TRUE)
	user.update_damage_overlays()
	return COMPONENT_CANCEL_ATTACK_CHAIN

/// Handles searing the feet of whoever walks over this without protection. Only active if the parent is a turf.
/datum/component/acid/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
	SIGNAL_HANDLER

	if(!isliving(arrived))
		return
	var/mob/living/crosser = arrived
	if(crosser.movement_type & FLYING)
		return
	if(crosser.m_intent == MOVE_INTENT_WALK)
		return
	if(prob(60))
		return

	var/acid_used = min(acid_volume * 0.05, 20)
	if(crosser.acid_act(acid_power, acid_used, FEET))
		playsound(crosser, 'sound/weapons/sear.ogg', 50, TRUE)
		to_chat(crosser, span_userdanger("The acid on the [parent] burns you!"))
		set_volume(max(acid_volume - acid_used, 10))


code:
/// Applied to clients when they receive an admin popup, alerting them to
/// their ticket.
/datum/component/admin_popup
	/// The user's most active ticket. If this is resolved, closed, or replied to,
	/// then the component will delete itself.
	var/datum/admin_help/ticket

	var/atom/movable/screen/admin_popup/admin_popup

/datum/component/admin_popup/Initialize(datum/admin_help/ticket)
	if (!istype(parent, /client))
		return COMPONENT_INCOMPATIBLE

	if (!istype(ticket))
		return COMPONENT_INCOMPATIBLE

	create_notice()

	RegisterSignal(
		ticket,
		list(
			COMSIG_ADMIN_HELP_MADE_INACTIVE,
			COMSIG_ADMIN_HELP_REPLIED,
			COMSIG_PARENT_QDELETING,
		),
		.proc/delete_self,
	)

/datum/component/admin_popup/Destroy(force, silent)
	var/client/parent_client = parent

	parent_client?.screen -= admin_popup
	QDEL_NULL(admin_popup)

	if (!QDELETED(ticket))
		UnregisterSignal(ticket, list(
			COMSIG_ADMIN_HELP_MADE_INACTIVE,
			COMSIG_ADMIN_HELP_REPLIED,
			COMSIG_PARENT_QDELETING,
		))

		ticket = null

	return ..()

/datum/component/admin_popup/proc/create_notice()
	if(admin_popup)
		qdel(admin_popup)
	admin_popup = new

	var/client/parent_client = parent
	admin_popup.maptext_width = getviewsize(parent_client.view_size.getView())[1] * world.icon_size
	parent_client.screen += admin_popup

/datum/component/admin_popup/proc/delete_self()
	SIGNAL_HANDLER
	qdel(src)

/// The UI element for admin popups
/atom/movable/screen/admin_popup
	icon = null
	icon_state = null
	plane = ABOVE_HUD_PLANE
	layer = ADMIN_POPUP_LAYER
	mouse_opacity = MOUSE_OPACITY_TRANSPARENT
	screen_loc = "TOP-5,LEFT"
	maptext_height = 480
	maptext_width = 480
	maptext = ""

	var/static/list/colors = list(
		COLOR_RED,
		COLOR_ORANGE,
		COLOR_YELLOW,
		COLOR_LIME,
		COLOR_CYAN,
		COLOR_PURPLE,
	)

	/// The last color chosen in the animation, sourced from the static list colors.
	var/last_color_index = 0

	/// The `world.time` when the last color update occurred.
	var/last_update_time = 0

/atom/movable/screen/admin_popup/Initialize(mapload, ...)
	. = ..()

	START_PROCESSING(SSobj, src)
	update_text()

/atom/movable/screen/admin_popup/Destroy()
	STOP_PROCESSING(SSobj, src)
	return ..()

/atom/movable/screen/admin_popup/process(delta_time)
	update_text()

/atom/movable/screen/admin_popup/proc/update_text()
	// Even if processing time changes, we want this to remain slow.
	// We want to pester them into reading their ticket, not give them a seizure!
	if (world.time - last_update_time < 2 SECONDS)
		return

	last_color_index = (last_color_index % colors.len) + 1

	var/message = ""
	message += "HEY! An admin is trying to talk to you!
Check your chat window, and click their name to respond!"
	message += ""

	maptext = MAPTEXT(message)
	last_update_time = world.time

/// Tries to give the target an admin popup.
/// If it fails, will send the error to the passed admin.
/proc/give_admin_popup(client/target, client/admin, message)
	log_admin("[key_name(admin)] sent an admin popup to [key_name(target)].")

	var/datum/admin_help/current_ticket = target.current_ticket
	if (!current_ticket)
		to_chat(admin, span_warning("[key_name(target)] had no active ahelp, aborting."))
		return

	admin.cmd_admin_pm(target, message)
	target.AddComponent(/datum/component/admin_popup, current_ticket)

repo_name: AnalWerewolf/sundown

license: mit


code:
/datum/component/anti_magic
	var/magic = FALSE
	var/holy = FALSE
	var/psychic = FALSE
	var/allowed_slots = ~ITEM_SLOT_BACKPACK
	var/charges = INFINITY
	var/blocks_self = TRUE
	var/datum/callback/reaction
	var/datum/callback/expire

/datum/component/anti_magic/Initialize(_magic = FALSE, _holy = FALSE, _psychic = FALSE, _allowed_slots, _charges, _blocks_self = TRUE, datum/callback/_reaction, datum/callback/_expire)
	if(isitem(parent))
		RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
		RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/on_drop)
	else if(ismob(parent))
		RegisterSignal(parent, COMSIG_MOB_RECEIVE_MAGIC, .proc/protect)
	else
		return COMPONENT_INCOMPATIBLE

	magic = _magic
	holy = _holy
	psychic = _psychic
	if(_allowed_slots)
		allowed_slots = _allowed_slots
	if(!isnull(_charges))
		charges = _charges
	blocks_self = _blocks_self
	reaction = _reaction
	expire = _expire

/datum/component/anti_magic/proc/on_equip(datum/source, mob/equipper, slot)
	SIGNAL_HANDLER

	if(!(allowed_slots & slot)) //Check that the slot is valid for antimagic
		UnregisterSignal(equipper, COMSIG_MOB_RECEIVE_MAGIC)
		return
	RegisterSignal(equipper, COMSIG_MOB_RECEIVE_MAGIC, .proc/protect, TRUE)

/datum/component/anti_magic/proc/on_drop(datum/source, mob/user)
	SIGNAL_HANDLER

	UnregisterSignal(user, COMSIG_MOB_RECEIVE_MAGIC)

/datum/component/anti_magic/proc/protect(datum/source, mob/user, _magic, _holy, _psychic, chargecost, self, list/protection_sources)
	SIGNAL_HANDLER

	if(((_magic && magic) || (_holy && holy) || (_psychic && psychic)) && (!self || blocks_self))
		protection_sources += parent
		reaction?.Invoke(user, chargecost, parent)
		charges -= chargecost
		if(charges <= 0)
			expire?.Invoke(user, parent)
			qdel(src)
		return COMPONENT_BLOCK_MAGIC


code:
/// Allows movables to be inserted/displayed in aquariums.
/datum/component/aquarium_content
	/// This is a datum that describes our in-aquarium functionality
	var/datum/aquarium_behaviour/properties

	/// Keeps track of our current aquarium.
	var/obj/structure/aquarium/current_aquarium

	//This is visual effect holder that will end up in aquarium's vis_contents
	var/obj/effect/vc_obj

	/// Base px offset of the visual object in current aquarium aka current base position
	var/base_px = 0
	/// Base px offset of the visual object in current aquarium aka current base position
	var/base_py = 0
	//Current layer for the visual object
	var/base_layer

	//If flopping animation was applied to parent, tracked to stop it on removal/destroy
	var/flopping = FALSE

/datum/component/aquarium_content/Initialize(property_type)
	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE
	if(ispath(property_type, /datum/aquarium_behaviour))
		properties = new property_type
	else
		CRASH("Invalid property type provided for aquarium content component")
	properties.parent = src

	ADD_TRAIT(parent, TRAIT_FISH_CASE_COMPATIBILE, src)
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/enter_aquarium)

	//If component is added to something already in aquarium at the time initialize it properly.
	var/atom/movable/movable_parent = parent
	if(istype(movable_parent.loc, /obj/structure/aquarium))
		on_inserted(movable_parent.loc)

/datum/component/aquarium_content/PreTransfer()
	. = ..()
	REMOVE_TRAIT(parent, TRAIT_FISH_CASE_COMPATIBILE, src)

/datum/component/aquarium_content/Destroy(force, silent)
	if(current_aquarium)
		remove_from_aquarium()
	stop_flopping()
	QDEL_NULL(vc_obj)
	QDEL_NULL(properties)
	return ..()

/datum/component/aquarium_content/proc/enter_aquarium(datum/source, OldLoc, Dir, Forced)
	SIGNAL_HANDLER
	var/atom/movable/movable_parent = parent
	if(istype(movable_parent.loc, /obj/structure/aquarium))
		on_inserted(movable_parent.loc)
	if(HAS_TRAIT(movable_parent.loc, TRAIT_FISH_SAFE_STORAGE))
		on_tank_stasis()

/datum/component/aquarium_content/proc/is_ready_to_insert(obj/structure/aquarium/aquarium)
	//This is kinda awful but we're unaware of other fish
	if(properties.unique)
		for(var/atom/movable/fish_or_prop in aquarium)
			if(fish_or_prop == parent)
				continue
			var/datum/component/aquarium_content/other_content = fish_or_prop.GetComponent(/datum/component/aquarium_content)
			if(other_content && other_content.properties.type == properties.type)
				return FALSE
	return TRUE

/datum/component/aquarium_content/proc/on_inserted(atom/aquarium)
	current_aquarium = aquarium
	RegisterSignal(current_aquarium, COMSIG_ATOM_EXITED, .proc/on_removed)
	RegisterSignal(current_aquarium, COMSIG_AQUARIUM_SURFACE_CHANGED, .proc/on_surface_changed)
	RegisterSignal(current_aquarium, COMSIG_AQUARIUM_FLUID_CHANGED,.proc/on_fluid_changed)
	RegisterSignal(current_aquarium, COMSIG_PARENT_ATTACKBY, .proc/attack_reaction)
	properties.on_inserted()

	//If we don't have vc object yet build it
	if(!vc_obj)
		vc_obj = generate_base_vc()

	//Set default position and layer
	set_vc_base_position()
	generate_animation()

	//Finally add it to to objects vis_contents
	current_aquarium.vis_contents |= vc_obj

/// Aquarium surface changed in some way, we need to recalculate base position and aninmation
/datum/component/aquarium_content/proc/on_surface_changed()
	SIGNAL_HANDLER
	set_vc_base_position()
	generate_animation() //our animation start point changed, gotta redo

/// Our aquarium is hit with stuff
/datum/component/aquarium_content/proc/attack_reaction(datum/source, obj/item/thing, mob/user, params)
	SIGNAL_HANDLER
	if(istype(thing, /obj/item/fish_feed))
		properties.on_feeding(thing.reagents)
		return COMPONENT_NO_AFTERATTACK
	else
		//stirred effect
		generate_animation()

/datum/component/aquarium_content/proc/on_fluid_changed()
	SIGNAL_HANDLER
	properties.on_fluid_changed()

/datum/component/aquarium_content/proc/remove_visual_from_aquarium()
	current_aquarium.vis_contents -= vc_obj
	if(base_layer)
		current_aquarium.free_layer(base_layer)

/// Generates common visual object, propeties that don't depend on aquarium surface
/datum/component/aquarium_content/proc/generate_base_vc()
	if(!properties)
		CRASH("Generating visual without properties.")

	var/obj/effect/visual = new
	properties.apply_appearance(visual)
	visual.vis_flags |= VIS_INHERIT_ID | VIS_INHERIT_PLANE //plane so it shows properly in containers on inventory ui for handheld cases
	return visual

/// Actually animates the vc holder
/datum/component/aquarium_content/proc/generate_animation()
	switch(properties.current_animation)
		if(AQUARIUM_ANIMATION_FISH_SWIM)
			swim_animation()
			return
		if(AQUARIUM_ANIMATION_FISH_DEAD)
			dead_animation()
			return

/// Create looping random path animation, pixel offsets parameters include offsets already
/datum/component/aquarium_content/proc/swim_animation()
	var/avg_width = round(properties.sprite_width / 2)
	var/avg_height = round(properties.sprite_height / 2)

	var/list/aq_properties = current_aquarium.get_surface_properties()
	var/px_min = aq_properties[AQUARIUM_PROPERTIES_PX_MIN] + avg_width - 16
	var/px_max = aq_properties[AQUARIUM_PROPERTIES_PX_MAX] - avg_width - 16
	var/py_min = aq_properties[AQUARIUM_PROPERTIES_PY_MIN] + avg_height - 16
	var/py_max = aq_properties[AQUARIUM_PROPERTIES_PY_MAX] - avg_width - 16

	var/origin_x = base_px
	var/origin_y = base_py
	var/prev_x = origin_x
	var/prev_y = origin_y
	animate(vc_obj, pixel_x = origin_x, time = 0, loop = -1) //Just to start the animation
	var/move_number = rand(3, 5) //maybe unhardcode this
	for(var/i in 1 to move_number)
		//If it's last movement, move back to start otherwise move to some random point
		var/target_x = i == move_number ? origin_x : rand(px_min,px_max) //could do with enforcing minimal delta for prettier zigzags
		var/target_y = i == move_number ? origin_y : rand(py_min,py_max)
		var/dx = prev_x - target_x
		var/dy = prev_y - target_y
		prev_x = target_x
		prev_y = target_y
		var/dist = abs(dx) + abs(dy)
		var/eyeballed_time = dist * 2 //2ds per px
		//Face the direction we're going
		var/matrix/dir_mx = properties.base_transform()
		if(dx <= 0) //assuming default sprite is facing left here
			dir_mx.Scale(-1, 1)
		animate(transform = dir_mx, time = 0, loop = -1)
		animate(pixel_x = target_x, pixel_y = target_y, time = eyeballed_time, loop = -1)

/datum/component/aquarium_content/proc/dead_animation()
	//Set base_py to lowest possible value
	var/avg_height = round(properties.sprite_height / 2)
	var/list/aq_properties = current_aquarium.get_surface_properties()
	var/py_min = aq_properties[AQUARIUM_PROPERTIES_PY_MIN] + avg_height - 16
	base_py = py_min
	animate(vc_obj, pixel_y = py_min, time = 1) //flop to bottom and end current animation.

#define PAUSE_BETWEEN_PHASES 15
#define PAUSE_BETWEEN_FLOPS 2
#define FLOP_COUNT 2
#define FLOP_DEGREE 20
#define FLOP_SINGLE_MOVE_TIME 1.5
#define JUMP_X_DISTANCE 5
#define JUMP_Y_DISTANCE 6
/// This animation should be applied to actual parent atom instead of vc_object.
/proc/flop_animation(atom/movable/animation_target)
	var/pause_between = PAUSE_BETWEEN_PHASES + rand(1, 5) //randomized a bit so fish are not in sync
	animate(animation_target, time = pause_between, loop = -1)
	//move nose down and up
	for(var/_ in 1 to FLOP_COUNT)
		var/matrix/up_matrix = matrix()
		up_matrix.Turn(FLOP_DEGREE)
		var/matrix/down_matrix = matrix()
		down_matrix.Turn(-FLOP_DEGREE)
		animate(transform = down_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
		animate(transform = up_matrix, time = FLOP_SINGLE_MOVE_TIME, loop = -1)
		animate(transform = matrix(), time = FLOP_SINGLE_MOVE_TIME, loop = -1, easing = BOUNCE_EASING | EASE_IN)
		animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
	//bounce up and down
	animate(time = pause_between, loop = -1, flags = ANIMATION_PARALLEL)
	var/jumping_right = FALSE
	var/up_time = 3 * FLOP_SINGLE_MOVE_TIME / 2
	for(var/_ in 1 to FLOP_COUNT)
		jumping_right = !jumping_right
		var/x_step = jumping_right ? JUMP_X_DISTANCE/2 : -JUMP_X_DISTANCE/2
		animate(time = up_time, pixel_y = JUMP_Y_DISTANCE , pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_IN)
		animate(time = up_time, pixel_y = -JUMP_Y_DISTANCE, pixel_x=x_step, loop = -1, flags= ANIMATION_RELATIVE, easing = BOUNCE_EASING | EASE_OUT)
		animate(time = PAUSE_BETWEEN_FLOPS, loop = -1)
#undef PAUSE_BETWEEN_PHASES
#undef PAUSE_BETWEEN_FLOPS
#undef FLOP_COUNT
#undef FLOP_DEGREE
#undef FLOP_SINGLE_MOVE_TIME
#undef JUMP_X_DISTANCE
#undef JUMP_Y_DISTANCE

/// Starts flopping animation
/datum/component/aquarium_content/proc/start_flopping()
	if(!flopping && istype(parent,/obj/item/fish)) //Requires update_transform/animate_wrappers to be less restrictive.
		flopping = TRUE
		flop_animation(parent)

/// Stops flopping animation
/datum/component/aquarium_content/proc/stop_flopping()
	if(flopping)
		flopping = FALSE
		animate(parent, transform = matrix()) //stop animation

/datum/component/aquarium_content/proc/set_vc_base_position()
	var/list/aq_properties = current_aquarium.get_surface_properties()
	if(properties.randomize_position)
		var/avg_width = round(properties.sprite_width / 2)
		var/avg_height = round(properties.sprite_height / 2)
		var/px_min = aq_properties[AQUARIUM_PROPERTIES_PX_MIN] + avg_width - 16
		var/px_max = aq_properties[AQUARIUM_PROPERTIES_PX_MAX] - avg_width - 16
		var/py_min = aq_properties[AQUARIUM_PROPERTIES_PY_MIN] + avg_height - 16
		var/py_max = aq_properties[AQUARIUM_PROPERTIES_PY_MAX] - avg_width - 16

		base_px = rand(px_min,px_max)
		base_py = rand(py_min,py_max)

		vc_obj.pixel_x = base_px
		vc_obj.pixel_y = base_py

	if(base_layer)
		current_aquarium.free_layer(base_layer)
	base_layer = current_aquarium.request_layer(properties.layer_mode)
	vc_obj.layer = base_layer

/datum/component/aquarium_content/proc/on_removed(datum/source, atom/movable/gone, direction)
	SIGNAL_HANDLER
	if(parent != gone)
		return
	remove_from_aquarium()

/datum/component/aquarium_content/proc/remove_from_aquarium()
	properties.before_removal()
	UnregisterSignal(current_aquarium, list(COMSIG_AQUARIUM_SURFACE_CHANGED, COMSIG_AQUARIUM_FLUID_CHANGED, COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_EXITED))
	remove_visual_from_aquarium()
	current_aquarium = null

/datum/component/aquarium_content/proc/on_tank_stasis()
	// Stop processing until inserted into aquarium again.
	stop_flopping()
	STOP_PROCESSING(SSobj, properties)


code:
///Allows you to set a theme for a set of areas without tying them to looping sounds explicitly
/datum/component/area_sound_manager
	///area -> looping sound type
	var/list/area_to_looping_type = list()
	///Current sound loop
	var/datum/looping_sound/our_loop
	///A list of "acceptable" z levels to be on. If you leave this, we're gonna delete ourselves
	var/list/accepted_zs
	///The timer id of our current start delay, if it exists
	var/timerid

/datum/component/area_sound_manager/Initialize(area_loop_pairs, change_on, remove_on, acceptable_zs)
	if(!ismovable(parent))
		return
	area_to_looping_type = area_loop_pairs
	accepted_zs = acceptable_zs
	change_the_track()

	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/react_to_move)
	RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, .proc/react_to_z_move)
	RegisterSignal(parent, change_on, .proc/handle_change)
	RegisterSignal(parent, remove_on, .proc/handle_removal)

/datum/component/area_sound_manager/Destroy(force, silent)
	QDEL_NULL(our_loop)
	. = ..()

/datum/component/area_sound_manager/proc/react_to_move(datum/source, atom/oldloc, dir, forced)
	SIGNAL_HANDLER
	var/list/loop_lookup = area_to_looping_type
	if(loop_lookup[get_area(oldloc)] == loop_lookup[get_area(parent)])
		return
	change_the_track(TRUE)

/datum/component/area_sound_manager/proc/react_to_z_move(datum/source, old_z, new_z)
	SIGNAL_HANDLER
	if(!length(accepted_zs) || (new_z in accepted_zs))
		return
	qdel(src)

/datum/component/area_sound_manager/proc/handle_removal(datum/source)
	SIGNAL_HANDLER
	qdel(src)

/datum/component/area_sound_manager/proc/handle_change(datum/source)
	SIGNAL_HANDLER
	change_the_track()

/datum/component/area_sound_manager/proc/change_the_track(skip_start = FALSE)
	var/time_remaining = 0

	if(our_loop)
		var/our_id = our_loop.timerid || timerid
		if(our_id)
			time_remaining = timeleft(our_id, SSsound_loops) || 0
			//Time left will sometimes return negative values, just ignore them and start a new sound loop now
			time_remaining = max(time_remaining, 0)
		QDEL_NULL(our_loop)

	var/area/our_area = get_area(parent)
	var/new_loop_type = area_to_looping_type[our_area]
	if(!new_loop_type)
		return

	our_loop = new new_loop_type(parent, FALSE, TRUE, skip_start)

	//If we're still playing, wait a bit before changing the sound so we don't double up
	if(time_remaining)
		timerid = addtimer(CALLBACK(src, .proc/start_looping_sound), time_remaining, TIMER_UNIQUE | TIMER_CLIENT_TIME | TIMER_STOPPABLE | TIMER_NO_HASH_WAIT | TIMER_DELETE_ME, SSsound_loops)
		return
	timerid = null
	our_loop.start()

/datum/component/area_sound_manager/proc/start_looping_sound()
	timerid = null
	if(our_loop)
		our_loop.start()


code:
/// Movables with this component will automatically return to their original turf if moved outside their initial area
/datum/component/areabound
	var/area/bound_area
	var/turf/reset_turf
	var/datum/movement_detector/move_tracker
	var/moving = FALSE //Used to prevent infinite recursion if your reset turf places you somewhere on enter or something

/datum/component/areabound/Initialize()
	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE
	bound_area = get_area(parent)
	reset_turf = get_turf(parent)
	move_tracker = new(parent,CALLBACK(src,.proc/check_bounds))

/datum/component/areabound/proc/check_bounds()
	var/atom/movable/AM = parent
	var/area/current = get_area(AM)
	if(current != bound_area)
		if(!reset_turf || reset_turf.loc != bound_area)
			stack_trace("Invalid areabound configuration") //qdel(src)
			return
		if(moving)
			stack_trace("Moved during a reset move, giving up to prevent infinite recursion. Turf: [reset_turf.type] at [reset_turf.x], [reset_turf.y], [reset_turf.z]")
			return
		moving = TRUE
		AM.forceMove(reset_turf)
		moving = FALSE

/datum/component/areabound/Destroy(force, silent)
	QDEL_NULL(move_tracker)
	. = ..()


code:
/datum/component/armor_plate
	var/amount = 0
	var/maxamount = 3
	var/upgrade_item = /obj/item/stack/sheet/animalhide/goliath_hide
	var/datum/armor/added_armor = list(MELEE = 10)
	var/upgrade_name

/datum/component/armor_plate/Initialize(_maxamount,obj/item/_upgrade_item,datum/armor/_added_armor)
	if(!isobj(parent))
		return COMPONENT_INCOMPATIBLE

	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/examine)
	RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/applyplate)
	RegisterSignal(parent, COMSIG_PARENT_PREQDELETED, .proc/dropplates)
	if(istype(parent, /obj/vehicle/sealed/mecha/working/ripley))
		RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, .proc/apply_mech_overlays)

	if(_maxamount)
		maxamount = _maxamount
	if(_upgrade_item)
		upgrade_item = _upgrade_item
	if(_added_armor)
		if(islist(_added_armor))
			added_armor = getArmor(arglist(_added_armor))
		else if (istype(_added_armor, /datum/armor))
			added_armor = _added_armor
		else
			stack_trace("Invalid type [_added_armor.type] passed as _armor_item argument to armorplate component")
	else
		added_armor = getArmor(arglist(added_armor))
	var/obj/item/typecast = upgrade_item
	upgrade_name = initial(typecast.name)

/datum/component/armor_plate/proc/examine(datum/source, mob/user, list/examine_list)
	SIGNAL_HANDLER

	//upgrade_item could also be typecast here instead
	if(ismecha(parent))
		if(amount)
			if(amount < maxamount)
				examine_list += span_notice("Its armor is enhanced with [amount] [upgrade_name].")
			else
				examine_list += span_notice("It's wearing a fearsome carapace entirely composed of [upgrade_name] - its pilot must be an experienced monster hunter.")
		else
			examine_list += span_notice("It has attachment points for strapping monster hide on for added protection.")
	else
		if(amount)
			examine_list += span_notice("It has been strengthened with [amount]/[maxamount] [upgrade_name].")
		else
			examine_list += span_notice("It can be strengthened with up to [maxamount] [upgrade_name].")

/datum/component/armor_plate/proc/applyplate(datum/source, obj/item/I, mob/user, params)
	SIGNAL_HANDLER

	if(!istype(I,upgrade_item))
		return
	if(amount >= maxamount)
		to_chat(user, span_warning("You can't improve [parent] any further!"))
		return

	if(istype(I,/obj/item/stack))
		I.use(1)
	else
		if(length(I.contents))
			to_chat(user, span_warning("[I] cannot be used for armoring while there's something inside!"))
			return
		qdel(I)

	var/obj/O = parent
	amount++
	O.armor = O.armor.attachArmor(added_armor)

	if(ismecha(O))
		var/obj/vehicle/sealed/mecha/R = O
		R.update_appearance()
		to_chat(user, span_info("You strengthen [R], improving its resistance against melee, bullet and laser damage."))
	else
		SEND_SIGNAL(O, COMSIG_ARMOR_PLATED, amount, maxamount)
		to_chat(user, span_info("You strengthen [O], improving its resistance against melee attacks."))

/datum/component/armor_plate/proc/dropplates(datum/source, force)
	SIGNAL_HANDLER

	if(ismecha(parent)) //items didn't drop the plates before and it causes erroneous behavior for the time being with collapsible helmets
		for(var/i in 1 to amount)
			new upgrade_item(get_turf(parent))

/datum/component/armor_plate/proc/apply_mech_overlays(obj/vehicle/sealed/mecha/mech, list/overlays)
	SIGNAL_HANDLER

	if(amount)
		var/overlay_string = "ripley-g"
		if(amount >= 3)
			overlay_string += "-full"
		if(!LAZYLEN(mech.occupants))
			overlay_string += "-open"
		overlays += overlay_string


code:
///This component indicates this object can be baked in an oven.
/datum/component/bakeable
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS // So you can change bake results with various cookstuffs
	///Result atom type of baking this object
	var/atom/bake_result
	///Amount of time required to bake the food
	var/required_bake_time = 2 MINUTES
	///Is this a positive bake result?
	var/positive_result = TRUE

	///Time spent baking so far
	var/current_bake_time = 0

/datum/component/bakeable/Initialize(bake_result, required_bake_time, positive_result, use_large_steam_sprite)
	. = ..()
	if(!isitem(parent)) //Only items support baking at the moment
		return COMPONENT_INCOMPATIBLE

	src.bake_result = bake_result
	src.required_bake_time = required_bake_time
	src.positive_result = positive_result

// Inherit the new values passed to the component
/datum/component/bakeable/InheritComponent(datum/component/bakeable/new_comp, original, bake_result, required_bake_time, positive_result, use_large_steam_sprite)
	if(!original)
		return
	if(bake_result)
		src.bake_result = bake_result
	if(required_bake_time)
		src.required_bake_time = required_bake_time
	if(positive_result)
		src.positive_result = positive_result

/datum/component/bakeable/RegisterWithParent()
	RegisterSignal(parent, COMSIG_ITEM_BAKED, .proc/OnBake)
	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/OnExamine)

/datum/component/bakeable/UnregisterFromParent()
	. = ..()
	UnregisterSignal(parent, list(COMSIG_ITEM_BAKED, COMSIG_PARENT_EXAMINE))

///Ran every time an item is baked by something
/datum/component/bakeable/proc/OnBake(datum/source, atom/used_oven, delta_time = 1)
	SIGNAL_HANDLER

	. = COMPONENT_HANDLED_BAKING

	. |= positive_result ? COMPONENT_BAKING_GOOD_RESULT : COMPONENT_BAKING_BAD_RESULT //Are we baking shit or great food?

	current_bake_time += delta_time * 10 //turn it into ds
	if(current_bake_time >= required_bake_time)
		FinishBaking(used_oven)

///Ran when an object finished baking
/datum/component/bakeable/proc/FinishBaking(atom/used_oven)

	var/atom/original_object = parent
	var/obj/item/plate/oven_tray/used_tray = original_object.loc
	var/atom/baked_result = new bake_result(used_tray)

	if(original_object.custom_materials)
		baked_result.set_custom_materials(original_object.custom_materials, 1)

	baked_result.pixel_x = original_object.pixel_x
	baked_result.pixel_y = original_object.pixel_y
	used_tray.AddToPlate(baked_result)

	if(positive_result)
		used_oven.visible_message(span_warning("You smell something great coming from [used_oven]"))
	else
		used_oven.visible_message(span_warning("You smell a burnt smell coming from [used_oven]"))
	SEND_SIGNAL(parent, COMSIG_BAKE_COMPLETED, baked_result)
	qdel(parent)

///Gives info about the items baking status so you can see if its almost done
/datum/component/bakeable/proc/OnExamine(atom/A, mob/user, list/examine_list)
	SIGNAL_HANDLER

	if(!current_bake_time) //Not baked yet
		if(positive_result)
			examine_list += span_notice("[parent] can be <b>baked</b> into \a [initial(bake_result.name)].")
		return

	if(positive_result)
		if(current_bake_time <= required_bake_time * 0.75)
			examine_list += span_notice("[parent] probably needs to be baked a bit longer!")
		else if(current_bake_time <= required_bake_time)
			examine_list += span_notice("[parent] seems to be almost finished baking!")
	else
		examine_list += span_danger("[parent] should probably not be baked for much longer!")


code:
/datum/component/beetlejuice
	var/keyword
	var/list/first_heard
	var/list/count
	var/max_delay = 3 SECONDS //How fast they need to be said
	var/min_count = 3
	var/cooldown = 30 SECONDS //Delay between teleports
	var/active = TRUE
	var/case_sensitive = FALSE
	var/regex/R

/datum/component/beetlejuice/Initialize()
	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE

	first_heard = list()
	count = list()

	var/atom/movable/O = parent
	keyword = O.name
	if(ismob(O))
		var/mob/M = parent
		keyword = M.real_name
	update_regex()

	RegisterSignal(SSdcs, COMSIG_GLOB_LIVING_SAY_SPECIAL, .proc/say_react)

/datum/component/beetlejuice/proc/update_regex()
	R = regex("[REGEX_QUOTE(keyword)]","g[case_sensitive ? "" : "i"]")

/datum/component/beetlejuice/vv_edit_var(var_name, var_value)
	. = ..()
	if (var_name == NAMEOF(src, keyword) || var_name == NAMEOF(src, case_sensitive))
		update_regex()

/datum/component/beetlejuice/proc/say_react(datum/source, mob/speaker,message)
	SIGNAL_HANDLER

	if(!speaker || speaker == parent || !message || !active)
		return
	var/found = R.Find(message)
	if(found)
		var/occurences = 1
		while(R.Find(message))
			occurences++
		R.next = 1

		if(!first_heard[speaker] || (first_heard[speaker] + max_delay < world.time))
			first_heard[speaker] = world.time
			count[speaker] = 0
		count[speaker] += occurences
		if(count[speaker] >= min_count)
			first_heard -= speaker
			count -= speaker
			apport(speaker)

/datum/component/beetlejuice/proc/apport(atom/target)
	var/atom/movable/AM = parent
	do_teleport(AM,get_turf(target))
	active = FALSE
	addtimer(VARSET_CALLBACK(src, active, TRUE), cooldown)


code:
/**
 * Component for clothing items that can pick up blood from decals and spread it around everywhere when walking, such as shoes or suits with integrated shoes.
 */
/datum/component/bloodysoles
	/// The type of the last grub pool we stepped in, used to decide the type of footprints to make
	var/last_blood_state = BLOOD_STATE_NOT_BLOODY

	/// How much of each grubby type we have on our feet
	var/list/bloody_shoes = list(BLOOD_STATE_HUMAN = 0,BLOOD_STATE_XENO = 0, BLOOD_STATE_OIL = 0, BLOOD_STATE_NOT_BLOODY = 0)

	/// The ITEM_SLOT_* slot the item is equipped on, if it is.
	var/equipped_slot

	/// The parent item but casted into atom type for easier use.
	var/atom/parent_atom

	/// Either the mob carrying the item, or the mob itself for the /feet component subtype
	var/mob/living/carbon/wielder

	/// The world.time when we last picked up blood
	var/last_pickup

/datum/component/bloodysoles/Initialize()
	if(!isclothing(parent))
		return COMPONENT_INCOMPATIBLE
	parent_atom = parent

	RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
	RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/on_drop)
	RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, .proc/on_clean)

/**
 * Unregisters from the wielder if necessary
 */
/datum/component/bloodysoles/proc/unregister()
	if(!QDELETED(wielder))
		UnregisterSignal(wielder, COMSIG_MOVABLE_MOVED)
		UnregisterSignal(wielder, COMSIG_STEP_ON_BLOOD)
	wielder = null
	equipped_slot = null

/**
 * Returns true if the parent item is obscured by something else that the wielder is wearing
 */
/datum/component/bloodysoles/proc/is_obscured()
	return wielder.check_obscured_slots(TRUE) & equipped_slot

/**
 * Run to update the icon of the parent
 */
/datum/component/bloodysoles/proc/update_icon()
	var/obj/item/parent_item = parent
	parent_item.update_slot_icon()

/**
 * Run to equally share the blood between us and a decal
 */
/datum/component/bloodysoles/proc/share_blood(obj/effect/decal/cleanable/pool)
	last_blood_state = pool.blood_state

	// Share the blood between our boots and the blood pool
	var/total_bloodiness = pool.bloodiness + bloody_shoes[last_blood_state]

	// We can however be limited by how much blood we can hold
	var/new_our_bloodiness = min(BLOOD_ITEM_MAX, total_bloodiness / 2)

	bloody_shoes[last_blood_state] = new_our_bloodiness
	pool.bloodiness = total_bloodiness - new_our_bloodiness // Give the pool the remaining blood incase we were limited

	if(HAS_TRAIT(parent_atom, TRAIT_LIGHT_STEP)) //the character is agile enough to don't mess their clothing and hands just from one blood splatter at floor
		return TRUE

	parent_atom.add_blood_DNA(pool.return_blood_DNA())
	update_icon()

/**
 * Find a blood decal on a turf that matches our last_blood_state
 */
/datum/component/bloodysoles/proc/find_pool_by_blood_state(turf/turfLoc, typeFilter = null)
	for(var/obj/effect/decal/cleanable/blood/pool in turfLoc)
		if(pool.blood_state == last_blood_state && (!typeFilter || istype(pool, typeFilter)))
			return pool

/**
 * Adds the parent type to the footprint's shoe_types var
 */
/datum/component/bloodysoles/proc/add_parent_to_footprint(obj/effect/decal/cleanable/blood/footprints/FP)
	FP.shoe_types |= parent.type

/**
 * Called when the parent item is equipped by someone
 *
 * Used to register our wielder
 */
/datum/component/bloodysoles/proc/on_equip(datum/source, mob/equipper, slot)
	SIGNAL_HANDLER

	if(!iscarbon(equipper))
		return
	var/obj/item/parent_item = parent
	if(!(parent_item.slot_flags & slot))
		unregister()
		return

	equipped_slot = slot
	wielder = equipper
	RegisterSignal(wielder, COMSIG_MOVABLE_MOVED, .proc/on_moved)
	RegisterSignal(wielder, COMSIG_STEP_ON_BLOOD, .proc/on_step_blood)

/**
 * Called when the parent item has been dropped
 *
 * Used to deregister our wielder
 */
/datum/component/bloodysoles/proc/on_drop(datum/source, mob/dropper)
	SIGNAL_HANDLER

	unregister()

/**
 * Called when the wielder has moved
 *
 * Used to make bloody footprints on the ground
 */
/datum/component/bloodysoles/proc/on_moved(datum/source, OldLoc, Dir, Forced)
	SIGNAL_HANDLER

	if(bloody_shoes[last_blood_state] == 0)
		return
	if(QDELETED(wielder) || is_obscured())
		return
	if(wielder.body_position == LYING_DOWN || !wielder.has_gravity(wielder.loc))
		return

	var/half_our_blood = bloody_shoes[last_blood_state] / 2

	// Add footprints in old loc if we have enough cream
	if(half_our_blood >= BLOOD_FOOTPRINTS_MIN)
		var/turf/oldLocTurf = get_turf(OldLoc)
		var/obj/effect/decal/cleanable/blood/footprints/oldLocFP = find_pool_by_blood_state(oldLocTurf, /obj/effect/decal/cleanable/blood/footprints)
		if(oldLocFP)
			// Footprints found in the tile we left, add us to it
			add_parent_to_footprint(oldLocFP)
			if (!(oldLocFP.exited_dirs & wielder.dir))
				oldLocFP.exited_dirs |= wielder.dir
				oldLocFP.update_appearance()
		else if(find_pool_by_blood_state(oldLocTurf))
			// No footprints in the tile we left, but there was some other blood pool there. Add exit footprints on it
			bloody_shoes[last_blood_state] -= half_our_blood
			update_icon()

			oldLocFP = new(oldLocTurf)
			if(!QDELETED(oldLocFP)) ///prints merged
				oldLocFP.blood_state = last_blood_state
				oldLocFP.exited_dirs |= wielder.dir
				add_parent_to_footprint(oldLocFP)
				oldLocFP.bloodiness = half_our_blood
				oldLocFP.add_blood_DNA(parent_atom.return_blood_DNA())
				oldLocFP.update_appearance()

			half_our_blood = bloody_shoes[last_blood_state] / 2

	// If we picked up the blood on this tick in on_step_blood, don't make footprints at the same place
	if(last_pickup && last_pickup == world.time)
		return

	// Create new footprints
	if(half_our_blood >= BLOOD_FOOTPRINTS_MIN)
		bloody_shoes[last_blood_state] -= half_our_blood
		update_icon()

		var/obj/effect/decal/cleanable/blood/footprints/FP = new(get_turf(parent_atom))
		if(!QDELETED(FP)) ///prints merged
			FP.blood_state = last_blood_state
			FP.entered_dirs |= wielder.dir
			add_parent_to_footprint(FP)
			FP.bloodiness = half_our_blood
			FP.add_blood_DNA(parent_atom.return_blood_DNA())
			FP.update_appearance()

/**
 * Called when the wielder steps in a pool of blood
 *
 * Used to make the parent item bloody
 */
/datum/component/bloodysoles/proc/on_step_blood(datum/source, obj/effect/decal/cleanable/pool)
	SIGNAL_HANDLER

	if(QDELETED(wielder) || is_obscured())
		return

	if(istype(pool, /obj/effect/decal/cleanable/blood/footprints) && pool.blood_state == last_blood_state)
		// The pool we stepped in was actually footprints with the same type
		var/obj/effect/decal/cleanable/blood/footprints/pool_FP = pool
		add_parent_to_footprint(pool_FP)
		if((bloody_shoes[last_blood_state] / 2) >= BLOOD_FOOTPRINTS_MIN && !(pool_FP.entered_dirs & wielder.dir))
			// If our feet are bloody enough, add an entered dir
			pool_FP.entered_dirs |= wielder.dir
			pool_FP.update_appearance()

	share_blood(pool)

	last_pickup = world.time

/**
 * Called when the parent item is being washed
 */
/datum/component/bloodysoles/proc/on_clean(datum/source, clean_types)
	SIGNAL_HANDLER

	if(!(clean_types & CLEAN_TYPE_BLOOD) || last_blood_state == BLOOD_STATE_NOT_BLOODY)
		return NONE

	bloody_shoes = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_OIL = 0, BLOOD_STATE_NOT_BLOODY = 0)
	last_blood_state = BLOOD_STATE_NOT_BLOODY
	update_icon()
	return COMPONENT_CLEANED

/**
 * Like its parent but can be applied to carbon mobs instead of clothing items
 */
/datum/component/bloodysoles/feet
	var/static/mutable_appearance/bloody_feet

/datum/component/bloodysoles/feet/Initialize()
	if(!iscarbon(parent))
		return COMPONENT_INCOMPATIBLE
	parent_atom = parent
	wielder = parent

	if(!bloody_feet)
		bloody_feet = mutable_appearance('icons/effects/blood.dmi', "shoeblood", SHOES_LAYER)

	RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, .proc/on_clean)
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_moved)
	RegisterSignal(parent, COMSIG_STEP_ON_BLOOD, .proc/on_step_blood)
	RegisterSignal(parent, COMSIG_CARBON_UNEQUIP_SHOECOVER, .proc/unequip_shoecover)
	RegisterSignal(parent, COMSIG_CARBON_EQUIP_SHOECOVER, .proc/equip_shoecover)

/datum/component/bloodysoles/feet/update_icon()
	if(ishuman(wielder))
		var/mob/living/carbon/human/human = wielder
		if(NOBLOODOVERLAY in human.dna.species.species_traits)
			return
		if(bloody_shoes[BLOOD_STATE_HUMAN] > 0 && !is_obscured())
			human.remove_overlay(SHOES_LAYER)
			human.overlays_standing[SHOES_LAYER] = bloody_feet
			human.apply_overlay(SHOES_LAYER)
		else
			human.update_inv_shoes()

/datum/component/bloodysoles/feet/add_parent_to_footprint(obj/effect/decal/cleanable/blood/footprints/FP)
	if(!ishuman(wielder))
		FP.species_types |= "unknown"
		return

	// Find any leg of our human and add that to the footprint, instead of the default which is to just add the human type
	for(var/X in wielder.bodyparts)
		var/obj/item/bodypart/affecting = X
		if(affecting.body_part == LEG_RIGHT || affecting.body_part == LEG_LEFT)
			if(!affecting.bodypart_disabled)
				FP.species_types |= affecting.species_id
				break

/datum/component/bloodysoles/feet/is_obscured()
	if(wielder.shoes)
		return TRUE
	return wielder.check_obscured_slots(TRUE) & ITEM_SLOT_FEET

/datum/component/bloodysoles/feet/on_moved(datum/source, OldLoc, Dir, Forced)
	/* SEPTIC EDIT REMOVAL
	if(wielder.num_legs < 2)
	*/
	//SEPTIC EDIT BEGIN
	if((wielder.num_legs < wielder.default_num_legs) || wielder.body_position != STANDING_UP)
	//SEPTIC EDIT END
		return

	..()

/datum/component/bloodysoles/feet/on_step_blood(datum/source, obj/effect/decal/cleanable/pool)
	/* SEPTIC EDIT REMOVAL
	if(wielder.num_legs < 2)
	*/
	//SEPTIC EDIT BEGIN
	if((wielder.num_legs < wielder.default_num_legs) || wielder.body_position != STANDING_UP)
	//SEPTIC EDIT END
		return

	..()

/datum/component/bloodysoles/feet/proc/unequip_shoecover(datum/source)
	SIGNAL_HANDLER

	update_icon()

/datum/component/bloodysoles/feet/proc/equip_shoecover(datum/source)
	SIGNAL_HANDLER

	update_icon()


code:
/datum/component/butchering
	/// Time in deciseconds taken to butcher something
	var/speed = 8 SECONDS
	/// Percentage effectiveness; numbers above 100 yield extra drops
	var/effectiveness = 100
	/// Percentage increase to bonus item chance
	var/bonus_modifier = 0
	/// Sound played when butchering
	var/butcher_sound = 'sound/effects/butcher.ogg'
	/// Whether or not this component can be used to butcher currently. Used to temporarily disable butchering
	var/butchering_enabled = TRUE
	/// Whether or not this component is compatible with blunt tools.
	var/can_be_blunt = FALSE
	/// Callback for butchering
	var/datum/callback/butcher_callback

/datum/component/butchering/Initialize(_speed, _effectiveness, _bonus_modifier, _butcher_sound, disabled, _can_be_blunt, _butcher_callback)
	if(_speed)
		speed = _speed
	if(_effectiveness)
		effectiveness = _effectiveness
	if(_bonus_modifier)
		bonus_modifier = _bonus_modifier
	if(_butcher_sound)
		butcher_sound = _butcher_sound
	if(disabled)
		butchering_enabled = FALSE
	if(_can_be_blunt)
		can_be_blunt = _can_be_blunt
	if(_butcher_callback)
		butcher_callback = _butcher_callback
	if(isitem(parent))
		RegisterSignal(parent, COMSIG_ITEM_ATTACK, .proc/onItemAttack)

/datum/component/butchering/proc/onItemAttack(obj/item/source, mob/living/M, mob/living/user)
	SIGNAL_HANDLER
	//SEPTIC EDIT BEGIN
	if(IS_NOT_HARM_INTENT(user, null))
		return
	//SEPTIC EDIT END
	if(M.stat == DEAD && (M.butcher_results || M.guaranteed_butcher_results)) //can we butcher it?
		if(butchering_enabled && (can_be_blunt || source.get_sharpness()))
			INVOKE_ASYNC(src, .proc/startButcher, source, M, user)
			return COMPONENT_CANCEL_ATTACK_CHAIN

	if(ishuman(M) && source.force && source.get_sharpness())
		var/mob/living/carbon/human/H = M
		/* SEPTIC EDIT REMOVAL
		if((user.pulling == H && user.grab_state >= GRAB_AGGRESSIVE) && user.zone_selected == BODY_ZONE_HEAD) // Only aggressive grabbed can be sliced.
			if(H.has_status_effect(/datum/status_effect/neck_slice))
		*/
		//SEPTIC EDIT BEGIN
		if((user.pulling == H && user.grab_state >= GRAB_AGGRESSIVE) && (user.zone_selected == BODY_ZONE_PRECISE_NECK))
			var/obj/item/bodypart/throat = H.get_bodypart(BODY_ZONE_PRECISE_NECK)
			if(throat.is_artery_torn())
		//SEPTIC EDIT END
				user.show_message(span_warning("[H]'s neck has already been already cut, you can't make the bleeding any worse!"), MSG_VISUAL, \
								span_warning("Their neck has already been already cut, you can't make the bleeding any worse!"))
				return COMPONENT_CANCEL_ATTACK_CHAIN
			INVOKE_ASYNC(src, .proc/startNeckSlice, source, H, user)
			return COMPONENT_CANCEL_ATTACK_CHAIN

/datum/component/butchering/proc/startButcher(obj/item/source, mob/living/M, mob/living/user)
	to_chat(user, span_notice("You begin to butcher [M]..."))
	playsound(M.loc, butcher_sound, 50, TRUE, -1)
	if(do_mob(user, M, speed) && M.Adjacent(source))
		Butcher(user, M)

/datum/component/butchering/proc/startNeckSlice(obj/item/source, mob/living/carbon/human/H, mob/living/user)
	if(DOING_INTERACTION_WITH_TARGET(user, H))
		to_chat(user, span_warning("You're already interacting with [H]!"))
		return

	user.visible_message(span_danger("[user] is slitting [H]'s throat!"), \
					span_danger("You start slicing [H]'s throat!"), \
					span_hear("You hear a cutting noise!"), ignored_mobs = H)
	H.show_message(span_userdanger("Your throat is being slit by [user]!"), MSG_VISUAL, \
					"Something is cutting into your neck!", NONE)
	log_combat(user, H, "attempted throat slitting", source)

	playsound(H.loc, butcher_sound, 50, TRUE, -1)
	if(do_mob(user, H, clamp(500 / source.force, 30, 100)) && H.Adjacent(source))
		if(H.has_status_effect(/datum/status_effect/neck_slice))
			user.show_message(span_warning("[H]'s neck has already been already cut, you can't make the bleeding any worse!"), MSG_VISUAL, \
							span_warning("Their neck has already been already cut, you can't make the bleeding any worse!"))
			return

		H.visible_message(span_danger("[user] slits [H]'s throat!"), \
					span_userdanger("[user] slits your throat..."))
		log_combat(user, H, "wounded via throat slitting", source)
		H.apply_damage(source.force, BRUTE, BODY_ZONE_HEAD, wound_bonus=CANT_WOUND) // easy tiger, we'll get to that in a sec
		/* SEPTIC EDIT REMOVAL
		var/obj/item/bodypart/slit_throat = H.get_bodypart(BODY_ZONE_HEAD)
		if(slit_throat)
			var/datum/wound/slash/critical/screaming_through_a_slit_throat = new
			screaming_through_a_slit_throat.apply_wound(slit_throat)
		H.apply_status_effect(/datum/status_effect/neck_slice)
		*/
		//SEPTIC EDIT BEGIN
		var/obj/item/bodypart/slit_throat = H.get_bodypart(BODY_ZONE_PRECISE_NECK)
		if(slit_throat)
			slit_throat.force_wound_upwards(/datum/wound/artery)
			slit_throat.force_wound_upwards(/datum/wound/artery)
			slit_throat.force_wound_upwards(/datum/wound/tendon)
			slit_throat.force_wound_upwards(/datum/wound/tendon)
			SEND_SIGNAL(H, COMSIG_CARBON_CLEAR_WOUND_MESSAGE)
		//SEPTIC EDIT END

/**
 * Handles a user butchering a target
 *
 * Arguments:
 * - [butcher][/mob/living]: The mob doing the butchering
 * - [meat][/mob/living]: The mob being butchered
 */
/datum/component/butchering/proc/Butcher(mob/living/butcher, mob/living/meat)
	var/list/results = list()
	var/turf/T = meat.drop_location()
	var/final_effectiveness = effectiveness - meat.butcher_difficulty
	var/bonus_chance = max(0, (final_effectiveness - 100) + bonus_modifier) //so 125 total effectiveness = 25% extra chance
	for(var/V in meat.butcher_results)
		var/obj/bones = V
		var/amount = meat.butcher_results[bones]
		for(var/_i in 1 to amount)
			if(!prob(final_effectiveness))
				if(butcher)
					to_chat(butcher, span_warning("You fail to harvest some of the [initial(bones.name)] from [meat]."))
				continue

			if(prob(bonus_chance))
				if(butcher)
					to_chat(butcher, span_info("You harvest some extra [initial(bones.name)] from [meat]!"))
				results += new bones (T)
			results += new bones (T)

		meat.butcher_results.Remove(bones) //in case you want to, say, have it drop its results on gib

	for(var/V in meat.guaranteed_butcher_results)
		var/obj/sinew = V
		var/amount = meat.guaranteed_butcher_results[sinew]
		for(var/i in 1 to amount)
			results += new sinew (T)
		meat.guaranteed_butcher_results.Remove(sinew)

	for(var/obj/item/carrion in results)
		var/list/meat_mats = carrion.has_material_type(/datum/material/meat)
		if(!length(meat_mats))
			continue
		carrion.set_custom_materials((carrion.custom_materials - meat_mats) + list(GET_MATERIAL_REF(/datum/material/meat/mob_meat, meat) = counterlist_sum(meat_mats)))

	if(butcher)
		butcher.visible_message(span_notice("[butcher] butchers [meat]."), \
								span_notice("You butcher [meat]."))
	butcher_callback?.Invoke(butcher, meat)
	meat.harvest(butcher)
	meat.gib(FALSE, FALSE, TRUE)

///Special snowflake component only used for the recycler.
/datum/component/butchering/recycler

/datum/component/butchering/recycler/Initialize(_speed, _effectiveness, _bonus_modifier, _butcher_sound, disabled, _can_be_blunt)
	if(!istype(parent, /obj/machinery/recycler)) //EWWW
		return COMPONENT_INCOMPATIBLE
	. = ..()
	if(. == COMPONENT_INCOMPATIBLE)
		return

	var/static/list/loc_connections = list(
		COMSIG_ATOM_ENTERED = .proc/on_entered,
	)
	AddComponent(/datum/component/connect_loc_behalf, parent, loc_connections)

/datum/component/butchering/recycler/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
	SIGNAL_HANDLER

	if(!isliving(arrived))
		return
	var/mob/living/victim = arrived
	var/obj/machinery/recycler/eater = parent
	if(eater.safety_mode || (eater.machine_stat & (BROKEN|NOPOWER))) //I'm so sorry.
		return
	if(victim.stat == DEAD && (victim.butcher_results || victim.guaranteed_butcher_results))
		Butcher(parent, victim)


code:
/**
 * Caltrop element; for hurting people when they walk over this.
 *
 * Used for broken glass, cactuses and four sided dice.
 */
/datum/component/caltrop
	///Minimum damage done when crossed
	var/min_damage

	///Maximum damage done when crossed
	var/max_damage

	///Probability of actually "firing", stunning and doing damage
	var/probability

	///Miscelanous caltrop flags; shoe bypassing, walking interaction, silence
	var/flags

	///The sound that plays when a caltrop is triggered.
	var/soundfile

	///given to connect_loc to listen for something moving over target
	var/static/list/crossed_connections = list(
		COMSIG_ATOM_ENTERED = .proc/on_entered,
	)

	///So we can update ant damage
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS

/datum/component/caltrop/Initialize(min_damage = 0, max_damage = 0, probability = 100, flags = NONE, soundfile = null)
	. = ..()
	if(!isatom(parent))
		return COMPONENT_INCOMPATIBLE

	src.min_damage = min_damage
	src.max_damage = max(min_damage, max_damage)
	src.probability = probability
	src.flags = flags
	src.soundfile = soundfile

	if(ismovable(parent))
		AddComponent(/datum/component/connect_loc_behalf, parent, crossed_connections)
	else
		RegisterSignal(get_turf(parent), COMSIG_ATOM_ENTERED, .proc/on_entered)

// Inherit the new values passed to the component
/datum/component/caltrop/InheritComponent(datum/component/caltrop/new_comp, original, min_damage, max_damage, probability, flags, soundfile)
	if(!original)
		return
	if(min_damage)
		src.min_damage = min_damage
	if(max_damage)
		src.max_damage = max_damage
	if(probability)
		src.probability = probability
	if(flags)
		src.flags = flags
	if(soundfile)
		src.soundfile = soundfile

/datum/component/caltrop/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
	SIGNAL_HANDLER

	if(!prob(probability))
		return

	if(!ishuman(arrived))
		return

	var/mob/living/carbon/human/H = arrived
	if(HAS_TRAIT(H, TRAIT_PIERCEIMMUNE))
		return

	if((flags & CALTROP_IGNORE_WALKERS) && H.m_intent == MOVE_INTENT_WALK)
		return

	if(H.movement_type & (FLOATING|FLYING)) //check if they are able to pass over us
		//gravity checking only our parent would prevent us from triggering they're using magboots / other gravity assisting items that would cause them to still touch us.
		return

	if(H.buckled) //if they're buckled to something, that something should be checked instead.
		return

	if(H.body_position == LYING_DOWN && !(flags & CALTROP_NOCRAWL)) //if we're not standing we cant step on the caltrop
		return

	var/picked_def_zone = pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
	var/obj/item/bodypart/O = H.get_bodypart(picked_def_zone)
	if(!istype(O))
		return

	if(O.status == BODYPART_ROBOTIC)
		return

	if (!(flags & CALTROP_BYPASS_SHOES))
		if ((H.wear_suit?.body_parts_covered | H.w_uniform?.body_parts_covered | H.shoes?.body_parts_covered) & FEET)
			return

	var/damage = rand(min_damage, max_damage)
	if(HAS_TRAIT(H, TRAIT_LIGHT_STEP))
		damage *= 0.75

	if(!(flags & CALTROP_SILENT) && !H.has_status_effect(/datum/status_effect/caltropped))
		H.apply_status_effect(/datum/status_effect/caltropped)
		H.visible_message(
			span_danger("[H] steps on [parent]."),
			span_userdanger("You step on [parent]!")
		)

	H.apply_damage(damage, BRUTE, picked_def_zone, wound_bonus = CANT_WOUND)

	if(!(flags & CALTROP_NOSTUN)) // Won't set off the paralysis.
		H.Paralyze(60)

	if(!soundfile)
		return
	playsound(H, soundfile, 15, TRUE, -3)

/datum/component/caltrop/UnregisterFromParent()
	if(ismovable(parent))
		qdel(GetComponent(/datum/component/connect_loc_behalf))


code:
// Used by /turf/open/chasm and subtypes to implement the "dropping" mechanic
/datum/component/chasm
	var/turf/target_turf
	var/fall_message = "GAH! Ah... where are you?"
	var/oblivion_message = "You stumble and stare into the abyss before you. It stares back, and you fall into the enveloping dark."

	/// List of refs to falling objects -> how many levels deep we've fallen
	var/static/list/falling_atoms = list()
	var/static/list/forbidden_types = typecacheof(list(
		/obj/singularity,
		/obj/energy_ball,
		/obj/narsie,
		/obj/docking_port,
		/obj/structure/lattice,
		/obj/structure/stone_tile,
		/obj/projectile,
		/obj/effect/projectile,
		/obj/effect/portal,
		/obj/effect/abstract,
		/obj/effect/hotspot,
		/obj/effect/landmark,
		/obj/effect/temp_visual,
		/obj/effect/light_emitter/tendril,
		/obj/effect/collapse,
		/obj/effect/particle_effect/ion_trails,
		/obj/effect/dummy/phased_mob,
		/obj/effect/mapping_helpers,
		/obj/effect/wisp,
		))

/datum/component/chasm/Initialize(turf/target)
	RegisterSignal(parent, COMSIG_ATOM_ENTERED, .proc/Entered)
	target_turf = target
	START_PROCESSING(SSobj, src) // process on create, in case stuff is still there

/datum/component/chasm/proc/Entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
	SIGNAL_HANDLER

	START_PROCESSING(SSobj, src)
	drop_stuff(arrived)

/datum/component/chasm/process()
	if (!drop_stuff())
		STOP_PROCESSING(SSobj, src)

/datum/component/chasm/proc/is_safe()
	//if anything matching this typecache is found in the chasm, we don't drop things
	var/static/list/chasm_safeties_typecache = typecacheof(list(/obj/structure/lattice/catwalk, /obj/structure/stone_tile))

	var/atom/parent = src.parent
	var/list/found_safeties = typecache_filter_list(parent.contents, chasm_safeties_typecache)
	for(var/obj/structure/stone_tile/S in found_safeties)
		if(S.fallen)
			LAZYREMOVE(found_safeties, S)
	return LAZYLEN(found_safeties)

/datum/component/chasm/proc/drop_stuff(AM)
	if (is_safe())
		return FALSE

	var/atom/parent = src.parent
	var/to_check = AM ? list(AM) : parent.contents
	for (var/thing in to_check)
		if (droppable(thing))
			. = TRUE
			INVOKE_ASYNC(src, .proc/drop, thing)

/datum/component/chasm/proc/droppable(atom/movable/AM)
	var/datum/weakref/falling_ref = WEAKREF(AM)
	// avoid an infinite loop, but allow falling a large distance
	if(falling_atoms[falling_ref] && falling_atoms[falling_ref] > 30)
		return FALSE
	if(!isliving(AM) && !isobj(AM))
		return FALSE
	if(is_type_in_typecache(AM, forbidden_types) || AM.throwing || (AM.movement_type & (FLOATING|FLYING)))
		return FALSE
	//Flies right over the chasm
	if(ismob(AM))
		var/mob/M = AM
		if(M.buckled) //middle statement to prevent infinite loops just in case!
			var/mob/buckled_to = M.buckled
			if((!ismob(M.buckled) || (buckled_to.buckled != M)) && !droppable(M.buckled))
				return FALSE
		if(ishuman(AM))
			var/mob/living/carbon/human/victim = AM
			if(istype(victim.belt, /obj/item/wormhole_jaunter))
				var/obj/item/wormhole_jaunter/jaunter = victim.belt
				var/turf/chasm = get_turf(victim)
				var/fall_into_chasm = jaunter.chasm_react(victim)
				if(!fall_into_chasm)
					chasm.visible_message(span_boldwarning("[victim] falls into the [chasm]!")) //To freak out any bystanders
				return fall_into_chasm
	return TRUE

/datum/component/chasm/proc/drop(atom/movable/AM)
	var/datum/weakref/falling_ref = WEAKREF(AM)
	//Make sure the item is still there after our sleep
	if(!AM || !falling_ref?.resolve())
		falling_atoms -= falling_ref
		return
	falling_atoms[falling_ref] = (falling_atoms[falling_ref] || 0) + 1
	var/turf/T = target_turf

	if(T)
		// send to the turf below
		AM.visible_message(span_boldwarning("[AM] falls into [parent]!"), span_userdanger("[fall_message]"))
		T.visible_message(span_boldwarning("[AM] falls from above!"))
		AM.forceMove(T)
		if(isliving(AM))
			var/mob/living/L = AM
			L.Paralyze(100)
			L.adjustBruteLoss(30)
		falling_atoms -= falling_ref

	else
		// send to oblivion
		AM.visible_message(span_boldwarning("[AM] falls into [parent]!"), span_userdanger("[oblivion_message]"))
		if (isliving(AM))
			var/mob/living/L = AM
			L.notransform = TRUE
			L.Paralyze(20 SECONDS)

		var/oldtransform = AM.transform
		var/oldcolor = AM.color
		var/oldalpha = AM.alpha
		animate(AM, transform = matrix() - matrix(), alpha = 0, color = rgb(0, 0, 0), time = 10)
		for(var/i in 1 to 5)
			//Make sure the item is still there after our sleep
			if(!AM || QDELETED(AM))
				return
			AM.pixel_y--
			sleep(2)

		//Make sure the item is still there after our sleep
		if(!AM || QDELETED(AM))
			return

		if(iscyborg(AM))
			var/mob/living/silicon/robot/S = AM
			qdel(S.mmi)
		if(isliving(AM))
			var/mob/living/L = AM
			if(L.stat != DEAD)
				L.death(TRUE)

		falling_atoms -= falling_ref
		qdel(AM)
		if(AM && !QDELETED(AM)) //It's indestructible
			var/atom/parent = src.parent
			parent.visible_message(span_boldwarning("[parent] spits out [AM]!"))
			AM.alpha = oldalpha
			AM.color = oldcolor
			AM.transform = oldtransform
			AM.throw_at(get_edge_target_turf(parent,pick(GLOB.alldirs)),rand(1, 10),rand(1, 10))


code:
#define CLICKBOX_LAYER FLOAT_LAYER-999

/**
 * This component adds a near-invisible underlay to a movable target to expand the clickable surface of its icon without
 * resorting to MOUSE_OPACITY_OPAQUE.
 * The underlay ignores the parent's color and alpha and can be offset.
 * The name is a portmanteau of "click" and "hitbox", because technically this isn't an hitbox,
 * but it helps catch clicks, and I don't want to give it a loooong name like openspace_item_click_handler
 */
/datum/component/clickbox
	dupe_mode = COMPONENT_DUPE_ALLOWED
	/// The icon state of the underlay.
	var/icon_state = "sphere"
	/// The offsets of the underlay.
	var/x_offset = 0
	var/y_offset = 0
	/// Maximum width/height of the underlay, in case the attached atom is scaled up.
	var/max_scale = 2
	/// Minimum width/height of the underlay, in case the attached atom is scaled down.
	var/min_scale = 0.5
	/// For simple animals that have different icon states when dead.
	var/dead_state
	/// the underlay that has been added to the parent.
	var/mutable_appearance/clickbox_undelay

/datum/component/clickbox/Initialize(icon_state = "sphere", x_offset = 0, y_offset = 0, max_scale = 2, min_scale = 0.5, dead_state)
	if(!isatom(parent))
		return COMPONENT_INCOMPATIBLE

	src.icon_state = icon_state
	src.x_offset = x_offset
	src.y_offset = y_offset
	src.max_scale = max_scale
	src.min_scale = min_scale

	RegisterSignal(parent, COMSIG_ATOM_VV_MODIFY_TRANSFORM, .proc/on_modify_or_update_transform)

	var/clickbox_icon_state = icon_state
	if(dead_state && isliving(parent))
		var/mob/living/living_parent = parent
		src.dead_state = dead_state
		RegisterSignal(living_parent, COMSIG_LIVING_POST_UPDATE_TRANSFORM, .proc/on_modify_or_update_transform)
		RegisterSignal(living_parent, COMSIG_LIVING_DEATH, .proc/on_death)
		RegisterSignal(living_parent, COMSIG_LIVING_REVIVE, .proc/on_revive)
		if(living_parent.stat == DEAD)
			clickbox_icon_state = dead_state
	update_underlay(clickbox_icon_state)

/datum/component/clickbox/UnregisterFromParent()
	var/atom/movable/mov_parent = parent
	UnregisterSignal(mov_parent, list(COMSIG_ATOM_VV_MODIFY_TRANSFORM, COMSIG_LIVING_POST_UPDATE_TRANSFORM, COMSIG_LIVING_DEATH, COMSIG_LIVING_REVIVE))
	mov_parent.underlays -= clickbox_undelay

/// Removes the old underlay and adds a new one if conditions are met. The underlay is scaled up/down if necessary
/datum/component/clickbox/proc/update_underlay(clickbox_icon_state)
	var/atom/movable/mov_parent = parent
	if(!clickbox_icon_state)
		clickbox_icon_state = clickbox_undelay?.icon_state || icon_state
	mov_parent.underlays -= clickbox_undelay // Remove the previous underlay.

	var/width = abs(mov_parent.transform.a) // Negative values flip the parent, so abs() is good to have here.
	var/height = abs(mov_parent.transform.e) // Idem.

	var/clickbox_width = 1
	if(width > max_scale)
		clickbox_width = max_scale/width
	else if(width < min_scale && width)
		clickbox_width = min_scale/width

	var/clickbox_height = 1
	if(height > max_scale)
		clickbox_height = max_scale/height
	else if(height < min_scale && height)
		clickbox_height = min_scale/height

	clickbox_undelay = mutable_appearance('icons/misc/clickbox.dmi', clickbox_icon_state, CLICKBOX_LAYER, alpha = 1, appearance_flags = RESET_COLOR|RESET_ALPHA)
	clickbox_undelay.transform = clickbox_undelay.transform.Scale(clickbox_width, clickbox_height)
	//Keeps the underlay more or less centered.
	clickbox_undelay.pixel_x = x_offset * 1/clickbox_width
	clickbox_undelay.pixel_y = y_offset * 1/clickbox_height
	mov_parent.underlays += clickbox_undelay

/datum/component/clickbox/proc/on_modify_or_update_transform(atom/source)
	SIGNAL_HANDLER
	update_underlay()

/datum/component/clickbox/proc/on_death(mob/living/source)
	SIGNAL_HANDLER
	update_underlay(dead_state)

/datum/component/clickbox/proc/on_revive(mob/living/source)
	SIGNAL_HANDLER
	update_underlay(icon_state)

#undef CLICKBOX_LAYER


code:
/**
 * Component that allows for highlighting of words or phrases in chat based on regular expressions.
 *
 * Hooks into the parent's COMSIG_MOVABLE_HEAR signal to wrap every regex match in the message
 * between  tags with the provided span class. This modifies the output that
 * is sent to the parent's chat window.
 *
 * Removal of this component should be done by calling [GetComponents(/datum/component/codeword_hearing)]
 * on the parent and then iterating through all components calling [delete_if_from_source(source)].
 */
/datum/component/codeword_hearing
	dupe_mode = COMPONENT_DUPE_ALLOWED

	/// Regex for matching words or phrases you want highlighted.
	var/regex/replace_regex
	/// The  to use for highlighting matches.
	var/span_class
	/// The source of this component. Used to identify the source in delete_if_from_source since this component is COMPONENT_DUPE_ALLOWED.
	var/source

/datum/component/codeword_hearing/Initialize(regex/codeword_regex, highlight_span_class, component_source)
	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE

	replace_regex = codeword_regex
	span_class = highlight_span_class
	source = component_source
	return ..()

/datum/component/codeword_hearing/RegisterWithParent()
	RegisterSignal(parent, COMSIG_MOVABLE_HEAR, .proc/handle_hearing)

/datum/component/codeword_hearing/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_MOVABLE_HEAR)

/// Callback for COMSIG_MOVABLE_HEAR which highlights syndicate code phrases in chat.
/datum/component/codeword_hearing/proc/handle_hearing(datum/source, list/hearing_args)
	SIGNAL_HANDLER

	var/message = hearing_args[HEARING_RAW_MESSAGE]
	message = replace_regex.Replace(message, "$1")
	hearing_args[HEARING_RAW_MESSAGE] = message

/// Since a parent can have multiple of these components on them simultaneously, this allows a datum to delete components from a specific source.
/datum/component/codeword_hearing/proc/delete_if_from_source(component_source)
	if(source == component_source)
		qdel(src)
		return TRUE

	return FALSE


code:
/datum/component/combustible_flooder
	// Gas type, molar count, and temperature. All self explanatory.
	var/gas_id
	var/gas_amount
	var/temp_amount

/datum/component/combustible_flooder/Initialize(gas_id, gas_amount, temp_amount)

	src.gas_id = gas_id
	src.gas_amount = gas_amount
	src.temp_amount = temp_amount

	RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/attackby_react)
	RegisterSignal(parent, COMSIG_ATOM_FIRE_ACT, .proc/flame_react)

/datum/component/combustible_flooder/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_PARENT_ATTACKBY)
	UnregisterSignal(parent, COMSIG_ATOM_FIRE_ACT)

/// Do the flooding.
/datum/component/combustible_flooder/proc/flood(mob/user, temp_amount)
	var/turf/open/flooded_turf = get_turf(parent)
	flooded_turf.atmos_spawn_air("[gas_id]=[gas_amount];TEMP=[temp_amount]")

	// Logging-related
	var/admin_message = "[parent] ignited in [ADMIN_VERBOSEJMP(flooded_turf)]"
	var/log_message = "[parent] ignited in [AREACOORD(flooded_turf)]"
	if(user)
		admin_message += " by [ADMIN_LOOKUPFLW(user)]"
		log_message += " by [key_name(user)]"
	else
		admin_message += " by fire"
		log_message += " by fire"
	message_admins(admin_message)
	log_game(log_message)

	// For floors
	if(isturf(parent))
		var/turf/K = parent
		K.ScrapeAway(1, CHANGETURF_INHERIT_AIR)
	else
		qdel(parent)

/// Hotspot related flooding reaction.
/datum/component/combustible_flooder/proc/flame_react(datum/source, exposed_temperature, exposed_volume)
	SIGNAL_HANDLER

	if(exposed_temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
		flood(temp_amount = exposed_temperature)

/// Being attacked by something
/datum/component/combustible_flooder/proc/attackby_react(datum/source, obj/item/thing, mob/user, params)
	SIGNAL_HANDLER

	if(thing.get_temperature() > FIRE_MINIMUM_TEMPERATURE_TO_EXIST)
		flood(user, thing.get_temperature())


code:
/// This component behaves similar to connect_loc, hooking into a signal on a tracked object's turf
/// It has the ability to react to that signal on behalf of a seperate listener however
/// This has great use, primarially for components, but it carries with it some overhead
/// So we do it seperately as it needs to hold state which is very likely to lead to bugs if it remains as an element.
/datum/component/connect_loc_behalf
	dupe_mode = COMPONENT_DUPE_UNIQUE

	/// An assoc list of signal -> procpath to register to the loc this object is on.
	var/list/connections

	var/atom/movable/tracked

	var/atom/tracked_loc

/datum/component/connect_loc_behalf/Initialize(atom/movable/tracked, list/connections)
	. = ..()
	if (!istype(tracked))
		return COMPONENT_INCOMPATIBLE
	src.connections = connections
	src.tracked = tracked

/datum/component/connect_loc_behalf/CheckDupeComponent(datum/component/component, atom/movable/tracked, list/connections)
	if(src.tracked != tracked)
		return FALSE

	// Not equivalent. Checks if they are not the same list via shallow comparison.
	if(!compare_list(src.connections, connections))
		return FALSE

	return TRUE

/datum/component/connect_loc_behalf/RegisterWithParent()
	RegisterSignal(tracked, COMSIG_MOVABLE_MOVED, .proc/on_moved)
	RegisterSignal(tracked, COMSIG_PARENT_QDELETING, .proc/handle_tracked_qdel)
	update_signals()

/datum/component/connect_loc_behalf/UnregisterFromParent()
	unregister_signals()
	UnregisterSignal(tracked, list(
		COMSIG_MOVABLE_MOVED,
		COMSIG_PARENT_QDELETING,
	))

	tracked = null

/datum/component/connect_loc_behalf/proc/handle_tracked_qdel()
	SIGNAL_HANDLER
	qdel(src)

/datum/component/connect_loc_behalf/proc/update_signals()
	unregister_signals()
	//You may ask yourself, isn't this just silencing an error?
	//The answer is yes, but there's no good cheap way to fix it
	//What happens is the tracked object or hell the listener gets say, deleted, which makes targets[old_loc] return a null
	//The null results in a bad index, because of course it does
	//It's not a solvable problem though, since both actions, the destroy and the move, are sourced from the same signal send
	//And sending a signal should be agnostic of the order of listeners
	//So we need to either pick the order agnositic, or destroy safe
	//And I picked destroy safe. Let's hope this is the right path!
	if(isnull(tracked.loc))
		return

	tracked_loc = tracked.loc

	for (var/signal in connections)
		parent.RegisterSignal(tracked_loc, signal, connections[signal])

/datum/component/connect_loc_behalf/proc/unregister_signals()
	if(isnull(tracked_loc))
		return

	for (var/signal in connections)
		parent.UnregisterSignal(tracked_loc, signal)

	tracked_loc = null

/datum/component/connect_loc_behalf/proc/on_moved(sigtype, atom/movable/tracked, atom/old_loc)
	SIGNAL_HANDLER
	update_signals()


code:
#define FORWARD 1
#define BACKWARD -1

#define ITEM_DELETE "delete"
#define ITEM_MOVE_INSIDE "move_inside"

/datum/component/construction
	var/list/steps
	var/result
	var/index = 1
	var/desc

/datum/component/construction/Initialize()
	if(!isatom(parent))
		return COMPONENT_INCOMPATIBLE

	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/examine)
	RegisterSignal(parent, COMSIG_PARENT_ATTACKBY,.proc/action)
	update_parent(index)

/datum/component/construction/proc/examine(datum/source, mob/user, list/examine_list)
	SIGNAL_HANDLER

	if(desc)
		examine_list += desc

/datum/component/construction/proc/on_step()
	if(index > steps.len)
		spawn_result()
	else
		update_parent(index)

/datum/component/construction/proc/action(datum/source, obj/item/I, mob/living/user)
	SIGNAL_HANDLER

	return INVOKE_ASYNC(src, .proc/check_step, I, user)

/datum/component/construction/proc/update_index(diff)
	index += diff
	on_step()

/datum/component/construction/proc/check_step(obj/item/I, mob/living/user)
	var/diff = is_right_key(I)
	if(diff && custom_action(I, user, diff))
		update_index(diff)
		return TRUE
	return FALSE

/datum/component/construction/proc/is_right_key(obj/item/I) // returns index step
	var/list/L = steps[index]
	if(check_used_item(I, L["key"]))
		return FORWARD //to the first step -> forward
	else if(check_used_item(I, L["back_key"]))
		return BACKWARD //to the last step -> backwards
	return FALSE

/datum/component/construction/proc/check_used_item(obj/item/I, key)
	if(!key)
		return FALSE

	if(ispath(key) && istype(I, key))
		return TRUE

	else if(I.tool_behaviour == key)
		return TRUE

	return FALSE

/datum/component/construction/proc/custom_action(obj/item/I, mob/living/user, diff)
	var/target_index = index + diff
	var/list/current_step = steps[index]
	var/list/target_step

	if(target_index > 0 && target_index <= steps.len)
		target_step = steps[target_index]

	. = TRUE

	if(I.tool_behaviour)
		. = I.use_tool(parent, user, 0, volume=50)

	else if(diff == FORWARD)
		switch(current_step["action"])
			if(ITEM_DELETE)
				. = user.transferItemToLoc(I, parent)
				if(.)
					qdel(I)

			if(ITEM_MOVE_INSIDE)
				. = user.transferItemToLoc(I, parent)

			// Using stacks
			else if(istype(I, /obj/item/stack))
				. = I.use_tool(parent, user, 0, volume=50, amount=current_step["amount"])

	// Going backwards? Undo the last action. Drop/respawn the items used in last action, if any.
	if(. && diff == BACKWARD && target_step && !target_step["no_refund"])
		var/target_step_key = target_step["key"]

		switch(target_step["action"])
			if(ITEM_DELETE)
				new target_step_key(drop_location())

			if(ITEM_MOVE_INSIDE)
				var/obj/item/located_item = locate(target_step_key) in parent
				if(located_item)
					located_item.forceMove(drop_location())

			else if(ispath(target_step_key, /obj/item/stack))
				new target_step_key(drop_location(), target_step["amount"])

/datum/component/construction/proc/spawn_result()
	// Some constructions result in new components being added.
	if(ispath(result, /datum/component))
		parent.AddComponent(result)
		qdel(src)

	else if(ispath(result, /atom))
		new result(drop_location())
		qdel(parent)

/datum/component/construction/proc/update_parent(step_index)
	var/list/step = steps[step_index]
	var/atom/parent_atom = parent

	if(step["desc"])
		desc = step["desc"]

	if(step["icon_state"])
		parent_atom.icon_state = step["icon_state"]

/datum/component/construction/proc/drop_location()
	var/atom/parent_atom = parent
	return parent_atom.drop_location()

// Unordered construction.
// Takes a list of part types, to be added in any order, as steps.
// Calls spawn_result() when every type has been added.
/datum/component/construction/unordered/check_step(obj/item/I, mob/living/user)
	for(var/typepath in steps)
		if(istype(I, typepath) && custom_action(I, user, typepath))
			steps -= typepath
			on_step()
			return TRUE
	return FALSE

/datum/component/construction/unordered/on_step()
	if(!steps.len)
		spawn_result()
	else
		update_parent(steps.len)

/datum/component/construction/unordered/update_parent(steps_left)
	return

/datum/component/construction/unordered/custom_action(obj/item/I, mob/living/user, typepath)
	return TRUE


code:
/// Container item, an item which can be stored by specialized containers.
/datum/component/container_item

/datum/component/container_item/Initialize()
	. = ..()
	RegisterSignal(parent, COMSIG_CONTAINER_TRY_ATTACH, .proc/try_attach)

/// Called when parent is added to the container.
/datum/component/container_item/proc/try_attach(datum/source, atom/container, mob/user)
	SIGNAL_HANDLER
	return FALSE


code:
/// Tank holder item - Added to an object which can be added to a tank holder.
/datum/component/container_item/tank_holder
	var/tank_holder_icon_state
	var/make_density

/datum/component/container_item/tank_holder/Initialize(state, density = TRUE)
	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE

	tank_holder_icon_state = state
	make_density = density
	return ..()

/datum/component/container_item/tank_holder/try_attach(datum/source, obj/structure/tank_holder/container, mob/user)
	if(!container || !istype(container))
		return FALSE
	var/obj/item/I = parent
	if(container.contents.len)
		if(user)
			to_chat(user, span_warning("There's already something in [container]."))
		return TRUE
	if(user)
		if(!user.transferItemToLoc(I, container))
			return TRUE
		to_chat(user, span_notice("You put [I] into [container]."))
	else
		I.forceMove(container)
	container.tank = I
	container.set_density(make_density)
	container.icon_state = tank_holder_icon_state
	return TRUE


code:
/// Tracks damage to add or remove crack overlays, when none are needed this components is qdeleted
/datum/component/cracked
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
	var/list/icon/crack_icons
	var/crack_integrity
	var/list/applied_cracks = list()

/datum/component/cracked/Initialize(list/crack_icons, crack_integrity)
	. = ..()
	if(!isobj(parent))
		return COMPONENT_INCOMPATIBLE
	src.crack_icons = crack_icons
	src.crack_integrity = crack_integrity

/datum/component/cracked/Destroy(force, silent)
	RemoveCracks(parent, length(applied_cracks))
	return ..()

/datum/component/cracked/RegisterWithParent()
	. = ..()
	RegisterSignal(parent, COMSIG_ATOM_INTEGRITY_CHANGED, .proc/IntegrityChanged)
	var/obj/master = parent
	var/integrity = master.get_integrity()
	IntegrityChanged(parent, integrity, integrity)

/datum/component/cracked/UnregisterFromParent()
	. = ..()
	UnregisterSignal(parent, COMSIG_ATOM_INTEGRITY_CHANGED)

/datum/component/cracked/proc/IntegrityChanged(obj/source, old_value, new_value)
	SIGNAL_HANDLER
	var/cracked_max_integrity = source.max_integrity * crack_integrity
	if(new_value >= cracked_max_integrity)
		qdel(src)
		return

	var/current_percent = 1 - (new_value / cracked_max_integrity)
	var/cracks = CEILING(10 * current_percent, 1) // 1 crack per 10% integrity lost
	var/current_cracks = length(applied_cracks)
	if(!islist(source.filters))
		if(isnull(source.filters))
			source.filters = list()
		else
			source.filters = list(source.filters)

	if(current_cracks == cracks)
		return

	if(current_cracks < cracks)
		AddCracks(source, cracks - current_cracks)
	else
		RemoveCracks(source, current_cracks - cracks)

/datum/component/cracked/proc/AddCracks(obj/source, count)
	for(var/i in 1 to count)
		var/rand_x = rand(0, 20) - 10
		var/rand_y = rand(0, 20) - 10
		var/icon/new_crack_icon = pick(crack_icons)
		var/list/new_filter_data = alpha_mask_filter(icon=new_crack_icon, x=rand_x, y=rand_y, flags=MASK_INVERSE)
		var/name
		for(var/k in 1 to 100)
			name = "crack[rand(0, 999)]"
			if(!applied_cracks[name])
				break
		if(applied_cracks[name])
			CRASH("No unique name could be found after 100 iterations.")
		applied_cracks[name] = TRUE
		source.add_filter(name, 1, new_filter_data)

/datum/component/cracked/proc/RemoveCracks(obj/source, count)
	for(var/i in 1 to count)
		var/removed_filter = pick(applied_cracks)
		applied_cracks -= removed_filter
		source.remove_filter(removed_filter)