Skip to content

Commit

Permalink
A red spy has entered the base: Adds Spies, a roundstart antagonist i…
Browse files Browse the repository at this point in the history
…nspired by Goonstation's Spy-Thief (tgstation#81231)

# Disclaimer: No Goon code was referenced or used in the making of this
PR

## About The Pull Request

[Design Document (Read this for more
information)](https://hackmd.io/@L9JPMsZhRO2wI25rNI6GYg/rkYKM9Yc6)

This PR adds Spies as a new roundstart antagonist type, inspired by
Spy-Thiefs from Goonstation.

Spies are tasked with stealing various objects around the station, from
insulated gloves to the black box, from the clown's left leg to the
bridge's communications console.

For every item stolen, the Spy is rewarded with a random item from the
Syndicate Uplink, plus some items uniquely available to the Spy. Stolen
items are then shipped off and sold on the Black Market Uplink, allowing
the crew - or maybe some other evil-doers - to get their hands on them.


![image](https://github.com/tgstation/tgstation/assets/51863163/f057d480-4545-44da-b8fe-a8d09a5d2dcf)

More ideas for theft items and bounties are welcome. 

## Why It's Good For The Game

See the design document for more information. 

In short: Adds a solo antagonist which has less impact than your
Traitors and Heretics, but more impact than Paradox Clones and Thieves.
In other words: On the same tier as old traitors.

Seeks to embrace the sandbox aspect of antagonists more by having no
precise greentext objective, and instead some suggestions for chaos you
can embark in. Have fun with it!

## Changelog

:cl: Melbert
add: Spies may now roam the halls of Space Station 13. Watch your
belongings closely.
/:cl:
  • Loading branch information
MrMelbert authored and larentoun committed Mar 1, 2024
1 parent 6ae76b4 commit cfb8458
Show file tree
Hide file tree
Showing 56 changed files with 2,474 additions and 164 deletions.
13 changes: 13 additions & 0 deletions code/__DEFINES/antagonists.dm
Expand Up @@ -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",
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -379,3 +384,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"
Expand Up @@ -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"
1 change: 1 addition & 0 deletions code/__DEFINES/is_helpers.dm
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions code/__DEFINES/logging.dm
Expand Up @@ -161,6 +161,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"
Expand Down
2 changes: 2 additions & 0 deletions code/__DEFINES/role_preferences.dm
Expand Up @@ -16,6 +16,7 @@
#define ROLE_OPERATIVE "Operative"
#define ROLE_TRAITOR "Traitor"
#define ROLE_WIZARD "Wizard"
#define ROLE_SPY "Spy"

// Midround roles
#define ROLE_ABDUCTOR "Abductor"
Expand Down Expand Up @@ -128,6 +129,7 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_REV_HEAD = 14,
ROLE_TRAITOR = 0,
ROLE_WIZARD = 14,
ROLE_SPY = 0,

// Midround
ROLE_ABDUCTOR = 0,
Expand Down
12 changes: 12 additions & 0 deletions code/__DEFINES/uplink.dm
Expand Up @@ -12,10 +12,22 @@
/// 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)

/// Traitor discount size categories
#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
4 changes: 4 additions & 0 deletions code/__HELPERS/logging/antagonists.dm
Expand Up @@ -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)
19 changes: 11 additions & 8 deletions code/controllers/subsystem/blackmarket.dm
Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm
Expand Up @@ -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
87 changes: 45 additions & 42 deletions code/datums/mind/antag.dm
Expand Up @@ -105,6 +105,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
Expand All @@ -115,53 +140,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)
Expand All @@ -178,22 +176,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 + "<br>"
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)
Expand Down

0 comments on commit cfb8458

Please sign in to comment.