diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm
index 3c7399d98fa..41c2b4b7459 100644
--- a/code/__DEFINES/dcs/signals/signals_global.dm
+++ b/code/__DEFINES/dcs/signals/signals_global.dm
@@ -84,6 +84,9 @@
/// (new_name, old_name)
#define COMSIG_GLOB_STATION_NAME_CHANGED "!station_name_changed"
+/// Global signal sent before we decide what job everyone has
+#define COMSIG_GLOB_PRE_JOBS_ASSIGNED "!pre_roles_assigned"
+
/// global signal when a global nullrod type is picked
#define COMSIG_GLOB_NULLROD_PICKED "!nullrod_picked"
diff --git a/code/__DEFINES/jobs.dm b/code/__DEFINES/jobs.dm
index 7feca0782dc..9b65e8a9c55 100644
--- a/code/__DEFINES/jobs.dm
+++ b/code/__DEFINES/jobs.dm
@@ -80,6 +80,7 @@
//Supply
#define JOB_QUARTERMASTER "Quartermaster"
#define JOB_CARGO_TECHNICIAN "Cargo Technician"
+#define JOB_CARGO_GORILLA "Cargo Gorilla"
#define JOB_SHAFT_MINER "Shaft Miner"
#define JOB_BITRUNNER "Bitrunner"
#define JOB_CUSTOMS_AGENT "Customs Agent" // SKYRAT EDIT ADDITION
@@ -164,31 +165,32 @@
#define JOB_DISPLAY_ORDER_CARGO_TECHNICIAN 20
#define JOB_DISPLAY_ORDER_SHAFT_MINER 21
#define JOB_DISPLAY_ORDER_BITRUNNER 22
-#define JOB_DISPLAY_ORDER_CHIEF_MEDICAL_OFFICER 23
-#define JOB_DISPLAY_ORDER_MEDICAL_DOCTOR 24
-#define JOB_DISPLAY_ORDER_PARAMEDIC 25
-#define JOB_DISPLAY_ORDER_CHEMIST 26
-#define JOB_DISPLAY_ORDER_VIROLOGIST 27
-#define JOB_DISPLAY_ORDER_CORONER 28
-#define JOB_DISPLAY_ORDER_RESEARCH_DIRECTOR 29
-#define JOB_DISPLAY_ORDER_SCIENTIST 30
-#define JOB_DISPLAY_ORDER_ROBOTICIST 31
-#define JOB_DISPLAY_ORDER_GENETICIST 32
-#define JOB_DISPLAY_ORDER_HEAD_OF_SECURITY 33
-#define JOB_DISPLAY_ORDER_WARDEN 34
-#define JOB_DISPLAY_ORDER_DETECTIVE 35
-#define JOB_DISPLAY_ORDER_SECURITY_OFFICER 36
-#define JOB_DISPLAY_ORDER_PRISONER 37
-#define JOB_DISPLAY_ORDER_SECURITY_MEDIC 38 //SKYRAT EDIT ADDITON
-#define JOB_DISPLAY_ORDER_CORRECTIONS_OFFICER 39 //SKYRAT EDIT ADDITON
-#define JOB_DISPLAY_ORDER_NANOTRASEN_CONSULTANT 40 //SKYRAT EDIT ADDITON
-#define JOB_DISPLAY_ORDER_BLUESHIELD 41 //SKYRAT EDIT ADDITON
-#define JOB_DISPLAY_ORDER_ORDERLY 42 //SKYRAT EDIT ADDITION
-#define JOB_DISPLAY_ORDER_SCIENCE_GUARD 43 //SKYRAT EDIT ADDITION
-#define JOB_DISPLAY_ORDER_BOUNCER 44 //SKYRAT EDIT ADDITION
-#define JOB_DISPLAY_ORDER_ENGINEER_GUARD 45 //SKYRAT EDIT ADDITION
-#define JOB_DISPLAY_ORDER_CUSTOMS_AGENT 46 //SKYRAT EDIT ADDITION
-#define JOB_DISPLAY_ORDER_EXP_CORPS 47 //SKYRAT EDIT ADDITON
+#define JOB_DISPLAY_ORDER_CARGO_GORILLA 23
+#define JOB_DISPLAY_ORDER_CHIEF_MEDICAL_OFFICER 24
+#define JOB_DISPLAY_ORDER_MEDICAL_DOCTOR 25
+#define JOB_DISPLAY_ORDER_PARAMEDIC 26
+#define JOB_DISPLAY_ORDER_CHEMIST 27
+#define JOB_DISPLAY_ORDER_VIROLOGIST 28
+#define JOB_DISPLAY_ORDER_CORONER 29
+#define JOB_DISPLAY_ORDER_RESEARCH_DIRECTOR 30
+#define JOB_DISPLAY_ORDER_SCIENTIST 31
+#define JOB_DISPLAY_ORDER_ROBOTICIST 32
+#define JOB_DISPLAY_ORDER_GENETICIST 33
+#define JOB_DISPLAY_ORDER_HEAD_OF_SECURITY 34
+#define JOB_DISPLAY_ORDER_WARDEN 35
+#define JOB_DISPLAY_ORDER_DETECTIVE 36
+#define JOB_DISPLAY_ORDER_SECURITY_OFFICER 37
+#define JOB_DISPLAY_ORDER_PRISONER 38
+#define JOB_DISPLAY_ORDER_SECURITY_MEDIC 100 //SKYRAT EDIT ADDITON
+#define JOB_DISPLAY_ORDER_CORRECTIONS_OFFICER 101 //SKYRAT EDIT ADDITON
+#define JOB_DISPLAY_ORDER_NANOTRASEN_CONSULTANT 102 //SKYRAT EDIT ADDITON
+#define JOB_DISPLAY_ORDER_BLUESHIELD 103 //SKYRAT EDIT ADDITON
+#define JOB_DISPLAY_ORDER_ORDERLY 104 //SKYRAT EDIT ADDITION
+#define JOB_DISPLAY_ORDER_SCIENCE_GUARD 105 //SKYRAT EDIT ADDITION
+#define JOB_DISPLAY_ORDER_BOUNCER 106 //SKYRAT EDIT ADDITION
+#define JOB_DISPLAY_ORDER_ENGINEER_GUARD 107 //SKYRAT EDIT ADDITION
+#define JOB_DISPLAY_ORDER_CUSTOMS_AGENT 108 //SKYRAT EDIT ADDITION
+#define JOB_DISPLAY_ORDER_EXP_CORPS 109 //SKYRAT EDIT ADDITON
#define DEPARTMENT_UNASSIGNED "No Department"
@@ -236,6 +238,10 @@
#define JOB_CAN_BE_INTERN (1<<8)
/// This job cannot have more slots opened by the Head of Personnel (but admins or other random events can still do this).
#define JOB_CANNOT_OPEN_SLOTS (1<<9)
+/// This job will not display on the job menu when there are no slots available, instead of appearing greyed out
+#define JOB_HIDE_WHEN_EMPTY (1<<10)
+/// This job cannot be signed up for at round start or recorded in your preferences
+#define JOB_LATEJOIN_ONLY (1<<11)
/// Combination flag for jobs which are considered regular crew members of the station.
#define STATION_JOB_FLAGS (JOB_ANNOUNCE_ARRIVAL|JOB_CREW_MANIFEST|JOB_EQUIP_RANK|JOB_CREW_MEMBER|JOB_NEW_PLAYER_JOINABLE|JOB_REOPEN_ON_ROUNDSTART_LOSS|JOB_ASSIGN_QUIRKS|JOB_CAN_BE_INTERN)
diff --git a/code/_globalvars/lists/mapping.dm b/code/_globalvars/lists/mapping.dm
index 8ac2f0d3ddf..b77942bad87 100644
--- a/code/_globalvars/lists/mapping.dm
+++ b/code/_globalvars/lists/mapping.dm
@@ -107,6 +107,7 @@ GLOBAL_LIST_EMPTY(generic_event_spawns)
/// essentially allowing a user to override generic job spawnpoints with a specific one
GLOBAL_LIST_EMPTY(jobspawn_overrides)
+GLOBAL_LIST_EMPTY(gorilla_start)
GLOBAL_LIST_EMPTY(wizardstart)
GLOBAL_LIST_EMPTY(nukeop_start)
GLOBAL_LIST_EMPTY(nukeop_leader_start)
diff --git a/code/_globalvars/lists/names.dm b/code/_globalvars/lists/names.dm
index bc195bcf5f4..c51fbaa9eb7 100644
--- a/code/_globalvars/lists/names.dm
+++ b/code/_globalvars/lists/names.dm
@@ -25,6 +25,7 @@ GLOBAL_LIST_INIT(megacarp_first_names, world.file2list("strings/names/megacarp1.
GLOBAL_LIST_INIT(megacarp_last_names, world.file2list("strings/names/megacarp2.txt"))
GLOBAL_LIST_INIT(cyberauth_names, world.file2list("strings/names/cyberauth.txt"))
GLOBAL_LIST_INIT(syndicate_monkey_names, world.file2list("strings/names/syndicate_monkey.txt"))
+GLOBAL_LIST_INIT(cargorilla_names, world.file2list("strings/names/cargorilla.txt"))
GLOBAL_LIST_INIT(guardian_first_names, world.file2list("strings/names/guardian_descriptions.txt"))
GLOBAL_LIST_INIT(guardian_tech_surnames, world.file2list("strings/names/guardian_gamepieces.txt"))
GLOBAL_LIST_INIT(guardian_fantasy_surnames, world.file2list("strings/names/guardian_tarot.txt"))
diff --git a/code/_onclick/hud/new_player.dm b/code/_onclick/hud/new_player.dm
index 376164235a7..6edd9c65d2a 100644
--- a/code/_onclick/hud/new_player.dm
+++ b/code/_onclick/hud/new_player.dm
@@ -1,38 +1,66 @@
/* SKYRAT EDIT REMOVAL - Custom HTML Lobby Screen
#define SHUTTER_MOVEMENT_DURATION 0.4 SECONDS
#define SHUTTER_WAIT_DURATION 0.2 SECONDS
+/// Maximum number of station trait buttons we will display, please think hard before creating scenarios where there are more than this
+#define MAX_STATION_TRAIT_BUTTONS_VERTICAL 3
/datum/hud/new_player
///Whether the menu is currently on the client's screen or not
var/menu_hud_status = TRUE
/datum/hud/new_player/New(mob/owner)
- ..()
+ . = ..()
- if(!owner || !owner.client)
+ if (!owner || !owner.client)
return
- if(owner.client.interviewee)
+ if (owner.client.interviewee)
return
var/list/buttons = subtypesof(/atom/movable/screen/lobby)
- for(var/button_type in buttons)
- var/atom/movable/screen/lobby/lobbyscreen = new button_type(our_hud = src)
+ for (var/atom/movable/screen/lobby/lobbyscreen as anything in buttons)
+ if (!initial(lobbyscreen.always_available))
+ continue
+ lobbyscreen = new lobbyscreen(our_hud = src)
lobbyscreen.SlowInit()
static_inventory += lobbyscreen
- if(!lobbyscreen.always_shown)
+ if (!lobbyscreen.always_shown)
lobbyscreen.RegisterSignal(src, COMSIG_HUD_LOBBY_COLLAPSED, TYPE_PROC_REF(/atom/movable/screen/lobby, collapse_button))
lobbyscreen.RegisterSignal(src, COMSIG_HUD_LOBBY_EXPANDED, TYPE_PROC_REF(/atom/movable/screen/lobby, expand_button))
- if(istype(lobbyscreen, /atom/movable/screen/lobby/button))
+ if (istype(lobbyscreen, /atom/movable/screen/lobby/button))
var/atom/movable/screen/lobby/button/lobby_button = lobbyscreen
lobby_button.owner = REF(owner)
+ add_station_trait_buttons()
+
+/// Display buttons for relevant station traits
+/datum/hud/new_player/proc/add_station_trait_buttons()
+ if (!mymob?.client || mymob.client.interviewee || !length(GLOB.lobby_station_traits))
+ return
+ var/buttons_created = 0
+ var/y_offset = 397
+ var/y_button_offset = 27
+ for (var/datum/station_trait/trait as anything in GLOB.lobby_station_traits)
+ if (!trait.can_display_lobby_button())
+ continue
+ var/atom/movable/screen/lobby/button/sign_up/sign_up_button = new(our_hud = src)
+ sign_up_button.SlowInit()
+ sign_up_button.owner = REF(mymob)
+ sign_up_button.screen_loc = offset_to_screen_loc(233, y_offset, mymob.client.view)
+ y_offset += y_button_offset
+ static_inventory += sign_up_button
+ trait.setup_lobby_button(sign_up_button)
+ buttons_created++
+ if (buttons_created >= MAX_STATION_TRAIT_BUTTONS_VERTICAL)
+ return
/atom/movable/screen/lobby
plane = SPLASHSCREEN_PLANE
layer = LOBBY_MENU_LAYER
screen_loc = "TOP,CENTER"
- ///Whether this HUD element can be hidden from the client's "screen" (moved off-screen) or not
+ /// Whether this HUD element can be hidden from the client's "screen" (moved off-screen) or not
var/always_shown = FALSE
+ /// If true we will create this button every time the HUD is generated
+ var/always_available = TRUE
///Set the HUD in New, as lobby screens are made before Atoms are Initialized.
/atom/movable/screen/lobby/New(loc, datum/hud/our_hud, ...)
@@ -413,6 +441,23 @@
var/mob/dead/new_player/new_player = hud.mymob
new_player.handle_player_polling()
+/// A generic "sign up" button used by station traits
+/atom/movable/screen/lobby/button/sign_up
+ icon = 'icons/hud/lobby/signup_button.dmi'
+ icon_state = "signup"
+ base_icon_state = "signup"
+ always_available = FALSE
+
+/atom/movable/screen/lobby/button/sign_up/MouseEntered(location, control, params)
+ . = ..()
+ if(QDELETED(src) || !desc)
+ return
+ openToolTip(usr, tip_src = src, params = params, title = name, content = desc,)
+
+/atom/movable/screen/lobby/button/sign_up/MouseExited()
+ . = ..()
+ closeToolTip(usr)
+
/atom/movable/screen/lobby/button/collapse
name = "Collapse Lobby Menu"
icon = 'icons/hud/lobby/collapse_expand.dmi'
@@ -540,5 +585,5 @@
#undef SHUTTER_MOVEMENT_DURATION
#undef SHUTTER_WAIT_DURATION
-
+#undef MAX_STATION_TRAIT_BUTTONS_VERTICAL
*/ // SKYRAT EDIT END
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index 5053b4672f3..e812e5b2b5d 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -55,6 +55,10 @@
/atom/movable/screen/proc/component_click(atom/movable/screen/component_button/component, params)
return
+/// Returns the mob this is being displayed to, if any
+/atom/movable/screen/proc/get_mob()
+ return hud?.mymob
+
/atom/movable/screen/text
icon = null
icon_state = null
diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm
index 8376059e910..027c70447b0 100644
--- a/code/controllers/subsystem/job.dm
+++ b/code/controllers/subsystem/job.dm
@@ -94,6 +94,18 @@ SUBSYSTEM_DEF(job)
set_overflow_role(CONFIG_GET(string/overflow_job)) // this must always go after load_jobs_from_config() due to how the legacy systems operate, this always takes precedent.
return SS_INIT_SUCCESS
+/// Returns a list of jobs that we are allowed to fuck with during random events
+/datum/controller/subsystem/job/proc/get_valid_overflow_jobs()
+ var/static/list/overflow_jobs
+ if (!isnull(overflow_jobs))
+ return overflow_jobs
+
+ overflow_jobs = list()
+ for (var/datum/job/check_job in joinable_occupations)
+ if (!check_job.allow_bureaucratic_error)
+ continue
+ overflow_jobs += check_job
+ return overflow_jobs
/datum/controller/subsystem/job/proc/set_overflow_role(new_overflow_role)
var/datum/job/new_overflow = ispath(new_overflow_role) ? GetJobType(new_overflow_role) : GetJob(new_overflow_role)
diff --git a/code/controllers/subsystem/processing/station.dm b/code/controllers/subsystem/processing/station.dm
index b30a276fc36..53413166e97 100644
--- a/code/controllers/subsystem/processing/station.dm
+++ b/code/controllers/subsystem/processing/station.dm
@@ -18,6 +18,7 @@ PROCESSING_SUBSYSTEM_DEF(station)
// Autowiki also wants consistent outputs, for example making sure the vending machine page always reports the normal products
#if !defined(UNIT_TESTS) && !defined(AUTOWIKI)
SetupTraits()
+ //display_lobby_traits() SKYRAT EDIT REMOVAL
#endif
announcer = new announcer() //Initialize the station's announcer datum
@@ -96,3 +97,14 @@ PROCESSING_SUBSYSTEM_DEF(station)
for(var/i in trait_instance.blacklist)
var/datum/station_trait/trait_to_remove = i
selectable_traits_by_types[initial(trait_to_remove.trait_type)] -= trait_to_remove
+
+/* SKYRAT EDIT REMOVAL
+/// Update station trait lobby buttons for clients who joined before we initialised this subsystem
+/datum/controller/subsystem/processing/station/proc/display_lobby_traits()
+ for (var/mob/dead/new_player/player as anything in GLOB.new_player_list)
+ var/datum/hud/new_player/observer_hud = player.hud_used
+ if (!istype(observer_hud))
+ continue
+ observer_hud.add_station_trait_buttons()
+ observer_hud.show_hud(observer_hud.hud_version)
+*/
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 97b2c85b2aa..bdc55ecdfc0 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -242,6 +242,7 @@ SUBSYSTEM_DEF(ticker)
var/can_continue = 0
can_continue = src.mode.pre_setup() //Choose antagonists
CHECK_TICK
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src)
can_continue = can_continue && SSjob.DivideOccupations() //Distribute jobs
CHECK_TICK
diff --git a/code/datums/components/ghost_direct_control.dm b/code/datums/components/ghost_direct_control.dm
index c4d08380db1..412ae0f767e 100644
--- a/code/datums/components/ghost_direct_control.dm
+++ b/code/datums/components/ghost_direct_control.dm
@@ -30,7 +30,7 @@
src.ban_type = ban_type
src.assumed_control_message = assumed_control_message || "You are [parent]!"
src.extra_control_checks = extra_control_checks
- src.after_assumed_control= after_assumed_control
+ src.after_assumed_control = after_assumed_control
var/mob/mob_parent = parent
LAZYADD(GLOB.joinable_mobs[format_text("[initial(mob_parent.name)]")], mob_parent)
@@ -42,9 +42,10 @@
. = ..()
RegisterSignal(parent, COMSIG_ATOM_ATTACK_GHOST, PROC_REF(on_ghost_clicked))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(parent, COMSIG_MOB_LOGIN, PROC_REF(on_login))
/datum/component/ghost_direct_control/UnregisterFromParent()
- UnregisterSignal(parent, list(COMSIG_ATOM_ATTACK_GHOST, COMSIG_ATOM_EXAMINE))
+ UnregisterSignal(parent, list(COMSIG_ATOM_ATTACK_GHOST, COMSIG_ATOM_EXAMINE, COMSIG_MOB_LOGIN))
return ..()
/datum/component/ghost_direct_control/Destroy(force, silent)
@@ -138,3 +139,8 @@
to_chat(new_body, span_boldnotice(assumed_control_message))
after_assumed_control?.Invoke(harbinger)
qdel(src)
+
+/// When someone else assumes control via some other means, get rid of our component
+/datum/component/ghost_direct_control/proc/on_login()
+ SIGNAL_HANDLER
+ qdel(src)
diff --git a/code/datums/station_traits/_station_trait.dm b/code/datums/station_traits/_station_trait.dm
index 8e8303c0366..5cbf0d4da9f 100644
--- a/code/datums/station_traits/_station_trait.dm
+++ b/code/datums/station_traits/_station_trait.dm
@@ -1,3 +1,6 @@
+/// Station traits displayed in the lobby
+GLOBAL_LIST_EMPTY(lobby_station_traits)
+
///Base class of station traits. These are used to influence rounds in one way or the other by influencing the levers of the station.
/datum/station_trait
///Name of the trait
@@ -22,6 +25,10 @@
var/trait_flags = STATION_TRAIT_MAP_UNRESTRICTED
/// Whether or not this trait can be reverted by an admin
var/can_revert = TRUE
+ /// If set to true we'll show a button on the lobby to notify people about this trait
+ var/sign_up_button = FALSE
+ /// Lobby buttons controlled by this trait
+ var/list/lobby_buttons = list()
/// The ID that we look for in dynamic.json. Not synced with 'name' because I can already see this go wrong
var/dynamic_threat_id
/// If ran during dynamic, do we reduce the total threat? Will be overriden by config if set
@@ -34,7 +41,8 @@
if(threat_reduction)
GLOB.dynamic_station_traits[src] = threat_reduction
-
+ if(sign_up_button)
+ GLOB.lobby_station_traits += src
if(trait_processes)
START_PROCESSING(SSstation, src)
if(trait_to_give)
@@ -43,14 +51,10 @@
/datum/station_trait/Destroy()
SSstation.station_traits -= src
GLOB.dynamic_station_traits.Remove(src)
+ destroy_lobby_buttons()
return ..()
-/// Proc ran when round starts. Use this for roundstart effects.
-/datum/station_trait/proc/on_round_start()
- SIGNAL_HANDLER
- return
-
-///type of info the centcom report has on this trait, if any.
+/// Returns the type of info the centcom report has on this trait, if any.
/datum/station_trait/proc/get_report()
return "[name] - [report_message]"
@@ -64,7 +68,7 @@
qdel(src)
-///Called by decals if they can be colored, to see if we got some cool colors for them. Only takes the first station trait
+/// Called by decals if they can be colored, to see if we got some cool colors for them. Only takes the first station trait
/proc/request_station_colors(atom/thing_to_color, pattern)
for(var/datum/station_trait/trait in SSstation.station_traits)
var/decal_color = trait.get_decal_color(thing_to_color, pattern || PATTERN_DEFAULT)
@@ -72,6 +76,53 @@
return decal_color
return null
-///Return a color for the decals, if any
+/// Return a color for the decals, if any
/datum/station_trait/proc/get_decal_color(thing_to_color, pattern)
return
+
+/// Return TRUE if we want to show a lobby button, by default we assume we don't want it after the round begins
+/datum/station_trait/proc/can_display_lobby_button()
+ return sign_up_button && !SSticker.HasRoundStarted()
+
+/* SKYRAT EDIT REMOVAL
+/// Apply any additional handling we need to our lobby button
+/datum/station_trait/proc/setup_lobby_button(atom/movable/screen/lobby/button/sign_up/lobby_button)
+ SHOULD_CALL_PARENT(TRUE)
+ lobby_buttons |= lobby_button
+ RegisterSignal(lobby_button, COMSIG_ATOM_UPDATE_ICON, PROC_REF(on_lobby_button_update_icon))
+ RegisterSignal(lobby_button, COMSIG_CLICK, PROC_REF(on_lobby_button_click))
+ RegisterSignal(lobby_button, COMSIG_QDELETING, PROC_REF(on_lobby_button_destroyed))
+ lobby_button.update_appearance(UPDATE_ICON)
+
+/// Called when our lobby button is clicked on
+/datum/station_trait/proc/on_lobby_button_click(atom/movable/screen/lobby/button/sign_up/lobby_button, location, control, params, mob/dead/new_player/user)
+ SIGNAL_HANDLER
+ return
+
+/// Called when our lobby button tries to update its appearance
+/datum/station_trait/proc/on_lobby_button_update_icon(atom/movable/screen/lobby/button/sign_up/lobby_button, updates)
+ SIGNAL_HANDLER
+ return
+
+/// Don't hold references to deleted buttons
+/datum/station_trait/proc/on_lobby_button_destroyed(atom/movable/screen/lobby/button/sign_up/lobby_button)
+ SIGNAL_HANDLER
+ lobby_buttons -= lobby_button
+*/
+
+/// Proc ran when round starts. Use this for roundstart effects. By default we clean up our buttons here.
+/datum/station_trait/proc/on_round_start()
+ SIGNAL_HANDLER
+ destroy_lobby_buttons()
+
+/// Remove all of our active lobby buttons
+/datum/station_trait/proc/destroy_lobby_buttons()
+ for (var/atom/movable/screen/button as anything in lobby_buttons)
+ var/mob/hud_owner = button.get_mob()
+ qdel(button)
+ if (QDELETED(hud_owner))
+ continue
+ var/datum/hud/using_hud = hud_owner.hud_used
+ using_hud?.show_hud(using_hud?.hud_version)
+ lobby_buttons = list()
+
diff --git a/code/datums/station_traits/job_traits.dm b/code/datums/station_traits/job_traits.dm
new file mode 100644
index 00000000000..63351af038c
--- /dev/null
+++ b/code/datums/station_traits/job_traits.dm
@@ -0,0 +1,101 @@
+/**
+ * A station trait which enables a temporary job
+ * Generally speaking these should always all be mutually exclusive, don't have too many at once
+ */
+/datum/station_trait/job
+ sign_up_button = TRUE
+ trait_flags = STATION_TRAIT_ABSTRACT
+ /// What tooltip to show on the button
+ var/button_desc = "Sign up to gain some kind of unusual job, not available in most rounds."
+ /// Type of job to enable
+ var/job_to_add = /datum/job/clown
+ /// Who signed up to this in the lobby
+ var/list/lobby_candidates
+
+/datum/station_trait/job/New()
+ . = ..()
+ blacklist += subtypesof(/datum/station_trait/job) - type // All but ourselves
+ RegisterSignal(SSdcs, COMSIG_GLOB_PRE_JOBS_ASSIGNED, PROC_REF(pre_jobs_assigned))
+
+/* SKYRAT EDIT REMOVAL
+/datum/station_trait/job/setup_lobby_button(atom/movable/screen/lobby/button/sign_up/lobby_button)
+ RegisterSignal(lobby_button, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_lobby_button_update_overlays))
+ lobby_button.desc = button_desc
+ return ..()
+
+
+/datum/station_trait/job/on_lobby_button_click(atom/movable/screen/lobby/button/sign_up/lobby_button, location, control, params, mob/dead/new_player/user)
+ if (LAZYFIND(lobby_candidates, user))
+ LAZYREMOVE(lobby_candidates, user)
+ else
+ LAZYADD(lobby_candidates, user)
+
+
+/datum/station_trait/job/on_lobby_button_update_icon(atom/movable/screen/lobby/button/sign_up/lobby_button, updates)
+ if (LAZYFIND(lobby_candidates, lobby_button.get_mob()))
+ lobby_button.base_icon_state = "signup_on"
+ else
+ lobby_button.base_icon_state = "signup"
+
+/// Add an overlay based on whether you are actively signed up for this role
+/datum/station_trait/job/proc/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays)
+ SIGNAL_HANDLER
+ overlays += LAZYFIND(lobby_candidates, lobby_button.get_mob()) ? "tick" : "cross"
+*/
+
+/// Called before we start assigning roles, assign ours first
+/datum/station_trait/job/proc/pre_jobs_assigned()
+ SIGNAL_HANDLER
+ sign_up_button = FALSE
+ destroy_lobby_buttons()
+ for (var/mob/dead/new_player/signee as anything in lobby_candidates)
+ if (isnull(signee) || !signee.client || !signee.mind || signee.ready != PLAYER_READY_TO_PLAY)
+ LAZYREMOVE(lobby_candidates, signee)
+ if (!LAZYLEN(lobby_candidates))
+ on_failed_assignment()
+ return // Nobody signed up :(
+ var/mob/dead/new_player/picked_player = pick(lobby_candidates)
+ picked_player.mind.assigned_role = new job_to_add()
+ lobby_candidates = null
+
+/// Called if we didn't assign a role before the round began, we add it to the latejoin menu instead
+/datum/station_trait/job/proc/on_failed_assignment()
+ var/datum/job/our_job = job_to_add
+ our_job = SSjob.GetJob(our_job::title)
+ our_job.total_positions++
+
+
+/// Adds a gorilla to the cargo department, replacing the sloth and the mech
+/datum/station_trait/job/cargorilla
+ name = "Cargo Gorilla"
+ button_desc = "Sign up to become the Cargo Gorilla, a peaceful shepherd of boxes."
+ weight = 1
+ show_in_report = FALSE // Selective attention test. Did you spot the gorilla?
+ job_to_add = /datum/job/cargo_gorilla
+ trait_flags = STATION_TRAIT_MAP_UNRESTRICTED
+
+/datum/station_trait/job/cargorilla/New()
+ . = ..()
+ RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(replace_cargo))
+
+/* SKYRAT EDIT REMOVAL
+/datum/station_trait/job/cargorilla/on_lobby_button_update_overlays(atom/movable/screen/lobby/button/sign_up/lobby_button, list/overlays)
+ . = ..()
+ overlays += LAZYFIND(lobby_candidates, lobby_button.get_mob()) ? "gorilla_on" : "gorilla_off"
+*/
+
+/// Remove the cargo equipment and personnel that are being replaced by a gorilla.
+/datum/station_trait/job/cargorilla/proc/replace_cargo(datum/source)
+ SIGNAL_HANDLER
+ var/mob/living/basic/sloth/cargo_sloth = GLOB.cargo_sloth
+ if(isnull(cargo_sloth))
+ lobby_candidates = list()
+ destroy_lobby_buttons() // Sorry folks
+ sign_up_button = FALSE
+ return
+
+ // hmm our sloth looks funny today
+ qdel(cargo_sloth)
+ // monkey carries the crates, the age of robot is over
+ if(GLOB.cargo_ripley)
+ qdel(GLOB.cargo_ripley)
diff --git a/code/datums/station_traits/negative_traits.dm b/code/datums/station_traits/negative_traits.dm
index 5c37fd7ede5..8d0583c9d75 100644
--- a/code/datums/station_traits/negative_traits.dm
+++ b/code/datums/station_traits/negative_traits.dm
@@ -123,7 +123,7 @@
/datum/station_trait/overflow_job_bureaucracy/proc/set_overflow_job_override(datum/source)
SIGNAL_HANDLER
- var/datum/job/picked_job = pick(SSjob.joinable_occupations)
+ var/datum/job/picked_job = pick(SSjob.get_valid_overflow_jobs())
chosen_job_name = lowertext(picked_job.title) // like Chief Engineers vs like chief engineers
SSjob.set_overflow_role(picked_job.type)
diff --git a/code/datums/station_traits/neutral_traits.dm b/code/datums/station_traits/neutral_traits.dm
index f0b2c625537..5e864738f8e 100644
--- a/code/datums/station_traits/neutral_traits.dm
+++ b/code/datums/station_traits/neutral_traits.dm
@@ -141,60 +141,6 @@
var/new_colored_assistant_type = pick(subtypesof(/datum/colored_assistant) - get_configured_colored_assistant_type())
GLOB.colored_assistant = new new_colored_assistant_type
-/datum/station_trait/cargorilla
- name = "Cargo Gorilla"
- trait_type = STATION_TRAIT_NEUTRAL
- weight = 1
- show_in_report = FALSE // Selective attention test. Did you spot the gorilla?
-
- /// The gorilla we created, we only hold this ref until the round starts.
- var/mob/living/basic/gorilla/cargorilla/cargorilla
-
-/datum/station_trait/cargorilla/New()
- . = ..()
- RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(replace_cargo))
-
-/// Replace some cargo equipment and 'personnel' with a gorilla.
-/datum/station_trait/cargorilla/proc/replace_cargo(datum/source)
- SIGNAL_HANDLER
-
- var/mob/living/basic/sloth/cargo_sloth = GLOB.cargo_sloth
- if(isnull(cargo_sloth))
- return
-
- cargorilla = new(cargo_sloth.loc)
- cargorilla.name = cargo_sloth.name
- // We do a poll on roundstart, don't let ghosts in early
- INVOKE_ASYNC(src, PROC_REF(make_id_for_gorilla))
- // hm our sloth looks funny today
- qdel(cargo_sloth)
-
- // monkey carries the crates, the age of robot is over
- if(GLOB.cargo_ripley)
- qdel(GLOB.cargo_ripley)
-
-/// Makes an ID card for the gorilla
-/datum/station_trait/cargorilla/proc/make_id_for_gorilla()
- var/obj/item/card/id/advanced/cargo_gorilla/gorilla_id = new(cargorilla.loc)
- gorilla_id.registered_name = cargorilla.name
- gorilla_id.update_label()
-
- cargorilla.put_in_hands(gorilla_id, del_on_fail = TRUE)
-
-/datum/station_trait/cargorilla/on_round_start()
- if(!cargorilla)
- return
-
- addtimer(CALLBACK(src, PROC_REF(get_ghost_for_gorilla), cargorilla), 12 SECONDS) // give ghosts a bit of time to funnel in
- cargorilla = null
-
-/// Get us a ghost for the gorilla.
-/datum/station_trait/cargorilla/proc/get_ghost_for_gorilla(mob/living/basic/gorilla/cargorilla/gorilla)
- if(QDELETED(gorilla))
- return
-
- gorilla.poll_for_gorilla()
-
/datum/station_trait/birthday
name = "Employee Birthday"
trait_type = STATION_TRAIT_NEUTRAL
diff --git a/code/modules/client/preferences/middleware/jobs.dm b/code/modules/client/preferences/middleware/jobs.dm
index d61fb53c98f..cee99bacec4 100644
--- a/code/modules/client/preferences/middleware/jobs.dm
+++ b/code/modules/client/preferences/middleware/jobs.dm
@@ -53,6 +53,8 @@
var/list/jobs = list()
for (var/datum/job/job as anything in SSjob.joinable_occupations)
+ if (job.job_flags & JOB_LATEJOIN_ONLY)
+ continue
var/datum/job_department/department_type = job.department_for_prefs || job.departments_list?[1]
if (isnull(department_type))
stack_trace("[job] does not have a department set, yet is a joinable occupation!")
@@ -118,6 +120,8 @@
var/list/job_required_experience = list()
for (var/datum/job/job as anything in SSjob.all_occupations)
+ if (job.job_flags & JOB_LATEJOIN_ONLY)
+ continue
var/required_playtime_remaining = job.required_playtime_remaining(user.client)
if (required_playtime_remaining)
job_required_experience[job.title] = list(
diff --git a/code/modules/events/bureaucratic_error.dm b/code/modules/events/bureaucratic_error.dm
index add5ad58b4a..0e44ac391c1 100644
--- a/code/modules/events/bureaucratic_error.dm
+++ b/code/modules/events/bureaucratic_error.dm
@@ -13,21 +13,17 @@
priority_announce("A recent bureaucratic error in the Organic Resources Department may result in personnel shortages in some departments and redundant staffing in others.", "Paperwork Mishap Alert")
/datum/round_event/bureaucratic_error/start()
- var/list/jobs = SSjob.joinable_occupations.Copy()
- /*
- if(prob(33)) // Only allows latejoining as a single role. Add latejoin AI bluespace pods for fun later.
+ var/list/jobs = SSjob.get_valid_overflow_jobs()
+ /* SKYRAT EDIT REMOVAL START
+ if(prob(33)) // Only allows latejoining as a single role.
var/datum/job/overflow = pick_n_take(jobs)
overflow.spawn_positions = -1
overflow.total_positions = -1 // Ensures infinite slots as this role. Assistant will still be open for those that cant play it.
for(var/job in jobs)
var/datum/job/current = job
- if(!current.allow_bureaucratic_error)
- continue
current.total_positions = 0
- else // Adds/removes a random amount of job slots from all jobs.
+ return
*/ // SKYRAT EDIT REMOVAL - no more locking off jobs
+ // Adds/removes a random amount of job slots from all jobs.
for(var/datum/job/current as anything in jobs)
- if(!current.allow_bureaucratic_error)
- continue
- var/ran = rand(1,4) // SKYRAT EDIT - no more locking off jobs
- current.total_positions = max(current.total_positions + ran, 0)
+ current.total_positions = max(current.total_positions + rand(-2,4), 1) // SKYRAT EDIT CHANGE - No more locking off jobs - ORIGINAL: current.total_positions = max(current.total_positions + rand(-2,4), 0)
diff --git a/code/modules/jobs/job_types/cargo_gorilla.dm b/code/modules/jobs/job_types/cargo_gorilla.dm
new file mode 100644
index 00000000000..96a79e3a4d3
--- /dev/null
+++ b/code/modules/jobs/job_types/cargo_gorilla.dm
@@ -0,0 +1,50 @@
+/datum/job/cargo_gorilla
+ title = JOB_CARGO_GORILLA
+ description = "Assist the supply department by moving freight and disposing of unwanted fruits."
+ department_head = list(JOB_QUARTERMASTER)
+ faction = FACTION_STATION
+ total_positions = 0
+ spawn_positions = 0
+ supervisors = SUPERVISOR_QM
+ spawn_type = /mob/living/basic/gorilla/cargorilla
+ config_tag = "CARGO_GORILLA"
+ random_spawns_possible = FALSE
+ display_order = JOB_DISPLAY_ORDER_CARGO_GORILLA
+ departments_list = list(/datum/job_department/cargo)
+ mail_goodies = list(
+ /obj/item/food/grown/banana = 1,
+ )
+ rpg_title = "Beast of Burden"
+ allow_bureaucratic_error = FALSE
+ job_flags = JOB_ANNOUNCE_ARRIVAL | JOB_NEW_PLAYER_JOINABLE | JOB_EQUIP_RANK | JOB_CANNOT_OPEN_SLOTS | JOB_HIDE_WHEN_EMPTY | JOB_LATEJOIN_ONLY
+
+/datum/job/cargo_gorilla/get_roundstart_spawn_point()
+ if (length(GLOB.gorilla_start))
+ return pick(GLOB.gorilla_start)
+ return ..()
+
+/datum/job/cargo_gorilla/get_spawn_mob(client/player_client, atom/spawn_point)
+ if (!player_client)
+ return
+ var/mob/living/the_big_man = new spawn_type(get_turf(spawn_point))
+ the_big_man.fully_replace_character_name(the_big_man.real_name, pick(GLOB.cargorilla_names))
+ return the_big_man
+
+/datum/job/cargo_gorilla/after_spawn(mob/living/spawned, client/player_client)
+ . = ..()
+ // Gorilla with a wage, what's he buyin?
+ var/datum/bank_account/bank_account = new(spawned.real_name, src)
+ bank_account.payday(STARTING_PAYCHECKS, TRUE)
+ bank_account.replaceable = FALSE
+ spawned.add_mob_memory(/datum/memory/key/account, remembered_id = bank_account.account_id)
+
+ var/obj/item/card/id/advanced/cargo_gorilla/gorilla_id = new(spawned.loc)
+ gorilla_id.registered_name = spawned.name
+ gorilla_id.update_label()
+ gorilla_id.registered_account = bank_account
+ bank_account.bank_cards += gorilla_id
+ spawned.put_in_hands(gorilla_id, del_on_fail = TRUE)
+
+ to_chat(spawned, span_boldnotice("You are Cargorilla, a pacifist friend of the station and carrier of freight."))
+ to_chat(spawned, span_notice("You can pick up crates by clicking on them, and drop them by clicking on the ground."))
+ spawned.mind.special_role = "Cargorilla"
diff --git a/code/modules/mob/dead/new_player/latejoin_menu.dm b/code/modules/mob/dead/new_player/latejoin_menu.dm
index 059a5014d0c..d2db3aa34b7 100644
--- a/code/modules/mob/dead/new_player/latejoin_menu.dm
+++ b/code/modules/mob/dead/new_player/latejoin_menu.dm
@@ -80,6 +80,8 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new)
)
if(job_availability != JOB_AVAILABLE)
+ if (job_datum.job_flags & JOB_HIDE_WHEN_EMPTY)
+ continue
job_data["unavailable_reason"] = get_job_unavailable_error_message(job_availability, job_datum.title)
if(job_datum.total_positions < 0)
@@ -95,6 +97,7 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new)
/datum/latejoin_menu/ui_static_data(mob/user)
var/list/departments = list()
+ var/mob/dead/new_player/owner = user
for(var/datum/job_department/department as anything in SSjob.joinable_departments)
var/list/department_jobs = list()
@@ -108,6 +111,8 @@ GLOBAL_DATUM_INIT(latejoin_menu, /datum/latejoin_menu, new)
//Jobs under multiple departments should only be displayed if this is their first department or the command department
if(LAZYLEN(job_datum.departments_list) > 1 && job_datum.departments_list[1] != department.type && !(job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND))
continue
+ if((job_datum.job_flags & JOB_HIDE_WHEN_EMPTY) && owner.IsJobUnavailable(job_datum.title, latejoin = TRUE) != JOB_AVAILABLE)
+ continue
var/list/job_data = list(
"command" = !!(job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND),
diff --git a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
index 1e17938c5fd..64cff6780df 100644
--- a/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
+++ b/code/modules/mob/living/basic/farm_animals/gorilla/gorilla.dm
@@ -158,31 +158,4 @@
ADD_TRAIT(src, TRAIT_PACIFISM, INNATE_TRAIT)
AddComponent(/datum/component/crate_carrier)
-/mob/living/basic/gorilla/cargorilla/death(gibbed)
- var/datum/component/potential_component = GetComponent(/datum/component/ghost_direct_control)
- if(!QDELETED(potential_component))
- qdel(potential_component)
- return ..()
-
-/**
- * Poll ghosts for control of the gorilla. Not added in init because we only want to poll when the round starts.
- * Preferably in future we can replace this with a popup on the lobby to queue to become a gorilla.
- */
-/mob/living/basic/gorilla/cargorilla/proc/poll_for_gorilla()
- AddComponent(\
- /datum/component/ghost_direct_control,\
- poll_candidates = TRUE,\
- poll_length = 30 SECONDS,\
- role_name = "Cargorilla",\
- assumed_control_message = "You are Cargorilla, a pacifist friend of the station and carrier of freight.",\
- poll_ignore_key = POLL_IGNORE_CARGORILLA,\
- after_assumed_control = CALLBACK(src, PROC_REF(became_player_controlled)),\
- )
-
-/// Called once a ghost assumes control
-/mob/living/basic/gorilla/cargorilla/proc/became_player_controlled()
- mind.set_assigned_role(SSjob.GetJobType(/datum/job/cargo_technician))
- mind.special_role = "Cargorilla"
- to_chat(src, span_notice("You can pick up crates by clicking on them, and drop them by clicking on the ground."))
-
#undef GORILLA_HANDS_LAYER
diff --git a/code/modules/mob/living/basic/pets/sloth.dm b/code/modules/mob/living/basic/pets/sloth.dm
index 0b1546ccf93..125a3a7b97d 100644
--- a/code/modules/mob/living/basic/pets/sloth.dm
+++ b/code/modules/mob/living/basic/pets/sloth.dm
@@ -44,11 +44,12 @@ GLOBAL_DATUM(cargo_sloth, /mob/living/basic/sloth)
AddElement(/datum/element/ai_retaliate)
AddComponent(/datum/component/tree_climber)
- if(!mapload || isnull(GLOB.cargo_sloth) || !is_station_level(z))
+ if(!mapload || !isnull(GLOB.cargo_sloth) || !is_station_level(z))
return
// If someone adds non-cargo sloths to maps we'll have a problem but we're fine for now
GLOB.cargo_sloth = src
+ GLOB.gorilla_start += get_turf(src)
/mob/living/basic/sloth/Destroy()
if(GLOB.cargo_sloth == src)
diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm
index 49539920920..6d6505d1dc5 100644
--- a/code/modules/tooltip/tooltip.dm
+++ b/code/modules/tooltip/tooltip.dm
@@ -107,22 +107,22 @@ Notes:
//Open a tooltip for user, at a location based on params
//Theme is a CSS class in tooltip.html, by default this wrapper chooses a CSS class based on the user's UI_style (Midnight, Plasmafire, Retro, etc)
//Includes sanity.checks
-/proc/openToolTip(mob/user = null, atom/movable/tip_src = null, params = null,title = "",content = "",theme = "")
- if(istype(user))
- if(user.client && user.client.tooltips)
- var/ui_style = user.client?.prefs?.read_preference(/datum/preference/choiced/ui_style)
- if(!theme && ui_style)
- theme = lowertext(ui_style)
- if(!theme)
- theme = "default"
- user.client.tooltips.show(tip_src, params,title,content,theme)
+/proc/openToolTip(mob/user = null, atom/movable/tip_src = null, params = null, title = "", content = "", theme = "")
+ if(!istype(user) || !user.client?.tooltips)
+ return
+ var/ui_style = user.client?.prefs?.read_preference(/datum/preference/choiced/ui_style)
+ if(!theme && ui_style)
+ theme = lowertext(ui_style)
+ if(!theme)
+ theme = "default"
+ user.client.tooltips.show(tip_src, params, title, content, theme)
//Arbitrarily close a user's tooltip
//Includes sanity checks.
/proc/closeToolTip(mob/user)
- if(istype(user))
- if(user.client && user.client.tooltips)
- user.client.tooltips.hide()
+ if(!istype(user) || !user.client?.tooltips)
+ return
+ user.client.tooltips.hide()
diff --git a/icons/hud/lobby/signup_button.dmi b/icons/hud/lobby/signup_button.dmi
new file mode 100644
index 00000000000..4ff31a316c2
Binary files /dev/null and b/icons/hud/lobby/signup_button.dmi differ
diff --git a/modular_skyrat/modules/title_screen/code/job_traits.dm b/modular_skyrat/modules/title_screen/code/job_traits.dm
new file mode 100644
index 00000000000..2bbf74a4081
--- /dev/null
+++ b/modular_skyrat/modules/title_screen/code/job_traits.dm
@@ -0,0 +1,2 @@
+/datum/station_trait/proc/on_lobby_button_click(mob/dead/new_player/user)
+ return
diff --git a/modular_skyrat/modules/title_screen/code/new_player.dm b/modular_skyrat/modules/title_screen/code/new_player.dm
index 33e8a964695..0841d6c0700 100644
--- a/modular_skyrat/modules/title_screen/code/new_player.dm
+++ b/modular_skyrat/modules/title_screen/code/new_player.dm
@@ -17,6 +17,11 @@
make_me_an_observer()
return
+ if(href_list["job_traits"])
+ play_lobby_button_sound()
+ show_job_traits()
+ return
+
if(href_list["server_swap"])
play_lobby_button_sound()
server_swap()
@@ -136,6 +141,28 @@
to_chat_immediate(src, "So long, spaceman.")
src.client << link(server_ip)
+/**
+ * Shows currently available job traits
+ */
+/mob/dead/new_player/proc/show_job_traits()
+ if (!client || client.interviewee)
+ return
+ if(!length(GLOB.lobby_station_traits))
+ to_chat(src, span_warning("There are currently no job traits available!"))
+ return
+ var/list/available_lobby_station_traits = list()
+ for (var/datum/station_trait/trait as anything in GLOB.lobby_station_traits)
+ if (!trait.can_display_lobby_button())
+ continue
+ available_lobby_station_traits += trait
+
+ var/datum/station_trait/clicked_trait = tgui_input_list(src, "Select a job trait to sign up for:", "Job Traits", available_lobby_station_traits)
+
+ if(!clicked_trait)
+ return
+
+ clicked_trait.on_lobby_button_click(src)
+
/**
* Shows the player a list of current polls, if any.
*/
diff --git a/modular_skyrat/modules/title_screen/code/station_traits.dm b/modular_skyrat/modules/title_screen/code/station_traits.dm
new file mode 100644
index 00000000000..a42bd11ecd2
--- /dev/null
+++ b/modular_skyrat/modules/title_screen/code/station_traits.dm
@@ -0,0 +1,7 @@
+/datum/station_trait/job/on_lobby_button_click(mob/dead/new_player/user)
+ if (LAZYFIND(lobby_candidates, user))
+ LAZYREMOVE(lobby_candidates, user)
+ to_chat(user, span_redtext("You have been removed from the [name] list of candidates."))
+ else
+ LAZYADD(lobby_candidates, user)
+ to_chat(user, span_greentext("You have been added to the [name] list of candidates."))
diff --git a/modular_skyrat/modules/title_screen/code/title_screen_html.dm b/modular_skyrat/modules/title_screen/code/title_screen_html.dm
index 30f3f2949e2..2c1d3075586 100644
--- a/modular_skyrat/modules/title_screen/code/title_screen_html.dm
+++ b/modular_skyrat/modules/title_screen/code/title_screen_html.dm
@@ -118,6 +118,9 @@ GLOBAL_LIST_EMPTY(startup_messages)
"}
+ if(length(GLOB.lobby_station_traits) && !SSticker.HasRoundStarted())
+ dat += {""}
+
if(!is_guest_key(src.key))
dat += playerpolls()
diff --git a/modular_skyrat/modules/title_screen/code/title_screen_subsystem.dm b/modular_skyrat/modules/title_screen/code/title_screen_subsystem.dm
index 3c73ee456a2..0cd00ee82fa 100644
--- a/modular_skyrat/modules/title_screen/code/title_screen_subsystem.dm
+++ b/modular_skyrat/modules/title_screen/code/title_screen_subsystem.dm
@@ -23,6 +23,8 @@ SUBSYSTEM_DEF(title)
var/list/progress_json = list()
/// The reference realtime that we're treating as 0 for this run
var/progress_reference_time = 0
+ /// A list of station traits that have lobby buttons
+ var/list/available_lobby_station_traits = list()
/datum/controller/subsystem/title/Initialize()
var/dat
diff --git a/strings/names/cargorilla.txt b/strings/names/cargorilla.txt
new file mode 100644
index 00000000000..ec135f5ca4d
--- /dev/null
+++ b/strings/names/cargorilla.txt
@@ -0,0 +1,7 @@
+Cala
+Cerchak
+Citrus
+Coco
+Grodd
+Paperwork
+Winston
diff --git a/tgstation.dme b/tgstation.dme
index 85913d6ed63..e492dafefe8 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1813,6 +1813,7 @@
#include "code\datums\skills\mining.dm"
#include "code\datums\station_traits\_station_trait.dm"
#include "code\datums\station_traits\admin_panel.dm"
+#include "code\datums\station_traits\job_traits.dm"
#include "code\datums\station_traits\negative_traits.dm"
#include "code\datums\station_traits\neutral_traits.dm"
#include "code\datums\station_traits\positive_traits.dm"
@@ -4208,6 +4209,7 @@
#include "code\modules\jobs\job_types\bartender.dm"
#include "code\modules\jobs\job_types\botanist.dm"
#include "code\modules\jobs\job_types\captain.dm"
+#include "code\modules\jobs\job_types\cargo_gorilla.dm"
#include "code\modules\jobs\job_types\cargo_technician.dm"
#include "code\modules\jobs\job_types\chemist.dm"
#include "code\modules\jobs\job_types\chief_engineer.dm"
@@ -7975,7 +7977,9 @@
#include "modular_skyrat\modules\time_clock\code\mind.dm"
#include "modular_skyrat\modules\time_clock\code\off_duty_component.dm"
#include "modular_skyrat\modules\title_screen\code\_title_screen_defines.dm"
+#include "modular_skyrat\modules\title_screen\code\job_traits.dm"
#include "modular_skyrat\modules\title_screen\code\new_player.dm"
+#include "modular_skyrat\modules\title_screen\code\station_traits.dm"
#include "modular_skyrat\modules\title_screen\code\title_screen_controls.dm"
#include "modular_skyrat\modules\title_screen\code\title_screen_html.dm"
#include "modular_skyrat\modules\title_screen\code\title_screen_pref_middleware.dm"
diff --git a/tgui/packages/tgui/interfaces/common/JobToIcon.ts b/tgui/packages/tgui/interfaces/common/JobToIcon.ts
index 87245260bed..75db6d4e678 100644
--- a/tgui/packages/tgui/interfaces/common/JobToIcon.ts
+++ b/tgui/packages/tgui/interfaces/common/JobToIcon.ts
@@ -12,6 +12,7 @@ const BASEICONS = {
Bitrunner: 'gamepad',
Botanist: 'seedling',
Captain: 'crown',
+ 'Cargo Gorilla': 'paw',
'Cargo Technician': 'box',
'CentCom Commander': 'star',
'CentCom Head Intern': 'pen-fancy',