diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm
index 545da8549b3..f3f5f7f1f9d 100644
--- a/code/__DEFINES/jobs.dm
+++ b/code/__DEFINES/jobs.dm
@@ -7,6 +7,9 @@
/// Job unavailable due to incompatibility with an antag role.
#define JOB_UNAVAILABLE_ANTAG_INCOMPAT 6
+/// Used when the `get_job_unavailable_error_message` proc can't make sense of a given code.
+#define GENERIC_JOB_UNAVAILABLE_ERROR "Error: Unknown job availability."
+
#define DEFAULT_RELIGION "Christianity"
#define DEFAULT_DEITY "Space Jesus"
#define DEFAULT_BIBLE "Default Bible Name"
@@ -137,7 +140,7 @@
#define JOB_DISPLAY_ORDER_PRISONER 35
-#define DEPARTMENT_UNASSIGNED "No department assigned"
+#define DEPARTMENT_UNASSIGNED "No Department"
#define DEPARTMENT_BITFLAG_SECURITY (1<<0)
#define DEPARTMENT_SECURITY "Security"
diff --git a/code/_onclick/hud/new_player.dm b/code/_onclick/hud/new_player.dm
index eda79ae073e..e7cfb0942bd 100644
--- a/code/_onclick/hud/new_player.dm
+++ b/code/_onclick/hud/new_player.dm
@@ -181,18 +181,19 @@
. = ..()
if(!.)
return
+
if(!SSticker?.IsRoundInProgress())
to_chat(hud.mymob, span_boldwarning("The round is either not ready, or has already finished..."))
return
//Determines Relevent Population Cap
var/relevant_cap
- var/hpc = CONFIG_GET(number/hard_popcap)
- var/epc = CONFIG_GET(number/extreme_popcap)
- if(hpc && epc)
- relevant_cap = min(hpc, epc)
+ var/hard_popcap = CONFIG_GET(number/hard_popcap)
+ var/extreme_popcap = CONFIG_GET(number/extreme_popcap)
+ if(hard_popcap && extreme_popcap)
+ relevant_cap = min(hard_popcap, extreme_popcap)
else
- relevant_cap = max(hpc, epc)
+ relevant_cap = max(hard_popcap, extreme_popcap)
var/mob/dead/new_player/new_player = hud.mymob
@@ -208,7 +209,13 @@
SSticker.queued_players += new_player
to_chat(new_player, span_notice("You have been added to the queue to join the game. Your position in queue is [SSticker.queued_players.len]."))
return
- new_player.LateChoices()
+
+ if(!LAZYACCESS(params2list(params), CTRL_CLICK))
+ GLOB.latejoin_menu.ui_interact(new_player)
+ else
+ to_chat(new_player, span_warning("Opening emergency fallback late join menu! If THIS doesn't show, ahelp immediately!"))
+ GLOB.latejoin_menu.fallback_ui(new_player)
+
/atom/movable/screen/lobby/button/join/proc/show_join_button()
SIGNAL_HANDLER
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index a44c7f89bdc..8065d2e6aff 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -475,13 +475,13 @@ SUBSYSTEM_DEF(ticker)
/datum/controller/subsystem/ticker/proc/check_queue()
if(!queued_players.len)
return
- var/hpc = CONFIG_GET(number/hard_popcap)
- if(!hpc)
+ var/hard_popcap = CONFIG_GET(number/hard_popcap)
+ if(!hard_popcap)
list_clear_nulls(queued_players)
- for (var/mob/dead/new_player/NP in queued_players)
- to_chat(NP, span_userdanger("The alive players limit has been released!
[html_encode(">>Join Game<<")]"))
- SEND_SOUND(NP, sound('sound/misc/notice1.ogg'))
- NP.LateChoices()
+ for (var/mob/dead/new_player/new_player in queued_players)
+ to_chat(new_player, span_userdanger("The alive players limit has been released!
[html_encode(">>Join Game<<")]"))
+ SEND_SOUND(new_player, sound('sound/misc/notice1.ogg'))
+ GLOB.latejoin_menu.ui_interact(new_player)
queued_players.len = 0
queue_delay = 0
return
@@ -492,11 +492,11 @@ SUBSYSTEM_DEF(ticker)
switch(queue_delay)
if(5) //every 5 ticks check if there is a slot available
list_clear_nulls(queued_players)
- if(living_player_count() < hpc)
+ if(living_player_count() < hard_popcap)
if(next_in_line?.client)
to_chat(next_in_line, span_userdanger("A slot has opened! You have approximately 20 seconds to join. \>\>Join Game\<\<"))
SEND_SOUND(next_in_line, sound('sound/misc/notice1.ogg'))
- next_in_line.LateChoices()
+ next_in_line.ui_interact(next_in_line)
return
queued_players -= next_in_line //Client disconnected, remove he
queue_delay = 0 //No vacancy: restart timer
diff --git a/code/game/machinery/newscaster/newscaster_machine.dm b/code/game/machinery/newscaster/newscaster_machine.dm
index c4bb2e95a57..91fcbb23368 100644
--- a/code/game/machinery/newscaster/newscaster_machine.dm
+++ b/code/game/machinery/newscaster/newscaster_machine.dm
@@ -140,7 +140,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster, 30)
data["user"]["department"] = card.registered_account.account_job.paycheck_department
else
data["user"]["job"] = "No Job"
- data["user"]["department"] = "No Department"
+ data["user"]["department"] = DEPARTMENT_UNASSIGNED
else if(issilicon(user))
var/mob/living/silicon/silicon_user = user
data["user"] = list()
diff --git a/code/modules/antagonists/separatist/separatist.dm b/code/modules/antagonists/separatist/separatist.dm
index 81e4106bd19..45ce9bf0ad5 100644
--- a/code/modules/antagonists/separatist/separatist.dm
+++ b/code/modules/antagonists/separatist/separatist.dm
@@ -105,7 +105,7 @@
objectives -= nation.objectives
/datum/antagonist/separatist/proc/setup_ui_color()
- var/list/hsl = rgb2num(nation.department.latejoin_color, COLORSPACE_HSL)
+ var/list/hsl = rgb2num(nation.department.ui_color, COLORSPACE_HSL)
hsl[3] = 25 //setting lightness very low
ui_color = rgb(hsl[1], hsl[2], hsl[3], space = COLORSPACE_HSL)
diff --git a/code/modules/jobs/departments/departments.dm b/code/modules/jobs/departments/departments.dm
index 27a7174da73..db45899b5c4 100644
--- a/code/modules/jobs/departments/departments.dm
+++ b/code/modules/jobs/departments/departments.dm
@@ -1,7 +1,7 @@
/// Singleton representing a category of jobs forming a department.
/datum/job_department
/// Department as displayed on different menus.
- var/department_name = "No department assigned"
+ var/department_name = DEPARTMENT_UNASSIGNED
/// Bitflags associated to the specific department.
var/department_bitflags = NONE
/// Typepath of the job datum leading this department.
@@ -12,8 +12,8 @@
var/display_order = 0
/// The header color to be displayed in the ban panel, classes defined in banpanel.css
var/label_class = "undefineddepartment"
- /// The color used in the latejoin menu.
- var/latejoin_color = "#6681a5"
+ /// The color used in TGUI or similar menus.
+ var/ui_color = "#9689db"
/// Job singleton datums associated to this department. Populated on job initialization.
var/list/department_jobs = list()
/// For separatists, what independent name prefix does their nation get named?
@@ -54,7 +54,7 @@
department_experience_type = EXP_TYPE_COMMAND
display_order = 1
label_class = "command"
- latejoin_color = "#ccccff"
+ ui_color = "#6681a5"
/datum/job_department/security
@@ -64,7 +64,7 @@
department_experience_type = EXP_TYPE_SECURITY
display_order = 2
label_class = "security"
- latejoin_color = "#ffdddd"
+ ui_color = "#d46a78"
nation_prefixes = list("Securi", "Beepski", "Shitcuri", "Red", "Stunba", "Flashbango", "Flasha", "Stanfordi")
/datum/job_department/engineering
@@ -74,7 +74,7 @@
department_experience_type = EXP_TYPE_ENGINEERING
display_order = 3
label_class = "engineering"
- latejoin_color = "#ffeeaa"
+ ui_color = "#dfb567"
nation_prefixes = list("Atomo", "Engino", "Power", "Teleco")
@@ -85,7 +85,7 @@
department_experience_type = EXP_TYPE_MEDICAL
display_order = 4
label_class = "medical"
- latejoin_color = "#ffddf0"
+ ui_color = "#65b2bd"
nation_prefixes = list("Mede", "Healtha", "Recova", "Chemi", "Viro", "Psych")
@@ -96,7 +96,7 @@
department_experience_type = EXP_TYPE_SCIENCE
display_order = 5
label_class = "science"
- latejoin_color = "#ffddff"
+ ui_color = "#c973c9"
nation_prefixes = list("Sci", "Griffa", "Geneti", "Explosi", "Mecha", "Xeno", "Nani", "Cyto")
@@ -107,7 +107,7 @@
department_experience_type = EXP_TYPE_SUPPLY
display_order = 6
label_class = "supply"
- latejoin_color = "#ddddff"
+ ui_color = "#cf9c6c"
nation_prefixes = list("Cargo", "Guna", "Suppli", "Mule", "Crate", "Ore", "Mini", "Shaf")
@@ -118,7 +118,7 @@
department_experience_type = EXP_TYPE_SERVICE
display_order = 7
label_class = "service"
- latejoin_color = "#bbe291"
+ ui_color = "#7cc46a"
nation_prefixes = list("Honka", "Boozo", "Fatu", "Danka", "Mimi", "Libra", "Jani", "Religi")
@@ -129,7 +129,7 @@
department_experience_type = EXP_TYPE_SILICON
display_order = 8
label_class = "silicon"
- latejoin_color = "#ccffcc"
+ ui_color = "#5dbda0"
/datum/job_department/silicon/generate_nation_name()
return "United Nations" //For nations ruleset specifically, because all other sources of nation creation cannot choose silicons
diff --git a/code/modules/mob/dead/new_player/latejoin_menu.dm b/code/modules/mob/dead/new_player/latejoin_menu.dm
new file mode 100644
index 00000000000..6d50efb61bb
--- /dev/null
+++ b/code/modules/mob/dead/new_player/latejoin_menu.dm
@@ -0,0 +1,218 @@
+#define JOB_CHOICE_YES "Yes"
+#define JOB_CHOICE_REROLL "Reroll"
+#define JOB_CHOICE_CANCEL "Cancel"
+
+GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new)
+
+/// Makes a list of jobs and pushes them to a DM list selector. Just in case someone did a special kind of fucky-wucky with TGUI.
+/datum/latejoin_menu/proc/fallback_ui(mob/dead/new_player/user)
+ var/list/jobs = list()
+ for(var/datum/job/job as anything in SSjob.joinable_occupations)
+ jobs += job.title
+
+ var/input_contents = input(user, "Pick a job to join as:", "Latejoin Job Selection") as null|anything in jobs
+
+ if(!input_contents)
+ return
+
+ user.AttemptLateSpawn(input_contents)
+
+/datum/ui_close(mob/dead/new_player/user)
+ . = ..()
+ if(istype(user))
+ user.jobs_menu_mounted = TRUE // Don't flood a user's chat if they open and close the UI.
+
+/datum/latejoin_menu/ui_interact(mob/dead/new_player/user, datum/tgui/ui)
+ if(!istype(user))
+ return
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ // In case they reopen the GUI
+ user.jobs_menu_mounted = FALSE
+ addtimer(CALLBACK(src, PROC_REF(scream_at_player), user), 5 SECONDS)
+
+ ui = new(user, src, "JobSelection", "Latejoin Menu")
+ ui.open()
+
+/datum/latejoin_menu/proc/scream_at_player(mob/dead/new_player/player)
+ if(!player.jobs_menu_mounted)
+ to_chat(player, span_notice("If the late join menu isn't showing, hold CTRL while clicking the join button!"))
+
+/datum/latejoin_menu/ui_data(mob/user)
+ var/mob/dead/new_player/owner = user
+ var/list/departments = list()
+ var/list/data = list(
+ "disable_jobs_for_non_observers" = SSlag_switch.measures[DISABLE_NON_OBSJOBS],
+ "round_duration" = DisplayTimeText(world.time - SSticker.round_start_time),
+ "departments" = departments,
+ )
+ if(SSshuttle.emergency)
+ switch(SSshuttle.emergency.mode)
+ if(SHUTTLE_ESCAPE)
+ data["shuttle_status"] = "The station has been evacuated."
+ if(SHUTTLE_CALL)
+ if(!SSshuttle.canRecall())
+ data["shuttle_status"] = "The station is currently undergoing evacuation procedures."
+
+ for(var/datum/job/prioritized_job in SSjob.prioritized_jobs)
+ if(prioritized_job.current_positions >= prioritized_job.total_positions)
+ SSjob.prioritized_jobs -= prioritized_job
+
+ for(var/datum/job_department/department as anything in SSjob.joinable_departments)
+ var/list/department_jobs = list()
+ var/list/department_data = list(
+ "jobs" = department_jobs,
+ "open_slots" = 0,
+ )
+ departments[department.department_name] = department_data
+
+ for(var/datum/job/job_datum as anything in department.department_jobs)
+ var/job_availability = owner.IsJobUnavailable(job_datum.title, latejoin = TRUE)
+
+ var/list/job_data = list(
+ "prioritized" = (job_datum in SSjob.prioritized_jobs),
+ "used_slots" = job_datum.current_positions,
+ "open_slots" = job_datum.total_positions < 0 ? "∞" : job_datum.total_positions,
+ )
+
+ if(job_availability != JOB_AVAILABLE)
+ job_data["unavailable_reason"] = get_job_unavailable_error_message(job_availability, job_datum.title)
+
+ if(job_datum.total_positions < 0)
+ department_data["open_slots"] = "∞"
+
+ if(department_data["open_slots"] != "∞")
+ department_data["open_slots"] += job_datum.total_positions - job_datum.current_positions
+
+ department_jobs[job_datum.title] = job_data
+
+ return data
+
+/datum/latejoin_menu/ui_static_data(mob/user)
+ var/list/departments = list()
+
+ for(var/datum/job_department/department as anything in SSjob.joinable_departments)
+ var/list/department_jobs = list()
+ var/list/department_data = list(
+ "jobs" = department_jobs,
+ "color" = department.ui_color,
+ )
+ departments[department.department_name] = department_data
+
+ for(var/datum/job/job_datum as anything in department.department_jobs)
+ var/datum/outfit/outfit = job_datum.outfit
+ var/datum/id_trim/trim = initial(outfit.id_trim)
+
+ var/list/job_data = list(
+ "command" = !!(job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND),
+ "description" = job_datum.description,
+ "icon" = initial(trim.orbit_icon),
+ )
+
+ department_jobs[job_datum.title] = job_data
+
+ return list("departments_static" = departments)
+
+/datum/latejoin_menu/ui_status(mob/user)
+ return isnewplayer(user) ? UI_INTERACTIVE : UI_CLOSE
+
+/datum/latejoin_menu/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/latejoin_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+
+ if(!ui.user.client || ui.user.client.interviewee || !isnewplayer(ui.user))
+ return TRUE
+
+ var/mob/dead/new_player/owner = ui.user
+
+ switch(action)
+ if("ui_mounted_with_no_bluescreen")
+ owner.jobs_menu_mounted = TRUE
+ if("select_job")
+ if(params["job"] == "Random")
+ var/job = get_random_job(owner)
+ if(!job)
+ return TRUE
+
+ params["job"] = job
+
+ if(!SSticker?.IsRoundInProgress())
+ tgui_alert(owner, "The round is either not ready, or has already finished...", "Oh No!")
+ return TRUE
+
+ if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
+ tgui_alert(owner, "There is an administrative lock on entering the game for non-observers!", "Oh No!")
+ return TRUE
+
+ //Determines Relevent Population Cap
+ var/relevant_cap
+ var/hard_popcap = CONFIG_GET(number/hard_popcap)
+ var/extreme_popcap = CONFIG_GET(number/extreme_popcap)
+ if(hard_popcap && extreme_popcap)
+ relevant_cap = min(hard_popcap, extreme_popcap)
+ else
+ relevant_cap = max(hard_popcap, extreme_popcap)
+
+ if(SSticker.queued_players.len && !(ckey(owner.key) in GLOB.admin_datums))
+ if((living_player_count() >= relevant_cap) || (owner != SSticker.queued_players[1]))
+ tgui_alert(owner, "The server is full!", "Oh No!")
+ return TRUE
+
+ // SAFETY: AttemptLateSpawn has it's own sanity checks. This is perfectly safe.
+ owner.AttemptLateSpawn(params["job"])
+ return TRUE
+
+ if("viewpoll")
+ var/datum/poll_question/poll = locate(params["viewpoll"]) in GLOB.polls
+ if(!poll)
+ return TRUE
+
+ owner.poll_player(poll)
+ return TRUE
+
+ if("votepollref")
+ var/datum/poll_question/poll = locate(params["votepollref"]) in GLOB.polls
+ if(!poll)
+ return TRUE
+
+ owner.vote_on_poll_handler(poll, params)
+ return TRUE
+
+/// Gives the user a random job that they can join as, and prompts them if they'd actually like to keep it, rerolling if not. Cancellable by the user.
+/// WARNING: BLOCKS THREAD!
+/datum/latejoin_menu/proc/get_random_job(mob/dead/new_player/owner)
+ var/list/dept_data = list()
+
+ for(var/datum/job_department/department as anything in SSjob.joinable_departments)
+ for(var/datum/job/job_datum as anything in department.department_jobs)
+ if(owner.IsJobUnavailable(job_datum.title, latejoin = TRUE) != JOB_AVAILABLE)
+ continue
+ dept_data += job_datum.title
+
+ if(dept_data.len <= 0) //Congratufuckinglations
+ tgui_alert(owner, "There are literally no random jobs available for you on this server, ahelp for assistance.", "Oh No!")
+ return
+
+ var/random_job
+
+ while(random_job != JOB_CHOICE_YES)
+ if(dept_data.len <= 0)
+ tgui_alert(owner, "It seems that there are no more random jobs available for you!", "Oh No!")
+ return
+
+ var/random = pick_n_take(dept_data)
+ var/list/random_job_options = list(JOB_CHOICE_YES, JOB_CHOICE_REROLL, JOB_CHOICE_CANCEL)
+
+ random_job = tgui_alert(owner, "[random]?", "Random Job", random_job_options)
+
+ if(random_job == JOB_CHOICE_CANCEL)
+ return
+ if(random_job == JOB_CHOICE_YES)
+ return random
+
+#undef JOB_CHOICE_YES
+#undef JOB_CHOICE_REROLL
+#undef JOB_CHOICE_CANCEL
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index b5a9fc0b1ca..f09b60e39ad 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -13,7 +13,8 @@
var/mob/living/new_character
///Used to make sure someone doesn't get spammed with messages if they're ineligible for roles.
var/ineligible_for_roles = FALSE
-
+ /// Used to track if the player's jobs menu sent a message saying it successfully mounted.
+ var/jobs_menu_mounted = FALSE
/mob/dead/new_player/Initialize(mapload)
@@ -38,81 +39,6 @@
/mob/dead/new_player/prepare_huds()
return
-/mob/dead/new_player/Topic(href, href_list[])
- if(src != usr)
- return
-
- if(!client)
- return
-
- if(client.interviewee)
- return FALSE
-
- if(href_list["late_join"]) //This still exists for queue messages in chat
- if(!SSticker?.IsRoundInProgress())
- to_chat(usr, span_boldwarning("The round is either not ready, or has already finished..."))
- return
- LateChoices()
- return
-
- if(href_list["cancrand"])
- src << browse(null, "window=randjob") //closes the random job window
- LateChoices()
- return
-
- if(href_list["SelectedJob"])
- if(href_list["SelectedJob"] == "Random")
- var/list/dept_data = list()
- for(var/datum/job_department/department as anything in SSjob.joinable_departments)
- for(var/datum/job/job_datum as anything in department.department_jobs)
- if(IsJobUnavailable(job_datum.title, TRUE) != JOB_AVAILABLE)
- continue
- dept_data += job_datum.title
- if(dept_data.len <= 0) //Congratufuckinglations
- tgui_alert(src, "There are literally no random jobs available for you on this server, ahelp for assistance.")
- return
- var/random = pick(dept_data)
- var/randomjob = "
| "
- var/column_counter = 0
-
- for(var/datum/job_department/department as anything in SSjob.joinable_departments)
- var/department_color = department.latejoin_color
- dat += " " - column_counter++ - if(column_counter > 0 && (column_counter % 3 == 0)) - dat += " | " - dat += " |