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 += "
Random Job
" - 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 ( + + ); +}; + +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. + +