Repository: WorldOfDarknessXIII/World-of-Darkness-13
Pull request #932: [Port] Adds Leaning to the game
Author: login: XeonMations
Created: 2025-03-11T19:25:07Z | Merged: 2025-03-20T08:53:23Z

<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull request process. -->

## About The Pull Request

<!-- Describe The Pull Request. Please be sure every change is documented or this can delay review and even discourage maintainers from merging your PR! -->

This is an edited port of https://github.com/BeeStation/BeeStation-Hornet/pull/12046 , which is a heavily edited port of various TG PRs.


## Why It's Good For The Game

<!-- Argue for the merits of your changes and how they benefit the game, especially if they are controversial and/or far reaching. If you can't actually explain WHY what you are doing will improve the game, then it probably isn't good for the game in the first place. -->

leaning...

## Testing Photographs and Procedure
<!-- Include any screenshots/videos/debugging steps of the modified code functioning successfully, ideally including edge cases. -->
<!-- You can uncomment line 1 @ _maps/_basemap.dm to boot up a test map that loads much faster. -->
<details>
<summary>Screenshots&Videos</summary>

https://github.com/user-attachments/assets/70226181-ef63-4654-bade-2a9225857429

</details>

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely observed by players or admins you should add a changelog. If your change does NOT meet this description, remove this section. Be sure to properly mark your PRs to prevent unnecessary GBP loss. You can read up on GBP and its effects on PRs in the tgstation guides for contributors. Please note that maintainers freely reserve the right to remove and add tags should they deem it appropriate. You can attempt to finagle the system all you want, but it's best to shoot for clear communication right off the bat. -->

:cl: XeonMations, itseasytosee, vinylspiders, MrMelbert, SmArtKar, optimumtact, SyncIt21
add: Added leaning to the game! You can now lean on things by dragging your character onto stuff next to it.
balance: You can now only climb walls when in harm intent.
/:cl:

<!-- Both :cl:'s are required for the changelog to work! You can put your name to the right of the first :cl: if you want to overwrite your GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the icon ingame) and delete the unneeded ones. Despite some of the tags, changelogs should generally represent how a player might be affected by the changes rather than a summary of the PR's contents. -->



Diff:
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index b94529e4d420..635f72013594 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -240,6 +240,15 @@
 	/// When returned from `COMSIG_ATOM_SINGULARITY_TRY_MOVE`, the singularity will move to that turf
 	#define SINGULARITY_TRY_MOVE_BLOCK (1 << 0)
 
+// From /atom/proc/set_density(new_value) for when an atom changes density
+#define COMSIG_ATOM_DENSITY_CHANGED "atom_density_change"
+
+#define COMSIG_MOVABLE_PULLED "movable_pulled"						//! signal sent out by an atom when it is being pulled by something else : (atom/puller)
+#define COMSIG_MOVABLE_NO_LONGER_PULLED "movable_no_longer_pulled"	//! signal sent out by an atom when it is no longer being pulled by something else : (atom/puller)
+
+///From living/set_resting(): (new_resting, silent, instant)
+#define COMSIG_LIVING_RESTING "living_resting"
+
 /////////////////
 
 ///from base of area/Entered(): (/area)
@@ -424,6 +433,9 @@
 ///from /obj/structure/door/crush(): (mob/living/crushed, /obj/machinery/door/crushing_door)
 #define COMSIG_LIVING_DOORCRUSHED "living_doorcrush"
 
+/// From base of /client/Move()
+#define COMSIG_MOB_CLIENT_MOVED "mob_client_moved"
+
 ///from base of mob/living/resist() (/mob/living)
 #define COMSIG_LIVING_RESIST "living_resist"
 ///from base of mob/living/IgniteMob() (/mob/living)
@@ -480,6 +492,13 @@
 #define COMSIG_LIVING_CAN_TRACK "mob_cantrack"
 	#define COMPONENT_CANT_TRACK (1<<0)
 
+/// From /mob/living/proc/stop_leaning()
+#define COMSIG_LIVING_STOPPED_LEANING "living_stopped_leaning"
+
+/// Called when a living mob has its resting updated: (resting_state)
+#define COMSIG_LIVING_RESTING_UPDATED "resting_updated"
+
+
 ///From /datum/component/creamed/Initialize()
 #define COMSIG_MOB_CREAMED "mob_creamed"
 ///From /obj/item/gun/proc/check_botched()
diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm
index d0c27e999bf7..437ccfa3dbf8 100644
--- a/code/__DEFINES/status_effects.dm
+++ b/code/__DEFINES/status_effects.dm
@@ -114,6 +114,9 @@
 #define STATUS_EFFECT_HIGHFIVE /datum/status_effect/high_fiving // you are angling for a high five
 
 #define STATUS_EFFECT_SURRENDER /datum/status_effect/surrender // gives an alert to quickly surrender
+
+#define STATUS_EFFECT_LEANING /datum/status_effect/leaning //! Makes you lean on something
+
 /////////////
 //  SLIME  //
 /////////////
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index ce9d12fd16cb..2dfb1b9bd9ce 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -531,3 +531,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
 
 /// Trait from [/datum/antagonist/nukeop/clownop]
 #define CLOWNOP_TRAIT "clownop"
