code:
///Footstep element. Plays footsteps at parents location when it is appropriate.
/datum/element/footstep
	element_flags = ELEMENT_DETACH|ELEMENT_BESPOKE
	id_arg_index = 2
	///A list containing living mobs and the number of steps they have taken since the last time their footsteps were played.
	var/list/steps_for_living = list()
	///volume determines the extra volume of the footstep. This is multiplied by the base volume, should there be one.
	var/volume
	///e_range stands for extra range - aka how far the sound can be heard. This is added to the base value and ignored if there isn't a base value.
	var/e_range
	///footstep_type is a define which determines what kind of sounds should get chosen.
	var/footstep_type
	///This can be a list OR a soundfile OR null. Determines whatever sound gets played.
	var/footstep_sounds
	///Whether or not to add variation to the sounds played
	var/sound_vary = FALSE

/datum/element/footstep/Attach(datum/target, footstep_type = FOOTSTEP_MOB_BAREFOOT, volume = 0.5, e_range = -8, sound_vary = FALSE)
	. = ..()
	if(!ismovable(target))
		return ELEMENT_INCOMPATIBLE
	src.volume = volume
	src.e_range = e_range
	src.footstep_type = footstep_type
	src.sound_vary = sound_vary
	switch(footstep_type)
		if(FOOTSTEP_MOB_HUMAN)
			if(!ishuman(target))
				return ELEMENT_INCOMPATIBLE
			RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/play_humanstep)
			steps_for_living[target] = 0
			return
		if(FOOTSTEP_MOB_CLAW)
			footstep_sounds = GLOB.clawfootstep
		if(FOOTSTEP_MOB_BAREFOOT)
			footstep_sounds = GLOB.barefootstep
		if(FOOTSTEP_MOB_HEAVY)
			footstep_sounds = GLOB.heavyfootstep
		if(FOOTSTEP_MOB_SHOE)
			footstep_sounds = GLOB.footstep
		if(FOOTSTEP_MOB_SLIME)
			footstep_sounds = 'sound/effects/footstep/slime1.ogg'
		if(FOOTSTEP_OBJ_MACHINE)
			footstep_sounds = 'sound/effects/bang.ogg'
			RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/play_simplestep_machine)
			return
		if(FOOTSTEP_OBJ_ROBOT)
			footstep_sounds = 'sound/effects/tank_treads.ogg'
			RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/play_simplestep_machine)
			return
	RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/play_simplestep)
	steps_for_living[target] = 0

/datum/element/footstep/Detach(atom/movable/source)
	UnregisterSignal(source, COMSIG_MOVABLE_MOVED)
	steps_for_living -= source
	return ..()

///Prepares a footstep for living mobs. Determines if it should get played. Returns the turf it should get played on. Note that it is always a /turf/open
/datum/element/footstep/proc/prepare_step(mob/living/source)
	var/turf/open/turf = get_turf(source)
	if(!istype(turf))
		return

	if(!turf.footstep || source.buckled || source.throwing || source.movement_type & (VENTCRAWLING | FLYING) || HAS_TRAIT(source, TRAIT_IMMOBILIZED))
		return

	if(source.body_position == LYING_DOWN) //play crawling sound if we're lying
		playsound(turf, 'sound/effects/footstep/crawl1.ogg', 15 * volume, falloff_distance = 1, vary = sound_vary)
		return

	if(iscarbon(source))
		var/mob/living/carbon/carbon_source = source
		if(!carbon_source.get_bodypart(BODY_ZONE_L_LEG) && !carbon_source.get_bodypart(BODY_ZONE_R_LEG))
			return
		if(carbon_source.m_intent == MOVE_INTENT_WALK)
			return// stealth
	steps_for_living[source] += 1
	var/steps = steps_for_living[source]

	if(steps >= 6)
		steps_for_living[source] = 0
		steps = 0

	if(steps % 2)
		return

	if(steps != 0 && !source.has_gravity(turf)) // don't need to step as often when you hop around
		return
	return turf

/datum/element/footstep/proc/play_simplestep(mob/living/source)
	SIGNAL_HANDLER

	var/turf/open/source_loc = prepare_step(source)
	if(!source_loc)
		return
	if(isfile(footstep_sounds) || istext(footstep_sounds))
		playsound(source_loc, footstep_sounds, volume, falloff_distance = 1, vary = sound_vary)
		return
	var/turf_footstep
	switch(footstep_type)
		if(FOOTSTEP_MOB_CLAW)
			turf_footstep = source_loc.clawfootstep
		if(FOOTSTEP_MOB_BAREFOOT)
			turf_footstep = source_loc.barefootstep
		if(FOOTSTEP_MOB_HEAVY)
			turf_footstep = source_loc.heavyfootstep
		if(FOOTSTEP_MOB_SHOE)
			turf_footstep = source_loc.footstep
	if(!turf_footstep)
		return
	playsound(source_loc, pick(footstep_sounds[turf_footstep][1]), footstep_sounds[turf_footstep][2] * volume, TRUE, footstep_sounds[turf_footstep][3] + e_range, falloff_distance = 1, vary = sound_vary)

/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source)
	SIGNAL_HANDLER

	if(HAS_TRAIT(source, TRAIT_SILENT_FOOTSTEPS))
		return

	var/volume_multiplier = 1
	var/range_adjustment = 0

	if(HAS_TRAIT(source, TRAIT_LIGHT_STEP))
		volume_multiplier = 0.6
		range_adjustment = -2

	var/turf/open/source_loc = prepare_step(source)
	if(!source_loc)
		return

	if ((source.wear_suit?.body_parts_covered | source.w_uniform?.body_parts_covered | source.shoes?.body_parts_covered) & FEET)
		// we are wearing shoes
		playsound(source_loc, pick(GLOB.footstep[source_loc.footstep][1]),
			GLOB.footstep[source_loc.footstep][2] * volume * volume_multiplier,
			TRUE,
			GLOB.footstep[source_loc.footstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary)
	else
		if(source.dna.species.special_step_sounds)
			playsound(source_loc, pick(source.dna.species.special_step_sounds), 50, TRUE, falloff_distance = 1, vary = sound_vary)
		else
			playsound(source_loc, pick(GLOB.barefootstep[source_loc.barefootstep][1]),
				GLOB.barefootstep[source_loc.barefootstep][2] * volume * volume_multiplier,
				TRUE,
				GLOB.barefootstep[source_loc.barefootstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary)

///Prepares a footstep for machine walking
/datum/element/footstep/proc/play_simplestep_machine(atom/movable/source)
	SIGNAL_HANDLER

	var/turf/open/source_loc = get_turf(source)
	if(!istype(source_loc))
		return
	playsound(source_loc, footstep_sounds, 50, falloff_distance = 1, vary = sound_vary)


code:
/datum/element/forced_gravity
	element_flags = ELEMENT_BESPOKE
	id_arg_index = 2
	var/gravity
	var/ignore_space

/datum/element/forced_gravity/Attach(datum/target, gravity=1, ignore_space=FALSE)
	. = ..()
	if(!isatom(target))
		return ELEMENT_INCOMPATIBLE

	src.gravity = gravity
	src.ignore_space = ignore_space

	RegisterSignal(target, COMSIG_ATOM_HAS_GRAVITY, .proc/gravity_check)
	if(isturf(target))
		RegisterSignal(target, COMSIG_TURF_HAS_GRAVITY, .proc/turf_gravity_check)

/datum/element/forced_gravity/Detach(datum/source)
	. = ..()
	var/static/list/signals_b_gone = list(COMSIG_ATOM_HAS_GRAVITY, COMSIG_TURF_HAS_GRAVITY)
	UnregisterSignal(source, signals_b_gone)

/datum/element/forced_gravity/proc/gravity_check(datum/source, turf/location, list/gravs)
	SIGNAL_HANDLER

	if(!ignore_space && isspaceturf(location))
		return
	gravs += gravity

/datum/element/forced_gravity/proc/turf_gravity_check(datum/source, atom/checker, list/gravs)
	SIGNAL_HANDLER

	return gravity_check(null, source, gravs)

repo_name: AnalWerewolf/sundown

license: mit


code:
///Attaching this element to something will make it float, get a special ai controller, and gives it a spooky outline.
/datum/element/haunted
	element_flags = ELEMENT_DETACH

/datum/element/haunted/Attach(datum/target)
	. = ..()
	if(!isitem(target))
		return COMPONENT_INCOMPATIBLE
	//Make em look spooky
	var/obj/item/master = target
	master.add_filter("haunt_glow", 2, list("type" = "outline", "color" = "#f8f8ff", "size" = 1))
	master.ai_controller = new /datum/ai_controller/haunted(master)
	master.AddElement(/datum/element/movetype_handler)
	ADD_TRAIT(master, TRAIT_MOVE_FLYING, ELEMENT_TRAIT)

/datum/element/haunted/Detach(datum/source)
	. = ..()
	var/atom/movable/master = source
	master.remove_filter("haunt_glow")
	QDEL_NULL(master.ai_controller)
	REMOVE_TRAIT(master, TRAIT_MOVE_FLYING, ELEMENT_TRAIT)
	master.RemoveElement(/datum/element/movetype_handler)
	return ..()


code:
/// Attachable to items. Plays a bikehorn sound whenever attack_self is called (with a cooldown).
/datum/element/honkspam
	element_flags = ELEMENT_DETACH

/datum/element/honkspam/Attach(datum/target)
	. = ..()
	if(!isitem(target))
		return ELEMENT_INCOMPATIBLE
	RegisterSignal(target, COMSIG_ITEM_ATTACK_SELF, .proc/interact)

/datum/element/honkspam/Detach(datum/source)
	UnregisterSignal(source, COMSIG_ITEM_ATTACK_SELF)
	return ..()

/datum/element/honkspam/proc/interact(obj/item/source, mob/user)
	SIGNAL_HANDLER
	if(HAS_TRAIT(source, TRAIT_HONKSPAMMING))
		return
	ADD_TRAIT(source, TRAIT_HONKSPAMMING, ELEMENT_TRAIT)
	playsound(source.loc, 'sound/items/bikehorn.ogg', 50, TRUE)
	addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_HONKSPAMMING, ELEMENT_TRAIT), 2 SECONDS)


code:
/**
 * Element for scaling item appearances in the overworld or in inventory/storage.
 *
 * This bespoke element allows for items to have varying sizes depending on their location.
 * The overworld simply refers to items being on a turf.  Inventory includes HUD item slots,
 * and storage is anywhere a storage component is used.
 * Scaling should affect the item's icon and all attached overlays (such as blood decals).
 *
 */
/datum/element/item_scaling
	element_flags = ELEMENT_BESPOKE
	id_arg_index = 2
	/// Scaling value when the attached item is in the overworld (on a turf).
	var/overworld_scaling
	/// Scaling value when the attached item is in a storage component or inventory slot.
	var/storage_scaling

/**
 * Attach proc for the item_scaling element
 *
 * The proc checks the target's type before attaching.  It then initializes
 * the target to overworld scaling.  The target should then rescale if it is placed
 * in inventory/storage on initialization.  Relevant signals are registered to listen
 * for pickup/drop or storage events.  Scaling values of 1 will result in items
 * returning to their original size.
 * Arguments:
 * * target - Datum to attach the element to.
 * * overworld_scaling - Integer or float to scale the item in the overworld.
 * * storage_scaling - Integer or float to scale the item in storage/inventory.
 */
/datum/element/item_scaling/Attach(datum/target, overworld_scaling, storage_scaling)
	. = ..()
	if(!isatom(target))
		return ELEMENT_INCOMPATIBLE
	// Initial scaling set to overworld_scaling when item is spawned.
	scale(target, overworld_scaling)

	src.overworld_scaling = overworld_scaling
	src.storage_scaling = storage_scaling

	// Object scaled when dropped/thrown OR when exiting a storage component.
	RegisterSignal(target, list(COMSIG_ITEM_DROPPED, COMSIG_STORAGE_EXITED), .proc/scale_overworld)
	// Object scaled when placed in an inventory slot OR when entering a storage component.
	RegisterSignal(target, list(COMSIG_ITEM_EQUIPPED, COMSIG_STORAGE_ENTERED), .proc/scale_storage)

/**
 * Detach proc for the item_scaling element.
 *
 * All registered signals are unregistered, and the attached element is removed from the target datum.
 * Arguments:
 * * target - Datum which the element is attached to.
 */
/datum/element/item_scaling/Detach(datum/target)
	UnregisterSignal(target, list(
		COMSIG_ITEM_PICKUP,
		COMSIG_ITEM_DROPPED,
		COMSIG_STORAGE_ENTERED,
		COMSIG_STORAGE_EXITED,
	))
	return ..()

/**
 * Scales the attached item's matrix.
 *
 * The proc first narrows the type of the source to (datums do not have a transform matrix).
 * It then creates an identity matrix, M, which is transformed by the scaling value.
 * The object's transform variable (matrix) is then set to the resulting value of M.
 * Arguments:
 * * source - Source datum which sent the signal.
 * * scaling - Integer or float to scale the item's matrix.
 */
