diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 122c0a041e7..cbd39a49a21 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -270,6 +270,9 @@
// Macro to get the current elapsed round time, rather than total world runtime
#define ROUND_TIME (round_start_time ? (world.time - round_start_time) : 0)
+// Macro that returns true if it's too early in a round to freely ghost out
+#define TOO_EARLY_TO_GHOST (config && (ROUND_TIME < (config.round_abandon_penalty_period)))
+
// Used by radios to indicate that they have sent a message via something other than subspace
#define RADIO_CONNECTION_FAIL 0
#define RADIO_CONNECTION_NON_SUBSPACE 1
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 0852622f2c6..5c11daabdba 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -435,14 +435,14 @@
/proc/SecondsToTicks(var/seconds)
return seconds * 10
-proc/pollCandidates(var/Question, var/be_special_type, var/antag_age_check = 0, var/poll_time = 300)
+proc/pollCandidates(var/Question, var/be_special_type, var/antag_age_check = 0, var/poll_time = 300, var/ignore_respawnability = 0)
var/roletext = be_special_type ? get_roletext(be_special_type) : null
var/list/mob/dead/observer/candidates = list()
var/time_passed = world.time
if(!Question)
Question = "Would you like to be a special role?"
- for(var/mob/dead/observer/G in player_list)
+ for(var/mob/dead/observer/G in (ignore_respawnability ? player_list : respawnable_list))
if(!G.key || !G.client)
continue
if(be_special_type)
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index b6dc1c58b8f..644740c9e20 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -22,6 +22,7 @@ var/global/list/spirits = list() //List of all the spirits, including Masks
var/global/list/living_mob_list = list() //List of all alive mobs, including clientless. Excludes /mob/new_player
var/global/list/dead_mob_list = list() //List of all dead mobs, including clientless. Excludes /mob/new_player
var/global/list/respawnable_list = list() //List of all mobs, dead or in mindless creatures that still be respawned.
+var/global/list/non_respawnable_keys = list() //List of ckeys that are excluded from respawning for remainder of round.
var/global/list/simple_animal_list = list() //List of all simple animals, including clientless
var/global/list/snpc_list = list() //List of all snpc's, including clientless
diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm
index 5cedd40abfc..09596d4445c 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -61,6 +61,7 @@
var/ToRban = 0
var/automute_on = 0 //enables automuting/spam prevention
var/jobs_have_minimal_access = 0 //determines whether jobs use minimal access or expanded access.
+ var/round_abandon_penalty_period = 30 MINUTES // Time from round start during which ghosting out is penalized
var/reactionary_explosions = 0 //If we use reactionary explosions, explosions that react to walls and doors
@@ -560,6 +561,9 @@
if("max_loadout_points")
config.max_loadout_points = text2num(value)
+ if("round_abandon_penalty_period")
+ config.round_abandon_penalty_period = MinutesToTicks(text2num(value))
+
else
diary << "Unknown setting in configuration: '[name]'"
diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm
index 3732c68d623..727f4d77d8c 100644
--- a/code/game/machinery/cryopod.dm
+++ b/code/game/machinery/cryopod.dm
@@ -174,7 +174,9 @@
var/mob/living/occupant = null // Person waiting to be despawned.
var/orient_right = null // Flips the sprite.
- var/time_till_despawn = 9000 // 15 minutes-ish safe period before being despawned.
+ // 15 minutes-ish safe period before being despawned.
+ var/time_till_despawn = 9000 // This is reduced by 90% if a player manually enters cryo
+ var/willing_time_divisor = 10
var/time_entered = 0 // Used to keep track of the safe period.
var/obj/item/device/radio/intercom/announce //
@@ -382,7 +384,12 @@
announce.autosay("[occupant.real_name] [on_store_message]", "[on_store_name]")
visible_message("\The [src] hums and hisses as it moves [occupant.real_name] into storage.")
- // Delete the mob.
+ // Ghost and delete the mob.
+ if(!occupant.get_ghost(1))
+ if(TOO_EARLY_TO_GHOST)
+ occupant.ghostize(0) // Players despawned too early may not re-enter the game
+ else
+ occupant.ghostize(1)
qdel(occupant)
occupant = null
name = initial(name)
@@ -404,6 +411,7 @@
var/willing = null //We don't want to allow people to be forced into despawning.
var/mob/living/M = G:affecting
+ time_till_despawn = initial(time_till_despawn)
if(!istype(M) || M.stat == DEAD)
to_chat(user, "Dead people can not be put into cryo.")
@@ -412,7 +420,7 @@
if(M.client)
if(alert(M,"Would you like to enter long-term storage?",,"Yes","No") == "Yes")
if(!M || !G || !G:affecting) return
- willing = 1
+ willing = willing_time_divisor
else
willing = 1
@@ -433,6 +441,8 @@
M.client.perspective = EYE_PERSPECTIVE
M.client.eye = src
+ time_till_despawn = initial(time_till_despawn) / willing
+
else //because why the fuck would you keep going if the mob isn't in the pod
to_chat(user, "You stop putting [M] into the cryopod.")
return
@@ -502,11 +512,12 @@
var/willing = null //We don't want to allow people to be forced into despawning.
+ time_till_despawn = initial(time_till_despawn)
if(L.client)
if(alert(L,"Would you like to enter cryosleep?",,"Yes","No") == "Yes")
if(!L) return
- willing = 1
+ willing = willing_time_divisor
else
willing = 1
@@ -523,6 +534,7 @@
to_chat(user, "\The [src] is in use.")
return
L.loc = src
+ time_till_despawn = initial(time_till_despawn) / willing
if(L.client)
L.client.perspective = EYE_PERSPECTIVE
@@ -622,6 +634,7 @@
usr.client.eye = src
usr.loc = src
src.occupant = usr
+ time_till_despawn = initial(time_till_despawn) / willing_time_divisor
if(orient_right)
icon_state = "[occupied_icon_state]-r"
diff --git a/code/game/response_team.dm b/code/game/response_team.dm
index 5f8543ca934..abda62f9420 100644
--- a/code/game/response_team.dm
+++ b/code/game/response_team.dm
@@ -114,13 +114,17 @@ var/ert_request_answered = 0
active_team = response_team_type
send_emergency_team = 1
- var/list/ert_candidates = pollCandidates("Join the Emergency Response Team?",, responseteam_age, 600)
+ var/list/ert_candidates = pollCandidates("Join the Emergency Response Team?",, responseteam_age, 600, 1)
if(!ert_candidates.len)
active_team.cannot_send_team()
send_emergency_team = 0
return
var/teamsize = 0
- for(var/mob/dead/observer/M in ert_candidates)
+ // Respawnable players get first dibs
+ for(var/mob/dead/observer/M in (ert_candidates & respawnable_list))
+ teamsize += M.JoinResponseTeam()
+ // If there's still open slots, non-respawnable players can fill them
+ for(var/mob/dead/observer/M in (ert_candidates - respawnable_list))
teamsize += M.JoinResponseTeam()
send_emergency_team = 0
if (!teamsize)
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 787bbcb5d9f..252636abcda 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1252,6 +1252,7 @@
message_admins("[key_name_admin(usr)] has sent [key_name_admin(M)] back to the Lobby.")
var/mob/new_player/NP = new()
+ non_respawnable_keys -= M.ckey
NP.ckey = M.ckey
qdel(M)
diff --git a/code/modules/mob/dead/observer/login.dm b/code/modules/mob/dead/observer/login.dm
index 7e3d3f6c4bf..eab9faf9a7a 100644
--- a/code/modules/mob/dead/observer/login.dm
+++ b/code/modules/mob/dead/observer/login.dm
@@ -3,5 +3,9 @@
if(ghostimage)
ghostimage.icon_state = src.icon_state
updateghostimages()
+
+ if(non_respawnable_keys[ckey])
+ can_reenter_corpse = 0
+ respawnable_list -= src
update_interface()
\ No newline at end of file
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index b15761d8c57..59c26c640bc 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -129,11 +129,15 @@ Works together with spawning an observer, noted above.
/mob/proc/ghostize(var/flags = GHOST_CAN_REENTER)
if(key)
+ if(non_respawnable_keys[ckey])
+ flags &= ~GHOST_CAN_REENTER
var/mob/dead/observer/ghost = new(src, flags) //Transfer safety to observer spawning proc.
ghost.timeofdeath = src.timeofdeath //BS12 EDIT
respawnable_list -= src
if(ghost.can_reenter_corpse)
respawnable_list += ghost
+ else
+ non_respawnable_keys[ckey] = 1
ghost.key = key
if(!(ghost.client && ghost.client.holder) && !config.antag_hud_allowed) // For new ghosts we remove the verb from even showing up if it's not allowed.
ghost.verbs -= /mob/dead/observer/verb/toggle_antagHUD // Poor guys, don't know what they are missing!
@@ -148,12 +152,28 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
set desc = "Relinquish your life and enter the land of the dead."
var/mob/M = src
+ var/warningmsg = null
+ var/obj/machinery/cryopod/P = istype(loc, /obj/machinery/cryopod) && loc
- if(stat == DEAD)
+ if(P)
+ if(TOO_EARLY_TO_GHOST)
+ warningmsg = "It's too early in the shift to enter cryo"
+ // If it's not too early, we'll skip straight to ghosting out without penalty
+ else if(suiciding && TOO_EARLY_TO_GHOST)
+ warningmsg = "You have committed suicide too early in the round"
+ else if(stat != DEAD)
+ warningmsg = "You are alive"
+ else if(non_respawnable_keys[ckey])
+ warningmsg = "You have lost your right to respawn"
+
+ if(!warningmsg)
ghostize(1)
else
- var/response = alert(src, "Are you -sure- you want to ghost?\n(You are alive. If you ghost now, you probably won't be able to rejoin the round! You can't change your mind, so choose wisely!)","Are you sure you want to ghost?","Ghost","Stay in body")
- if(response != "Ghost") return //didn't want to ghost after-all
+ var/response
+ var/alertmsg = "Are you -sure- you want to ghost?\n([warningmsg]. If you ghost now, you probably won't be able to rejoin the round! You can't change your mind, so choose wisely!)"
+ response = alert(src, alertmsg,"Are you sure you want to ghost?","Ghost","Stay in body")
+ if(response != "Ghost")
+ return //didn't want to ghost after-all
resting = 1
var/mob/dead/observer/ghost = ghostize(0) //0 parameter is so we can never re-enter our body, "Charlie, you can never come baaaack~" :3
ghost.timeofdeath = world.time // Because the living mob won't have a time of death and we want the respawn timer to work properly.
@@ -162,8 +182,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
Morgue = M.loc
if(Morgue)
Morgue.update()
- if(istype(M.loc, /obj/machinery/cryopod))
- var/obj/machinery/cryopod/P = M.loc
+ if(P)
if(!P.control_computer)
P.find_control_computer(urgent=1)
if(P.control_computer)
diff --git a/code/modules/mob/living/carbon/brain/posibrain.dm b/code/modules/mob/living/carbon/brain/posibrain.dm
index aad88276473..ea9dffd3ca0 100644
--- a/code/modules/mob/living/carbon/brain/posibrain.dm
+++ b/code/modules/mob/living/carbon/brain/posibrain.dm
@@ -13,6 +13,7 @@
req_access = list(access_robotics)
mecha = null//This does not appear to be used outside of reference in mecha.dm.
var/silenced = 0 //if set to 1, they can't talk.
+ var/next_ping_at = 0
/obj/item/device/mmi/posibrain/attack_self(mob/user as mob)
@@ -53,6 +54,8 @@
return 0
if(jobban_isbanned(O, "Cyborg") || jobban_isbanned(O,"nonhumandept"))
return 0
+ if(!O.can_reenter_corpse)
+ return 0
if(O.client)
return 1
return 0
@@ -190,7 +193,11 @@
/obj/item/device/mmi/posibrain/attack_ghost(var/mob/dead/observer/O)
if(searching)
volunteer(O)
- else
+ return
+ if(brainmob && brainmob.key)
+ return // No point pinging a posibrain with a player already inside
+ if(check_observer(O) && (world.time >= next_ping_at))
+ next_ping_at = world.time + (20 SECONDS)
var/turf/T = get_turf_or_move(src.loc)
for(var/mob/M in viewers(T))
M.show_message("\blue The positronic brain pings softly.")
diff --git a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
index 3a316b0140b..aeed17ef9fd 100644
--- a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
+++ b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
@@ -113,7 +113,7 @@
if(istype(src,/mob/dead/observer))
var/mob/dead/observer/G = src
if(G.has_enabled_antagHUD == 1 && config.antag_hud_restricted)
- to_chat(usr, "\blue Upon using the antagHUD you forfeited the ability to join the round.")
+ to_chat(usr, "Upon using the antagHUD you forfeited the ability to join the round.")
return
if(G.started_as_observer == 1)
joinedasobserver = 1
@@ -133,6 +133,9 @@
to_chat(usr, "You must wait 10 minutes to respawn as a drone!")
return
+ if(alert("Are you sure you want to respawn as a drone?", "Are you sure?", "Yes", "No") != "Yes")
+ return
+
for(var/obj/machinery/drone_fabricator/DF in world)
if(DF.stat & NOPOWER || !DF.produce_drones)
continue
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index 9c8d1cab95d..19065faad8c 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -403,6 +403,8 @@
return 0
if(O.mind && O.mind.current && O.mind.current.stat != DEAD)
return 0
+ if(!O.can_reenter_corpse)
+ return 0
if(O.has_enabled_antagHUD == 1 && config.antag_hud_restricted)
return 0
return 1
diff --git a/config/example/config.txt b/config/example/config.txt
index 2f723c704f4..eb44c6d828a 100644
--- a/config/example/config.txt
+++ b/config/example/config.txt
@@ -324,3 +324,8 @@ DISABLE_SPACE_RUINS
## How many loadout points players may spend in character setup
#MAX_LOADOUT_POINTS 5
+
+# How many minutes players must wait, from round start, before they can ghost out
+# and still qualify for re-entering the round. Defaults to 30.
+# Setting this to 0 will disable the penalty period
+#ROUND_ABANDON_PENALTY_PERIOD 30