diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 9e5854e71a8..1dc36786a19 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -680,7 +680,7 @@
var/new_obj_type = input("Select objective type:", "Objective type", def_value) as null|anything in list(
"assassinate", "assassinateonce", "blood", "debrain", "protect", "prevent", "hijack", "escape", "survive", "steal",
- "nuclear", "absorb", "destroy", "maroon", "identity theft", "incriminate", "custom")
+ "nuclear", "absorb", "destroy", "maroon", "identity theft", "download", "incriminate", "custom")
if(!new_obj_type)
return
@@ -798,6 +798,10 @@
new_objective.update_explanation_text()
var/datum/objective/escape/escape_with_identity/O = new_objective
O.target_real_name = new_objective.target.current.real_name
+
+ if("download")
+ new_objective = /datum/objective/download
+
if("incriminate")
new_objective = /datum/objective/incriminate
diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index fac0edc2ade..4386166e1ef 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -990,6 +990,8 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective)
else
return FALSE
+// Flayers
+
#define SWARM_GOAL_LOWER_BOUND 130
#define SWARM_GOAL_UPPER_BOUND 400
@@ -1019,6 +1021,181 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective)
#undef SWARM_GOAL_LOWER_BOUND
#undef SWARM_GOAL_UPPER_BOUND
+/datum/objective/download
+ name = "Download Files"
+ needs_target = FALSE
+ var/obj/machinery/computer/target_console = null
+ var/target_console_room = null
+
+/datum/objective/download/New()
+ find_target()
+ update_explanation_text()
+ establish_signals()
+ return ..()
+
+/datum/objective/download/Destroy()
+ if(target_console)
+ UnregisterSignal(target_console, COMSIG_PARENT_QDELETING)
+ return ..()
+
+/datum/objective/download/establish_signals()
+ if(target_console)
+ RegisterSignal(target_console, COMSIG_PARENT_QDELETING, PROC_REF(on_console_destroyed), override = TRUE)
+
+/datum/objective/download/proc/on_console_destroyed()
+ SIGNAL_HANDLER // COMSIG_PARENT_QDELETING
+
+ var/list/owners = get_owners()
+ for(var/datum/mind/M in owners)
+ to_chat(M.current, "
We sense the target console has been compromised. New vulnerability located.")
+ SEND_SOUND(M.current, sound('sound/ambience/alarm4.ogg'))
+
+ target_console = null
+ find_target()
+ if(!target_console)
+ holder.remove_objective(src)
+
+ // Update explanation text with new target
+ update_explanation_text()
+
+ // Announce the updated objective with new target
+ for(var/datum/mind/M in owners)
+ var/list/messages = M.prepare_announce_objectives(FALSE)
+ to_chat(M.current, chat_box_red(messages.Join("
")))
+
+/datum/objective/download/find_target()
+ if(target_console)
+ return
+
+ var/list/possible_computers = list()
+ var/list/computer_areas = list()
+
+ var/list/restricted_area_computer_types = list(
+ // ID management computers
+ /obj/machinery/computer/card, // Main HOP ID computer
+ /obj/machinery/computer/card/minor/hos, // Security ID computer
+ /obj/machinery/computer/card/minor/cmo, // Medical ID computer
+ /obj/machinery/computer/card/minor/qm, // Supply ID computer
+ /obj/machinery/computer/card/minor/rd, // Science ID computer
+ /obj/machinery/computer/card/minor/ce, // Engineering ID computer
+
+ // Security
+ /obj/machinery/computer/prisoner, // Prisoner management
+ /obj/machinery/computer/brigcells, // Brig cell management
+
+ // Command
+ /obj/machinery/computer/communications, // Command comms console
+ /obj/machinery/computer/teleporter, // Teleporter control
+
+ // Science
+ /obj/machinery/computer/message_monitor, // Message monitor
+ )
+
+ // Get all computers of the specified types
+ for(var/computer_type in restricted_area_computer_types)
+ var/list/computers_of_type = SSmachines.get_by_type(computer_type, subtypes = FALSE)
+ for(var/obj/machinery/computer/comp in computers_of_type)
+ // Skip deleted/invalid computers
+ if(QDELETED(comp))
+ continue
+ var/turf/comp_turf = get_turf(comp)
+ if(!comp_turf || !is_station_level(comp_turf.z))
+ continue
+ var/area/comp_area = get_area(comp)
+ possible_computers += comp
+ computer_areas[comp] = comp_area ? comp_area.name : "(Unknown Location - Please create an issue on GitHub!)"
+
+ if(length(possible_computers))
+ target_console = pick(possible_computers)
+ target_console_room = computer_areas[target_console]
+ establish_signals()
+ else
+ // Fallback if no computers found
+ target_console = null
+ target_console_room = "(Unknown Location - Please create an issue on GitHub!)"
+
+/datum/objective/download/found_target()
+ return target_console
+
+// Formats as title case except for "the".
+// E.g. "the Communications Console"
+/datum/objective/download/proc/get_formatted_console_name()
+ if(!target_console)
+ return "an unknown console"
+
+ var/console_name = target_console.name
+
+ var/list/words = splittext(console_name, " ")
+ var/formatted_name = ""
+ var/first_word = TRUE
+
+ for(var/word in words)
+ if(first_word && lowertext(word) == "the")
+ first_word = FALSE
+ continue
+
+ if(!first_word)
+ formatted_name += " "
+
+ // Capitalize first letter of each word
+ formatted_name += uppertext(copytext(word, 1, 2)) + lowertext(copytext(word, 2))
+ first_word = FALSE
+
+ return "the " + formatted_name
+
+/datum/objective/download/update_explanation_text()
+ explanation_text = "Use your charging implant on [get_formatted_console_name()] in the [target_console_room] to download your next objective."
+
+// We already check that the player is an IPC when assigning this objective,
+// but this protects us from cases like cybernetic revolution where the implant could be lost.
+/datum/objective/download/proc/enforce_charging_implant()
+ for(var/datum/mind/M in get_owners())
+ var/mob/living/carbon/human/H = M.current
+ if(!H)
+ continue
+
+ var/obj/item/organ/internal/left_arm_implant = H.get_organ_slot("l_arm_device")
+ var/obj/item/organ/internal/right_arm_implant = H.get_organ_slot("r_arm_device")
+
+ // Already have a charger, do nothing
+ if(istype(left_arm_implant, /obj/item/organ/internal/cyberimp/arm/power_cord) || istype(right_arm_implant, /obj/item/organ/internal/cyberimp/arm/power_cord))
+ continue
+
+ var/obj/item/organ/internal/cyberimp/arm/power_cord/implant = new /obj/item/organ/internal/cyberimp/arm/power_cord()
+
+ // Try to install in the first available slot
+ if(!left_arm_implant)
+ implant.slot = "l_arm_device"
+ implant.parent_organ = "l_arm"
+ implant.insert(H)
+ else if(!right_arm_implant)
+ implant.slot = "r_arm_device"
+ implant.parent_organ = "r_arm"
+ implant.insert(H)
+ else
+ // Both slots occupied, remove left arm implant and replace with charging implant
+ left_arm_implant.remove(H)
+ qdel(left_arm_implant)
+ implant.slot = "l_arm_device"
+ implant.parent_organ = "l_arm"
+ implant.insert(H)
+
+// This is called from computer.dm when the do_after of downloading is completed
+/datum/objective/download/proc/complete_objective()
+ for(var/datum/mind/M in get_owners())
+ to_chat(M.current, "
*gzzt* Authentication success! Welcome, [M.current.name]. Thank you for- for- for-...")
+
+ var/datum/antagonist/mindflayer/flayer_datum = M.has_antag_datum(/datum/antagonist/mindflayer)
+
+ holder.replace_objective(src, flayer_datum.roll_single_human_objective())
+
+ SEND_SOUND(M.current, sound('sound/ambience/alarm4.ogg'))
+ var/list/messages = M.prepare_announce_objectives(FALSE)
+ to_chat(M.current, chat_box_red(messages.Join("
")))
+
+/datum/objective/download/check_completion()
+ return TRUE
+
// Traders
// These objectives have no check_completion, they exist only to tell Sol Traders what to aim for.
diff --git a/code/game/gamemodes/objective_holder.dm b/code/game/gamemodes/objective_holder.dm
index dd7b1710875..8edbae5f949 100644
--- a/code/game/gamemodes/objective_holder.dm
+++ b/code/game/gamemodes/objective_holder.dm
@@ -65,8 +65,10 @@
* Replace old_objective with new_objective
*/
/datum/objective_holder/proc/replace_objective(datum/objective/old_objective, datum/objective/new_objective, datum/original_target_department, list/original_steal_list)
- new_objective.target_department = original_target_department
- new_objective.steal_list = original_steal_list
+ if(!isnull(original_target_department))
+ new_objective.target_department = original_target_department
+ if(!isnull(original_steal_list))
+ new_objective.steal_list = original_steal_list
new_objective = add_objective(new_objective, add_to_list = FALSE)
// Replace where the old objective was, with the new one
objectives.Insert(objectives.Find(old_objective), new_objective)
diff --git a/code/game/machinery/computer/computer.dm b/code/game/machinery/computer/computer.dm
index 27800ac71e4..7ddeb62dada 100644
--- a/code/game/machinery/computer/computer.dm
+++ b/code/game/machinery/computer/computer.dm
@@ -19,6 +19,8 @@
var/force_no_power_icon_state = FALSE
/// Cached list of colors associated with overlays
var/list/cached_emissive_color = list()
+ /// Reference to active download animation effect
+ var/obj/effect/temp_visual/computer_download/active_download_effect
/obj/machinery/computer/Initialize(mapload)
. = ..()
@@ -133,8 +135,18 @@
if(prob(10))
obj_break(ENERGY)
+/obj/machinery/computer/Destroy()
+ if(active_download_effect)
+ qdel(active_download_effect)
+ active_download_effect = null
+ return ..()
+
/obj/machinery/computer/deconstruct(disassembled = TRUE, mob/user)
on_deconstruction()
+ // Destroy flayer download effect if present
+ if(active_download_effect)
+ qdel(active_download_effect)
+ active_download_effect = null
if(!(flags & NODECONSTRUCT))
if(circuit) //no circuit, no computer frame
var/obj/structure/computerframe/A = new /obj/structure/computerframe(loc)
@@ -179,6 +191,51 @@
return FALSE
return ..()
+/obj/machinery/computer/attack_by(obj/item/I, mob/user, params)
+ // Check if someone is trying to Download using a power cable
+ if(!istype(I, /obj/item/apc_powercord) || !ishuman(user))
+ return ..()
+
+ var/mob/living/carbon/human/H = user
+ var/datum/antagonist/mindflayer/flayer_datum = H.mind?.has_antag_datum(/datum/antagonist/mindflayer)
+
+ if(flayer_datum)
+ for(var/datum/objective/download/download_obj in flayer_datum.get_antag_objectives())
+ if(download_obj.target_console == src)
+ var/old_icon_screen = icon_screen
+ to_chat(user, "You insert your power cable into the data port on the console and begin the transfer...")
+ active_download_effect = new /obj/effect/temp_visual/computer_download(get_turf(src), src)
+ if(do_after(user, 18 SECONDS, target = src))
+ download_obj.complete_objective()
+ if(active_download_effect)
+ qdel(active_download_effect)
+ active_download_effect = null
+ icon_screen = old_icon_screen
+ return TRUE
+ else
+ to_chat(user, "Your power cable is ejected, interrupting the transfer.")
+ if(active_download_effect)
+ qdel(active_download_effect)
+ active_download_effect = null
+ icon_screen = old_icon_screen
+ return TRUE
+
+ // Not a flayer, or wrong download console.
+ to_chat(user, "You insert your power cable into the data port on the console, hoping to find something interesting.")
+ if(do_after(user, 18 SECONDS, target = src))
+ show_random_download_message(user)
+ return TRUE
+ else
+ to_chat(user, "Your power cable is ejected, interrupting the transfer.")
+ return TRUE
+
+/obj/machinery/computer/proc/show_random_download_message(mob/user)
+ var/list/download_messages = list(
+ "A message flashes on the screen: \"[user.name] is not in the sudoers file. This incident will be reported.\""
+ )
+ var/message = pick(download_messages)
+ to_chat(user, message)
+
/obj/machinery/computer/screwdriver_act(mob/user, obj/item/I)
. = TRUE
if(!I.tool_start_check(src, user, 0))
@@ -199,3 +256,26 @@
desc = "A computer long since rendered non-functional due to lack of maintenance. \
It is spitting out error messages."
circuit = /obj/item/circuitboard/nonfunctional
+
+// Temp effect for download objective
+/obj/effect/temp_visual/computer_download
+ icon = 'icons/obj/computer.dmi'
+ icon_state = "flayer_downloading_transparent"
+ duration = 180
+ layer = OBJ_LAYER - 0.1
+ randomdir = FALSE
+ var/obj/machinery/computer/target_computer
+
+/obj/effect/temp_visual/computer_download/Initialize(mapload, obj/machinery/computer/computer)
+ if(computer)
+ target_computer = computer
+ setDir(target_computer.dir)
+ pixel_x = target_computer.pixel_x
+ pixel_y = target_computer.pixel_y
+ . = ..()
+
+/obj/effect/temp_visual/computer_download/Destroy()
+ if(target_computer)
+ target_computer.active_download_effect = null
+ target_computer = null
+ return ..()
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index b5a4d8be31c..598d74ee0aa 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -409,14 +409,14 @@ GLOBAL_LIST_EMPTY(antagonists)
* Create and assign a full set of randomized, basic human traitor objectives.
* can_hijack - If you want the 5% chance for the antagonist to be able to roll hijack, only true for traitors
*/
-/datum/antagonist/proc/forge_basic_objectives(can_hijack = FALSE)
+/datum/antagonist/proc/forge_basic_objectives(can_hijack = FALSE, number_of_objectives = GLOB.configuration.gamemode.traitor_objectives_amount)
// Hijack objective.
if(can_hijack && prob(5) && !(locate(/datum/objective/hijack) in owner.get_all_objectives()))
add_antag_objective(/datum/objective/hijack)
return // Hijack should be their only objective (normally), so return.
// Will give normal steal/kill/etc. type objectives.
- for(var/i in 1 to GLOB.configuration.gamemode.traitor_objectives_amount)
+ for(var/i in 1 to number_of_objectives)
forge_single_human_objective()
var/can_succeed_if_dead = TRUE
@@ -447,7 +447,7 @@ GLOBAL_LIST_EMPTY(antagonists)
* After that, add it to the switch list.
* The kill objective pool weight has been done by putting the old code through a million or so runs to figure out averages, to keep it consistant.
*/
-/datum/antagonist/proc/forge_single_human_objective()
+/datum/antagonist/proc/roll_single_human_objective()
var/datum/objective/objective_to_add
var/list/static/the_objective_list = list(KILL_OBJECTIVE = 47, THEFT_OBJECTIVE = 42, INCRIMINATE_OBJECTIVE = 5, PROTECT_OBJECTIVE = 6)
var/list/the_nonstatic_kill_list = list(DEBRAIN_OBJECTIVE = 50, MAROON_OBJECTIVE = 285, ASS_ONCE_OBJECTIVE = 199, ASS_OBJECTIVE = 466)
@@ -488,7 +488,11 @@ GLOBAL_LIST_EMPTY(antagonists)
if(delayed_objectives)
objective_to_add = new /datum/objective/delayed(objective_to_add)
- add_antag_objective(objective_to_add)
+
+ return objective_to_add
+
+/datum/antagonist/proc/forge_single_human_objective()
+ add_antag_objective(roll_single_human_objective())
#undef KILL_OBJECTIVE
#undef THEFT_OBJECTIVE
diff --git a/code/modules/antagonists/mind_flayer/flayer_datum.dm b/code/modules/antagonists/mind_flayer/flayer_datum.dm
index 06365713c6f..e38421f6f3f 100644
--- a/code/modules/antagonists/mind_flayer/flayer_datum.dm
+++ b/code/modules/antagonists/mind_flayer/flayer_datum.dm
@@ -59,7 +59,11 @@
/datum/antagonist/mindflayer/give_objectives()
add_antag_objective(/datum/objective/swarms)
- forge_basic_objectives()
+ if(ismachineperson(owner.current) && prob(25)) // 25% chance to have an objective locked behind Download, only for IPCs
+ add_antag_objective(/datum/objective/download)
+ forge_basic_objectives(FALSE, GLOB.configuration.gamemode.traitor_objectives_amount - 1)
+ else
+ forge_basic_objectives()
/datum/antagonist/mindflayer/exfiltrate(mob/living/carbon/human/extractor, obj/item/radio/radio)
remove_all_abilities()
diff --git a/icons/obj/computer.dmi b/icons/obj/computer.dmi
index 09f58ca93b1..70e467d08df 100644
Binary files a/icons/obj/computer.dmi and b/icons/obj/computer.dmi differ