/datum/element/item_scaling/proc/scale(datum/source, scaling)
	var/atom/scalable_object = source
	var/matrix/M = matrix()
	scalable_object.transform = M.Scale(scaling)

/**
 * Signal handler for COMSIG_ITEM_DROPPED or COMSIG_STORAGE_EXITED
 *
 * Longer detailed paragraph about the proc
 * including any relevant detail
 * Arguments:
 * * source - Source datum which sent the signal.
 */
/datum/element/item_scaling/proc/scale_overworld(datum/source)
	SIGNAL_HANDLER

	scale(source, overworld_scaling)

/**
 * Signal handler for COMSIG_ITEM_EQUIPPED or COMSIG_STORAGE_ENTERED.
 *
 * Longer detailed paragraph about the proc
 * including any relevant detail
 * Arguments:
 * * source - Source datum which sent the signal.
 */
/datum/element/item_scaling/proc/scale_storage(datum/source)
	SIGNAL_HANDLER

	scale(source, storage_scaling)


code:
/**
 * Kneecapping element replaces the item's secondary attack with an aimed attack at the kneecaps under certain circumstances.
 *
 * Element is incompatible with non-items. Requires the parent item to have a force equal to or greater than WOUND_MINIMUM_DAMAGE.
 * Also requires that the parent can actually get past pre_secondary_attack without the attack chain cancelling.
 *
 * Kneecapping attacks have a wounding bonus between severe and critical+10 wound thresholds. Without some serious wound protecting
 * armour this all but guarantees a wound of some sort. The attack is directed specifically at a limb and the limb takes the damage.
 *
 * Requires the attacker to be aiming for either leg zone, which will be targetted specifically. They will than have a 3-second long
 * do_mob before executing the attack.
 *
 * Kneecapping requires the target to either be on the floor, immobilised or buckled to something. And also to have an appropriate leg.
 *
 * Passing all the checks will cancel the entire attack chain.
 */
/datum/element/kneecapping
	element_flags = ELEMENT_DETACH

/datum/element/kneecapping/Attach(datum/target)
	if(!isitem(target))
		stack_trace("Kneecapping element added to non-item object: \[[target]\]")
		return ELEMENT_INCOMPATIBLE

	var/obj/item/target_item = target

	if(target_item.force < WOUND_MINIMUM_DAMAGE)
		stack_trace("Kneecapping element added to item with too little force to wound: \[[target]\]")
		return ELEMENT_INCOMPATIBLE

	. = ..()

	if(. == ELEMENT_INCOMPATIBLE)
		return

	RegisterSignal(target, COMSIG_ITEM_ATTACK_SECONDARY , .proc/try_kneecap_target)

/datum/element/kneecapping/Detach(datum/target)
	UnregisterSignal(target, COMSIG_ITEM_ATTACK_SECONDARY)

	return ..()

/**
 * Signal handler for COMSIG_ITEM_ATTACK_SECONDARY. Does checks for pacifism, zones and target state before either returning nothing
 * if the special attack could not be attempted, performing the ordinary attack procs instead - Or cancelling the attack chain if
 * the attack can be started.
 */
/datum/element/kneecapping/proc/try_kneecap_target(obj/item/source, mob/living/carbon/target, mob/attacker, params)
	SIGNAL_HANDLER

	if((attacker.zone_selected != BODY_ZONE_L_LEG) && (attacker.zone_selected != BODY_ZONE_R_LEG))
		return

	if(HAS_TRAIT(attacker, TRAIT_PACIFISM))
		return

	if(!iscarbon(target))
		return

	if(!target.buckled && !HAS_TRAIT(target, TRAIT_FLOORED) && !HAS_TRAIT(target, TRAIT_IMMOBILIZED))
		return

	var/obj/item/bodypart/leg = target.get_bodypart(attacker.zone_selected)

	if(!leg)
		return

	. = COMPONENT_SECONDARY_CANCEL_ATTACK_CHAIN

	INVOKE_ASYNC(src, .proc/do_kneecap_target, source, leg, target, attacker)

/**
 * After a short do_mob, attacker applies damage to the given leg with a significant wounding bonus, applying the weapon's force as damage.
 */
/datum/element/kneecapping/proc/do_kneecap_target(obj/item/weapon, obj/item/bodypart/leg, mob/living/carbon/target, mob/attacker)
	if(LAZYACCESS(attacker.do_afters, weapon))
		return

	attacker.visible_message(span_warning("[attacker] carefully aims [attacker.p_their()] [weapon] for a swing at [target]'s kneecaps!"), span_danger("You carefully aim \the [weapon] for a swing at [target]'s kneecaps!"))
	log_combat(attacker, target, "started aiming a swing to break the kneecaps of", weapon)

	if(do_mob(attacker, target, 3 SECONDS, interaction_key = weapon))
		attacker.visible_message(span_warning("[attacker] swings [attacker.p_their()] [weapon] at [target]'s kneecaps!"), span_danger("You swing \the [weapon] at [target]'s kneecaps!"))
		var/datum/wound/blunt/severe/severe_wound_type = /datum/wound/blunt/severe
		var/datum/wound/blunt/critical/critical_wound_type = /datum/wound/blunt/critical
		leg.receive_damage(brute = weapon.force, wound_bonus = rand(initial(severe_wound_type.threshold_minimum), initial(critical_wound_type.threshold_minimum) + 10))
		log_combat(attacker, target, "broke the kneecaps of", weapon)
		target.update_damage_overlays()
		attacker.do_attack_animation(target, used_item = weapon)
		playsound(source = get_turf(weapon), soundin = weapon.hitsound, vol = weapon.get_clamped_volume(), vary = TRUE)


code:
/// An element which enables certain items to tap people on their knees to measure brain health
/datum/element/kneejerk
	element_flags = ELEMENT_DETACH

/datum/element/kneejerk/Attach(datum/target)
	. = ..()

	if (!isitem(target))
		return ELEMENT_INCOMPATIBLE

	RegisterSignal(target, COMSIG_ITEM_ATTACK, .proc/on_item_attack)

/datum/element/kneejerk/Detach(datum/source, ...)
	. = ..()

	UnregisterSignal(source, COMSIG_ITEM_ATTACK)

/datum/element/kneejerk/proc/on_item_attack(datum/source, mob/living/target, mob/living/user, params)
	SIGNAL_HANDLER

	var/list/modifiers = params2list(params)

	if((user.zone_selected == BODY_ZONE_L_LEG || user.zone_selected == BODY_ZONE_R_LEG) && LAZYACCESS(modifiers, RIGHT_CLICK) && target.buckled)
		tap_knee(source, target, user)

		return COMPONENT_SKIP_ATTACK

/datum/element/kneejerk/proc/tap_knee(obj/item/item, mob/living/target, mob/living/user)
	var/selected_zone = user.zone_selected
	var/obj/item/bodypart/r_leg = target.get_bodypart(BODY_ZONE_R_LEG)
	var/obj/item/bodypart/l_leg = target.get_bodypart(BODY_ZONE_L_LEG)
	var/obj/item/organ/brain/target_brain = target.getorganslot(ORGAN_SLOT_BRAIN)

	if(!ishuman(target))
		return

	if((selected_zone == BODY_ZONE_R_LEG) && !r_leg)
		return
	if((selected_zone == BODY_ZONE_L_LEG) && !l_leg)
		return

	user.do_attack_animation(target)
	target.visible_message(span_warning("[user] gently taps [target]'s knee with [item]."), \
		span_userdanger("[user] taps your knee with [item]."))

	if(target.stat == DEAD) //dead men have no reflexes!
		return

	if(!target_brain)
		return

	var/target_brain_damage = target_brain.damage

	if(target_brain_damage < BRAIN_DAMAGE_MILD) //a healthy brain produces a normal reaction
		playsound(target, 'sound/weapons/punchmiss.ogg', 25, TRUE, -1)
		target.visible_message(span_danger("[target]'s leg kicks out sharply!"), \
			span_danger("Your leg kicks out sharply!"))

	else if(target_brain_damage < BRAIN_DAMAGE_SEVERE) //a mildly damaged brain produces a delayed reaction
		playsound(target, 'sound/weapons/punchmiss.ogg', 15, TRUE, -1)
		target.visible_message(span_danger("After a moment, [target]'s leg kicks out sharply!"), \
			span_danger("After a moment, your leg kicks out sharply!"))

	else if(target_brain_damage < BRAIN_DAMAGE_DEATH) //a severely damaged brain produces a delayed + weaker reaction
		playsound(target, 'sound/weapons/punchmiss.ogg', 5, TRUE, -1)
		target.visible_message(span_danger("After a moment, [target]'s leg kicks out weakly!"), \
			span_danger("After a moment, your leg kicks out weakly!"))

	return


code:
/**
 * Mobs and items with this element will knock movable targets they hit away from them.
 * Guns and turrets will instead fire projectiles with similar effects.
 */
/datum/element/knockback
	/// distance the atom will be thrown
	var/throw_distance
	/// whether this can throw anchored targets (tables, etc)
	var/throw_anchored
	/// whether this is a gentle throw (default false means people thrown into walls are stunned / take damage)
	var/throw_gentle

/datum/element/knockback/Attach(datum/target, throw_distance = 1, throw_anchored = FALSE, throw_gentle = FALSE)
	. = ..()
	if(ismachinery(target) || isstructure(target) || isgun(target)) // turrets, etc
		RegisterSignal(target, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit)
	else if(isitem(target))
		RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, .proc/item_afterattack)
	else if(ishostile(target))
		RegisterSignal(target, COMSIG_HOSTILE_POST_ATTACKINGTARGET, .proc/hostile_attackingtarget)
	else
		return ELEMENT_INCOMPATIBLE

	src.throw_distance = throw_distance
	src.throw_anchored = throw_anchored
	src.throw_gentle = throw_gentle

/datum/element/knockback/Detach(datum/source)
	UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT))
	return ..()

/// triggered after an item attacks something
/datum/element/knockback/proc/item_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters)
	SIGNAL_HANDLER

	if(!proximity_flag)
		return
	do_knockback(target, user, get_dir(source, target))

/// triggered after a hostile simplemob attacks something
/datum/element/knockback/proc/hostile_attackingtarget(mob/living/simple_animal/hostile/attacker, atom/target, success)
	SIGNAL_HANDLER
	if(!success)
		return
	do_knockback(target, attacker, get_dir(attacker, target))

/// triggered after a projectile hits something
/datum/element/knockback/proc/projectile_hit(atom/fired_from, atom/movable/firer, atom/target, Angle)
	SIGNAL_HANDLER

	do_knockback(target, null, angle2dir(Angle))

/**
 * Throw a target in a direction
 *
 * Arguments:
 * * target - Target atom to throw
 * * thrower - Thing that caused this atom to be thrown
 * * throw_dir - Direction to throw the atom
 */
/datum/element/knockback/proc/do_knockback(atom/target, mob/thrower, throw_dir)
	if(!ismovable(target) || throw_dir == null)
		return
	var/atom/movable/throwee = target
	if(throwee.anchored && !throw_anchored)
		return
	if(throw_distance < 0)
		throw_dir = turn(throw_dir, 180)
		throw_distance *= -1
	var/atom/throw_target = get_edge_target_turf(throwee, throw_dir)
	throwee.safe_throw_at(throw_target, throw_distance, 1, thrower, gentle = throw_gentle)


code:
/**
 * Heals the user (if attached to an item) or the mob itself (if attached to a hostile simple mob)
 * by a flat amount whenever a successful attack is performed against another living mob.
 */
/datum/element/lifesteal
	element_flags = ELEMENT_DETACH|ELEMENT_BESPOKE
	id_arg_index = 2
	/// heals a constant amount every time a hit occurs
	var/flat_heal
	var/static/list/damage_heal_order = list(BRUTE, BURN, OXY)

/datum/element/lifesteal/Attach(datum/target, flat_heal)
	. = ..()
	if(isgun(target))
		RegisterSignal(target, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit)
	else if(isitem(target))
		RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, .proc/item_afterattack)
	else if(ishostile(target))
		RegisterSignal(target, COMSIG_HOSTILE_POST_ATTACKINGTARGET, .proc/hostile_attackingtarget)
	else
		return ELEMENT_INCOMPATIBLE

	src.flat_heal = flat_heal

/datum/element/lifesteal/Detach(datum/source)
	UnregisterSignal(source, list(COMSIG_PROJECTILE_ON_HIT, COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET))
	return ..()

/datum/element/lifesteal/proc/item_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters)
	SIGNAL_HANDLER

	if(!proximity_flag)
		return
	do_lifesteal(user, target)

/datum/element/lifesteal/proc/hostile_attackingtarget(mob/living/simple_animal/hostile/attacker, atom/target, success)
	SIGNAL_HANDLER

	if(!success)
		return
	do_lifesteal(attacker, target)

/datum/element/lifesteal/proc/projectile_hit(atom/fired_from, atom/movable/firer, atom/target, Angle)
	SIGNAL_HANDLER

	do_lifesteal(firer, target)

