diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm
index 8eceebe02b7..89a210acf37 100644
--- a/code/__DEFINES/admin.dm
+++ b/code/__DEFINES/admin.dm
@@ -74,6 +74,7 @@
#define ADMIN_PUNISHMENT_SUPPLYPOD_QUICK "Supply Pod (Quick)"
#define ADMIN_PUNISHMENT_SUPPLYPOD "Supply Pod"
#define ADMIN_PUNISHMENT_MAZING "Puzzle"
+#define ADMIN_PUNISHMENT_IMMERSE "Fully Immerse"
#define AHELP_ACTIVE 1
#define AHELP_CLOSED 2
diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm
index 75022a9987b..48a94c388b6 100644
--- a/code/__DEFINES/components.dm
+++ b/code/__DEFINES/components.dm
@@ -220,6 +220,8 @@
#define SPEECH_FORCED 7 */
#define COMSIG_MOB_DEADSAY "mob_deadsay" // from /mob/say_dead(): (mob/speaker, message)
#define MOB_DEADSAY_SIGNAL_INTERCEPT 1
+#define COMSIG_MOB_EMOTE "mob_emote" // from /mob/living/emote(): ()
+
// /mob/living signals
#define COMSIG_LIVING_RESIST "living_resist" //from base of mob/living/resist() (/mob/living)
#define COMSIG_LIVING_IGNITED "living_ignite" //from base of mob/living/IgniteMob() (/mob/living)
@@ -244,6 +246,8 @@
// /mob/living/carbon signals
#define COMSIG_CARBON_SOUNDBANG "carbon_soundbang" //from base of mob/living/carbon/soundbang_act(): (list(intensity))
+#define COMSIG_CARBON_GAIN_ORGAN "carbon_gain_organ" //from /item/organ/proc/Insert() (/obj/item/organ/)
+#define COMSIG_CARBON_LOSE_ORGAN "carbon_lose_organ" //from /item/organ/proc/Remove() (/obj/item/organ/)
// /mob/living/simple_animal/hostile signals
#define COMSIG_HOSTILE_ATTACKINGTARGET "hostile_attackingtarget"
diff --git a/code/datums/components/manual_blinking.dm b/code/datums/components/manual_blinking.dm
new file mode 100644
index 00000000000..2459ec438f1
--- /dev/null
+++ b/code/datums/components/manual_blinking.dm
@@ -0,0 +1,83 @@
+/datum/component/manual_blinking
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ var/obj/item/organ/eyes/E
+ var/warn_grace = FALSE
+ var/warn_dying = FALSE
+ var/last_blink
+ var/check_every = 20 SECONDS
+ var/grace_period = 6 SECONDS
+ var/damage_rate = 1 // organ damage taken per tick
+ var/list/valid_emotes = list(/datum/emote/living/carbon/blink, /datum/emote/living/carbon/blink_r)
+
+/datum/component/manual_blinking/Initialize()
+ if(!iscarbon(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ var/mob/living/carbon/C = parent
+ E = C.getorganslot(ORGAN_SLOT_EYES)
+
+ if(E)
+ START_PROCESSING(SSdcs, src)
+ last_blink = world.time
+ to_chat(C, "You suddenly realize you're blinking manually.")
+
+/datum/component/manual_blinking/Destroy(force, silent)
+ E = null
+ STOP_PROCESSING(SSdcs, src)
+ to_chat(parent, "You revert back to automatic blinking.")
+ return ..()
+
+/datum/component/manual_blinking/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOB_EMOTE, .proc/check_emote)
+ RegisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN, .proc/check_added_organ)
+ RegisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN, .proc/check_removed_organ)
+ RegisterSignal(parent, COMSIG_LIVING_REVIVE, .proc/restart)
+ RegisterSignal(parent, COMSIG_MOB_DEATH, .proc/pause)
+
+/datum/component/manual_blinking/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_MOB_EMOTE)
+ UnregisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN)
+ UnregisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN)
+ UnregisterSignal(parent, COMSIG_LIVING_REVIVE)
+ UnregisterSignal(parent, COMSIG_MOB_DEATH)
+
+/datum/component/manual_blinking/proc/restart()
+ START_PROCESSING(SSdcs, src)
+
+/datum/component/manual_blinking/proc/pause()
+ STOP_PROCESSING(SSdcs, src)
+
+/datum/component/manual_blinking/process()
+ var/mob/living/carbon/C = parent
+
+ if(world.time > (last_blink + check_every + grace_period))
+ if(!warn_dying)
+ to_chat(C, "Your eyes begin to wither, you need to blink!")
+ warn_dying = TRUE
+
+ E.applyOrganDamage(damage_rate)
+ else if(world.time > (last_blink + check_every))
+ if(!warn_grace)
+ to_chat(C, "You feel a need to blink!")
+ warn_grace = TRUE
+
+/datum/component/manual_blinking/proc/check_added_organ(mob/who_cares, obj/item/organ/O)
+ var/obj/item/organ/eyes/new_eyes = O
+
+ if(istype(new_eyes,/obj/item/organ/eyes))
+ E = new_eyes
+ START_PROCESSING(SSdcs, src)
+
+/datum/component/manual_blinking/proc/check_removed_organ(mob/who_cares, obj/item/organ/O)
+ var/obj/item/organ/eyes/bye_beyes = O // oh come on, that's pretty good
+
+ if(istype(bye_beyes, /obj/item/organ/eyes))
+ E = null
+ STOP_PROCESSING(SSdcs, src)
+
+/datum/component/manual_blinking/proc/check_emote(mob/living/carbon/user, datum/emote/emote)
+ if(emote.type in valid_emotes)
+ warn_grace = FALSE
+ warn_dying = FALSE
+ last_blink = world.time
diff --git a/code/datums/components/manual_breathing.dm b/code/datums/components/manual_breathing.dm
new file mode 100644
index 00000000000..da1c6cffc47
--- /dev/null
+++ b/code/datums/components/manual_breathing.dm
@@ -0,0 +1,93 @@
+/datum/component/manual_breathing
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ var/obj/item/organ/lungs/L
+ var/warn_grace = FALSE
+ var/warn_dying = FALSE
+ var/last_breath
+ var/check_every = 12 SECONDS
+ var/grace_period = 6 SECONDS
+ var/damage_rate = 1 // organ damage taken per tick
+ var/datum/emote/next_breath_type = /datum/emote/inhale
+
+/datum/component/manual_breathing/Initialize()
+ if(!iscarbon(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ var/mob/living/carbon/C = parent
+ L = C.getorganslot(ORGAN_SLOT_LUNGS)
+
+ if(L)
+ START_PROCESSING(SSdcs, src)
+ last_breath = world.time
+ to_chat(C, "You suddenly realize you're breathing manually.")
+
+/datum/component/manual_breathing/Destroy(force, silent)
+ L = null
+ STOP_PROCESSING(SSdcs, src)
+ to_chat(parent, "You revert back to automatic breathing.")
+ return ..()
+
+/datum/component/manual_breathing/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOB_EMOTE, .proc/check_emote)
+ RegisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN, .proc/check_added_organ)
+ RegisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN, .proc/check_removed_organ)
+ RegisterSignal(parent, COMSIG_LIVING_REVIVE, .proc/restart)
+ RegisterSignal(parent, COMSIG_MOB_DEATH, .proc/pause)
+
+/datum/component/manual_breathing/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_MOB_EMOTE)
+ UnregisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN)
+ UnregisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN)
+ UnregisterSignal(parent, COMSIG_LIVING_REVIVE)
+ UnregisterSignal(parent, COMSIG_MOB_DEATH)
+
+/datum/component/manual_breathing/proc/restart()
+ START_PROCESSING(SSdcs, src)
+
+/datum/component/manual_breathing/proc/pause()
+ STOP_PROCESSING(SSdcs, src)
+
+/datum/component/manual_breathing/process()
+ var/mob/living/carbon/C = parent
+
+ var/next_text = initial(next_breath_type.key)
+ if(world.time > (last_breath + check_every + grace_period))
+ if(!warn_dying)
+ to_chat(C, "You begin to suffocate, you need to [next_text]!")
+ warn_dying = TRUE
+
+ L.applyOrganDamage(damage_rate)
+ C.losebreath += 0.8
+ else if(world.time > (last_breath + check_every))
+ if(!warn_grace)
+ to_chat(C, "You feel a need to [next_text]!")
+ warn_grace = TRUE
+
+/datum/component/manual_breathing/proc/check_added_organ(mob/who_cares, obj/item/organ/O)
+ var/obj/item/organ/eyes/new_lungs = O
+
+ if(istype(new_lungs,/obj/item/organ/lungs))
+ L = new_lungs
+ START_PROCESSING(SSdcs, src)
+
+/datum/component/manual_breathing/proc/check_removed_organ(mob/who_cares, obj/item/organ/O)
+ var/obj/item/organ/lungs/old_lungs = O
+
+ if(istype(old_lungs, /obj/item/organ/lungs))
+ L = null
+ STOP_PROCESSING(SSdcs, src)
+
+/datum/component/manual_breathing/proc/check_emote(mob/living/carbon/user, datum/emote/emote)
+ if(emote.type == next_breath_type)
+ if(next_breath_type == /datum/emote/inhale)
+ next_breath_type = /datum/emote/exhale
+ else
+ next_breath_type = /datum/emote/inhale
+
+ warn_grace = FALSE
+ warn_dying = FALSE
+ last_breath = world.time
+
+ var/mob/living/carbon/C = parent
+ C.losebreath = max(0, C.losebreath - 0.4)
diff --git a/code/modules/admin/secrets.dm b/code/modules/admin/secrets.dm
index 825b0844f22..57e866c8249 100644
--- a/code/modules/admin/secrets.dm
+++ b/code/modules/admin/secrets.dm
@@ -69,6 +69,8 @@
Change bomb cap
Mass Purrbation
Mass Remove Purrbation
+ Fully Immerse Everyone
+ Un-Fully Immerse Everyone
"}
dat += "
"
@@ -595,6 +597,22 @@
purrbation.")
log_admin("[key_name(usr)] has removed everyone from purrbation.")
+ if("massimmerse")
+ if(!check_rights(R_FUN))
+ return
+ mass_immerse()
+ message_admins("[key_name_admin(usr)] has Fully Immersed \
+ everyone!")
+ log_admin("[key_name(usr)] has Fully Immersed everyone.")
+
+ if("unmassimmerse")
+ if(!check_rights(R_FUN))
+ return
+ mass_immerse(remove=TRUE)
+ message_admins("[key_name_admin(usr)] has Un-Fully Immersed \
+ everyone!")
+ log_admin("[key_name(usr)] has Un-Fully Immersed everyone.")
+
if("customportal")
if(!check_rights(R_FUN))
return
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index b1830ba3873..dc5e9ef4839 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -1020,6 +1020,20 @@ Traitors and the like can also be revived with the previous role mostly intact.
dat += "
Add New Goal"
usr << browse(dat, "window=goals;size=400x400")
+/proc/immerse_player(mob/living/carbon/target, toggle=TRUE, remove=FALSE)
+ var/list/immersion_components = list(/datum/component/manual_breathing, /datum/component/manual_blinking)
+
+ for(var/immersies in immersion_components)
+ var/has_component = target.GetComponent(immersies)
+
+ if(has_component && (toggle || remove))
+ qdel(has_component)
+ else if(toggle || !remove)
+ target.AddComponent(immersies)
+
+/proc/mass_immerse(remove=FALSE)
+ for(var/mob/living/carbon/M in GLOB.mob_list)
+ immerse_player(M, toggle=FALSE, remove=remove)
/client/proc/toggle_hub()
set category = "Server"
@@ -1040,7 +1054,7 @@ Traitors and the like can also be revived with the previous role mostly intact.
if(!check_rights(R_ADMIN) || !check_rights(R_FUN))
return
- var/list/punishment_list = list(ADMIN_PUNISHMENT_LIGHTNING, ADMIN_PUNISHMENT_BRAINDAMAGE, ADMIN_PUNISHMENT_GIB, ADMIN_PUNISHMENT_BSA, ADMIN_PUNISHMENT_FIREBALL, ADMIN_PUNISHMENT_ROD, ADMIN_PUNISHMENT_SUPPLYPOD_QUICK, ADMIN_PUNISHMENT_SUPPLYPOD, ADMIN_PUNISHMENT_MAZING)
+ var/list/punishment_list = list(ADMIN_PUNISHMENT_LIGHTNING, ADMIN_PUNISHMENT_BRAINDAMAGE, ADMIN_PUNISHMENT_GIB, ADMIN_PUNISHMENT_BSA, ADMIN_PUNISHMENT_FIREBALL, ADMIN_PUNISHMENT_ROD, ADMIN_PUNISHMENT_SUPPLYPOD_QUICK, ADMIN_PUNISHMENT_SUPPLYPOD, ADMIN_PUNISHMENT_MAZING, ADMIN_PUNISHMENT_IMMERSE)
var/punishment = input("Choose a punishment", "DIVINE SMITING") as null|anything in sortList(punishment_list)
@@ -1100,11 +1114,12 @@ Traitors and the like can also be revived with the previous role mostly intact.
plaunch.temp_pod.effectStun = TRUE
plaunch.ui_interact(usr)
return //We return here because punish_log() is handled by the centcom_podlauncher datum
-
if(ADMIN_PUNISHMENT_MAZING)
if(!puzzle_imprison(target))
to_chat(usr,"Imprisonment failed!")
return
+ if(ADMIN_PUNISHMENT_IMMERSE)
+ immerse_player(target)
punish_log(target, punishment)
diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm
index b7dc2425107..7577bcca357 100644
--- a/code/modules/mob/emote.dm
+++ b/code/modules/mob/emote.dm
@@ -19,6 +19,7 @@
silenced = TRUE
continue
if(P.run_emote(src, param, m_type, intentional))
+ SEND_SIGNAL(src, COMSIG_MOB_EMOTE, P, act, m_type, message, intentional)
return TRUE
if(intentional && !silenced)
to_chat(src, "Unusable emote '[act]'. Say *help for a list.")
diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm
index 158267a60c1..784dcf86bff 100644
--- a/code/modules/mob/living/emote.dm
+++ b/code/modules/mob/living/emote.dm
@@ -520,3 +520,13 @@
to_chat(user, "You ready your slapping hand.")
else
to_chat(user, "You're incapable of slapping in your current state.")
+
+/datum/emote/inhale
+ key = "inhale"
+ key_third_person = "inhales"
+ message = "breathes in."
+
+/datum/emote/exhale
+ key = "exhale"
+ key_third_person = "exhales"
+ message = "breathes out."
diff --git a/code/modules/surgery/organs/organ_internal.dm b/code/modules/surgery/organs/organ_internal.dm
index 271c7895163..89f658e6168 100644
--- a/code/modules/surgery/organs/organ_internal.dm
+++ b/code/modules/surgery/organs/organ_internal.dm
@@ -38,6 +38,8 @@
else
qdel(replaced)
+ SEND_SIGNAL(M, COMSIG_CARBON_GAIN_ORGAN, src)
+
owner = M
M.internal_organs |= src
M.internal_organs_slot[slot] = src
@@ -59,6 +61,9 @@
for(var/X in actions)
var/datum/action/A = X
A.Remove(M)
+
+ SEND_SIGNAL(M, COMSIG_CARBON_LOSE_ORGAN, src)
+
START_PROCESSING(SSobj, src)
diff --git a/tgstation.dme b/tgstation.dme
index 503a81f55a1..06580ae52ba 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -394,6 +394,8 @@
#include "code\datums\components\lifesteal.dm"
#include "code\datums\components\lockon_aiming.dm"
#include "code\datums\components\magnetic_catch.dm"
+#include "code\datums\components\manual_blinking.dm"
+#include "code\datums\components\manual_breathing.dm"
#include "code\datums\components\material_container.dm"
#include "code\datums\components\mirage_border.dm"
#include "code\datums\components\mood.dm"