[READY] Syndicate Contractors (#14605)

* Syndicate Contractors initial PR

* Finalize initial version

DNP

Finalize initial version

* Baton now costs 6 Rep, show dead extraction penalty, fix scroll

* Reduce total TC, add zippo, balloon, icon tweaks

* Address moxian, AA and Farie

* Fix mode.txt

* oops

* Address Farie 2

* Tweak baton to work around stam crits, address SteelSlayer

* Address TM issues, TP menu

* Fix wrongly merged DME

* Tick contractor DM files again

* Remove step_y

* TC rewards always higher than preceding difficulty's reward

* Address Farie

* Remove extraction_turf from handle_target_return

* fix doc

* Merge part2

* Address AA again
This commit is contained in:
dearmochi
2020-12-09 12:03:23 -05:00
committed by GitHub
parent 2897b82345
commit 5edee29a50
73 changed files with 3588 additions and 250 deletions
@@ -0,0 +1,75 @@
/**
* # Contractor antagonist datum
*
* A variant of the Traitor, Contractors rely on kidnapping crew members to earn TC.
*
* Contractors are supplied with some unique items
* and three random low cost contraband items to help kickstart their contracts.
* A Traitor may become a Contractor if given the chance (random).
* They will forfeit all their initial TC and receive the above items.
* The opportunity to become a Contractor goes away after some time or if the traitor spends any initial TC.
*/
/datum/antagonist/traitor/contractor
name = "Contractor"
// Settings
/// How many telecrystals a traitor must forfeit to become a contractor.
var/tc_cost = 20
/// How long a traitor's chance to become a contractor lasts before going away. In deciseconds.
var/offer_duration = 10 MINUTES
// Variables
/// The associated contractor uplink. Only present if the offer was accepted.
var/obj/item/contractor_uplink/contractor_uplink = null
/// world.time at which the offer will expire.
var/offer_deadline = -1
/datum/antagonist/traitor/contractor/finalize_traitor()
..()
// Setup the vars and contractor stuff in the uplink
var/obj/item/uplink/hidden/U = owner.find_syndicate_uplink()
if(!U)
stack_trace("Potential contractor [owner] spawned without a hidden uplink!")
return
U.contractor = src
offer_deadline = world.time + offer_duration
// Greet them with the unique message
var/greet_text = "Contractors forfeit [tc_cost] telecrystals for the privilege of taking on kidnapping contracts for credit and TC payouts that can add up to more than the normal starting amount of TC.<br>"\
+ "If you are interested, simply access your hidden uplink and select the \"Contracting Opportunity\" tab for more information.<br>"
to_chat(owner.current, "<b><font size=4 color=red>You have been offered a chance to become a Contractor.</font></b><br>")
to_chat(owner.current, "<font color=red>[greet_text]</font>")
to_chat(owner.current, "<b><i><font color=red>This offer will expire in 10 minutes starting now (expiry time: <u>[station_time_timestamp(time = offer_deadline)]</u>).</font></i></b>")
/datum/antagonist/traitor/contractor/update_traitor_icons_added(datum/mind/traitor_mind)
if(!contractor_uplink)
return ..()
var/hud_name = "hudcontractor"
if(locate(/datum/objective/hijack) in owner.objectives)
hud_name = "hudhijackcontractor"
var/datum/atom_hud/antag/traitorhud = GLOB.huds[ANTAG_HUD_TRAITOR]
traitorhud.join_hud(owner.current, null)
set_antag_hud(owner.current, hud_name)
/**
* Accepts the offer to be a contractor if possible.
*/
/datum/antagonist/traitor/contractor/proc/become_contractor(mob/living/carbon/human/M, obj/item/uplink/U)
if(contractor_uplink || !istype(M))
return
if(U.uses < tc_cost || world.time >= offer_deadline)
var/reason = (U.uses < tc_cost) ? \
"you have insufficient telecrystals ([tc_cost] needed in total)" : \
"the deadline has passed"
to_chat(M, "<span class='warning'>You can no longer become a contractor as [reason].</span>")
return
// Give the kit
var/obj/item/storage/box/syndie_kit/contractor/B = new(M)
M.put_in_hands(B)
contractor_uplink = locate(/obj/item/contractor_uplink, B)
contractor_uplink.hub = new(M.mind, contractor_uplink)
// Update AntagHUD icon
update_traitor_icons_added(owner)
// Remove the TC
U.uses -= tc_cost
@@ -0,0 +1,182 @@
/**
* # Syndicate Hub
*
* Describes and manages the contracts and rewards for a single contractor.
*/
/datum/contractor_hub
// Settings
/// The number of contracts to generate initially.
var/num_contracts = 6
/// How much Contractor Rep to earn per contract completion.
var/rep_per_completion = 2
/// Completing every contract at a given difficulty will always result in a sum of TC greater or equal than the difficulty's threshold.
/// Structure: EXTRACTION_DIFFICULTY_(EASY|MEDIUM|HARD) => number
var/difficulty_tc_thresholds = list(
EXTRACTION_DIFFICULTY_EASY = 20,
EXTRACTION_DIFFICULTY_MEDIUM = 30,
EXTRACTION_DIFFICULTY_HARD = 40,
)
/// Maximum variation a single contract's TC reward can have upon generation.
/// In other words: final_reward = CEILING((tc_threshold / num_contracts) * (1 + (rand(-100, 100) / 100) * tc_variation), 1)
var/tc_variation = 0.25
/// TC reward multiplier if the target was extracted DEAD. Should be a penalty so between 0 and 1.
/// The final amount is rounded up.
var/dead_penalty = 0.2
/// List of purchases that can be done for Rep.
var/list/datum/rep_purchase/purchases = list(
/datum/rep_purchase/reroll,
/datum/rep_purchase/item/pinpointer,
/datum/rep_purchase/item/fulton,
/datum/rep_purchase/blackout,
/datum/rep_purchase/item/zippo,
/datum/rep_purchase/item/balloon,
)
// Variables
/// The contractor associated to this hub.
var/datum/mind/owner = null
/// The contractor uplink associated to this hub.
var/obj/item/contractor_uplink/contractor_uplink = null
/// The current contract in progress.
var/datum/syndicate_contract/current_contract = null
/// The contracts offered by the hub.
var/list/datum/syndicate_contract/contracts = null
/// List of targets from each contract in [/datum/contractor_hub/var/contracts].
/// Used to make sure two contracts from the same hub don't have the same target.
var/list/datum/mind/targets = null
/// Amount of telecrystals available for redeeming.
var/reward_tc_available = 0
/// Total amount of paid out telecrystals since the start.
var/reward_tc_paid_out = 0
/// The number of completed contracts.
var/completed_contracts = 0
/// Amount of Contractor Rep available for spending.
var/rep = 0
/// Current UI page index.
var/page = HUB_PAGE_CONTRACTS
/datum/contractor_hub/New(datum/mind/O, obj/item/contractor_uplink/U)
owner = O
contractor_uplink = U
// Instantiate purchases
for(var/i in 1 to length(purchases))
if(ispath(purchases[i]))
var/datum/rep_purchase/P = purchases[i]
purchases[i] = new P
else
stack_trace("Expected Hub purchase [purchases[i]] to be a type but it wasn't!")
/datum/contractor_hub/ui_host(mob/user)
return contractor_uplink
/**
* Called when the loading animation completes for the first time.
*/
/datum/contractor_hub/proc/first_login(mob/user)
if(!is_user_authorized(user))
return
user.playsound_local(user, 'sound/effects/contractstartup.ogg', 30, FALSE)
generate_contracts()
SStgui.update_uis(src)
/**
* Regenerates a list of contracts for the contractor to take up.
*/
/datum/contractor_hub/proc/generate_contracts()
contracts = list()
targets = list()
var/num_to_generate = min(num_contracts, length(GLOB.data_core.locked))
if(num_to_generate <= 0) // ?
return
// Contract generation
var/total_earnable_tc = list(0, 0, 0)
for(var/i in 1 to num_to_generate)
var/datum/syndicate_contract/C = new(src, owner, targets)
// Calculate TC reward for each difficulty
C.reward_tc = list(null, null, null)
for(var/difficulty in EXTRACTION_DIFFICULTY_EASY to EXTRACTION_DIFFICULTY_HARD)
var/amount_tc = calculate_tc_reward(num_to_generate, difficulty)
// Bump up the TC reward a little if it's too close to the lower difficulty's reward
if(difficulty > EXTRACTION_DIFFICULTY_EASY)
amount_tc = max(amount_tc, C.reward_tc[difficulty - 1] + (difficulty - 1))
C.reward_tc[difficulty] = amount_tc
total_earnable_tc[difficulty] += amount_tc
// Add to lists
contracts += C
targets += C.contract.target
// Fill the gap if a difficulty doesn't meet the TC threshold
for(var/difficulty in EXTRACTION_DIFFICULTY_EASY to EXTRACTION_DIFFICULTY_HARD)
var/total = total_earnable_tc[difficulty]
var/missing = difficulty_tc_thresholds[difficulty] - total
if(missing <= 0)
continue
// Just add the missing TC to a random contract
var/datum/syndicate_contract/C = pick(contracts)
C.reward_tc[difficulty] += missing
/**
* Generates an amount of TC to be used as a contract reward for the given difficulty.
*
* Arguments:
* * total_contracts - The number of contracts being generated.
* * difficulty - The difficulty to base the threshold from.
*/
/datum/contractor_hub/proc/calculate_tc_reward(total_contracts, difficulty = EXTRACTION_DIFFICULTY_EASY)
ASSERT(total_contracts > 0)
return CEILING((difficulty_tc_thresholds[difficulty] / total_contracts) * (1 + (rand(-100, 100) / 100) * tc_variation), 1)
/**
* Called when a [/datum/syndicate_contract] has been completed.
*
* Arguments:
* * tc - The final amount of TC to award.
* * creds - The final amount of credits to award.
*/
/datum/contractor_hub/proc/on_completion(tc, creds)
completed_contracts++
reward_tc_available += tc
rep += rep_per_completion
owner?.initial_account?.credit(creds, pick(list(
"CONGRATULATIONS. You are the 10,000th visitor of SquishySlimes.squish. Please find attached your [creds] credits.",
"Congratulations on winning your bet in the latest Clown vs. Mime match! Your account was credited with [creds] credits.",
"Deer fund beneficiary, We have please to imform you that overdue fund payments has finally is approved and yuor account credited with [creds] creadits.",
"Hey bro. How's it going? You bought me a beer a long time ago and I want to pay you back with [creds] creds. Enjoy!",
"Thank you for your initial investment of 500 credits! We have credited your account with [creds] as a token of appreciation.",
"Your refund request for 100 Dr. Maxman pills with the reason \"I need way more than 100 pills!\" has been received. We have credited your account with [creds] credits.",
"Your refund request for your WetSkrell.nt subscription has been received. We have credited your account with [creds] credits.",
)))
// Clean up
current_contract = null
/**
* Gives any unclaimed TC to the given mob.
*
* Arguments:
* * M - The mob to give the TC to.
*/
/datum/contractor_hub/proc/claim_tc(mob/living/M)
if(reward_tc_available <= 0)
return
// Spawn the crystals
var/obj/item/stack/telecrystal/TC = new(get_turf(M), reward_tc_available)
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H.put_in_hands(TC))
to_chat(H, "<span class='notice'>Your payment materializes into your hands!</span>")
else
to_chat(M, "<span class='notice'>Your payment materializes on the floor.</span>")
// Update info
reward_tc_paid_out += reward_tc_available
reward_tc_available = 0
/**
* Returns whether the given mob is allowed to connect to the uplink.
*
* Arguments:
* * M - The mob.
*/
/datum/contractor_hub/proc/is_user_authorized(mob/living/carbon/M)
return M.mind.has_antag_datum(/datum/antagonist/traitor/contractor)
@@ -0,0 +1,124 @@
/datum/contractor_hub/ui_act(action, list/params)
if(..())
return
. = TRUE
if(!contracts)
if(action == "complete_load_animation")
first_login(usr)
else
switch(action)
if("page")
var/newpage = text2num(params["page"])
if(!(newpage in list(HUB_PAGE_CONTRACTS, HUB_PAGE_SHOP)))
return
page = newpage
if("extract")
var/error_message = current_contract?.start_extraction_process(ui_host(), usr)
if(length(error_message))
to_chat(usr, "<span class='warning'>[error_message]</span>")
if("claim")
claim_tc(usr)
if("activate")
var/datum/syndicate_contract/C = locateUID(params["uid"])
var/difficulty = text2num(params["difficulty"])
if(!istype(C) || !(C in contracts) || !(difficulty in list(EXTRACTION_DIFFICULTY_EASY, EXTRACTION_DIFFICULTY_MEDIUM, EXTRACTION_DIFFICULTY_HARD)))
return
C.initiate(usr, difficulty)
if("abort")
current_contract?.fail("Aborted by agent.")
if("purchase")
var/datum/rep_purchase/P = locateUID(params["uid"])
if(!istype(P) || !(P in purchases) || rep < P.cost)
return
P.buy(src, usr)
else
return FALSE
var/obj/item/U = ui_host()
U?.add_fingerprint(usr)
/datum/contractor_hub/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
ui = new(user, src, ui_key, "Contractor", "Syndicate Contractor Uplink", 500, 600, master_ui, state)
ui.open()
ui.set_autoupdate(FALSE)
/datum/contractor_hub/ui_data(mob/user)
var/list/data = list()
if(!contracts)
if(!is_user_authorized(user))
data["unauthorized"] = TRUE
return data
data["load_animation_completed"] = FALSE
return data
else
data["load_animation_completed"] = TRUE
data["page"] = page
data["tc_available"] = reward_tc_available
data["tc_paid_out"] = reward_tc_paid_out
data["completed_contracts"] = completed_contracts
data["contract_active"] = !isnull(current_contract)
data["rep"] = rep
switch(page)
if(HUB_PAGE_CONTRACTS)
var/list/contracts = list()
data["contracts"] = contracts
for(var/c in contracts)
var/datum/syndicate_contract/C = c
if(C.status == CONTRACT_STATUS_INVALID)
continue
var/list/contract_data = list(
uid = C.UID(),
status = C.status,
target_name = C.target_name,
fluff_message = C.fluff_message,
has_photo = !isnull(C.target_photo),
)
if(C.target_photo)
usr << browse_rsc(C.target_photo, "target_photo_[C.UID()].png")
switch(C.status)
if(CONTRACT_STATUS_INACTIVE)
var/list/difficulties = list(null, null, null)
contract_data["difficulties"] = difficulties
for(var/difficulty in EXTRACTION_DIFFICULTY_EASY to EXTRACTION_DIFFICULTY_HARD)
difficulties[difficulty] = list(name = C.contract.candidate_zones[difficulty].map_name, reward = C.reward_tc[difficulty])
if(CONTRACT_STATUS_ACTIVE)
contract_data["world_time"] = world.time
contract_data["time_left"] = C.extraction_deadline - world.time
if(CONTRACT_STATUS_COMPLETED)
contract_data["completed_time"] = C.completed_time
contract_data["dead_extraction"] = C.dead_extraction
if(CONTRACT_STATUS_FAILED)
contract_data["fail_reason"] = C.fail_reason
if(C.contract.extraction_zone)
contract_data["objective"] = list(
extraction_zone = C.contract.extraction_zone.map_name,
reward_tc = C.reward_tc[C.chosen_difficulty],
reward_credits = C.reward_credits,
)
contracts += list(contract_data)
data["can_extract"] = current_contract?.contract.can_start_extraction_process(ui_host(), usr) || FALSE
if(HUB_PAGE_SHOP)
var/list/buyables = list()
for(var/p in purchases)
var/datum/rep_purchase/P = p
buyables += list(list(
uid = P.UID(),
name = P.name,
description = P.description,
cost = P.cost,
stock = P.stock,
))
data["buyables"] = buyables
return data
@@ -0,0 +1,235 @@
/**
* # Contract Objective
*
* Describes the target to kidnap and the extraction area of a [/datum/syndicate_contract].
*/
/datum/objective/contract
// Settings
/// Jobs that cannot be the kidnapping target.
var/static/list/forbidden_jobs = list(
/datum/job/captain,
)
/// Static whitelist of area names that can be used as an extraction zone, structured by difficulty.
/// An area's difficulty should be measured in how crowded it generally is, how out of the way it is and so on.
/// Outdoor or invalid areas are filtered out.
/// Structure: EXTRACTION_DIFFICULTY_(EASY|MEDIUM|HARD) => list(<area name>)
var/static/list/possible_zone_names = list(
EXTRACTION_DIFFICULTY_EASY = list(
// Rooms
"Alternate Construction Area",
"Barber Shop",
"Escape Shuttle Hallway Podbay",
"Garden",
"Incinerator",
"Locker Room",
"Locker Toilets",
"Maintenance Bar",
"Medical Secondary Storage",
"Mechanic Workshop",
"Port Emergency Storage",
"Psych Room",
"Toxins Launch Room",
"Toxins Mixing Room",
"Turbine",
"Virology",
"Waste Disposal",
// Maintenance
"Aft Port Solar Maintenance",
"Aft Starboard Solar Maintenance",
"Arrivals North Maintenance",
"Bar Maintenance",
"Cargo Maintenance",
"Dormitory Maintenance",
"Electrical Maintenance",
"EVA Maintenance",
"Engineering Maintenance",
"Fore Port Solar Maintenance",
"Fore Starboard Solar Maintenance",
"Genetics Maintenance",
"Locker Room Maintenance",
"Medbay Maintenance",
"Science Maintenance",
),
EXTRACTION_DIFFICULTY_MEDIUM = list(
// Rooms
"Aft Primary Hallway",
"Atmospherics",
"Arcade",
"Assembly Line",
"Auxiliary Tool Storage",
"Break Room",
"Blueshield's Office",
"Cargo Bay",
"Chapel",
"Chapel Office",
"Clown's Office",
"Construction Area",
"Courtroom",
"Dormitory Toilets",
"Engineering",
"Engineering Control Room",
"Escape Shuttle Hallway",
"Experimentation Lab",
"Holodeck Alpha",
"Hydroponics",
"Library",
"Mime's Office",
"Mining Dock",
"Morgue",
"Office Supplies",
"Pet Store",
"Primary Tool Storage",
"Research Division",
"Security Checkpoint",
"Technical Storage",
"Teleporter",
"Toxins Storage",
"Vacant Office",
"Research Testing Lab",
"Xenobiology Lab",
// Maintenance
"Atmospherics Maintenance",
"Bridge Maintenance",
),
EXTRACTION_DIFFICULTY_HARD = list(
// No AI Chamber because I'm not that sadistic.
// Most Bridge areas are excluded because of they'd be basically impossible. So are Brig areas.
"AI Satellite Antechamber",
"AI Satellite Atmospherics",
"AI Satellite Service",
"AI Satellite Hallway",
"Bar",
"Cargo Office",
"Central Primary Hallway",
"Chemistry",
"Chief Engineer's office",
"Chief Medical Officer's office",
"Cloning Lab",
"Cryogenics",
"Dorms",
"Engineering Equipment Storage",
"Engineering Foyer",
"EVA Storage",
"Gateway",
"Genetics Lab",
"Gravity Generator",
"Head of Personnel's Office",
"Heads of Staff Meeting Room",
"Kitchen", // Chef CQC is no joke.
"Mech Bay",
"Medbay",
"Medbay Reception",
"Medical Storage",
"Medical Treatment Center",
"Medbay Patient Ward",
"Messaging Server Room",
"Mr Chang's",
"Nanotrasen Representative's Office",
"Paramedic",
"Port Primary Hallway",
"Quartermaster's Office",
"Research Director's Office",
"Research and Development",
"Robotics Lab",
"Surgery 1",
"Surgery 2",
"Telecoms Central Compartment",
"Secure Storage",
),
)
// Variables
/// The designated area where the kidnapee must be extracted to complete the objective.
var/area/extraction_zone = null
/// The contract's difficulty. Determines the reward on completion.
var/chosen_difficulty = EXTRACTION_DIFFICULTY_EASY
/// Associated lazy list of areas the contractor can pick from and extract the kidnapee there.
/// Structure: EXTRACTION_DIFFICULTY_(EASY|MEDIUM|HARD) => /area
var/list/area/candidate_zones = null
/// List of people who cannot be selected as contract target.
var/list/datum/mind/target_blacklist = null
/// Static list that is basically [/datum/objective/contract/var/possible_zone_names] but with area names replaced by /area objects if available.
var/static/list/possible_zones = null
/// The owning [/datum/syndicatce_contract].
var/datum/syndicate_contract/owning_contract = null
/// Name fixer regex because area names have rogue characters sometimes.
var/static/regex/name_fixer = regex("(\[a-z0-9 \\'\]+)$", "ig")
/datum/objective/contract/New(contract)
owning_contract = contract
// Init static variable
if(!possible_zones)
// Compute the list of all zones by their name first
var/list/all_areas_by_name = list()
for(var/a in GLOB.all_areas)
var/area/A = a
if(A.outdoors || !is_station_level(A.z))
continue
var/i = findtext(A.map_name, name_fixer)
if(i)
var/clean_name = copytext(A.map_name, i)
clean_name = replacetext(clean_name, "\\", "")
all_areas_by_name[clean_name] = A
possible_zones = list()
for(var/difficulty in EXTRACTION_DIFFICULTY_EASY to EXTRACTION_DIFFICULTY_HARD)
var/list/difficulty_areas = list()
for(var/area_name in possible_zone_names[difficulty])
var/area/A = all_areas_by_name[area_name]
if(!A)
continue
difficulty_areas += A
possible_zones += list(difficulty_areas)
// Select zones
for(var/difficulty in EXTRACTION_DIFFICULTY_EASY to EXTRACTION_DIFFICULTY_HARD)
pick_candidate_zone(difficulty)
return ..()
/datum/objective/contract/is_invalid_target(datum/mind/possible_target)
if((possible_target.assigned_job in forbidden_jobs) || (target_blacklist && (possible_target in target_blacklist)))
return TARGET_INVALID_BLACKLISTED
return ..()
/datum/objective/contract/on_target_cryo()
if(owning_contract.status in list(CONTRACT_STATUS_COMPLETED, CONTRACT_STATUS_FAILED))
return
// We pick the target ourselves so we don't want the default behaviour.
owning_contract.invalidate()
/**
* Assigns a randomly selected zone to the contract's selectable zone at the given difficulty.
*
* Arguments:
* * difficulty - The difficulty to assign.
*/
/datum/objective/contract/proc/pick_candidate_zone(difficulty = EXTRACTION_DIFFICULTY_EASY)
if(!candidate_zones)
candidate_zones = list(null, null, null)
candidate_zones[difficulty] = pick(possible_zones[difficulty])
/**
* Updates the objective's information with the given difficulty.
*
* Arguments:
* * difficulty - The chosen difficulty.
* * S - The parent [/datum/syndicate_contract].
*/
/datum/objective/contract/proc/choose_difficulty(difficulty = EXTRACTION_DIFFICULTY_EASY, datum/syndicate_contract/S)
. = FALSE
if(!ISINDEXSAFE(candidate_zones, difficulty))
return
var/area/A = candidate_zones[difficulty]
extraction_zone = A
chosen_difficulty = difficulty
explanation_text = "Kidnap [S.target_name] by any means and extract them in [A.map_name] using your Contractor Uplink. You will earn [S.reward_tc[difficulty]] telecrystals and [S.reward_credits] credits upon completion. Your reward will be severely reduced if your target is dead."
return TRUE
/**
* Returns whether the extraction process can be started.
*
* Arguments:
* * M - The contractor.
* * target - The target.
*/
/datum/objective/contract/proc/can_start_extraction_process(mob/living/carbon/human/M, mob/living/carbon/human/target)
return get_area(M) == extraction_zone && get_area(target) == extraction_zone
@@ -0,0 +1,65 @@
/**
* # Rep Purchase
*
* Describes something that can be purchased with Contractor Rep.
*/
/datum/rep_purchase
/// The display name of the purchase.
var/name = ""
/// The description of the purchase.
var/description = "This shouldn't appear."
/// The price in Contractor Rep of the purchase.
var/cost = 0
/// How many times the purchase can be made.
/// -1 means infinite stock.
var/stock = -1
/**
* Attempts to perform the purchase.
*
* Returns TRUE or FALSE depending on whether the purchase succeeded.
*
* Arguments:
* * hub - The contractor hub.
* * user - The user who is making the purchase.
*/
/datum/rep_purchase/proc/buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
. = FALSE
if(hub.owner.current != user)
to_chat(user, "<span class='warning'>You were not recognized as this hub's original user.</span>")
return
if(hub.rep < cost)
to_chat(user, "<span class='warning'>You do not have enough Rep.</span>")
return
if(stock == 0)
to_chat(user, "<span class='warning'>This item is out of stock.</span>")
return
else if(stock > 0)
stock--
hub.rep -= cost
on_buy(hub, user)
return TRUE
/**
* Called when the purchase was made successfully.
*
* Arguments:
* * hub - The contractor hub.
* * user - The user who made the purchase.
*/
/datum/rep_purchase/proc/on_buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
return
/**
* # Rep Purchase - Item
*
* Describes an item that can be purchased with Contractor Rep.
*/
/datum/rep_purchase/item
/// The typepath of the item to instantiate and give to the buyer on purchase.
var/obj/item/item_type = null
/datum/rep_purchase/item/on_buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
..()
var/obj/item/I = new item_type(user)
user.put_in_hands(I)
@@ -0,0 +1,27 @@
/**
* # Rep Purchase - Contractor Balloon
*/
/datum/rep_purchase/item/balloon
name = "Contractor Balloon"
description = "An unique black and gold balloon with no purpose other than showing off. All contracts must be completed in the hardest location to unlock this."
cost = 12
stock = 1
item_type = /obj/item/toy/syndicateballoon/contractor
/datum/rep_purchase/item/balloon/buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
var/eligible = TRUE
for(var/c in hub.contracts)
var/datum/syndicate_contract/C = c
if(C.status != CONTRACT_STATUS_COMPLETED || C.chosen_difficulty != EXTRACTION_DIFFICULTY_HARD)
eligible = FALSE
break
if(!eligible)
to_chat(user, "<span class='warning'>All of your contracts must be completed in the hardest location to be eligible for this item.</span>")
return FALSE
return ..()
/obj/item/toy/syndicateballoon/contractor
name = "contractor balloon"
desc = "A black and gold balloon carried only by legendary Syndicate agents."
icon_state = "contractorballoon"
item_state = "contractorballoon"
@@ -0,0 +1,25 @@
/**
* # Rep Purchase - Blackout
*/
/datum/rep_purchase/blackout
name = "Blackout"
description = "Overloads the station's power net, shorting random APCs."
cost = 3
// Settings
/// How long a contractor must wait before calling another blackout, in deciseconds.
var/static/cooldown = 15 MINUTES
// Variables
/// Static cooldown variable for blackouts.
var/static/next_blackout = -1
/datum/rep_purchase/blackout/buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
if(next_blackout > world.time)
var/timeleft = (next_blackout - world.time) / 10
to_chat(user, "<span class='warning'>Another blackout may not be requested for [seconds_to_clock(timeleft)].</span>")
return FALSE
return ..()
/datum/rep_purchase/blackout/on_buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
..()
next_blackout = world.time + cooldown
power_failure()
@@ -0,0 +1,18 @@
/**
* # Rep Purchase - Fulton Extraction Kit
*/
/datum/rep_purchase/item/fulton
name = "Fulton Extraction Kit"
description = "A balloon that can be used to extract equipment or personnel to a Fulton Recovery Beacon. Anything not bolted down can be moved. Link the pack to a beacon by using the pack in hand."
cost = 1
stock = 1
item_type = /obj/item/storage/box/contractor/fulton_kit
/obj/item/storage/box/contractor/fulton_kit
name = "fulton extraction kit"
icon_state = "box_of_doom"
/obj/item/storage/box/contractor/fulton_kit/New()
..()
new /obj/item/extraction_pack(src)
new /obj/item/fulton_core(src)
@@ -0,0 +1,9 @@
/**
* # Rep Purchase - Contractor Pinpointer
*/
/datum/rep_purchase/item/pinpointer
name = "Contractor Pinpointer"
description = "A low accuracy pinpointer that can track anyone in the sector without the need for suit sensors. Can only be used by the first person to activate it."
cost = 1
stock = 2
item_type = /obj/item/pinpointer/crew/contractor
@@ -0,0 +1,28 @@
/**
* # Rep Purchase - Contract Reroll
*/
/datum/rep_purchase/reroll
name = "Contract Reroll"
description = "Replaces your inactive contracts with new ones, containing a new target and extraction zones."
cost = 2
/datum/rep_purchase/reroll/buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
var/eligible = FALSE
for(var/c in hub.contracts)
var/datum/syndicate_contract/C = c
if(C.status == CONTRACT_STATUS_INACTIVE)
eligible = TRUE
break
if(!eligible)
to_chat(user, "<span class='warning'>There are no inactive contracts that can be rerolled.</span>")
return FALSE
return ..()
/datum/rep_purchase/reroll/on_buy(datum/contractor_hub/hub, mob/living/carbon/human/user)
..()
var/changed = 0
for(var/c in hub.contracts)
var/datum/syndicate_contract/C = c
if(C.status == CONTRACT_STATUS_INACTIVE && C.generate())
changed++
hub.contractor_uplink?.message_holder("Agent, we have replaced [changed] contract\s with new ones.")
@@ -0,0 +1,16 @@
/**
* # Rep Purchase - Contractor Zippo Lighter
*/
/datum/rep_purchase/item/zippo
name = "Contractor Zippo Lighter"
description = "An unique black and gold zippo lighter with no purpose other than showing off."
cost = 12
stock = 1
item_type = /obj/item/lighter/zippo/contractor
/obj/item/lighter/zippo/contractor
name = "contractor zippo lighter"
desc = "An unique black and gold zippo commonly carried by elite Syndicate agents."
icon_state = "contractorzippo"
icon_on = "contractorzippoon"
icon_off = "contractorzippo"
@@ -0,0 +1,575 @@
#define DEFAULT_NAME "Unknown"
#define DEFAULT_RANK "Unknown"
#define EXTRACTION_PHASE_PREPARE 5 SECONDS
#define EXTRACTION_PHASE_PORTAL 5 SECONDS
#define COMPLETION_NOTIFY_DELAY 5 SECONDS
#define RETURN_BRUISE_CHANCE 50
#define RETURN_BRUISE_DAMAGE 20
#define RETURN_SOUVENIR_CHANCE 10
/**
* # Syndicate Contract
*
* Describes a contract that can be completed by a [/datum/antagonist/traitor/contractor].
*/
/datum/syndicate_contract
// Settings
/// Cooldown before making another extraction request in deciseconds.
var/extraction_cooldown = 10 MINUTES
/// How long an extraction portal remains before going away. Should be less than [/datum/syndicate_contract/var/extraction_cooldown].
var/portal_duration = 5 MINUTES
/// How long a target remains in the Syndicate jail.
var/prison_time = 4 MINUTES
/// List of items a target can get randomly after their return.
var/list/obj/item/souvenirs = list(
/obj/item/bedsheet/syndie,
/obj/item/clothing/under/syndicate/tacticool,
/obj/item/coin/antagtoken/syndicate,
/obj/item/poster/syndicate_recruitment,
/obj/item/reagent_containers/food/snacks/syndicake,
/obj/item/reagent_containers/food/snacks/tatortot,
/obj/item/storage/box/fakesyndiesuit,
/obj/item/storage/fancy/cigarettes/cigpack_syndicate,
/obj/item/toy/figure/syndie,
/obj/item/toy/nuke,
/obj/item/toy/plushie/nukeplushie,
/obj/item/toy/sword,
/obj/item/toy/syndicateballoon,
)
/// The base credits reward upon completion. Multiplied by the two lower bounds below.
var/credits_base = 100
// The lower bound of the credits reward multiplier.
var/credits_lower_mult = 25
// The upper bound of the credits reward multiplier.
var/credits_upper_mult = 40
// Implants (non cybernetic ones) that shouldn't be removed when a victim gets kidnapped.
// Typecache; initialized in New()
var/static/implants_to_keep = null
// Variables
/// The owning contractor hub.
var/datum/contractor_hub/owning_hub = null
/// The [/datum/objective/contract] associated to this contract.
var/datum/objective/contract/contract = null
/// Current contract status.
var/status = CONTRACT_STATUS_INVALID
/// Formatted station time at which the contract was completed, if applicable.
var/completed_time
/// Whether the contract was completed with the victim being dead on extraction.
var/dead_extraction = FALSE
/// Visual reason as to why the contract failed, if applicable.
var/fail_reason
/// The selected difficulty.
var/chosen_difficulty = -1
/// The flare indicating the extraction point.
var/obj/effect/contractor_flare/extraction_flare = null
/// The extraction portal.
var/obj/effect/portal/redspace/contractor/extraction_portal = null
/// The world.time at which the current extraction fulton will vanish and another extraction can be requested.
var/extraction_deadline = -1
/// Name of the target to display on the UI.
var/target_name
/// Fluff message explaining why the kidnapee is the target.
var/fluff_message
/// The target's photo to display on the UI.
var/image/target_photo = null
/// Amount of telecrystals the contract will receive upon completion, depending on the chosen difficulty.
/// Structure: EXTRACTION_DIFFICULTY_(EASY|MEDIUM|HARD) => number
var/list/reward_tc = null
/// Amount of credits the contractor will receive upon completion.
var/reward_credits = 0
/// The kidnapee's belongings. Set upon extraction by the contractor.
var/list/obj/item/victim_belongings = null
/// Temporary objects that are available to the kidnapee during their time in jail. These are deleted when the victim is returned.
var/list/obj/temp_objs = null
/// Deadline reached timer handle. Deletes the portal and tells the agent to call extraction again.
var/extraction_timer_handle = null
/// Prisoner jail timer handle. On completion, returns the prisoner back to station.
var/prisoner_timer_handle = null
/// Whether the additional fluff story from any contractor completing all of their contracts was made already or not.
var/static/nt_am_board_resigned = FALSE
/datum/syndicate_contract/New(datum/contractor_hub/hub, datum/mind/owner, list/datum/mind/target_blacklist, target_override)
// Init settings
if(!implants_to_keep)
implants_to_keep = typecacheof(list(
// These two are specifically handled in code to prevent usage, but are included here for clarity.
/obj/item/implant/storage,
/obj/item/implant/uplink,
// The rest
/obj/item/implant/adrenalin,
/obj/item/implant/emp,
/obj/item/implant/explosive,
/obj/item/implant/freedom,
/obj/item/implant/traitor,
))
// Initialize
owning_hub = hub
contract = new /datum/objective/contract(src)
contract.owner = owner
contract.target_blacklist = target_blacklist
generate(target_override)
/**
* Fills the contract with valid data to be used.
*/
/datum/syndicate_contract/proc/generate(target_override)
. = FALSE
// Select the target
var/datum/mind/T
if(target_override)
contract.target = target_override
T = target_override
else
contract.find_target()
T = contract.target
if(!T)
return
// In case the contract is invalidated
contract.extraction_zone = null
contract.target_blacklist |= T
for(var/difficulty in EXTRACTION_DIFFICULTY_EASY to EXTRACTION_DIFFICULTY_HARD)
contract.pick_candidate_zone(difficulty)
// Fill data
var/datum/data/record/R = find_record("name", T.name, GLOB.data_core.general)
target_name = "[R?.fields["name"] || T.current?.real_name || DEFAULT_NAME], the [R?.fields["rank"] || T.assigned_role || DEFAULT_RANK]"
reward_credits = credits_base * rand(credits_lower_mult, credits_upper_mult)
// Fluff message
var/base = pick(strings(CONTRACT_STRINGS_WANTED, "basemessage"))
var/verb_string = pick(strings(CONTRACT_STRINGS_WANTED, "verb"))
var/noun = pickweight(strings(CONTRACT_STRINGS_WANTED, "noun"))
var/location = pickweight(strings(CONTRACT_STRINGS_WANTED, "location"))
fluff_message = "[base] [verb_string] [noun] [location]."
// Photo
if(R?.fields["photo"])
var/icon/temp = new('icons/turf/floors.dmi', pick("floor", "wood", "darkfull", "stairs"))
temp.Blend(R.fields["photo"], ICON_OVERLAY)
target_photo = temp
// OK
status = CONTRACT_STATUS_INACTIVE
fail_reason = ""
return TRUE
/**
* Begins the contract if possible.
*
* Arguments:
* * M - The contractor.
* * difficulty - The chosen difficulty level.
*/
/datum/syndicate_contract/proc/initiate(mob/living/M, difficulty = EXTRACTION_DIFFICULTY_EASY)
. = FALSE
if(status != CONTRACT_STATUS_INACTIVE || !ISINDEXSAFE(reward_tc, difficulty))
return
else if(owning_hub.current_contract)
to_chat(M, "<span class='warning'>You already have an ongoing contract!</span>")
return
if(!contract.choose_difficulty(difficulty, src))
return FALSE
status = CONTRACT_STATUS_ACTIVE
chosen_difficulty = difficulty
owning_hub.current_contract = src
owning_hub.contractor_uplink?.message_holder("Request for this contract confirmed. Good luck, agent.", 'sound/machines/terminal_prompt.ogg')
return TRUE
/**
* Marks the contract as completed and gives the rewards to the contractor.
*
* Arguments:
* * target_dead - Whether the target was extracted dead.
*/
/datum/syndicate_contract/proc/complete(target_dead = FALSE)
if(status != CONTRACT_STATUS_ACTIVE)
return
var/final_tc_reward = reward_tc[chosen_difficulty]
if(target_dead)
final_tc_reward = CEILING(final_tc_reward * owning_hub.dead_penalty, 1)
// Notify the Hub
owning_hub.on_completion(final_tc_reward, reward_credits)
// Finalize
status = CONTRACT_STATUS_COMPLETED
completed_time = station_time_timestamp()
dead_extraction = target_dead
addtimer(CALLBACK(src, .proc/notify_completion, final_tc_reward, reward_credits, target_dead), COMPLETION_NOTIFY_DELAY)
/**
* Marks the contract as invalid and effectively cancels it for later use.
*/
/datum/syndicate_contract/proc/invalidate()
if(!owning_hub)
return
if(status in list(CONTRACT_STATUS_COMPLETED, CONTRACT_STATUS_FAILED))
return
clean_up()
var/pre_text
if(src == owning_hub.current_contract)
owning_hub.current_contract = null
pre_text = "Agent, it appears the target you were tasked to kidnap can no longer be reached."
else
pre_text = "Agent, a still inactive contract can no longer be done as the target has gone off our sensors."
var/outcome_text
if(generate())
status = CONTRACT_STATUS_INACTIVE
outcome_text = "Luckily, there is another target on station we can interrogate. A new contract can be found in your uplink."
else
// Too bad.
status = CONTRACT_STATUS_INVALID
outcome_text = "Unfortunately, we could not find another target to interrogate and thus we cannot give you another contract."
if(owning_hub.contractor_uplink)
owning_hub.contractor_uplink.message_holder("[pre_text] [outcome_text]", 'sound/machines/terminal_prompt_deny.ogg')
SStgui.update_uis(owning_hub)
/**
* Marks the contract as failed and stops it.
*
* Arguments:
* * difficulty - The visual reason as to why the contract failed.
*/
/datum/syndicate_contract/proc/fail(reason)
if(status != CONTRACT_STATUS_ACTIVE)
return
// Update info
owning_hub.current_contract = null
status = CONTRACT_STATUS_FAILED
fail_reason = reason
// Notify
clean_up()
owning_hub.contractor_uplink?.message_holder("You failed to kidnap the target, agent. Do not disappoint us again.", 'sound/machines/terminal_prompt_deny.ogg')
/**
* Initiates the extraction process if conditions are met.
*
* Arguments:
* * M - The contractor.
*/
/datum/syndicate_contract/proc/start_extraction_process(obj/item/contractor_uplink/U, mob/living/carbon/human/M)
if(!U?.Adjacent(M))
return "Where in space is your uplink?!"
else if(status != CONTRACT_STATUS_ACTIVE)
return "This contract is not active."
else if(extraction_deadline > world.time)
return "Another extraction attempt cannot be made yet."
var/mob/target = contract.target.current
if(!target)
invalidate()
return "The target is no longer on our sensors. Your contract will be invalidated and replaced with another one."
else if(!contract.can_start_extraction_process(M, target))
return "You and the target must be standing in the extraction area to start the extraction process."
M.visible_message("<span class='notice'>[M] starts entering a cryptic series of characters on [U].</span>",\
"<span class='notice'>You start entering an extraction signal to your handlers on [U]...</span>")
if(do_after(M, EXTRACTION_PHASE_PREPARE, target = M))
if(!U.Adjacent(M) || extraction_deadline > world.time)
return
var/obj/effect/contractor_flare/F = new(get_turf(M))
extraction_flare = F
extraction_deadline = world.time + extraction_cooldown
M.visible_message("<span class='notice'>[M] enters a mysterious code on [U] and pulls a black and gold flare from [M.p_their()] belongings before lighting it.</span>",\
"<span class='notice'>You finish entering the signal on [U] and light an extraction flare, initiating the extraction process.</span>")
addtimer(CALLBACK(src, .proc/open_extraction_portal, U, M, F), EXTRACTION_PHASE_PORTAL)
extraction_timer_handle = addtimer(CALLBACK(src, .proc/deadline_reached), portal_duration, TIMER_STOPPABLE)
/**
* Opens the extraction portal.
*
* Arguments:
* * U - The uplink.
* * M - The contractor.
* * F - The flare.
*/
/datum/syndicate_contract/proc/open_extraction_portal(obj/item/contractor_uplink/U, mob/living/carbon/human/M, obj/effect/contractor_flare/F)
if(!U || !M || status != CONTRACT_STATUS_ACTIVE)
invalidate()
return
else if(!F)
U.message_holder("Extraction flare could not be located, agent. Ensure the extraction zone is clear before signaling us.", 'sound/machines/terminal_prompt_deny.ogg')
return
else if(!ismob(contract.target.current))
invalidate()
return
U.message_holder("Extraction signal received, agent. [GLOB.using_map.full_name]'s bluespace transport jamming systems have been sabotaged. "\
+ "We have opened a temporary portal at your flare location - proceed to the target's extraction by inserting them into the portal.", 'sound/effects/confirmdropoff.ogg')
// Open a portal
var/obj/effect/portal/redspace/contractor/P = new(get_turf(F), pick(GLOB.syndieprisonwarp), null, 0)
P.contract = src
P.contractor_mind = M.mind
P.target_mind = contract.target
extraction_portal = P
do_sparks(4, FALSE, P.loc)
/**
* Called when a contract target has been extracted through the portal.
*
* Arguments:
* * M - The target mob.
* * P - The extraction portal.
*/
/datum/syndicate_contract/proc/target_received(mob/living/M, obj/effect/portal/redspace/contractor/P)
INVOKE_ASYNC(src, .proc/clean_up)
complete(M.stat == DEAD)
handle_target_experience(M, P)
/**
* Notifies the uplink's holder that a contract has been completed.
*
* Arguments:
* * tc - How many telecrystals they have received.
* * creds - How many credits they have received.
* * target_dead - Whether the target was extracted dead.
*/
/datum/syndicate_contract/proc/notify_completion(tc, creds, target_dead)
var/penalty_text = ""
if(target_dead)
penalty_text = " (penalty applied as the target was extracted dead)"
owning_hub.contractor_uplink?.message_holder("Well done, agent. The package has been received and will be processed shortly before being returned. "\
+ "As agreed, you have been credited with [tc] telecrystals[penalty_text] and [creds] credits.", 'sound/machines/terminal_prompt_confirm.ogg')
/**
* Handles the target's experience from extraction.
*
* Arguments:
* * M - The target mob.
* * P - The extraction portal.
*/
/datum/syndicate_contract/proc/handle_target_experience(mob/living/M, obj/effect/portal/redspace/contractor/P)
var/turf/T = get_turf(P)
var/mob/living/carbon/human/H = M
// Prepare their return
prisoner_timer_handle = addtimer(CALLBACK(src, .proc/handle_target_return, M, T), prison_time, TIMER_STOPPABLE)
LAZYSET(GLOB.prisoner_belongings.prisoners, M, src)
// Shove all of the victim's items in the secure locker.
victim_belongings = list()
var/list/obj/item/stuff_to_transfer = list()
// Cybernetic implants get removed first (to deal with NODROP stuff)
for(var/obj/item/organ/internal/cyberimp/I in H.internal_organs)
// Greys get to keep their implant
if(isgrey(H) && istype(I, /obj/item/organ/internal/cyberimp/brain/speech_translator))
continue
// Try removing it
I = I.remove(H)
if(I)
stuff_to_transfer += I
// Regular items get removed in second
for(var/obj/item/I in M)
// Any items we don't want to take from them?
if(istype(H))
// Keep their uniform and shoes
if(I == H.w_uniform || I == H.shoes)
continue
// Plasmamen are no use if they're crispy
if(isplasmaman(H) && I == H.head)
continue
// Any kind of non-syndie implant gets potentially removed (mindshield, etc)
if(istype(I, /obj/item/implant))
if(istype(I, /obj/item/implant/storage)) // Storage stays, but items within get confiscated
var/obj/item/implant/storage/storage_implant = I
for(var/it in storage_implant.storage)
storage_implant.storage.remove_from_storage(it)
stuff_to_transfer += it
continue
else if(istype(I, /obj/item/implant/uplink)) // Uplink stays, but is jammed while in jail
var/obj/item/implant/uplink/uplink_implant = I
uplink_implant.hidden_uplink.is_jammed = TRUE
continue
else if(is_type_in_typecache(I, implants_to_keep))
continue
qdel(I)
continue
if(M.unEquip(I))
stuff_to_transfer += I
// Transfer it all (or drop it if not possible)
for(var/i in stuff_to_transfer)
var/obj/item/I = i
if(GLOB.prisoner_belongings.give_item(I))
victim_belongings += I
else if(!((ABSTRACT|NODROP) in I.flags)) // Anything that can't be put on hold, just drop it on the ground
I.forceMove(T)
// Give some species the necessary to survive. Courtesy of the Syndicate.
if(istype(H))
var/obj/item/tank/emergency_oxygen/tank
var/obj/item/clothing/mask/breath/mask
if(isvox(H))
tank = new /obj/item/tank/emergency_oxygen/nitrogen(H)
mask = new /obj/item/clothing/mask/breath/vox(H)
else if(isplasmaman(H))
tank = new /obj/item/tank/emergency_oxygen/plasma(H)
mask = new /obj/item/clothing/mask/breath(H)
if(tank)
H.equip_to_appropriate_slot(tank)
H.equip_to_appropriate_slot(mask)
tank.toggle_internals(H, TRUE)
M.update_icons()
// Supply them with some chow. How generous is the Syndicate?
var/obj/item/reagent_containers/food/snacks/breadslice/food = new(get_turf(M))
food.name = "stale bread"
food.desc = "Looks like your captors care for their prisoners as much as their bread."
food.trash = null
food.reagents.add_reagent("nutriment", 5) // It may be stale, but it still has to be nutritive enough for the whole duration!
if(prob(10))
// Mold adds a bit of spice to it
food.name = "moldy bread"
food.reagents.add_reagent("fungus", 1)
var/obj/item/reagent_containers/food/drinks/drinkingglass/drink = new(get_turf(M))
drink.reagents.add_reagent("tea", 25) // British coders beware, tea in glasses
temp_objs = list(food, drink)
// Narrate their kidnapping and torturing experience.
if(M.stat != DEAD)
// Heal them up - gets them out of crit/soft crit.
M.reagents.add_reagent("omnizine", 20)
to_chat(M, "<span class='warning'>You feel strange...</span>")
M.Paralyse(30 SECONDS_TO_LIFE_CYCLES)
M.EyeBlind(35 SECONDS_TO_LIFE_CYCLES)
M.EyeBlurry(35 SECONDS_TO_LIFE_CYCLES)
M.AdjustConfused(35 SECONDS_TO_LIFE_CYCLES)
sleep(6 SECONDS)
to_chat(M, "<span class='warning'>That portal did something to you...</span>")
sleep(6.5 SECONDS)
to_chat(M, "<span class='warning'>Your head pounds... It feels like it's going to burst out your skull!</span>")
sleep(3 SECONDS)
to_chat(M, "<span class='warning'>Your head pounds...</span>")
sleep(10 SECONDS)
to_chat(M, "<span class='specialnotice'>A million voices echo in your head... <i>\"Your mind held many valuable secrets - \
we thank you for providing them. Your value is expended, and you will be ransomed back to your station. We always get paid, \
so it's only a matter of time before we send you back...\"</i></span>")
/**
* Handles the target's return to station.
*
* Arguments:
* * M - The target mob.
*/
/datum/syndicate_contract/proc/handle_target_return(mob/living/M)
var/list/turf/possible_turfs = list()
for(var/turf/T in contract.extraction_zone.contents)
if(!isspaceturf(T) && !isunsimulatedturf(T) && !is_blocked_turf(T))
possible_turfs += T
var/turf/destination = length(possible_turfs) ? pick(possible_turfs) : pick(GLOB.latejoin)
// Make a closet to return the target and their items neatly
var/obj/structure/closet/closet = new
closet.forceMove(destination)
// Return their items
for(var/i in victim_belongings)
var/obj/item/I = GLOB.prisoner_belongings.remove_item(i)
if(!I)
continue
I.forceMove(closet)
victim_belongings = list()
// Clean up
var/obj/item/implant/uplink/uplink_implant = locate() in M
uplink_implant?.hidden_uplink?.is_jammed = FALSE
QDEL_LIST(temp_objs)
// Chance for souvenir or bruises
if(prob(RETURN_SOUVENIR_CHANCE))
to_chat(M, "<span class='notice'>Your captors left you a souvenir for your troubles!</span>")
var/obj/item/souvenir = pick(souvenirs)
new souvenir(closet)
else if(prob(RETURN_BRUISE_CHANCE) && M.health >= 50)
to_chat(M, "<span class='warning'>You were roughed up a little by your captors before being sent back!</span>")
M.adjustBruteLoss(RETURN_BRUISE_DAMAGE)
// Return them a bit confused.
M.visible_message("<span class='notice'>[M] vanishes...</span>")
M.forceMove(closet)
M.Paralyse(3 SECONDS_TO_LIFE_CYCLES)
M.EyeBlurry(5 SECONDS_TO_LIFE_CYCLES)
M.AdjustConfused(5 SECONDS_TO_LIFE_CYCLES)
M.Dizzy(35)
do_sparks(4, FALSE, destination)
// Newscaster story
var/datum/data/record/R = find_record("name", contract.target.name, GLOB.data_core.general)
var/initials = ""
for(var/s in splittext(R?.fields["name"] || M.real_name || DEFAULT_NAME, " "))
initials = initials + "[s[1]]."
var/datum/feed_message/FM = new
FM.author = "Nyx Daily"
FM.admin_locked = TRUE
FM.body = "Suspected Syndicate activity was reported in the system. Rumours have surfaced about a [R?.fields["rank"] || M?.mind.assigned_role || DEFAULT_RANK] aboard the [GLOB.using_map.full_name] being the victim of a kidnapping.\n\n" +\
"A reliable source said the following: There was a note with the victim's initials which were \"[initials]\" and a scribble saying \"[fluff_message]\""
GLOB.news_network.get_channel_by_name("Nyx Daily")?.add_message(FM)
// Bonus story if the contractor has done all their contracts (appears only once per round)
if(!nt_am_board_resigned && (owning_hub.completed_contracts >= owning_hub.num_contracts))
nt_am_board_resigned = TRUE
var/datum/feed_message/FM2 = new
FM2.author = "Nyx Daily"
FM2.admin_locked = TRUE
FM2.body = "Nanotrasen's Asset Management board has resigned today after a series of kidnappings aboard the [GLOB.using_map.full_name]." +\
"One former member of the board was heard saying: \"I can't do this anymore. How does a single shift on this cursed station manage to cost us over ten million Credits in ransom payments? Is there no security aboard?!\""
GLOB.news_network.get_channel_by_name("Nyx Daily")?.add_message(FM2)
for(var/nc in GLOB.allNewscasters)
var/obj/machinery/newscaster/NC = nc
NC.alert_news("Nyx Daily")
prisoner_timer_handle = null
GLOB.prisoner_belongings.prisoners[M] = null
/**
* Called when the extraction window closes.
*/
/datum/syndicate_contract/proc/deadline_reached()
clean_up()
owning_hub.contractor_uplink?.message_holder("The window for extraction has closed and so did the portal, agent. You will need to call for another extraction so we can open a new portal.")
SStgui.update_uis(owning_hub)
/**
* Cleans up the contract.
*/
/datum/syndicate_contract/proc/clean_up()
QDEL_NULL(extraction_flare)
QDEL_NULL(extraction_portal)
deltimer(extraction_timer_handle)
extraction_deadline = -1
extraction_timer_handle = null
#undef DEFAULT_NAME
#undef DEFAULT_RANK
#undef EXTRACTION_PHASE_PREPARE
#undef EXTRACTION_PHASE_PORTAL
#undef COMPLETION_NOTIFY_DELAY
#undef RETURN_BRUISE_CHANCE
#undef RETURN_BRUISE_DAMAGE
#undef RETURN_SOUVENIR_CHANCE
@@ -0,0 +1,27 @@
/obj/item/melee/classic_baton/telescopic/contractor
name = "contractor baton"
desc = "A compact, specialised baton issued to Syndicate contractors. Applies light electrical shocks to targets."
// Overrides
affect_silicon = TRUE
stun_time = 1
cooldown = 2.5 SECONDS
force_off = 5
force_on = 15
item_state_on = "contractor_baton"
icon_state_off = "contractor_baton_0"
icon_state_on = "contractor_baton_1"
stun_sound = 'sound/weapons/contractorbatonhit.ogg'
extend_sound = 'sound/weapons/contractorbatonextend.ogg'
// Settings
/// Stamina damage to deal on stun.
var/stamina_damage = 70
/// Jitter to deal on stun.
var/jitter_amount = 5 SECONDS_TO_JITTER
/// Stutter to deal on stun.
var/stutter_amount = 10 SECONDS_TO_LIFE_CYCLES
/obj/item/melee/classic_baton/telescopic/contractor/stun(mob/living/target, mob/living/user)
. = ..()
target.adjustStaminaLoss(stamina_damage)
target.Jitter(jitter_amount)
target.AdjustStuttering(stutter_amount)
@@ -0,0 +1,98 @@
/obj/item/storage/box/syndie_kit/contractor
name = "contractor kit"
desc = "A box containing supplies destined to Syndicate contractors."
// Settings
/// Amount of random items to be added to the contractor kit.
/// See [/obj/item/storage/box/syndie_kit/contractor/var/item_list] for the available items.
var/num_additional_items = 3
/// Items that may be part of the random items given to a contractor as part of their kit.
/// Ideally all about 5 TC or less and fit the theme. Some of these are nukeops only.
/// One item may show up only once.
var/list/item_list = list(
// Offensive
/obj/item/gun/projectile/automatic/c20r/toy,
/obj/item/storage/box/syndie_kit/throwing_weapons,
/obj/item/pen/edagger,
/obj/item/gun/projectile/automatic/toy/pistol/riot,
/obj/item/soap/syndie,
/obj/item/storage/box/syndie_kit/dart_gun,
/obj/item/gun/syringe/rapidsyringe,
/obj/item/storage/backpack/duffel/syndie/x4,
// Mixed
/obj/item/storage/box/syndie_kit/emp,
/obj/item/flashlight/emp,
// Support
/obj/item/storage/box/syndidonkpockets,
/obj/item/storage/belt/military/traitor,
/obj/item/clothing/shoes/chameleon/noslip,
/obj/item/storage/toolbox/syndicate,
/obj/item/storage/backpack/duffel/syndie/surgery,
/obj/item/multitool/ai_detect,
/obj/item/encryptionkey/binary,
/obj/item/jammer,
/obj/item/implanter/freedom,
)
/obj/item/storage/box/syndie_kit/contractor/New()
..()
new /obj/item/paper/contractor_guide(src)
new /obj/item/contractor_uplink(src)
new /obj/item/storage/box/syndie_kit/contractor_loadout(src)
// Add the random items
for(var/i in 1 to num_additional_items)
var/obj/item/I = pick_n_take(item_list)
new I(src)
/obj/item/storage/box/syndie_kit/contractor_loadout
name = "contractor standard loadout box"
desc = "A standard issue box included in a contractor kit."
/obj/item/storage/box/syndie_kit/contractor_loadout/New()
..()
new /obj/item/clothing/head/helmet/space/syndicate/contractor(src)
new /obj/item/clothing/suit/space/syndicate/contractor(src)
new /obj/item/melee/classic_baton/telescopic/contractor(src)
new /obj/item/clothing/under/chameleon(src)
new /obj/item/clothing/mask/chameleon(src)
new /obj/item/card/id/syndicate(src)
new /obj/item/storage/fancy/cigarettes/cigpack_syndicate(src)
new /obj/item/lighter/zippo(src)
/obj/item/paper/contractor_guide
name = "contractor guide"
/obj/item/paper/contractor_guide/Initialize()
info = {"<p>Welcome agent, congratulations on your new position as a Syndicate contractor. On top of your already assigned objectives,
this kit will provide you contracts to take on for telecrystal payments.</p>
<p>Provided within is your specialist contractor space suit. It's even more compact, being able to fit into a pocket, and faster than the
Syndicate space suit available to you on your hidden uplink. We also provide you a chameleon jumpsuit and mask, both of which can be changed
to any form you need for the moment. The cigarettes are a special blend - they will heal your injuries slowly over time.</p>
<p>Three additional items have been randomly selected from what we had available and included in this kit. We hope they're useful to you for your mission.</p>
<p>The Contractor Hub, available in your contractor uplink, can provide you unique items and abilities. These are bought using Contractor Rep,
with two Rep being provided each time you complete a contract.</p>
<h3>Using the Contractor Uplink</h3>
<ol>
<li>Take the contractor uplink from this kit and activate it.</li>
<li>From there, you can accept a contract, and redeem your TC payments from completed contracts.</li>
<li>The payment number shown in brackets is the bonus you'll receive when bringing your target <b>alive</b>. You receive the
other number regardless of whether they were alive or not.</li>
<li>Contracts are completed by bringing the target to the designated extraction zone, calling for extraction, and putting them
inside the extraction portal.</li>
</ol>
<p>Be careful when accepting a contract. While you'll be able to see its extraction zone beforehand, cancelling will make it
unavailable to take on again.</p>
<h3>Extracting</h3>
<ol>
<li>Make sure both yourself and your target are at the extraction zone.</li>
<li>Call the extraction, and stand back from the drop point.</li>
<li>If it fails, make sure your target is inside, and there's a free space for the extraction portal to appear.</li>
<li>Grab your target, and drag them into the extraction portal.</li>
</ol>
<h3>Ransoms</h3>
<p>We need your target for our own reasons, but we ransom them back to your mission area once their use is served. They will return back
from where you sent them off from in several minutes time. Don't worry, we give you a cut of what we get paid. We pay this into whatever
ID card you have equipped, on top of the TC payment we give.</p>
<p>Good luck agent. You can burn this document with the supplied lighter.</p>"}
return ..()
@@ -0,0 +1,48 @@
/obj/item/pinpointer/crew/contractor
name = "contractor pinpointer"
desc = "A handheld tracking device that points to crew without needing suit sensors at the cost of accuracy."
icon_state = "pinoff_contractor"
icon_off = "pinoff_contractor"
icon_null = "pinonnull_contractor"
icon_direct = "pinondirect_contractor"
icon_close = "pinonclose_contractor"
icon_medium = "pinonmedium_contractor"
icon_far = "pinonfar_contractor"
/// The minimum range for the pinpointer to function properly.
var/min_range = 20
/// The first person to have used the item. If this is set already, no one else can use it.
var/mob/owner = null
/obj/item/pinpointer/crew/contractor/point_at(atom/target)
if(target && trackable(target))
// Calc dir
var/turf/T = get_turf(target)
var/turf/L = get_turf(src)
dir = get_dir(L, T)
// Calc dist
var/dist = get_dist(L, T)
if(ISINRANGE(dist, -1, min_range))
icon_state = icon_direct
else if(ISINRANGE(dist, min_range + 1, min_range + 8))
icon_state = icon_close
else if(ISINRANGE(dist, min_range + 9, min_range + 16))
icon_state = icon_medium
else if(ISINRANGE(dist, min_range + 16, INFINITY))
icon_state = icon_far
else
icon_state = icon_null
/obj/item/pinpointer/crew/contractor/trackable(mob/living/carbon/human/H)
var/turf/here = get_turf(src)
var/turf/there = get_turf(H)
return here && there && there.z == here.z
/obj/item/pinpointer/crew/contractor/attack_self(mob/living/user)
if(owner)
if(owner != user)
to_chat(user, "<span class='warning'>[src] refuses to do anything.</span>")
return
else
owner = user
to_chat(user, "<span class='notice'>[src] now recognizes you as its sole user.</span>")
return ..()
@@ -0,0 +1,42 @@
/**
* # Contractor Uplink
*
* A contractor's point of contact with their Contractor Hub.
*/
/obj/item/contractor_uplink
name = "contractor uplink"
desc = "A standard, Syndicate issued tablet for handling important contracts while on the field."
icon = 'icons/obj/device.dmi'
icon_state = "contractor_uplink"
w_class = WEIGHT_CLASS_SMALL
slot_flags = SLOT_BELT
origin_tech = "programming=5;syndicate=4" // Hackerman encryption
/// The Contractor Hub associated with this uplink.
var/datum/contractor_hub/hub = null
/obj/item/contractor_uplink/Destroy()
// Right now, one uplink = one hub so this is fine.
QDEL_NULL(hub)
return ..()
/obj/item/contractor_uplink/attack_self(mob/user)
hub.ui_interact(user)
/**
* Sends a message to the mob holding this item.
*
* Arguments:
* * text - The text to send.
* * sndfile - The sound to play to the holder only.
*/
/obj/item/contractor_uplink/proc/message_holder(text, sndfile)
var/mob/living/M = loc
while(!istype(M) && M?.loc)
M = M.loc
if(!istype(M))
return
to_chat(M, "<span class='notice'>[bicon(src)] Incoming encrypted transmission from your handlers. Message as follows:</span><br />"\
+ "<span class='boldnotice'>[text]</span>")
if(sndfile)
M.playsound_local(get_turf(M), sndfile, 30, FALSE)
@@ -0,0 +1,120 @@
/**
* # Contractor Extraction Flare
*
* Used to designate where the [/obj/effect/portal/redspace/contractor] should spawn during the extraction process.
*/
/obj/effect/contractor_flare
name = "contractor extraction flare"
icon = 'icons/obj/lighting.dmi'
icon_state = "flare-contractor-on"
/obj/effect/contractor_flare/New()
..()
playsound(loc, 'sound/goonstation/misc/matchstick_light.ogg', 50, TRUE)
set_light(8, l_color = "#FFD165")
/obj/effect/contractor_flare/Destroy()
new /obj/effect/decal/cleanable/ash(loc)
return ..()
/**
* # Contractor Extraction Portal
*
* Used to extract contract targets and send them to the Syndicate jail for a few minutes.
*/
/obj/effect/portal/redspace/contractor
name = "suspicious portal"
icon_state = "portal-syndicate"
/// The contract associated with this portal.
var/datum/syndicate_contract/contract = null
/// The mind of the contractor. Used to tell them they shouldn't be taking the portal.
var/datum/mind/contractor_mind = null
/// The mind of the kidnapping target. Prevents non-targets from taking the portal.
var/datum/mind/target_mind = null
/obj/effect/portal/redspace/contractor/can_teleport(atom/movable/A)
var/mob/living/M = A
if(!istype(M))
return FALSE
if(M == usr && M.mind == contractor_mind)
to_chat(M, "<span class='warning'>The portal is here to extract the contract target, not you!</span>")
return FALSE
if(M.mind != target_mind)
if(usr?.mind == contractor_mind) // Contractor shoving a non-target into the portal
to_chat(M, "<span class='warning'>Somehow you are not sure [M] is the target you have to kidnap.</span>")
return FALSE
else if(usr == M) // Non-target trying to enter the portal
to_chat(M, "<span class='warning'>Somehow you are not sure this is a good idea.</span>")
return FALSE
return FALSE
return ..()
/obj/effect/portal/redspace/contractor/teleport(atom/movable/M)
. = ..()
if(.)
contract.target_received(M, src)
/**
* # Prisoner Belongings Closet
*
* Cannot be opened. Contains the belongings of all kidnapped targets.
* Any item added inside stops processing and starts again when removed.
*/
/obj/structure/closet/secure_closet/contractor
anchored = TRUE
can_be_emaged = FALSE
max_integrity = INFINITY
/// Lazy list of atoms which should process again when taken out.
var/list/atom/suspended_items = null
/// Lazy, associative list of prisoners being held as part of a contract.
/// Structure: [/mob/living] => [/datum/syndicate_contract]
var/list/prisoners = null
/obj/structure/closet/secure_closet/contractor/New()
..()
if(!GLOB.prisoner_belongings)
GLOB.prisoner_belongings = src
/obj/structure/closet/secure_closet/contractor/allowed(mob/M)
return FALSE
/**
* Tries to add an atom for temporary holding, suspending its processing.
*
* Arguments:
* * A - The atom to add.
*/
/obj/structure/closet/secure_closet/contractor/proc/give_item(atom/A)
if(ismob(A)) // No mobs allowed
return FALSE
var/obj/item/I = A
if(!istype(I))
return FALSE
if(I.isprocessing)
LAZYSET(suspended_items, I.UID(), list(I, (I in SSfastprocess.processing)))
STOP_PROCESSING(SSobj, I)
I.loc = src // No forceMove because we don't want to trigger anything here
return TRUE
/**
* Removes an atom from temporary holding.
*
* Arguments:
* * A - The atom to remove.
*/
/obj/structure/closet/secure_closet/contractor/proc/remove_item(atom/A)
if(!(A in contents))
return
var/obj/item/I = A
if(!istype(I))
return FALSE
// Resume processing if it was paused
var/list/tuple = LAZYACCESS(suspended_items, I.UID())
if(tuple)
if(tuple[2])
START_PROCESSING(SSfastprocess, I)
else
START_PROCESSING(SSobj, I)
suspended_items[I.UID()] = null
I.loc = loc // No forceMove because we don't want to trigger anything here
return I
@@ -12,7 +12,8 @@
var/should_equip = TRUE
var/traitor_kind = TRAITOR_HUMAN
var/list/assigned_targets = list() // This includes assassinate as well as steal objectives. prevents duplicate objectives
/// Whether the traitor can specialize into a contractor.
var/is_contractor = FALSE
/datum/antagonist/traitor/on_gain()
if(owner.current && isAI(owner.current))
@@ -410,3 +411,8 @@
<b>The code responses were:</b> <span class='redtext'>[responses]</span><br>"
return message
/datum/antagonist/traitor/specialization(datum/mind/new_owner)
if(isAI(new_owner?.current) || !is_contractor)
return ..()
return new /datum/antagonist/traitor/contractor