/datum/element/lifesteal/proc/do_lifesteal(atom/heal_target, atom/damage_target)
	if(isliving(heal_target) && isliving(damage_target))
		var/mob/living/healing = heal_target
		var/mob/living/damaging = damage_target
		if(damaging.stat != DEAD)
			healing.heal_ordered_damage(flat_heal, damage_heal_order)


code:
/**
 * Attached to movable atoms with opacity. Listens to them move and updates their old and new turf loc's opacity accordingly.
 */
/datum/element/light_blocking
	element_flags = ELEMENT_DETACH

/datum/element/light_blocking/Attach(datum/target)
	. = ..()
	if(!ismovable(target))
		return ELEMENT_INCOMPATIBLE
	RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/on_target_move)
	var/atom/movable/movable_target = target
	if(!isturf(movable_target.loc))
		return
	for(var/turf/turf_loc as anything in movable_target.locs)
		turf_loc.add_opacity_source(target)

/datum/element/light_blocking/Detach(datum/target)
	. = ..()
	UnregisterSignal(target, list(COMSIG_MOVABLE_MOVED))
	var/atom/movable/movable_target = target
	if(!isturf(movable_target.loc))
		return
	for(var/turf/turf_loc as anything in movable_target.locs)
		turf_loc.remove_opacity_source(target)

///Updates old and new turf loc opacities.
/datum/element/light_blocking/proc/on_target_move(atom/movable/source, atom/old_loc, dir, forced, list/old_locs)
	SIGNAL_HANDLER
	if(isturf(old_loc))
		if(old_locs)
			for(var/turf/old_turf as anything in old_locs)
				old_turf.remove_opacity_source(source)
		else
			var/turf/old_turf = old_loc
			old_turf.remove_opacity_source(source)
	if(isturf(source.loc))
		for(var/turf/new_turf as anything in source.locs)
			new_turf.add_opacity_source(source)


code:
/**
 * Makes anything that it attaches to incapable of producing light
 */
/datum/element/light_eaten
	element_flags = ELEMENT_DETACH

/datum/element/light_eaten/Attach(atom/target)
	if(!isatom(target))
		return ELEMENT_INCOMPATIBLE

	. = ..()
	var/atom/atom_target = target
	RegisterSignal(atom_target, COMSIG_ATOM_SET_LIGHT_POWER, .proc/block_light_power)
	RegisterSignal(atom_target, COMSIG_ATOM_SET_LIGHT_RANGE, .proc/block_light_range)
	RegisterSignal(atom_target, COMSIG_ATOM_SET_LIGHT_ON, .proc/block_light_on)
	RegisterSignal(atom_target, COMSIG_PARENT_EXAMINE, .proc/on_examine)

	/// Because the lighting system does not like movable lights getting set_light() called.
	switch(atom_target.light_system)
		if(STATIC_LIGHT)
			target.set_light(0, 0, null, FALSE)
		else
			target.set_light_power(0)
			target.set_light_range(0)
			target.set_light_on(FALSE)

/datum/element/light_eaten/Detach(datum/source)
	UnregisterSignal(source, list(
		COMSIG_ATOM_SET_LIGHT_POWER,
		COMSIG_ATOM_SET_LIGHT_RANGE,
		COMSIG_ATOM_SET_LIGHT_ON,
		COMSIG_PARENT_EXAMINE,
	))
	return ..()

/// Prevents the light power of the target atom from exceeding 0 or increasing.
/datum/element/light_eaten/proc/block_light_power(atom/eaten_light, new_power)
	SIGNAL_HANDLER
	if(new_power > 0)
		return COMPONENT_BLOCK_LIGHT_UPDATE
	if(new_power > eaten_light.light_power)
		return COMPONENT_BLOCK_LIGHT_UPDATE
	return NONE

/// Prevents the light range of the target atom from exceeding 0 while the light power is greater than 0.
/datum/element/light_eaten/proc/block_light_range(atom/eaten_light, new_range)
	SIGNAL_HANDLER
	if(new_range <= 0)
		return NONE
	if(eaten_light.light_power <= 0)
		return NONE
	return COMPONENT_BLOCK_LIGHT_UPDATE

/// Prevents the light from turning on while the light power is greater than 0.
/datum/element/light_eaten/proc/block_light_on(atom/eaten_light, new_on)
	SIGNAL_HANDLER
	if(!new_on)
		return NONE
	if(eaten_light.light_power <= 0)
		return NONE
	return COMPONENT_BLOCK_LIGHT_UPDATE

/// Signal handler for light eater flavortext
/datum/element/light_eaten/proc/on_examine(atom/eaten_light, mob/examiner, list/examine_text)
	SIGNAL_HANDLER
	examine_text += span_warning("It's dark and empty...")
	if(isliving(examiner) && prob(20))
		var/mob/living/target = examiner
		examine_text += span_danger("You can feel something in [eaten_light.p_them()] gnash at your eyes!")
		target.blind_eyes(5)
		target.blur_eyes(10)
	return NONE


code:
/**
 * Makes anything it attaches to capable of permanently removing something's ability to produce light.
 *
 * The temporary equivalent is [/datum/component/light_eater]
 */
/datum/element/light_eater
	element_flags = ELEMENT_DETACH

/datum/element/light_eater/Attach(datum/target)
	if(isatom(target))
		if(ismovable(target))
			RegisterSignal(target, COMSIG_MOVABLE_IMPACT, .proc/on_throw_impact)
			if(isitem(target))
				RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, .proc/on_afterattack)
				RegisterSignal(target, COMSIG_ITEM_HIT_REACT, .proc/on_hit_reaction)
			else if(isprojectile(target))
				RegisterSignal(target, COMSIG_PROJECTILE_ON_HIT, .proc/on_projectile_hit)
	else if(istype(target, /datum/reagent))
		RegisterSignal(target, COMSIG_REAGENT_EXPOSE_ATOM, .proc/on_expose_atom)
	else
		return ELEMENT_INCOMPATIBLE

	return ..()

/datum/element/light_eater/Detach(datum/source)
	UnregisterSignal(source, list(
		COMSIG_MOVABLE_IMPACT,
		COMSIG_ITEM_AFTERATTACK,
		COMSIG_ITEM_HIT_REACT,
		COMSIG_PROJECTILE_ON_HIT,
		COMSIG_REAGENT_EXPOSE_ATOM,
	))
	return ..()

/**
 * Makes the light eater consume all of the lights attached to the target atom.
 *
 * Arguments:
 * - [food][/atom]: The atom to start the search for lights at.
 * - [eater][/datum]: The light eater being used in this case.
 */
/datum/element/light_eater/proc/eat_lights(atom/food, datum/eater)
	var/list/buffet = table_buffet(food)
	if(!LAZYLEN(buffet))
		return 0

	. = 0
	for(var/morsel in buffet)
		. += devour(morsel, eater)

	if(!.)
		return

	food.visible_message(
		span_danger("Something dark in [eater] lashes out at [food] and [food.p_their()] light goes out in an instant!"),
		span_userdanger("You feel something dark in [eater] lash out and gnaw through your light in an instant! It recedes just as fast, but you can feel that [eater.p_theyve()] left something hungry behind."),
		span_danger("You feel a gnawing pulse eat at your sight.")
	)

/**
 * Aggregates a list of the light sources attached to the target atom.
 *
 * Arguments:
 * - [comissary][/atom]: The origin node of all of the light sources to search through.
 * - [devourer][/datum]: The light eater this element is attached to. Since the element is compatible with reagents this needs to be a datum.
 */
/datum/element/light_eater/proc/table_buffet(atom/commisary, datum/devourer)
	. = list()
	SEND_SIGNAL(commisary, COMSIG_LIGHT_EATER_QUEUE, ., devourer)
	for(var/nom in commisary.light_sources)
		var/datum/light_source/morsel = nom
		.[morsel.source_atom] = TRUE

/**
 * Consumes the light on the target, permanently rendering it incapable of producing light
 *
 * Arguments:
 * - [morsel][/atom]: The light-producing thing we are eating
 * - [eater][/datum]: The light eater eating the morsel. This is the datum that the element is attached to that started this chain.
 */
/datum/element/light_eater/proc/devour(atom/morsel, datum/eater)
	if(morsel.light_power <= 0 || morsel.light_range <= 0 || !morsel.light_on)
		return FALSE
	if(SEND_SIGNAL(morsel, COMSIG_LIGHT_EATER_ACT, eater) & COMPONENT_BLOCK_LIGHT_EATER)
		return FALSE // Either the light eater can't eat it or it had special behaviors.

	morsel.AddElement(/datum/element/light_eaten)
	SEND_SIGNAL(src, COMSIG_LIGHT_EATER_DEVOUR, morsel)
	return TRUE

/////////////////////
// SIGNAL HANDLERS //
/////////////////////

/**
 * Called when a movable source is thrown and strikes a target
 *
 * Arugments:
 * - [source][/atom/movable]: The movable atom that was thrown
 * - [hit_atom][/atom]: The target atom that was struck by the source in flight
 * - [thrownthing][/datum/thrownthing]: A datum containing the information for the throw
 */
/datum/element/light_eater/proc/on_throw_impact(atom/movable/source, atom/hit_atom, datum/thrownthing/thrownthing)
	SIGNAL_HANDLER
	eat_lights(hit_atom, source)
	return NONE

/**
 * Called when a target is attacked with a source item
 *
 * Arguments:
 * - [source][/obj/item]: The item what was used to strike the target
 * - [target][/atom]: The atom being struck by the user with the source
 * - [user][/mob/living]: The mob using the source to strike the target
 * - proximity: Whether the strike was in melee range so you can't eat lights from cameras
 */
/datum/element/light_eater/proc/on_afterattack(obj/item/source, atom/target, mob/living/user, proximity)
	SIGNAL_HANDLER
	if(!proximity)
		return NONE
	eat_lights(target, source)
	return NONE

/**
 * Called when a source object is used to block a thrown object, projectile, or attack
 *
 * Arguments:
 * - [source][/obj/item]: The item what was used to block the target
 * - [owner][/mob/living/carbon/human]: The mob that blocked the target with the source
 * - [hitby][/atom/movable]: The movable that was blocked by the owner with the source
 * - attack_text: The text tring that will be used to report that the target was blocked
 * - final_block_chance: The probability of blocking the target with the source
 * - attack_type: The type of attack that was blocked
 */
/datum/element/light_eater/proc/on_hit_reaction(obj/item/source, mob/living/carbon/human/owner, atom/movable/hitby, attack_text, final_block_chance, damage, attack_type)
	SIGNAL_HANDLER
	if(prob(final_block_chance))
		eat_lights(hitby, source)
	return NONE

/**
 * Called when a source projectile strikes a target atom
 *
 * Arguments:
 * - [source][/obj/projectile]: The projectile striking the target atom
 * - [firer][/atom/movable]: The movable atom that fired the projectile
 * - [target][/atom]: The atom that was struck by the projectile
 * - angle: The angle the target was struck at
 */
/datum/element/light_eater/proc/on_projectile_hit(obj/projectile/source, atom/movable/firer, atom/target, angle)
	SIGNAL_HANDLER
	eat_lights(target, source)
	return NONE

/**
 * Called when a source reagent exposes a target atom
 *
 * Arguments:
 * - [source][/datum/reagent]: The reagents that exposed the target atom
 * - [target][/atom]: The atom that was exposed to the light reater reagents
 * - reac_volume: The volume of the reagents the target was exposed to
 */
/datum/element/light_eater/proc/on_expose_atom(datum/reagent/source, atom/target, reac_volume)
	SIGNAL_HANDLER
	eat_lights(target, source)
	return NONE


code:
/**
 * movement_turf_changer element; which makes the movement of a movable atom change the turf it moved to
 *
 * Used for moonicorns!
 */
/datum/element/movement_turf_changer
	element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
	id_arg_index = 2
	///Path of the turf added on top
	var/turf_type

/datum/element/movement_turf_changer/Attach(datum/target, turf_type)
	. = ..()

	if(!ismovable(target))
		return ELEMENT_INCOMPATIBLE

	src.turf_type = turf_type
	RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/on_moved)

/datum/element/movement_turf_changer/Detach(datum/target)
	UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
	. = ..()

/datum/element/movement_turf_changer/proc/on_moved(atom/movable/target, atom/origin, direction, forced)
	SIGNAL_HANDLER

	var/turf/destination = target.loc
	if(!isturf(destination) || istype(destination, turf_type))
		return

	destination.PlaceOnTop(turf_type)


code:
#define DO_FLOATING_ANIM(target) \
	animate(target, pixel_y = 2, time = 1 SECONDS, loop = -1, flags = ANIMATION_RELATIVE);\
	animate(pixel_y = -2, time = 1 SECONDS, flags = ANIMATION_RELATIVE)

/**
 * An element that enables and disables movetype bitflags whenever the relative traits are added or removed.
 * It also handles the +2/-2 pixel y anim loop typical of mobs possessing the FLYING or FLOATING movetypes.
 * This element is necessary for the TRAIT_MOVE_ traits to work correctly, so make sure to attach this element
 * before adding them to non-living movables.
 */
/datum/element/movetype_handler
	element_flags = ELEMENT_DETACH

	var/list/attached_atoms = list()
	var/list/paused_floating_anim_atoms = list()