+
+/// Apply this to make a mob not dense, and remove it when you want it to no longer make them undense, other sorces of undesity will still apply. Always define a unique source when adding a new instance of this!
+#define TRAIT_UNDENSE "undense"
+/// A trait gained by leaning against a wall
+#define TRAIT_LEANING "leaning"
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index e5328fa6aac6..06ce4729dd79 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -188,7 +188,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
 		"TRAIT_HANDS_BLOCK_PROJECTILES" = TRAIT_HANDS_BLOCK_PROJECTILES,
 		"TRAIT_ENHANCED_MELEE_DODGE" = TRAIT_ENHANCED_MELEE_DODGE,
 		"TRAIT_SUPERNATURAL_DEXTERITY" = TRAIT_SUPERNATURAL_DEXTERITY,
-		"TRAIT_PASS_THROUGH_WALLS" = TRAIT_PASS_THROUGH_WALLS
+		"TRAIT_PASS_THROUGH_WALLS" = TRAIT_PASS_THROUGH_WALLS,
+		"TRAIT_UNDENSE" = TRAIT_UNDENSE,
+		"TRAIT_LEANING" = TRAIT_LEANING
 	),
 	/obj/item/bodypart = list(
 		"TRAIT_PARALYSIS" = TRAIT_PARALYSIS
diff --git a/code/_onclick/drag_drop.dm b/code/_onclick/drag_drop.dm
index 26fd1d9ceab1..6fcc3f4dc2e5 100644
--- a/code/_onclick/drag_drop.dm
+++ b/code/_onclick/drag_drop.dm
@@ -21,8 +21,6 @@
 // receive a mousedrop
 /atom/proc/MouseDrop_T(atom/dropping, mob/user)
 	SEND_SIGNAL(src, COMSIG_MOUSEDROPPED_ONTO, dropping, user)
-	return
-
 
 /client/MouseDown(object, location, control, params)
 	if (mouse_down_icon)
diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm
index e19b796c347b..327bc3e32e21 100644
--- a/code/datums/components/_component.dm
+++ b/code/datums/components/_component.dm
@@ -159,12 +159,7 @@
 /datum/component/proc/UnregisterFromParent()
 	return
 
-/**
- * Register to listen for a signal from the passed in target
- *
- * This sets up a listening relationship such that when the target object emits a signal
- * the source datum this proc is called upon, will receive a callback to the given proctype
- * Return values from procs registered must be a bitfield
+/* Return values from procs registered must be a bitfield
  *
  * Arguments:
  * * datum/target The target to listen for signals from
diff --git a/code/datums/components/leanable.dm b/code/datums/components/leanable.dm
new file mode 100644
index 000000000000..fef6640453ff
--- /dev/null
+++ b/code/datums/components/leanable.dm
@@ -0,0 +1,149 @@
+/// Things with this component can be leaned onto, optionally exclusive to RMB dragging in the future
+/datum/component/leanable
+	dupe_mode = COMPONENT_DUPE_UNIQUE
+	/// How much will mobs that lean onto this object be offset
+	var/leaning_offset = 11
+	/// Leaning mob of our parent, as only one person can lean on us at a time
+	var/mob/living/leaning_mob
+	/// Is this object currently leanable?
+	var/is_currently_leanable = TRUE
+
+/datum/component/leanable/Initialize(mob/living/leaner, leaning_offset = 11)
+	. = ..()
+	src.leaning_offset = leaning_offset
+	var/atom/leanable_atom = parent
+	is_currently_leanable = leanable_atom.density
+	leaning_mob = leaner
+
+/datum/component/leanable/RegisterWithParent()
+	RegisterSignal(parent, COMSIG_MOUSEDROPPED_ONTO, PROC_REF(mousedrop_receive))
+	RegisterSignal(leaning_mob, COMSIG_LIVING_STOPPED_LEANING, PROC_REF(stopped_leaning))
+	RegisterSignal(parent, COMSIG_ATOM_DENSITY_CHANGED, PROC_REF(on_density_change))
+	if(!mousedrop_receive(parent, leaning_mob, leaning_mob))
+		stopped_leaning(src)
+
+/datum/component/leanable/UnregisterFromParent()
+	UnregisterSignal(parent, COMSIG_MOUSEDROPPED_ONTO)
+	UnregisterSignal(leaning_mob, COMSIG_LIVING_STOPPED_LEANING)
+	UnregisterSignal(parent, COMSIG_ATOM_DENSITY_CHANGED)
+	leaning_mob = null
+
+/datum/component/leanable/proc/stopped_leaning(obj/source)
+	SIGNAL_HANDLER
+	qdel(src)
+
+/datum/component/leanable/proc/mousedrop_receive(atom/source, atom/movable/dropped, mob/user, params)
+	if(dropped != user) //Have we been dropped on a valid leanable object?
+		return FALSE
+	var/mob/living/leaner = dropped
+	leaning_mob = leaner
+	if(!iscarbon(dropped) && !iscyborg(dropped)) //Are we not a cyborg or carbon?
+		return FALSE
+	if(!(usr == leaner)) //Are we trying to lean someone else?
+		return FALSE
+	if(leaner.incapacitated(ignore_restraints = TRUE) || leaner.stat != CONSCIOUS || leaner.notransform || leaner.buckled || leaner.body_position == LYING_DOWN) //Are we in a valid state?
+		return FALSE
+	if(HAS_TRAIT_FROM(leaner, TRAIT_UNDENSE, TRAIT_LEANING)) //Are we leaning already?
+		return FALSE
+	if(ISDIAGONALDIR(get_dir(leaner, source)) || ((get_dir(leaner, source)) == SOUTH)) //Not leaning on a corner, idiot, or a south wall because it looks bad
+		return FALSE
+	if(!is_currently_leanable) //Is the object currently able to be leaned on?
+		return FALSE
+
+	leaner.apply_status_effect(STATUS_EFFECT_LEANING, source, leaning_offset)
+	return TRUE
+
+/datum/component/leanable/proc/on_density_change()
+	SIGNAL_HANDLER
+	is_currently_leanable = !is_currently_leanable
+	if(!is_currently_leanable)
+		leaning_mob.stop_leaning(src)
+
+/**
+ * Makes the mob lean on an atom
+ * Arguments
+ *
+ * * atom/lean_target - the target the mob is trying to lean on
+ * * leaning_offset - pixel offset to apply on the mob when leaning
+ */
+/mob/living/proc/start_leaning(atom/lean_target, leaning_offset)
+	var/new_x = lean_target.pixel_x + base_pixel_x + body_position_pixel_x_offset
+	var/new_y = lean_target.pixel_y + base_pixel_y + body_position_pixel_y_offset
+	switch(get_dir(src, lean_target))
+		if(NORTH)
+			new_y += leaning_offset
+		if(WEST)
+			new_x -= leaning_offset
+		if(EAST)
+			new_x += leaning_offset
+
+	animate(src, 0.2 SECONDS, pixel_x = new_x, pixel_y = new_y)
+	ADD_TRAIT(src, TRAIT_UNDENSE, TRAIT_LEANING)
+	visible_message(
+		span_notice("[src] leans against [lean_target]."),
+		span_notice("You lean against [lean_target]."),
+	)
+	leaned_object = lean_target
+	RegisterSignal(src, list(
+		COMSIG_MOB_CLIENT_MOVED,
+		COMSIG_HUMAN_DISARM_HIT,
+		COMSIG_MOVABLE_PULLED,
+		COMSIG_PARENT_QDELETING,
+		COMSIG_LIVING_RESIST, // Pressing B!
+		COMSIG_LIVING_MINOR_SHOCK, //If we are hit by a stunbaton
+		COMSIG_LIVING_RESTING_UPDATED //If we are downed
+	), PROC_REF(stop_leaning))
+	RegisterSignal(lean_target, COMSIG_AIRLOCK_OPEN, PROC_REF(fall_into))
+	RegisterSignal(lean_target, COMSIG_MOVABLE_MOVED, PROC_REF(fall_into_ex_turf))
+
+/mob/living/proc/stop_leaning(atom/parent)
+	SIGNAL_HANDLER
+	remove_status_effect(/datum/status_effect/leaning)
+	UnregisterSignal(src, list(
+		COMSIG_MOB_CLIENT_MOVED,
+		COMSIG_HUMAN_DISARM_HIT,
+		COMSIG_MOVABLE_PULLED,
+		COMSIG_PARENT_QDELETING,
+		COMSIG_LIVING_RESIST,
+		COMSIG_LIVING_MINOR_SHOCK,
+		COMSIG_LIVING_RESTING_UPDATED
+	))
+	UnregisterSignal(leaned_object, list(COMSIG_AIRLOCK_OPEN, COMSIG_MOVABLE_MOVED))
+	leaned_object = null
+	animate(src, 0.2 SECONDS, pixel_x = base_pixel_x + body_position_pixel_x_offset, pixel_y = base_pixel_y + body_position_pixel_y_offset)
+	REMOVE_TRAIT(src, TRAIT_UNDENSE, TRAIT_LEANING)
+	SEND_SIGNAL(src, COMSIG_LIVING_STOPPED_LEANING)
+
+
+/// You fall on your face if you get teleported while leaning
+/mob/living/proc/teleport_away_while_leaning(datum/source)
+	SIGNAL_HANDLER
+	// Make sure we unregister signal handlers and reset animation
+	stop_leaning()
+	// -1000 aura
+	visible_message(span_notice("[src] falls flat on [p_their()] face from losing [p_their()] balance!"), span_warning("You fall suddenly as the object you were leaning on vanishes from contact with you!"))
+	Knockdown(3 SECONDS)
+
+/mob/living/proc/fall_into(datum/source, atom/moved, direction)
+	SIGNAL_HANDLER
+	if(HAS_TRAIT(src, NO_GRAVITY_TRAIT)) //If there's no gravity on the mob, don't fall lmao
+		return
+	fall_forced(get_turf(source))
+
+/mob/living/proc/fall_into_ex_turf(datum/source, atom/moved, direction)
+	SIGNAL_HANDLER
+	var/fall_location = get_step(get_turf(source), turn(direction, 180)) //Invert the direction the mech moved to, so we fall where the mech once was.
+	fall(fall_location)
+
+/mob/living/proc/fall(location)
+	stop_leaning() // Make sure we unregister signal handlers and reset animation
+	Move(location)
+	visible_message(span_notice("[src] falls flat on [p_their()] face from losing [p_their()] balance!"), span_warning("You fall suddenly!"))
+	Knockdown(3 SECONDS) //boowomp
+
+// Fall_forced is only really used for airlocks, because their density is dense when the airlock is still opening.
+/mob/living/proc/fall_forced(location)
+	stop_leaning() // Make sure we unregister signal handlers and reset animation
+	forceMove(location)
+	visible_message(span_notice("[src] falls flat on [p_their()] face from losing [p_their()] balance!"), span_warning("You fall suddenly as the door you were leaning on opens!"))
+	Knockdown(3 SECONDS) //boowomp
diff --git a/code/datums/elements/climbable.dm b/code/datums/elements/climbable.dm
index 84870197ce36..857361ce972c 100644
--- a/code/datums/elements/climbable.dm
+++ b/code/datums/elements/climbable.dm
@@ -66,7 +66,7 @@
 	if(HAS_TRAIT(user, TRAIT_FREERUNNING)) //do you have any idea how fast I am???
 		adjusted_climb_time *= 0.8
 	LAZYADDASSOC(current_climbers, climbed_thing, user)
-	if(do_after(user, adjusted_climb_time, climbed_thing))
+	if(do_after(user, adjusted_climb_time, climbed_thing) && !HAS_TRAIT(user, TRAIT_LEANING))
 		if(QDELETED(climbed_thing)) //Checking if structure has been destroyed
 			return
 		if(do_climb(climbed_thing, user))
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index d3184f684dcf..2fc2748e5e8b 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -333,3 +333,29 @@
 	tick_interval = INFINITY
 	status_type = STATUS_EFFECT_REFRESH
 	alert_type = null
+
+/atom/movable/screen/alert/status_effect/leaning
+	name = "Leaning"
+	desc = "You're leaning on something!"
+	icon_state = "buckled"
+
+/atom/movable/screen/alert/status_effect/leaning/Click()
+	var/mob/living/L = usr
+	if(!istype(L) || L != owner)
+		return
+	L.changeNext_move(CLICK_CD_RESIST)
+	if(L.last_special <= world.time)
+		return L.stop_leaning()
+
+/datum/status_effect/leaning
+	id = "leaning"
+	duration = -1
+	tick_interval = -1
+	status_type = STATUS_EFFECT_UNIQUE
+	alert_type = /atom/movable/screen/alert/status_effect/leaning
+
+/datum/status_effect/leaning/on_creation(mob/living/carbon/new_owner, atom/object, leaning_offset = 11)
+	. = ..()
+	if(!.)
+		return
+	new_owner.start_leaning(object, leaning_offset)
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index e4324b1b88a1..88f4d393353a 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -1764,6 +1764,15 @@
 			return max_grav
 	return SSmapping.level_trait(T.z, ZTRAIT_GRAVITY)
 
+///Setter for the `density` variable to append behavior related to its changing.
+/atom/proc/set_density(new_value)
+	SHOULD_CALL_PARENT(TRUE)
+	if(density == new_value)
+		return
+	. = density
+	density = new_value
+	SEND_SIGNAL(src, COMSIG_ATOM_DENSITY_CHANGED)
+
 /**
  * Causes effects when the atom gets hit by a rust effect from heretics
  *
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 88fdf7a7b7c5..35909ed129d0 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -264,15 +264,16 @@
 		if(!supress_message)
 			M.visible_message("<span class='warning'>[src] grabs [M] passively.</span>", \
 				"<span class='danger'>[src] grabs you passively.</span>")
+	SEND_SIGNAL(pulling, COMSIG_MOVABLE_PULLED)
 	return TRUE
 
 /atom/movable/proc/stop_pulling()
 	if(pulling)
 		pulling.set_pulledby(null)
 		setGrabState(GRAB_PASSIVE)
+		SEND_SIGNAL(pulling, COMSIG_MOVABLE_NO_LONGER_PULLED)
 		pulling = null
 
-
 ///Reports the event of the change in value of the pulledby variable.
 /atom/movable/proc/set_pulledby(new_pulledby)
 	if(new_pulledby == pulledby)
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index f5a448b18738..1cde8936a994 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -26,6 +26,12 @@
 	if(climbable)
 		AddElement(/datum/element/climbable)
 
+/obj/structure/railing/MouseDrop_T(atom/dropping, mob/user, params)
+	. = ..()
+
+	if(!climbable)
+		LoadComponent(/datum/component/leanable, dropping)
+
 /obj/structure/railing/attackby(obj/item/I, mob/living/user, params)
 	..()
 	add_fingerprint(user)
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index 2ccb1ebd1f58..1929692cd09e 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -97,6 +97,11 @@
 	if(current_size >= STAGE_FIVE)
 		deconstruct(FALSE)
 
+/obj/structure/window/MouseDrop_T(atom/dropping, mob/user)
+	. = ..()
+
+	LoadComponent(/datum/component/leanable, dropping)
+
 /obj/structure/window/CanAllowThrough(atom/movable/mover, turf/target)
 	. = ..()
 	if(.)
diff --git a/code/game/turfs/closed/walls.dm b/code/game/turfs/closed/walls.dm
index 93cf8043ed78..2963cf33ac50 100644
--- a/code/game/turfs/closed/walls.dm
+++ b/code/game/turfs/closed/walls.dm
@@ -45,7 +45,6 @@
 		fixed_underlay = string_assoc_list(fixed_underlay)
 		underlays += underlay_appearance
 
-
 /turf/closed/wall/Destroy()
 	if(is_station_level(z))
 		GLOB.station_turfs -= src
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 1347feeb255c..3154f56d1f07 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -1166,8 +1166,8 @@
 		var/total_athletics = get_total_athletics()
 		to_chat(src, "<span class='notice'>You start climbing up...</span>")
 
-		var/result = do_after(src, 50 - (total_dexterity + total_athletics * 5), 0)
-		if(!result)
+		var/result = do_after(src, 50 - (total_dexterity + total_athletics * 5), src)
+		if(!result || HAS_TRAIT(src, TRAIT_LEANING))
 			to_chat(src, "<span class='warning'>You were interrupted and failed to climb up.</span>")
 			return
 
@@ -1201,19 +1201,6 @@
 
 	return
 
-/mob/living/carbon/human/MouseDrop(atom/over_object)
-	. = ..()
-	if(src == usr)
-		if(istype(over_object, /turf/closed/wall/vampwall))
-			if(get_dist(src, over_object) < 2)
-				var/turf/above_turf = locate(x, y, z + 1)
-				if(above_turf && istype(above_turf, /turf/open/openspace))
-					climb_wall(above_turf)
-				else
-					to_chat(src, "<span class='warning'>You can't climb there!</span>")
-
-	return
-
 /mob/living/carbon/human/proc/climb_down(turf/open/openspace/target_turf)
 	if(body_position != STANDING_UP)
 		return
diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm
index ef518022e3c8..a5c6a81297a7 100644
--- a/code/modules/mob/living/carbon/human/human_movement.dm
+++ b/code/modules/mob/living/carbon/human/human_movement.dm
@@ -52,3 +52,8 @@
 	if(dna.species.space_move(src))
 		return TRUE
 	return ..()
+
+/mob/living/carbon/human/MouseDrop_T(atom/dropping, atom/user)
+	. = ..()
+	if(dropping != src)
+		LoadComponent(/datum/component/leanable, dropping)
diff --git a/code/modules/mob/living/init_signals.dm b/code/modules/mob/living/init_signals.dm
index 3d4a4af81b88..017e5f0f7a13 100644
--- a/code/modules/mob/living/init_signals.dm
+++ b/code/modules/mob/living/init_signals.dm
@@ -41,6 +41,8 @@
 	RegisterSignal(src, COMSIG_MOVETYPE_FLAG_ENABLED, PROC_REF(on_movement_type_flag_enabled))
 	RegisterSignal(src, COMSIG_MOVETYPE_FLAG_DISABLED, PROC_REF(on_movement_type_flag_disabled))
 
+	RegisterSignal(src, list(SIGNAL_ADDTRAIT(TRAIT_UNDENSE), SIGNAL_REMOVETRAIT(TRAIT_UNDENSE)), PROC_REF(undense_changed))
+
 	RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_SKITTISH), PROC_REF(on_skittish_trait_gain))
 	RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_SKITTISH), PROC_REF(on_skittish_trait_loss))
 
@@ -72,6 +74,10 @@
 	SIGNAL_HANDLER
 	REMOVE_TRAIT(src, TRAIT_KNOCKEDOUT, TRAIT_DEATHCOMA)
 
+/// Called when [TRAIT_UNDENSE] is gained or lost
+/mob/living/proc/undense_changed(datum/source)
+	SIGNAL_HANDLER
+	update_density()
 
 /// Called when [TRAIT_IMMOBILIZED] is added to the mob.
 /mob/living/proc/on_immobilized_trait_gain(datum/source)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index de1e5919de4e..c8c093f9055d 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -536,12 +536,14 @@
 				to_chat(src, "<span class='notice'>You stand up.</span>")
 			get_up(instant)
 
+	SEND_SIGNAL(src, COMSIG_LIVING_RESTING, new_resting, silent, instant)
 	update_resting()
 
 
 /// Proc to append and redefine behavior to the change of the [/mob/living/var/resting] variable.
 /mob/living/proc/update_resting()
 	update_rest_hud_icon()
+	SEND_SIGNAL(src, COMSIG_LIVING_RESTING_UPDATED, resting)
 
 
 /mob/living/proc/get_up(instant = FALSE)
@@ -586,7 +588,11 @@
 	REMOVE_TRAIT(src, TRAIT_PULL_BLOCKED, LYING_DOWN_TRAIT)
 	body_position_pixel_y_offset = 0
 
-
+/mob/living/proc/update_density()
+	if(HAS_TRAIT(src, TRAIT_UNDENSE))
+		set_density(FALSE)
+	else
+		set_density(TRUE)
 
 //Recursive function to find everything a mob is holding. Really shitty proc tbh.
 /mob/living/get_contents()
@@ -1413,6 +1419,7 @@
 		if(M.can_be_held && U.pulling == M)
 			M.mob_try_pickup(U)//blame kevinz
 			return//dont open the mobs inventory if you are picking them up
+
 	. = ..()
 
 /mob/living/proc/mob_pickup(mob/living/L)
@@ -1933,13 +1940,13 @@
 		set_lying_angle(pick(90, 270))
 		set_body_position(LYING_DOWN)
 		on_fall()
-
+	SEND_SIGNAL(src, COMSIG_LIVING_RESTING_UPDATED)
 
 /// Proc to append behavior to the condition of being floored. Called when the condition ends.
 /mob/living/proc/on_floored_end()
 	if(!resting)
 		get_up()
-
+	SEND_SIGNAL(src, COMSIG_LIVING_RESTING_UPDATED)
 
 /// Proc to append behavior to the condition of being handsblocked. Called when the condition starts.
 /mob/living/proc/on_handsblocked_start()
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index 5d851d2c9574..ef441ac783b1 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -286,3 +286,6 @@
 	var/max_demon_chi = 0
 	COOLDOWN_DECLARE(chi_restore)
 	var/datum/action/chi_discipline/chi_ranged
+
+	//If we are currently leaning on something, and what that object is
+	var/atom/leaned_object
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 8ff385c1673c..05dbabd05a6b 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -160,6 +160,10 @@
 		if(mob.throwing)
 			mob.throwing.finalize(FALSE)
 
+		// At this point we've moved the client's attached mob. This is one of the only ways to guess that a move was done
+		// as a result of player input and not because they were pulled or any other magic.
+		SEND_SIGNAL(mob, COMSIG_MOB_CLIENT_MOVED)
+
 	var/atom/movable/P = mob.pulling
 	if(P && !ismob(P) && P.density)
 		mob.setDir(turn(mob.dir, 180))
diff --git a/code/modules/wod13/decor.dm b/code/modules/wod13/decor.dm
index 17957f5b3110..779e7434c683 100644
--- a/code/modules/wod13/decor.dm
+++ b/code/modules/wod13/decor.dm
@@ -415,6 +415,13 @@
 			if(V.upper)
 				icon_state = "[initial(icon_state)]-snow"
 
+/obj/structure/hydrant/MouseDrop_T(atom/dropping, mob/user, params)
+	. = ..()
+
+	if(HAS_TRAIT(user, TRAIT_DWARF)) //Only lean on the fire hydrant if we are smol
+		//Adds the component only once. We do it here & not in Initialize() because there are tons of windows & we don't want to add to their init times
+		LoadComponent(/datum/component/leanable, dropping)
+
 /obj/structure/vampcar
 	name = "car"
 	desc = "It drives."
diff --git a/code/modules/wod13/npc.dm b/code/modules/wod13/npc.dm
index 1e941388014c..4de58543624c 100644
--- a/code/modules/wod13/npc.dm
+++ b/code/modules/wod13/npc.dm
@@ -526,17 +526,10 @@
 		if(get_dist(src, CPN.walktarget) <= getaway)
 			walk(src,0)
 			CPN.walktarget = null
-//		for(var/obj/effect/decal/cleanable/blood/B in NewLoc)
-//			if(B)
-//				if(B.bloodiness)
-//					walk(src,0)
-//					CPN.walktarget = null
-//					if(!CPN.CheckMove())
-//						step_away(CPN, B)
 	if(HAS_TRAIT(src, TRAIT_RUBICON))
 		if(istype(NewLoc, /turf/open/floor/plating/shit))
 			return
-	..()
+	. = ..()
 
 
 /mob/living/carbon/human/toggle_resting()
diff --git a/code/modules/wod13/onyxcombat.dm b/code/modules/wod13/onyxcombat.dm
index fd14146fc35b..55c0d7841bb8 100644
--- a/code/modules/wod13/onyxcombat.dm
+++ b/code/modules/wod13/onyxcombat.dm
@@ -477,7 +477,7 @@
 					H.harm_focus = H.dir
 
 /mob/living/carbon/Move(atom/newloc, direct, glide_size_override)
-	..()
+	. = ..()
 	if(a_intent == INTENT_HARM && client)
 		setDir(harm_focus)
 	else
diff --git a/code/modules/wod13/structures/doors/vampdoor.dm b/code/modules/wod13/structures/doors/vampdoor.dm
index 3e1b9eef7b8f..925fdc781094 100644
--- a/code/modules/wod13/structures/doors/vampdoor.dm
+++ b/code/modules/wod13/structures/doors/vampdoor.dm
@@ -80,6 +80,12 @@
 		var/odds = value ? clamp((value/max_rand_value), 0, 1) : 0
 		. += "<span class='notice'>As an expert in lockpicking, you estimate that you have a [round(odds*100, 1)]% chance to lockpick this door successfully.</span>"
 
+/obj/structure/vampdoor/MouseDrop_T(atom/dropping, mob/user, params)
+	. = ..()
+
+	//Adds the component only once. We do it here & not in Initialize() because there are tons of windows & we don't want to add to their init times
+	LoadComponent(/datum/component/leanable, dropping)
+
 /obj/structure/vampdoor/attack_hand(mob/user)
 	. = ..()
 	var/mob/living/N = user
@@ -124,6 +130,7 @@
 		layer = OPEN_DOOR_LAYER
 		to_chat(user, "<span class='notice'>You open [src].</span>")
 		closed = FALSE
+		SEND_SIGNAL(src, COMSIG_AIRLOCK_OPEN)
 	else
 		for(var/mob/living/L in src.loc)
 			if(L)
diff --git a/code/modules/wod13/wallcode.dm b/code/modules/wod13/wallcode.dm
index 19148d4253f3..9a7e90a9f228 100644
--- a/code/modules/wod13/wallcode.dm
+++ b/code/modules/wod13/wallcode.dm
@@ -68,38 +68,19 @@
 /turf/closed/wall/vampwall/attack_hand(mob/user)
 	return
 
-/*/turf/closed/wall/vampwall/attack_hand(mob/user)
-	if(ismob(user))
-		var/mob/living/carbon/human/H = user
-
-		var/new_z = H.z + 1
-		var/turf/above_turf = locate(H.x, H.y, new_z)
-		if(H.body_position != STANDING_UP)
-			return
-		if(above_turf && istype(above_turf, /turf/open/openspace))
-			to_chat(H, "<span class='notice'>You start climbing up...")
-
-			var/initial_x = H.x
-			var/initial_y = H.y
-			var/initial_z = H.z
-
-			spawn(20)
-				if(H.x != initial_x || H.y != initial_y || H.z != initial_z)
-					return
-
-				var/roll = rand(1, 20)
-				var/physique = H.physique
-				if(roll + physique*2 >= 15)
-					H.loc = above_turf
-					var/turf/forward_turf = get_step(H.loc, H.dir)
-					if(forward_turf && !forward_turf.density)
-						H.forceMove(forward_turf)
-						to_chat(H, "<span class='notice'>You climb up.</span>")
-				else
-					to_chat(user, ("<span class='warning'>You fail to climb up.</span>"))
+/turf/closed/wall/vampwall/MouseDrop_T(atom/dropping, mob/user, params)
+	. = ..()
+	if(user.a_intent != INTENT_HARM)
+		//Adds the component only once. We do it here & not in Initialize() because there are tons of windows & we don't want to add to their init times
+		LoadComponent(/datum/component/leanable, dropping)
 	else
-		return
-*/
+		if(get_dist(user, src) < 2)
+			var/turf/above_turf = locate(user.x, user.y, user.z + 1)
+			if(above_turf && istype(above_turf, /turf/open/openspace))
+				var/mob/living/carbon/human/carbon_human = user
+				carbon_human.climb_wall(above_turf)
+			else
+				to_chat(user, "<span class='warning'>You can't climb there!</span>")
 
 /turf/closed/wall/vampwall/ex_act(severity, target)
 	return
@@ -966,6 +947,7 @@
 	icon_state = "wallpaper"
 	plane = GAME_PLANE
 	layer = ABOVE_NORMAL_TURF_LAYER	//WALLPAPER_LAYER dont work
+	mouse_opacity = 0
 
 /obj/effect/decal/wallpaper/Initialize()
 	..()
diff --git a/tgstation.dme b/tgstation.dme
index 04f1cc785a76..401e3ef18f74 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -483,6 +483,7 @@
 #include "code\datums\components\knockback.dm"
 #include "code\datums\components\knockoff.dm"
 #include "code\datums\components\label.dm"
+#include "code\datums\components\leanable.dm"
 #include "code\datums\components\lifesteal.dm"
 #include "code\datums\components\lockon_aiming.dm"
 #include "code\datums\components\manual_blinking.dm"


Reviews:
user: login: Agateophobia
body: LGTM, probably needs a testmerge to find all of the stupid silly bugs our stuff might introduce.
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-11T21:53:37Z
user: login: hex37
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T23:20:08Z
user: login: Agateophobia
body: lgtm
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-13T09:31:01Z
user: login: Metekillot
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T09:43:50Z
user: login: XeonMations
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T20:18:38Z

Review comments:
diff_hunk: @@ -204,6 +204,15 @@
 
 	signal_enabled = TRUE
 
+/// Registers multiple signals to the same proc.
+/datum/proc/RegisterSignals(datum/target, list/signal_types, proctype, override = FALSE)
path: code/datums/components/_component.dm
user: login: hex37
body: RegisterSignal already takes multiple signals?
created_at: 2025-03-11T23:20:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +66,8 @@
 	if(HAS_TRAIT(user, TRAIT_FREERUNNING)) //do you have any idea how fast I am???
 		adjusted_climb_time *= 0.8
 	LAZYADDASSOC(current_climbers, climbed_thing, user)
+	if(HAS_TRAIT(user, TRAIT_LEANING))
path: code/datums/elements/climbable.dm
user: login: Metekillot
body: This returns after this object receives the user as a current climber.
created_at: 2025-03-13T09:37:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,3 +333,29 @@
 	tick_interval = INFINITY
 	status_type = STATUS_EFFECT_REFRESH
 	alert_type = null
+
+/atom/movable/screen/alert/status_effect/leaning
+	name = "Leaning"
+	desc = "You're leaning on something!"
+	icon_state = "buckled"
+
+/atom/movable/screen/alert/status_effect/leaning/Click()
+	var/mob/living/L = usr
+	if(!istype(L) || L != owner)
+		return
+	L.changeNext_move(CLICK_CD_RESIST)
path: code/datums/status_effects/neutral.dm
user: login: Metekillot
body: Why are we giving them a cooldown this long for ceasing to lean on something?
created_at: 2025-03-13T09:38:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,6 +26,13 @@
 	if(climbable)
 		AddElement(/datum/element/climbable)
 
+/obj/structure/railing/MouseDrop_T(atom/dropping, mob/user, params)
+	. = ..()
+
+	if(!climbable)
+		//Adds the component only once. We do it here & not in Initialize() because there are tons of windows & we don't want to add to their init times
path: code/game/objects/structures/railings.dm
user: login: Metekillot
body: What does this comment on windows have to do with railings?
created_at: 2025-03-13T09:39:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -286,3 +286,6 @@
 	var/max_demon_chi = 0
 	COOLDOWN_DECLARE(chi_restore)
 	var/datum/action/chi_discipline/chi_ranged
+
+	//If we are currently leaning on something, and what that object is
+	var/atom/leaned_object
path: code/modules/mob/living/living_defines.dm
user: login: Metekillot
body: Is this not able to be tracked on a status effect instead? Does every single living entity in the game need a discrete reference to an object they're leaning on, even when they're not leaning on something?
created_at: 2025-03-13T09:41:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -286,3 +286,6 @@
 	var/max_demon_chi = 0
 	COOLDOWN_DECLARE(chi_restore)
 	var/datum/action/chi_discipline/chi_ranged
+
+	//If we are currently leaning on something, and what that object is
+	var/atom/leaned_object
path: code/modules/mob/living/living_defines.dm
user: login: XeonMations
body: Yes, I couldn't figure out a way to easily get this via the status effect.
created_at: 2025-03-14T20:18:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: Agateophobia
created_at: 2025-03-11T20:11:18Z
body: Our god and saviour is here.
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-11T21:56:17Z
body: #### Test Merge Deployed

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: 5adf3562ca109f83c4c36803b6fa83924009b25c
Pull Request: 5385316e13a7222aa6d52a8d609bc28fef2bdecc
Server: 4dbc5decb1ba137ae94201195eeffcbefa87a75e

##### Comment

</details>
author_association: COLLABORATOR
user: login: Nyvrem
created_at: 2025-03-11T23:12:01Z
body: Harm intent climbing doesn't seem to work.
author_association: CONTRIBUTOR
user: login: wod13-server
created_at: 2025-03-11T23:25:28Z
body: #### Test Merge Removed
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-12T03:49:05Z
body: #### Test Merge Deployed

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: 5d957f49c0c26d89c2a3d87e3a6d1b6e9563219f
Pull Request: 327a0880e1bae2d7abd5f40a4d751bec52fc4edd
Server: 08a005cbcc63faa9313b6944645818d4221167a3

##### Comment

</details>
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-12T08:46:16Z
body: #### Test Merge Updated

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: 6b28c89141fd84402e1814bd937a24dd16d86594
Pull Request: 327a0880e1bae2d7abd5f40a4d751bec52fc4edd
Server: b83506509ac6ccdf6938f11049b1b83d52f640aa

##### Comment

</details>
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-12T16:38:26Z
body: #### Test Merge Updated

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: c84fab24be610eae9c690b6ab3ab1740c35d5ea0
Pull Request: 327a0880e1bae2d7abd5f40a4d751bec52fc4edd
Server: 4230099709926ce81a7a9596abd7132b270cd856

##### Comment

</details>
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-12T21:20:34Z
body: #### Test Merge Updated

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: c84fab24be610eae9c690b6ab3ab1740c35d5ea0
Pull Request: 7a5d02fb0a4debb30dc279504bde03c02585895f
Server: 22ac74b93fdeed5c93430e7b296af3e612f73317

##### Comment

</details>
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-15T12:29:43Z
body: #### Test Merge Updated

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: 67a10e6fa93c58a64a7c339b3967253e4a294337
Pull Request: 7a5d02fb0a4debb30dc279504bde03c02585895f
Server: 4fd2eaf4e213d0c696d7fdc51030634be3a23480

##### Comment

</details>
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-15T12:50:46Z
body: #### Test Merge Updated

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: 748781d35e75fb1c3eb3ce623bf7630cc2569d1b
Pull Request: 7a5d02fb0a4debb30dc279504bde03c02585895f
Server: f78f0c1f950c7214e1a558942207c54408153682

##### Comment

</details>
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-03-15T23:33:06Z
body: #### Test Merge Updated

<details><summary>Details</summary>

##### Server Instance
Live Server

##### Merged By
XeonMations

##### Revision
Origin: be4353a63405be65f39f048814f926683e6ab236
Pull Request: 7a5d02fb0a4debb30dc279504bde03c02585895f
Server: 53d4acc5f0132e595db6a4aa57d1204e63a849f2

##### Comment

</details>
author_association: COLLABORATOR
user: login: wod13-server
created_at: 2025-04-13T22:08:31Z
body: <!-- tgs_test_merge_comment -->
## Test merge deployment history:

<details><summary>Test Merge Removed @ 04/13/2025 22:07:57 +00:00:</summary>

##### Server Instance
Live Server
</details>

author_association: COLLABORATOR