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