/datum/element/movetype_handler/Attach(datum/target)
	. = ..()
	if(!ismovable(target))
		return ELEMENT_INCOMPATIBLE
	if(attached_atoms[target]) //Already attached.
		return

	var/atom/movable/movable_target = target
	RegisterSignal(movable_target, GLOB.movement_type_addtrait_signals, .proc/on_movement_type_trait_gain)
	RegisterSignal(movable_target, GLOB.movement_type_removetrait_signals, .proc/on_movement_type_trait_loss)
	RegisterSignal(movable_target, SIGNAL_ADDTRAIT(TRAIT_NO_FLOATING_ANIM), .proc/on_no_floating_anim_trait_gain)
	RegisterSignal(movable_target, SIGNAL_REMOVETRAIT(TRAIT_NO_FLOATING_ANIM), .proc/on_no_floating_anim_trait_loss)
	RegisterSignal(movable_target, COMSIG_PAUSE_FLOATING_ANIM, .proc/pause_floating_anim)
	attached_atoms[movable_target] = TRUE

	if(movable_target.movement_type & (FLOATING|FLYING) && !HAS_TRAIT(movable_target, TRAIT_NO_FLOATING_ANIM))
		DO_FLOATING_ANIM(movable_target)

/datum/element/movetype_handler/Detach(datum/source)
	var/list/signals_to_remove = list(
		SIGNAL_ADDTRAIT(TRAIT_NO_FLOATING_ANIM),
		SIGNAL_REMOVETRAIT(TRAIT_NO_FLOATING_ANIM),
		COMSIG_PAUSE_FLOATING_ANIM
	)
	signals_to_remove += GLOB.movement_type_addtrait_signals
	signals_to_remove += GLOB.movement_type_removetrait_signals
	UnregisterSignal(source, signals_to_remove)

	attached_atoms -= source
	paused_floating_anim_atoms -= source
	stop_floating(source)
	return ..()

/// Called when a movement type trait is added to the movable. Enables the relative bitflag.
/datum/element/movetype_handler/proc/on_movement_type_trait_gain(atom/movable/source, trait)
	SIGNAL_HANDLER
	var/flag = GLOB.movement_type_trait_to_flag[trait]
	if(source.movement_type & flag)
		return
	var/old_state = source.movement_type
	source.movement_type |= flag
	if(!(old_state & (FLOATING|FLYING)) && (source.movement_type & (FLOATING|FLYING)) && !paused_floating_anim_atoms[source] && !HAS_TRAIT(source, TRAIT_NO_FLOATING_ANIM))
		DO_FLOATING_ANIM(source)
	SEND_SIGNAL(source, COMSIG_MOVETYPE_FLAG_ENABLED, flag, old_state)

/// Called when a movement type trait is removed from the movable. Disables the relative bitflag if it wasn't there in the compile-time bitfield.
/datum/element/movetype_handler/proc/on_movement_type_trait_loss(atom/movable/source, trait)
	SIGNAL_HANDLER
	var/flag = GLOB.movement_type_trait_to_flag[trait]
	if(initial(source.movement_type) & flag)
		return
	var/old_state = source.movement_type
	source.movement_type &= ~flag
	if((old_state & (FLOATING|FLYING)) && !(source.movement_type & (FLOATING|FLYING)))
		stop_floating(source)
	SEND_SIGNAL(source, COMSIG_MOVETYPE_FLAG_DISABLED, flag, old_state)

/// Called when the TRAIT_NO_FLOATING_ANIM trait is added to the movable. Stops it from bobbing up and down.
/datum/element/movetype_handler/proc/on_no_floating_anim_trait_gain(atom/movable/source, trait)
	SIGNAL_HANDLER
	stop_floating(source)

/// Called when the TRAIT_NO_FLOATING_ANIM trait is removed from the mob. Restarts the bobbing animation.
/datum/element/movetype_handler/proc/on_no_floating_anim_trait_loss(atom/movable/source, trait)
	SIGNAL_HANDLER
	if(source.movement_type & (FLOATING|FLYING) && !paused_floating_anim_atoms[source])
		DO_FLOATING_ANIM(source)

///Pauses the floating animation for the duration of the timer... plus [tickrate - (world.time + timer) % tickrate] to be precise.
/datum/element/movetype_handler/proc/pause_floating_anim(atom/movable/source, timer)
	SIGNAL_HANDLER
	if(paused_floating_anim_atoms[source] < world.time + timer)
		stop_floating(source)
		if(!length(paused_floating_anim_atoms))
			START_PROCESSING(SSdcs, src) //1 second tickrate.
		paused_floating_anim_atoms[source] = world.time + timer

/datum/element/movetype_handler/process()
	for(var/_paused in paused_floating_anim_atoms)
		var/atom/movable/paused = _paused
		if(paused_floating_anim_atoms[paused] < world.time)
			if(paused.movement_type & (FLOATING|FLYING) && !HAS_TRAIT(paused, TRAIT_NO_FLOATING_ANIM))
				DO_FLOATING_ANIM(paused)
			paused_floating_anim_atoms -= paused
	if(!length(paused_floating_anim_atoms))
		STOP_PROCESSING(SSdcs, src)

/// Stops the above. Also not a comsig proc.
/datum/element/movetype_handler/proc/stop_floating(atom/movable/target)
	var/final_pixel_y = target.base_pixel_y
	if(isliving(target)) //Living mobs also have a 'body_position_pixel_y_offset' variable that has to be taken into account here.
		var/mob/living/living_target = target
		final_pixel_y += living_target.body_position_pixel_y_offset
	animate(target, pixel_y = final_pixel_y, time = 1 SECONDS)

#undef DO_FLOATING_ANIM


code:
/// This living will be slower when pulling/moving anything in the given typecache
/datum/element/nerfed_pulling
	element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
	id_arg_index = 2

	/// The typecache of things that shouldn't be easily movable
	var/list/typecache

/datum/element/nerfed_pulling/Attach(datum/target, list/typecache)
	. = ..()

	if (!isliving(target))
		return ELEMENT_INCOMPATIBLE

	src.typecache = typecache

	RegisterSignal(target, COMSIG_LIVING_PUSHING_MOVABLE, .proc/on_push_movable)
	RegisterSignal(target, COMSIG_LIVING_UPDATING_PULL_MOVESPEED, .proc/on_updating_pull_movespeed)

/datum/element/nerfed_pulling/Detach(mob/living/source)
	source.remove_movespeed_modifier(/datum/movespeed_modifier/nerfed_bump)
	source.remove_movespeed_modifier(/datum/movespeed_modifier/nerfed_pull)

	UnregisterSignal(source, list(COMSIG_LIVING_PUSHING_MOVABLE, COMSIG_LIVING_UPDATING_PULL_MOVESPEED))

	return ..()

/datum/element/nerfed_pulling/proc/on_push_movable(mob/living/source, atom/movable/being_pushed)
	SIGNAL_HANDLER

	if (!will_slow_down(being_pushed))
		return

	source.add_movespeed_modifier(/datum/movespeed_modifier/nerfed_bump)
	addtimer(CALLBACK(source, /mob/proc/remove_movespeed_modifier, /datum/movespeed_modifier/nerfed_bump), 1 SECONDS, TIMER_OVERRIDE | TIMER_UNIQUE)

/datum/element/nerfed_pulling/proc/on_updating_pull_movespeed(mob/living/source)
	SIGNAL_HANDLER

	if (!will_slow_down(source.pulling))
		source.remove_movespeed_modifier(/datum/movespeed_modifier/nerfed_pull)
		return

	source.add_movespeed_modifier(/datum/movespeed_modifier/nerfed_pull)

/datum/element/nerfed_pulling/proc/will_slow_down(datum/input)
	return !isnull(input) && typecache[input.type]

/datum/movespeed_modifier/nerfed_pull
	multiplicative_slowdown = 5.5

/datum/movespeed_modifier/nerfed_bump
	multiplicative_slowdown = 5.5


code:
/** Object integrity regeneration element added by alien alloy.
 */
/datum/element/obj_regen
	element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
	id_arg_index = 2
	/// The rate of regeneration as a function of maximum integrity.
	var/rate
	/// The objects that are regenerating due to this element.
	var/list/processing = list()
	/// The current stack of objects we are processing.
	var/list/currentrun
	/// Whether we stopped processing early the last tick.
	var/resumed = FALSE

/datum/element/obj_regen/Attach(obj/target, _rate=0)
	. = ..()
	if(!istype(target))
		return ELEMENT_INCOMPATIBLE
	if(_rate <= 0)
		return ELEMENT_INCOMPATIBLE

	rate = _rate
	RegisterSignal(target, COMSIG_ATOM_TAKE_DAMAGE, .proc/on_take_damage)
	if(target.get_integrity() < target.max_integrity)
		if(!length(processing))
			START_PROCESSING(SSobj, src)
		processing |= target

/datum/element/obj_regen/Detach(obj/target)
	UnregisterSignal(target, COMSIG_ATOM_TAKE_DAMAGE)
	processing -= target
	if(!length(processing))
		STOP_PROCESSING(SSobj, src)

/// Handles beginning processing objects.
/datum/element/obj_regen/proc/on_take_damage(obj/target, damage_amt)
	SIGNAL_HANDLER
	if(!damage_amt)
		return
	if(!length(processing))
		START_PROCESSING(SSobj, src)
	processing |= target

/// Handle regenerating attached objects.
/datum/element/obj_regen/process(delta_time)
	set waitfor = FALSE

	if(!resumed)
		currentrun = processing.Copy()

	resumed = FALSE
	var/list/cached_run = currentrun
	if(!length(cached_run))
		if(!length(processing))
			STOP_PROCESSING(SSobj, src)
			return
		return

	var/cached_rate = rate
	while(length(cached_run))
		var/obj/regen_obj = cached_run[cached_run.len]
		cached_run.len--

		if(QDELETED(regen_obj))
			processing -= regen_obj
			if(!length(processing))
				STOP_PROCESSING(SSobj, src)
				return PROCESS_KILL
			if(CHECK_TICK)
				resumed = TRUE
				return
			continue

		if(!regen_obj.repair_damage(regen_obj.max_integrity * cached_rate))
			processing -= regen_obj
			if(!length(processing))
				STOP_PROCESSING(SSobj, src)
				return PROCESS_KILL

		if(CHECK_TICK)
			resumed = TRUE
			return


code:
/**
 * allow players to easily use items such as iron rods, rcds on open space without
 * having to pixelhunt for portions not occupied by object or mob visuals.
 */
/datum/element/openspace_item_click_handler
	element_flags = ELEMENT_DETACH

/datum/element/openspace_item_click_handler/Attach(datum/target)
	. = ..()
	if(!isitem(target))
		return ELEMENT_INCOMPATIBLE
	RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, .proc/on_afterattack)

/datum/element/openspace_item_click_handler/Detach(datum/source)
	UnregisterSignal(source, COMSIG_ITEM_AFTERATTACK)
	return ..()

//Invokes the proctype with a turf above as target.
/datum/element/openspace_item_click_handler/proc/on_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters)
	SIGNAL_HANDLER
	if(target.z == user.z)
		return
	var/turf/turf_above = get_step_multiz(target, UP)
	if(turf_above?.z == user.z)
		INVOKE_ASYNC(source, /obj/item.proc/handle_openspace_click, turf_above, user, user.CanReach(turf_above, source), click_parameters)


code:
/**
 * # Pet bonus element!
 *
 * Bespoke element that plays a fun message, sends a heart out, and gives a stronger mood bonus when you pet this animal.
 * I may have been able to make this work for carbons, but it would have been interjecting on some help mode interactions anyways.
 */
/datum/element/pet_bonus
	element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
	id_arg_index = 2

	///optional cute message to send when you pet your pet!
	var/emote_message
	///actual moodlet given, defaults to the pet animal one
	var/moodlet

/datum/element/pet_bonus/Attach(datum/target, emote_message, moodlet = /datum/mood_event/pet_animal)
	. = ..()
	if(!isliving(target))
		return ELEMENT_INCOMPATIBLE

	src.emote_message = emote_message
	src.moodlet = moodlet
	RegisterSignal(target, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand)

/datum/element/pet_bonus/Detach(datum/target)
	. = ..()
	UnregisterSignal(target, COMSIG_ATOM_ATTACK_HAND)

/datum/element/pet_bonus/proc/on_attack_hand(mob/living/pet, mob/living/petter, list/modifiers)
	SIGNAL_HANDLER

	if(pet.stat != CONSCIOUS || petter.combat_mode || LAZYACCESS(modifiers, RIGHT_CLICK))
		return

	new /obj/effect/temp_visual/heart(pet.loc)
	if(emote_message && prob(33))
		pet.manual_emote(emote_message)
	SEND_SIGNAL(petter, COMSIG_ADD_MOOD_EVENT, pet, moodlet, pet)


