diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index 387fdae5c68..8dd96b8d9bc 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -203,6 +203,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_CULT_IMMUNITY "cult_immunity"
#define TRAIT_FLATTENED "flattened"
#define SM_HALLUCINATION_IMMUNE "supermatter_hallucination_immune"
+#define TRAIT_CONTORTED_BODY "contorted_body"
//***** ITEM AND MOB TRAITS *****//
/// Show what machine/door wires do when held.
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index ddf721f7632..96a02b3ec8a 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -75,7 +75,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_SHOW_WIRE_INFO" = TRAIT_SHOW_WIRE_INFO,
"TRAIT_CAN_BE_EATEN_BY_LIZARDS" = TRAIT_EDIBLE_BUG,
"TRAIT_FLATTENED" = TRAIT_FLATTENED,
- "TRAIT_SM_HALLUCINATION_IMMUNE" = SM_HALLUCINATION_IMMUNE
+ "TRAIT_SM_HALLUCINATION_IMMUNE" = SM_HALLUCINATION_IMMUNE,
+ "TRAIT_CONTORTED_BODY" = TRAIT_CONTORTED_BODY
),
/obj/item = list(
"TRAIT_SHOW_WIRE_INFO" = TRAIT_SHOW_WIRE_INFO,
diff --git a/code/datums/components/caltrop.dm b/code/datums/components/caltrop.dm
index c26560d2d20..4446dde3a26 100644
--- a/code/datums/components/caltrop.dm
+++ b/code/datums/components/caltrop.dm
@@ -45,6 +45,9 @@
if(H.flying || H.floating || H.buckled)
return
+ if(IS_HORIZONTAL(H) && HAS_TRAIT(H, TRAIT_CONTORTED_BODY))
+ return TRUE
+
var/damage = rand(min_damage, max_damage)
H.apply_damage(damage, BRUTE, picked_def_zone)
diff --git a/code/game/machinery/OpTable.dm b/code/game/machinery/OpTable.dm
index 42f993cd9bb..7f74df70595 100644
--- a/code/game/machinery/OpTable.dm
+++ b/code/game/machinery/OpTable.dm
@@ -49,6 +49,10 @@
return TRUE
if(istype(mover) && mover.checkpass(PASSTABLE))
return TRUE
+ if(isliving(mover))
+ var/mob/living/our_mover = mover
+ if(IS_HORIZONTAL(our_mover) && HAS_TRAIT(our_mover, TRAIT_CONTORTED_BODY))
+ return TRUE
else
return FALSE
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index d8ca792f6cc..3a146a7d15f 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -748,8 +748,12 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
attack_ai(user)
/obj/machinery/door/airlock/CanPass(atom/movable/mover, turf/target, height=0)
- if(istype(mover) && mover.checkpass(PASSDOOR) && !locked) // We really dont want mice/drones to get shocked on a door they're passing through
- return TRUE
+ if(istype(mover) && !locked)
+ if(mover.checkpass(PASSDOOR))
+ return TRUE
+ var/mob/living/living_mover = mover
+ if((istype(living_mover) && HAS_TRAIT(living_mover, TRAIT_CONTORTED_BODY) && IS_HORIZONTAL(living_mover))) // We really dont want people to get shocked on a door they're passing through
+ return TRUE
if(isElectrified() && density && isitem(mover))
var/obj/item/I = mover
if(I.flags & CONDUCT)
diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm
index 303523f8f58..d829305b3d6 100644
--- a/code/game/machinery/doors/firedoor.dm
+++ b/code/game/machinery/doors/firedoor.dm
@@ -289,6 +289,14 @@
F.update_icon()
qdel(src)
+/obj/machinery/door/firedoor/CanPass(atom/movable/mover, turf/target)
+ if(..())
+ return TRUE
+ if(isliving(mover) && !locked)
+ var/mob/living/living_mover = mover
+ if(HAS_TRAIT(living_mover, TRAIT_CONTORTED_BODY) && IS_HORIZONTAL(living_mover))
+ return TRUE
+
/obj/machinery/door/firedoor/border_only
icon = 'icons/obj/doors/edge_doorfire.dmi'
flags = ON_BORDER
diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm
index c5d34376ead..0d3d3111056 100644
--- a/code/game/machinery/doors/windowdoor.dm
+++ b/code/game/machinery/doors/windowdoor.dm
@@ -134,7 +134,11 @@
/obj/machinery/door/window/CanPass(atom/movable/mover, turf/target, height=0)
if(istype(mover) && mover.checkpass(PASSGLASS))
- return 1
+ return TRUE
+ if(isliving(mover))
+ var/mob/living/living_mover = mover
+ if(HAS_TRAIT(living_mover, TRAIT_CONTORTED_BODY) && IS_HORIZONTAL(living_mover))
+ return TRUE
if(get_dir(loc, target) == dir) //Make sure looking at appropriate border
return !density
if(istype(mover, /obj/structure/window))
@@ -162,7 +166,11 @@
/obj/machinery/door/window/CheckExit(atom/movable/mover, turf/target)
if(istype(mover) && mover.checkpass(PASSGLASS))
- return 1
+ return TRUE
+ if(isliving(mover))
+ var/mob/living/living_mover = mover
+ if(HAS_TRAIT(living_mover, TRAIT_CONTORTED_BODY) && IS_HORIZONTAL(living_mover))
+ return TRUE
if(get_dir(loc, target) == dir)
return !density
else
diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm
index bd6009ce3d6..035de168476 100644
--- a/code/game/objects/structures/crates_lockers/crates.dm
+++ b/code/game/objects/structures/crates_lockers/crates.dm
@@ -70,11 +70,13 @@
playsound(loc, close_sound, close_sound_volume, TRUE, -3)
var/itemcount = 0
- for(var/obj/O in get_turf(src))
+ for(var/atom/movable/O in get_turf(src))
if(itemcount >= storage_capacity)
break
if(O.density || O.anchored || istype(O,/obj/structure/closet))
continue
+ if(ismob(O) && !HAS_TRAIT(O, TRAIT_CONTORTED_BODY))
+ continue
if(istype(O, /obj/structure/bed)) //This is only necessary because of rollerbeds and swivel chairs.
var/obj/structure/bed/B = O
if(B.has_buckled_mobs())
diff --git a/code/game/objects/structures/morgue.dm b/code/game/objects/structures/morgue.dm
index 0e7ad16c155..0b71005e3e8 100644
--- a/code/game/objects/structures/morgue.dm
+++ b/code/game/objects/structures/morgue.dm
@@ -254,18 +254,21 @@
connected = null
return ..()
-/obj/structure/tray/m_tray/CanPass(atom/movable/mover, turf/target, height=0)
+/obj/structure/m_tray/CanPass(atom/movable/mover, turf/target, height=0)
if(height == 0)
return TRUE
-
- if(istype(mover) && mover.checkpass(PASSTABLE))
- return TRUE
+ if(istype(mover))
+ if(mover.checkpass(PASSTABLE))
+ return TRUE
+ var/mob/living/our_mover = mover
+ if(istype(our_mover) && IS_HORIZONTAL(our_mover) && HAS_TRAIT(our_mover, TRAIT_CONTORTED_BODY))
+ return TRUE
if(locate(/obj/structure/table) in get_turf(mover))
return TRUE
return FALSE
-/obj/structure/tray/m_tray/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
+/obj/structure/m_tray/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
. = !density
if(ismovable(caller))
var/atom/movable/mover = caller
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index a63c83f8bdf..2d93cf9b154 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -80,8 +80,8 @@
if(istype(mover, /obj/item/projectile))
return TRUE
if(ismob(mover))
- var/mob/M = mover
- if(M.flying)
+ var/mob/living/M = mover
+ if(M.flying || (IS_HORIZONTAL(M) && HAS_TRAIT(M, TRAIT_CONTORTED_BODY)))
return TRUE
if(mover.throwing)
return TRUE
@@ -107,8 +107,8 @@
return TRUE
if(istype(O, /obj/item/projectile))
return TRUE
- if(ismob(O))
- if(M.flying || M.floating)
+ if(istype(M))
+ if(M.flying || M.floating || (IS_HORIZONTAL(M) && HAS_TRAIT(M, TRAIT_CONTORTED_BODY)))
return TRUE
if(O.throwing)
return TRUE
diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm
index 5e96f473b7e..cd742fffd70 100644
--- a/code/game/objects/structures/tables_racks.dm
+++ b/code/game/objects/structures/tables_racks.dm
@@ -122,8 +122,8 @@
if(istype(mover,/obj/item/projectile))
return (check_cover(mover,target))
if(ismob(mover))
- var/mob/M = mover
- if(M.flying)
+ var/mob/living/M = mover
+ if(M.flying || (IS_HORIZONTAL(M) && HAS_TRAIT(M, TRAIT_CONTORTED_BODY)))
return TRUE
if(istype(mover) && mover.checkpass(PASSTABLE))
return TRUE
@@ -868,8 +868,12 @@
return 1
if(!density) //Because broken racks -Agouri |TODO: SPRITE!|
return 1
- if(istype(mover) && mover.checkpass(PASSTABLE))
- return 1
+ if(istype(mover))
+ if(mover.checkpass(PASSTABLE))
+ return TRUE
+ var/mob/living/living_mover = mover
+ if(istype(living_mover) && IS_HORIZONTAL(living_mover) && HAS_TRAIT(living_mover, TRAIT_CONTORTED_BODY))
+ return TRUE
if(mover.throwing)
return 1
else
diff --git a/code/modules/antagonists/changeling/powers/contort_body.dm b/code/modules/antagonists/changeling/powers/contort_body.dm
new file mode 100644
index 00000000000..c79146f93ec
--- /dev/null
+++ b/code/modules/antagonists/changeling/powers/contort_body.dm
@@ -0,0 +1,26 @@
+/datum/action/changeling/contort_body
+ name = "Contort Body"
+ desc = "We contort our body, allowing us to fit in and under things we normally wouldn't be able to. Costs 5 chemicals."
+ button_icon_state = "contort_body"
+ chemical_cost = 5
+ dna_cost = 2
+ power_type = CHANGELING_PURCHASABLE_POWER
+
+/datum/action/changeling/contort_body/Remove(mob/M)
+ REMOVE_TRAIT(M, TRAIT_CONTORTED_BODY, CHANGELING_TRAIT)
+ ..()
+
+/datum/action/changeling/contort_body/sting_action(mob/living/user)
+ if(HAS_TRAIT_FROM(user, TRAIT_CONTORTED_BODY, CHANGELING_TRAIT))
+ REMOVE_TRAIT(user, TRAIT_CONTORTED_BODY, CHANGELING_TRAIT)
+ to_chat(user, "Our body stiffens and returns to form.")
+ if(IS_HORIZONTAL(user))
+ user.layer = initial(user.layer)
+ else
+ ADD_TRAIT(user, TRAIT_CONTORTED_BODY, CHANGELING_TRAIT)
+ to_chat(user, "We contort our form to allow us to fit in and under things we normally wouldn't be able to.")
+ if(IS_HORIZONTAL(user))
+ user.layer = TURF_LAYER + 0.2
+
+ SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]"))
+ return TRUE
diff --git a/code/modules/mob/living/living_status_procs.dm b/code/modules/mob/living/living_status_procs.dm
index 87d4f424998..6d5bd6726a4 100644
--- a/code/modules/mob/living/living_status_procs.dm
+++ b/code/modules/mob/living/living_status_procs.dm
@@ -111,7 +111,10 @@ STATUS EFFECTS
/mob/living/proc/on_lying_down(new_lying_angle)
if(layer == initial(layer)) //to avoid things like hiding larvas.
- layer = LYING_MOB_LAYER //so mob lying always appear behind standing mobs
+ if(HAS_TRAIT(src, TRAIT_CONTORTED_BODY))
+ layer = TURF_LAYER + 0.2
+ else
+ layer = LYING_MOB_LAYER //so mob lying always appear behind standing mobs
pixel_y = PIXEL_Y_OFFSET_LYING
ADD_TRAIT(src, TRAIT_UI_BLOCKED, LYING_DOWN_TRAIT)
ADD_TRAIT(src, TRAIT_CANNOT_PULL, LYING_DOWN_TRAIT)
@@ -120,7 +123,7 @@ STATUS EFFECTS
set_lying_angle(pick(90, 270))
/mob/living/proc/on_standing_up()
- if(layer == LYING_MOB_LAYER)
+ if(layer == LYING_MOB_LAYER || HAS_TRAIT(src, TRAIT_CONTORTED_BODY))
layer = initial(layer)
set_density(initial(density))
REMOVE_TRAITS_IN(src, LYING_DOWN_TRAIT)
diff --git a/icons/mob/actions/actions.dmi b/icons/mob/actions/actions.dmi
index e52e343bf03..205b92496f7 100644
Binary files a/icons/mob/actions/actions.dmi and b/icons/mob/actions/actions.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 9be879f59bf..898ce5fe0b0 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -1312,6 +1312,7 @@
#include "code\modules\antagonists\changeling\powers\become_headslug.dm"
#include "code\modules\antagonists\changeling\powers\biodegrade.dm"
#include "code\modules\antagonists\changeling\powers\chameleon_skin.dm"
+#include "code\modules\antagonists\changeling\powers\contort_body.dm"
#include "code\modules\antagonists\changeling\powers\digitalcamo.dm"
#include "code\modules\antagonists\changeling\powers\epinephrine.dm"
#include "code\modules\antagonists\changeling\powers\fakedeath.dm"