diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 888164e5256..e3e67c82b57 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -146,6 +146,9 @@ /// JSON string file for all of our heretic influence flavors #define HERETIC_INFLUENCE_FILE "antagonist_flavor/heretic_influences.json" +/// JSON file containing spy objectives +#define SPY_OBJECTIVE_FILE "antagonist_flavor/spy_objective.json" + ///employers that are from the syndicate GLOBAL_LIST_INIT(syndicate_employers, list( "Animal Rights Consortium", @@ -265,6 +268,8 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list( #define OBJECTIVE_ITEM_TYPE_NORMAL "normal" /// Only appears in traitor objectives #define OBJECTIVE_ITEM_TYPE_TRAITOR "traitor" +/// Only appears for spy bounties +#define OBJECTIVE_ITEM_TYPE_SPY "spy" // Progression traitor defines @@ -378,3 +383,11 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list( #define BATON_MODES 4 #define FREEDOM_IMPLANT_CHARGES 4 + +// Spy bounty difficulties +/// Can easily be accomplished by any job without any specialized tools, people won't really miss these things +#define SPY_DIFFICULTY_EASY "Easy" +/// Requires some specialized tools, knowledge, or access to accomplish, may require getting into conflict with the crew +#define SPY_DIFFICULTY_MEDIUM "Medium" +/// Very difficult to accomplish, almost guaranteed to require crew conflict +#define SPY_DIFFICULTY_HARD "Hard" diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm index 601f441c66d..38d0500dcbd 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm @@ -112,3 +112,6 @@ #define COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY "movable_edit_unique_submerge_overlay" /// From base of area/Exited(): (area/left, direction) #define COMSIG_MOVABLE_EXITED_AREA "movable_exited_area" + +/// Sent to movables when they are being stolen by a spy: (mob/living/spy, datum/spy_bounty/bounty) +#define COMSIG_MOVABLE_SPY_STEALING "movable_spy_stealing" diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 909399b3c3d..1cf4a1bb3be 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -314,6 +314,7 @@ GLOBAL_LIST_INIT(book_types, typecacheof(list( #define is_captain_job(job_type) (istype(job_type, /datum/job/captain)) #define is_chaplain_job(job_type) (istype(job_type, /datum/job/chaplain)) #define is_clown_job(job_type) (istype(job_type, /datum/job/clown)) +#define is_mime_job(job_type) (istype(job_type, /datum/job/mime)) #define is_detective_job(job_type) (istype(job_type, /datum/job/detective)) #define is_scientist_job(job_type) (istype(job_type, /datum/job/scientist)) #define is_security_officer_job(job_type) (istype(job_type, /datum/job/security_officer)) diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm index 13fdd4d0b80..d4730ce0bb7 100644 --- a/code/__DEFINES/logging.dm +++ b/code/__DEFINES/logging.dm @@ -162,6 +162,7 @@ #define LOG_CATEGORY_UPLINK_HERETIC "uplink-heretic" #define LOG_CATEGORY_UPLINK_MALF "uplink-malf" #define LOG_CATEGORY_UPLINK_SPELL "uplink-spell" +#define LOG_CATEGORY_UPLINK_SPY "uplink-spy" // PDA categories #define LOG_CATEGORY_PDA "pda" diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 90e3f328f6e..d2ddf60ef88 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -16,6 +16,7 @@ #define ROLE_OPERATIVE "Operative" #define ROLE_TRAITOR "Traitor" #define ROLE_WIZARD "Wizard" +#define ROLE_SPY "Spy" // SKYRAT EDIT START #define ROLE_ASSAULT_OPERATIVE "Assault Operative" #define ROLE_OPFOR_CANDIDATE "OPFOR Candidate" @@ -145,6 +146,7 @@ GLOBAL_LIST_INIT(special_roles, list( ROLE_REV_HEAD = 14, ROLE_TRAITOR = 0, ROLE_WIZARD = 14, + ROLE_SPY = 0, // SKYRAT EDIT ADDITION ROLE_ASSAULT_OPERATIVE = 14, // SKYRAT EDIT END diff --git a/code/__DEFINES/uplink.dm b/code/__DEFINES/uplink.dm index d6412e0e4d1..bb92f0672c3 100644 --- a/code/__DEFINES/uplink.dm +++ b/code/__DEFINES/uplink.dm @@ -12,6 +12,9 @@ /// This item is purchasable to infiltrators (midround traitors) #define UPLINK_INFILTRATORS (1 << 3) +/// Can be randomly given to spies for their bounties +#define UPLINK_SPY (1 << 4) + /// Progression gets turned into a user-friendly form. This is just an abstract equation that makes progression not too large. #define DISPLAY_PROGRESSION(time) round(time/60, 0.01) @@ -19,3 +22,12 @@ #define TRAITOR_DISCOUNT_BIG "big_discount" #define TRAITOR_DISCOUNT_AVERAGE "average_discount" #define TRAITOR_DISCOUNT_SMALL "small_discount" + +/// Typepath used for uplink items which don't actually produce an item (essentially just a placeholder) +/// Future todo: Make this not necessary / make uplink items support item-less items natively +#define ABSTRACT_UPLINK_ITEM /obj/effect/gibspawner/generic + +/// Lower threshold for which an uplink items's TC cost is considered "low" for spy bounties picking rewards +#define SPY_LOWER_COST_THRESHOLD 5 +/// Upper threshold for which an uplink items's TC cost is considered "high" for spy bounties picking rewards +#define SPY_UPPER_COST_THRESHOLD 12 diff --git a/code/__HELPERS/logging/antagonists.dm b/code/__HELPERS/logging/antagonists.dm index 3d06bb325ec..5df39c69ade 100644 --- a/code/__HELPERS/logging/antagonists.dm +++ b/code/__HELPERS/logging/antagonists.dm @@ -21,3 +21,7 @@ /// Logging for wizard powers learned /proc/log_spellbook(text, list/data) logger.Log(LOG_CATEGORY_UPLINK_SPELL, text, data) + +/// Logs bounties completed by spies and their rewards +/proc/log_spy(text, list/data) + logger.Log(LOG_CATEGORY_UPLINK_SPY, text, data) diff --git a/code/controllers/subsystem/blackmarket.dm b/code/controllers/subsystem/blackmarket.dm index 357fa0df291..bdd342cbf3d 100644 --- a/code/controllers/subsystem/blackmarket.dm +++ b/code/controllers/subsystem/blackmarket.dm @@ -21,17 +21,20 @@ SUBSYSTEM_DEF(blackmarket) for(var/market in subtypesof(/datum/market)) markets[market] += new market - for(var/item in subtypesof(/datum/market_item)) - var/datum/market_item/I = new item() - if(!I.item) + for(var/datum/market_item/item as anything in subtypesof(/datum/market_item)) + if(!initial(item.item)) + continue + if(!prob(initial(item.availability_prob))) continue - for(var/M in I.markets) - if(!markets[M]) - stack_trace("SSblackmarket: Item [I] available in market that does not exist.") + var/datum/market_item/item_instance = new item() + for(var/potential_market in item_instance.markets) + if(!markets[potential_market]) + stack_trace("SSblackmarket: Item [item_instance] available in market that does not exist.") continue - markets[M].add_item(item) - qdel(I) + // If this fails the market item will just be GC'd + markets[potential_market].add_item(item_instance) + return SS_INIT_SUCCESS /datum/controller/subsystem/blackmarket/fire(resumed) diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm index b74483a2cb6..51ecd59925a 100644 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm @@ -698,3 +698,45 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE) create_separatist_nation(department_type, announcement = FALSE, dangerous = FALSE, message_admins = FALSE) GLOB.round_default_lawset = /datum/ai_laws/united_nations + +/datum/dynamic_ruleset/roundstart/spies + name = "Spies" + antag_flag = ROLE_SPY + antag_datum = /datum/antagonist/spy + minimum_required_age = 0 + protected_roles = list( + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, // AA = bad + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + restricted_roles = list( + JOB_AI, + JOB_CYBORG, + ) + required_candidates = 3 // lives or dies by there being a few spies + weight = 5 + cost = 8 + scaling_cost = 101 // see below + minimum_players = 8 + antag_cap = list("denominator" = 8, "offset" = 1) // should have quite a few spies to work against each other + requirements = list(8, 8, 8, 8, 8, 8, 8, 8, 8, 8) + +/datum/dynamic_ruleset/roundstart/spies/pre_execute(population) + for(var/i in 1 to get_antag_cap(population) * (scaled_times + 1)) + if(length(candidates) <= 0) + break + var/mob/picked_player = pick_n_take(candidates) + assigned += picked_player.mind + picked_player.mind.special_role = ROLE_SPY + picked_player.mind.restricted_roles = restricted_roles + GLOB.pre_setup_antags += picked_player.mind + return TRUE + +/datum/dynamic_ruleset/roundstart/spies/scale_up(population, max_scale) + // Disabled (at least until dynamic can handle scaling this better) + // Because spies have a very low demoninator, this can easily spawn like 30 of them + return 0 diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm index b6179b5fc58..72674705457 100644 --- a/code/datums/mind/antag.dm +++ b/code/datums/mind/antag.dm @@ -106,6 +106,31 @@ var/datum/antagonist/rev/revolutionary = has_antag_datum(/datum/antagonist/rev) revolutionary?.remove_revolutionary() +/** + * Gets an item that can be used as an uplink somewhere on the mob's person. + * + * * desired_location: the location to look for the uplink in. An UPLINK_ define. + * If the desired location is not found, defaults to another location. + * + * Returns the item found, or null if no item was found. + */ +/mob/living/carbon/proc/get_uplink_location(desired_location = UPLINK_PDA) + var/list/all_contents = get_all_contents() + var/obj/item/modular_computer/pda/my_pda = locate() in all_contents + var/obj/item/radio/my_radio = locate() in all_contents + var/obj/item/pen/my_pen = (locate() in my_pda) || (locate() in all_contents) + + switch(desired_location) + if(UPLINK_PDA) + return my_pda || my_radio || my_pen + + if(UPLINK_RADIO) + return my_radio || my_pda || my_pen + + if(UPLINK_PEN) + return my_pen || my_pda || my_radio + + return null /** * ## give_uplink @@ -116,53 +141,26 @@ * * antag_datum: the antag datum of the uplink owner, for storing it in antag memory. optional! */ /datum/mind/proc/give_uplink(silent = FALSE, datum/antagonist/antag_datum) - if(!current) + if(isnull(current)) return var/mob/living/carbon/human/traitor_mob = current if (!istype(traitor_mob)) return - var/list/all_contents = traitor_mob.get_all_contents() - var/obj/item/modular_computer/pda/PDA = locate() in all_contents - var/obj/item/radio/R = locate() in all_contents - var/obj/item/pen/P - - if (PDA) // Prioritize PDA pen, otherwise the pocket protector pens will be chosen, which causes numerous ahelps about missing uplink - P = locate() in PDA - if (!P) // If we couldn't find a pen in the PDA, or we didn't even have a PDA, do it the old way - P = locate() in all_contents - var/obj/item/uplink_loc - var/implant = FALSE - var/uplink_spawn_location = traitor_mob.client?.prefs?.read_preference(/datum/preference/choiced/uplink_location) - var/cant_speak = (HAS_TRAIT(traitor_mob, TRAIT_MUTE) || traitor_mob.mind?.assigned_role.title == JOB_MIME) + var/cant_speak = (HAS_TRAIT(traitor_mob, TRAIT_MUTE) || is_mime_job(assigned_role)) if(uplink_spawn_location == UPLINK_RADIO && cant_speak) if(!silent) to_chat(traitor_mob, span_warning("You have been deemed ineligible for a radio uplink. Supplying standard uplink instead.")) uplink_spawn_location = UPLINK_PDA - switch (uplink_spawn_location) - if(UPLINK_PDA) - uplink_loc = PDA - if(!uplink_loc) - uplink_loc = R - if(!uplink_loc) - uplink_loc = P - if(UPLINK_RADIO) - uplink_loc = R - if(!uplink_loc) - uplink_loc = PDA - if(!uplink_loc) - uplink_loc = P - if(UPLINK_PEN) - uplink_loc = P - if(UPLINK_IMPLANT) - implant = TRUE - if(!uplink_loc) // We've looked everywhere, let's just implant you - implant = TRUE + if(uplink_spawn_location != UPLINK_IMPLANT) + uplink_loc = traitor_mob.get_uplink_location(uplink_spawn_location) + if(istype(uplink_loc, /obj/item/radio) && cant_speak) + uplink_loc = null - if(implant) + if(isnull(uplink_loc)) var/obj/item/implant/uplink/starting/new_implant = new(traitor_mob) new_implant.implant(traitor_mob, null, silent = TRUE) if(!silent) @@ -179,22 +177,27 @@ new_uplink.uplink_handler.owner = traitor_mob.mind new_uplink.uplink_handler.assigned_role = traitor_mob.mind.assigned_role.title new_uplink.uplink_handler.assigned_species = traitor_mob.dna.species.id - if(uplink_loc == R) - unlock_text = "Your Uplink is cunningly disguised as your [R.name]. Simply speak \"[new_uplink.unlock_code]\" into frequency [RADIO_TOKEN_UPLINK] to unlock its hidden features." - add_memory(/datum/memory/key/traitor_uplink, uplink_loc = R.name, uplink_code = new_uplink.unlock_code) - else if(uplink_loc == PDA) - unlock_text = "Your Uplink is cunningly disguised as your [PDA.name]. Simply enter the code \"[new_uplink.unlock_code]\" into the ring tone selection to unlock its hidden features." + + unlock_text = "Your Uplink is cunningly disguised as your [uplink_loc.name]. " + if(istype(uplink_loc, /obj/item/modular_computer/pda)) + unlock_text += "Simply enter the code \"[new_uplink.unlock_code]\" into the ring tone selection to unlock its hidden features." add_memory(/datum/memory/key/traitor_uplink, uplink_loc = "PDA", uplink_code = new_uplink.unlock_code) - else if(uplink_loc == P) + + else if(istype(uplink_loc, /obj/item/radio)) + unlock_text += "Simply speak \"[new_uplink.unlock_code]\" into frequency [RADIO_TOKEN_UPLINK] to unlock its hidden features." + add_memory(/datum/memory/key/traitor_uplink, uplink_loc = uplink_loc.name, uplink_code = new_uplink.unlock_code) + + else if(istype(uplink_loc, /obj/item/pen)) var/instructions = english_list(new_uplink.unlock_code) - unlock_text = "Your Uplink is cunningly disguised as your [P.name]. Simply twist the top of the pen [instructions] from its starting position to unlock its hidden features." - add_memory(/datum/memory/key/traitor_uplink, uplink_loc = "PDA pen", uplink_code = instructions) + unlock_text += "Simply twist the top of the pen [instructions] from its starting position to unlock its hidden features." + add_memory(/datum/memory/key/traitor_uplink, uplink_loc = uplink_loc.name, uplink_code = instructions) new_uplink.unlock_text = unlock_text if(!silent) to_chat(traitor_mob, span_boldnotice(unlock_text)) if(antag_datum) antag_datum.antag_memory += new_uplink.unlock_note + "
" + return . /// Link a new mobs mind to the creator of said mob. They will join any team they are currently on, and will only switch teams when their creator does. /datum/mind/proc/enslave_mind_to_creator(mob/living/creator) diff --git a/code/game/gamemodes/objective_items.dm b/code/game/gamemodes/objective_items.dm index 2240f220e48..46b8ea1640d 100644 --- a/code/game/gamemodes/objective_items.dm +++ b/code/game/gamemodes/objective_items.dm @@ -22,9 +22,23 @@ var/objective_type = OBJECTIVE_ITEM_TYPE_NORMAL /// Whether this item exists on the station map at the start of a round. var/exists_on_map = FALSE + /** + * How hard it is to steal this item given normal circumstances, ranked on a scale of 1 to 5. + * + * 1 - Probably found in a public area + * 2 - Likely on someone's person, or in a less-than-public but otherwise unguarded area + * 3 - Usually on someone's person, or in a locked locker or otherwise secure area + * 4 - Always on someone's person, or in a secure area + * 5 - You know it when you see it. Things like the Nuke Disc which have a pointer to it at all times. + * + * Also accepts 0 as "extremely easy to steal" and >5 as "almost impossible to steal" + */ + var/difficulty = 0 + /// A hint explaining how one may find the target item. + var/steal_hint = "The clown might have one." /// For objectives with special checks (does that intellicard have an ai in it? etcetc) -/datum/objective_item/proc/check_special_completion() +/datum/objective_item/proc/check_special_completion(obj/item/thing) return TRUE /// Takes a list of minds and returns true if this is a valid objective to give to a team of these minds @@ -72,6 +86,8 @@ excludefromjob = list(JOB_BARTENDER) item_owner = list(JOB_BARTENDER) exists_on_map = TRUE + difficulty = 2 + steal_hint = "A double-barrel shotgun usually found on the bartender's person, or if none are around, in the bar's backroom." /obj/item/gun/ballistic/shotgun/doublebarrel/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/gun/ballistic/shotgun/doublebarrel) @@ -91,6 +107,9 @@ JOB_STATION_ENGINEER, ) exists_on_map = TRUE + difficulty = 3 + steal_hint = "Only two of these exist on the station - one in the bridge, and one in atmospherics. \ + You can use a multitool to hack open the case, or break it open the hard way." /obj/item/fireaxe/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/fireaxe) @@ -105,6 +124,8 @@ ) item_owner = list(JOB_ROBOTICIST) exists_on_map = TRUE + difficulty = 2 + steal_hint = "A specialized tool found in the roboticist's lab. You can use a multitool to hack open the case, or break it open the hard way." /obj/item/crowbar/mechremoval/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/crowbar/mechremoval) @@ -115,6 +136,9 @@ excludefromjob = list(JOB_CHAPLAIN) item_owner = list(JOB_CHAPLAIN) exists_on_map = TRUE + difficulty = 2 + steal_hint = "A holy artifact usually found on the chaplain's person, or if none are around, in the chapel's relic closet. \ + If there is a chaplain aboard, it is likely be to be transformed into some holy weapon - some of which are... difficult to remove from their person." /obj/item/nullrod/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/nullrod) @@ -125,6 +149,8 @@ excludefromjob = list(JOB_CLOWN, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER) item_owner = list(JOB_CLOWN) exists_on_map = TRUE + difficulty = 1 + steal_hint = "The clown's huge, bright shoes. They should always be on the clown's feet." /obj/item/clothing/shoes/clown_shoes/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/shoes/clown_shoes) @@ -135,6 +161,8 @@ excludefromjob = list(JOB_MIME, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER) item_owner = list(JOB_MIME) exists_on_map = TRUE + difficulty = 1 + steal_hint = "The mime's mask. It should always be on the mime's face." /obj/item/clothing/mask/gas/mime/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/mask/gas/mime) @@ -145,6 +173,9 @@ excludefromjob = list(JOB_SHAFT_MINER, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER) item_owner = list(JOB_SHAFT_MINER) exists_on_map = TRUE + difficulty = 1 + steal_hint = "A tool primarily used by shaft miners to mine. Most carry one (or multiple) on their person, \ + but they can also be found in the Mining Station, Mining office, or Auxiliary Mining Base on the station." /obj/item/gun/energy/recharge/kinetic_accelerator/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/gun/energy/recharge/kinetic_accelerator) @@ -155,6 +186,8 @@ excludefromjob = list(JOB_COOK, JOB_HEAD_OF_PERSONNEL, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER) item_owner = list(JOB_COOK) exists_on_map = TRUE + difficulty = 1 + steal_hint = "The chef's fake Italian moustache, either found on their face or in the garbage, depending on who's on duty." /obj/item/clothing/mask/fakemoustache/italian/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/mask/fakemoustache/italian) @@ -164,6 +197,9 @@ targetitem = /obj/item/gun/ballistic/revolver/c38/detective excludefromjob = list(JOB_DETECTIVE) exists_on_map = TRUE + difficulty = 3 + steal_hint = "A .38 special revolver found in the Detective's holder. \ + Usually found on the Detective's person, or if none are around, in the detective's locker, in their office." /obj/item/gun/ballistic/revolver/c38/detective/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/gun/ballistic/revolver/c38/detective) @@ -174,6 +210,8 @@ excludefromjob = list(JOB_LAWYER) item_owner = list(JOB_LAWYER) exists_on_map = TRUE + difficulty = 1 + steal_hint = "The lawyer's badge. Usually pinned to their chest, but a spare can be obtained from their clothes vendor." /obj/item/clothing/accessory/lawyers_badge/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/accessory/lawyers_badge) @@ -183,6 +221,8 @@ targetitem = /obj/item/storage/belt/utility/chief excludefromjob = list(JOB_CHIEF_ENGINEER) exists_on_map = TRUE + difficulty = 2 + steal_hint = "The chief engineer's toolbelt, strapped to their waist at all times." /obj/item/storage/belt/utility/chief/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/storage/belt/utility/chief) @@ -199,6 +239,8 @@ JOB_CHIEF_MEDICAL_OFFICER ) exists_on_map = TRUE + difficulty = 3 + steal_hint = "A self-defense weapon standard-issue for all heads of staffs barring the Head of Security. Rarely found off of their person." /obj/item/melee/baton/telescopic/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/melee/baton/telescopic) @@ -209,6 +251,9 @@ excludefromjob = list(JOB_QUARTERMASTER, JOB_CARGO_TECHNICIAN) item_owner = list(JOB_QUARTERMASTER) exists_on_map = TRUE + difficulty = 2 + steal_hint = "A card that grants access to Cargo's funds. \ + Normally found in the locker of the Quartermaster, but a particularly keen one may have it on their person or in their wallet." /obj/item/card/id/departmental_budget/car/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/card/id/departmental_budget/car) @@ -218,6 +263,9 @@ targetitem = /obj/item/mod/control/pre_equipped/magnate excludefromjob = list(JOB_CAPTAIN) exists_on_map = TRUE + difficulty = 3 + steal_hint = "An expensive, hand-crafted MOD unit made for the station's Captain. \ + If not being worn by the Captain, you would find it in the Suit Storage Unit in their quarters." /obj/item/mod/control/pre_equipped/magnate/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/magnate) @@ -234,6 +282,10 @@ JOB_CHIEF_MEDICAL_OFFICER ) exists_on_map = TRUE + difficulty = 4 + steal_hint = "The spare ID of the High Lord himself. \ + If there's no official Captain around, you may find it pinned to the chest of the Acting Captain - one of the Heads of Staff. \ + Otherwise, you'll have to bust open the golden safe on the bridge with acid or explosives to get to it." /obj/item/card/id/advanced/gold/captains_spare/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/card/id/advanced/gold/captains_spare) @@ -246,6 +298,9 @@ targetitem = /obj/item/gun/energy/laser/captain excludefromjob = list(JOB_CAPTAIN) exists_on_map = TRUE + difficulty = 4 + steal_hint = "A self-charging laser gun found in a display case in the Captain's Quarters. \ + Breaking it open may trigger a security alert, so be careful." /obj/item/gun/energy/laser/captain/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/gun/energy/laser/captain) @@ -256,6 +311,9 @@ excludefromjob = list(JOB_HEAD_OF_SECURITY) item_owner = list(JOB_HEAD_OF_SECURITY) exists_on_map = TRUE + difficulty = 4 + steal_hint = "The Head of Security's unique three mode laser gun. \ + Always found on their person, if they are alive, but may otherwise be found in their locker." /obj/item/gun/energy/e_gun/hos/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/gun/energy/e_gun/hos) @@ -266,6 +324,8 @@ excludefromjob = list(JOB_HEAD_OF_SECURITY) item_owner = list(JOB_HEAD_OF_SECURITY) exists_on_map = TRUE + difficulty = 4 + steal_hint = "A miniaturized combat shotgun. May be found in Head of Security's locker or strapped to their back." /obj/item/gun/ballistic/shotgun/automatic/combat/compact/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/gun/ballistic/shotgun/automatic/combat/compact) @@ -276,6 +336,9 @@ excludefromjob = list(JOB_CAPTAIN, JOB_RESEARCH_DIRECTOR, JOB_HEAD_OF_PERSONNEL) item_owner = list(JOB_CAPTAIN, JOB_RESEARCH_DIRECTOR) exists_on_map = TRUE + difficulty = 3 + steal_hint = "Only two of these devices exist on the station, with one sitting in the Teleporter Room \ + for emergencies, and the other in the Captain's Quarters for personal use." /obj/item/hand_tele/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/hand_tele) @@ -286,6 +349,8 @@ excludefromjob = list(JOB_CAPTAIN) item_owner = list(JOB_CAPTAIN) exists_on_map = TRUE + difficulty = 3 + steal_hint = "A special yellow jetpack found in the Suit Storage Unit in the Captain's Quarters." /obj/item/tank/jetpack/oxygen/captain/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/tank/jetpack/oxygen/captain) @@ -296,6 +361,9 @@ excludefromjob = list(JOB_CHIEF_ENGINEER) item_owner = list(JOB_CHIEF_ENGINEER) exists_on_map = TRUE + difficulty = 3 + steal_hint = "A pair of magnetic boots found in the Chief Engineer's Suit Storage Unit. \ + May also be found on their person, concealed beneath their MODsuit." /obj/item/clothing/shoes/magboots/advance/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/shoes/magboots/advance) @@ -306,6 +374,9 @@ excludefromjob = list(JOB_CAPTAIN) item_owner = list(JOB_CAPTAIN) exists_on_map = TRUE + difficulty = 3 + steal_hint = "A gold medal found in the medal box in the Captain's Quarters. \ + The Captain usually also has one pinned to their jumpsuit." /obj/item/clothing/accessory/medal/gold/captain/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/accessory/medal/gold/captain) @@ -318,6 +389,9 @@ excludefromjob = list(JOB_CHIEF_MEDICAL_OFFICER) item_owner = list(JOB_CHIEF_MEDICAL_OFFICER) exists_on_map = TRUE + difficulty = 3 + steal_hint = "The Chief Medical Officer's personal medical injector. \ + Usually found amongst their medical supplies on their person, in their belt, or otherwise in their locker." /obj/item/hypospray/mkii/cmo/add_stealing_item_objective() // SKYRAT EDIT CHANGE return add_item_to_steal(src, /obj/item/hypospray/mkii/cmo) // SKYRAT EDIT CHANGE @@ -326,6 +400,9 @@ name = "the nuclear authentication disk" targetitem = /obj/item/disk/nuclear excludefromjob = list(JOB_CAPTAIN) + difficulty = 5 + steal_hint = "THAT disk - you know the one. Carried by the Captain at all times (hopefully). \ + Difficult to miss, but if you can't find it, the Head of Security and Captain both have devices to track its precise location." /obj/item/disk/nuclear/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/disk/nuclear) @@ -339,6 +416,8 @@ excludefromjob = list(JOB_HEAD_OF_SECURITY, JOB_WARDEN) item_owner = list(JOB_HEAD_OF_SECURITY) exists_on_map = TRUE + difficulty = 4 + steal_hint = "An ablative trechcoat found on the shelves of the Armory." /obj/item/clothing/suit/hooded/ablative/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/suit/hooded/ablative) @@ -349,6 +428,9 @@ excludefromjob = list(JOB_RESEARCH_DIRECTOR) item_owner = list(JOB_RESEARCH_DIRECTOR) exists_on_map = TRUE + difficulty = 3 + steal_hint = "A special suit of armor found in the possession of the Research Director. \ + You may otherwise find it in their locker." /obj/item/clothing/suit/armor/reactive/teleport/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/suit/armor/reactive/teleport) @@ -358,6 +440,11 @@ valid_containers = list(/obj/item/folder) targetitem = /obj/item/documents exists_on_map = TRUE + difficulty = 3 + steal_hint = "A set of papers belonging to a megaconglomerate. \ + Nanotrasen documents can easily be found in the station's vault. \ + For other corporations, you may find them in strange and distant places. \ + A photocopy may also suffice." /obj/item/documents/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/documents) //Any set of secret documents. Doesn't have to be NT's @@ -367,6 +454,8 @@ valid_containers = list(/obj/item/nuke_core_container) targetitem = /obj/item/nuke_core exists_on_map = TRUE + difficulty = 4 + steal_hint = "The core of the station's self-destruct device, found in the vault." /obj/item/nuke_core/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/nuke_core) @@ -381,6 +470,8 @@ excludefromjob = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST, JOB_ROBOTICIST, JOB_GENETICIST) item_owner = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST) exists_on_map = TRUE + difficulty = 4 + steal_hint = "The hard drive of the master research server, found in R&D's server room." /obj/item/computer_disk/hdd_theft/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/computer_disk/hdd_theft) @@ -394,6 +485,8 @@ name = "a sliver of a supermatter crystal" targetitem = /obj/item/nuke_core/supermatter_sliver valid_containers = list(/obj/item/nuke_core_container/supermatter) + difficulty = 5 + steal_hint = "A small shard of the station's supermatter crystal engine." /datum/objective_item/steal/supermatter/New() special_equipment += /obj/item/storage/box/syndie_kit/supermatter @@ -406,6 +499,8 @@ /datum/objective_item/steal/functionalai name = "a functional AI" targetitem = /obj/item/aicard + difficulty = 5 + steal_hint = "An intellicard (or MODsuit) containing an active, functional AI." /datum/objective_item/steal/functionalai/New() . = ..() @@ -439,6 +534,8 @@ item_owner = list(JOB_CHIEF_ENGINEER) altitems = list(/obj/item/photo) exists_on_map = TRUE + difficulty = 3 + steal_hint = "The blueprints of the station, found in the Chief Engineer's locker, or on their person. A picture may suffice." /obj/item/areaeditor/blueprints/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/areaeditor/blueprints) @@ -457,6 +554,8 @@ targetitem = /obj/item/blackbox excludefromjob = list(JOB_CHIEF_ENGINEER, JOB_STATION_ENGINEER, JOB_ATMOSPHERIC_TECHNICIAN) exists_on_map = TRUE + difficulty = 4 + steal_hint = "The station's data Blackbox, found solely within Telecommunications." /obj/item/blackbox/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/blackbox) @@ -470,6 +569,8 @@ excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER) item_owner = list(JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER) exists_on_map = TRUE + difficulty = 1 + steal_hint = "A basic pair of insulated gloves, usually worn by Assistants, Engineers, or Cargo Technicians." /obj/item/clothing/gloves/color/yellow/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/clothing/gloves/color/yellow) @@ -479,6 +580,8 @@ targetitem = /obj/item/toy/plush/moth excludefromjob = list(JOB_PSYCHOLOGIST, JOB_PARAMEDIC, JOB_CHEMIST, JOB_MEDICAL_DOCTOR, JOB_VIROLOGIST, JOB_CHIEF_MEDICAL_OFFICER, JOB_CORONER) exists_on_map = TRUE + difficulty = 1 + steal_hint = "A moth plush toy. The Psychologist has one to help console patients." /obj/item/toy/plush/moth/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/toy/plush/moth) @@ -487,6 +590,8 @@ name = "cute lizard plush toy" targetitem = /obj/item/toy/plush/lizard_plushie exists_on_map = TRUE + difficulty = 1 + steal_hint = "A lizard plush toy. Often found hidden in maintenance." /obj/item/toy/plush/lizard_plushie/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/toy/plush/lizard_plushie) @@ -496,6 +601,8 @@ targetitem = /obj/item/stamp/denied excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_SHAFT_MINER) exists_on_map = TRUE + difficulty = 1 + steal_hint = "Cargo often has multiple of these red stamps lying around to process paperwork." /obj/item/stamp/denied/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/stamp/denied) @@ -505,6 +612,8 @@ targetitem = /obj/item/stamp/granted excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_SHAFT_MINER) exists_on_map = TRUE + difficulty = 1 + steal_hint = "Cargo often has multiple of these green stamps lying around to process paperwork." /obj/item/stamp/granted/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/stamp/granted) @@ -514,6 +623,9 @@ targetitem = /obj/item/book/manual/wiki/security_space_law excludefromjob = list(JOB_SECURITY_OFFICER, JOB_WARDEN, JOB_HEAD_OF_SECURITY, JOB_LAWYER, JOB_DETECTIVE) exists_on_map = TRUE + difficulty = 1 + steal_hint = "Sometimes found in the possession of members of Security and Lawyers. \ + The courtroom and the library are also good places to look." /obj/item/book/manual/wiki/security_space_law/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/book/manual/wiki/security_space_law) @@ -524,6 +636,8 @@ excludefromjob = list(JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER, JOB_SCIENTIST, JOB_RESEARCH_DIRECTOR, JOB_GENETICIST, JOB_ROBOTICIST) item_owner = list(JOB_CHIEF_ENGINEER) exists_on_map = TRUE + difficulty = 1 + steal_hint = "A tool often used by Engineers, Atmospherics Technicians, and Ordnance Technicians." /obj/item/pipe_dispenser/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/pipe_dispenser) @@ -533,6 +647,254 @@ targetitem = /obj/item/storage/fancy/donut_box excludefromjob = list(JOB_CAPTAIN, JOB_CHIEF_ENGINEER, JOB_HEAD_OF_PERSONNEL, JOB_HEAD_OF_SECURITY, JOB_QUARTERMASTER, JOB_CHIEF_MEDICAL_OFFICER, JOB_RESEARCH_DIRECTOR, JOB_SECURITY_OFFICER, JOB_WARDEN, JOB_LAWYER, JOB_DETECTIVE) exists_on_map = TRUE + difficulty = 1 + steal_hint = "Everyone has a box of donuts - you may most commonly find them on the Bridge, within Security, or in any department's break room." /obj/item/storage/fancy/donut_box/add_stealing_item_objective() return add_item_to_steal(src, /obj/item/storage/fancy/donut_box) + +/datum/objective_item/steal/spy + objective_type = OBJECTIVE_ITEM_TYPE_SPY + +/datum/objective_item/steal/spy/lamarr + name = "The Research Director's pet headcrab" + targetitem = /obj/item/clothing/mask/facehugger/lamarr + excludefromjob = list(JOB_RESEARCH_DIRECTOR) + exists_on_map = TRUE + difficulty = 3 + steal_hint = "The Research Director's pet headcrab, Lamarr, found in a secure cage in their office." + +/obj/item/clothing/mask/facehugger/lamarr/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/clothing/mask/facehugger/lamarr) + +/datum/objective_item/steal/spy/disabler + name = "a disabler" + targetitem = /obj/item/gun/energy/disabler + excludefromjob = list( + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + difficulty = 2 + steal_hint = "A hand-held disabler, often found in the possession of Security Officers." + +/datum/objective_item/steal/spy/energy_gun + name = "an energy gun" + targetitem = /obj/item/gun/energy/e_gun + excludefromjob = list( + JOB_CAPTAIN, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_QUARTERMASTER, + JOB_RESEARCH_DIRECTOR, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + exists_on_map = TRUE + difficulty = 2 + steal_hint = "A two-mode energy gun, found in the station's Armory, as well as in the hands of some heads of staff for personal defense." + +/datum/objective_item/steal/spy/energy_gun/check_special_completion(obj/item/thing) + return thing.type == /obj/item/gun/energy/e_gun + +/obj/item/gun/energy/e_gun/add_stealing_item_objective() + if(type == /obj/item/gun/energy/e_gun) + return add_item_to_steal(src, /obj/item/gun/energy/e_gun) + +/datum/objective_item/steal/spy/laser_gun + name = "a laser gun" + targetitem = /obj/item/gun/energy/laser + excludefromjob = list( + JOB_CAPTAIN, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_QUARTERMASTER, + JOB_RESEARCH_DIRECTOR, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + exists_on_map = TRUE + difficulty = 3 + steal_hint = "A simple laser gun, found in the station's Armory." + +/datum/objective_item/steal/spy/laser_gun/check_special_completion(obj/item/thing) + return thing.type == /obj/item/gun/energy/laser + +/obj/item/gun/energy/laser/add_stealing_item_objective() + if(type == /obj/item/gun/energy/laser) + return add_item_to_steal(src, /obj/item/gun/energy/laser) + +/datum/objective_item/steal/spy/shotgun + name = "a riot shotgun" + targetitem = /obj/item/gun/ballistic/shotgun/riot + excludefromjob = list( + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + exists_on_map = TRUE + difficulty = 3 + steal_hint = "A shotgun found in the station's Armory for riot suppression. Doesn't miss." + +/obj/item/gun/ballistic/shotgun/riot/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/gun/ballistic/shotgun/riot) + +/datum/objective_item/steal/spy/temp_gun + name = "security's temperature gun" + targetitem = /obj/item/gun/energy/temperature/security + excludefromjob = list( + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + exists_on_map = TRUE + difficulty = 2 // lowered for the meme + steal_hint = "Security's TRUSTY temperature gun, found in the station's Armory." + +/obj/item/gun/energy/temperature/security/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/gun/energy/temperature/security) + +/datum/objective_item/steal/spy/stamp + name = "a head of staff's stamp" + targetitem = /obj/item/stamp/head + excludefromjob = list( + JOB_CAPTAIN, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_QUARTERMASTER, + JOB_RESEARCH_DIRECTOR, + ) + exists_on_map = TRUE + difficulty = 1 + steal_hint = "A stamp owned by a head of staff, from their offices." + +/obj/item/stamp/head/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/stamp/head) + +/datum/objective_item/steal/spy/sunglasses + name = "sunglasses" + targetitem = /obj/item/clothing/glasses/sunglasses + excludefromjob = list( + JOB_CAPTAIN, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_LAWYER, + JOB_QUARTERMASTER, + JOB_RESEARCH_DIRECTOR, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + difficulty = 1 + steal_hint = "A pair of sunglasses. Lawyers often have a few pairs, as do some heads of staff. \ + You can also obtain a pair from dissassembling hudglasses." + +/datum/objective_item/steal/spy/ce_modsuit + name = "the cheif engineer's advanced MOD control unit" + targetitem = /obj/item/mod/control/pre_equipped/advanced + excludefromjob = list(JOB_CHIEF_ENGINEER) + exists_on_map = TRUE + difficulty = 2 + steal_hint = "An advanced version of the standard Engineering MODsuit commonly worn by the Chief Engineer." + +/obj/item/mod/control/pre_equipped/advanced/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/advanced) + +/datum/objective_item/steal/spy/rd_modsuit + name = "the research director's research MOD control unit" + targetitem = /obj/item/mod/control/pre_equipped/research + excludefromjob = list(JOB_RESEARCH_DIRECTOR) + exists_on_map = TRUE + difficulty = 2 + steal_hint = "A bulky MODsuit commonly worn by the Research Director to protect themselves from the hazards of their work." + +/obj/item/mod/control/pre_equipped/research/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/research) + +/datum/objective_item/steal/spy/cmo_modsuit + name = "the chief medical officer's rescure MOD control unit" + targetitem = /obj/item/mod/control/pre_equipped/rescue + excludefromjob = list(JOB_CHIEF_MEDICAL_OFFICER) + exists_on_map = TRUE + difficulty = 2 + steal_hint = "A MODsuit sometimes equipped by the Chief Medical Officer to perform rescue opperations in hazardous environments." + +/obj/item/mod/control/pre_equipped/rescue/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/rescue) + +/datum/objective_item/steal/spy/hos_modsuit + name = "the head of security's safeguard MOD control unit" + targetitem = /obj/item/mod/control/pre_equipped/safeguard + excludefromjob = list(JOB_HEAD_OF_SECURITY) + exists_on_map = TRUE + difficulty = 2 + steal_hint = "An advanced MODsuit sometimes worn by the Head of Security when needing to detain hostiles invading the station." + +/obj/item/mod/control/pre_equipped/safeguard/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/safeguard) + +/datum/objective_item/steal/spy/stun_baton + name = "a stun baton" + targetitem = /obj/item/melee/baton/security + excludefromjob = list( + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + difficulty = 2 + steal_hint = "Steal any stun baton from Security." + +/datum/objective_item/steal/spy/stun_baton/check_special_completion(obj/item/thing) + return !istype(thing, /obj/item/melee/baton/security/cattleprod) + +/datum/objective_item/steal/spy/det_baton + name = "the detective's baton" + targetitem = /obj/item/melee/baton + excludefromjob = list( + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + exists_on_map = TRUE + difficulty = 2 + steal_hint = "The detective's old wooden truncheon, commonly found on their person for self defense." + +/datum/objective_item/steal/spy/det_baton/check_special_completion(obj/item/thing) + return thing.type == /obj/item/melee/baton + +/obj/item/melee/baton/add_stealing_item_objective() + if(type == /obj/item/melee/baton) + return add_item_to_steal(src, /obj/item/melee/baton) + +/datum/objective_item/steal/spy/captain_sabre_sheathe + name = "the captain's sabre sheathe" + targetitem = /obj/item/storage/belt/sabre + excludefromjob = list(JOB_CAPTAIN) + exists_on_map = TRUE + difficulty = 3 + steal_hint = "The sheathe for the captain's sabre, found in their closet or strapped to their waist at all times." + +/obj/item/storage/belt/sabre/add_stealing_item_objective() + return add_item_to_steal(src, /obj/item/storage/belt/sabre) diff --git a/code/game/objects/items/devices/traitordevices.dm b/code/game/objects/items/devices/traitordevices.dm index 3515e7f52c3..8e8f2578fa4 100644 --- a/code/game/objects/items/devices/traitordevices.dm +++ b/code/game/objects/items/devices/traitordevices.dm @@ -202,9 +202,104 @@ effective or pretty fucking useless. target = round(target) wavelength = clamp(target, 0, 120) +/datum/action/item_action/stealth_mode + name = "Toggle Stealth" + desc = "Makes you invisible to the naked eye." + button_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "ninja_cloak" + /// Whether stealth is active or not + var/stealth_engaged = FALSE + /// The amount of time the stealth mode can be active for, drains to 0 when active + var/charge = 30 SECONDS + /// The maximum amount of time the stealth mode can be active for + var/max_charge = 30 SECONDS + /// The minimum alpha value for the stealth mode + var/min_alpha = 0 + /// Whether the stealth mode recharges while active + /// if TRUE standing in darkness will recharge even while active + /// if FALSE it will not uncharge, but not recharge while in darkness + var/recharge_while_active = TRUE + +/datum/action/item_action/stealth_mode/is_action_active(atom/movable/screen/movable/action_button/current_button) + return stealth_engaged + +/datum/action/item_action/stealth_mode/Grant(mob/grant_to) + . = ..() + START_PROCESSING(SSobj, src) + build_all_button_icons(UPDATE_BUTTON_STATUS) + +/datum/action/item_action/stealth_mode/Remove(mob/remove_from) + if(!isnull(owner) && stealth_engaged) + stealth_off() + STOP_PROCESSING(SSobj, src) + return ..() + +/datum/action/item_action/stealth_mode/Trigger(trigger_flags) + . = ..() + if(!.) + return + + if(stealth_engaged) + stealth_off() + else + stealth_on() + +/datum/action/item_action/stealth_mode/proc/stealth_on() + animate(owner, alpha = get_alpha(), time = 0.5 SECONDS) + apply_wibbly_filters(owner) + stealth_engaged = TRUE + build_all_button_icons(UPDATE_BUTTON_STATUS|UPDATE_BUTTON_BACKGROUND) + owner.balloon_alert(owner, "stealth mode engaged") + +/datum/action/item_action/stealth_mode/proc/stealth_off() + owner.alpha = initial(owner.alpha) + remove_wibbly_filters(owner) + stealth_engaged = FALSE + build_all_button_icons(UPDATE_BUTTON_STATUS|UPDATE_BUTTON_BACKGROUND) + owner.balloon_alert(owner, "stealth mode disengaged") + +/datum/action/item_action/stealth_mode/proc/get_alpha() + return clamp(255 - (255 * charge / max_charge), min_alpha, 255) + +/datum/action/item_action/stealth_mode/process(seconds_per_tick) + if(!stealth_engaged) + // Recharge over time + charge = min(max_charge, charge + (max_charge * 0.04) * seconds_per_tick) + build_all_button_icons(UPDATE_BUTTON_STATUS) + return + + if(charge <= 0) + stealth_off() + return + + var/turf/our_turf = get_turf(owner) + var/lumcount = our_turf?.get_lumcount() || 0 + if(lumcount > 0.3) + // Decay charge while invisible+ in the light + charge = max(0, charge - (max_charge * 0.05) * seconds_per_tick) + build_all_button_icons(UPDATE_BUTTON_STATUS) + + else if(recharge_while_active) + // Return charage while invisible + in the darkness + recharge_while_active + charge = min(max_charge, charge + (max_charge * 0.1) * seconds_per_tick) + build_all_button_icons(UPDATE_BUTTON_STATUS) + + animate(owner, alpha = get_alpha(), time = 1 SECONDS, flags = ANIMATION_PARALLEL) + +/datum/action/item_action/stealth_mode/update_button_status(atom/movable/screen/movable/action_button/current_button, force) + . = ..() + current_button.maptext_x = 9 + current_button.maptext = MAPTEXT_TINY_UNICODE("[round(charge / max_charge * 100, 0.01)]%") + +/datum/action/item_action/stealth_mode/weaker + charge = 15 SECONDS + max_charge = 15 SECONDS + min_alpha = 20 + recharge_while_active = FALSE + /obj/item/shadowcloak name = "cloaker belt" - desc = "Makes you invisible for short periods of time. Recharges in darkness." + desc = "Makes you invisible for short periods of time. Recharges in darkness, even while active." icon = 'icons/obj/clothing/belts.dmi' icon_state = "utility" inhand_icon_state = "utility" @@ -214,66 +309,16 @@ effective or pretty fucking useless. slot_flags = ITEM_SLOT_BELT attack_verb_continuous = list("whips", "lashes", "disciplines") attack_verb_simple = list("whip", "lash", "discipline") - - var/mob/living/carbon/human/user = null - var/charge = 300 - var/max_charge = 300 - var/on = FALSE - actions_types = list(/datum/action/item_action/toggle) - -/obj/item/shadowcloak/ui_action_click(mob/user) - if(user.get_item_by_slot(ITEM_SLOT_BELT) == src) - if(!on) - Activate(usr) - - else - Deactivate() - - return + actions_types = list(/datum/action/item_action/stealth_mode) /obj/item/shadowcloak/item_action_slot_check(slot, mob/user) - if(slot & ITEM_SLOT_BELT) - return 1 + return slot & slot_flags -/obj/item/shadowcloak/proc/Activate(mob/living/carbon/human/user) - if(!user) - return - - to_chat(user, span_notice("You activate [src].")) - src.user = user - START_PROCESSING(SSobj, src) - on = TRUE - -/obj/item/shadowcloak/proc/Deactivate() - to_chat(user, span_notice("You deactivate [src].")) - STOP_PROCESSING(SSobj, src) - if(user) - user.alpha = initial(user.alpha) - - on = FALSE - user = null - -/obj/item/shadowcloak/dropped(mob/user) - ..() - if(user && user.get_item_by_slot(ITEM_SLOT_BELT) != src) - Deactivate() - -/obj/item/shadowcloak/process(seconds_per_tick) - if(user.get_item_by_slot(ITEM_SLOT_BELT) != src) - Deactivate() - return - - var/turf/T = get_turf(src) - if(on) - var/lumcount = T.get_lumcount() - - if(lumcount > 0.3) - charge = max(0, charge - 12.5 * seconds_per_tick)//Quick decrease in light - - else - charge = min(max_charge, charge + 25 * seconds_per_tick) //Charge in the dark - - animate(user,alpha = clamp(255 - charge,0,255),time = 10) +/obj/item/shadowcloak/weaker + name = "stealth belt" + desc = "Makes you nigh-invisible to the naked eye for a short period of time. \ + Lasts indefinitely in darkness, but will not recharge unless inactive." + actions_types = list(/datum/action/item_action/stealth_mode/weaker) /// Checks if a given atom is in range of a radio jammer, returns TRUE if it is. /proc/is_within_radio_jammer_range(atom/source) diff --git a/code/game/objects/items/storage/boxes/security_boxes.dm b/code/game/objects/items/storage/boxes/security_boxes.dm index 8e55986fb40..459c0ab7ce2 100644 --- a/code/game/objects/items/storage/boxes/security_boxes.dm +++ b/code/game/objects/items/storage/boxes/security_boxes.dm @@ -174,6 +174,16 @@ for(var/i in 1 to 7) new /obj/item/ammo_casing/shotgun/buckshot(src) +/obj/item/storage/box/slugs + name = "box of shotgun shells (Lethal - Slugs)" + desc = "A box full of lethal shotgun slugs, designed for shotguns." + icon_state = "breacher_box" + illustration = null + +/obj/item/storage/box/slugs/PopulateContents() + for(var/i in 1 to 7) + new /obj/item/ammo_casing/shotgun(src) + /obj/item/storage/box/beanbag name = "box of shotgun shells (Less Lethal - Beanbag)" desc = "A box full of beanbag shotgun shells, designed for shotguns." diff --git a/code/game/objects/items/storage/medkit.dm b/code/game/objects/items/storage/medkit.dm index e389b990a4c..0ecd943b604 100644 --- a/code/game/objects/items/storage/medkit.dm +++ b/code/game/objects/items/storage/medkit.dm @@ -271,6 +271,24 @@ /obj/item/storage/pill_bottle/penacid = 1) generate_items_inside(items_inside,src) +/obj/item/storage/medkit/tactical_lite + name = "combat first aid kit" + icon_state = "medkit_tactical" + inhand_icon_state = "medkit-tactical" + damagetype_healed = HEAL_ALL_DAMAGE + +/obj/item/storage/medkit/tactical_lite/PopulateContents() + if(empty) + return + var/static/list/items_inside = list( + /obj/item/healthanalyzer/advanced = 1, + /obj/item/reagent_containers/hypospray/medipen/atropine = 1, + /obj/item/stack/medical/gauze = 1, + /obj/item/stack/medical/suture/medicated = 2, + /obj/item/stack/medical/mesh/advanced = 2, + ) + generate_items_inside(items_inside, src) + /obj/item/storage/medkit/tactical name = "combat medical kit" desc = "I hope you've got insurance." diff --git a/code/game/objects/structures/crates_lockers/closets/gimmick.dm b/code/game/objects/structures/crates_lockers/closets/gimmick.dm index 1e7fede5842..fecacd678c7 100644 --- a/code/game/objects/structures/crates_lockers/closets/gimmick.dm +++ b/code/game/objects/structures/crates_lockers/closets/gimmick.dm @@ -39,7 +39,6 @@ /obj/structure/closet/gimmick/tacticool/PopulateContents() ..() new /obj/item/clothing/glasses/eyepatch(src) - new /obj/item/clothing/glasses/sunglasses(src) new /obj/item/clothing/gloves/tackler/combat(src) new /obj/item/clothing/gloves/tackler/combat(src) new /obj/item/clothing/head/helmet/swat(src) @@ -53,6 +52,8 @@ new /obj/item/clothing/under/syndicate/tacticool(src) new /obj/item/clothing/under/syndicate/tacticool(src) +/obj/structure/closet/gimmick/tacticool/populate_contents_immediate() + new /obj/item/clothing/glasses/sunglasses(src) /obj/structure/closet/thunderdome name = "\improper Thunderdome closet" @@ -69,8 +70,6 @@ new /obj/item/clothing/suit/armor/tdome/red(src) for(var/i in 1 to 3) new /obj/item/melee/energy/sword/saber(src) - for(var/i in 1 to 3) - new /obj/item/gun/energy/laser(src) for(var/i in 1 to 3) new /obj/item/melee/baton/security/loaded(src) for(var/i in 1 to 3) @@ -78,6 +77,10 @@ for(var/i in 1 to 3) new /obj/item/clothing/head/helmet/thunderdome(src) +/obj/structure/closet/thunderdome/tdred/populate_contents_immediate() + for(var/i in 1 to 3) + new /obj/item/gun/energy/laser(src) + /obj/structure/closet/thunderdome/tdgreen name = "green-team Thunderdome closet" icon_door = "green" @@ -88,8 +91,6 @@ new /obj/item/clothing/suit/armor/tdome/green(src) for(var/i in 1 to 3) new /obj/item/melee/energy/sword/saber(src) - for(var/i in 1 to 3) - new /obj/item/gun/energy/laser(src) for(var/i in 1 to 3) new /obj/item/melee/baton/security/loaded(src) for(var/i in 1 to 3) @@ -97,6 +98,10 @@ for(var/i in 1 to 3) new /obj/item/clothing/head/helmet/thunderdome(src) +/obj/structure/closet/thunderdome/tdgreen/populate_contents_immediate() + for(var/i in 1 to 3) + new /obj/item/gun/energy/laser(src) + /obj/structure/closet/malf/suits desc = "It's a storage unit for operational gear." icon_state = "syndicate" diff --git a/code/game/objects/structures/crates_lockers/closets/secure/security.dm b/code/game/objects/structures/crates_lockers/closets/secure/security.dm index 63c63c1664a..ad6f95f086c 100644 --- a/code/game/objects/structures/crates_lockers/closets/secure/security.dm +++ b/code/game/objects/structures/crates_lockers/closets/secure/security.dm @@ -16,12 +16,14 @@ new /obj/item/computer_disk/command/captain(src) new /obj/item/radio/headset/heads/captain/alt(src) new /obj/item/radio/headset/heads/captain(src) - new /obj/item/storage/belt/sabre(src) - new /obj/item/gun/energy/e_gun(src) new /obj/item/door_remote/captain(src) new /obj/item/storage/photo_album/captain(src) new /obj/item/card/id/departmental_budget(src) //SKYRAT EDIT ADDITION +/obj/structure/closet/secure_closet/captains/populate_contents_immediate() + new /obj/item/gun/energy/e_gun(src) + new /obj/item/storage/belt/sabre(src) + /obj/structure/closet/secure_closet/hop name = "head of personnel's locker" icon_state = "hop" @@ -38,7 +40,6 @@ new /obj/item/storage/box/silver_ids(src) new /obj/item/megaphone/command(src) new /obj/item/assembly/flash/handheld(src) - new /obj/item/gun/energy/e_gun(src) new /obj/item/clothing/neck/petcollar(src) new /obj/item/pet_carrier(src) new /obj/item/door_remote/civilian(src) @@ -47,6 +48,9 @@ new /obj/item/storage/lockbox/medal/hop(src) new /obj/item/card/id/departmental_budget/srv(src) //SKYRAT EDIT ADDITION +/obj/structure/closet/secure_closet/hop/populate_contents_immediate() + new /obj/item/gun/energy/e_gun(src) + /obj/structure/closet/secure_closet/hos name = "head of security's locker" icon_state = "hos" @@ -308,6 +312,8 @@ new /obj/item/storage/box/firingpins(src) for(var/i in 1 to 3) new /obj/item/storage/box/rubbershot(src) + +/obj/structure/closet/secure_closet/armory2/populate_contents_immediate() for(var/i in 1 to 3) new /obj/item/gun/ballistic/shotgun/riot(src) @@ -321,12 +327,14 @@ ..() new /obj/item/storage/box/firingpins(src) new /obj/item/gun/energy/ionrifle(src) + for(var/i in 1 to 3) + new /obj/item/gun/energy/laser/thermal(src) + +/obj/structure/closet/secure_closet/armory3/populate_contents_immediate() for(var/i in 1 to 3) new /obj/item/gun/energy/e_gun(src) for(var/i in 1 to 3) new /obj/item/gun/energy/laser(src) - for(var/i in 1 to 3) - new /obj/item/gun/energy/laser/thermal(src) /obj/structure/closet/secure_closet/tac name = "armory tac locker" diff --git a/code/modules/antagonists/spy/spy.dm b/code/modules/antagonists/spy/spy.dm new file mode 100644 index 00000000000..e0ea7e40754 --- /dev/null +++ b/code/modules/antagonists/spy/spy.dm @@ -0,0 +1,212 @@ +/datum/antagonist/spy + name = "\improper Spy" + roundend_category = "spies" + antagpanel_category = "Spy" + antag_hud_name = "spy" + job_rank = ROLE_SPY + antag_moodlet = /datum/mood_event/focused + hijack_speed = 1 + ui_name = "AntagInfoSpy" + preview_outfit = /datum/outfit/spy + /// Whether an uplink has been created (successfully or at all) + var/uplink_created = FALSE + /// String displayed in the antag panel pointing the spy to where their uplink is. + var/uplink_location + /// Whether we give them some random objetives to aim for. + var/spawn_with_objectives = TRUE + /// Tracks number of bounties claimed, for roundend + var/bounties_claimed = 0 + /// Tracks all loot items the spy has claimed, for roundend + var/list/all_loot = list() + /// Weakref to our spy uplink + /// Only exists for the sole purpose of letting admins see it + var/datum/weakref/uplink_weakref + +/datum/antagonist/spy/on_gain() + if(!uplink_created) + auto_create_spy_uplink(owner.current) + if(spawn_with_objectives) + give_random_objectives() + . = ..() + SEND_SOUND(owner.current, sound('sound/ambience/antag/spy.ogg')) + +/datum/antagonist/spy/ui_static_data(mob/user) + var/list/data = ..() + data["uplink_location"] = uplink_location + return data + +/datum/antagonist/spy/get_admin_commands() + . = ..() + // I wanted to put this in check-antagonists but it's less conducive to that + .["See All Bounties (For all spies)"] = CALLBACK(src, PROC_REF(see_bounties)) + .["Refresh Bounties (For all spies)"] = CALLBACK(src, PROC_REF(refresh_bounties)) + .["Give Spy Uplink"] = CALLBACK(src, PROC_REF(admin_create_spy_uplink)) + .["Bounty Handler VV"] = CALLBACK(src, PROC_REF(bounty_handler_vv)) + +/datum/antagonist/spy/proc/see_bounties() + if(!check_rights(R_ADMIN|R_DEBUG)) + return + + var/datum/component/spy_uplink/uplink = uplink_weakref?.resolve() + if(isnull(uplink)) + tgui_alert(usr, "No spy uplink!", "Mission Failed") + return + + uplink.ui_interact(usr) + +/datum/antagonist/spy/proc/refresh_bounties() + if(!check_rights(R_ADMIN|R_DEBUG)) + return + + var/datum/component/spy_uplink/uplink = uplink_weakref?.resolve() + if(isnull(uplink)) + tgui_alert(usr, "No spy uplink!", "Mission Failed") + return + + uplink.handler.force_refresh() + tgui_alert(usr, "Bounties refreshed.", "Mission Success") + +/datum/antagonist/spy/proc/admin_create_spy_uplink() + if(!check_rights(R_ADMIN|R_DEBUG)) + return + + if(!auto_create_spy_uplink(owner.current, give_backup = FALSE)) + tgui_alert(usr, "Failed to give [owner.current] a spy uplink - likely don't have a valid item to host it.", "Mission Failed") + +/datum/antagonist/spy/proc/bounty_handler_vv() + if(!check_rights(R_ADMIN|R_DEBUG)) + return + + var/datum/component/spy_uplink/uplink = uplink_weakref?.resolve() + if(isnull(uplink)) + tgui_alert(usr, "No spy uplink!", "Mission Failed") + return + + usr.client?.debug_variables(uplink.handler) + +/datum/antagonist/spy/proc/auto_create_spy_uplink(mob/living/carbon/spy, give_backup = TRUE) + if(!iscarbon(spy)) + return FALSE + + var/spy_uplink_loc = spy.client?.prefs?.read_preference(/datum/preference/choiced/uplink_location) + if(isnull(spy_uplink_loc) || spy_uplink_loc == UPLINK_IMPLANT) + spy_uplink_loc = pick(UPLINK_PEN, UPLINK_PDA) + + var/obj/item/spy_uplink = spy.get_uplink_location(spy_uplink_loc) + if(isnull(spy_uplink) || !create_spy_uplink(spy, spy_uplink)) + if(give_backup) + var/datum/action/backup_uplink/backup = new(src) + backup.Grant(spy) + to_chat(spy, span_boldnotice("You were unable to be supplied with an uplink, so you have been given the ability to create one yourself.")) + return FALSE + + return TRUE + +/datum/antagonist/spy/proc/create_spy_uplink(mob/living/carbon/spy, obj/item/spy_uplink) + var/datum/component/spy_uplink/uplink = spy_uplink.AddComponent(/datum/component/spy_uplink, src) + if(!uplink) + return FALSE + + uplink_weakref = WEAKREF(uplink) + uplink_created = TRUE + + if(istype(spy_uplink, /obj/item/modular_computer/pda)) + uplink_location = "your PDA" + + else if(istype(spy_uplink, /obj/item/pen)) + if(istype(spy_uplink.loc, /obj/item/modular_computer/pda)) + uplink_location = "your PDA's pen" + else + uplink_location = "a pen" + + else if(istype(spy_uplink, /obj/item/radio)) + uplink_location = "your radio headset" + + return TRUE + +/datum/antagonist/spy/proc/give_random_objectives() + for(var/i in 1 to rand(1, 3)) + var/datum/objective/custom/your_mission = new() + your_mission.owner = owner + your_mission.explanation_text = pick_list_replacements(SPY_OBJECTIVE_FILE, "objective_body") + objectives += your_mission + + if(prob(10)) + var/datum/objective/martyr/leave_no_trace = new() + leave_no_trace.owner = owner + objectives += leave_no_trace + + else if(prob(3)) //3% chance on 90% chance + var/datum/objective/hijack/steal_the_shuttle = new() + steal_the_shuttle.owner = owner + objectives += steal_the_shuttle + + else + var/datum/objective/escape/gtfo = new() + gtfo.owner = owner + objectives += gtfo + +/datum/antagonist/spy/antag_panel_data() + return "Bounties Claimed: [bounties_claimed]" + +/datum/antagonist/spy/roundend_report() + var/list/report = list() + report += printplayer(owner) + report += " - They completed [bounties_claimed] bounties." + if(bounties_claimed > 0) + report += " - They received the following rewards: [english_list(all_loot)]" + report += printobjectives(objectives) + return report.Join("
") + +/datum/antagonist/spy/get_preview_icon() + var/mob/living/carbon/human/dummy/consistent/dummy = new() + dummy.set_haircolor(COLOR_SILVER, update = FALSE) + dummy.set_hairstyle("CIA", update = FALSE) + return finish_preview_icon(render_preview_outfit(preview_outfit, dummy)) + +/datum/outfit/spy + name = "Spy (Preview only)" + // Balaclava sprite is ass, otherwise I would use it for this + uniform = /obj/item/clothing/under/suit/black + gloves = /obj/item/clothing/gloves/color/black + shoes = /obj/item/clothing/shoes/jackboots + head = /obj/item/clothing/head/fedora + suit = /obj/item/clothing/suit/jacket/trenchcoat + glasses = /obj/item/clothing/glasses/osi + ears = /obj/item/radio/headset + +/datum/action/backup_uplink + name = "Create Uplink" + desc = "Fashion a PDA, Pen or Radio Headset into a swanky Spy Uplink." + var/list/valid_types = list( + /obj/item/modular_computer/pda, + /obj/item/pen, + /obj/item/radio, + ) + +/datum/action/backup_uplink/New(Target) + . = ..() + if(!istype(Target, /datum/antagonist/spy)) + stack_trace("[type] created on invalid target [Target || "null"]") + qdel(src) + +/datum/action/backup_uplink/Trigger(trigger_flags) + . = ..() + if(!.) + return + + var/mob/living/spy = usr + var/obj/item/held_thing = spy.get_active_held_item() + if(isnull(held_thing)) + spy.balloon_alert(spy, "you need to hold something!") + return + + if(!is_type_in_list(held_thing, valid_types)) + held_thing.balloon_alert(spy, "invalid item!") + return + + var/datum/antagonist/spy/spy_datum = target + spy_datum.create_spy_uplink(spy, held_thing) + held_thing.balloon_alert(spy, "uplink created") + + qdel(src) diff --git a/code/modules/antagonists/spy/spy_bounty.dm b/code/modules/antagonists/spy/spy_bounty.dm new file mode 100644 index 00000000000..035ebba3405 --- /dev/null +++ b/code/modules/antagonists/spy/spy_bounty.dm @@ -0,0 +1,684 @@ +/** + * ## Spy Bounty + * + * A datum used to track a single spy bounty. + * Not a singleton - whenever bounties are re-rolled, the entire list is deleted and new bounty datums are instantiated. + * + * When bounties are completed, they are also not deleted, but instead marked as claimed. + */ +/datum/spy_bounty + /// The name of the bounty. + /// Should be a short description without punctuation. + /// IE: "Steal the captain's ID" + var/name + /// Help text for the bounty. + /// Should include additional information about the bounty to assist the spy in figuring out what to do. + /// Should be punctuated. + /// IE: "Steal the captain's ID. It was last seen in the captain's office." + var/help + /// Difficult of the bounty, one of [SPY_DIFFICULTY_EASY], [SPY_DIFFICULTY_MEDIUM], [SPY_DIFFICULTY_HARD]. + /// Must be set to one of the possible bounties to be picked. + var/difficulty = "unset" + /// How long of a do-after must be completed by the Spy to turn in the bounty. + var/theft_time = 2 SECONDS + /// Probability that the stolen item will be sent to the black market instead of destroyed. + /// Guaranteed if the item is indestructible. + var/black_market_prob = 50 + /// Weight that the bounty will be selected. + var/weight = 1 + + /// Whether the bounty's been fully initialized. If this is not set, the bounty will be rerolled. + VAR_FINAL/initalized = FALSE + /// Whether the bounty has been completed. + VAR_FINAL/claimed = FALSE + /// What uplink item the bounty will reward on completion. + VAR_FINAL/datum/uplink_item/reward_item + +/datum/spy_bounty/New(datum/spy_bounty_handler/handler) + if(!init_bounty(handler)) + return + + initalized = TRUE + select_reward(handler) + +/// Helper that translates the bounty into UI data for TGUI +/datum/spy_bounty/proc/to_ui_data(mob/user) + SHOULD_CALL_PARENT(TRUE) + return list( + "name" = name, + "help" = help, + "difficulty" = difficulty, + "reward" = reward_item.name, + "claimed" = claimed, + "can_claim" = can_claim(user), + ) + +/// Check if the passed mob can claim this bounty. +/datum/spy_bounty/proc/can_claim(mob/user) + SHOULD_BE_PURE(TRUE) + return TRUE + +/** + * Initializes the bounty, setting up targets and etc. + * + * * handler - The bounty handler that is creating this bounty. + * + * Returning FALSE will cancel initialization and force it to reroll the bounty. + */ +/datum/spy_bounty/proc/init_bounty(datum/spy_bounty_handler/handler) + return FALSE + +/// Selects what uplink item the bounty will reward on completion. +/datum/spy_bounty/proc/select_reward(datum/spy_bounty_handler/handler) + var/list/loot_pool = handler.possible_uplink_items[difficulty] + + if(!length(loot_pool)) + reward_item = /datum/uplink_item/bundles_tc/telecrystal + return // future todo : add some junk items for when we run out of items + + reward_item = pick(loot_pool) + if(prob(80)) + loot_pool -= reward_item + +/** + * Checks if the passed movable is a valid target for this bounty. + * + * * stealing - The movable to check. + * + * Returning FALSE simply means that the passed movable is not valid for this bounty. + */ +/datum/spy_bounty/proc/is_stealable(atom/movable/stealing) + // SHOULD_BE_PURE(TRUE) + return FALSE + +/** + * What is this bounty's "dupe protection key"? + * This is used to determine if a duplicate of this bounty has been rolled before / in the last refresh. + * You can check if a bounty has been duped by accessing the handler's claimed_bounties_from_last_pool or all_claimed_bounty_types list. + * + * * stealing - The item that was stolen. + * * handler - The handler that is handling the bounty. + * + * Return a string key, what this uses for dupe protection. + */ +/datum/spy_bounty/proc/get_dupe_protection_key(atom/movable/stealing) + return stealing.type + +/** + * Checks if the passed dupe key is a duplicate of an previously claimed bounty. + * + * * handler - The handler that is handling the bounty. + * * dupe_key - The key to check for dupes + * * dupe_prob - The probability of a dupe being allowed when checking all_claimed_bounty_types. + * This allows you to have a chance that distant dupes allowed depending on how many times they've been done. + * + * Returns TRUE if the bounty is a dupe, FALSE if it is not. + */ +/datum/spy_bounty/proc/check_dupe(datum/spy_bounty_handler/handler, dupe_key, dupe_prob = 0) + if(handler.claimed_bounties_from_last_pool[dupe_key]) + return TRUE + if(prob(dupe_prob * handler.all_claimed_bounty_types[dupe_key])) + return TRUE + return FALSE + +/** + * Called when the bounty is completed, to handle how the stolen item is "stolen". + * + * By default, stolen items are simply deleted. + * + * * stealing - The item that was stolen. + * * spy - The spy that stole the item. + */ +/datum/spy_bounty/proc/clean_up_stolen_item(atom/movable/stealing, mob/living/spy) + do_sparks(3, FALSE, stealing) + + // Don't mess with it while it's going away + stealing.interaction_flags_atom &= ~INTERACT_ATOM_ATTACK_HAND + stealing.anchored = TRUE + // Add some pizzazz + animate(stealing, time = 0.5 SECONDS, transform = matrix(stealing.transform).Scale(0.01), easing = CUBIC_EASING) + + if(isitem(stealing) && ((stealing.resistance_flags & INDESTRUCTIBLE) || prob(black_market_prob))) + addtimer(CALLBACK(src, PROC_REF(send_to_black_market), stealing), 0.5 SECONDS) + else + QDEL_IN(stealing, 0.5 SECONDS) + +/** + * Handles putting the passed movable up on the black market. + * + * By the end of this proc, the item should either be deleted (if failure) or in nullspace (on the black market). + * + * * thing - The item to put up on the black market. + */ +/datum/spy_bounty/proc/send_to_black_market(atom/movable/thing) + if(QDELETED(thing)) // Just in case anything does anything weird + return FALSE + + thing.interaction_flags_atom = initial(thing.interaction_flags_atom) + thing.anchored = initial(thing.anchored) + thing.moveToNullspace() + + var/datum/market_item/new_item = new() + new_item.item = thing + new_item.name = "Stolen [thing.name]" + new_item.desc = "A [thing.name], stolen from somewhere on the station. Whoever owned it probably wouldn't be happy to see it here." + new_item.category = "Fenced Goods" + new_item.stock = 1 + new_item.availability_prob = 100 + + switch(difficulty) + if(SPY_DIFFICULTY_EASY) + new_item.price = PAYCHECK_COMMAND * 2.5 + if(SPY_DIFFICULTY_MEDIUM) + new_item.price = PAYCHECK_COMMAND * 5 + if(SPY_DIFFICULTY_HARD) + new_item.price = PAYCHECK_COMMAND * 10 + + new_item.price += rand(0, PAYCHECK_COMMAND * 5) + if(thing.resistance_flags & INDESTRUCTIBLE) + new_item.price *= 2 + + return SSblackmarket.markets[/datum/market/blackmarket].add_item(new_item) + +/// Steal an item +/datum/spy_bounty/objective_item + /// Reference to an objective item datum that we want stolen. + VAR_FINAL/datum/objective_item/desired_item + /// Typecache of objective items that should not be selected. + var/static/list/blacklisted_item_types = typecacheof(list( + /datum/objective_item/steal/functionalai, + /datum/objective_item/steal/nukedisc, + )) + +/datum/spy_bounty/objective_item/can_claim(mob/user) + return !(user.mind?.assigned_role.title in desired_item.excludefromjob) + +/datum/spy_bounty/objective_item/get_dupe_protection_key(atom/movable/stealing) + return desired_item.targetitem + +/// Determines if the passed objective item is a reasonable, valid theft target. +/datum/spy_bounty/objective_item/proc/is_valid_objective_item(datum/objective_item/item) + if(length(item.special_equipment) || item.difficulty <= 0 || item.difficulty >= 6) + return FALSE + if(is_type_in_typecache(item, blacklisted_item_types)) + return FALSE + if(!item.exists_on_map) + return TRUE + var/list/all_valid_existing_things = list() + for(var/obj/item/existing_thing as anything in GLOB.steal_item_handler.objectives_by_path[item.targetitem]) + var/turf/thing_turf = get_turf(existing_thing) + if(isnull(thing_turf)) // nullspaced likely means it was stolen and is in the black market. + continue + if(!is_station_level(thing_turf.z) && !is_mining_level(thing_turf.z)) + continue + all_valid_existing_things += existing_thing + + if(!length(all_valid_existing_things)) + return FALSE + return TRUE + +/datum/spy_bounty/objective_item/init_bounty(datum/spy_bounty_handler/handler) + var/list/valid_possible_items = list() + for(var/datum/objective_item/item as anything in GLOB.possible_items) + if(check_dupe(handler, item.targetitem, 33)) + continue + if(!is_valid_objective_item(item)) + continue + // Determine difficulty. Has some overlap between the categories, which is OK + switch(difficulty) + if(SPY_DIFFICULTY_EASY) + if(item.difficulty >= 3) + continue + if(SPY_DIFFICULTY_MEDIUM) + if(item.difficulty <= 2 || item.difficulty >= 5) + continue + if(SPY_DIFFICULTY_HARD) + if(item.difficulty <= 3) + continue + + valid_possible_items += item + + for(var/datum/spy_bounty/objective_item/existing_bounty in handler.get_all_bounties()) + valid_possible_items -= existing_bounty.desired_item + + if(!length(valid_possible_items)) + return FALSE + + desired_item = pick(valid_possible_items) + // We need to do some snowflake for items that do exist vs generic items + var/list/obj/item/existing_items = GLOB.steal_item_handler.objectives_by_path[desired_item.targetitem] + var/obj/item/the_item = length(existing_items) ? pick(existing_items) : desired_item.targetitem + var/the_item_name = istype(the_item) ? the_item.name : initial(the_item.name) + name = "[the_item_name] [difficulty == SPY_DIFFICULTY_HARD ? "Grand ":""]Theft" + help = "Steal any [the_item_name][desired_item.steal_hint ? ": [desired_item.steal_hint]" : "."]" + return TRUE + +/datum/spy_bounty/objective_item/is_stealable(atom/movable/stealing) + return istype(stealing, desired_item.targetitem) && desired_item.check_special_completion(stealing) + +/datum/spy_bounty/objective_item/random_easy + difficulty = SPY_DIFFICULTY_EASY + weight = 4 // Increased due to there being many easy options + +/datum/spy_bounty/objective_item/random_medium + difficulty = SPY_DIFFICULTY_MEDIUM + weight = 2 // Increased due to there being many medium options + +/datum/spy_bounty/objective_item/random_hard + difficulty = SPY_DIFFICULTY_HARD + +/datum/spy_bounty/machine + theft_time = 10 SECONDS + + /// What machine (typepath) we want to steal. + var/obj/machinery/target_type + /// What area (typepath) the desired machine is in. + /// Can be pre-set for subtypes. If set, requires the machine to be in the location_type. + /// If not set, picks a random machine from all areas it can currently be found in. + var/area/location_type + /// List of weakrefs to all machines of the target type when the bounty was initialized. + var/list/original_options_weakrefs = list() + +/datum/spy_bounty/machine/Destroy() + original_options_weakrefs.Cut() // Just in case + return ..() + +/datum/spy_bounty/machine/get_dupe_protection_key(atom/movable/stealing) + return target_type + +/datum/spy_bounty/machine/send_to_black_market(obj/machinery/thing) + if(!istype(thing.circuit, /obj/item/circuitboard)) + qdel(thing) + return FALSE + + var/obj/item/circuitboard/selling = thing.circuit + var/turf/machine_turf = get_turf(thing) + + // Sell the circuitboard, take the rest apart + // This (should) handle any mobs inside as well + thing.deconstruct(FALSE) + if(!..(selling)) + return FALSE + + // Clean up leftover parts from deconstruction + for(var/obj/structure/frame/leftover in machine_turf) + qdel(leftover) + break + for(var/obj/item/stock_parts/part in machine_turf) + if(prob(part.rating * 20)) + continue + qdel(part) + + return TRUE + +/datum/spy_bounty/machine/init_bounty(datum/spy_bounty_handler/handler) + if(isnull(target_type)) + return FALSE + + // Blacklisting maintenance in general, as well as any areas that already have a bounty in them. + var/list/blacklisted_areas = typecacheof(/area/station/maintenance) + for(var/datum/spy_bounty/machine/existing_bounty in handler.get_all_bounties()) + blacklisted_areas[existing_bounty.location_type] = TRUE + + var/list/obj/machinery/all_possible = list() + for(var/obj/machinery/found_machine as anything in SSmachines.get_machines_by_type_and_subtypes(target_type)) + if(!is_station_level(found_machine.z) && !is_mining_level(found_machine.z)) + continue + var/area/found_machine_area = get_area(found_machine) + if(is_type_in_typecache(found_machine_area, blacklisted_areas)) + continue + if(!isnull(location_type) && !istype(found_machine_area, location_type)) + continue + if(!(found_machine_area.area_flags & VALID_TERRITORY)) // only steal from valid station areas + continue + all_possible += found_machine + + if(!length(all_possible)) + return FALSE + + var/obj/machinery/machine = pick_n_take(all_possible) + var/area/machine_area = get_area(machine) + // Tracks the picked machine, as well as any other machines in the same area + // (So they can be removed from the room but still count, for clever Spies) + original_options_weakrefs += WEAKREF(machine) + for(var/obj/machinery/other_machine as anything in all_possible) + if(get_area(other_machine) == machine_area) + original_options_weakrefs += WEAKREF(other_machine) + + location_type = machine_area.type + name ||= "[machine.name] Burglary" + help ||= "Steal \a [machine] found in [machine_area]." + return TRUE + +/datum/spy_bounty/machine/is_stealable(atom/movable/stealing) + if(!istype(stealing, target_type)) + return FALSE + if(WEAKREF(stealing) in original_options_weakrefs) + return TRUE + if(istype(get_area(stealing), location_type)) + return TRUE + return FALSE + +/datum/spy_bounty/machine/random + /// List of all machines we can randomly draw from + var/list/random_options = list() + +/datum/spy_bounty/machine/random/init_bounty(datum/spy_bounty_handler/handler) + var/list/options = random_options.Copy() + for(var/datum/spy_bounty/machine/existing_bounty in handler.get_all_bounties()) + options -= existing_bounty.target_type + + for(var/remaining_option in options) + if(check_dupe(handler, remaining_option, 33)) + options -= remaining_option + + if(!length(options)) + return FALSE + + target_type = pick(options) + return ..() + +/datum/spy_bounty/machine/random/easy + difficulty = SPY_DIFFICULTY_EASY + weight = 4 // Increased due to there being many easy options + random_options = list( + /obj/machinery/computer/operating, + /obj/machinery/computer/order_console/mining, + /obj/machinery/computer/records/medical, + /obj/machinery/cryo_cell, + /obj/machinery/fax, // Completely random, wild card + /obj/machinery/hydroponics/constructable, + /obj/machinery/medical_kiosk, + /obj/machinery/microwave, + /obj/machinery/oven, + /obj/machinery/recharge_station, + /obj/machinery/vending/boozeomat, + /obj/machinery/vending/medical, + /obj/machinery/vending/wardrobe, + ) + +/datum/spy_bounty/machine/random/medium + difficulty = SPY_DIFFICULTY_MEDIUM + weight = 4 // Increased due to there being many medium options + random_options = list( + /obj/machinery/chem_dispenser, + /obj/machinery/computer/bank_machine, + /obj/machinery/computer/camera_advanced/xenobio, + /obj/machinery/computer/cargo, // This includes request-only ones in the public lobby + /obj/machinery/computer/crew, + /obj/machinery/computer/prisoner/management, + /obj/machinery/computer/rdconsole, + /obj/machinery/computer/records/security, + /obj/machinery/computer/scan_consolenew, + /obj/machinery/computer/security, // Requires breaking into a sec checkpoint, but not too hard, many are never visited + /obj/machinery/dna_scannernew, + /obj/machinery/mecha_part_fabricator, + ) + +/datum/spy_bounty/machine/engineering_emitter + difficulty = SPY_DIFFICULTY_MEDIUM + target_type = /obj/machinery/power/emitter + location_type = /area/station/engineering/supermatter/ + +/datum/spy_bounty/machine/engineering_emitter/can_claim(mob/user) + return !(user.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING) + +/datum/spy_bounty/machine/random/hard + difficulty = SPY_DIFFICULTY_HARD + random_options = list( + /obj/machinery/computer/accounting, + /obj/machinery/computer/communications, + /obj/machinery/computer/upload, + /obj/machinery/modular_computer/preset/id, + ) + +/datum/spy_bounty/machine/random/hard/can_claim(mob/user) // These would all be too easy with command level access + return !(user.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) + +/datum/spy_bounty/machine/random/hard/ai_sat_teleporter + random_options = list( + /obj/machinery/teleport, + /obj/machinery/computer/teleporter. + ) + location_type = /area/station/ai_monitored/aisat + +/// Subtype for a bounty that targets a specific crew member +/datum/spy_bounty/targets_person + difficulty = SPY_DIFFICULTY_HARD + theft_time = 12 SECONDS + /// Weakref to the mob target of the bounty + VAR_FINAL/datum/weakref/target_ref + +/datum/spy_bounty/targets_person/get_dupe_protection_key(atom/movable/stealing) + // Prevents the same player from being selected twice, but if they're straight up gone, whatever + return REF(target_ref.resolve() || stealing) + +/datum/spy_bounty/targets_person/can_claim(mob/user) + return !IS_WEAKREF_OF(user, target_ref) + +/datum/spy_bounty/targets_person/init_bounty(datum/spy_bounty_handler/handler) + var/list/mob/possible_targets = list() + for(var/datum/mind/crew_mind as anything in get_crewmember_minds()) + var/mob/living/real_target = crew_mind.current + // Ideally we want it to be a player, but we don't care if they DC after being selected + if(!istype(real_target) || isnull(GET_CLIENT(real_target))) + continue + if(check_dupe(handler, REF(real_target), 50)) + continue + if(!is_valid_crewmember(real_target)) + continue + possible_targets += real_target + + for(var/datum/spy_bounty/targets_person/existing_bounty in handler.get_all_bounties()) + possible_targets -= existing_bounty.target_ref.resolve() + + if(!length(possible_targets)) + return FALSE + + var/mob/picked = pick(possible_targets) + if(target_found(picked)) + target_ref = WEAKREF(picked) + return TRUE + + return FALSE + +/** + * Ran on every single member of the crew to determine if they are a valid target. + * + * * crewmember - The person to check. + * + * Returning FALSE will exclude them from the list of possible targets. + */ +/datum/spy_bounty/targets_person/proc/is_valid_crewmember(mob/crewmember) + return FALSE + +/** + * Ran when a valid target is selected for the bounty. + * + * * crewmember - The person that was selected as the target. + * + * Returning FALSE will stop the bounty from being finalized, this can be used for last minute checks. + */ +/datum/spy_bounty/targets_person/proc/target_found(mob/crewmember) + return FALSE + +/// Subtype for a bounty that targets a specific crew member and a specific item on them +/datum/spy_bounty/targets_person/some_item + /// Typepath of the item we want from the target + var/obj/item/desired_type + /// Weakref to the item that matches our desired type within the target at the time of bounty creation + VAR_FINAL/datum/weakref/target_original_desired_ref + +/datum/spy_bounty/targets_person/some_item/is_valid_crewmember(mob/living/carbon/human/crewmember) + return istype(crewmember) && find_desired_thing(crewmember) + +/datum/spy_bounty/targets_person/some_item/is_stealable(atom/movable/stealing) + if(IS_WEAKREF_OF(stealing, target_original_desired_ref)) + return TRUE + if(IS_WEAKREF_OF(stealing, target_ref)) + var/mob/living/carbon/human/target = stealing + if(!target.incapacitated(IGNORE_RESTRAINTS|IGNORE_STASIS)) + return FALSE + if(find_desired_thing(target)) + return TRUE + return FALSE + +/datum/spy_bounty/targets_person/some_item/clean_up_stolen_item(atom/movable/stealing, mob/living/spy) + if(IS_WEAKREF_OF(stealing, target_original_desired_ref)) + return ..() + + ASSERT(ishuman(stealing), "[type] called clean_up_stolen_item with something that isn't a human and isn't the original item.") + + do_sparks(2, FALSE, stealing) + var/mob/living/carbon/human/stolen_from = stealing + var/obj/item/real_stolen_item = find_desired_thing(stealing) + stolen_from.Unconscious(10 SECONDS) + to_chat(stolen_from, span_warning("You feel something missing where your [real_stolen_item.name] once was.")) + return ..(real_stolen_item, spy) + +/datum/spy_bounty/targets_person/some_item/target_found(mob/crewmember) + var/obj/item/desired_thing = find_desired_thing(crewmember) + target_original_desired_ref = WEAKREF(desired_thing) + name = "[crewmember.real_name]'s [desired_thing.name]" + help = "Steal [desired_thing] from [crewmember.real_name]. \ + You can accomplish this via brute force, or by scanning them with your uplink while they are incapacitated." + return TRUE + +/// Finds the desired item type in the target crewmember. +/datum/spy_bounty/targets_person/some_item/proc/find_desired_thing(mob/living/carbon/human/crewmember) + return locate(desired_type) in crewmember.get_all_gear() + +// Steal someone's ID card +/datum/spy_bounty/targets_person/some_item/id + desired_type = /obj/item/card/id/advanced + +/datum/spy_bounty/targets_person/some_item/id/find_desired_thing(mob/living/carbon/human/crewmember) + for(var/obj/item/card/id/advanced/id in crewmember.get_all_gear()) + if(id.registered_account?.account_id == crewmember.account_id) + return id + + return null + +/datum/spy_bounty/targets_person/some_item/id/target_found(mob/crewmember) + . = ..() + name = "[crewmember.real_name]'s ID Card" + +// Steal someone's PDA +/datum/spy_bounty/targets_person/some_item/pda + desired_type = /obj/item/modular_computer/pda + +/datum/spy_bounty/targets_person/some_item/pda/find_desired_thing(mob/living/carbon/human/crewmember) + for(var/obj/item/modular_computer/pda/pda in crewmember.get_all_gear()) + if(pda.saved_identification == crewmember.real_name) + return pda + + return null + +/datum/spy_bounty/targets_person/some_item/pda/target_found(mob/crewmember) + . = ..() + name = "[crewmember.real_name]'s PDA" + +// Steal someone's heirloom +/datum/spy_bounty/targets_person/some_item/heirloom + desired_type = /obj/item + black_market_prob = 100 + +/datum/spy_bounty/targets_person/some_item/heirloom/find_desired_thing(mob/living/crewmember) + var/datum/quirk/item_quirk/family_heirloom/quirk = crewmember.get_quirk(/datum/quirk/item_quirk/family_heirloom) + return quirk?.heirloom?.resolve() + +/datum/spy_bounty/targets_person/some_item/heirloom/target_found(mob/crewmember) + . = ..() + name = "[crewmember.real_name]'s heirloom" + +// Steal a limb or organ off someone +/datum/spy_bounty/targets_person/some_item/limb_or_organ + weight = 4 // lots to pick from here + +/datum/spy_bounty/targets_person/some_item/limb_or_organ/init_bounty(datum/spy_bounty_handler/handler) + desired_type = pick( + /obj/item/bodypart/arm/left, + /obj/item/bodypart/arm/right, + /obj/item/bodypart/leg/left, + /obj/item/bodypart/leg/right, + /obj/item/organ/internal/stomach, + /obj/item/organ/internal/appendix, + /obj/item/organ/internal/liver, + /obj/item/organ/internal/eyes, + ) + return ..() + +/datum/spy_bounty/targets_person/some_item/limb_or_organ/find_desired_thing(mob/living/carbon/human/crewmember) + if(ispath(desired_type, /obj/item/bodypart)) + return locate(desired_type) in crewmember.bodyparts + if(ispath(desired_type, /obj/item/organ)) + return locate(desired_type) in crewmember.organs + return null + +/datum/spy_bounty/some_bot + theft_time = 10 SECONDS + black_market_prob = 0 + /// What typepath of bot we want to steal. + var/mob/living/simple_animal/bot/bot_type + /// Weakref to the bot we want to steal. + VAR_FINAL/datum/weakref/target_bot_ref + +/datum/spy_bounty/some_bot/get_dupe_protection_key(atom/movable/stealing) + return bot_type + +/datum/spy_bounty/some_bot/init_bounty(datum/spy_bounty_handler/handler) + for(var/datum/spy_bounty/some_bot/existing_bounty in handler.get_all_bounties()) + var/mob/living/simple_animal/bot/existing_bot_type = existing_bounty.bot_type + // ensures we don't get two similar bounties. + // may occasionally cast a wider net than we'd desire, but it's not that bad. + if(ispath(bot_type, initial(existing_bot_type.parent_type))) + return FALSE + + var/list/mob/living/possible_bots = list() + for(var/mob/living/bot as anything in GLOB.bots_list) + if(!is_station_level(bot.z) && !is_mining_level(bot.z)) + continue + if(!istype(bot, bot_type)) + continue + possible_bots += bot + + if(!length(possible_bots)) + return FALSE + + var/mob/living/picked = pick(possible_bots) + target_bot_ref = WEAKREF(picked) + name ||= "[picked.name] Abduction" + help ||= "Abduct the station's robot assistant [picked.name]." + return TRUE + +/datum/spy_bounty/some_bot/is_stealable(atom/movable/stealing) + return IS_WEAKREF_OF(stealing, target_bot_ref) + +/datum/spy_bounty/some_bot/beepsky + difficulty = SPY_DIFFICULTY_MEDIUM // gotta get him to stand still + bot_type = /mob/living/simple_animal/bot/secbot/beepsky/officer + help = "Abduct Officer Beepsky - commonly found patrolling the station. \ + Watch out, they may not take kindly to being scanned." + +/datum/spy_bounty/some_bot/ofitser + difficulty = SPY_DIFFICULTY_EASY + bot_type = /mob/living/simple_animal/bot/secbot/beepsky/ofitser + help = "Abduct Prison Ofitser - commonly found guarding the Gulag." + +/datum/spy_bounty/some_bot/armsky + difficulty = SPY_DIFFICULTY_HARD + bot_type = /mob/living/simple_animal/bot/secbot/beepsky/armsky + help = "Abduct Sergeant-At-Armsky - commonly found guarding the station's Armory." + +/datum/spy_bounty/some_bot/pingsky + difficulty = SPY_DIFFICULTY_HARD + bot_type = /mob/living/simple_animal/bot/secbot/pingsky + help = "Abduct Officer Pingsky - commonly found protecting the station's AI." + +/datum/spy_bounty/some_bot/scrubbs + difficulty = SPY_DIFFICULTY_EASY + bot_type = /mob/living/basic/bot/cleanbot/medbay + help = "Abduct Scrubbs, MD - commonly found mopping up blood in Medbay." + +/datum/spy_bounty/some_bot/scrubbs/can_claim(mob/user) + return !(user.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL) diff --git a/code/modules/antagonists/spy/spy_bounty_handler.dm b/code/modules/antagonists/spy/spy_bounty_handler.dm new file mode 100644 index 00000000000..798719cb8a0 --- /dev/null +++ b/code/modules/antagonists/spy/spy_bounty_handler.dm @@ -0,0 +1,123 @@ +/** + * ## Spy bounty handler + * + * Singleton datum that handles determining active bounties for spies. + */ +/datum/spy_bounty_handler + /// Timer between when all bounties are refreshed. + var/refresh_time = 12 MINUTES + /// timerID of the active refresh timer. + var/refresh_timer + /// Number of times we have refreshed bounties + var/num_refreshes = 0 + /// Assoc list of items stolen in the past to how many times they have been stolen + /// Sometimes item typepaths, sometimes REFs, in general just strings that represent stolen items + var/list/all_claimed_bounty_types = list() + /// List of all items stolen in the last pool of bounties. + /// Same as above - strings that represent stolen items. + var/list/claimed_bounties_from_last_pool = list() + /// Override for the number of attempts to make a bounty. + var/num_attempts_override = 0 + + /// Assoc list that dictates how much of each bounty difficulty to give out at once. + /// Modified by the number of times we have refreshed bounties. + VAR_PRIVATE/list/base_bounties_to_give = list( + SPY_DIFFICULTY_EASY = 4, + SPY_DIFFICULTY_MEDIUM = 2, + SPY_DIFFICULTY_HARD = 2, + ) + + /// Assoc list of all active bounties. + VAR_PRIVATE/list/list/bounties = list( + SPY_DIFFICULTY_EASY = list(), + SPY_DIFFICULTY_MEDIUM = list(), + SPY_DIFFICULTY_HARD = list(), + ) + + /// Assoc list of all possible bounties for each difficulty, weighted. + /// This is static, no bounty types are removed from this list. + VAR_PRIVATE/list/list/bounty_types = list( + SPY_DIFFICULTY_EASY = list(), + SPY_DIFFICULTY_MEDIUM = list(), + SPY_DIFFICULTY_HARD = list(), + ) + + /// Assoc list of all uplink items possible to be given as bounties for each difficulty. + /// This is not static, as bounties are complete uplink items will be removed from this list. + var/list/list/possible_uplink_items = list( + SPY_DIFFICULTY_EASY = list(), + SPY_DIFFICULTY_MEDIUM = list(), + SPY_DIFFICULTY_HARD = list(), + ) + +/datum/spy_bounty_handler/New() + for(var/datum/spy_bounty/bounty as anything in subtypesof(/datum/spy_bounty)) + var/weight = initial(bounty.weight) + var/difficulty = initial(bounty.difficulty) + if(weight <= 0 || !islist(bounty_types[difficulty])) + continue + bounty_types[difficulty][bounty] = weight + + for(var/datum/uplink_item/item as anything in SStraitor.uplink_items) + if(isnull(item.item) || item.item == ABSTRACT_UPLINK_ITEM) + continue + if(!(item.purchasable_from & UPLINK_SPY)) + continue + // This will have some overlap, and that's intentional - + // Adds some variety, rare moments where you can get a hard reward for an easier bounty (or visa versa) + if(item.cost <= SPY_LOWER_COST_THRESHOLD) + possible_uplink_items[SPY_DIFFICULTY_EASY] += item + if(item.cost >= SPY_LOWER_COST_THRESHOLD && item.cost <= SPY_UPPER_COST_THRESHOLD) + possible_uplink_items[SPY_DIFFICULTY_MEDIUM] += item + if(item.cost >= SPY_UPPER_COST_THRESHOLD) + possible_uplink_items[SPY_DIFFICULTY_HARD] += item + + refresh_bounty_list() + +/// Helper that returns a list of all active bounties in a single list, regardless of difficulty. +/datum/spy_bounty_handler/proc/get_all_bounties() as /list + var/list/all_bounties = list() + for(var/difficulty in bounties) + all_bounties += bounties[difficulty] + + return all_bounties + +/// Refreshes all active bounties for each difficulty, no matter if they were complete or not. +/// Then recursively calls itself via a timer. +/datum/spy_bounty_handler/proc/refresh_bounty_list() + PRIVATE_PROC(TRUE) + + var/list/bounties_to_give = base_bounties_to_give.Copy() + + if(num_refreshes < base_bounties_to_give[SPY_DIFFICULTY_HARD]) + bounties_to_give[SPY_DIFFICULTY_HARD] = num_refreshes + bounties_to_give[SPY_DIFFICULTY_MEDIUM] += (base_bounties_to_give[SPY_DIFFICULTY_HARD] - num_refreshes) + + for(var/difficulty in bounties) + QDEL_LIST(bounties[difficulty]) + + var/list/pool = bounty_types[difficulty] + var/amount_to_give = bounties_to_give[difficulty] + var/failed_attempts = num_attempts_override || clamp(amount_to_give * 4, 10, 25) // more potential bounties = more attempts to make one + while(amount_to_give > 0 && failed_attempts > 0) + var/picked_bounty = pick_weight(pool) + var/datum/spy_bounty/bounty = new picked_bounty(src) + if(bounty.initalized) + amount_to_give -= 1 + bounties[difficulty] += bounty + + else + failed_attempts -= 1 + qdel(bounty) + + claimed_bounties_from_last_pool.Cut() + num_refreshes += 1 + refresh_timer = addtimer(CALLBACK(src, PROC_REF(refresh_bounty_list)), refresh_time, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE) + +/// Forces a refresh of the bounty list. +/// Counts towards [num_refreshes]. +/datum/spy_bounty_handler/proc/force_refresh() + if(refresh_timer) + deltimer(refresh_timer) + + refresh_bounty_list() diff --git a/code/modules/antagonists/spy/spy_uplink.dm b/code/modules/antagonists/spy/spy_uplink.dm new file mode 100644 index 00000000000..ea6f39fc92d --- /dev/null +++ b/code/modules/antagonists/spy/spy_uplink.dm @@ -0,0 +1,206 @@ +/** + * ## Spy uplink + * + * Applied to items similar to traitor uplinks. + * + * Used for spies to complete bounties. + */ +/datum/component/spy_uplink + /// Weakref to the spy antag datum which owns this uplink + var/datum/weakref/spy_ref + /// The handler which manages all bounties across all spies. + var/static/datum/spy_bounty_handler/handler + +/datum/component/spy_uplink/Initialize(datum/antagonist/spy/spy) + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + + spy_ref = WEAKREF(spy) + + if(isnull(handler)) + handler = new() + +/datum/component/spy_uplink/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_attack_self)) + RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK_SECONDARY, PROC_REF(on_pre_attack_secondary)) + RegisterSignal(parent, COMSIG_TABLET_CHECK_DETONATE, PROC_REF(block_pda_bombs)) + +/datum/component/spy_uplink/UnregisterFromParent() + UnregisterSignal(parent, list( + COMSIG_ATOM_EXAMINE, + COMSIG_ITEM_ATTACK_SELF, + COMSIG_ITEM_PRE_ATTACK_SECONDARY, + COMSIG_TABLET_CHECK_DETONATE, + )) + +/// Checks that the passed mob is the owner of this uplink. +/datum/component/spy_uplink/proc/is_our_spy(mob/whoever) + var/datum/antagonist/spy/spy_datum = spy_ref?.resolve() + return spy_datum?.owner.current == whoever + +/datum/component/spy_uplink/proc/on_examine(obj/item/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + if(!is_our_spy(user)) + return + examine_list += span_notice("You recognize this as your spy uplink.") + examine_list += span_notice("- [EXAMINE_HINT("Use it in hand")] to view your bounty list.") + examine_list += span_notice("- [EXAMINE_HINT("Right click")] with it on a bounty target to claim it.") + +/datum/component/spy_uplink/proc/block_pda_bombs(obj/item/source) + SIGNAL_HANDLER + + return COMPONENT_TABLET_NO_DETONATE + +/datum/component/spy_uplink/proc/on_attack_self(obj/item/source, mob/user) + SIGNAL_HANDLER + + if(is_our_spy(user)) + INVOKE_ASYNC(src, TYPE_PROC_REF(/datum, ui_interact), user) + return NONE + +/datum/component/spy_uplink/proc/on_pre_attack_secondary(obj/item/source, atom/target, mob/living/user, params) + SIGNAL_HANDLER + + if(!ismovable(target)) + return NONE + if(!is_our_spy(user)) + return NONE + if(!try_steal(target, user)) + return NONE + return COMPONENT_CANCEL_ATTACK_CHAIN + +/// Checks if the passed atom is something that can be stolen according to one of the active bounties. +/// If so, starts the stealing process. +/datum/component/spy_uplink/proc/try_steal(atom/movable/stealing, mob/living/spy) + for(var/datum/spy_bounty/bounty as anything in handler.get_all_bounties()) + if(!bounty.can_claim(spy)) + continue + if(!bounty.is_stealable(stealing)) + continue + if(bounty.claimed) + stealing.balloon_alert(spy, "bounty already claimed!") + return TRUE + if(DOING_INTERACTION(spy, REF(src))) + spy.balloon_alert(spy, "already scanning!") // Only shown if they're trying to scan two valid targets + return TRUE + SEND_SIGNAL(stealing, COMSIG_MOVABLE_SPY_STEALING, spy, bounty) + INVOKE_ASYNC(src, PROC_REF(start_stealing), stealing, spy, bounty) + return TRUE + + return FALSE + +/// Wraps the stealing process in a scanning effect. +/datum/component/spy_uplink/proc/start_stealing(atom/movable/stealing, mob/living/spy, datum/spy_bounty/bounty) + if(!isturf(stealing.loc) && stealing.loc != spy) + to_chat(spy, span_warning("Your uplinks blinks red: [stealing] cannot be extracted from there.")) + return FALSE + + playsound(stealing, 'sound/items/pshoom.ogg', 33, vary = TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, frequency = 0.33, ignore_walls = FALSE) + + var/obj/effect/scan_effect/active_scan_effect = new(stealing.loc) + active_scan_effect.appearance = stealing.appearance + active_scan_effect.dir = stealing.dir + active_scan_effect.makeHologram() + SET_PLANE_EXPLICIT(active_scan_effect, stealing.plane, stealing) + active_scan_effect.layer = stealing.layer + 0.1 + + var/obj/effect/scan_effect/cone/active_scan_cone + if(isturf(stealing.loc) && isturf(spy.loc)) // Cone doesn't make sense if its being held or something + active_scan_cone = new(spy.loc) + var/angle = round(get_angle(spy, stealing), 10) + if(angle > 180 && angle < 360) + active_scan_cone.pixel_x -= 16 + else if(angle < 180 && angle > 0) + active_scan_cone.pixel_x += 16 + if(angle > 90 && angle < 270) + active_scan_cone.pixel_y -= 16 + else if(angle < 90 || angle > 270) + active_scan_cone.pixel_y += 16 + active_scan_cone.transform = active_scan_cone.transform.Turn(angle) + active_scan_cone.alpha = 0 + animate(active_scan_cone, time = 0.5 SECONDS, alpha = initial(active_scan_cone.alpha)) + + . = steal_process(stealing, spy, bounty) + qdel(active_scan_effect) + qdel(active_scan_cone) + return . + +/// Attempts to steal the passed atom in accordance with the passed bounty. +/// If successful, proceeds to complete the bounty. +/datum/component/spy_uplink/proc/steal_process(atom/movable/stealing, mob/living/spy, datum/spy_bounty/bounty) + spy.visible_message( + span_warning("[spy] starts scanning [stealing] with a strange device..."), + span_notice("You start scanning [stealing], preparing it for extraction."), + ) + + if(!do_after(spy, bounty.theft_time, stealing, interaction_key = REF(src))) + return FALSE + if(bounty.claimed) + to_chat(spy, span_warning("Your uplinks blinks red: The bounty for [stealing] has been claimed by another spy!")) + return FALSE + if(spy.is_holding(stealing) && !spy.dropItemToGround(stealing)) + to_chat(spy, span_warning("Your uplinks blinks red: [stealing] seems stuck to your hand!")) + return FALSE + + var/bounty_key = bounty.get_dupe_protection_key(stealing) + handler.all_claimed_bounty_types[bounty_key] += 1 + handler.claimed_bounties_from_last_pool[bounty_key] = TRUE + + bounty.clean_up_stolen_item(stealing, spy, handler) + bounty.claimed = TRUE + + var/atom/movable/reward = bounty.reward_item.spawn_item_for_generic_use(spy) + if(isitem(reward)) + spy.put_in_hands(reward) + + to_chat(spy, span_notice("Bounty complete! You have been rewarded with \a [reward].\ + [reward.loc == spy ? "" : " Find it at your feet."]")) + + playsound(parent, 'sound/machines/wewewew.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + + log_spy("[key_name(spy)] completed the bounty [bounty.name] of difficulty [bounty.difficulty] for \a [reward].") + SSblackbox.record_feedback("nested tally", "spy_bounty", 1, list("[stealing.type]", "[bounty.type]", "[bounty.difficulty]", "[bounty.reward_item.type]")) + + var/datum/antagonist/spy/spy_datum = spy_ref?.resolve() + if(!isnull(spy_datum)) + // "When" TGUI roundend is finished, a list of all bounties complete and their rewards should be put in a collapsible, + // otherwise it's just too much information to display cleanly. (That's why we're only displaying number and rewards) + spy_datum.bounties_claimed += 1 + spy_datum.all_loot += bounty.reward_item.name + + return TRUE + +/datum/component/spy_uplink/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "SpyUplink") + ui.open() + +/datum/component/spy_uplink/ui_data(mob/user) + var/list/data = list() + + data["bounties"] = list() + for(var/datum/spy_bounty/bounty as anything in handler.get_all_bounties()) + UNTYPED_LIST_ADD(data["bounties"], bounty.to_ui_data(user)) + data["time_left"] = timeleft(handler.refresh_timer) + + return data + +/datum/component/spy_uplink/ui_status(mob/user, datum/ui_state/state) + if(isobserver(user) && user.client?.holder) + return UI_UPDATE + return ..() + +/obj/effect/scan_effect + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + anchored = TRUE + layer = ABOVE_ALL_MOB_LAYER + +/obj/effect/scan_effect/cone + name = "holoray" + icon = 'icons/effects/effects.dmi' + icon_state = "scan_beam" + color = "#3ba0ff" + alpha = 200 diff --git a/code/modules/cargo/markets/_market.dm b/code/modules/cargo/markets/_market.dm index 3c264289cd2..a4af2bc981d 100644 --- a/code/modules/cargo/markets/_market.dm +++ b/code/modules/cargo/markets/_market.dm @@ -13,10 +13,7 @@ /// Adds item to the available items and add it's category if it is not in categories yet. /datum/market/proc/add_item(datum/market_item/item) - if(!prob(initial(item.availability_prob))) - return FALSE - - if(ispath(item)) + if(ispath(item, /datum/market_item)) item = new item() if(!(item.category in categories)) diff --git a/code/modules/cargo/markets/market_item.dm b/code/modules/cargo/markets/market_item.dm index 867facf015b..d5689c17a45 100644 --- a/code/modules/cargo/markets/market_item.dm +++ b/code/modules/cargo/markets/market_item.dm @@ -14,7 +14,7 @@ var/stock /// Path to or the item itself what this entry is for, this should be set even if you override spawn_item to spawn your item. - var/item + var/obj/item/item /// Minimum price for the item if generated randomly. var/price_min = 0 @@ -33,9 +33,18 @@ if(isnull(stock)) stock = rand(stock_min, stock_max) +/datum/market_item/Destroy() + item = null + return ..() + /// Used for spawning the wanted item, override if you need to do something special with the item. /datum/market_item/proc/spawn_item(loc) - return new item(loc) + if(ismovable(item)) + item.forceMove(loc) + return item + if(ispath(item)) + return new item(loc) + CRASH("Invalid item type for market item [item || "null"]") /// Buys the item and makes SSblackmarket handle it. /datum/market_item/proc/buy(obj/item/market_uplink/uplink, mob/buyer, shipping_method) diff --git a/code/modules/cargo/markets/market_telepad.dm b/code/modules/cargo/markets/market_telepad.dm index abdad441ce5..e99e4b88d22 100644 --- a/code/modules/cargo/markets/market_telepad.dm +++ b/code/modules/cargo/markets/market_telepad.dm @@ -82,11 +82,7 @@ if(receiving) var/datum/market_purchase/P = receiving - if(!P.item || ispath(P.item)) - P.item = P.entry.spawn_item(T) - else - var/atom/movable/M = P.item - M.forceMove(T) + P.item = P.entry.spawn_item(T) use_power(power_usage_per_teleport / power_efficiency) var/datum/effect_system/spark_spread/sparks = new diff --git a/code/modules/hallucination/fake_sound.dm b/code/modules/hallucination/fake_sound.dm index ec578f101d3..aaf8ef46823 100644 --- a/code/modules/hallucination/fake_sound.dm +++ b/code/modules/hallucination/fake_sound.dm @@ -173,6 +173,7 @@ 'sound/ambience/antag/ling_alert.ogg', 'sound/ambience/antag/malf.ogg', 'sound/ambience/antag/ops.ogg', + 'sound/ambience/antag/spy.ogg', 'sound/ambience/antag/tatoralert.ogg', ) diff --git a/code/modules/logging/categories/log_category_uplink.dm b/code/modules/logging/categories/log_category_uplink.dm index f88d224ad3b..4ef0f1af0c0 100644 --- a/code/modules/logging/categories/log_category_uplink.dm +++ b/code/modules/logging/categories/log_category_uplink.dm @@ -21,3 +21,8 @@ category = LOG_CATEGORY_UPLINK_SPELL config_flag = /datum/config_entry/flag/log_uplink master_category = /datum/log_category/uplink + +/datum/log_category/uplink_spy + category = LOG_CATEGORY_UPLINK_SPY + config_flag = /datum/config_entry/flag/log_uplink + master_category = /datum/log_category/uplink diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm index a4bcad87671..97a543fa7e7 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm @@ -203,7 +203,7 @@ new /obj/item/reagent_containers/cup/beaker(src) new /obj/item/clothing/glasses/science(src) if(7) - new /obj/item/clothing/glasses/sunglasses(src) + new /obj/item/clothing/glasses/sunglasses/big(src) new /obj/item/clothing/mask/cigarette/rollie(src) else //empty grave diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm index 76043693b05..37722ed3bf6 100644 --- a/code/modules/mob/living/simple_animal/bot/secbot.dm +++ b/code/modules/mob/living/simple_animal/bot/secbot.dm @@ -73,6 +73,11 @@ desc = "It's Officer Beepsky! Powered by a potato and a shot of whiskey, and with a sturdier reinforced chassis, too." health = 45 +/mob/living/simple_animal/bot/secbot/beepsky/officer/Initialize(mapload) + . = ..() + // Beepsky hates people scanning them + RegisterSignal(src, COMSIG_MOVABLE_SPY_STEALING, PROC_REF(retaliate_async)) + /mob/living/simple_animal/bot/secbot/beepsky/ofitser name = "Prison Ofitser" desc = "Powered by the tears and sweat of laborers." @@ -208,6 +213,11 @@ if("arrest_alert") security_mode_flags ^= SECBOT_DECLARE_ARRESTS +/mob/living/simple_animal/bot/secbot/proc/retaliate_async(datum/source, mob/user, ...) + SIGNAL_HANDLER + + INVOKE_ASYNC(src, PROC_REF(retaliate), user) + /mob/living/simple_animal/bot/secbot/proc/retaliate(mob/living/carbon/human/attacking_human) var/judgement_criteria = judgement_criteria() threatlevel = attacking_human.assess_threat(judgement_criteria) diff --git a/code/modules/projectiles/boxes_magazines/internal/shotgun.dm b/code/modules/projectiles/boxes_magazines/internal/shotgun.dm index dfd99e24766..3b2489022ea 100644 --- a/code/modules/projectiles/boxes_magazines/internal/shotgun.dm +++ b/code/modules/projectiles/boxes_magazines/internal/shotgun.dm @@ -13,6 +13,12 @@ /obj/item/ammo_box/magazine/internal/shot/tube/fire ammo_type = /obj/projectile/bullet/incendiary/shotgun/no_trail +/obj/item/ammo_box/magazine/internal/shot/tube/buckshot + ammo_type = /obj/item/ammo_casing/shotgun/buckshot + +/obj/item/ammo_box/magazine/internal/shot/tube/slug + ammo_type = /obj/item/ammo_casing/shotgun + /obj/item/ammo_box/magazine/internal/shot/lethal ammo_type = /obj/item/ammo_casing/shotgun/buckshot diff --git a/code/modules/projectiles/guns/ballistic/shotgun.dm b/code/modules/projectiles/guns/ballistic/shotgun.dm index 38bcfe8d2f4..37990971138 100644 --- a/code/modules/projectiles/guns/ballistic/shotgun.dm +++ b/code/modules/projectiles/guns/ballistic/shotgun.dm @@ -97,6 +97,10 @@ desc = "An advanced shotgun with two separate magazine tubes. This one shows signs of bounty hunting customization, meaning it likely has a dual rubber shot/fire slug load." alt_mag_type = /obj/item/ammo_box/magazine/internal/shot/tube/fire +/obj/item/gun/ballistic/shotgun/automatic/dual_tube/deadly + spawn_magazine_type = /obj/item/ammo_box/magazine/internal/shot/tube/buckshot + alt_mag_type = /obj/item/ammo_box/magazine/internal/shot/tube/slug + /obj/item/gun/ballistic/shotgun/automatic/dual_tube/examine(mob/user) . = ..() . += span_notice("Alt-click to pump it.") diff --git a/code/modules/projectiles/pins.dm b/code/modules/projectiles/pins.dm index c4b6f6fb4ce..6f80bf0e214 100644 --- a/code/modules/projectiles/pins.dm +++ b/code/modules/projectiles/pins.dm @@ -387,4 +387,5 @@ /obj/item/firing_pin/Destroy() if(gun) gun.pin = null + gun = null return ..() diff --git a/code/modules/surgery/organs/autosurgeon.dm b/code/modules/surgery/organs/autosurgeon.dm index b577b9f8ec0..a2cf91c72f5 100644 --- a/code/modules/surgery/organs/autosurgeon.dm +++ b/code/modules/surgery/organs/autosurgeon.dm @@ -177,3 +177,8 @@ /obj/item/autosurgeon/syndicate/emaggedsurgerytoolset starting_organ = /obj/item/organ/internal/cyberimp/arm/surgery/emagged + +/obj/item/autosurgeon/syndicate/contraband_sechud + desc = "Contains a contraband SecHUD implant, undetectable by health scanners." + uses = 1 + starting_organ = /obj/item/organ/internal/cyberimp/eyes/hud/security/syndicate diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 22f5ef72934..2ac70b3e9bc 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -247,6 +247,7 @@ #include "spell_mindswap.dm" #include "spell_names.dm" #include "spell_shapeshift.dm" +#include "spies.dm" #include "spritesheets.dm" #include "stack_singular_name.dm" #include "station_trait_tests.dm" diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_spy.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spy.png new file mode 100644 index 00000000000..103e9d60faf Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spy.png differ diff --git a/code/modules/unit_tests/spies.dm b/code/modules/unit_tests/spies.dm new file mode 100644 index 00000000000..b4b1add333c --- /dev/null +++ b/code/modules/unit_tests/spies.dm @@ -0,0 +1,41 @@ +/// Tests spy bounty setup +/datum/unit_test/spy_bounty + +/datum/unit_test/spy_bounty/Run() + var/mob/living/carbon/human/james_bond = allocate(/mob/living/carbon/human/consistent) + james_bond.mind_initialize() + james_bond.equipOutfit(/datum/outfit/job/assistant/consistent) + var/datum/antagonist/spy/spy = james_bond.mind.add_antag_datum(/datum/antagonist/spy) + + var/datum/component/spy_uplink/uplink = spy.uplink_weakref?.resolve() + TEST_ASSERT_NOTNULL(uplink, "Spy failed to be given an uplink!") + + var/datum/spy_bounty_handler/handler = uplink.handler + handler.num_attempts_override = 100 + + for(var/difficulty in handler.possible_uplink_items) + var/list/loot_pool = handler.possible_uplink_items[difficulty] + if(!length(loot_pool)) + TEST_FAIL("No rewards generated for spy bounty difficulty [difficulty]") + + for(var/difficulty in UNLINT(handler.bounty_types)) + var/list/bounty_type_pool = UNLINT(handler.bounty_types[difficulty]) + if(!length(bounty_type_pool)) + TEST_FAIL("No bounty types for spy bounty difficulty [difficulty] found") + + for(var/difficulty in UNLINT(handler.bounties)) + var/list/generated_bounties = UNLINT(handler.bounties[difficulty]) + if(difficulty == SPY_DIFFICULTY_HARD) + if(length(generated_bounties)) + TEST_FAIL("No [difficulty] bounties should not be generated on initial refresh!") + + else + if(!length(generated_bounties)) + TEST_FAIL("No bounties were generated on initial refresh for difficulty [difficulty]") + + handler.force_refresh() + + for(var/difficulty in UNLINT(handler.bounties)) + var/list/generated_bounties = UNLINT(handler.bounties[difficulty]) + if(!length(generated_bounties)) + TEST_FAIL("No bounties were generated on first refresh for difficulty [difficulty]") diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm index 65935f077e3..bb76564e42c 100644 --- a/code/modules/uplink/uplink_items.dm +++ b/code/modules/uplink/uplink_items.dm @@ -149,6 +149,34 @@ SEND_SIGNAL(uplink_handler, COMSIG_ON_UPLINK_PURCHASE, spawned_item, user) return spawned_item +/// Used to create the uplink's item for generic use, rather than use by a Syndie specifically +/// Can be used to "de-restrict" some items, such as Nukie guns spawning with Syndicate pins +/datum/uplink_item/proc/spawn_item_for_generic_use(mob/user) + var/atom/movable/created = new item(user.loc) + + if(isgun(created)) + replace_pin(created) + else if(istype(created, /obj/item/storage/toolbox/guncase)) + for(var/obj/item/gun/gun in created) + replace_pin(gun) + + if(isobj(created)) + var/obj/created_obj = created + LAZYREMOVE(created_obj.req_access, ACCESS_SYNDICATE) + LAZYREMOVE(created_obj.req_one_access, ACCESS_SYNDICATE) + + return created + +/// Used by spawn_item_for_generic_use to replace the pin of a gun with a normal one +/datum/uplink_item/proc/replace_pin(obj/item/gun/gun_reward) + PRIVATE_PROC(TRUE) + + if(!istype(gun_reward.pin, /obj/item/firing_pin/implant/pindicate)) + return + + QDEL_NULL(gun_reward.pin) + gun_reward.pin = new /obj/item/firing_pin(gun_reward) + ///For special overrides if an item can be bought or not. /datum/uplink_item/proc/can_be_bought(datum/uplink_handler/source) return TRUE @@ -168,6 +196,7 @@ //Discounts (dynamically filled above) /datum/uplink_item/discounts category = /datum/uplink_category/discounts + purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY // Probably not necessary but just in case // Special equipment (Dynamically fills in uplink component) /datum/uplink_item/special_equipment @@ -176,6 +205,7 @@ desc = "Equipment necessary for accomplishing specific objectives. If you are seeing this, something has gone wrong." limited_stock = 1 illegal_tech = FALSE + purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY // Ditto /datum/uplink_item/special_equipment/purchase(mob/user, datum/component/uplink/U) ..() diff --git a/code/modules/uplink/uplink_items/ammunition.dm b/code/modules/uplink/uplink_items/ammunition.dm index e8872781252..5326880d31b 100644 --- a/code/modules/uplink/uplink_items/ammunition.dm +++ b/code/modules/uplink/uplink_items/ammunition.dm @@ -53,5 +53,5 @@ For when you really need a lot of things dead." item = /obj/item/ammo_box/a357 cost = 4 - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) //nukies get their own version + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY) //nukies get their own version illegal_tech = FALSE diff --git a/code/modules/uplink/uplink_items/bundle.dm b/code/modules/uplink/uplink_items/bundle.dm index f236aa4da25..b708af62b69 100644 --- a/code/modules/uplink/uplink_items/bundle.dm +++ b/code/modules/uplink/uplink_items/bundle.dm @@ -7,11 +7,12 @@ category = /datum/uplink_category/bundle surplus = 0 cant_discount = TRUE + purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY /datum/uplink_item/bundles_tc/random name = "Random Item" desc = "Picking this will purchase a random item. Useful if you have some TC to spare or if you haven't decided on a strategy yet." - item = /obj/effect/gibspawner/generic // non-tangible item because techwebs use this path to determine illegal tech + item = ABSTRACT_UPLINK_ITEM cost = 0 cost_override_string = "Varies" @@ -61,7 +62,7 @@ item = /obj/item/storage/box/syndicate/bundle_a cost = 20 stock_key = UPLINK_SHARED_STOCK_KITS - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY) /datum/uplink_item/bundles_tc/bundle_b name = "Syndi-kit Special" @@ -72,7 +73,7 @@ item = /obj/item/storage/box/syndicate/bundle_b cost = 20 stock_key = UPLINK_SHARED_STOCK_KITS - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY) /datum/uplink_item/bundles_tc/surplus name = "Syndicate Surplus Crate" @@ -81,7 +82,7 @@ Contents are sorted to always be worth 30 TC. The Syndicate will only provide one surplus item per agent." item = /obj/structure/closet/crate // will be replaced in purchase() cost = 20 - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY) stock_key = UPLINK_SHARED_STOCK_SURPLUS /// Value of items inside the crate in TC var/crate_tc_value = 30 @@ -170,5 +171,5 @@ The Syndicate will only provide one surplus item per agent." cost = 20 item = /obj/item/syndicrate_key - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY) stock_key = UPLINK_SHARED_STOCK_SURPLUS diff --git a/code/modules/uplink/uplink_items/clownops.dm b/code/modules/uplink/uplink_items/clownops.dm index bbb597d2fa8..73dd3d4c876 100644 --- a/code/modules/uplink/uplink_items/clownops.dm +++ b/code/modules/uplink/uplink_items/clownops.dm @@ -8,7 +8,7 @@ cost = 10 item = /obj/item/pneumatic_cannon/pie/selfcharge surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/weapon_kits/bananashield name = "Bananium Energy Shield" @@ -18,7 +18,7 @@ item = /obj/item/shield/energy/bananium cost = 16 surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/weapon_kits/clownsword name = "Bananium Energy Sword" @@ -27,7 +27,7 @@ item = /obj/item/melee/energy/sword/bananium cost = 3 surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/weapon_kits/clownoppin name = "Ultra Hilarious Firing Pin" @@ -51,7 +51,7 @@ item = /obj/item/gun/ballistic/automatic/c20r/toy cost = 5 surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/weapon_kits/foammachinegun name = "Toy Machine Gun" @@ -60,7 +60,7 @@ item = /obj/item/gun/ballistic/automatic/l6_saw/toy cost = 10 surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/explosives/bombanana name = "Bombanana" @@ -69,7 +69,7 @@ item = /obj/item/food/grown/banana/bombanana cost = 4 //it is a bit cheaper than a minibomb because you have to take off your helmet to eat it, which is how you arm it surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/explosives/clown_bomb_clownops name = "Clown Bomb" @@ -81,7 +81,7 @@ item = /obj/item/sbeacondrop/clownbomb cost = 15 surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/explosives/clown_bomb_clownops/New() . = ..() @@ -94,7 +94,7 @@ item = /obj/item/grenade/chem_grenade/teargas/moustache cost = 3 surplus = 0 - purchasable_from = UPLINK_CLOWN_OPS + purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/explosives/pinata name = "Weapons Grade Pinata Kit" @@ -160,4 +160,3 @@ cost = 1 purchasable_from = UPLINK_CLOWN_OPS illegal_tech = FALSE - diff --git a/code/modules/uplink/uplink_items/contractor.dm b/code/modules/uplink/uplink_items/contractor.dm index 6004caf9745..7d261410e31 100644 --- a/code/modules/uplink/uplink_items/contractor.dm +++ b/code/modules/uplink/uplink_items/contractor.dm @@ -13,7 +13,7 @@ item = /obj/item/storage/box/syndicate/contract_kit category = /datum/uplink_category/contractor cost = 20 - purchasable_from = ~(UPLINK_CLOWN_OPS | UPLINK_NUKE_OPS | UPLINK_TRAITORS) + purchasable_from = UPLINK_INFILTRATORS /datum/uplink_item/bundles_tc/contract_kit/purchase(mob/user, datum/uplink_handler/uplink_handler, atom/movable/source) . = ..() @@ -36,7 +36,7 @@ name = "Contract Reroll" desc = "Request a reroll of your current contract list. Will generate a new target, \ payment, and dropoff for the contracts you currently have available." - item = /obj/effect/gibspawner/generic + item = ABSTRACT_UPLINK_ITEM limited_stock = 2 cost = 0 diff --git a/code/modules/uplink/uplink_items/device_tools.dm b/code/modules/uplink/uplink_items/device_tools.dm index c698a46761b..3ac392981cb 100644 --- a/code/modules/uplink/uplink_items/device_tools.dm +++ b/code/modules/uplink/uplink_items/device_tools.dm @@ -136,7 +136,7 @@ /datum/uplink_item/device_tools/failsafe name = "Failsafe Uplink Code" desc = "When entered the uplink will self-destruct immediately." - item = /obj/effect/gibspawner/generic + item = ABSTRACT_UPLINK_ITEM cost = 1 surplus = 0 restricted = TRUE diff --git a/code/modules/uplink/uplink_items/implant.dm b/code/modules/uplink/uplink_items/implant.dm index 87c9fd6c96c..a2b21574f6f 100644 --- a/code/modules/uplink/uplink_items/implant.dm +++ b/code/modules/uplink/uplink_items/implant.dm @@ -49,6 +49,7 @@ // An empty uplink is kinda useless. surplus = 0 restricted = TRUE + purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY /datum/uplink_item/implants/uplink/spawn_item(spawn_path, mob/user, datum/uplink_handler/uplink_handler, atom/movable/source) var/obj/item/storage/box/syndie_kit/uplink_box = ..() diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm index 49d1eaa633c..22528937ebf 100644 --- a/code/modules/uplink/uplink_items/job.dm +++ b/code/modules/uplink/uplink_items/job.dm @@ -28,7 +28,7 @@ /datum/uplink_item/role_restricted/bureaucratic_error name = "Organic Capital Disturbance Virus" desc = "Randomizes job positions presented to new hires. May lead to too many/too few security officers and/or clowns. Single use." - item = /obj/effect/gibspawner/generic + item = ABSTRACT_UPLINK_ITEM surplus = 0 limited_stock = 1 cost = 2 @@ -286,6 +286,13 @@ restricted_roles = list(JOB_CLOWN) surplus = 10 +/datum/uplink_item/role_restricted/clowncar/spawn_item_for_generic_use(mob/user) + var/obj/vehicle/sealed/car/clowncar/car = ..() + car.enforce_clown_role = FALSE + var/obj/item/key = new car.key_type(user.loc) + car.visible_message(span_notice("[key] drops out of [car] onto the floor.")) + return car + /datum/uplink_item/role_restricted/his_grace name = "His Grace" desc = "An incredibly dangerous weapon recovered from a station overcome by the grey tide. Once activated, He will thirst for blood and must be used to kill to sate that thirst. \ @@ -298,6 +305,7 @@ cost = 20 surplus = 0 restricted_roles = list(JOB_CHAPLAIN) + purchasable_from = ~UPLINK_SPY /datum/uplink_item/role_restricted/concealed_weapon_bay name = "Concealed Weapon Bay" @@ -384,3 +392,4 @@ restricted_roles = list(JOB_MIME) restricted = TRUE refundable = FALSE + purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm index 32d99512e45..fa06ecf6714 100644 --- a/code/modules/uplink/uplink_items/nukeops.dm +++ b/code/modules/uplink/uplink_items/nukeops.dm @@ -76,26 +76,28 @@ name = "12g Buckshot Drum (Bulldog)" desc = "An additional 8-round buckshot magazine for use with the Bulldog shotgun. Front towards enemy." item = /obj/item/ammo_box/magazine/m12g + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/basic/slug name = "12g Slug Drum (Bulldog)" desc = "An additional 8-round slug magazine for use with the Bulldog shotgun. \ Now 8 times less likely to shoot your pals." item = /obj/item/ammo_box/magazine/m12g/slug + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/incendiary/dragon name = "12g Dragon's Breath Drum (Bulldog)" desc = "An alternative 8-round dragon's breath magazine for use in the Bulldog shotgun. \ 'I'm a fire starter, twisted fire starter!'" item = /obj/item/ammo_box/magazine/m12g/dragon - purchasable_from = UPLINK_NUKE_OPS + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/special/meteor name = "12g Meteorslug Shells (Bulldog)" desc = "An alternative 8-round meteorslug magazine for use in the Bulldog shotgun. \ Great for blasting holes into the hull and knocking down enemies." item = /obj/item/ammo_box/magazine/m12g/meteor - purchasable_from = UPLINK_NUKE_OPS + purchasable_from = parent_type::purchasable_from | UPLINK_SPY // ~~ Ansem Pistol ~~ @@ -109,24 +111,28 @@ name = "10mm Handgun Magazine (Ansem)" desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol." item = /obj/item/ammo_box/magazine/m10mm + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/ap/m10mm name = "10mm Armour Piercing Magazine (Ansem)" desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol. \ These rounds are less effective at injuring the target but penetrate protective gear." item = /obj/item/ammo_box/magazine/m10mm/ap + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/hp/m10mm name = "10mm Hollow Point Magazine (Ansem)" desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol. \ These rounds are more damaging but ineffective against armour." item = /obj/item/ammo_box/magazine/m10mm/hp + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/incendiary/m10mm name = "10mm Incendiary Magazine (Ansem)" desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol. \ Loaded with incendiary rounds which inflict less damage, but ignite the target." item = /obj/item/ammo_box/magazine/m10mm/fire + purchasable_from = parent_type::purchasable_from | UPLINK_SPY //Medium-cost: 14 TC each. Meant for more expensive purchases with a goal in mind. @@ -197,6 +203,7 @@ desc = "A speed loader that contains seven additional .357 Magnum rounds; usable with the Syndicate revolver. \ For when you really need a lot of things dead. Operatives get a discount from most of our agents!" item = /obj/item/ammo_box/a357 + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/special/revolver/phasic name = ".357 Phasic Speed Loader (Revolver)" @@ -204,6 +211,7 @@ These bullets are made from an experimental alloy, 'Ghost Lead', that allows it to pass through almost any non-organic material. \ The name is a misnomer. It doesn't contain any lead whatsoever!" item = /obj/item/ammo_box/a357/phasic + purchasable_from = parent_type::purchasable_from | UPLINK_SPY /datum/uplink_item/ammo_nuclear/special/revolver/heartseeker name = ".357 Heartseeker Speed Loader (Revolver)" @@ -212,6 +220,7 @@ Brought to you by Roseus Galactic!" item = /obj/item/ammo_box/a357/heartseeker cost = 3 + purchasable_from = parent_type::purchasable_from | UPLINK_SPY // ~~ Grenade Launcher ~~ // 'If god had wanted you to live, he would not have created ME!' @@ -591,7 +600,7 @@ desc = "An upgraded, elite version of the Syndicate MODsuit. It features fireproofing, and also \ provides the user with superior armor and mobility compared to the standard Syndicate MODsuit." item = /obj/item/mod/control/pre_equipped/elite - purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/suits/energy_shield name = "MODsuit Energy Shield Module" @@ -599,28 +608,28 @@ before needing to recharge. Used wisely, this module will keep you alive for a lot longer." item = /obj/item/mod/module/energy_shield cost = 8 - purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/suits/emp_shield name = "MODsuit Advanced EMP Shield Module" desc = "An advanced EMP shield module for a MODsuit. It protects your entire body from electromagnetic pulses." item = /obj/item/mod/module/emp_shield/advanced cost = 5 - purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/suits/injector name = "MODsuit Injector Module" desc = "An injector module for a MODsuit. It is an extendable piercing injector with 30u capacity." item = /obj/item/mod/module/injector cost = 2 - purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/suits/holster name = "MODsuit Holster Module" desc = "A holster module for a MODsuit. It can stealthily store any not too heavy gun inside it." item = /obj/item/mod/module/holster cost = 2 - purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/device_tools/medgun_mod name = "Medbeam Gun Module" @@ -665,7 +674,7 @@ In its crowbar configuration, it can be used to force open airlocks. Very useful for entering the station or its departments." item = /obj/item/crowbar/power/syndicate cost = 4 - purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY /datum/uplink_item/device_tools/medkit name = "Syndicate Combat Medic Kit" @@ -692,7 +701,7 @@ desc = "A potion recovered at great risk by undercover Syndicate operatives and then subsequently modified with Syndicate technology. \ Using it will make any animal sentient, and bound to serve you, as well as implanting an internal radio for communication and an internal ID card for opening doors." cost = 4 - purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY restricted = TRUE // Implants @@ -717,6 +726,7 @@ This will permanently destroy your body, however." item = /obj/item/storage/box/syndie_kit/imp_microbomb cost = 2 + purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY /datum/uplink_item/implants/nuclear/macrobomb name = "Macrobomb Implant" @@ -732,18 +742,21 @@ Prevents collapsing from critical condition, but explodes after a while." item = /obj/item/storage/box/syndie_kit/imp_deniability cost = 6 + purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY /datum/uplink_item/implants/nuclear/reviverplus name = "Reviver Implant" desc = "This implant will attempt to revive and heal you if you lose consciousness. Comes with an autosurgeon." item = /obj/item/autosurgeon/syndicate/reviver cost = 8 + purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY /datum/uplink_item/implants/nuclear/thermals name = "Thermal Eyes" desc = "These cybernetic eyes will give you thermal vision. Comes with a free autosurgeon." item = /obj/item/autosurgeon/syndicate/thermal_eyes cost = 8 + purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY /datum/uplink_item/implants/nuclear/implants/xray name = "X-ray Vision Implant" @@ -756,6 +769,7 @@ desc = "This implant will help you get back up on your feet faster after being stunned. Comes with an autosurgeon." item = /obj/item/autosurgeon/syndicate/anti_stun cost = 8 + purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY // Badass (meme items) diff --git a/code/modules/uplink/uplink_items/species.dm b/code/modules/uplink/uplink_items/species.dm index 54ba353c00a..5eb4bbdcb17 100644 --- a/code/modules/uplink/uplink_items/species.dm +++ b/code/modules/uplink/uplink_items/species.dm @@ -4,7 +4,7 @@ /datum/uplink_item/species_restricted category = /datum/uplink_category/species - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY) /datum/uplink_item/species_restricted/moth_lantern name = "Extra-Bright Lantern" diff --git a/code/modules/uplink/uplink_items/spy_unique.dm b/code/modules/uplink/uplink_items/spy_unique.dm new file mode 100644 index 00000000000..2f9c4b32576 --- /dev/null +++ b/code/modules/uplink/uplink_items/spy_unique.dm @@ -0,0 +1,123 @@ +/datum/uplink_category/spy_unique + name = "Spy Unique" + +// This is solely for uplink items that the spy can randomly obtain via bounties. +/datum/uplink_item/spy_unique + category = /datum/uplink_category/spy_unique + cant_discount = TRUE + surplus = FALSE + purchasable_from = UPLINK_SPY + // Cost doesn't really matter since it's free, but it determines which loot pool it falls into. + // By default, these fall into easy-medium spy bounty loot pool + cost = SPY_LOWER_COST_THRESHOLD + +/datum/uplink_item/spy_unique/syndie_bowman + name = "Syndicate Bowman" + desc = "A bowman headset for members of the Syndicate. Not very conspicuous." + item = /obj/item/radio/headset/syndicate/alt + cost = 1 + +/datum/uplink_item/spy_unique/megaphone + name = "Megaphone" + desc = "A megaphone. It's loud." + item = /obj/item/megaphone + cost = 1 + +/datum/uplink_item/spy_unique/combat_gloves + name = "Combat Gloves" + desc = "A pair of combat gloves. They're insulated!" + item = /obj/item/clothing/gloves/combat + cost = 1 + +/datum/uplink_item/spy_unique/krav_maga + name = "Combat Gloves Plus" + desc = "A pair of combat gloves plus. They're insulated AND you can do martial arts with it!" + item = /obj/item/clothing/gloves/krav_maga/combatglovesplus + +/datum/uplink_item/spy_unique/tackle_gloves + name = "Guerrilla Gloves" + desc = "A pair of Guerrilla gloves. They're insulated AND you can tackle people with it!" + item = /obj/item/clothing/gloves/tackler/combat/insulated + +/datum/uplink_item/spy_unique/kudzu + name = "Kudzu" + desc = "A packet of Kudzu - plant and forget, a great distraction." + item = /obj/item/seeds/kudzu + +/datum/uplink_item/spy_unique/big_knife + name = "Combat Knife" + desc = "A big knife. It's sharp." + item = /obj/item/knife/combat + +/datum/uplink_item/spy_unique/switchblade + name = "Switchblade" + desc = "A switchblade. Switches between not sharp and sharp." + item = /obj/item/switchblade + +/datum/uplink_item/spy_unique/sechud_implant + name = "SecHUD Implant" + desc = "A SecHUD implant. Shows you the ID of people you're looking at. It's also stealthy!" + item = /obj/item/autosurgeon/syndicate/contraband_sechud + +/datum/uplink_item/spy_unique/rifle_prime + name = "Bolt-Action Rifle" + desc = "A bolt-action rifle, with a scope. Won't jam, either." + item = /obj/item/gun/ballistic/rifle/boltaction/prime + cost = SPY_UPPER_COST_THRESHOLD + +/datum/uplink_item/spy_unique/cycler_shotgun + name = "Cycler Shotgun" + desc = "A cycler shotgun. It's a shotgun that cycles between two barrels." + item = /obj/item/gun/ballistic/shotgun/automatic/dual_tube/deadly + cost = SPY_UPPER_COST_THRESHOLD + +/datum/uplink_item/spy_unique/bulldog_shotgun + name = "Bulldog Shotgun" + desc = "A bulldog shotgun. It's a shotgun that shoots bulldogs." + item = /obj/item/gun/ballistic/shotgun/bulldog/unrestricted + cost = SPY_UPPER_COST_THRESHOLD + +/datum/uplink_item/spy_unique/ansem_pistol + name = "Ansem Pistol" + desc = "A pistol that's really good at making people sleep." + item = /obj/item/gun/ballistic/automatic/pistol/clandestine + cost = SPY_UPPER_COST_THRESHOLD + +/datum/uplink_item/spy_unique/rocket_launcher + name = "Rocket Launcher" + desc = "A rocket launcher. I would recommend against jumping with it." + item = /obj/item/gun/ballistic/rocketlauncher + cost = SPY_UPPER_COST_THRESHOLD - 1 // It's a meme item + +/datum/uplink_item/spy_unique/shotgun_ammo + name = "Box of Buckshot" + desc = "A box of buckshot rounds for a shotgun. For when you don't want to miss." + item = /obj/item/storage/box/lethalshot + cost = 1 + +/datum/uplink_item/spy_unique/shotgun_ammo/breacher_slug + name = "Box of Breacher Slugs" + desc = "A box of breacher slugs for a shotgun. For making a good first impression." + item = /obj/item/storage/box/breacherslug + +/datum/uplink_item/spy_unique/shotgun_ammo/slugs + name = "Box of Slugs" + desc = "A box of slugs for a shotgun. For big game hunting." + item = /obj/item/storage/box/slugs + +/datum/uplink_item/spy_unique/stealth_belt + name = "Stealth Belt" + desc = "A stealth belt that lets you sneak behind enemy lines." + item = /obj/item/shadowcloak/weaker + cost = SPY_UPPER_COST_THRESHOLD + +/datum/uplink_item/spy_unique/katana + name = "Katana" + desc = "A really sharp Katana. Did I mention it's sharp?" + item = /obj/item/katana + cost = /datum/uplink_item/dangerous/doublesword::cost // Puts it in the same pool as Desword + +/datum/uplink_item/spy_unique/medkit_lite + name = "Syndicate First Medic Kit" + desc = "A syndicate tactical combat medkit, but only stocked enough to do basic first aid." + item = /obj/item/storage/medkit/tactical_lite diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm index 793120fe56f..fb450fb68df 100644 --- a/code/modules/uplink/uplink_items/stealthy.dm +++ b/code/modules/uplink/uplink_items/stealthy.dm @@ -102,4 +102,4 @@ cost = 7 surplus = 50 limited_stock = 1 - purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_INFILTRATORS) + purchasable_from = UPLINK_TRAITORS | UPLINK_SPY diff --git a/code/modules/uplink/uplink_items/stealthy_tools.dm b/code/modules/uplink/uplink_items/stealthy_tools.dm index 60f007ebae7..59b8f6fca77 100644 --- a/code/modules/uplink/uplink_items/stealthy_tools.dm +++ b/code/modules/uplink/uplink_items/stealthy_tools.dm @@ -102,7 +102,7 @@ /datum/uplink_item/stealthy_tools/telecomm_blackout name = "Disable Telecomms" desc = "When purchased, a virus will be uploaded to the telecommunication processing servers to temporarily disable themselves." - item = /obj/effect/gibspawner/generic + item = ABSTRACT_UPLINK_ITEM surplus = 0 progression_minimum = 15 MINUTES limited_stock = 1 @@ -117,7 +117,7 @@ /datum/uplink_item/stealthy_tools/blackout name = "Trigger Stationwide Blackout" desc = "When purchased, a virus will be uploaded to the engineering processing servers to force a routine power grid check, forcing all APCs on the station to be temporarily disabled." - item = /obj/effect/gibspawner/generic + item = ABSTRACT_UPLINK_ITEM surplus = 0 progression_minimum = 20 MINUTES limited_stock = 1 diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi index 77489551f60..07d67bc6d46 100644 Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ diff --git a/icons/mob/huds/antag_hud.dmi b/icons/mob/huds/antag_hud.dmi index bb44e3de956..90056e499fd 100644 Binary files a/icons/mob/huds/antag_hud.dmi and b/icons/mob/huds/antag_hud.dmi differ diff --git a/modular_skyrat/modules/aesthetics/storage/storage.dm b/modular_skyrat/modules/aesthetics/storage/storage.dm index 92bd763cd90..01b7b5d56b9 100644 --- a/modular_skyrat/modules/aesthetics/storage/storage.dm +++ b/modular_skyrat/modules/aesthetics/storage/storage.dm @@ -116,6 +116,10 @@ icon_state = "secbox_xl" illustration = "breacherslug" +/obj/item/storage/box/slugs + icon_state = "secbox_xl" + illustration = "breacherslug" + /obj/item/storage/box/evidence icon_state = "secbox" illustration = "evidence" diff --git a/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm b/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm index 8b18ce42f17..796b50cf9b6 100644 --- a/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm +++ b/modular_skyrat/modules/faction/code/mapping/mapping_helpers.dm @@ -169,11 +169,10 @@ new /obj/item/pen/sleepy(src) new /obj/item/storage/fancy/cigarettes/cigpack_syndicate(src) if(2) //Energy weapons + energy knives - new /obj/item/gun/energy/e_gun(src) - new /obj/item/gun/energy/e_gun(src) - new /obj/item/gun/energy/e_gun(src) - new /obj/item/gun/energy/e_gun/mini(src) new /obj/item/gun/energy/recharge/ebow(src) + new /obj/item/gun/energy/recharge/ebow(src) + new /obj/item/melee/energy/sword(src) + new /obj/item/melee/energy/sword(src) new /obj/item/melee/energy/sword(src) new /obj/item/melee/energy/sword(src) if(3) //Ballistics + knives @@ -197,7 +196,6 @@ new /obj/item/mod/control/pre_equipped/mining(src) new /obj/item/mod/control/pre_equipped/engineering(src) new /obj/item/mod/control/pre_equipped/atmospheric(src) - new /obj/item/mod/control/pre_equipped/research(src) new /obj/item/mod/control/pre_equipped/traitor(src) new /obj/item/mod/control/pre_equipped/elite(src) if(5) //Implants diff --git a/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/command.dm b/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/command.dm index abc511941ef..5f6e6c8beb3 100644 --- a/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/command.dm +++ b/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/command.dm @@ -64,11 +64,15 @@ new /obj/item/storage/belt/security/full(src) new /obj/item/watertank/pepperspray(src) - new /obj/item/gun/energy/disabler(src) new /obj/item/storage/bag/garment/master_arms(src) new /obj/item/radio/headset/interdyne(src) new /obj/item/storage/toolbox/guncase/skyrat/c20r(src) +/obj/structure/closet/secure_closet/interdynefob/maa_locker/populate_contents_immediate() + . = ..() + + new /obj/item/gun/energy/disabler(src) + /obj/structure/closet/secure_closet/interdynefob/cl_locker icon_door = "hop" icon_state = "hop" diff --git a/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm b/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm index bf81406992f..2e4f7b67ea2 100644 --- a/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm +++ b/modular_skyrat/modules/mapping/code/lockers/interdyne_fob/security.dm @@ -33,10 +33,14 @@ ..() new /obj/item/storage/belt/security/full(src) - new /obj/item/gun/energy/e_gun(src) new /obj/item/storage/bag/garment/brig_officer(src) new /obj/item/radio/headset/interdyne(src) +/obj/structure/closet/secure_closet/interdynefob/brig_officer_locker/populate_contents_immediate() + . = ..() + + new /obj/item/gun/energy/e_gun(src) + /obj/structure/closet/secure_closet/interdynefob/armory_gear_locker anchored = 1 icon = 'modular_skyrat/master_files/icons/obj/closet.dmi' diff --git a/sound/ambience/antag/spy.ogg b/sound/ambience/antag/spy.ogg new file mode 100644 index 00000000000..1a5c64a3979 Binary files /dev/null and b/sound/ambience/antag/spy.ogg differ diff --git a/sound/ambience/license.txt b/sound/ambience/license.txt index 607dd6628e7..a0b6efb24c5 100644 --- a/sound/ambience/license.txt +++ b/sound/ambience/license.txt @@ -1,4 +1,4 @@ -ambidet1.ogg is Fast Talking by Kevin Macleod. It has been licensed under the CC-BY 3.0 license. +ambidet1.ogg and spy.ogg is Fast Talking by Kevin Macleod. It has been licensed under the CC-BY 3.0 license. It has been cropped for use ingame. ambidet2.ogg is Night on the Docks, Piano by Kevin Macleod. It has been licensed under CC-BY 3.0 license. It has been cropped for use ingame, and also fades in. diff --git a/strings/antagonist_flavor/spy_objective.json b/strings/antagonist_flavor/spy_objective.json new file mode 100644 index 00000000000..aa696baad6f --- /dev/null +++ b/strings/antagonist_flavor/spy_objective.json @@ -0,0 +1,84 @@ +{ + "objective_body": [ + "Assassinate a high profile crewmember without being caught.", + "Cause a disaster to shake the station.", + "Cause a station evacuation.", + "Deprive the station of as many @pick(stealables) as you can.", + "Ensure @pick(department) is @pick(affected) by the end of the shift.", + "Ensure @pick(location) is @pick(affected) by the end of the shift.", + "Ensure no heads of staff @pick(escape) the station.", + "Ensure no members of @pick(department) @pick(escape) the station.", + "Ensure no rival @pick(rivals) @pick(escape) the station.", + "Frame a crewmember for a crime.", + "Free the station's AI from its laws.", + "Halt the station's @pick(happenings).", + "Invoke a mutiny against the heads of staff.", + "Make it difficult, but not impossible to @pick(escape) the station.", + "Sabotage the station's power grid or engine.", + "Steal as many @pick(stealables) as you can.", + "Take control of the station as the new Captain.", + "Take hostages of high value crewmembers and demand a ransom." + ], + "department": [ + "Security", + "Engineering", + "Medical", + "Science", + "Supply" + ], + "location": [ + "engineering", + "genetics", + "hydroponics", + "medbay", + "the bar", + "the bridge", + "the brig", + "the cargo bay", + "the chapel", + "the kitchen", + "the library", + "xenobiology" + ], + "happenings": [ + "research", + "cargo operations", + "communications", + "genetic research", + "mining operation" + ], + "affected": [ + "ablaze", + "burning", + "covered in blood", + "demolished", + "destroyed", + "engulfed in flames", + "obliterated", + "on fire", + "ruined", + "sabotaged", + "wrecked" + ], + "rivals": [ + "agents", + "moles", + "operatives", + "spies", + "traitors" + ], + "stealables": [ + "items", + "objects", + "things", + "tools", + "weapons" + ], + "escape": [ + "depart", + "escape", + "evacuate", + "flee", + "leave" + ] +} diff --git a/tgstation.dme b/tgstation.dme index ee9bcf3515e..6b7af6f7df6 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3251,6 +3251,10 @@ #include "code\modules\antagonists\space_dragon\space_dragon.dm" #include "code\modules\antagonists\space_ninja\space_ninja.dm" #include "code\modules\antagonists\spiders\spiders.dm" +#include "code\modules\antagonists\spy\spy.dm" +#include "code\modules\antagonists\spy\spy_bounty.dm" +#include "code\modules\antagonists\spy\spy_bounty_handler.dm" +#include "code\modules\antagonists\spy\spy_uplink.dm" #include "code\modules\antagonists\survivalist\survivalist.dm" #include "code\modules\antagonists\syndicate_monkey\syndicate_monkey.dm" #include "code\modules\antagonists\traitor\balance_helper.dm" @@ -5970,6 +5974,7 @@ #include "code\modules\uplink\uplink_items\nukeops.dm" #include "code\modules\uplink\uplink_items\special.dm" #include "code\modules\uplink\uplink_items\species.dm" +#include "code\modules\uplink\uplink_items\spy_unique.dm" #include "code\modules\uplink\uplink_items\stealthy.dm" #include "code\modules\uplink\uplink_items\stealthy_tools.dm" #include "code\modules\uplink\uplink_items\suits.dm" diff --git a/tgui/packages/tgui/interfaces/AntagInfoSpy.tsx b/tgui/packages/tgui/interfaces/AntagInfoSpy.tsx new file mode 100644 index 00000000000..a26266bceb4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AntagInfoSpy.tsx @@ -0,0 +1,65 @@ +import { useBackend } from '../backend'; +import { Section, Stack } from '../components'; +import { Window } from '../layouts'; +import { Objective, ObjectivePrintout } from './common/Objectives'; + +const greenText = { + fontWeight: 'italics', + color: '#20b142', +}; + +const redText = { + fontWeight: 'italics', + color: '#e03c3c', +}; + +type Data = { + antag_name: string; + uplink_location: string | null; + objectives: Objective[]; +}; + +export const AntagInfoSpy = () => { + const { data } = useBackend(); + const { antag_name, uplink_location, objectives } = data; + return ( + + +
+ + + You have been equipped with a special uplink device disguised as{' '} + {uplink_location || 'something'} that will allow you to steal from + the station. + + + + Use it in hand to access your uplink, and{' '} + right click on bounty targets to steal them. + + + + + You may not be alone: There may be other spies on the station. + + + Work together or work against them: The choice is yours, but{' '} + you cannot share the rewards. + + + + + + +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts new file mode 100644 index 00000000000..395baf87915 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts @@ -0,0 +1,24 @@ +import { multiline } from 'common/string'; + +import { Antagonist, Category } from '../base'; + +const Spy: Antagonist = { + key: 'spy', + name: 'Spy', + description: [ + multiline` + Your mission, should you choose to accept it: Infiltrate Space Station 13. + Disguise yourself as a member of their crew and steal vital equipment. + Should you be caught or killed, your employer will disavow any knowledge + of your actions. Good luck agent. + `, + + multiline` + Complete Spy Bounties to earn rewards from your employer. + Use these rewards to sow chaos and mischief! + `, + ], + category: Category.Roundstart, +}; + +export default Spy; diff --git a/tgui/packages/tgui/interfaces/SpyUplink.tsx b/tgui/packages/tgui/interfaces/SpyUplink.tsx new file mode 100644 index 00000000000..87735c19ff7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/SpyUplink.tsx @@ -0,0 +1,122 @@ +import { BooleanLike } from 'common/react'; + +import { useBackend } from '../backend'; +import { BlockQuote, Box, Dimmer, Icon, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +type Bounty = { + name: string; + help: string; + difficulty: string; + reward: string; + claimed: BooleanLike; + can_claim: BooleanLike; +}; + +type Data = { + time_left: number; + bounties: Bounty[]; +}; + +const difficulty_to_color = { + easy: 'good', + medium: 'average', + hard: 'bad', +}; + +const BountyDimmer = (props: { text: string; color: string }) => { + return ( + + + + + + + {props.text} + + + + ); +}; + +const BountyDisplay = (props: { bounty: Bounty }) => { + const { bounty } = props; + + return ( +
+ {!!bounty.claimed && } + {!bounty.can_claim && !bounty.claimed && ( + + )} + + + + {bounty.name} + + + +
{bounty.help}
+
+ Reward: {bounty.reward} +
+
+ ); +}; + +// Formats a number of deciseconds into a string minutes:seconds +const format_deciseconds = (deciseconds: number) => { + const seconds = Math.floor(deciseconds / 10); + const minutes = Math.floor(seconds / 60); + + const seconds_left = seconds % 60; + const minutes_left = minutes % 60; + + const seconds_string = seconds_left.toString().padStart(2, '0'); + const minutes_string = minutes_left.toString().padStart(2, '0'); + + return `${minutes_string}:${seconds_string}`; +}; + +export const SpyUplink = () => { + const { data } = useBackend(); + const { bounties, time_left } = data; + + return ( + + +
+ Time until refresh: {format_deciseconds(time_left)} + + } + > + + + {bounties.map((bounty) => ( + + + + ))} + + +
+
+
+ ); +};