code:
/// -- Plant backfire element --
/// Certain high-danger plants, like death-nettles, will backfire and harm the holder if they're not properly protected.
/// If a user is protected with something like leather gloves, they can handle them normally.
/// If they're not protected properly, we invoke a callback on the user, harming or inconveniencing them.
/datum/element/plant_backfire
	element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
	id_arg_index = 2
	/// Whether we stop the current action if backfire is triggered (EX: returning CANCEL_ATTACK_CHAIN)
	var/cancel_action = FALSE
	/// Any extra traits we want to check in addition to TRAIT_PLANT_SAFE. Mobs with a trait in this list will be considered safe. List of traits.
	var/extra_traits
	/// Any plant genes we want to check that are required for our plant to be dangerous. Plants without a gene in this list will be considered safe. List of typepaths.
	var/extra_genes

/datum/element/plant_backfire/Attach(datum/target, cancel_action = FALSE, extra_traits, extra_genes)
	. = ..()
	if(!isitem(target))
		return ELEMENT_INCOMPATIBLE

	src.cancel_action = cancel_action
	src.extra_traits = extra_traits
	src.extra_genes = extra_genes

	RegisterSignal(target, COMSIG_ITEM_PRE_ATTACK, .proc/attack_safety_check)
	RegisterSignal(target, COMSIG_ITEM_PICKUP, .proc/pickup_safety_check)
	RegisterSignal(target, COMSIG_MOVABLE_PRE_THROW, .proc/throw_safety_check)

/datum/element/plant_backfire/Detach(datum/target)
	. = ..()
	UnregisterSignal(target, list(COMSIG_ITEM_PRE_ATTACK, COMSIG_ITEM_PICKUP, COMSIG_MOVABLE_PRE_THROW))

/*
 * Checks before we attack if we're okay to continue.
 *
 * source - our plant
 * user - the mob wielding our [source]
 */
/datum/element/plant_backfire/proc/attack_safety_check(datum/source, atom/target, mob/user)
	SIGNAL_HANDLER

	if(plant_safety_check(source, user))
		return
	SEND_SIGNAL(source, COMSIG_PLANT_ON_BACKFIRE, user)
	if(cancel_action)
		return COMPONENT_CANCEL_ATTACK_CHAIN

/*
 * Checks before we pick up the plant if we're okay to continue.
 *
 * source - our plant
 * user - the mob picking our [source]
 */
/datum/element/plant_backfire/proc/pickup_safety_check(datum/source, mob/user)
	SIGNAL_HANDLER

	if(plant_safety_check(source, user))
		return
	SEND_SIGNAL(source, COMSIG_PLANT_ON_BACKFIRE, user)

/*
 * Checks before we throw the plant if we're okay to continue.
 *
 * source - our plant
 * thrower - the mob throwing our [source]
 */
/datum/element/plant_backfire/proc/throw_safety_check(datum/source, list/arguments)
	SIGNAL_HANDLER

	var/mob/living/thrower = arguments[4] // 4th arg = mob/thrower
	if(plant_safety_check(source, thrower))
		return
	SEND_SIGNAL(source, COMSIG_PLANT_ON_BACKFIRE, thrower)
	if(cancel_action)
		return COMPONENT_CANCEL_THROW

/*
 * Actually checks if our user is safely handling our plant.
 *
 * Checks for TRAIT_PLANT_SAFE, and returns TRUE if we have it.
 * Then, any extra traits we need to check (Like TRAIT_PIERCEIMMUNE for nettles) and returns TRUE if we have one of them.
 * Then, any extra genes we need to check (Like liquid contents for bluespace tomatos) and returns TRUE if we don't have the gene.
 *
 * source - our plant
 * user - the carbon handling our [source]
 *
 * returns FALSE if none of the checks are successful.
 */
/datum/element/plant_backfire/proc/plant_safety_check(datum/source, mob/living/carbon/user)
	if(!istype(user))
		return TRUE

	if(HAS_TRAIT(user, TRAIT_PLANT_SAFE))
		return TRUE

	for(var/checked_trait in extra_traits)
		if(HAS_TRAIT(user, checked_trait))
			return TRUE

	var/obj/item/parent_item = source
	var/obj/item/seeds/our_seed = parent_item.get_plant_seed()
	if(our_seed)
		for(var/checked_gene in extra_genes)
			if(!our_seed.get_gene(checked_gene))
				return TRUE

	for(var/obj/item/clothing/worn_item in user.get_equipped_items())
		if((worn_item.body_parts_covered & HANDS) && (worn_item.clothing_flags & THICKMATERIAL))
			return TRUE

	return FALSE


code:
/// Designates the atom as a "point of interest", meaning it can be directly orbited
/datum/element/point_of_interest
	element_flags = ELEMENT_DETACH

/datum/element/point_of_interest/Attach(datum/target)
	if (!isatom(target))
		return ELEMENT_INCOMPATIBLE

	// New players are abstract mobs assigned to people who are still in the lobby screen.
	// As a result, they are not a valid POI and should never be a valid POI. If they
	// somehow get this element attached to them, there's something we need to debug.
	if(isnewplayer(target))
		return ELEMENT_INCOMPATIBLE

	SSpoints_of_interest.on_poi_element_added(target)
	return ..()

/datum/element/point_of_interest/Detach(datum/target)
	SSpoints_of_interest.on_poi_element_removed(target)
	return ..()


code:
/// This hostile will not be able to attack a given typecache, and will receive
/// a balloon alert when it tries to.
/datum/element/prevent_attacking_of_types
	element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
	id_arg_index = 2

	/// The typecache of things this hostile can't attack
	var/list/typecache

	/// The message to send to the hostile mob when they try to attack something they can't
	var/alert_message

/datum/element/prevent_attacking_of_types/Attach(datum/target, list/typecache, alert_message)
	. = ..()

	if (!ishostile(target))
		return ELEMENT_INCOMPATIBLE

	src.alert_message = alert_message
	src.typecache = typecache

	RegisterSignal(target, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, .proc/on_pre_attacking_target)

/datum/element/prevent_attacking_of_types/Detach(datum/source, ...)
	UnregisterSignal(source, COMSIG_HOSTILE_PRE_ATTACKINGTARGET)
	return ..()

/datum/element/prevent_attacking_of_types/proc/on_pre_attacking_target(mob/source, atom/target)
	SIGNAL_HANDLER

	if (!typecache[target.type])
		return

	target.balloon_alert(source, alert_message)

	return COMPONENT_HOSTILE_NO_ATTACK


code:
/datum/element/rad_insulation
	element_flags = ELEMENT_DETACH | ELEMENT_BESPOKE
	id_arg_index = 2
	var/amount // Multiplier for radiation strength passing through

/datum/element/rad_insulation/Attach(datum/target, _amount=RAD_MEDIUM_INSULATION, protects=TRUE, contamination_proof=TRUE)
	. = ..()
	if(!isatom(target))
		return COMPONENT_INCOMPATIBLE

	if(protects) // Does this protect things in its contents from being affected?
		RegisterSignal(target, COMSIG_ATOM_RAD_PROBE, .proc/rad_probe_react)
	if(contamination_proof) // Can this object be contaminated?
		RegisterSignal(target, COMSIG_ATOM_RAD_CONTAMINATING, .proc/rad_contaminating)
	if(_amount != 1) // If it's 1 it won't have any impact on radiation passing through anyway
		RegisterSignal(target, COMSIG_ATOM_RAD_WAVE_PASSING, .proc/rad_pass)

	amount = _amount

/datum/element/rad_insulation/proc/rad_probe_react(datum/source)
	SIGNAL_HANDLER

	return COMPONENT_BLOCK_RADIATION

/datum/element/rad_insulation/proc/rad_contaminating(datum/source, strength)
	SIGNAL_HANDLER

	return COMPONENT_BLOCK_CONTAMINATION

/datum/element/rad_insulation/proc/rad_pass(datum/source, datum/radiation_wave/wave, width)
	SIGNAL_HANDLER

	wave.intensity = wave.intensity*(1-((1-amount)/width)) // The further out the rad wave goes the less it's affected by insulation (larger width)
	return COMPONENT_RAD_WAVE_HANDLED


code:
///This proc is used by basic mobs to give them a simple ranged attack! In theory this could be extended to
/datum/element/ranged_attacks
	element_flags = ELEMENT_DETACH | ELEMENT_BESPOKE
	id_arg_index = 2
	var/casingtype = /obj/item/ammo_casing/glockroach
	var/projectilesound = 'sound/weapons/gun/pistol/shot.ogg'
	var/projectiletype

/datum/element/ranged_attacks/Attach(atom/movable/target, casingtype, projectilesound, projectiletype)
	. = ..()
	if(!isbasicmob(target))
		return COMPONENT_INCOMPATIBLE

	src.casingtype = casingtype
	src.projectilesound = projectilesound
	src.projectiletype = projectiletype

	RegisterSignal(target, COMSIG_MOB_ATTACK_RANGED, .proc/fire_ranged_attack)

	if(casingtype && projectiletype)
		CRASH("Set both casing type and projectile type in [target]'s ranged attacks element! uhoh! stinky!")

/datum/element/ranged_attacks/Detach(datum/target)
	UnregisterSignal(target, COMSIG_MOB_ATTACK_RANGED)
	return ..()

/datum/element/ranged_attacks/proc/fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers)
	SIGNAL_HANDLER
	INVOKE_ASYNC(src, .proc/async_fire_ranged_attack, firer, target, modifiers)

/datum/element/ranged_attacks/proc/async_fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers)
	var/turf/startloc = get_turf(firer)

	if(casingtype)
		var/obj/item/ammo_casing/casing = new casingtype(startloc)
		playsound(firer, projectilesound, 100, TRUE)
		casing.fire_casing(target, firer, null, null, null, ran_zone(), 0,  firer)

	else if(projectiletype)
		var/obj/projectile/P = new projectiletype(startloc)
		playsound(firer, projectilesound, 100, TRUE)
		P.starting = startloc
		P.firer = firer
		P.fired_from = firer
		P.yo = target.y - startloc.y
		P.xo = target.x - startloc.x
		P.original = target
		P.preparePixelProjectile(target, firer)
		P.fire()


code:
/**
 * This element is used to indicate that a movable atom can be mounted by mobs in order to ride it. The movable is considered mounted when a mob is buckled to it,
 * at which point a [riding component][/datum/component/riding] is created on the movable, and that component handles the actual riding behavior.
 *
 * Besides the target, the ridable element has one argument: the component subtype. This is not really ideal since there's ~20-30 component subtypes rather than
 * having the behavior defined on the ridable atoms themselves or some such, but because the old riding behavior was so horrifyingly spread out and redundant,
 * just having the variables, behavior, and procs be standardized is still a big improvement.
 */
/datum/element/ridable
	element_flags = ELEMENT_BESPOKE
	id_arg_index = 2

	/// The specific riding component subtype we're loading our instructions from, don't leave this as default please!
	var/riding_component_type = /datum/component/riding
	/// If we have a xenobio red potion applied to us, we get split off so we can pass our special status onto new riding components
	var/potion_boosted = FALSE

/datum/element/ridable/Attach(atom/movable/target, component_type = /datum/component/riding, potion_boost = FALSE)
	. = ..()
	if(!ismovable(target))
		return COMPONENT_INCOMPATIBLE

	if(component_type == /datum/component/riding)
		stack_trace("Tried attaching a ridable element to [target] with basic/abstract /datum/component/riding component type. Please designate a specific riding component subtype when adding the ridable element.")
		return COMPONENT_INCOMPATIBLE

	riding_component_type = component_type
	potion_boosted = potion_boost

	RegisterSignal(target, COMSIG_MOVABLE_PREBUCKLE, .proc/check_mounting)
	if(isvehicle(target))
		RegisterSignal(target, COMSIG_PARENT_ATTACKBY, .proc/check_potion)

/datum/element/ridable/Detach(datum/target)
	UnregisterSignal(target, list(COMSIG_MOVABLE_PREBUCKLE, COMSIG_PARENT_ATTACKBY))
	return ..()

/// Someone is buckling to this movable, which is literally the only thing we care about (other than speed potions)
/datum/element/ridable/proc/check_mounting(atom/movable/target_movable, mob/living/potential_rider, force = FALSE, ride_check_flags = NONE)
	SIGNAL_HANDLER

	if(HAS_TRAIT(potential_rider, TRAIT_CANT_RIDE))
		return

	var/arms_needed = 0
	if(ride_check_flags & RIDER_NEEDS_ARMS)
		arms_needed = 2
	else if(ride_check_flags & RIDER_NEEDS_ARM)
		arms_needed = 1
		ride_check_flags &= ~RIDER_NEEDS_ARM
		ride_check_flags |= RIDER_NEEDS_ARMS

	if(arms_needed && !equip_buckle_inhands(potential_rider, arms_needed, target_movable)) // can be either 1 (cyborg riding) or 2 (human piggybacking) hands
		potential_rider.visible_message(span_warning("[potential_rider] can't get a grip on [target_movable] because [potential_rider.p_their()] hands are full!"),
			span_warning("You can't get a grip on [target_movable] because your hands are full!"))
		return COMPONENT_BLOCK_BUCKLE

	if((ride_check_flags & RIDER_NEEDS_LEGS) && HAS_TRAIT(potential_rider, TRAIT_FLOORED))
		potential_rider.visible_message(span_warning("[potential_rider] can't get [potential_rider.p_their()] footing on [target_movable]!"),
			span_warning("You can't get your footing on [target_movable]!"))
		return COMPONENT_BLOCK_BUCKLE

	var/mob/living/target_living = target_movable

	// need to see if !equip_buckle_inhands() checks are enough to skip any needed incapac/restrain checks
	// CARRIER_NEEDS_ARM shouldn't apply if the ridden isn't even a living mob
	if((ride_check_flags & CARRIER_NEEDS_ARM) && !equip_buckle_inhands(target_living, 1, target_living, potential_rider)) // hardcode 1 hand for now
		target_living.visible_message(span_warning("[target_living] can't get a grip on [potential_rider] because [target_living.p_their()] hands are full!"),
			span_warning("You can't get a grip on [potential_rider] because your hands are full!"))
		return COMPONENT_BLOCK_BUCKLE

	target_living.AddComponent(riding_component_type, potential_rider, force, ride_check_flags, potion_boost = potion_boosted)

