diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm
index e0a0d80e7f3..6923787b638 100644
--- a/code/__DEFINES/jobs.dm
+++ b/code/__DEFINES/jobs.dm
@@ -6,6 +6,10 @@
#define JOB_UNAVAILABLE_SLOTFULL 5
/// 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"
@@ -170,7 +174,7 @@
#define JOB_DISPLAY_ORDER_ENGINEER_GUARD 44 //SKYRAT EDIT ADDITION
#define JOB_DISPLAY_ORDER_CUSTOMS_AGENT 45 //SKYRAT EDIT ADDITION
-#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 4af0b5c2cd7..15c8e9b184a 100644
--- a/code/_onclick/hud/new_player.dm
+++ b/code/_onclick/hud/new_player.dm
@@ -190,18 +190,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
@@ -224,7 +225,11 @@
return
// SKYRAT EDIT END
- 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 8f8ade1ac31..e08018b02f0 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -499,13 +499,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
@@ -516,11 +516,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 6f0c6f1d468..e88774f2e0e 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..2a9f980a630
--- /dev/null
+++ b/code/modules/mob/dead/new_player/latejoin_menu.dm
@@ -0,0 +1,221 @@
+#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/latejoin_menu/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)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ // In case they reopen the GUI
+ if(istype(user))
+ 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(istype(player) && !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, round_seconds_to = 1),
+ "departments" = departments,
+ )
+ if(SSshuttle.emergency)
+ switch(SSshuttle.emergency.mode)
+ if(SHUTTLE_ESCAPE)
+ data["shuttle_status"] = "The station has been evacuated."
+ if(SHUTTLE_CALL, SHUTTLE_DOCKED, SHUTTLE_IGNITING, SHUTTLE_ESCAPE)
+ 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)
+
+// we can't use GLOB.new_player_state here since it also allows any admin to see the ui, which will cause runtimes
+/datum/latejoin_menu/ui_status(mob/user)
+ return isnewplayer(user) ? UI_INTERACTIVE : UI_CLOSE
+
+/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
+
+ // SKYRAT EDIT ADDITION START - Flavourtext requirement
+ if(length_char(owner.client.prefs.read_preference(/datum/preference/text/flavor_text)) < FLAVOR_TEXT_CHAR_REQUIREMENT)
+ to_chat(owner, span_notice("You need at least [FLAVOR_TEXT_CHAR_REQUIREMENT] characters of flavor text to join the round. You have [length_char(owner.client.prefs.read_preference(/datum/preference/text/flavor_text))] characters."))
+ return
+ // SKYRAT EDIT END
+
+ //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 7d6bdf173e6..56886a7351a 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,83 +39,6 @@
/mob/dead/new_player/prepare_huds()
return
-/* SKYRAT EDIT REMOVAL - MOVED TO MODULAR
-/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 = "
[random] Reroll Cancel "
- var/datum/browser/popup = new(src, "randjob", "Random Job
", 200, 150)
- popup.set_window_options("can_close=0")
- popup.set_content(randomjob)
- popup.open(FALSE)
- return
- if(!SSticker?.IsRoundInProgress())
- to_chat(usr, span_danger("The round is either not ready, or has already finished..."))
- return
-
- if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
- to_chat(usr, span_notice("There is an administrative lock on entering the game!"))
- 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)
- else
- relevant_cap = max(hpc, epc)
-
-
-
- if(SSticker.queued_players.len && !(ckey(key) in GLOB.admin_datums))
- if((living_player_count() >= relevant_cap) || (src != SSticker.queued_players[1]))
- to_chat(usr, span_warning("Server is full."))
- return
-
- AttemptLateSpawn(href_list["SelectedJob"])
- return
-
- if(href_list["viewpoll"])
- var/datum/poll_question/poll = locate(href_list["viewpoll"]) in GLOB.polls
- poll_player(poll)
-
- if(href_list["votepollref"])
- var/datum/poll_question/poll = locate(href_list["votepollref"]) in GLOB.polls
- vote_on_poll_handler(poll, href_list)
-*/
-
//When you cop out of the round (NB: this HAS A SLEEP FOR PLAYER INPUT IN IT)
/mob/dead/new_player/proc/make_me_an_observer()
if(QDELETED(src) || !src.client)
@@ -131,11 +55,11 @@
show_title_screen() // SKYRAT EDIT ADDITION
return FALSE
+ hide_title_screen() // SKYRAT EDIT ADDITION - Skyrat Titlescreen
var/mob/dead/observer/observer = new()
spawning = TRUE
observer.started_as_observer = TRUE
- close_spawn_windows()
var/obj/effect/landmark/observer_start/O = locate(/obj/effect/landmark/observer_start) in GLOB.landmarks_list
to_chat(src, span_notice("Now teleporting."))
if (O)
@@ -185,7 +109,8 @@
//SKYRAT EDIT END
if(JOB_UNAVAILABLE_ANTAG_INCOMPAT)
return "[jobtitle] is not compatible with some antagonist role assigned to you."
- return "Error: Unknown job availability."
+
+ return GENERIC_JOB_UNAVAILABLE_ERROR
/mob/dead/new_player/proc/IsJobUnavailable(rank, latejoin = FALSE)
var/datum/job/job = SSjob.GetJob(rank)
@@ -227,7 +152,6 @@
return FALSE
if(SSshuttle.arrivals)
- close_spawn_windows() //In case we get held up
if(SSshuttle.arrivals.damaged && CONFIG_GET(flag/arrivals_shuttle_require_safe_latejoin))
tgui_alert(usr,"The arrivals shuttle is currently malfunctioning! You cannot join.")
return FALSE
@@ -245,6 +169,7 @@
tgui_alert(usr, "There was an unexpected error putting you into your requested job. If you cannot join with any job, you should contact an admin.")
return FALSE
+ hide_title_screen()// SKYRAT EDIT ADDITION
mind.late_joiner = TRUE
var/atom/destination = mind.assigned_role.get_latejoin_spawn_point()
if(!destination)
@@ -315,13 +240,13 @@
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CREWMEMBER_JOINED, character, rank)
- //SKYRAT EDIT ADDITION
+ // SKYRAT EDIT ADDITION START
if(humanc)
for(var/datum/loadout_item/item as anything in loadout_list_to_datums(humanc?.client?.prefs?.loadout_list))
if (item.restricted_roles && length(item.restricted_roles) && !(job.title in item.restricted_roles))
continue
item.post_equip_item(humanc.client?.prefs, humanc)
- //SKYRAT EDIT END
+ // SKYRAT EDIT END
/mob/dead/new_player/proc/AddEmploymentContract(mob/living/carbon/human/employee)
//TODO: figure out a way to exclude wizards/nukeops/demons from this.
@@ -330,65 +255,9 @@
if(!employmentCabinet.virgin)
employmentCabinet.addFile(employee)
-
-/mob/dead/new_player/proc/LateChoices()
- var/list/dat = list()
- if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
- dat += "Only Observers may join at this time.
"
- dat += "Round Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]
"
- dat += "Alert Level: [capitalize(SSsecurity_level.get_current_level_as_text())]
" // SKYRAT EDIT ADDITION
- if(SSshuttle.emergency)
- switch(SSshuttle.emergency.mode)
- if(SHUTTLE_ESCAPE)
- dat += "The station has been evacuated.
"
- if(SHUTTLE_CALL)
- if(!SSshuttle.canRecall())
- dat += "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
- dat += ""
- var/column_counter = 0
-
- for(var/datum/job_department/department as anything in SSjob.joinable_departments)
- var/department_color = department.latejoin_color
- dat += ""
- dat += "[department.department_name] "
- var/list/dept_data = list()
- for(var/datum/job/job_datum as anything in department.department_jobs)
- if(IsJobUnavailable(job_datum.title, TRUE) != JOB_AVAILABLE)
- continue
-
- var/command_bold = ""
- if(job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
- command_bold = " command"
- if(job_datum in SSjob.prioritized_jobs)
- dept_data += "[job_datum.title] ([job_datum.current_positions]) "
- else
- dept_data += "[job_datum.title] ([job_datum.current_positions]) "
-
-
-
- if(!length(dept_data))
- dept_data += "No positions open. "
- dat += dept_data.Join()
- dat += " "
- column_counter++
- if(column_counter > 0 && (column_counter % 3 == 0))
- dat += " "
- dat += "
"
- dat += ""
- dat += ""
- var/datum/browser/popup = new(src, "latechoices", "Choose Profession", 680, 580)
- popup.add_stylesheet("playeroptions", 'html/browser/playeroptions.css')
- popup.set_content(jointext(dat, ""))
- popup.open(FALSE) // 0 is passed to open so that it doesn't use the onclose() proc
-
-
/// Creates, assigns and returns the new_character to spawn as. Assumes a valid mind.assigned_role exists.
/mob/dead/new_player/proc/create_character(atom/destination)
spawning = TRUE
- close_spawn_windows()
mind.active = FALSE //we wish to transfer the key manually
var/mob/living/spawning_mob = mind.assigned_role.get_spawn_mob(client, destination)
@@ -435,12 +304,6 @@
/mob/dead/new_player/Move()
return 0
-
-/mob/dead/new_player/proc/close_spawn_windows()
- hide_title_screen() // SKYRAT EDIT ADDITION
- src << browse(null, "window=latechoices") //closes late choices window (Hey numbnuts go make this tgui)
- src << browse(null, "window=randjob") //closes the random job window
-
// Used to make sure that a player has a valid job preference setup, used to knock players out of eligibility for anything if their prefs don't make sense.
// A "valid job preference setup" in this situation means at least having one job set to low, or not having "return to lobby" enabled
// Prevents "antag rolling" by setting antag prefs on, all jobs to never, and "return to lobby if preferences not available"
diff --git a/code/modules/modular_computers/file_system/programs/bounty_board.dm b/code/modules/modular_computers/file_system/programs/bounty_board.dm
index 97be2c2f0d4..271ac321306 100644
--- a/code/modules/modular_computers/file_system/programs/bounty_board.dm
+++ b/code/modules/modular_computers/file_system/programs/bounty_board.dm
@@ -30,7 +30,7 @@
data["user"]["department"] = current_user.account_job.paycheck_department
else
data["user"]["job"] = "No Job"
- data["user"]["department"] = "No Department"
+ data["user"]["department"] = DEPARTMENT_UNASSIGNED
else
data["user"] = list()
data["user"]["name"] = user.name
diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm
index 1db21509119..2f3156bb2ed 100644
--- a/code/modules/vending/_vending.dm
+++ b/code/modules/vending/_vending.dm
@@ -946,7 +946,7 @@ GLOBAL_LIST_EMPTY(vending_products)
.["user"]["department"] = C.registered_account.account_job.paycheck_department
else
.["user"]["job"] = "No Job"
- .["user"]["department"] = "No Department"
+ .["user"]["department"] = DEPARTMENT_UNASSIGNED
.["stock"] = list()
for (var/datum/data/vending_product/product_record in product_records + coin_records + hidden_records)
diff --git a/modular_skyrat/master_files/code/modules/jobs/departments/departments.dm b/modular_skyrat/master_files/code/modules/jobs/departments/departments.dm
index 10ed5ce01da..1b88a250ca4 100644
--- a/modular_skyrat/master_files/code/modules/jobs/departments/departments.dm
+++ b/modular_skyrat/master_files/code/modules/jobs/departments/departments.dm
@@ -5,4 +5,7 @@
department_experience_type = EXP_TYPE_CENTRAL_COMMAND
display_order = 1
label_class = "command"
- latejoin_color = "#86ff82"
+ ui_color = "#86ff82"
+
+/datum/job_department/security
+ ui_color = "#528ccf"
diff --git a/modular_skyrat/master_files/code/modules/mob/dead/new_player/latejoin_menu.dm b/modular_skyrat/master_files/code/modules/mob/dead/new_player/latejoin_menu.dm
new file mode 100644
index 00000000000..60546f1eae8
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/mob/dead/new_player/latejoin_menu.dm
@@ -0,0 +1,8 @@
+/datum/latejoin_menu/ui_data(mob/user)
+ . = ..()
+ var/color = SSsecurity_level.get_current_level_as_text()
+ switch(color)
+ if("delta", "gamma")
+ color = "red"
+
+ .["alert_level"] = list("name" = capitalize(SSsecurity_level.get_current_level_as_text()), "color" = color)
diff --git a/modular_skyrat/modules/title_screen/code/new_player.dm b/modular_skyrat/modules/title_screen/code/new_player.dm
index d8c39acbfc7..701932ccf33 100644
--- a/modular_skyrat/modules/title_screen/code/new_player.dm
+++ b/modular_skyrat/modules/title_screen/code/new_player.dm
@@ -62,58 +62,7 @@
if(href_list["late_join"])
play_lobby_button_sound()
- if(!SSticker?.IsRoundInProgress())
- to_chat(src, span_boldwarning("The round is either not ready, or has already finished..."))
- return
-
- //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 || (relevant_cap && living_player_count() >= relevant_cap && !(ckey(key) in GLOB.admin_datums)))
- to_chat(src, span_danger("[CONFIG_GET(string/hard_popcap_message)]"))
-
- var/queue_position = SSticker.queued_players.Find(src)
- if(queue_position == 1)
- to_chat(src, span_notice("You are next in line to join the game. You will be notified when a slot opens up."))
- else if(queue_position)
- to_chat(src, span_notice("There are [queue_position-1] players in front of you in the queue to join the game."))
- else
- SSticker.queued_players += src
- to_chat(src, span_notice("You have been added to the queue to join the game. Your position in queue is [SSticker.queued_players.len]."))
- return
-
- if(length_char(src.client.prefs.read_preference(/datum/preference/text/flavor_text)) < FLAVOR_TEXT_CHAR_REQUIREMENT)
- to_chat(src, span_notice("You need at least [FLAVOR_TEXT_CHAR_REQUIREMENT] characters of flavor text to join the round. You have [length_char(src.client.prefs.read_preference(/datum/preference/text/flavor_text))] characters."))
- return
-
- LateChoices()
- return
-
- if(href_list["cancrand"])
- src << browse(null, "window=randjob") //closes the random job window
- LateChoices()
- return
-
- if(href_list["SelectedJob"])
- select_job(href_list["SelectedJob"])
- return
-
- if(href_list["viewpoll"])
- play_lobby_button_sound()
- var/datum/poll_question/poll = locate(href_list["viewpoll"]) in GLOB.polls
- poll_player(poll)
- return
-
- if(href_list["votepollref"])
- var/datum/poll_question/poll = locate(href_list["votepollref"]) in GLOB.polls
- vote_on_poll_handler(poll, href_list)
- return
+ GLOB.latejoin_menu.ui_interact(usr)
if(href_list["title_is_ready"])
title_screen_is_ready = TRUE
@@ -164,52 +113,6 @@
/mob/dead/new_player/proc/play_lobby_button_sound()
SEND_SOUND(src, sound('modular_skyrat/master_files/sound/effects/save.ogg'))
-/**
- * Selects a new job or gives random if unset.
- */
-/mob/dead/new_player/proc/select_job(job)
- if(job == "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 = "
[random] Reroll Cancel "
- var/datum/browser/popup = new(src, "randjob", "Random Job
", 200, 150)
- popup.set_window_options("can_close=0")
- popup.set_content(randomjob)
- popup.open(FALSE)
- return
-
- if(!SSticker?.IsRoundInProgress())
- to_chat(usr, span_danger("The round is either not ready, or has already finished..."))
- return
-
- if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
- to_chat(usr, span_notice("There is an administrative lock on entering the game!"))
- return
-
- //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(LAZYLEN(SSticker.queued_players) && !(ckey(key) in GLOB.admin_datums))
- if((living_player_count() >= relevant_cap) || (src != SSticker.queued_players[1]))
- to_chat(usr, span_warning("Server is full."))
- return
-
- AttemptLateSpawn(job)
-
/**
* Allows the player to select a server to join from any loaded servers.
*/
diff --git a/tgstation.dme b/tgstation.dme
index fd9284a6098..13f4aab26d8 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3633,6 +3633,7 @@
#include "code\modules\mob\camera\camera.dm"
#include "code\modules\mob\dead\crew_manifest.dm"
#include "code\modules\mob\dead\dead.dm"
+#include "code\modules\mob\dead\new_player\latejoin_menu.dm"
#include "code\modules\mob\dead\new_player\login.dm"
#include "code\modules\mob\dead\new_player\logout.dm"
#include "code\modules\mob\dead\new_player\new_player.dm"
@@ -5110,6 +5111,7 @@
#include "modular_skyrat\master_files\code\modules\jobs\job_types\cyborg.dm"
#include "modular_skyrat\master_files\code\modules\language\language.dm"
#include "modular_skyrat\master_files\code\modules\language\language_holders.dm"
+#include "modular_skyrat\master_files\code\modules\mob\dead\new_player\latejoin_menu.dm"
#include "modular_skyrat\master_files\code\modules\mob\dead\new_player\preferences_setup.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\blood.dm"
#include "modular_skyrat\master_files\code\modules\mob\living\emote_popup.dm"
diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts
index 49f500ebd29..3902feec5cd 100644
--- a/tgui/packages/common/collections.ts
+++ b/tgui/packages/common/collections.ts
@@ -300,3 +300,24 @@ export const binaryInsertWith =
copy.splice(binarySearch(getKey, collection, value), 0, value);
return copy;
};
+
+const isObject = (obj: unknown) => typeof obj === 'object' && obj !== null;
+
+// Does a deep merge of two objects. DO NOT FEED CIRCULAR OBJECTS!!
+export const deepMerge = (...objects: any[]): any => {
+ const target = {};
+ for (const object of objects) {
+ for (const key of Object.keys(object)) {
+ const targetValue = target[key];
+ const objectValue = object[key];
+ if (Array.isArray(targetValue) && Array.isArray(objectValue)) {
+ target[key] = [...targetValue, ...objectValue];
+ } else if (isObject(targetValue) && isObject(objectValue)) {
+ target[key] = deepMerge(targetValue, objectValue);
+ } else {
+ target[key] = objectValue;
+ }
+ }
+ }
+ return target;
+};
diff --git a/tgui/packages/common/color.js b/tgui/packages/common/color.js
index 672fce529b6..b59d82247aa 100644
--- a/tgui/packages/common/color.js
+++ b/tgui/packages/common/color.js
@@ -22,6 +22,23 @@ export class Color {
}
return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${alpha})`;
}
+
+ // Darkens a color by a given percent. Returns a color, which can have toString called to get it's rgba() css value.
+ darken(percent) {
+ percent /= 100;
+ return new Color(
+ this.r - this.r * percent,
+ this.g - this.g * percent,
+ this.b - this.b * percent,
+ this.a
+ );
+ }
+
+ // Brightens a color by a given percent. Returns a color, which can have toString called to get it's rgba() css value.
+ lighten(percent) {
+ // No point in rewriting code we already have.
+ return this.darken(-percent);
+ }
}
/**
diff --git a/tgui/packages/tgui/components/StyleableSection.tsx b/tgui/packages/tgui/components/StyleableSection.tsx
new file mode 100644
index 00000000000..ad38984fee4
--- /dev/null
+++ b/tgui/packages/tgui/components/StyleableSection.tsx
@@ -0,0 +1,26 @@
+import { SFC } from 'inferno';
+import { Box } from './Box';
+
+// The cost of flexibility and prettiness.
+export const StyleableSection: SFC<{
+ style?;
+ titleStyle?;
+ textStyle?;
+ title?;
+ titleSubtext?;
+}> = (props) => {
+ return (
+
+ {/* Yes, this box (line above) is missing the "Section" class. This is very intentional, as the layout looks *ugly* with it.*/}
+
+
+ {props.title}
+
+ {props.titleSubtext}
+
+
+ {props.children}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/components/index.js b/tgui/packages/tgui/components/index.js
index 121d2bfa4e2..32af982767a 100644
--- a/tgui/packages/tgui/components/index.js
+++ b/tgui/packages/tgui/components/index.js
@@ -37,6 +37,7 @@ export { RestrictedInput } from './RestrictedInput';
export { RoundGauge } from './RoundGauge';
export { Section } from './Section';
export { Slider } from './Slider';
+export { StyleableSection } from './StyleableSection';
export { Stack } from './Stack';
export { Table } from './Table';
export { Tabs } from './Tabs';
diff --git a/tgui/packages/tgui/interfaces/JobSelection.tsx b/tgui/packages/tgui/interfaces/JobSelection.tsx
new file mode 100644
index 00000000000..1c691a020fe
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/JobSelection.tsx
@@ -0,0 +1,211 @@
+import { useBackend } from '../backend';
+import { Box, Button, StyleableSection, Icon, Stack, NoticeBox } from '../components';
+import { Window } from '../layouts';
+import { Color } from 'common/color';
+import { SFC } from 'inferno';
+import { JobToIcon } from './common/JobToIcon';
+import { deepMerge } from 'common/collections';
+import { BooleanLike } from 'common/react';
+
+type Job = {
+ unavailable_reason: string | null;
+ command: BooleanLike;
+ open_slots: number;
+ used_slots: number;
+ icon: string;
+ prioritized: BooleanLike;
+ description: string;
+};
+
+type Department = {
+ color: string;
+ jobs: Record;
+ open_slots: number;
+};
+
+type Data = {
+ departments_static: Record;
+ departments: Record;
+ alert_state: string;
+ shuttle_status: string;
+ disable_jobs_for_non_observers: BooleanLike;
+ priority: BooleanLike;
+ round_duration: string;
+ alert_level: { name: string; color: string }; // SKYRAT EDIT ADDITION - Alert level on jobs menu
+};
+
+export const JobEntry: SFC<{
+ jobName: string;
+ job: Job;
+ department: Department;
+ onClick: () => void;
+}> = (data) => {
+ const jobName = data.jobName;
+ const job = data.job;
+ const department = data.department;
+ const jobIcon = job.icon || JobToIcon[jobName] || null;
+ return (
+
+
+ The HoP wants more people in this job!
+
+ {job.description}
+ >
+ ) : (
+ job.description
+ ))
+ }
+ onClick={() => {
+ !job.unavailable_reason && data.onClick();
+ }}>
+ <>
+ {jobIcon && }
+ {job.command ? {jobName} : jobName}
+
+ {job.used_slots} / {job.open_slots}
+
+ >
+
+ );
+};
+
+export const JobSelection = (props, context) => {
+ const { act, data } = useBackend(context);
+ if (!data?.departments_static) {
+ return null; // Stop TGUI whitescreens with TGUI-dev!
+ }
+ const departments: Record = deepMerge(
+ data.departments,
+ data.departments_static
+ );
+
+ return (
+ {
+ // Send a heartbeat back to DM to let it know the window is alive and well
+ act('ui_mounted_with_no_bluescreen');
+ }}>
+
+
+ {data.shuttle_status && (
+ {data.shuttle_status}
+ )}
+ {
+ // SKYRAT EDIT ADDITION - Alert level on jobs menu
+
+ The current alert level is: {data.alert_level.name}
+
+ }
+
+ It is currently {data.round_duration} into the shift.
+
+ act('select_job', { 'job': 'Random' })}
+ content="Random Job!"
+ tooltip="Roll target random job. You can re-roll or cancel your random job if you don't like it."
+ />
+ >
+ }
+ titleStyle={{ 'min-height': '3.4em' }}>
+
+ {Object.entries(departments).map((departmentEntry) => {
+ const departmentName = departmentEntry[0];
+ const entry = departmentEntry[1];
+ return (
+
+
+ {departmentName}
+
+ {entry.open_slots +
+ (entry.open_slots === 1 ? ' slot' : ' slots') +
+ ' available'}
+
+ >
+ }
+ style={{
+ 'background-color': entry.color,
+ 'margin-bottom': '1em',
+ 'break-inside': 'avoid-column',
+ }}
+ titleStyle={{
+ 'border-bottom-color': Color.fromHex(entry.color)
+ .darken(50)
+ .toString(),
+ }}
+ textStyle={{
+ 'color': Color.fromHex(entry.color)
+ .darken(80)
+ .toString(),
+ }}>
+
+ {Object.entries(entry.jobs).map((job) => (
+
+ {
+ act('select_job', { job: job[0] });
+ }}
+ />
+
+ ))}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/Orbit/constants.ts b/tgui/packages/tgui/interfaces/Orbit/constants.ts
index 55b06984cb4..431c02980f0 100644
--- a/tgui/packages/tgui/interfaces/Orbit/constants.ts
+++ b/tgui/packages/tgui/interfaces/Orbit/constants.ts
@@ -25,12 +25,6 @@ export const ANTAG2GROUP = {
'Wizard Minion': 'Wizard Federation',
} as const;
-export const JOB2ICON = {
- 'AI': 'eye',
- 'Cyborg': 'robot',
- 'Personal AI': 'mobile-alt',
-} as const;
-
export enum THREAT {
Low = 2,
Medium = 5,
diff --git a/tgui/packages/tgui/interfaces/Orbit/index.tsx b/tgui/packages/tgui/interfaces/Orbit/index.tsx
index c2b3644f403..e812c0b6bfe 100644
--- a/tgui/packages/tgui/interfaces/Orbit/index.tsx
+++ b/tgui/packages/tgui/interfaces/Orbit/index.tsx
@@ -5,7 +5,8 @@ import { useBackend, useLocalState } from 'tgui/backend';
import { Button, Collapsible, Icon, Input, LabeledList, NoticeBox, Section, Stack } from 'tgui/components';
import { Window } from 'tgui/layouts';
import { collateAntagonists, getDisplayColor, getDisplayName, isJobOrNameMatch } from './helpers';
-import { ANTAG2COLOR, JOB2ICON } from './constants';
+import { ANTAG2COLOR } from './constants';
+import { JobToIcon } from '../common/JobToIcon';
import type { AntagGroup, Observable, OrbitData } from './types';
export const Orbit = (props, context) => {
@@ -225,7 +226,7 @@ const ObservableItem = (
return (
act('orbit', { auto_observe: autoObserve, ref: ref })}
tooltip={(!!health || !!extra) && }
tooltipPosition="bottom-start">
diff --git a/tgui/packages/tgui/interfaces/common/JobToIcon.ts b/tgui/packages/tgui/interfaces/common/JobToIcon.ts
new file mode 100644
index 00000000000..272ef157a7a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/common/JobToIcon.ts
@@ -0,0 +1,6 @@
+// Used to handle jobs that don't have a trim.
+export const JobToIcon = {
+ 'AI': 'eye',
+ 'Cyborg': 'robot',
+ 'Personal AI': 'mobile-alt',
+} as const;