From 286b651f621c0f4d6480464af08ab56c747f0846 Mon Sep 17 00:00:00 2001 From: Dahlular Date: Mon, 11 Apr 2022 06:22:01 -0600 Subject: [PATCH] Timeclock; need TGUI updates --- code/game/machinery/announcement_system.dm | 7 + config/config.txt | 2 + config/hyperstation/jobs.txt | 12 + hyperstation/code/__DEFINES/misc.dm | 11 + .../configuration/entries/timeoff.dm | 11 + .../code/controllers/subsystem/job.dm | 18 + .../game/machinery/announcement_system.dm | 3 + .../code/game/machinery/computer/timeclock.dm | 310 ++++++++++++++++++ .../code/game/objects/items/cards_ids.dm | 2 + .../circuitboards/computer_circuitboards.dm | 3 + .../code/modules/client/client_defines.dm | 9 + .../code/modules/jobs/job_types/_job.dm | 3 + .../icons/obj/machinery/timeclock.dmi | Bin 0 -> 5646 bytes tgstation.dme | 9 + tgui/packages/tgui/interfaces/TimeClock.js | 144 ++++++++ .../tgui/interfaces/common/RankIcon.js | 119 +++++++ 16 files changed, 663 insertions(+) create mode 100644 config/hyperstation/jobs.txt create mode 100644 hyperstation/code/__DEFINES/misc.dm create mode 100644 hyperstation/code/controllers/configuration/entries/timeoff.dm create mode 100644 hyperstation/code/controllers/subsystem/job.dm create mode 100644 hyperstation/code/game/machinery/announcement_system.dm create mode 100644 hyperstation/code/game/machinery/computer/timeclock.dm create mode 100644 hyperstation/code/game/objects/items/cards_ids.dm create mode 100644 hyperstation/code/game/objects/items/circuitboards/computer_circuitboards.dm create mode 100644 hyperstation/code/modules/client/client_defines.dm create mode 100644 hyperstation/code/modules/jobs/job_types/_job.dm create mode 100644 hyperstation/icons/obj/machinery/timeclock.dmi create mode 100644 tgui/packages/tgui/interfaces/TimeClock.js create mode 100644 tgui/packages/tgui/interfaces/common/RankIcon.js diff --git a/code/game/machinery/announcement_system.dm b/code/game/machinery/announcement_system.dm index b521eb153..5ace33c71 100644 --- a/code/game/machinery/announcement_system.dm +++ b/code/game/machinery/announcement_system.dm @@ -102,6 +102,13 @@ GLOBAL_LIST_EMPTY(announcement_systems) else if(message_type == "ARRIVALS_BROKEN") message = "The arrivals shuttle has been damaged. Docking for repairs..." + //Hyper edit + else if (message_type == "ONDUTY") + message = CompileText(onduty, user, rank) + else if (message_type == "OFFDUTY") + message = CompileText(offduty, user, rank) + //Hyper edit end + if(channels.len == 0) radio.talk_into(src, message, null) else diff --git a/config/config.txt b/config/config.txt index a5720a805..a481ec955 100644 --- a/config/config.txt +++ b/config/config.txt @@ -5,6 +5,8 @@ $include dbconfig.txt $include comms.txt $include antag_rep.txt $include donator_groupings.txt +# Hyperstation Configs... +$include hyperstation/jobs.txt # You can use the @ character at the beginning of a config option to lock it from being edited in-game # Example usage: diff --git a/config/hyperstation/jobs.txt b/config/hyperstation/jobs.txt new file mode 100644 index 000000000..f9353be69 --- /dev/null +++ b/config/hyperstation/jobs.txt @@ -0,0 +1,12 @@ +# Allow time off to be enabled +## Uncomment to allow players to use their accrued time off +## Defaults to off because this requires the database unless you want them to +## use only what they accrued during the current round +#TIME_OFF + +# Allow PTO job changes +## Currently this is required if using the previous to allow users to change +## jobs. This may be changed in the future and can be used to expand the functionality +## for something such as on-the-go job changing within the department, without heads. +## Thus it remains here. +#PTO_JOB_CHANGE \ No newline at end of file diff --git a/hyperstation/code/__DEFINES/misc.dm b/hyperstation/code/__DEFINES/misc.dm new file mode 100644 index 000000000..245bcc21e --- /dev/null +++ b/hyperstation/code/__DEFINES/misc.dm @@ -0,0 +1,11 @@ +/////////////// +// PTO Types // +/////////////// +#define PTO_COMMAND "Command" +#define PTO_SECURITY "Security" +#define PTO_MEDICAL "Medical" +#define PTO_ENGINEERING "Engineering" +#define PTO_SCIENCE "Science" +#define PTO_CARGO "Cargo" +#define PTO_CIVILIAN "Civilian" +#define PTO_CYBORG "Silicon" diff --git a/hyperstation/code/controllers/configuration/entries/timeoff.dm b/hyperstation/code/controllers/configuration/entries/timeoff.dm new file mode 100644 index 000000000..04127ed37 --- /dev/null +++ b/hyperstation/code/controllers/configuration/entries/timeoff.dm @@ -0,0 +1,11 @@ +/// Toggles time off for jobs +/datum/config_entry/flag/time_off + +/// Toggles changing jobs with PTO +/datum/config_entry/flag/pto_job_change + +/// PTO hour cap +/datum/config_entry/number/pto_cap + config_entry_value = 100 /// Total hours + min_val = 0 /// Minimum hourly value + integer = FALSE /// We can be a float for partial hours diff --git a/hyperstation/code/controllers/subsystem/job.dm b/hyperstation/code/controllers/subsystem/job.dm new file mode 100644 index 000000000..998f228f8 --- /dev/null +++ b/hyperstation/code/controllers/subsystem/job.dm @@ -0,0 +1,18 @@ +/** + * Some modifications to the jobs subsystem, courtesy of SandPoot. + * If we pull from upstream we should have this somewhere at some point, so it's shoehorned + * in here for the moment. + * Insert unhelpful comment here. + */ + +/datum/controller/subsystem/job/proc/get_job_name(job_name) + if (!job_name) + return "Unknown" // Invalid job + //var/all_alt_titles = get_all_alt_titles() + //if (job_name in all_alt_titles) // Check if the name is an alt title + //return get_job_name(all_alt_titles[job_name]) // Locate the original job title and return it + if (job_name in get_all_job_icons()) // Check if the job has a HUD icon + return job_name + if (job_name in get_all_centcom_jobs()) // Return the NT logo if it is a CentCom job + return "CentCom" + return "Unknown" // Return unknown if none of the above apply diff --git a/hyperstation/code/game/machinery/announcement_system.dm b/hyperstation/code/game/machinery/announcement_system.dm new file mode 100644 index 000000000..63cbd87de --- /dev/null +++ b/hyperstation/code/game/machinery/announcement_system.dm @@ -0,0 +1,3 @@ +/obj/machinery/announcement_system + var/onduty = "%PERSON has moved On-Duty as %RANK." + var/offduty = "%PERSON, %RANK, has moved Off-Duty." diff --git a/hyperstation/code/game/machinery/computer/timeclock.dm b/hyperstation/code/game/machinery/computer/timeclock.dm new file mode 100644 index 000000000..4801f2bd1 --- /dev/null +++ b/hyperstation/code/game/machinery/computer/timeclock.dm @@ -0,0 +1,310 @@ +/// Timeclock terminal, ported from VOREStation +/obj/machinery/computer/timeclock + name = "timeclock terminal" // Name of the object + icon = 'hyperstation/icons/obj/machinery/timeclock.dmi' // Spritesheet for the object's icon + icon_state = "timeclock" // Icon state from the spritesheet + icon_keyboard = null // Keyboard state because this is a computer and we need to tell it to not have a keyboard + light_color = "#0099ff" // TODO: Adjust this // Color for the light coming from the object + light_power = 0.5 // Brightness of the light coming from the object + layer = ABOVE_WINDOW_LAYER // Layer for the object + density = FALSE // Density of the object, so we can walk through it + circuit = /obj/item/circuitboard/computer/timeclock // Circuitboard for the object in case it gets destroyed + + var/obj/item/card/id/card // Inserted ID card + +/// For when we create a new timeclock +/obj/machinery/computer/timeclock/New() + ..() // Let's do this just to be safe. + +// For when a timeclock is destroyed +/obj/machinery/computer/timeclock/Destroy() + if (card) // If we're holding an ID and get destroyed + card.forceMove(get_turf(src)) // Get rid of the fucker + card = null // And make sure we know it's gone + . = ..() // Someone's gonna ask later, refer them here. + // This shit just sets our return value to our parent proc's return value. + +// Determines what icon to used based on a couple factors +/obj/machinery/computer/timeclock/update_icon() + if (!process()) // If we can't process + icon_state = "[initial(icon_state)]_off" // we must be offline. + else if (card) // If we have an ID + icon_state = "[initial(icon_state)]_card" // display it! + else // All else fails? + icon_state = "[initial(icon_state)]" // We're just a clock. + +/// Allows the timeclock to update its icon and lighting on power change, should power go out +/obj/machinery/computer/timeclock/power_change() + var/old_stat = stat // Save our old stats for later + ..() // Call the parent proc to handle the actual powernet shit + if (old_stat != stat) // If our stat changed + update_icon() // update our icon + if (stat & NOPOWER) // If we no longer have power + set_light(0) // turn off our lights + else // Otherwise + set_light(brightness_on) // turn on our lights + +/// Handle clicking with an object +/obj/machinery/computer/timeclock/attackby(obj/I, mob/user) + if (istype(I, /obj/item/card/id)) // If the user clicked with an ID in hand + if (!card && user.canUnEquip(I)) // Check if we already have an ID and that the user can drop the ID + I.forceMove(src) // Move the ID into our own location + card = I // Set our card to the ID + SStgui.update_uis(src) // Update all open UIs for us + update_icon() // Update our icon to reflect the new ID + else if (card) // There must've already been an ID inserted + to_chat(user, "There is already an ID card inside.") + return // Quit doing shit so we don't hit the timeclock + . = ..() // Set our return value to that of the parent proc + +/// Handle user clicking +/obj/machinery/computer/timeclock/attack_hand(var/mob/user as mob) + if (..()) // If for some reason the parent proc returns true + return // We won't do anything + user.set_machine(src) // Otherwise, set the mob's machine to us + ui_interact(user) // Provide a UI to the user + +/// Handle UI interactions with this arcane shit known as tee gee yew eye +/obj/machinery/computer/timeclock/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) // Attempt to update the UI + if (!ui) // If the UI doesn't exist + ui = new(user, src, "TimeClock", name) // Make a new one + ui.open() // And open it for the user + +/// Handle UI data +/obj/machinery/computer/timeclock/ui_data(mob/user) + var/list/data = ..() // Call the parent object's data proc and assign that to the data list + + // Data for showing user's own PTO + if (user.client) // If the client exists + data["department_hours"] = \ + SANITIZE_LIST(user.client.department_hours) // Add the department hours list into data + data["user_name"] = "[user]" // Set the username to the user + + // Data about the card we put into the timeclock + data["card"] = null // Add card data, + data["assignment"] = null // Assignment data, + data["job_datum"] = null // The job datum, + data["allow_change_job"] = null // Whether or not we can change jobs, + data["job_choices"] = null // And the possible jobs + if (card) // If we have a card + data["card"] = "[card]" // Set card data, + data["assignment"] = card.assignment // Assignment data, + var/datum/job/job = \ + SSjob.GetJob(SSjob.get_job_name(card.assignment)) // Create a new job datum, + if (job) // If the job exists + data["job_datum"] = list( // Set job datum to a list with + "title" = job.title, // The job title, + "departments" = \ + flags_to_english(job.department_flag, job.flag), // The job department, + "selection_color" = job.selection_color, // The selection color, + "timeoff_factor" = job.timeoff_factor, // The timeoff factor, + "pto_department" = job.pto_type // And the PTO type + ) + if (CONFIG_GET(flag/time_off) \ + && CONFIG_GET(flag/pto_job_change)) // If we allow timeoff and job changing + data["allow_change_job"] = TRUE // Add that to the data + if (job && job.timeoff_factor < 0) // They're off duty so we have to lookup available jobs + data["job_choices"] = \ + get_open_on_duty_jobs(user, job.pto_type) // Set the job choices + + return data // Give back the data list for subprocs + +/// The user interacted with me? owo! +/obj/machinery/computer/timeclock/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if (..()) // Check if the parent UI had anything to say + return TRUE // If so, we don't care to do anything + + add_fingerprint(usr) // Add the user's fingerprint to the list so sec can find them + + switch (action) // Check the action completed + if ("id") // If the ID slot was clicked + if (card) // Check if we have an ID already + usr.put_in_hands(card) // If so, give the user the card + card = null // And make sure we don't have it + else // Otherwise + var/obj/item/I = usr.get_active_held_item() // Get the item in their active hand + if (istype(I, /obj/item/card/id) && usr.canUnEquip(I)) // Check if it's an ID and they can unequip the ID + I.forceMove(src) // If so, move it into us + card = I // And make sure we know we have it + update_icon() // Update our icon too because we did something + return TRUE // And stop handling any UI in this run + if ("switch-to-on-duty-rank") // If they switched to on-duty + if (check_face()) // Check that their face is visible + if (check_card_cooldown()) // And their card isn't on cooldown + make_on_duty(params["switch-to-on-duty-rank"], \ + params["switch-to-on-duty-assignment"]) // Finally make them on-duty with the requested rank+assignment + usr.put_in_hands(card) // Give them their new card + card = null // And get it out of us + update_icon() // Update our icon in case the card was removed + return TRUE // And stop handling UI in this run + if ("switch-to-off-duty") // If they switched to off-duty + if (check_face()) // Check that their face is visible + if (check_card_cooldown()) // And that their card isn't on cooldown + make_off_duty() // Finally make them off-duty + usr.put_in_hands(card) // Shit out their ID into their hand + card = null // And get rid of that card + update_icon() // Update our icon in case the card was removed + return TRUE // And stop handling any UI in this run + +/// Gets the open on-duty jobs available to a user in a specified department +/obj/machinery/computer/timeclock/proc/get_open_on_duty_jobs(var/mob/user, var/department) + var/list/available_jobs = list() // Make a list of available jobs + for (var/datum/job/job in SSjob.occupations) // For all jobs in existing occupations + if (is_open_onduty_job(user, department, job)) // Check if the job is open and on-duty for a user and given department + available_jobs[job.title] = list(job.title) // If it is, add it to the list of available jobs + if (job.alt_titles) // If the job has alt-titles (Like Station Engineer->Mechanic) + for (var/alt_job in job.alt_titles) // Go through all the alt titles + if (alt_job != job.title) // And if it isn't the current job's title (Station Engineer's alt title of Station Engineer) + available_jobs[job.title] += alt_job // Add it to the list under that job title + return available_jobs // Return the newly-filled list of available jobs + +/// How does xenobio work again? Oh right, we're checking the user's face here to allow access and prevent ne'er-do-wells from using someone's PTO +/obj/machinery/computer/timeclock/proc/check_face() + if (!card) // If no card is inserted (why are we checking their face?) + to_chat(usr, "No ID is inserted.") + return FALSE // Face check failed + var/mob/living/carbon/human/H = usr // Get the usr as a human data type + if (!(istype(H))) // If they somehow aren't a human?? + to_chat(usr, "Invalid user detected. Access denied.") + return FALSE // Face check failed + else if ((H.wear_mask && (H.wear_mask.flags_inv & HIDEFACE)) \ + || (H.head && (H.head.flags_inv & HIDEFACE))) // No, you can't hide your face + to_chat(usr, "Facial recognition scan failed due to physical obstruction. Access denied.") + return FALSE // Face check failed + else if (H.get_face_name() == "Unknown" \ + || !(H.real_name == card.registered_name)) // If they're unknown or their real name isn't the name on the ID + to_chat(usr, "Facial recognition scan failed. Access denied.") + return FALSE // Face check failed + else // Otherwise + return TRUE // Face check success! + +/// Force users to wait 10 minutes between clocking in and out +/obj/machinery/computer/timeclock/proc/check_card_cooldown() + if (!card) // If we don't have a card + return FALSE // We can't check cooldown, fail the check + var/time_left = 10 MINUTES - (world.time - card.last_job_switch) // Determine how much time is left before the next switch + if (time_left > 0) // If there's any time left at all + to_chat(usr, "You need to wait another [round((time_left / 10) / 60, 1)] minute\s before you can switch.") + return FALSE // Fail the check + return TRUE // Otherwise pass the check + +/// Makes the active card on-duty with a set rank and assignment +/obj/machinery/computer/timeclock/proc/make_on_duty(var/new_rank, var/new_assignment) + var/datum/job/old_job = \ + SSjob.GetJob(SSjob.get_job_name(card.assignment)) // Get their old job from the card + var/datum/job/new_job = SSjob.GetJob(new_rank) // And their new job from the rank + + if (!old_job \ + || !is_open_onduty_job(usr, old_job.pto_type, new_job)) // If there's no old job or it's not an open and on-duty job + return // Do nothing + if (new_assignment != new_job.title \ + && !(new_assignment in new_job.alt_titles)) // If the new assignment isn't the new job's title or alt-title + return // Do nothing + if (new_job) // As long as there's a new job + card.access = new_job.get_access() // Set the card's access to the new job's access + card.assignment = new_assignment // And the card's assignment to the new assignment + card.name = text(\ + "[card.registered_name]'s ID Card ([card.assignment])") // And change the card's name + GLOB.data_core.manifest_modify(card.registered_name, \ + card.assignment) // Apply the changes to the crew manifest + card.last_job_switch = world.time // Set the last job switch on the card to the current world time + new_job.current_positions++ // Add one to the current positions for the new job + var/mob/living/carbon/human/H = usr // Get the caller as a human data type + H.mind.assigned_role = card.assignment // Set the assigned role of the caller's mind to the assignment on the card + if (GLOB.announcement_systems.len) // If there are any announcement systems + var/obj/machinery/announcement_system/announcer = \ + pick(GLOB.announcement_systems) // Pick an announcer, any announcer! + announcer.announce("ONDUTY", card.registered_name, \ + card.assignment, list()) // Make the announcement for now on-duty personnel + +/// Makes the active card off-duty +/obj/machinery/computer/timeclock/proc/make_off_duty() + var/datum/job/found_job = \ + SSjob.GetJob(SSjob.get_job_name(card.assignment)) // Get the current job from the inserted card + if (!found_job) // If the card somehow doesn't have a job + return // https://www.youtube.com/watch?v=2k0SmqbBIpQ + var/new_dept = found_job.pto_type || PTO_CIVILIAN // New department is either the department's PTO type or civilian PTO + var/datum/job/pto_job = null // Create a new PTO job + for (var/datum/job/job in SSjob.occupations) // For all jobs in the list of occupations + if (job.pto_type == new_dept \ + && job.timeoff_factor < 0) // If the job is the department's PTO type and has a negative timeoff factor + pto_job = job // That's the new PTO job + break // And stop looking + if (pto_job) // If a PTO job was found + // Apparently we aren't using this? I don't fucking know + // var/old_title = card.assignment + card.access = pto_job.get_access() // Assign the PTO job's access to the ID + card.assignment = pto_job.title // And the PTO job's title to the ID + card.name = text(\ + "[card.registered_name]'s ID Card ([card.assignment])") // Set the card's new name + GLOB.data_core.manifest_modify(card.registered_name, \ + card.assignment) // And apply that change to the crew manifest + card.last_job_switch = world.time // Set the last job switch on the card to the world's current time + var/mob/living/carbon/human/H = usr // Get the caller as a human data type + H.mind.assigned_role = pto_job.title // Set their mind's assigned role to the PTO job's title + found_job.current_positions-- // Remove one from the found job's position count since they no longer have that job + if (GLOB.announcement_systems.len) // If there are any announcement systems + var/obj/machinery/announcement_system/announcer = \ + pick(GLOB.announcement_systems) // Pick an announcement system, any announcement system! + announcer.announce("OFFDUTY", card.registered_name, \ + card.assignment, list()) // Make an off-duty announcement with that system + return // I really don't know why this return exists we always return anyways lol + +/// Check if a job is open and on-duty for a given user and department +/obj/machinery/computer/timeclock/proc/is_open_onduty_job(var/mob/user, var/department, var/datum/job/job) + return job \ + && job.current_positions <= job.total_positions \ + && !jobban_isbanned(user, SSjob.get_job_name(job.title)) \ + && job.player_old_enough(user.client) \ + && job.pto_type == department \ + && job.timeoff_factor > 0 // I feel like this requires some HEAVY explanation so here we go. + // First we check if the job exists + // Then we check that there are enough open slots + // Then we check if the user is jobbanned or not and negate that because it'll return positive if they're banned + // Then we have to check that they have enough playtime to actually join as that job + // Then we have to check that the PTO type matches the department requested (can't join security with a PTO type of medical) + // Then we have to check that there's a timeoff value so you can actually accrue hours of PTO by playing as that role + +/// Convert a department and job flag to an english phrase +/obj/machinery/computer/timeclock/proc/flags_to_english(var/department,var/flag) + if (department == ENGSEC) // If the department flag is engineering or security (or silicon apparently?) + switch (flag) // Switch based on the flag + if (CAPTAIN, HOS, WARDEN, CHIEF) // Captain, Head of Security, Warden or Chief Engineer + return "Command" // Are all command + if (DETECTIVE, OFFICER) // Detectives and Officers + return "Security" // Are all security + //if (BRIGDOC) // Brig Docs + //return "Medsec" // Are medical and science + if (LAMBENT) // Lambent + return "Lambent" // Are obviously Lambent. Dunno what you expected fam + if (ENGINEER, ATMOSTECH) // Engineers and Atmos Technicians + return "Engineering" // Are engineering + if (ROBOTICIST) // Roboticist is defined here but I think it was supposed to be with medsci? Just in case. + return "Science" // They're science + else // Technically this won't always be silicon + return "Silicon" // But we're listing your ass as silicon anyways. Cope. + else if (department == MEDSCI) // Otherwise, if the department flag is medical or science + switch (flag) // Switch based on the flag (again) + if (RD_JF, CMO_JF) // Research Director or Chief Medical Officer + return "Command" // Are both command + if (SCIENTIST, ROBOTICIST) // Scientists and Roboticists (this is where I think this was meant to be but it's still with ENGSEC just in case) + return "Science" // Are obviously science + if (CHEMIST, DOCTOR, VIROLOGIST) // Chemists, Doctors, Virologists and Paramedics + return "Medical" // Are medical + if (GENETICIST) // Geneticists + return "Medsci" // Are kind of a special, middle of the road medical and science case + else // If you're landing here you're fucked + return "What the fuck?" // So what the fuck? + else if (department == CIVILIAN) // Otherwise you must be civilian, right? Right??? + switch (flag) // Once again a switch based on the flag + if (HOP, QUARTERMASTER) // Head of Personnel and Quartermaster + return "Command" // Are command + //if (PRISONER) // Prisoners + //return "Prisoner" // Are... prisoners? They shouldn't be able to PTO but if they manage to break out they can access a console so no fuck you here + if (CARGOTECH, MINER) // Cargo Technicians and Miners + return "Cargo" // Are cargonians + else // Otherwise + return "Civilian" // You must be a civilian. You could be an assisstant or a clown or a bartender, I don't really care. + else // New department combo? Wack. + return "What the fuck?" // what the fuck? diff --git a/hyperstation/code/game/objects/items/cards_ids.dm b/hyperstation/code/game/objects/items/cards_ids.dm new file mode 100644 index 000000000..8ea19ec93 --- /dev/null +++ b/hyperstation/code/game/objects/items/cards_ids.dm @@ -0,0 +1,2 @@ +/obj/item/card/id + var/last_job_switch /// Last job switch for card's owner diff --git a/hyperstation/code/game/objects/items/circuitboards/computer_circuitboards.dm b/hyperstation/code/game/objects/items/circuitboards/computer_circuitboards.dm new file mode 100644 index 000000000..c1a843992 --- /dev/null +++ b/hyperstation/code/game/objects/items/circuitboards/computer_circuitboards.dm @@ -0,0 +1,3 @@ +/obj/item/circuitboard/computer/timeclock + name = "Timeclock (Computer Board)" + build_path = /obj/machinery/computer/timeclock diff --git a/hyperstation/code/modules/client/client_defines.dm b/hyperstation/code/modules/client/client_defines.dm new file mode 100644 index 000000000..705945fac --- /dev/null +++ b/hyperstation/code/modules/client/client_defines.dm @@ -0,0 +1,9 @@ +/** + * Additional client defines. I needed somewhere to put these so here it is! + */ +/client + ////////////////////////////////////// + // Things that require the database // + ////////////////////////////////////// + var/list/department_hours = list() // Track hours of leave accrued for each department + var/list/play_hours = list() // Tracks total playtime hours for each department diff --git a/hyperstation/code/modules/jobs/job_types/_job.dm b/hyperstation/code/modules/jobs/job_types/_job.dm new file mode 100644 index 000000000..9051523de --- /dev/null +++ b/hyperstation/code/modules/jobs/job_types/_job.dm @@ -0,0 +1,3 @@ +/datum/job + var/timeoff_factor = 3 + var/pto_type diff --git a/hyperstation/icons/obj/machinery/timeclock.dmi b/hyperstation/icons/obj/machinery/timeclock.dmi new file mode 100644 index 0000000000000000000000000000000000000000..164cd291234cd5293feabc99c880ab26db0d9a35 GIT binary patch literal 5646 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�Z_TI#saW=yKx?=CQaN-wPvo`L_R@Sj0oLL)qGp)rJ%D4)B+5^`g zgH$2M2#sn=%p2&%v<}t9h>U0QP4?A^hXm%jf_Z7*g_<^(lz*c|VShful`t5fK?W-u z8T(aDB!x0meD?hcmN$0cmUsd~{tA}e$*Ei}>nBlOGDLl%a9QZGf5mfnH+^nteD*c4r8to;{qXe~ zK2eD^6Ak!wTNzM9(w^_cyX3O0r21forFF53*RRnXeWWaiRp{sNo5>)&RZsjWvEFrs zrrCrWyjAyW*)7})N2x`{?gdD=f*h1ly}r$yqzUFX@>N@W=2iFCJ($OZT}lT5&wU$_ z81+2f(9PrEFPQ1^IS!e>x+Vq>ffv8=kTk z%O=%YOSwgHn3ZtH%O$jva-$Oq@l&ujZ3%?Y5Tj++?oZ5>mX%h%Ghunigh$FiBV7jG zZcT1{$#z%wTM>0obZ#W5j{QXC`=Zo{_1|T}JQFOJuI_9h#4ngIDF1_*|6{M7_o)%L zojLHsk;miV=D*fYzV13q6X4_PS+B>kEr&_D?OT0IItL5FCPX5ku9;j*Xi@LpGsmMM zqD9{5Y$2~aYC*R~gZ^;V$!z6VkX~XeXi!kcfBo;{RGgf{=@>#JewjK30GR1 z33O<0nbST;io9m3hnWdgijjK^C-%}GuCo##(ZG`OqH&w{S;f`f9H%+mzMAIYc;A#s zz1sT8X%%4@>ph9SfQxQj#z=B# zn0+)mN8yesf6E09F?*UIv(L^K8za}a&(11;H&%%88Z3?^eftfgF*l~TXH{_I+7s{g z{#i4=8+p=-z1ZP7#`{L6-E_aOtIp~oezEFt%Yy3qt`z;?I#+uda-kp90 zGrDG&2mA{qQnB$YHbSZk_X-Qnz=`C#f!?n;6Wk92Y9WwTUr*KH-i_Q1IfPaN=Sqj= z0|zv-5We}~NxB~V3aE&;$?r7Qe}Yz=mRZm>kkIT?_SlL*`cFg-Vhq}T@T8{33-Awt zk52gBMejPSM)%b>`KEqGt2`-`i{jZxTq+QO#RMs3t6%v*!`{Prr+i&<=h7)+o8Bs3 z1;;%W@Dc0QF~u95rhw85oTt%sxSlUe<4*A9MN-(@4vozV+U5;H58GBS^Ap_W(E{el zCEX~qT0W>D13DKS^c>l~W6c?)0&XL~&ym9!RD#5s;mPIghHEw>)Ah`dbEH>j2Mzt; zaL>h6K=e00-W!YiVTZLdw*5iQVVwj4GDmv{5~J|Lu__;!*=Ca;!2EN=s7|N+7}*M| zKFPy2%e}YG{8AluCMBJ^fy~uMx(1Xmv4WOZkmwr88F4v9YExBE7qHmcHZK5Av2HPX z@El=QuKxno*~C*c8d>Y=tfG!3m>nw*P&Q e(S3CSIYqRnd!FrgNOIbWva@kKP`Lk8%zpu { + const { act, data } = useBackend(context); + + const { + department_hours, + user_name, + card, + assignment, + job_datum, + allow_change_job, + job_choices, + } = data; + + return ( + + +
+ + OOC Note: PTO acquired is account-wide and shared across all characters. + Info listed below is not IC information. + +
+ + {Object.keys(department_hours).map(key => ( + 6 + ? "good" + : department_hours[key] > 1 + ? "average" + : "bad" + }> + {toFixed(department_hours[key], 1)} {department_hours[key] === 1 ? "hour" : "hours"} + + ))} + +
+
+
+ + + + + {!!job_datum && ( + + + + + + + + + + + + {job_datum.title} + + + + + + + {job_datum.departments} + + + {job_datum.economic_modifier} + + + {job_datum.timeoff_factor > 0 && ( + + Earns PTO - {job_datum.pto_department} + + ) || job_datum.timeoff_factor < 0 && ( + + Requires PTO - {job_datum.pto_department} + + ) || ( + + Neutral + + )} + + + )} + +
+ {!!(allow_change_job && job_datum ** job_datum.timeoff_factor !== 0 && assignment !== "Dismissed") && ( +
+ {job_datum.timeoff_factor > 0 && ( + department_hours[job_datum.pto_department] > 0 && ( + + ) || ( + + Warning: You do not have enough accrued time off to go off-duty. + + ) + ) || ( + Object.keys(job_choices).length && Object.keys(job_choices).map(job => { + let alt_titles = job_choices[job]; + + return alt_titles.map(title => ( + + )); + }) || ( + + No Open Positions - See Head Of Personnel + + ) + )} +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/tgui/packages/tgui/interfaces/common/RankIcon.js b/tgui/packages/tgui/interfaces/common/RankIcon.js new file mode 100644 index 000000000..bdb88742f --- /dev/null +++ b/tgui/packages/tgui/interfaces/common/RankIcon.js @@ -0,0 +1,119 @@ +import { Icon } from "../../components"; + +const rank2icon = { + // Command + 'Colony Director': 'user-tie', + 'Site Manager': 'user-tie', + 'Overseer': 'user-tie', + 'Head of Personnel': 'briefcase', + 'Crew Resources Officer': 'briefcase', + 'Deputy Director': 'briefcase', + 'Command Secretary': 'user-tie', + // Security + 'Head of Security': 'user-shield', + 'Security Commander': 'user-shield', + 'Chief of Security': 'user-shield', + 'Warden': ['city', 'shield-alt'], + 'Detective': 'search', + 'Forensic Technician': 'search', + 'Security Officer': 'user-shield', + 'Junior Officer': 'user-shield', + // Engineering + 'Chief Engineer': 'toolbox', + 'Atmospheric Technician': 'wind', + 'Station Engineer': 'toolbox', + 'Maintenance Technician': 'wrench', + 'Engine Technician': 'toolbox', + 'Electrician': 'toolbox', + // Medical + 'Chief Medical Officer': 'user-md', + 'Chemist': 'mortar-pestle', + 'Pharmacist': 'mortar-pestle', + 'Medical Doctor': 'user-md', + 'Surgeon': 'user-md', + 'Emergency Physician': 'user-md', + 'Nurse': 'user-md', + 'Virologist': 'disease', + 'Paramedic': 'ambulance', + 'Emergency Medical Technician': 'ambulance', + 'Psychiatrist': 'couch', + 'Psychologist': 'couch', + // Science + 'Research Director': 'user-graduate', + 'Research Supervisor': 'user-graduate', + 'Roboticist': 'robot', + 'Biomechanical Engineer': ['wrench', 'heartbeat'], + 'Mechatronic Engineer': 'wrench', + 'Scientist': 'flask', + 'Xenoarchaeologist': 'flask', + 'Anomalist': 'flask', + 'Phoron Researcher': 'flask', + 'Circuit Designer': 'car-battery', + 'Xenobiologist': 'meteor', + 'Xenobotanist': ['biohazard', 'seedling'], + // Cargo + 'Quartermaster': 'box-open', + 'Supply Chief': 'warehouse', + 'Cargo Technician': 'box-open', + 'Shaft Miner': 'hard-hat', + 'Drill Technician': 'hard-hat', + // Exploration + 'Pathfinder': 'binoculars', + 'Explorer': 'user-astronaut', + 'Field Medic': ['user-md', 'user-astronaut'], + 'Pilot': 'space-shuttle', + // Civvies + 'Bartender': 'glass-martini', + 'Barista': 'coffee', + 'Botanist': 'leaf', + 'Gardener': 'leaf', + 'Chaplain': 'place-of-worship', + 'Counselor': 'couch', + 'Chef': 'utensils', + 'Cook': 'utensils', + 'Entertainer': 'smile-beam', + 'Performer': 'smile-beam', + 'Musician': 'guitar', + 'Stagehand': 'smile-beam', + // All of the interns + 'Intern': 'school', + 'Apprentice Engineer': ['school', 'wrench'], + 'Medical Intern': ['school', 'user-md'], + 'Lab Assistant': ['school', 'flask'], + 'Security Cadet': ['school', 'shield-alt'], + 'Jr. Cargo Tech': ['school', 'box'], + 'Jr. Explorer': ['school', 'user-astronaut'], + 'Server': ['school', 'utensils'], + // Back to civvies + 'Internal Affairs Agent': 'balance-scale', + 'Janitor': 'broom', + 'Custodian': 'broom', + 'Sanitation Technician': 'hand-sparkles', + 'Maid': 'broom', + 'Librarian': 'book', + 'Journalist': 'newspaper', + 'Writer': 'book', + 'Historian': 'chalkboard-teacher', + 'Professor': 'chalkboard-teacher', + 'Visitor': 'user', + // Special roles + 'Emergency Responder': 'fighter-jet', +}; + +export const RankIcon = (props, context) => { + const { + rank, + color = 'label', + } = props; + + let rankObj = rank2icon[rank]; + if (typeof rankObj === "string") { + return ; + } else if (Array.isArray(rankObj)) { + return rankObj.map(icon => ( + + )); + } else { + return ; + } +}; \ No newline at end of file