/// Try putting the appropriate number of [riding offhand items][/obj/item/riding_offhand] into the target's hands, return FALSE if we can't
/datum/element/ridable/proc/equip_buckle_inhands(mob/living/carbon/human/user, amount_required = 1, atom/movable/target_movable, riding_target_override = null)
	var/atom/movable/AM = target_movable
	var/amount_equipped = 0
	for(var/amount_needed = amount_required, amount_needed > 0, amount_needed--)
		var/obj/item/riding_offhand/inhand = new /obj/item/riding_offhand(user)
		if(!riding_target_override)
			inhand.rider = user
		else
			inhand.rider = riding_target_override
		inhand.parent = AM
		for(var/obj/item/I in user.held_items) // delete any hand items like slappers that could still totally be used to grab on
			if((I.obj_flags & HAND_ITEM))
				qdel(I)

		// this would be put_in_hands() if it didn't have the chance to sleep, since this proc gets called from a signal handler that relies on what this returns
		var/inserted_successfully = FALSE
		if(user.put_in_active_hand(inhand))
			inserted_successfully = TRUE
		else
			var/hand = user.get_empty_held_index_for_side(LEFT_HANDS) || user.get_empty_held_index_for_side(RIGHT_HANDS)
			if(hand && user.put_in_hand(inhand, hand))
				inserted_successfully = TRUE

		if(inserted_successfully)
			amount_equipped++
		else
			qdel(inhand)
			break

	if(amount_equipped >= amount_required)
		return TRUE
	else
		unequip_buckle_inhands(user, target_movable)
		return FALSE

/// Checks to see if we've been hit with a red xenobio potion to make us faster. This is only registered if we're a vehicle
/datum/element/ridable/proc/check_potion(atom/movable/ridable_atom, obj/item/slimepotion/speed/speed_potion, mob/living/user, params)
	SIGNAL_HANDLER

	if(!istype(speed_potion))
		return
	if(potion_boosted)
		to_chat(user, span_warning("[ridable_atom] has already been coated with red, that's as fast as it'll go!"))
		return
	if(ridable_atom.has_buckled_mobs()) // effect won't take place til the next time someone mounts it, so just prevent that situation
		to_chat(user, span_warning("It's too dangerous to smear [speed_potion] on [ridable_atom] while it's being ridden!"))
		return

	var/speed_limit = round(CONFIG_GET(number/movedelay/run_delay) * 0.85, 0.01)
	var/datum/component/riding/theoretical_riding_component = riding_component_type
	var/theoretical_speed = initial(theoretical_riding_component.vehicle_move_delay)

	if(theoretical_speed <= speed_limit) // i say speed but this is actually move delay, so you have to be ABOVE the speed limit to pass
		to_chat(user, span_warning("[ridable_atom] can't be made any faster!"))
		return

	Detach(ridable_atom)
	ridable_atom.AddElement(/datum/element/ridable, component_type = riding_component_type, potion_boost = TRUE)
	to_chat(user, span_notice("You slather the red gunk over [ridable_atom], making it faster."))
	ridable_atom.remove_atom_colour(WASHABLE_COLOUR_PRIORITY)
	ridable_atom.add_atom_colour("#FF0000", FIXED_COLOUR_PRIORITY)
	qdel(speed_potion)
	return COMPONENT_NO_AFTERATTACK

/// Remove all of the relevant [riding offhand items][/obj/item/riding_offhand] from the target
/datum/element/ridable/proc/unequip_buckle_inhands(mob/living/carbon/user, atom/movable/target_movable)
	var/atom/movable/AM = target_movable
	for(var/obj/item/riding_offhand/O in user.contents)
		if(O.parent != AM)
			CRASH("RIDING OFFHAND ON WRONG MOB")
		if(O.selfdeleting)
			continue
		else
			qdel(O)
	return TRUE

/obj/item/riding_offhand
	name = "offhand"
	icon = 'icons/obj/items_and_weapons.dmi'
	icon_state = "offhand"
	w_class = WEIGHT_CLASS_HUGE
	item_flags = ABSTRACT | DROPDEL | NOBLUDGEON
	resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
	var/mob/living/carbon/rider
	var/mob/living/parent
	var/selfdeleting = FALSE

/obj/item/riding_offhand/dropped()
	selfdeleting = TRUE
	. = ..()

/obj/item/riding_offhand/equipped()
	if(loc != rider && loc != parent)
		selfdeleting = TRUE
		qdel(src)
	. = ..()

/obj/item/riding_offhand/Destroy()
	var/atom/movable/AM = parent
	if(selfdeleting)
		if(rider in AM.buckled_mobs)
			AM.unbuckle_mob(rider)
	. = ..()

/obj/item/riding_offhand/on_thrown(mob/living/carbon/user, atom/target)
	if(rider == user)
		return //Piggyback user.
	user.unbuckle_mob(rider)
	if(HAS_TRAIT(user, TRAIT_PACIFISM))
		to_chat(user, span_notice("You gently let go of [rider]."))
		return
	return rider


code:
/**
 * Adding this element to an atom will have it automatically render an overlay.
 * The overlay can be specified in new as the first paramter; if not set it defaults to rust_overlay's rust_default
 */
/datum/element/rust
	element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
	id_arg_index = 2
	/// The rust image itself, since the icon and icon state are only used as an argument
	var/image/rust_overlay

/datum/element/rust/Attach(atom/target, rust_icon = 'icons/effects/rust_overlay.dmi', rust_icon_state = "rust_default")
	. = ..()
	if(!isatom(target))
		return COMPONENT_INCOMPATIBLE
	if(!rust_overlay)
		rust_overlay = image(rust_icon, rust_icon)
	ADD_TRAIT(target, TRAIT_RUSTY, src)
	RegisterSignal(target, COMSIG_ATOM_UPDATE_OVERLAYS, .proc/apply_rust_overlay)
	RegisterSignal(target, COMSIG_PARENT_EXAMINE, .proc/handle_examine)
	RegisterSignal(target, list(COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_WELDER), COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_RUSTSCRAPER)), .proc/secondary_tool_act)
	// Unfortunately registering with parent sometimes doesn't cause an overlay update
	target.update_icon(UPDATE_OVERLAYS)

/datum/element/rust/Detach(atom/source)
	. = ..()
	UnregisterSignal(source, COMSIG_ATOM_UPDATE_OVERLAYS)
	UnregisterSignal(source, COMSIG_PARENT_EXAMINE)
	UnregisterSignal(source, list(COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_WELDER), COMSIG_ATOM_SECONDARY_TOOL_ACT(TOOL_RUSTSCRAPER)))
	REMOVE_TRAIT(source, TRAIT_RUSTY, src)
	source.update_icon(UPDATE_OVERLAYS)

/datum/element/rust/proc/handle_examine(datum/source, mob/user, list/examine_text)
	SIGNAL_HANDLER
	examine_text += span_notice("[source] is very rusty, you could probably burn or scrape it off.")

/datum/element/rust/proc/apply_rust_overlay(atom/parent_atom, list/overlays)
	SIGNAL_HANDLER
	overlays |= rust_overlay

/// Because do_after sleeps we register the signal here and defer via an async call
/datum/element/rust/proc/secondary_tool_act(atom/source, mob/user, obj/item/item)
	SIGNAL_HANDLER
	INVOKE_ASYNC(src, .proc/handle_tool_use, source, user, item)
	return COMPONENT_BLOCK_TOOL_ATTACK

/// We call this from secondary_tool_act because we sleep with do_after
/datum/element/rust/proc/handle_tool_use(atom/source, mob/user, obj/item/item)
	switch(item.tool_behaviour)
		if(TOOL_WELDER)
			if(item.use(5))
				user.balloon_alert(user, "burning off rust...")
				if(!do_after(user, 5 SECONDS * item.toolspeed, source))
					return
				user.balloon_alert(user, "burned off rust")
				Detach(source)
				return
		if(TOOL_RUSTSCRAPER)
			user.balloon_alert(user, "scraping off rust...")
			if(!do_after(user, 2 SECONDS * item.toolspeed, source))
				return
			user.balloon_alert(user, "scraped off rust")
			Detach(source)
			return


code:
/datum/element/selfknockback
	element_flags = ELEMENT_BESPOKE
	id_arg_index = 2
	var/override_throw_val
	var/override_speed_val

/* Adds the ability to put recoil on various things, throwing the user back proportional to the force and speed set.
Important note: Force/Throw_Amount, is how far you want to throw a user. By default, it is calculated based on damage below.
Speed_Amount, is a measure of how long it will take a user to get thrown to the target. By default, it is calculated by
clamping the Knockback_Force value below. */
/datum/element/selfknockback/Attach(datum/target, throw_amount, speed_amount)
	. = ..()
	if(isitem(target))
		RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, .proc/Item_SelfKnockback)
	else if(isprojectile(target))
		RegisterSignal(target, COMSIG_PROJECTILE_FIRE, .proc/Projectile_SelfKnockback)
	else
		return ELEMENT_INCOMPATIBLE

	override_throw_val = throw_amount
	override_speed_val = speed_amount

/datum/element/selfknockback/Detach(datum/source)
	. = ..()
	UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_PROJECTILE_FIRE))

/datum/element/selfknockback/proc/Get_Knockback_Force(default_force)
	if(override_throw_val)
		return override_throw_val
	else
		return default_force

/datum/element/selfknockback/proc/Get_Knockback_Speed(default_speed)
	if(override_speed_val)
		return override_speed_val
	else
		return default_speed

/datum/element/selfknockback/proc/Item_SelfKnockback(obj/item/I, atom/attacktarget, mob/usertarget, proximity_flag)
	SIGNAL_HANDLER

	if(isturf(attacktarget) && !attacktarget.density)
		return
	if(proximity_flag || (get_dist(attacktarget, usertarget) <= I.reach))
		var/knockback_force = Get_Knockback_Force(clamp(CEILING((I.force / 10), 1), 1, 5))
		var/knockback_speed = Get_Knockback_Speed(clamp(knockback_force, 1, 5))

		var/target_angle = get_angle(attacktarget, usertarget)
		var/move_target = get_ranged_target_turf(usertarget, angle2dir(target_angle), knockback_force)
		usertarget.throw_at(move_target, knockback_force, knockback_speed)
		usertarget.visible_message(span_warning("[usertarget] gets thrown back by the force of \the [I] impacting \the [attacktarget]!"), span_warning("The force of \the [I] impacting \the [attacktarget] sends you flying!"))

/datum/element/selfknockback/proc/Projectile_SelfKnockback(obj/projectile/P)
	SIGNAL_HANDLER

	if(!P.firer)
		return

	var/knockback_force = Get_Knockback_Force(clamp(CEILING((P.damage / 10), 1), 1, 5))
	var/knockback_speed = Get_Knockback_Speed(clamp(knockback_force, 1, 5))

	var/atom/movable/knockback_target = P.firer
	var/move_target = get_edge_target_turf(knockback_target, angle2dir(P.original_angle+180))
	knockback_target.throw_at(move_target, knockback_force, knockback_speed)


code:
/**
 * ## series element!
 *
 * bespoke element that assigns a series number to toys on examine, and shows their series name!
 * used for mechas and rare collectable hats, should totally be used for way more ;)
 */
/datum/element/series
	element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
	id_arg_index = 2
	var/list/subtype_list
	var/series_name

/datum/element/series/Attach(datum/target, subtype, series_name)
	. = ..()
	if(!isatom(target))
		return ELEMENT_INCOMPATIBLE
	if(!subtype)
		stack_trace("series element without subtype given!")
		return ELEMENT_INCOMPATIBLE
	subtype_list = subtypesof(subtype)
	src.series_name = series_name
	var/atom/attached = target
	RegisterSignal(attached, COMSIG_PARENT_EXAMINE, .proc/on_examine)

/datum/element/series/Detach(datum/target)
	. = ..()
	UnregisterSignal(target, COMSIG_PARENT_EXAMINE)

///signal called examining
/datum/element/series/proc/on_examine(datum/target, mob/user, list/examine_list)
	SIGNAL_HANDLER

	var/series_number = subtype_list.Find(target.type)
	examine_list += span_boldnotice("[target] is part of the \"[series_name]\" series!")
	examine_list += span_notice("Collect them all: [series_number]/[length(subtype_list)].")


