diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index 019e35a760f1..a7f9a1cc99a0 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -191,6 +191,8 @@ GLOBAL_LIST_EMPTY(objectives)
if(!(eq_path in T.contents))
new eq_path(T)
return
+ if(LAZYLEN(get_owners()) == 0)
+ return
var/datum/mind/receiver = pick(get_owners())
if(receiver && receiver.current)
if(ishuman(receiver.current))
@@ -990,6 +992,8 @@ GLOBAL_LIST_EMPTY(possible_items_special)
/datum/objective/destroy/find_target(dupe_search_range, blacklist)
var/list/possible_targets = active_ais(1)
+ if(LAZYLEN(possible_targets) == 0)
+ return
var/mob/living/silicon/ai/target_ai = pick(possible_targets)
target = target_ai.mind
update_explanation_text()
@@ -1358,7 +1362,7 @@ GLOBAL_LIST_EMPTY(possible_items_special)
* Kill Pet
*/
/datum/objective/minor/pet
- name = "assasinate-pet"
+ name = "assassinate-pet"
explanation_text = "Assassinate the HoP's assistant, Ian."
/// Pet
var/mob/living/pet
@@ -1381,7 +1385,7 @@ GLOBAL_LIST_EMPTY(possible_items_special)
var/mob/A = locate(P) in GLOB.mob_living_list
if(A && is_station_level(A.z))
possible_pets += P
- if(!possible_pets)
+ if(!possible_pets || LAZYLEN(possible_pets) == 0)
return
var/chosen_pet = rand(1, possible_pets.len)
pet = locate(possible_pets[chosen_pet]) in GLOB.mob_living_list
@@ -1405,7 +1409,8 @@ GLOBAL_LIST_EMPTY(possible_items_special)
update_explanation_text()
/datum/objective/minor/pet/update_explanation_text()
- explanation_text = "Assassinate the important animal, [pet.name]"
+ if(pet?.name)
+ explanation_text = "Assassinate the important animal, [pet.name]"
/datum/objective/minor/pet/copy_target(datum/objective/minor/pet/old_obj)
. = ..()
@@ -1689,7 +1694,7 @@ GLOBAL_LIST_EMPTY(possible_items_special)
/datum/objective/gimmick/check_completion()
return TRUE
-
+
/datum/objective/gimmick/admin_edit(mob/admin)
update_explanation_text()
diff --git a/code/game/gamemodes/objective_items.dm b/code/game/gamemodes/objective_items.dm
index 4a923f59efac..eb1136c62afa 100644
--- a/code/game/gamemodes/objective_items.dm
+++ b/code/game/gamemodes/objective_items.dm
@@ -29,7 +29,7 @@
/datum/objective_item/steal/caplaser
name = "the Captain's antique laser gun."
targetitem = /obj/item/gun/energy/laser/captain
- difficulty = 5
+ difficulty = 10
excludefromjob = list("Captain")
/datum/objective_item/steal/hoslaser
@@ -47,7 +47,7 @@
/datum/objective_item/steal/jetpack
name = "the Captain's jetpack."
targetitem = /obj/item/tank/jetpack/oxygen/captain
- difficulty = 5
+ difficulty = 10
excludefromjob = list("Captain")
/datum/objective_item/steal/magboots
@@ -71,7 +71,7 @@
/datum/objective_item/steal/nukedisc
name = "the nuclear authentication disk."
targetitem = /obj/item/disk/nuclear
- difficulty = 5
+ difficulty = 15
excludefromjob = list("Captain")
/datum/objective_item/steal/nukedisc/check_special_completion(obj/item/disk/nuclear/N)
@@ -80,7 +80,7 @@
/datum/objective_item/steal/reflector
name = "a reflective jacket."
targetitem = /obj/item/clothing/suit/armor/laserproof
- difficulty = 3
+ difficulty = 5
excludefromjob = list("Head of Security", "Warden")
/datum/objective_item/steal/aiupload
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index e82d53f88f43..15e2d127118f 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -2297,6 +2297,68 @@
to_chat(src.owner, span_danger("Unable to locate fax!"))
return
owner.send_admin_fax(F)
+
+ else if(href_list["uplink_custom_obj"])
+ if(!check_rights(R_ADMIN))
+ return
+ var/datum/uplink_item/new_objective/UPL = locate(href_list["uplink_custom_obj"])
+ var/mob/requester = locate(href_list["requester"]) in GLOB.mob_list
+ if(!requester)
+ to_chat(usr, span_danger("Requesting mob doesn't exist anymore!"))
+ return
+ if(!UPL)
+ to_chat(usr, span_danger("NewObjective datum doesn't exist anymore!"))
+ return
+ if(UPL.admin_forging)
+ to_chat(usr, span_danger("Another admin is already forging an objective for this request!"))
+ return
+ if(UPL.obj_set)
+ to_chat(usr, span_danger("An objective has already been set for this request!"))
+ return
+ usr.client.uplink_custom_obj(UPL, requester)
+
+ else if(href_list["uplink_custom_obj_accept"])
+ if(!check_rights(R_ADMIN))
+ return
+ var/obj/item/folder/objective/FLD = locate(href_list["uplink_custom_obj_accept"])
+ var/mob/requester = locate(href_list["requester"]) in GLOB.mob_list
+ if(!FLD)
+ to_chat(usr, span_danger("Objective Folder does not exist!"))
+ return
+ if(!requester)
+ to_chat(usr, span_danger("Requesting mob doesn't exist anymore!"))
+ return
+ if(!FLD.objective)
+ to_chat(usr, span_danger("Objective Folder has no objective!"))
+ return
+ if(FLD.objective.completed)
+ to_chat(usr, span_danger("Objective is already marked complete by another admin!"))
+ return
+ FLD.objective.completed = TRUE
+ to_chat(requester, span_notice("The folder lets out a small beep, letting you know that its objective has been marked as complete."))
+ requester.playsound_local(get_turf(FLD), 'sound/machines/ping.ogg', 20, 0)
+ message_admins("[key_name_admin(usr)] has marked the custom objective, [FLD.objective.explanation_text], as complete.")
+
+ else if(href_list["uplink_custom_obj_deny"])
+ if(!check_rights(R_ADMIN))
+ return
+ var/obj/item/folder/objective/FLD = locate(href_list["uplink_custom_obj_deny"])
+ var/mob/requester = locate(href_list["requester"]) in GLOB.mob_list
+ if(!FLD)
+ to_chat(usr, span_danger("Objective Folder does not exist!"))
+ return
+ if(!requester)
+ to_chat(usr, span_danger("Requesting mob doesn't exist anymore!"))
+ return
+ if(!FLD.objective)
+ to_chat(usr, span_danger("Objective Folder has no objective!"))
+ return
+ if(FLD.objective.completed)
+ to_chat(usr, span_danger("Objective is already marked complete by another admin!"))
+ return
+ to_chat(requester, span_danger("The folder lets out a harsh beep, letting you know that its objective has not been completed."))
+ requester.playsound_local(get_turf(FLD), 'sound/machines/buzz-two.ogg', 20, 0)
+ message_admins("[key_name_admin(usr)] has marked the custom objective, [FLD.objective.explanation_text], as incomplete.")
/client/proc/send_global_fax()
set category = "Admin.Round Interaction"
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index ee3dc3e7238c..99adcbc48de4 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -1556,3 +1556,65 @@ Traitors and the like can also be revived with the previous role mostly intact.
return
else
usr.forceMove(T)
+
+/client/proc/uplink_custom_obj(var/datum/uplink_item/new_objective/objective_uplink_datum, mob/requester)
+ if(!check_rights(R_ADMIN))
+ return
+
+ message_admins("[key_name_admin(usr)] is forging a custom objective for [ADMIN_LOOKUPFLW(requester)].")
+
+
+ var/obj_txt = "Kill everyone."
+ if(objective_uplink_datum.difficulty == 0)
+ obj_txt = stripped_input(usr, "Custom objective:", "Objective", obj_txt)
+ else
+ var/diff_txt = list("EASY", "MEDIUM", "HARD")[objective_uplink_datum.difficulty]
+ obj_txt = stripped_input(usr, "Custom [diff_txt] objective:", "Objective", obj_txt)
+ if(!obj_txt)
+ objective_uplink_datum.admin_forging = FALSE
+ message_admins("[key_name_admin(usr)] decided not to forge a custom objective.")
+ objective_uplink_datum.cancelled(requester)
+ return
+
+ var/difficulty = 1
+ if(objective_uplink_datum.difficulty == 0)
+ difficulty = input("1-3", "Set Difficulty Rating (Determines TC)", difficulty) as null|num
+ if(!difficulty || difficulty < 1 || difficulty > 3)
+ objective_uplink_datum.admin_forging = FALSE
+ if(difficulty < 1 || difficulty > 3)
+ to_chat(usr, span_danger("Difficulty must be 1, 2, or 3!"))
+ message_admins("[key_name_admin(usr)] decided not to forge a custom objective.")
+ objective_uplink_datum.cancelled(requester)
+ return
+ else
+ difficulty = objective_uplink_datum.difficulty
+
+ if(!objective_uplink_datum)
+ objective_uplink_datum.admin_forging = FALSE
+ to_chat(usr, span_danger("Requesting uplink item no longer exists!"))
+ message_admins("[key_name_admin(usr)] decided not to forge a custom objective.")
+ objective_uplink_datum.cancelled(requester)
+ return
+
+ if(!requester)
+ objective_uplink_datum.admin_forging = FALSE
+ to_chat(usr, span_danger("Requesting mob no longer exists!"))
+ message_admins("[key_name_admin(usr)] decided not to forge a custom objective.")
+ objective_uplink_datum.cancelled(requester)
+ return
+
+ var/datum/objective/custom/forged_objective = new /datum/objective/custom
+ forged_objective.explanation_text = obj_txt
+
+ var/obj/item/folder/objective/folder = objective_uplink_datum.spawn_objective(requester, forged_objective, difficulty)
+
+ var/diff_txt = list("RANDOM", "EASY", "MEDIUM", "HARD")[difficulty+1]
+ objective_uplink_datum.admin_forging = FALSE
+ if(folder)
+ deltimer(objective_uplink_datum.timer)
+ message_admins("[key_name_admin(usr)] forged a folder objective for [ADMIN_LOOKUPFLW(requester)] with difficulty rating [diff_txt]: [obj_txt]")
+ log_game("[key_name(usr)] forged a folder objective for [key_name(requester)] with difficulty rating [diff_txt]: [obj_txt]")
+ else
+ to_chat(usr, span_danger("Failed to create objective folder!"))
+ message_admins("[key_name_admin(usr)] attempted to forge a folder objective for [ADMIN_LOOKUPFLW(requester)] with difficulty rating [diff_txt]: [obj_txt], but it failed to create.")
+ log_game("[key_name(usr)] attempted to forge a folder objective for [key_name(requester)] with difficulty rating [diff_txt]: [obj_txt], but it failed to create.")
diff --git a/code/modules/paperwork/folders.dm b/code/modules/paperwork/folders.dm
index ae42022663ac..52189bf1d292 100644
--- a/code/modules/paperwork/folders.dm
+++ b/code/modules/paperwork/folders.dm
@@ -130,3 +130,180 @@
. = ..()
new /obj/item/documents/syndicate/mining(src)
update_appearance(UPDATE_ICON)
+
+/// For traitors: New objective
+/obj/item/folder/objective
+ var/datum/objective/objective // Object not typepath
+ var/difficulty = 0
+ var/tc = 0
+ var/admin_msg = FALSE
+ // Steal objectives initialized later
+ var/list/easy_objectives = newlist(
+ /datum/objective/minor/pet, // Kill a pet
+ )
+ var/list/med_objectives = newlist(
+ /datum/objective/assassinate/once, // Kill someone once
+ )
+ var/list/hard_objectives = newlist(
+ /datum/objective/destroy, // Kill AI
+ )
+
+/obj/item/folder/objective/Initialize(mapload, _user, _obj, _diff)
+ . = ..()
+
+ init_steal_objs()
+
+ difficulty = _diff ? _diff : rand(1,3)
+ if(!isnum(difficulty))
+ difficulty = rand(1,3)
+ difficulty = round(clamp(difficulty, 1, 3)) // safety
+
+ SSblackbox.record_feedback("tally", "SOMS_create", 1, list("EASY", "MEDIUM", "HARD")[difficulty])
+
+ tc = clamp(difficulty * 2 + rand(-1,1), 2, 10)
+ forge_objective(_obj)
+
+ var/folder_type = rand(1,5)
+ var/folder_color = "gray"
+ switch(folder_type)
+ if(2)
+ desc = "A blue folder."
+ icon_state = "folder_blue"
+ folder_color = "blue"
+ if(3)
+ desc = "A red folder."
+ icon_state = "folder_red"
+ folder_color = "red"
+ if(4)
+ desc = "A yellow folder."
+ icon_state = "folder_yellow"
+ folder_color = "yellow"
+ if(5)
+ desc = "A white folder."
+ icon_state = "folder_white"
+ folder_color = "white"
+ update_icon()
+ if(_user)
+ to_chat(_user, span_notice("Your objective has been curated. You will find it as a [folder_color] folder in [get_area_name(src, TRUE)]."))
+
+/// Initialize steal objectives based on difficulty
+/obj/item/folder/objective/proc/init_steal_objs()
+ for(var/I in subtypesof(/datum/objective_item/steal))
+ var/datum/objective/steal/newsteal = new /datum/objective/steal
+ var/datum/objective_item/steal/S = new I
+ if(!S.TargetExists())
+ continue
+ if(LAZYLEN(S.special_equipment) > 0) // No special equipment allowed
+ continue
+ newsteal.set_target(S)
+ if(S.difficulty < 5) // 1-4 is easy
+ easy_objectives += newsteal
+ else if(S.difficulty >= 5 && S.difficulty < 10) // 5-9 is medium
+ med_objectives += newsteal
+ else if(S.difficulty >= 10) // 10+ is hard
+ hard_objectives += newsteal
+ else
+ CRASH("Invalid difficulty on steal objective! [S?.difficulty]")
+
+/obj/item/folder/objective/proc/forge_objective(_obj)
+ if(_obj)
+ objective = _obj
+ else
+ var/list/potential_objectives = list(easy_objectives, med_objectives, hard_objectives)[difficulty]
+ var/inf_protection = 0 // Threshold of 30 may have to be raised if more objectives are added
+ // This will cycle through invalid objectives
+ while(!objective || objective.explanation_text == "Nothing." || objective.explanation_text == "Free Objective")
+ inf_protection++
+ if(inf_protection >= 30)
+ break
+
+ if(objective)
+ potential_objectives.Remove(objective)
+ qdel(objective)
+
+ if(LAZYLEN(potential_objectives) <= 0)
+ break
+
+ objective = pick(potential_objectives)
+
+ // i hate objective code so much WHO WROTE THIS????
+ if(!istype(objective, /datum/objective/steal))
+ objective.find_target()
+
+ if(istype(objective, /datum/objective/minor))
+ var/datum/objective/minor/O = objective
+ O.finalize()
+
+ objective.update_explanation_text()
+
+ if(LAZYLEN(potential_objectives) <= 0 || inf_protection >= 30)
+ qdel(src)
+ CRASH("No valid [list("EASY", "MEDIUM", "HARD")[difficulty]] objective could be chosen! Deleting folder!")
+
+ // Cleanup
+ for(var/datum/objective/O in easy_objectives)
+ easy_objectives.Remove(O)
+ if(O != objective)
+ qdel(O)
+ for(var/datum/objective/O in med_objectives)
+ med_objectives.Remove(O)
+ if(O != objective)
+ qdel(O)
+ for(var/datum/objective/O in hard_objectives)
+ hard_objectives.Remove(O)
+ if(O != objective)
+ qdel(O)
+
+/obj/item/folder/objective/attack_self(mob/user)
+ . = ..()
+ if(is_syndicate(user))
+ ui_interact(user)
+ objective.owner = user.mind
+
+/obj/item/folder/objective/ui_interact(mob/user, datum/tgui/ui)
+ . = ..()
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ // Open UI
+ admin_msg = FALSE
+ ui = new(user, src, "FolderObjective")
+ ui.open()
+
+/obj/item/folder/objective/ui_static_data(mob/user)
+ . = ..()
+ .["tc"] = tc || "0"
+ .["difficulty"] = list("EASY", "MEDIUM", "HARD")[difficulty]
+ .["objective_text"] = objective?.explanation_text
+ .["admin_msg"] = admin_msg
+
+/obj/item/folder/objective/ui_act(action, list/params)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("check_done")
+ if(objective.check_completion())
+ to_chat(usr, span_notice("NOTICE: OBJECTIVE COMPLETE. GOOD WORK AGENT. DISPENSING REWARD. MAKE SURE THE OBJECTIVE STAYS COMPLETED OR WE WILL HURT YOU."))
+ to_chat(usr, "\The [src] suddenly transforms into [tc] telecrystal[tc == 1 ? "" : "s"]!")
+ usr.playsound_local(loc, 'sound/machines/ping.ogg', 20, 0)
+ var/obj/item/stack/telecrystal/reward = new /obj/item/stack/telecrystal
+ reward.amount = tc
+ dropped(usr, TRUE)
+ usr.put_in_hands(reward)
+ if(usr.mind?.has_antag_datum(/datum/antagonist/traitor))
+ var/datum/antagonist/traitor/T = usr.mind.has_antag_datum(/datum/antagonist/traitor)
+ T.add_objective(objective) // Keep the objective done or you will redtext
+ SSblackbox.record_feedback("tally", "SOMS_finish", 1, list("EASY", "MEDIUM", "HARD")[difficulty])
+ qdel(src)
+ return TRUE
+ else if(istype(objective, /datum/objective/custom))
+ admin_msg = TRUE
+ message_admins("[ADMIN_LOOKUPFLW(usr)] has requested an admin objective be checked for completion ([objective.explanation_text]). (MARK COMPLETED) (MARK INCOMPLETE)")
+ to_chat(usr, span_danger("NOTICE: SENT OBJECTIVE STATUS TO COMMAND FOR REVIEW."))
+ return TRUE
+ else
+ to_chat(usr, span_danger("ERR: OBJECTIVE NOT COMPLETE"))
+ usr.playsound_local(loc, 'sound/machines/buzz-two.ogg', 20, 0)
+ return TRUE
+
+
diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm
index 02868530da54..5c3352188898 100644
--- a/code/modules/uplink/uplink_items.dm
+++ b/code/modules/uplink/uplink_items.dm
@@ -24,6 +24,8 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
continue
if (I.restricted && !allow_restricted)
continue
+ if(istype(I, /datum/uplink_item/new_objective) && prob(50))
+ continue
if(!filtered_uplink_items[I.category])
filtered_uplink_items[I.category] = list()
@@ -3472,3 +3474,67 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
desc = "Omnizine infused gummy bears. Grape flavor. Chew throughly!"
item = /obj/item/storage/pill_bottle/gummies/omnizine
cost = 1
+
+/// SOMS
+/datum/uplink_item/new_objective
+ name = "Request Objective (RANDOM)"
+ category = "Request Objectives"
+ desc = "Sends a single-use signal to your employers, where they will determine your new objective \
+ and send down a folder to a random location containing your new instructions. \
+ Upon completion, you will be rewarded depending on the difficulty of the objective."
+ item = /obj/item/folder/red
+ cost = 4
+ surplus = 0
+ limited_stock = 1
+ cant_discount = TRUE
+ var/difficulty = 0 // 0 is random, then 1-3
+ var/timer // Timer to delete if admin forges objective
+ var/admin_forging = FALSE // If an admin is currently forging an objective
+ var/set_on_noforge = FALSE // If the timer expired, but is waiting for an admin to cancel forging
+ var/obj_set = FALSE // If the objective has been set
+
+/datum/uplink_item/new_objective/easy
+ name = "Request Objective (EASY)"
+ difficulty = 1
+ cost = 2
+
+/datum/uplink_item/new_objective/medium
+ name = "Request Objective (MEDIUM)"
+ difficulty = 2
+ cost = 4
+
+/datum/uplink_item/new_objective/hard
+ name = "Request Objective (HARD)"
+ difficulty = 3
+ cost = 6
+
+/datum/uplink_item/new_objective/spawn_item(spawn_path, mob/user, datum/component/uplink/U)
+ to_chat(user, span_notice("Signal sent to command. Awaiting response (ETA ~1 minute)..."))
+ var/diff_txt = list("RANDOM", "EASY", "MEDIUM", "HARD")[difficulty+1]
+ message_admins("[ADMIN_LOOKUPFLW(user)] has requested an objective ([diff_txt]). (FORGE CUSTOM OBJECTIVE?) (AUTO SET IN 1 MINUTE)")
+ timer = addtimer(CALLBACK(src, PROC_REF(spawn_objective), user), 1 MINUTES, TIMER_STOPPABLE)
+
+/datum/uplink_item/new_objective/proc/spawn_objective(user, _obj, _diff)
+ if(admin_forging) // If the timer expired while an admin was editing it
+ set_on_noforge = TRUE // Set a random objective if the admin decides not to set one
+ return
+ var/new_diff = _diff ? _diff : difficulty
+ obj_set = TRUE
+ var/turf/open/floor/F
+ var/can_see = TRUE
+ var/see_loops = 0 // infinite loop protection
+
+ while(can_see && see_loops < 40)
+ F = find_safe_turf(dense_atoms = FALSE)
+ can_see = FALSE
+ for(var/mob/living/M in view(13, F))
+ if(M.client)
+ can_see = TRUE
+ break
+ see_loops++
+
+ return new /obj/item/folder/objective(F, user, _obj, new_diff)
+
+/datum/uplink_item/new_objective/proc/cancelled(user)
+ if(set_on_noforge)
+ spawn_objective(user)
diff --git a/tgui/packages/tgui/interfaces/FolderObjective.js b/tgui/packages/tgui/interfaces/FolderObjective.js
new file mode 100644
index 000000000000..b28fec09d9e6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/FolderObjective.js
@@ -0,0 +1,40 @@
+import { useBackend } from '../backend';
+import { Button, LabeledList, NoticeBox, Section } from '../components';
+import { Window } from '../layouts';
+
+export const FolderObjective = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ tc,
+ difficulty,
+ objective_text,
+ admin_msg,
+ } = data;
+ return (
+
+
+
+
+
+
+ {objective_text}
+
+
+ {admin_msg ? (
+
+ Your objective status is being reviewed by command.
+ Please try again later.
+
+ ) : ""}
+
+
+ );
+};