code:
/**
 * # simple flying element!
 *
 * Non bespoke element (1 in existence) that makes animals fly while living and... not while dead!
 * Note: works for carbons and above, but please do something better. humans have wings got dangit!
 */
/datum/element/simple_flying
	element_flags = ELEMENT_DETACH

/datum/element/simple_flying/Attach(datum/target)
	. = ..()
	if(!isliving(target))
		return ELEMENT_INCOMPATIBLE
	var/mob/living/valid_target = target
	on_stat_change(valid_target, new_stat = valid_target.stat) //immediately try adding flight if they're conscious
	RegisterSignal(target, COMSIG_MOB_STATCHANGE, .proc/on_stat_change)

/datum/element/simple_flying/Detach(datum/target)
	. = ..()
	UnregisterSignal(target, COMSIG_MOB_STATCHANGE)

///signal called by the stat of the target changing
/datum/element/simple_flying/proc/on_stat_change(mob/living/simple_animal/target, new_stat)
	SIGNAL_HANDLER

	if(new_stat == CONSCIOUS)
		ADD_TRAIT(target, TRAIT_MOVE_FLYING, ROUNDSTART_TRAIT)
	else
		REMOVE_TRAIT(target, TRAIT_MOVE_FLYING, ROUNDSTART_TRAIT)


code:
/*
 * An element that makes mobs run into lockers when they bump into them.
 */

/datum/element/skittish
	element_flags = ELEMENT_DETACH

/datum/element/skittish/Attach(datum/target)
	. = ..()
	if(!isliving(target))
		return ELEMENT_INCOMPATIBLE

	RegisterSignal(target, COMSIG_MOVABLE_BUMP, .proc/Bump)

/datum/element/skittish/Detach(datum/target)
	UnregisterSignal(target, COMSIG_MOVABLE_BUMP)
	. = ..()

/datum/element/skittish/proc/Bump(mob/living/scooby, atom/target)
	SIGNAL_HANDLER
	if(scooby.stat != CONSCIOUS || scooby.m_intent != MOVE_INTENT_RUN)
		return

	if(!istype(target, /obj/structure/closet))
		return

	var/obj/structure/closet/closet = target

	if(!closet.divable)
		// Things like secure crates can blow up under certain circumstances
		return

	var/turf/closet_turf = get_turf(closet)

	if(!closet.opened)
		if(closet.locked)
			closet.togglelock(scooby, silent = TRUE)
		if(!closet.open(scooby))
			// No message if unable to open, since this is on Bump, spammy potential
			return

	// If it's a crate, "dive for cover" and start resting so people can jump into crates without slamming the lid on their head
	if(closet.horizontal)
		// need to rest before moving, otherwise "can't get crate to close" message will be printed erroneously
		scooby.set_resting(TRUE, silent = TRUE)

	scooby.forceMove(closet_turf)

	if(!closet.close(scooby))
		to_chat(scooby, span_warning("You can't get [closet] to close!"))
		if(closet.horizontal)
			scooby.set_resting(FALSE, silent = TRUE)
		return

	closet.togglelock(scooby, silent = TRUE)

	if(closet.horizontal)
		scooby.set_resting(FALSE, silent = TRUE)

	closet_turf.visible_message(span_warning("[scooby] dives into [closet]!"))
	// If you run into a locker, you don't want to run out immediately
	scooby.Immobilize(0.5 SECONDS)


code:
/datum/element/snailcrawl
	element_flags = ELEMENT_DETACH

/datum/element/snailcrawl/Attach(datum/target)
	. = ..()
	if(!ismovable(target))
		return ELEMENT_INCOMPATIBLE
	var/P
	if(iscarbon(target))
		P = .proc/snail_crawl
	else
		P = .proc/lubricate
	RegisterSignal(target, COMSIG_MOVABLE_MOVED, P)

/datum/element/snailcrawl/Detach(mob/living/carbon/target)
	. = ..()
	UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
	if(istype(target))
		target.remove_movespeed_modifier(/datum/movespeed_modifier/snail_crawl)

/datum/element/snailcrawl/proc/snail_crawl(mob/living/carbon/snail)
	SIGNAL_HANDLER

	if(snail.resting && !snail.buckled && lubricate(snail))
		snail.add_movespeed_modifier(/datum/movespeed_modifier/snail_crawl)
	else
		snail.remove_movespeed_modifier(/datum/movespeed_modifier/snail_crawl)

/datum/element/snailcrawl/proc/lubricate(atom/movable/snail)
	SIGNAL_HANDLER

	var/turf/open/OT = get_turf(snail)
	if(istype(OT))
		OT.MakeSlippery(TURF_WET_LUBE, 20)
		return TRUE


code:
/datum/element/spooky
	element_flags = ELEMENT_DETACH|ELEMENT_BESPOKE
	id_arg_index = 2
	var/too_spooky = TRUE //will it spawn a new instrument?

/datum/element/spooky/Attach(datum/target, too_spooky = TRUE)
	. = ..()
	if(!isitem(target))
		return ELEMENT_INCOMPATIBLE
	src.too_spooky = too_spooky
	RegisterSignal(target, COMSIG_ITEM_ATTACK, .proc/spectral_attack)

/datum/element/spooky/Detach(datum/source)
	UnregisterSignal(source, COMSIG_ITEM_ATTACK)
	return ..()

/datum/element/spooky/proc/spectral_attack(datum/source, mob/living/carbon/C, mob/user)
	SIGNAL_HANDLER

	if(ishuman(user)) //this weapon wasn't meant for mortals.
		var/mob/living/carbon/human/U = user
		if(!istype(U.dna.species, /datum/species/skeleton))
			U.adjustStaminaLoss(35) //Extra Damage
			U.Jitter(35)
			U.stuttering = 20
			if(U.getStaminaLoss() > 95)
				to_chat(U, "Your ears weren't meant for this spectral sound.")
				INVOKE_ASYNC(src, .proc/spectral_change, U)
			return

	if(ishuman(C))
		var/mob/living/carbon/human/H = C
		if(istype(H.dna.species, /datum/species/skeleton))
			return //undeads are unaffected by the spook-pocalypse.
		if(istype(H.dna.species, /datum/species/zombie))
			H.adjustStaminaLoss(25)
			H.Paralyze(15) //zombies can't resist the doot
		C.Jitter(35)
		C.stuttering = 20
		if((!istype(H.dna.species, /datum/species/skeleton)) && (!istype(H.dna.species, /datum/species/golem)) && (!istype(H.dna.species, /datum/species/android)) && (!istype(H.dna.species, /datum/species/jelly)))
			C.adjustStaminaLoss(25) //boneless humanoids don't lose the will to live
		to_chat(C, "DOOT")
		INVOKE_ASYNC(src, .proc/spectral_change, H)

	else //the sound will spook monkeys.
		C.Jitter(15)
		C.stuttering = 20

/datum/element/spooky/proc/spectral_change(mob/living/carbon/human/H, mob/user)
	if((H.getStaminaLoss() > 95) && (!istype(H.dna.species, /datum/species/skeleton)) && (!istype(H.dna.species, /datum/species/golem)) && (!istype(H.dna.species, /datum/species/android)) && (!istype(H.dna.species, /datum/species/jelly)))
		H.Paralyze(20)
		H.set_species(/datum/species/skeleton)
		H.visible_message(span_warning("[H] has given up on life as a mortal."))
		var/T = get_turf(H)
		if(too_spooky)
			if(prob(90))
				var/obj/item/instrument = pick(
					/obj/item/instrument/saxophone/spectral,
					/obj/item/instrument/trumpet/spectral,
					/obj/item/instrument/trombone/spectral,
				)
				new instrument(T)
			else
				to_chat(H, span_boldwarning("The spooky gods forgot to ship your instrument. Better luck next unlife."))
		to_chat(H, span_boldnotice("You are the spooky skeleton!"))
		to_chat(H, span_boldnotice("A new life and identity has begun. Help your fellow skeletons into bringing out the spooky-pocalypse. You haven't forgotten your past life, and are still beholden to past loyalties."))
		change_name(H) //time for a new name!

/datum/element/spooky/proc/change_name(mob/living/carbon/human/H)
	var/t = sanitize_name(stripped_input(H, "Enter your new skeleton name", H.real_name, null, MAX_NAME_LEN))
	if(!t)
		t = "spooky skeleton"
	H.fully_replace_character_name(null, t)


code:
#define SHORT 5/7
#define TALL 7/5

/**
 * squish.dm
 *
 * It's an element that squishes things. After the duration passes, it reverses the transformation it squished with, taking into account if they are a different orientation than they started (read: rotationally-fluid)
 *
 * Normal squishes apply vertically, as if the target is being squished from above, but you can set reverse to TRUE if you want to squish them from the sides, like if they pancake into a wall from the East or West
*/
/datum/element/squish
	element_flags = ELEMENT_DETACH

/datum/element/squish/Attach(datum/target, duration=20 SECONDS, reverse=FALSE)
	. = ..()
	if(!iscarbon(target))
		return ELEMENT_INCOMPATIBLE

	var/mob/living/carbon/C = target
	var/was_lying = C.body_position == LYING_DOWN
	addtimer(CALLBACK(src, .proc/Detach, C, was_lying, reverse), duration)

	if(reverse)
		C.transform = C.transform.Scale(SHORT, TALL)
	else
		C.transform = C.transform.Scale(TALL, SHORT)

/datum/element/squish/Detach(mob/living/carbon/C, was_lying, reverse)
	. = ..()
	if(istype(C))
		var/is_lying = C.body_position == LYING_DOWN

		if(reverse)
			is_lying = !is_lying

		if(was_lying == is_lying)
			C.transform = C.transform.Scale(SHORT, TALL)
		else
			C.transform = C.transform.Scale(TALL, SHORT)

#undef SHORT
#undef TALL


code:
/// An element for atoms that, when dragged and dropped onto a mob, opens a strip panel.
/datum/element/strippable
	element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
	id_arg_index = 2

	/// An assoc list of keys to /datum/strippable_item
	var/list/items

	/// A proc path that returns TRUE/FALSE if we should show the strip panel for this entity.
	/// If it does not exist, the strip menu will always show.
	/// Will be called with (mob/user).
	var/should_strip_proc_path

	/// An existing strip menus
	var/list/strip_menus

/datum/element/strippable/Attach(datum/target, list/items, should_strip_proc_path)
	. = ..()
	if (!isatom(target))
		return ELEMENT_INCOMPATIBLE

	RegisterSignal(target, COMSIG_MOUSEDROP_ONTO, .proc/mouse_drop_onto)

	src.items = items
	src.should_strip_proc_path = should_strip_proc_path

/datum/element/strippable/Detach(datum/source)
	. = ..()

	UnregisterSignal(source, COMSIG_MOUSEDROP_ONTO)

	if (!isnull(strip_menus))
		qdel(strip_menus[source])
		strip_menus -= source

/datum/element/strippable/proc/mouse_drop_onto(datum/source, atom/over, mob/user)
	SIGNAL_HANDLER

	if (user == source)
		return

	if (over != user)
		return

	// Cyborgs buckle people by dragging them onto them, unless in combat mode.
	if (iscyborg(user))
		var/mob/living/silicon/robot/cyborg_user = user
		if (!cyborg_user.combat_mode)
			return

	if (!isnull(should_strip_proc_path) && !call(source, should_strip_proc_path)(user))
		return

	var/datum/strip_menu/strip_menu

	if (isnull(strip_menu))
		strip_menu = new(source, src)
		LAZYSET(strip_menus, source, strip_menu)

	INVOKE_ASYNC(strip_menu, /datum/.proc/ui_interact, user)

/// A representation of an item that can be stripped down
/datum/strippable_item
	/// The STRIPPABLE_ITEM_* key
	var/key

	/// Should we warn about dangerous clothing?
	var/warn_dangerous_clothing = TRUE

/// Gets the item from the given source.
/datum/strippable_item/proc/get_item(atom/source)

/// Tries to equip the item onto the given source.
/// Returns TRUE/FALSE depending on if it is allowed.
/// This should be used for checking if an item CAN be equipped.
/// It should not perform the equipping itself.
/datum/strippable_item/proc/try_equip(atom/source, obj/item/equipping, mob/user)
	if(SEND_SIGNAL(user, COMSIG_TRY_STRIP, source, equipping) & COMPONENT_CANT_STRIP)
		return FALSE

	if (HAS_TRAIT(equipping, TRAIT_NODROP))
		to_chat(user, span_warning("You can't put [equipping] on [source], it's stuck to your hand!"))
		return FALSE

	return TRUE

/// Start the equipping process. This is the proc you should yield in.
/// Returns TRUE/FALSE depending on if it is allowed.
/datum/strippable_item/proc/start_equip(atom/source, obj/item/equipping, mob/user)
	if (warn_dangerous_clothing && isclothing(source))
		var/obj/item/clothing/clothing = source
		if(clothing.clothing_flags & DANGEROUS_OBJECT)
			source.visible_message(
				span_danger("[user] tries to put [equipping] on [source]."),
				span_userdanger("[user] tries to put [equipping] on you."),
				ignored_mobs = user,
			)
		else
			source.visible_message(
				span_notice("[user] tries to put [equipping] on [source]."),
				span_notice("[user] tries to put [equipping] on you."),
				ignored_mobs = user,
			)

		if(ishuman(source))
			var/mob/living/carbon/human/victim_human = source
			if(victim_human.key && !victim_human.client) // AKA braindead
				if(victim_human.stat <= SOFT_CRIT && LAZYLEN(victim_human.afk_thefts) <= AFK_THEFT_MAX_MESSAGES)
					var/list/new_entry = list(list(user.name, "tried equipping you with [equipping]", world.time))
					LAZYADD(victim_human.afk_thefts, new_entry)

	to_chat(user, span_notice("You try to put [equipping] on [source]..."))

	var/log = "[key_name(source)] is having [equipping] put on them by [key_name(user)]"
	user.log_message(log, LOG_ATTACK, color="red")
	source.log_message(log, LOG_VICTIM, color="red", log_globally=FALSE)

	return TRUE

/// The proc that places the item on the source. This should not yield.
/datum/strippable_item/proc/finish_equip(atom/source, obj/item/equipping, mob/user)
	SHOULD_NOT_SLEEP(TRUE)

/// Tries to unequip the item from the given source.
/// Returns TRUE/FALSE depending on if it is allowed.
/// This should be used for checking if it CAN be unequipped.
/// It should not perform the unequipping itself.
/datum/strippable_item/proc/try_unequip(atom/source, mob/user)
	SHOULD_NOT_SLEEP(TRUE)

	var/obj/item/item = get_item(source)
	if (isnull(item))
		return FALSE

	if (ismob(source))
		if(SEND_SIGNAL(user, COMSIG_TRY_STRIP, source, item) & COMPONENT_CANT_STRIP)
			return FALSE
		var/mob/mob_source = source
		if (!item.canStrip(user, mob_source))
			return FALSE

	return TRUE

/// Start the unequipping process. This is the proc you should yield in.
/// Returns TRUE/FALSE depending on if it is allowed.
/datum/strippable_item/proc/start_unequip(atom/source, mob/user)
	var/obj/item/item = get_item(source)
	if (isnull(item))
		return FALSE

	source.visible_message(
		span_warning("[user] tries to remove [source]'s [item.name]."),
		span_userdanger("[user] tries to remove your [item.name]."),
		ignored_mobs = user,
	)

	to_chat(user, span_danger("You try to remove [source]'s [item]..."))
	user.log_message("[key_name(source)] is being stripped of [item] by [key_name(user)]", LOG_ATTACK, color="red")
	source.log_message("[key_name(source)] is being stripped of [item] by [key_name(user)]", LOG_VICTIM, color="red", log_globally=FALSE)
	item.add_fingerprint(src)

	if(ishuman(source))
		var/mob/living/carbon/human/victim_human = source
		if(victim_human.key && !victim_human.client) // AKA braindead
			if(victim_human.stat <= SOFT_CRIT && LAZYLEN(victim_human.afk_thefts) <= AFK_THEFT_MAX_MESSAGES)
				var/list/new_entry = list(list(user.name, "tried unequipping your [item.name]", world.time))
				LAZYADD(victim_human.afk_thefts, new_entry)

	return TRUE

/// The proc that unequips the item from the source. This should not yield.
/datum/strippable_item/proc/finish_unequip(atom/source, mob/user)

/// Returns a STRIPPABLE_OBSCURING_* define to report on whether or not this is obscured.
/datum/strippable_item/proc/get_obscuring(atom/source)
	SHOULD_NOT_SLEEP(TRUE)
	return STRIPPABLE_OBSCURING_NONE

/// Returns the ID of this item's strippable action.
/// Return `null` if there is no alternate action.
/// Any return value of this must be in StripMenu.
/datum/strippable_item/proc/get_alternate_action(atom/source, mob/user)
	return null

/// Performs an alternative action on this strippable_item.
/// `has_alternate_action` needs to be TRUE.
/// Returns FALSE if blocked by signal, TRUE otherwise.
/datum/strippable_item/proc/alternate_action(atom/source, mob/user)
	SHOULD_CALL_PARENT(TRUE)
	if(SEND_SIGNAL(user, COMSIG_TRY_ALT_ACTION, source) & COMPONENT_CANT_ALT_ACTION)
		return FALSE
	return TRUE

/// Returns whether or not this item should show.
/datum/strippable_item/proc/should_show(atom/source, mob/user)
	return TRUE

/// A preset for equipping items onto mob slots
/datum/strippable_item/mob_item_slot
	/// The ITEM_SLOT_* to equip to.
	var/item_slot

/datum/strippable_item/mob_item_slot/get_item(atom/source)
	if (!ismob(source))
		return null

	var/mob/mob_source = source
	return mob_source.get_item_by_slot(item_slot)

/datum/strippable_item/mob_item_slot/try_equip(atom/source, obj/item/equipping, mob/user)
	. = ..()
	if (!.)
		return

	if (!ismob(source))
		return FALSE

	if (!equipping.mob_can_equip(
		source,
		user,
		item_slot,
		disable_warning = TRUE,
		bypass_equip_delay_self = TRUE,
	))
		to_chat(user, span_warning("\The [equipping] doesn't fit in that place!"))
		return FALSE

	return TRUE

/datum/strippable_item/mob_item_slot/start_equip(atom/source, obj/item/equipping, mob/user)
	. = ..()
	if (!.)
		return

	if (!ismob(source))
		return FALSE

	if (!do_mob(user, source, get_equip_delay(equipping)))
		return FALSE

	if (!equipping.mob_can_equip(
		source,
		user,
		item_slot,
		disable_warning = TRUE,
		bypass_equip_delay_self = TRUE,
	))
		return FALSE

	if (!user.temporarilyRemoveItemFromInventory(equipping))
		return FALSE

	return TRUE

/datum/strippable_item/mob_item_slot/finish_equip(atom/source, obj/item/equipping, mob/user)
	if (!ismob(source))
		return FALSE

	var/mob/mob_source = source
	mob_source.equip_to_slot(equipping, item_slot)

/datum/strippable_item/mob_item_slot/get_obscuring(atom/source)
	if (iscarbon(source))
		var/mob/living/carbon/carbon_source = source
		return (carbon_source.check_obscured_slots() & item_slot) \
			? STRIPPABLE_OBSCURING_COMPLETELY \
			: STRIPPABLE_OBSCURING_NONE

	return FALSE

/datum/strippable_item/mob_item_slot/start_unequip(atom/source, mob/user)
	. = ..()
	if (!.)
		return

	return start_unequip_mob(get_item(source), source, user)

/datum/strippable_item/mob_item_slot/finish_unequip(atom/source, mob/user)
	var/obj/item/item = get_item(source)
	if (isnull(item))
		return FALSE

	if (!ismob(source))
		return FALSE

	return finish_unequip_mob(item, source, user)

/// Returns the delay of equipping this item to a mob
/datum/strippable_item/mob_item_slot/proc/get_equip_delay(obj/item/equipping)
	return equipping.equip_delay_other

/// A utility function for `/datum/strippable_item`s to start unequipping an item from a mob.
/proc/start_unequip_mob(obj/item/item, mob/source, mob/user, strip_delay)
	if (!do_mob(user, source, strip_delay || item.strip_delay, interaction_key = REF(item)))
		return FALSE

	return TRUE

/// A utility function for `/datum/strippable_item`s to finish unequipping an item from a mob.
/proc/finish_unequip_mob(obj/item/item, mob/source, mob/user)
	if (!item.doStrip(user, source))
		return FALSE

	user.log_message("[key_name(source)] has been stripped of [item] by [key_name(user)]", LOG_ATTACK, color="red")
	source.log_message("[key_name(source)] has been stripped of [item] by [key_name(user)]", LOG_VICTIM, color="red", log_globally=FALSE)

	// Updates speed in case stripped speed affecting item
	source.update_equipment_speed_mods()

/// A representation of the stripping UI
/datum/strip_menu
	/// The owner who has the element /datum/element/strippable
	var/atom/movable/owner

	/// The strippable element itself
	var/datum/element/strippable/strippable

	/// A lazy list of user mobs to a list of strip menu keys that they're interacting with
	var/list/interactions

/datum/strip_menu/New(atom/movable/owner, datum/element/strippable/strippable)
	. = ..()
	src.owner = owner
	src.strippable = strippable

/datum/strip_menu/Destroy()
	owner = null
	strippable = null

	return ..()

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

/datum/strip_menu/ui_assets(mob/user)
	return list(
		get_asset_datum(/datum/asset/simple/inventory),
	)

/datum/strip_menu/ui_data(mob/user)
	var/list/data = list()

	var/list/items = list()

	for (var/strippable_key in strippable.items)
		var/datum/strippable_item/item_data = strippable.items[strippable_key]

		if (!item_data.should_show(owner, user))
			continue

		var/list/result

		if(strippable_key in LAZYACCESS(interactions, user))
			LAZYSET(result, "interacting", TRUE)

		var/obscuring = item_data.get_obscuring(owner)
		if (obscuring != STRIPPABLE_OBSCURING_NONE)
			LAZYSET(result, "obscured", obscuring)
			items[strippable_key] = result
			continue

		var/obj/item/item = item_data.get_item(owner)
		if (isnull(item))
			items[strippable_key] = result
			continue

		LAZYINITLIST(result)

		result["icon"] = icon2base64(icon(item.icon, item.icon_state))
		result["name"] = item.name
		result["alternate"] = item_data.get_alternate_action(owner, user)

		items[strippable_key] = result

	data["items"] = items

	// While most `\the`s are implicit, this one is not.
	// In this case, `\The` would otherwise be used.
	// This doesn't match with what it's used for, which is to say "Stripping the alien drone",
	// as opposed to "Stripping The alien drone".
	// Human names will still show without "the", as they are proper nouns.
	data["name"] = "\the [owner]"

	return data

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

	. = TRUE

	var/mob/user = usr

	switch (action)
		if ("use")
			var/key = params["key"]
			var/datum/strippable_item/strippable_item = strippable.items[key]

			if (isnull(strippable_item))
				return

			if (!strippable_item.should_show(owner, user))
				return

			if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY)
				return

			var/item = strippable_item.get_item(owner)
			if (isnull(item))
				var/obj/item/held_item = user.get_active_held_item()
				if (isnull(held_item))
					return

				if (strippable_item.try_equip(owner, held_item, user))
					LAZYORASSOCLIST(interactions, user, key)

					// Yielding call
					var/should_finish = strippable_item.start_equip(owner, held_item, user)

					LAZYREMOVEASSOC(interactions, user, key)

					if (!should_finish)
						return

					if (QDELETED(src) || QDELETED(owner))
						return

					// They equipped an item in the meantime
					if (!isnull(strippable_item.get_item(owner)))
						return

					if (!user.Adjacent(owner))
						return

					strippable_item.finish_equip(owner, held_item, user)
			else if (strippable_item.try_unequip(owner, user))
				LAZYORASSOCLIST(interactions, user, key)

				var/should_unequip = strippable_item.start_unequip(owner, user)

				LAZYREMOVEASSOC(interactions, user, key)

				// Yielding call
				if (!should_unequip)
					return

				if (QDELETED(src) || QDELETED(owner))
					return

				// They changed the item in the meantime
				if (strippable_item.get_item(owner) != item)
					return

				if (!user.Adjacent(owner))
					return

				strippable_item.finish_unequip(owner, user)
		if ("alt")
			var/key = params["key"]
			var/datum/strippable_item/strippable_item = strippable.items[key]

			if (isnull(strippable_item))
				return

			if (!strippable_item.should_show(owner, user))
				return

			if (strippable_item.get_obscuring(owner) == STRIPPABLE_OBSCURING_COMPLETELY)
				return

			var/item = strippable_item.get_item(owner)
			if (isnull(item))
				return

			if (isnull(strippable_item.get_alternate_action(owner, user)))
				return

			LAZYORASSOCLIST(interactions, user, key)

			// Potentially yielding
			strippable_item.alternate_action(owner, user)

			LAZYREMOVEASSOC(interactions, user, key)

/datum/strip_menu/ui_host(mob/user)
	return owner

/datum/strip_menu/ui_state(mob/user)
	return GLOB.always_state

/datum/strip_menu/ui_status(mob/user, datum/ui_state/state)
	return min(
		ui_status_only_living(user, owner),
		ui_status_user_has_free_hands(user, owner),
		ui_status_user_is_adjacent(user, owner),
		HAS_TRAIT(user, TRAIT_CAN_STRIP) ? UI_INTERACTIVE : UI_UPDATE,
		max(
			ui_status_user_is_conscious_and_lying_down(user),
			ui_status_user_is_abled(user, owner),
		),
	)

/// Creates an assoc list of keys to /datum/strippable_item
/proc/create_strippable_list(types)
	var/list/strippable_items = list()

	for (var/strippable_type in types)
		var/datum/strippable_item/strippable_item = new strippable_type
		strippable_items[strippable_item.key] = strippable_item

	return strippable_items