[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
@@ -20,6 +20,9 @@
|
||||
#define DROWSY "drowsy"
|
||||
#define JITTER "jitter"
|
||||
|
||||
/// Jitter decays at a rate of 3 per life cycle, 15 if resting.
|
||||
#define SECONDS_TO_JITTER SECONDS_TO_LIFE_CYCLES*3
|
||||
|
||||
//I hate adding defines like this but I'd much rather deal with bitflags than lists and string searches
|
||||
#define BRUTELOSS 1
|
||||
#define FIRELOSS 2
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Contract Statuses
|
||||
/// The contract is invalid for some reason and cannot be taken. It may be made valid later.
|
||||
#define CONTRACT_STATUS_INVALID -1
|
||||
/// The contract hasn't been started yet.
|
||||
#define CONTRACT_STATUS_INACTIVE 0
|
||||
/// The contract is in progress.
|
||||
#define CONTRACT_STATUS_ACTIVE 1
|
||||
/// The contract has been completed successfully.
|
||||
#define CONTRACT_STATUS_COMPLETED 2
|
||||
/// The contract failed for some reason.
|
||||
#define CONTRACT_STATUS_FAILED 3
|
||||
|
||||
// Difficulties. Note that they follow each other numerically and should stay that way as some code relies on that.
|
||||
/// Easy difficulty area to extract the kidnapee. Low rewards.
|
||||
#define EXTRACTION_DIFFICULTY_EASY 1
|
||||
/// Medium difficulty area to extract the kidnapee. Moderate rewards.
|
||||
#define EXTRACTION_DIFFICULTY_MEDIUM 2
|
||||
/// Hard difficulty area to extract the kidnapee. High rewards.
|
||||
#define EXTRACTION_DIFFICULTY_HARD 3
|
||||
|
||||
/// The name of the strings file containing data to use for contract fluff texts.
|
||||
#define CONTRACT_STRINGS_WANTED "syndicate_wanted_messages.json"
|
||||
|
||||
// UI page numbers.
|
||||
#define HUB_PAGE_CONTRACTS 1
|
||||
#define HUB_PAGE_SHOP 2
|
||||
|
||||
GLOBAL_DATUM(prisoner_belongings, /obj/structure/closet/secure_closet/contractor)
|
||||
@@ -7,6 +7,7 @@
|
||||
#define TARGET_INVALID_GOLEM 6
|
||||
#define TARGET_INVALID_EVENT 7
|
||||
#define TARGET_INVALID_IS_TARGET 8
|
||||
#define TARGET_INVALID_BLACKLISTED 9
|
||||
|
||||
//gamemode istype helpers
|
||||
#define GAMEMODE_IS_BLOB (SSticker && istype(SSticker.mode, /datum/game_mode/blob))
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#define TICKS *world.tick_lag
|
||||
|
||||
#define SECONDS_TO_LIFE_CYCLES /2
|
||||
|
||||
#define DS2TICKS(DS) ((DS)/world.tick_lag)
|
||||
|
||||
#define TICKS2DS(T) ((T) TICKS)
|
||||
|
||||
@@ -28,6 +28,7 @@ GLOBAL_LIST_EMPTY(latejoin_gateway)
|
||||
GLOBAL_LIST_EMPTY(latejoin_cryo)
|
||||
GLOBAL_LIST_EMPTY(latejoin_cyborg)
|
||||
GLOBAL_LIST_EMPTY(prisonwarp) //prisoners go to these
|
||||
GLOBAL_LIST_EMPTY(syndieprisonwarp) //contractor targets go to these
|
||||
GLOBAL_LIST_EMPTY(xeno_spawn)//Aliens spawn at these.
|
||||
GLOBAL_LIST_EMPTY(ertdirector)
|
||||
GLOBAL_LIST_EMPTY(emergencyresponseteamspawn)
|
||||
|
||||
@@ -346,6 +346,57 @@
|
||||
. += "<a href='?src=[UID()];traitor=traitor'>traitor</a>|<b>NO</b>"
|
||||
|
||||
. += _memory_edit_role_enabled(ROLE_TRAITOR)
|
||||
// Contractor
|
||||
. += "<br><b><i>contractor</i></b>: "
|
||||
var/datum/antagonist/traitor/contractor/C = has_antag_datum(/datum/antagonist/traitor/contractor)
|
||||
if(C)
|
||||
var/status
|
||||
if(C.contractor_uplink) // Offer accepted
|
||||
status = "<b><font color='red'>CONTRACTOR</font></b>"
|
||||
else if(world.time >= C.offer_deadline)
|
||||
status = "<b><font color='darkorange'>CONTRACTOR (EXPIRED)</font></b>"
|
||||
else
|
||||
status = "<b><font color='orange'>CONTRACTOR (PENDING)</font></b>"
|
||||
. += "[status]|<a href='?src=[UID()];contractor=clear'>no</a>"
|
||||
// List all their contracts
|
||||
if(C.contractor_uplink)
|
||||
. += "<br><b>Contracts:</b>"
|
||||
if(C.contractor_uplink.hub.contracts)
|
||||
var/count = 1
|
||||
for(var/co in C.contractor_uplink.hub.contracts)
|
||||
var/datum/syndicate_contract/CO = co
|
||||
. += "<br><B>Contract #[count++]</B>: "
|
||||
. += "<a href='?src=[UID()];cuid=[CO.UID()];contractor=target'><b>[CO.contract.target?.name || "Invalid target!"]</b></a>|"
|
||||
. += "<a href='?src=[UID()];cuid=[CO.UID()];contractor=locations'>locations</a>|"
|
||||
. += "<a href='?src=[UID()];cuid=[CO.UID()];contractor=other'>more</a>|"
|
||||
switch(CO.status)
|
||||
if(CONTRACT_STATUS_INVALID)
|
||||
. += "<b>INVALID</b>"
|
||||
if(CONTRACT_STATUS_INACTIVE)
|
||||
. += "inactive"
|
||||
if(CONTRACT_STATUS_ACTIVE)
|
||||
. += "<b><font color='orange'>ACTIVE</font></b>|"
|
||||
. += "<a href='?src=[UID()];cuid=[CO.UID()];contractor=interrupt'>interrupt</a>|"
|
||||
. += "<a href='?src=[UID()];cuid=[CO.UID()];contractor=fail'>fail</a>"
|
||||
if(CONTRACT_STATUS_COMPLETED)
|
||||
. += "<font color='green'>COMPLETED</font>"
|
||||
if(CONTRACT_STATUS_FAILED)
|
||||
. += "<font color='red'>FAILED</font>"
|
||||
. += "<br>"
|
||||
. += "<a href='?src=[UID()];contractor=add'>Add Contract</a><br>"
|
||||
. += "Claimable TC: <a href='?src=[UID()];contractor=tc'>[C.contractor_uplink.hub.reward_tc_available]</a><br>"
|
||||
. += "Available Rep: <a href='?src=[UID()];contractor=rep'>[C.contractor_uplink.hub.rep]</a><br>"
|
||||
else
|
||||
. += "<br>"
|
||||
. += "<i>Has not logged in to contractor uplink</i>"
|
||||
else
|
||||
if(has_antag_datum(/datum/antagonist/traitor))
|
||||
if(find_syndicate_uplink())
|
||||
. += "<a href='?src=[UID()];contractor=contractor'>contractor</a>|<b>NO</b>"
|
||||
else
|
||||
. += "contractor|<b>NO</b>|No Uplink"
|
||||
else
|
||||
. += "contractor|<b>NO</b>"
|
||||
// Mindslave
|
||||
. += "<br><b><i>mindslaved</i></b>: "
|
||||
if(has_antag_datum(/datum/antagonist/mindslave))
|
||||
@@ -1167,6 +1218,236 @@
|
||||
log_admin("[key_name(usr)] has automatically forged objectives for [key_name(current)]")
|
||||
message_admins("[key_name_admin(usr)] has automatically forged objectives for [key_name_admin(current)]")
|
||||
|
||||
else if(href_list["contractor"])
|
||||
var/datum/antagonist/traitor/contractor/C = has_antag_datum(/datum/antagonist/traitor/contractor)
|
||||
var/datum/contractor_hub/H = C && C.contractor_uplink?.hub
|
||||
switch(href_list["contractor"])
|
||||
if("clear")
|
||||
if(!C)
|
||||
return
|
||||
var/memory = C.antag_memory // Need to preserve the codewords and such
|
||||
// Clean up contractor stuff
|
||||
var/obj/item/uplink/hidden/U = find_syndicate_uplink()
|
||||
U?.contractor = null
|
||||
C.silent = TRUE
|
||||
remove_antag_datum(/datum/antagonist/traitor/contractor)
|
||||
// Traitor them again
|
||||
if(!has_antag_datum(/datum/antagonist/traitor, FALSE))
|
||||
var/datum/antagonist/traitor/T = new()
|
||||
T.give_objectives = FALSE
|
||||
T.should_equip = FALSE
|
||||
T.silent = TRUE
|
||||
T.antag_memory += memory
|
||||
add_antag_datum(T)
|
||||
// Notify
|
||||
to_chat(current, "<span class='warning'><font size=3><b>You are no longer a Contractor!</b></font></span>")
|
||||
log_admin("[key_name(usr)] has de-contractored [key_name(current)]")
|
||||
message_admins("[key_name_admin(usr)] has de-contractored [key_name_admin(current)]")
|
||||
|
||||
if("contractor")
|
||||
if(has_antag_datum(/datum/antagonist/traitor/contractor))
|
||||
return
|
||||
var/datum/antagonist/traitor/T = has_antag_datum(/datum/antagonist/traitor)
|
||||
var/memory = T?.antag_memory // Need to preserve the codewords and such
|
||||
// Replace the traitor datum by a contractor one
|
||||
remove_antag_datum(/datum/antagonist/traitor)
|
||||
C = new()
|
||||
C.give_objectives = FALSE
|
||||
C.should_equip = FALSE
|
||||
C.silent = TRUE
|
||||
C.antag_memory += memory
|
||||
add_antag_datum(C)
|
||||
// Notify
|
||||
log_admin("[key_name(usr)] has contractored [key_name(current)]")
|
||||
message_admins("[key_name_admin(usr)] has contractored [key_name_admin(current)]")
|
||||
|
||||
if("add")
|
||||
if(!C)
|
||||
return
|
||||
var/list/possible_targets = list()
|
||||
for(var/foo in SSticker.minds)
|
||||
var/datum/mind/possible_target = foo
|
||||
if(src == possible_target || !possible_target.current || !possible_target.key)
|
||||
continue
|
||||
possible_targets[possible_target.name] = possible_target
|
||||
|
||||
var/choice = input(usr, "Select the contract target:", "Add Contract") as null|anything in possible_targets
|
||||
var/datum/mind/target = possible_targets[choice]
|
||||
if(!target || !target.current || !target.key)
|
||||
return
|
||||
var/datum/syndicate_contract/new_contract = new(H, src, list(), target)
|
||||
new_contract.reward_tc = list(0, 0, 0)
|
||||
H.contracts += new_contract
|
||||
SStgui.update_uis(C.contractor_uplink.hub)
|
||||
log_admin("[key_name(usr)] has given a new contract to [key_name(current)] with [target.current] as the target")
|
||||
message_admins("[key_name_admin(usr)] has given a new contract to [key_name_admin(current)] with [target.current] as the target")
|
||||
|
||||
if("tc")
|
||||
if(!C)
|
||||
return
|
||||
var/new_tc = input(usr, "Enter the new amount of TC:", "Set Claimable TC", H.reward_tc_available) as num|null
|
||||
new_tc = text2num(new_tc)
|
||||
if(isnull(new_tc) || new_tc < 0)
|
||||
return
|
||||
H.reward_tc_available = new_tc
|
||||
SStgui.update_uis(C.contractor_uplink.hub)
|
||||
log_admin("[key_name(usr)] has set [key_name(current)]'s claimable TC to [new_tc]")
|
||||
message_admins("[key_name_admin(usr)] has set [key_name_admin(current)]'s claimable TC to [new_tc]")
|
||||
|
||||
if("rep")
|
||||
if(!C)
|
||||
return
|
||||
var/new_rep = input(usr, "Enter the new amount of Rep:", "Set Available Rep", H.rep) as num|null
|
||||
new_rep = text2num(new_rep)
|
||||
if(isnull(new_rep) || new_rep < 0)
|
||||
return
|
||||
H.rep = new_rep
|
||||
SStgui.update_uis(C.contractor_uplink.hub)
|
||||
log_admin("[key_name(usr)] has set [key_name(current)]'s contractor Rep to [new_rep]")
|
||||
message_admins("[key_name_admin(usr)] has set [key_name_admin(current)]'s contractor Rep to [new_rep]")
|
||||
|
||||
// Contract specific actions
|
||||
if("target")
|
||||
if(!C)
|
||||
return
|
||||
var/datum/syndicate_contract/CO = locateUID(href_list["cuid"])
|
||||
if(!istype(CO))
|
||||
return
|
||||
|
||||
var/list/possible_targets = list()
|
||||
for(var/foo in SSticker.minds)
|
||||
var/datum/mind/possible_target = foo
|
||||
if(src == possible_target || !possible_target.current || !possible_target.key)
|
||||
continue
|
||||
possible_targets[possible_target.name] = possible_target
|
||||
|
||||
var/choice = input(usr, "Select the new contract target:", "Set Contract Target") as null|anything in possible_targets
|
||||
var/datum/mind/target = possible_targets[choice]
|
||||
if(!target || !target.current || !target.key)
|
||||
return
|
||||
// Update
|
||||
var/datum/data/record/R = find_record("name", target.name, GLOB.data_core.general)
|
||||
var/name = R?.fields["name"] || target.name || "Unknown"
|
||||
var/rank = R?.fields["rank"] || target.assigned_role || "Unknown"
|
||||
CO.contract.target = target
|
||||
CO.target_name = "[name], the [rank]"
|
||||
if(R?.fields["photo"])
|
||||
var/icon/temp = new('icons/turf/floors.dmi', pick("floor", "wood", "darkfull", "stairs"))
|
||||
temp.Blend(R.fields["photo"], ICON_OVERLAY)
|
||||
CO.target_photo = temp
|
||||
// Notify
|
||||
SStgui.update_uis(C.contractor_uplink.hub)
|
||||
log_admin("[key_name(usr)] has set [key_name(current)]'s contract target to [target.current]")
|
||||
message_admins("[key_name_admin(usr)] has set [key_name_admin(current)]'s contract target to [target.current]")
|
||||
|
||||
if("locations")
|
||||
if(!C)
|
||||
return
|
||||
var/datum/syndicate_contract/CO = locateUID(href_list["cuid"])
|
||||
if(!istype(CO))
|
||||
return
|
||||
|
||||
var/list/difficulty_choices = list()
|
||||
for(var/diff in EXTRACTION_DIFFICULTY_EASY to EXTRACTION_DIFFICULTY_HARD)
|
||||
var/area/A = CO.contract.candidate_zones[diff]
|
||||
difficulty_choices["[A.name] ([CO.reward_tc[diff]] TC)"] = diff
|
||||
|
||||
var/choice_diff = input(usr, "Select the location to change:", "Set Contract Location") as null|anything in difficulty_choices
|
||||
var/difficulty = difficulty_choices[choice_diff]
|
||||
if(!difficulty)
|
||||
return
|
||||
|
||||
var/list/area_choices = list()
|
||||
for(var/a in return_sorted_areas())
|
||||
var/area/A = a
|
||||
if(A.outdoors || !is_station_level(A.z))
|
||||
continue
|
||||
area_choices += A
|
||||
|
||||
var/new_area = input(usr, "Select the new location:", "Set Contract Location", CO.contract.candidate_zones[difficulty]) in area_choices
|
||||
if(!new_area)
|
||||
return
|
||||
|
||||
var/new_reward = input(usr, "Enter the new amount of rewarded TC:", "Set Contract Location", CO.reward_tc[difficulty]) as num|null
|
||||
new_reward = text2num(new_reward)
|
||||
if(isnull(new_reward) || new_reward < 0)
|
||||
return
|
||||
CO.contract.candidate_zones[difficulty] = new_area
|
||||
CO.reward_tc[difficulty] = new_reward
|
||||
SStgui.update_uis(C.contractor_uplink.hub)
|
||||
log_admin("[key_name(usr)] has set [key_name(current)]'s contract location to [new_area] with [new_reward] TC as reward")
|
||||
message_admins("[key_name_admin(usr)] has set [key_name_admin(current)]'s contract location to [new_area] with [new_reward] TC as reward")
|
||||
|
||||
if("other")
|
||||
if(!C)
|
||||
return
|
||||
var/datum/syndicate_contract/CO = locateUID(href_list["cuid"])
|
||||
if(!istype(CO))
|
||||
return
|
||||
|
||||
var/choice = input(usr, "Select an action to take:", "Other Contract Actions") in list("Edit Fluff Message", "Edit Prison Time", "Edit Credits Reward", "Delete Contract", "Cancel")
|
||||
if(!choice)
|
||||
return
|
||||
|
||||
switch(choice)
|
||||
if("Edit Fluff Message")
|
||||
var/new_message = input(usr, "Enter the new fluff message:", "Edit Fluff Message", CO.fluff_message) as message|null
|
||||
if(!new_message)
|
||||
return
|
||||
CO.fluff_message = new_message
|
||||
log_admin("[key_name(usr)] has edited [key_name(current)]'s contract fluff message")
|
||||
message_admins("[key_name_admin(usr)] has edited [key_name_admin(current)]'s contract fluff message")
|
||||
if("Edit Prison Time")
|
||||
var/new_time = input(usr, "Enter the new prison time in seconds:", "Edit Prison Time", CO.prison_time / 10) as num|null
|
||||
if(!new_time || new_time < 0)
|
||||
return
|
||||
CO.prison_time = new_time SECONDS
|
||||
log_admin("[key_name(usr)] has edited [key_name(current)]'s contract prison time to [new_time] seconds")
|
||||
message_admins("[key_name_admin(usr)] has edited [key_name_admin(current)]'s contract prison time to [new_time] seconds")
|
||||
if("Edit Credits Reward")
|
||||
var/new_creds = input(usr, "Enter the new credits reward:", "Edit Credits Reward", CO.reward_credits) as num|null
|
||||
if(!new_creds || new_creds < 0)
|
||||
return
|
||||
CO.reward_credits = new_creds
|
||||
log_admin("[key_name(usr)] has edited [key_name(current)]'s contract reward credits to [new_creds]")
|
||||
message_admins("[key_name_admin(usr)] has edited [key_name_admin(current)]'s contract reward credits to [new_creds]")
|
||||
if("Delete Contract")
|
||||
if(CO.status == CONTRACT_STATUS_ACTIVE)
|
||||
CO.fail("Contract interrupted forcibly.")
|
||||
H.contracts -= CO
|
||||
log_admin("[key_name(usr)] has deleted [key_name(current)]'s contract")
|
||||
message_admins("[key_name_admin(usr)] has deleted [key_name_admin(current)]'s contract")
|
||||
else
|
||||
return
|
||||
SStgui.update_uis(C.contractor_uplink.hub)
|
||||
|
||||
if("interrupt")
|
||||
if(!C)
|
||||
return
|
||||
var/datum/syndicate_contract/CO = locateUID(href_list["cuid"])
|
||||
if(!istype(CO) || CO.status != CONTRACT_STATUS_ACTIVE)
|
||||
return
|
||||
H.current_contract = null
|
||||
CO.contract.extraction_zone = null
|
||||
CO.status = CONTRACT_STATUS_INACTIVE
|
||||
CO.clean_up()
|
||||
log_admin("[key_name(usr)] has interrupted [key_name(current)]'s contract")
|
||||
message_admins("[key_name_admin(usr)] has interrupted [key_name_admin(current)]'s contract")
|
||||
|
||||
if("fail")
|
||||
if(!C)
|
||||
return
|
||||
var/datum/syndicate_contract/CO = locateUID(href_list["cuid"])
|
||||
if(!istype(CO) || CO.status != CONTRACT_STATUS_ACTIVE)
|
||||
return
|
||||
var/fail_reason = sanitize(input(usr, "Enter the fail reason:", "Fail Contract") as text|null)
|
||||
if(!fail_reason || CO.status != CONTRACT_STATUS_ACTIVE)
|
||||
return
|
||||
CO.fail(fail_reason)
|
||||
SStgui.update_uis(C.contractor_uplink.hub)
|
||||
log_admin("[key_name(usr)] has failed [key_name(current)]'s contract with reason: [fail_reason]")
|
||||
message_admins("[key_name_admin(usr)] has failed [key_name_admin(current)]'s contract with reason: [fail_reason]")
|
||||
|
||||
else if(href_list["mindslave"])
|
||||
switch(href_list["mindslave"])
|
||||
if("clear")
|
||||
|
||||
@@ -465,6 +465,9 @@ NOTE: there are two lists of areas in the end of this file: centcom and station
|
||||
name = "\improper Syndicate Infiltrators"
|
||||
icon_state = "syndie-elite"
|
||||
|
||||
/area/syndicate_mothership/jail
|
||||
name = "\improper Syndicate Jail"
|
||||
|
||||
//EXTRA
|
||||
|
||||
/area/asteroid // -- TLE
|
||||
|
||||
@@ -54,6 +54,27 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective)
|
||||
if(possible_targets.len > 0)
|
||||
target = pick(possible_targets)
|
||||
|
||||
/**
|
||||
* Called when the objective's target goes to cryo.
|
||||
*/
|
||||
/datum/objective/proc/on_target_cryo()
|
||||
if(owner?.current)
|
||||
to_chat(owner.current, "<BR><span class='userdanger'>You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!</span>")
|
||||
SEND_SOUND(owner.current, 'sound/ambience/alarm4.ogg')
|
||||
target = null
|
||||
INVOKE_ASYNC(src, .proc/post_target_cryo)
|
||||
|
||||
/**
|
||||
* Called a tick after when the objective's target goes to cryo.
|
||||
*/
|
||||
/datum/objective/proc/post_target_cryo()
|
||||
find_target()
|
||||
if(!target)
|
||||
GLOB.all_objectives -= src
|
||||
owner?.objectives -= src
|
||||
qdel(src)
|
||||
owner?.announce_objectives()
|
||||
|
||||
/datum/objective/assassinate
|
||||
martyr_compatible = 1
|
||||
|
||||
@@ -98,6 +119,11 @@ GLOBAL_LIST_INIT(potential_theft_objectives, (subtypesof(/datum/theft_objective)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
/datum/objective/mutiny/on_target_cryo()
|
||||
// We don't want revs to get objectives that aren't for heads of staff. Letting
|
||||
// them win or lose based on cryo is silly so we remove the objective.
|
||||
qdel(src)
|
||||
|
||||
/datum/objective/maroon
|
||||
martyr_compatible = 1
|
||||
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
var/traitors_possible = 4 //hard limit on traitors if scaling is turned off
|
||||
var/const/traitor_scaling_coeff = 5.0 //how much does the amount of players get divided by to determine traitors
|
||||
var/antag_datum = /datum/antagonist/traitor //what type of antag to create
|
||||
// Contractor related
|
||||
/// Minimum number of possible contractors regardless of the number of traitors.
|
||||
var/min_contractors = 1
|
||||
/// How many contractors there are in proportion to traitors.
|
||||
/// Calculated as: num_contractors = max(min_contractors, CEILING(num_traitors * contractor_traitor_ratio, 1))
|
||||
var/contractor_traitor_ratio = 0.25
|
||||
/// List of traitors who are eligible to become a contractor.
|
||||
var/list/datum/mind/selected_contractors = list()
|
||||
|
||||
/datum/game_mode/traitor/announce()
|
||||
to_chat(world, "<B>The current game mode is - Traitor!</B>")
|
||||
@@ -44,6 +52,8 @@
|
||||
else
|
||||
num_traitors = max(1, min(num_players(), traitors_possible))
|
||||
|
||||
var/num_contractors = max(min_contractors, CEILING(num_traitors * contractor_traitor_ratio, 1))
|
||||
|
||||
for(var/j = 0, j < num_traitors, j++)
|
||||
if(!possible_traitors.len)
|
||||
break
|
||||
@@ -52,6 +62,8 @@
|
||||
traitor.special_role = SPECIAL_ROLE_TRAITOR
|
||||
traitor.restricted_roles = restricted_jobs
|
||||
possible_traitors.Remove(traitor)
|
||||
if(num_contractors-- > 0)
|
||||
selected_contractors += traitor
|
||||
|
||||
if(!pre_traitors.len)
|
||||
return 0
|
||||
@@ -61,6 +73,7 @@
|
||||
/datum/game_mode/traitor/post_setup()
|
||||
for(var/datum/mind/traitor in pre_traitors)
|
||||
var/datum/antagonist/traitor/new_antag = new antag_datum()
|
||||
new_antag.is_contractor = (traitor in selected_contractors)
|
||||
addtimer(CALLBACK(traitor, /datum/mind.proc/add_antag_datum, new_antag), rand(10,100))
|
||||
if(!exchange_blue)
|
||||
exchange_blue = -1 //Block latejoiners from getting exchange objectives
|
||||
@@ -117,6 +130,27 @@
|
||||
else
|
||||
special_role_text = "antagonist"
|
||||
|
||||
var/datum/antagonist/traitor/contractor/contractor = traitor.has_antag_datum(/datum/antagonist/traitor/contractor)
|
||||
if(contractor?.contractor_uplink)
|
||||
var/count = 1
|
||||
var/earned_tc = contractor.contractor_uplink.hub.reward_tc_paid_out
|
||||
for(var/c in contractor.contractor_uplink.hub.contracts)
|
||||
var/datum/syndicate_contract/C = c
|
||||
// Locations
|
||||
var/locations = list()
|
||||
for(var/a in C.contract.candidate_zones)
|
||||
var/area/A = a
|
||||
locations += (A == C.contract.extraction_zone ? "<b><u>[A.map_name]</u></b>" : A.map_name)
|
||||
var/display_locations = english_list(locations, and_text = " or ")
|
||||
// Result
|
||||
var/result = ""
|
||||
if(C.status == CONTRACT_STATUS_COMPLETED)
|
||||
result = "<font color='green'><B>Success!</B></font>"
|
||||
else if(C.status != CONTRACT_STATUS_INACTIVE)
|
||||
result = "<font color='red'>Fail.</font>"
|
||||
text += "<br><font color='orange'><B>Contract #[count]</B></font>: Kidnap and extract [C.target_name] at [display_locations]. [result]"
|
||||
count++
|
||||
text += "<br><font color='orange'><B>[earned_tc] TC were earned from the contracts.</B></font>"
|
||||
|
||||
if(traitorwin)
|
||||
text += "<br><font color='green'><B>The [special_role_text] was successful!</B></font><br>"
|
||||
|
||||
@@ -379,24 +379,9 @@
|
||||
|
||||
//Update any existing objectives involving this mob.
|
||||
for(var/datum/objective/O in GLOB.all_objectives)
|
||||
// We don't want revs to get objectives that aren't for heads of staff. Letting
|
||||
// them win or lose based on cryo is silly so we remove the objective.
|
||||
if(istype(O,/datum/objective/mutiny) && O.target == occupant.mind)
|
||||
qdel(O)
|
||||
else if(O.target && istype(O.target,/datum/mind))
|
||||
if(O.target == occupant.mind)
|
||||
if(O.owner && O.owner.current)
|
||||
to_chat(O.owner.current, "<BR><span class='userdanger'>You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!</span>")
|
||||
O.owner.current << 'sound/ambience/alarm4.ogg'
|
||||
O.target = null
|
||||
spawn(1) //This should ideally fire after the occupant is deleted.
|
||||
if(!O) return
|
||||
O.find_target()
|
||||
if(!(O.target))
|
||||
GLOB.all_objectives -= O
|
||||
O.owner.objectives -= O
|
||||
qdel(O)
|
||||
O.owner.announce_objectives()
|
||||
if(O.target != occupant.mind)
|
||||
continue
|
||||
O.on_target_cryo()
|
||||
if(occupant.mind && occupant.mind.assigned_role)
|
||||
//Handle job slot/tater cleanup.
|
||||
var/job = occupant.mind.assigned_role
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
poster_type = /obj/structure/sign/poster/official/random
|
||||
icon_state = "rolled_poster_legit"
|
||||
|
||||
/obj/item/poster/syndicate_recruitment
|
||||
poster_type = /obj/structure/sign/poster/contraband/syndicate_recruitment
|
||||
icon_state = "rolled_poster"
|
||||
|
||||
//############################## THE ACTUAL DECALS ###########################
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
GLOB.prisonwarp += loc
|
||||
qdel(src)
|
||||
|
||||
if("syndieprisonwarp")
|
||||
GLOB.syndieprisonwarp += loc
|
||||
qdel(src)
|
||||
|
||||
if("prisonsecuritywarp")
|
||||
GLOB.prisonsecuritywarp += loc
|
||||
qdel(src)
|
||||
|
||||
@@ -23,6 +23,10 @@ GLOBAL_LIST_EMPTY(world_uplinks)
|
||||
var/job = null
|
||||
var/temp_category
|
||||
var/uplink_type = "traitor"
|
||||
/// If set, the uplink will show the option to become a contractor through this variable.
|
||||
var/datum/antagonist/traitor/contractor/contractor = null
|
||||
/// Whether the uplink is jammed and cannot be used to order items.
|
||||
var/is_jammed = FALSE
|
||||
|
||||
/obj/item/uplink/ui_host()
|
||||
return loc
|
||||
@@ -78,6 +82,9 @@ GLOBAL_LIST_EMPTY(world_uplinks)
|
||||
return pick(random_items)
|
||||
|
||||
/obj/item/uplink/proc/buy(var/datum/uplink_item/UI, var/reference)
|
||||
if(is_jammed)
|
||||
to_chat(usr, "<span class='warning'>[src] seems to be jammed - it cannot be used here!</span>")
|
||||
return
|
||||
if(!UI)
|
||||
return
|
||||
if(UI.limited_stock == 0)
|
||||
@@ -144,7 +151,10 @@ GLOBAL_LIST_EMPTY(world_uplinks)
|
||||
// Checks to see if the value meets the target. Like a frequency being a traitor_frequency, in order to unlock a headset.
|
||||
// If true, it accesses trigger() and returns 1. If it fails, it returns false. Use this to see if you need to close the
|
||||
// current item's menu.
|
||||
/obj/item/uplink/hidden/proc/check_trigger(mob/user as mob, var/value, var/target)
|
||||
/obj/item/uplink/hidden/proc/check_trigger(mob/user, var/value, var/target)
|
||||
if(is_jammed)
|
||||
to_chat(user, "<span class='warning'>[src] seems to be jammed - it cannot be used here!</span>")
|
||||
return
|
||||
if(value == target)
|
||||
trigger(user)
|
||||
return TRUE
|
||||
@@ -160,6 +170,16 @@ GLOBAL_LIST_EMPTY(world_uplinks)
|
||||
var/list/data = list()
|
||||
|
||||
data["crystals"] = uses
|
||||
data["modal"] = ui_modal_data(src)
|
||||
|
||||
if(contractor)
|
||||
var/list/contractor_data = list(
|
||||
available = uses >= contractor.tc_cost && world.time < contractor.offer_deadline,
|
||||
affordable = uses >= contractor.tc_cost,
|
||||
accepted = !isnull(contractor.contractor_uplink),
|
||||
time_left = contractor.offer_deadline - world.time,
|
||||
)
|
||||
data["contractor"] = contractor_data
|
||||
|
||||
return data
|
||||
|
||||
@@ -198,6 +218,8 @@ GLOBAL_LIST_EMPTY(world_uplinks)
|
||||
return
|
||||
|
||||
. = TRUE
|
||||
if(tgui_act_modal(action, params))
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("lock")
|
||||
@@ -217,6 +239,28 @@ GLOBAL_LIST_EMPTY(world_uplinks)
|
||||
var/datum/uplink_item/UI = uplink_items[params["item"]]
|
||||
return buy(UI, UI ? UI.reference : "")
|
||||
|
||||
/**
|
||||
* Called in tgui_act() to process modal actions
|
||||
*
|
||||
* Arguments:
|
||||
* * action - The action passed by tgui
|
||||
* * params - The params passed by tgui
|
||||
*/
|
||||
/obj/item/uplink/hidden/proc/tgui_act_modal(action, list/params)
|
||||
. = TRUE
|
||||
var/id = params["id"]
|
||||
switch(ui_modal_act(src, action, params))
|
||||
if(UI_MODAL_OPEN)
|
||||
if(id == "become_contractor")
|
||||
ui_modal_boolean(src, id, "")
|
||||
return
|
||||
if(UI_MODAL_ANSWER)
|
||||
if(id == "become_contractor")
|
||||
if(text2num(params["answer"]))
|
||||
var/datum/antagonist/traitor/contractor/C = usr.mind.has_antag_datum(/datum/antagonist/traitor/contractor)
|
||||
C?.become_contractor(usr, src)
|
||||
return
|
||||
return FALSE
|
||||
|
||||
// I placed this here because of how relevant it is.
|
||||
// You place this in your uplinkable item to check if an uplink is active or not.
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
/// Delay in deci-seconds between two non-lethal attacks
|
||||
#define BATON_STUN_COOLDOWN 4 SECONDS
|
||||
/// Force of the telescopic baton when deployed
|
||||
#define BATON_TELESCOPIC_FORCE_DEPLOYED 10
|
||||
|
||||
/**
|
||||
* # Police Baton
|
||||
*
|
||||
@@ -18,6 +13,18 @@
|
||||
slot_flags = SLOT_BELT
|
||||
force = 12 //9 hit crit
|
||||
w_class = WEIGHT_CLASS_NORMAL
|
||||
// Settings
|
||||
/// Whether the baton can stun silicon mobs
|
||||
var/affect_silicon = FALSE
|
||||
/// The stun time (in life cycles) for non-silicons
|
||||
var/stun_time = 6 SECONDS_TO_LIFE_CYCLES
|
||||
/// The stun time (in life cycles) for silicons
|
||||
var/stun_time_silicon = 10 SECONDS_TO_LIFE_CYCLES
|
||||
/// Cooldown in deciseconds between two knockdowns
|
||||
var/cooldown = 4 SECONDS
|
||||
/// Sound to play when knocking someone down
|
||||
var/stun_sound = 'sound/effects/woodhit.ogg'
|
||||
// Variables
|
||||
/// Whether the baton is on cooldown
|
||||
var/on_cooldown = FALSE
|
||||
/// Whether the baton is toggled on (to allow attacking)
|
||||
@@ -39,27 +46,68 @@
|
||||
user.take_organ_damage(force * 2)
|
||||
return
|
||||
|
||||
if(user.a_intent == INTENT_HARM || isrobot(target)) // Lethal attack or it's a borg (can't knock them down!)
|
||||
if(user.a_intent == INTENT_HARM)
|
||||
return ..()
|
||||
else if(!on_cooldown) // Non-lethal attack - knock them down
|
||||
if(on_cooldown)
|
||||
return
|
||||
if(issilicon(target) && !affect_silicon)
|
||||
return ..()
|
||||
else
|
||||
stun(target, user)
|
||||
|
||||
/**
|
||||
* Called when a target is about to be hit non-lethally.
|
||||
*
|
||||
* Arguments:
|
||||
* * target - The mob about to be hit
|
||||
* * user - The attacking user
|
||||
*/
|
||||
/obj/item/melee/classic_baton/proc/stun(mob/living/target, mob/living/user)
|
||||
if(issilicon(target))
|
||||
user.visible_message("<span class='danger'>[user] pulses [target]'s sensors with [src]!</span>",\
|
||||
"<span class='danger'>You pulse [target]'s sensors with [src]!</span>")
|
||||
on_silicon_stun(target, user)
|
||||
else
|
||||
// Check for shield/countering
|
||||
if(ishuman(target))
|
||||
var/mob/living/carbon/human/H = target
|
||||
if(H.check_shields(src, 0, "[user]'s [name]", MELEE_ATTACK))
|
||||
return
|
||||
return FALSE
|
||||
if(check_martial_counter(H, user))
|
||||
return
|
||||
// Visuals and sound
|
||||
user.do_attack_animation(target)
|
||||
playsound(target, 'sound/effects/woodhit.ogg', 75, TRUE, -1)
|
||||
add_attack_logs(user, target, "Stunned with [src]")
|
||||
target.visible_message("<span class='danger'>[user] has knocked down [target] with \the [src]!</span>", \
|
||||
"<span class='userdanger'>[user] has knocked down [target] with \the [src]!</span>")
|
||||
// Hit 'em
|
||||
target.LAssailant = iscarbon(user) ? user : null
|
||||
target.Weaken(3)
|
||||
on_cooldown = TRUE
|
||||
addtimer(CALLBACK(src, .proc/cooldown_finished), BATON_STUN_COOLDOWN)
|
||||
return FALSE
|
||||
user.visible_message("<span class='danger'>[user] knocks down [target] with [src]!</span>",\
|
||||
"<span class='danger'>You knock down [target] with [src]!</span>")
|
||||
on_non_silicon_stun(target, user)
|
||||
// Visuals and sound
|
||||
user.do_attack_animation(target)
|
||||
playsound(target, stun_sound, 75, TRUE, -1)
|
||||
add_attack_logs(user, target, "Stunned with [src]")
|
||||
// Hit 'em
|
||||
target.LAssailant = iscarbon(user) ? user : null
|
||||
target.Weaken(stun_time)
|
||||
on_cooldown = TRUE
|
||||
addtimer(CALLBACK(src, .proc/cooldown_finished), cooldown)
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* Called when a silicon has been stunned.
|
||||
*
|
||||
* Arguments:
|
||||
* * target - The hit mob
|
||||
* * user - The attacking user
|
||||
*/
|
||||
/obj/item/melee/classic_baton/proc/on_silicon_stun(mob/living/silicon/target, mob/living/user)
|
||||
target.flash_eyes(affect_silicon = TRUE)
|
||||
|
||||
/**
|
||||
* Called when a non-silicon has been stunned.
|
||||
*
|
||||
* Arguments:
|
||||
* * target - The hit mob
|
||||
* * user - The attacking user
|
||||
*/
|
||||
/obj/item/melee/classic_baton/proc/on_non_silicon_stun(mob/living/target, mob/living/user)
|
||||
return
|
||||
|
||||
/**
|
||||
* Called some time after a non-lethal attack
|
||||
@@ -86,13 +134,23 @@
|
||||
/obj/item/melee/classic_baton/telescopic
|
||||
name = "telescopic baton"
|
||||
desc = "A compact yet robust personal defense weapon. Can be concealed when folded."
|
||||
icon_state = "telebaton_0"
|
||||
item_state = null
|
||||
slot_flags = SLOT_BELT
|
||||
w_class = WEIGHT_CLASS_SMALL
|
||||
needs_permit = FALSE
|
||||
force = 0
|
||||
on = FALSE
|
||||
/// Force when concealed
|
||||
var/force_off = 0
|
||||
/// Force when extended
|
||||
var/force_on = 10
|
||||
/// Item state when extended
|
||||
var/item_state_on = "nullrod"
|
||||
/// Icon state when concealed
|
||||
var/icon_state_off = "telebaton_0"
|
||||
/// Icon state when extended
|
||||
var/icon_state_on = "telebaton_1"
|
||||
/// Sound to play when concealing or extending
|
||||
var/extend_sound = 'sound/weapons/batonextend.ogg'
|
||||
/// Attack verbs when concealed (created on Initialize)
|
||||
var/static/list/attack_verb_off
|
||||
/// Attack verbs when extended (created on Initialize)
|
||||
@@ -103,31 +161,53 @@
|
||||
if(!attack_verb_off)
|
||||
attack_verb_off = list("hit", "poked")
|
||||
attack_verb_on = list("smacked", "struck", "cracked", "beaten")
|
||||
icon_state = icon_state_off
|
||||
force = force_off
|
||||
attack_verb = on ? attack_verb_on : attack_verb_off
|
||||
|
||||
/obj/item/melee/classic_baton/telescopic/attack_self(mob/user)
|
||||
on = !on
|
||||
icon_state = "telebaton_[on]"
|
||||
icon_state = on ? icon_state_on : icon_state_off
|
||||
if(on)
|
||||
to_chat(user, "<span class='warning'>You extend the baton.</span>")
|
||||
item_state = "nullrod"
|
||||
to_chat(user, "<span class='warning'>You extend [src].</span>")
|
||||
item_state = item_state_on
|
||||
w_class = WEIGHT_CLASS_BULKY //doesnt fit in backpack when its on for balance
|
||||
force = BATON_TELESCOPIC_FORCE_DEPLOYED //stunbaton damage
|
||||
force = force_on //stunbaton damage
|
||||
attack_verb = attack_verb_on
|
||||
else
|
||||
to_chat(user, "<span class='notice'>You collapse the baton.</span>")
|
||||
to_chat(user, "<span class='notice'>You collapse [src].</span>")
|
||||
item_state = null //no sprite for concealment even when in hand
|
||||
slot_flags = SLOT_BELT
|
||||
w_class = WEIGHT_CLASS_SMALL
|
||||
force = 0 //not so robust now
|
||||
force = force_off //not so robust now
|
||||
attack_verb = attack_verb_off
|
||||
// Update mob hand visuals
|
||||
if(ishuman(user))
|
||||
var/mob/living/carbon/human/H = user
|
||||
H.update_inv_l_hand()
|
||||
H.update_inv_r_hand()
|
||||
playsound(loc, 'sound/weapons/batonextend.ogg', 50, TRUE)
|
||||
// Update blood splatter
|
||||
if(blood_overlay)
|
||||
cut_overlay(blood_overlay)
|
||||
qdel(blood_overlay)
|
||||
add_blood_overlay(blood_overlay_color)
|
||||
playsound(loc, extend_sound, 50, TRUE)
|
||||
add_fingerprint(user)
|
||||
|
||||
#undef BATON_STUN_COOLDOWN
|
||||
#undef BATON_TELESCOPIC_FORCE_DEPLOYED
|
||||
/obj/item/melee/classic_baton/telescopic/blood_splatter_index()
|
||||
return "\ref[icon]-[icon_state]"
|
||||
|
||||
/obj/item/melee/classic_baton/telescopic/add_blood_overlay(color)
|
||||
var/index = blood_splatter_index()
|
||||
var/icon/blood_splatter_icon = GLOB.blood_splatter_icons[index]
|
||||
if(!blood_splatter_icon)
|
||||
blood_splatter_icon = icon(icon, icon_state)
|
||||
blood_splatter_icon.Blend("#ffffff", ICON_ADD)
|
||||
blood_splatter_icon.Blend(icon('icons/effects/blood.dmi', "itemblood"), ICON_MULTIPLY)
|
||||
blood_splatter_icon = fcopy_rsc(blood_splatter_icon)
|
||||
GLOB.blood_splatter_icons[index] = blood_splatter_icon
|
||||
|
||||
blood_overlay = image(blood_splatter_icon)
|
||||
blood_overlay.color = color
|
||||
blood_overlay_color = color
|
||||
add_overlay(blood_overlay)
|
||||
|
||||
@@ -270,6 +270,9 @@ GLOBAL_VAR_INIT(nologevent, 0)
|
||||
<A href='?_src_=holder;tdome2=[M.UID()]'>Thunderdome 2</A> |
|
||||
<A href='?_src_=holder;tdomeadmin=[M.UID()]'>Thunderdome Admin</A> |
|
||||
<A href='?_src_=holder;tdomeobserve=[M.UID()]'>Thunderdome Observer</A> |
|
||||
<A href='?_src_=holder;contractor_stop=[M.UID()]'>Stop Syndicate Jail Timer</A> |
|
||||
<A href='?_src_=holder;contractor_start=[M.UID()]'>Start Syndicate Jail Timer</A> |
|
||||
<A href='?_src_=holder;contractor_release=[M.UID()]'>Release now from Syndicate Jail</A> |
|
||||
"}
|
||||
|
||||
body += {"<br>
|
||||
|
||||
@@ -1477,6 +1477,77 @@
|
||||
log_admin("[key_name(usr)] has sent [key_name(M)] to the thunderdome. (Observer.)")
|
||||
message_admins("[key_name_admin(usr)] has sent [key_name_admin(M)] to the thunderdome. (Observer.)", 1)
|
||||
|
||||
else if(href_list["contractor_stop"])
|
||||
if(!check_rights(R_DEBUG|R_ADMIN))
|
||||
return
|
||||
|
||||
var/mob/M = locateUID(href_list["contractor_stop"])
|
||||
if(!istype(M))
|
||||
to_chat(usr, "<span class='warning'>This can only be used on instances of type /mob.</span>")
|
||||
return
|
||||
|
||||
var/datum/syndicate_contract/contract = LAZYACCESS(GLOB.prisoner_belongings.prisoners, M)
|
||||
if(!contract)
|
||||
to_chat(usr, "<span class='warning'>[M] is currently not imprisoned by the Syndicate.</span>")
|
||||
return
|
||||
if(!contract.prisoner_timer_handle)
|
||||
to_chat(usr, "<span class='warning'>[M] is already NOT scheduled to return from the Syndicate Jail.</span>")
|
||||
return
|
||||
|
||||
deltimer(contract.prisoner_timer_handle)
|
||||
contract.prisoner_timer_handle = null
|
||||
to_chat(usr, "Stopped automatic return of [M] from the Syndicate Jail.")
|
||||
message_admins("[key_name_admin(usr)] has stopped the automatic return of [key_name_admin(M)] from the Syndicate Jail")
|
||||
log_admin("[key_name(usr)] has stopped the automatic return of [key_name(M)] from the Syndicate Jail")
|
||||
|
||||
else if(href_list["contractor_start"])
|
||||
if(!check_rights(R_DEBUG|R_ADMIN))
|
||||
return
|
||||
|
||||
var/mob/M = locateUID(href_list["contractor_start"])
|
||||
if(!istype(M))
|
||||
to_chat(usr, "<span class='warning'>This can only be used on instances of type /mob.</span>")
|
||||
return
|
||||
|
||||
var/datum/syndicate_contract/contract = LAZYACCESS(GLOB.prisoner_belongings.prisoners, M)
|
||||
if(!contract)
|
||||
to_chat(usr, "<span class='warning'>[M] is currently not imprisoned by the Syndicate.</span>")
|
||||
return
|
||||
if(contract.prisoner_timer_handle)
|
||||
to_chat(usr, "<span class='warning'>[M] is already scheduled to return from the Syndicate Jail.</span>")
|
||||
return
|
||||
|
||||
var/time_seconds = input(usr, "Enter the jail time in seconds:", "Start Syndicate Jail Timer") as num|null
|
||||
time_seconds = text2num(time_seconds)
|
||||
if(time_seconds < 0)
|
||||
return
|
||||
|
||||
contract.prisoner_timer_handle = addtimer(CALLBACK(contract, /datum/syndicate_contract.proc/handle_target_return, M), time_seconds * 10, TIMER_STOPPABLE)
|
||||
to_chat(usr, "Started automatic return of [M] from the Syndicate Jail in [time_seconds] second\s.")
|
||||
message_admins("[key_name_admin(usr)] has started the automatic return of [key_name_admin(M)] from the Syndicate Jail in [time_seconds] second\s")
|
||||
log_admin("[key_name(usr)] has started the automatic return of [key_name(M)] from the Syndicate Jail in [time_seconds] second\s")
|
||||
|
||||
else if(href_list["contractor_release"])
|
||||
if(!check_rights(R_DEBUG|R_ADMIN))
|
||||
return
|
||||
|
||||
var/mob/M = locateUID(href_list["contractor_release"])
|
||||
if(!istype(M))
|
||||
to_chat(usr, "<span class='warning'>This can only be used on instances of type /mob.</span>")
|
||||
return
|
||||
|
||||
var/datum/syndicate_contract/contract = LAZYACCESS(GLOB.prisoner_belongings.prisoners, M)
|
||||
if(!contract)
|
||||
to_chat(usr, "<span class='warning'>[M] is currently not imprisoned by the Syndicate.</span>")
|
||||
return
|
||||
|
||||
deltimer(contract.prisoner_timer_handle)
|
||||
contract.handle_target_return(M)
|
||||
to_chat(usr, "Immediately returned [M] from the Syndicate Jail.")
|
||||
message_admins("[key_name_admin(usr)] has immediately returned [key_name_admin(M)] from the Syndicate Jail")
|
||||
log_admin("[key_name(usr)] has immediately returned [key_name(M)] from the Syndicate Jail")
|
||||
|
||||
|
||||
else if(href_list["aroomwarp"])
|
||||
if(!check_rights(R_SERVER|R_EVENT)) return
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -186,3 +186,18 @@
|
||||
name = "Black Engineering Space Suit"
|
||||
icon_state = "syndicate-black-engie"
|
||||
item_state = "syndicate-black"
|
||||
|
||||
/obj/item/clothing/head/helmet/space/syndicate/contractor
|
||||
name = "contractor helmet"
|
||||
desc = "A specialised black and gold helmet that's more compact than its standard Syndicate counterpart. Can be ultra-compressed into even the tightest of spaces."
|
||||
w_class = WEIGHT_CLASS_SMALL
|
||||
icon_state = "syndicate-helm-contractor"
|
||||
item_state = "syndicate-helm-contractor"
|
||||
|
||||
/obj/item/clothing/suit/space/syndicate/contractor
|
||||
name = "contractor space suit"
|
||||
desc = "A specialised black and gold space suit that's quicker, and more compact than its standard Syndicate counterpart. Can be ultra-compressed into even the tightest of spaces."
|
||||
slowdown = 1
|
||||
w_class = WEIGHT_CLASS_SMALL
|
||||
icon_state = "syndicate-contractor"
|
||||
item_state = "syndicate-contractor"
|
||||
|
||||
@@ -306,7 +306,7 @@
|
||||
cooldown_add = 50
|
||||
activation_sound = 'sound/magic/timeparadox2.ogg'
|
||||
var/list/banned_items_typecache = list(/obj/item/storage, /obj/item/implant, /obj/item/implanter, /obj/item/disk/nuclear,
|
||||
/obj/item/projectile, /obj/item/spellbook, /obj/item/clothing/mask/facehugger)
|
||||
/obj/item/projectile, /obj/item/spellbook, /obj/item/clothing/mask/facehugger, /obj/item/contractor_uplink)
|
||||
|
||||
/obj/machinery/anomalous_crystal/refresher/New()
|
||||
..()
|
||||
|
||||
@@ -115,10 +115,9 @@
|
||||
r_hand_obj.flags ^= NODROP
|
||||
|
||||
/obj/item/organ/internal/cyberimp/brain/anti_drop/remove(var/mob/living/carbon/M, special = 0)
|
||||
. = ..()
|
||||
if(active)
|
||||
ui_action_click()
|
||||
|
||||
return ..()
|
||||
|
||||
/obj/item/organ/internal/cyberimp/brain/anti_stun
|
||||
name = "CNS Rebooter implant"
|
||||
|
||||
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 295 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 507 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
@@ -31,6 +31,7 @@
|
||||
#include "code\__DEFINES\colors.dm"
|
||||
#include "code\__DEFINES\combat.dm"
|
||||
#include "code\__DEFINES\construction.dm"
|
||||
#include "code\__DEFINES\contractors.dm"
|
||||
#include "code\__DEFINES\contracts.dm"
|
||||
#include "code\__DEFINES\crafting.dm"
|
||||
#include "code\__DEFINES\criminal_status.dm"
|
||||
@@ -1269,6 +1270,23 @@
|
||||
#include "code\modules\antagonists\survivalist\survivalist.dm"
|
||||
#include "code\modules\antagonists\traitor\datum_mindslave.dm"
|
||||
#include "code\modules\antagonists\traitor\datum_traitor.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\contractor.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\contractor_hub.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\contractor_hub_ui.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\objective_contract.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\syndicate_contract.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\_base.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\balloon.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\blackout.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\fulton.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\pinpointer.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\reroll.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\zippo.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\items\contractor_baton.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\items\contractor_kit.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\items\contractor_pinpointer.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\items\contractor_uplink.dm"
|
||||
#include "code\modules\antagonists\traitor\contractor\items\extraction_items.dm"
|
||||
#include "code\modules\antagonists\wishgranter\wishgranter.dm"
|
||||
#include "code\modules\arcade\arcade_base.dm"
|
||||
#include "code\modules\arcade\arcade_prize.dm"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"basemessage": [
|
||||
"Fugitive from the law due to",
|
||||
"Needs to be interrogated for information about",
|
||||
"Wanted by The Syndicate for",
|
||||
"Ransomable to Nanotrasen for",
|
||||
"Has exploitable information about"
|
||||
],
|
||||
"verb": [
|
||||
"murdering",
|
||||
"killing",
|
||||
"accidentally destroying",
|
||||
"destroying",
|
||||
"knowing information about",
|
||||
"stealing",
|
||||
"slipping",
|
||||
"sabotaging",
|
||||
"robusting",
|
||||
"collaborating with",
|
||||
"being close friends with",
|
||||
"cloning",
|
||||
"befriending",
|
||||
"bombing",
|
||||
"kidnapping",
|
||||
"pretending to be",
|
||||
"seducing",
|
||||
"ignoring",
|
||||
"assassinating",
|
||||
"drinking",
|
||||
"hating",
|
||||
"slapping",
|
||||
"swatting",
|
||||
"skreeing at",
|
||||
"toolboxing",
|
||||
"swirling",
|
||||
"wearing",
|
||||
"[REDACTED]"
|
||||
],
|
||||
"noun": {
|
||||
"secret plans": 50,
|
||||
"the hand teleporter": 50,
|
||||
"an NT CentCom uniform": 50,
|
||||
"a supermatter shard": 50,
|
||||
"internal Syndicate documents": 50,
|
||||
"experimental Nanotrasen technology": 50,
|
||||
"bluespace crystals": 50,
|
||||
"a cult": 50,
|
||||
"shapeshifting creatures": 50,
|
||||
"a bluespace artillery": 50,
|
||||
"a DNA vault": 50,
|
||||
"a toolbox": 10,
|
||||
"a bar of soap": 10,
|
||||
"Ian": 10,
|
||||
"a Diona": 10,
|
||||
"a Drask": 10,
|
||||
"a Golem": 10,
|
||||
"a Grey": 10,
|
||||
"a Human": 10,
|
||||
"a Kidan": 10,
|
||||
"a Monkey": 10,
|
||||
"a Nucleation": 10,
|
||||
"a Plasmaman": 10,
|
||||
"a Shadow": 10,
|
||||
"a Skeleton": 10,
|
||||
"a Skrell": 10,
|
||||
"a Slime": 10,
|
||||
"a Slime Person": 10,
|
||||
"a Tajaran": 10,
|
||||
"a Vox": 10,
|
||||
"a Vulpkanin": 10,
|
||||
"a Wryn": 10,
|
||||
"an IPC": 10,
|
||||
"an Unathi": 10,
|
||||
"[REDACTED]": 10,
|
||||
"two million credits": 10,
|
||||
"one billion credits": 5,
|
||||
"research on floor cluwnes": 5,
|
||||
"Officer Beepsky": 5,
|
||||
"clown tears": 5,
|
||||
"Nanotrasen's swimsuit calendar": 5,
|
||||
"a suspicious bus": 5,
|
||||
"cat ears": 5,
|
||||
"the anime archive": 5,
|
||||
"the CMO's cat": 5,
|
||||
"WetSkrell.nt": 5,
|
||||
"YALPER": 1,
|
||||
"absolutely nothing": 1,
|
||||
"Cuban Pete": 1,
|
||||
"your mother": 1,
|
||||
"four cups of coffee a day": 1,
|
||||
"ducks": 1
|
||||
},
|
||||
"location": {
|
||||
"on NSS Cyberiad": 50,
|
||||
"on NSS Kerberos": 50,
|
||||
"on NSS Cerebron": 50,
|
||||
"somewhere in deep space": 50,
|
||||
"on a remote syndicate base": 50,
|
||||
"in a secure NT facility": 50,
|
||||
"while on an escape shuttle": 10,
|
||||
"while infiltrating CentCom headquarters": 10,
|
||||
"deep in the necropolis": 10,
|
||||
"in [REDACTED]": 10,
|
||||
"during a drunken bar fight": 5,
|
||||
"while stuck in a bathroom": 5,
|
||||
"in a back alley on Mars": 5
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ export const Section = props => {
|
||||
stretchContents,
|
||||
noTopPadding,
|
||||
children,
|
||||
scrollable,
|
||||
...rest
|
||||
} = props;
|
||||
const hasTitle = !isFalsy(title) || !isFalsy(buttons);
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
import { Component, Fragment } from 'inferno';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, Flex, Icon, LabeledList, Modal, Section, Tabs } from '../components';
|
||||
import { Countdown } from '../components/Countdown';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
const contractStatuses = {
|
||||
1: ["ACTIVE", "good"],
|
||||
2: ["COMPLETED", "good"],
|
||||
3: ["FAILED", "bad"],
|
||||
};
|
||||
|
||||
// Lifted from /tg/station
|
||||
const terminalMessages = [
|
||||
"Recording biometric data...",
|
||||
"Analyzing embedded syndicate info...",
|
||||
"STATUS CONFIRMED",
|
||||
"Contacting Syndicate database...",
|
||||
"Awaiting response...",
|
||||
"Awaiting response...",
|
||||
"Awaiting response...",
|
||||
"Awaiting response...",
|
||||
"Awaiting response...",
|
||||
"Awaiting response...",
|
||||
"Response received, ack 4851234...",
|
||||
"CONFIRM ACC " + (Math.round(Math.random() * 20000)),
|
||||
"Setting up private accounts...",
|
||||
"CONTRACTOR ACCOUNT CREATED",
|
||||
"Searching for available contracts...",
|
||||
"Searching for available contracts...",
|
||||
"Searching for available contracts...",
|
||||
"Searching for available contracts...",
|
||||
"CONTRACTS FOUND",
|
||||
"WELCOME, AGENT",
|
||||
];
|
||||
|
||||
export const Contractor = (properties, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
let body;
|
||||
if (data.unauthorized) {
|
||||
body = (
|
||||
<Flex.Item grow="1" backgroundColor="rgba(0, 0, 0, 0.8)">
|
||||
<FakeTerminal
|
||||
height="100%"
|
||||
allMessages={["ERROR: UNAUTHORIZED USER"]}
|
||||
finishedTimeout={100}
|
||||
onFinished={() => {}}
|
||||
/>
|
||||
</Flex.Item>
|
||||
);
|
||||
} else if (!data.load_animation_completed) {
|
||||
body = (
|
||||
<Flex.Item grow="1" backgroundColor="rgba(0, 0, 0, 0.8)">
|
||||
<FakeTerminal
|
||||
height="100%"
|
||||
allMessages={terminalMessages}
|
||||
finishedTimeout={3000}
|
||||
onFinished={() => act('complete_load_animation')}
|
||||
/>
|
||||
</Flex.Item>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<Fragment>
|
||||
<Flex.Item basis="content">
|
||||
<Summary />
|
||||
</Flex.Item>
|
||||
<Flex.Item basis="content" mt="0.5rem">
|
||||
<Navigation />
|
||||
</Flex.Item>
|
||||
<Flex.Item grow="1" overflow="hidden">
|
||||
{data.page === 1 ? (
|
||||
<Contracts height="100%" />
|
||||
) : (
|
||||
<Hub height="100%" />
|
||||
)}
|
||||
</Flex.Item>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
const [viewingPhoto, _setViewingPhoto] = useLocalState(context, "viewingPhoto", "");
|
||||
return (
|
||||
<Window theme="syndicate">
|
||||
{viewingPhoto && (
|
||||
<PhotoZoom />
|
||||
)}
|
||||
<Window.Content className="Contractor">
|
||||
<Flex direction="column" height="100%">
|
||||
{body}
|
||||
</Flex>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = (properties, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const {
|
||||
tc_available,
|
||||
tc_paid_out,
|
||||
completed_contracts,
|
||||
rep,
|
||||
} = data;
|
||||
return (
|
||||
<Section
|
||||
title="Summary"
|
||||
buttons={
|
||||
<Box verticalAlign="middle" mt="0.25rem">
|
||||
{rep} Rep
|
||||
</Box>
|
||||
}
|
||||
{...properties}>
|
||||
<Flex>
|
||||
<Box flexBasis="50%">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="TC Available" verticalAlign="middle">
|
||||
<Flex align="center">
|
||||
<Flex.Item grow="1">
|
||||
{tc_available} TC
|
||||
</Flex.Item>
|
||||
<Button
|
||||
disabled={tc_available <= 0}
|
||||
content="Claim"
|
||||
mx="0.75rem"
|
||||
mb="0"
|
||||
flexBasis="content"
|
||||
onClick={() => act("claim")}
|
||||
/>
|
||||
</Flex>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="TC Earned">
|
||||
{tc_paid_out} TC
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Box>
|
||||
<Box flexBasis="50%">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Contracts Completed" verticalAlign="middle">
|
||||
<Box height="20px" lineHeight="20px" display="inline-block">
|
||||
{completed_contracts}
|
||||
</Box>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Contractor Status" verticalAlign="middle">
|
||||
ACTIVE
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Navigation = (properties, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const {
|
||||
page,
|
||||
} = data;
|
||||
return (
|
||||
<Tabs {...properties}>
|
||||
<Tabs.Tab
|
||||
selected={page === 1}
|
||||
onClick={() => act("page", {
|
||||
page: 1,
|
||||
})}>
|
||||
<Icon name="suitcase" />
|
||||
Contracts
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
selected={page === 2}
|
||||
onClick={() => act("page", {
|
||||
page: 2,
|
||||
})}>
|
||||
<Icon name="shopping-cart" />
|
||||
Hub
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const Contracts = (properties, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const {
|
||||
contracts,
|
||||
contract_active,
|
||||
can_extract,
|
||||
} = data;
|
||||
const activeContract = !!contract_active && contracts.filter(c => c.status === 1)[0];
|
||||
const extractionCooldown = activeContract && activeContract.time_left > 0;
|
||||
const [_viewingPhoto, setViewingPhoto] = useLocalState(context, "viewingPhoto", "");
|
||||
return (
|
||||
<Section
|
||||
title="Available Contracts"
|
||||
overflow="auto"
|
||||
buttons={
|
||||
<Button
|
||||
disabled={!can_extract || extractionCooldown}
|
||||
icon="parachute-box"
|
||||
content={[
|
||||
"Call Extraction",
|
||||
extractionCooldown && (
|
||||
<Countdown
|
||||
timeLeft={activeContract.time_left}
|
||||
format={(v, f) => " (" + f.substr(3) + ")"}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
onClick={() => act("extract")}
|
||||
/>
|
||||
}
|
||||
{...properties}>
|
||||
{contracts.slice().sort((a, b) => {
|
||||
if (a.status === 1) {
|
||||
return -1;
|
||||
} else if (b.status === 1) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.status - b.status;
|
||||
}
|
||||
}).map(contract => (
|
||||
<Section
|
||||
key={contract.uid}
|
||||
title={(
|
||||
<Flex>
|
||||
<Flex.Item grow="1" color={contract.status === 1 && "good"}>
|
||||
{contract.target_name}
|
||||
</Flex.Item>
|
||||
<Flex.Item basis="content">
|
||||
{contract.has_photo && (
|
||||
<Button
|
||||
icon="camera"
|
||||
mb="-0.5rem"
|
||||
ml="0.5rem"
|
||||
onClick={() => setViewingPhoto("target_photo_" + contract.uid + ".png")}
|
||||
/>
|
||||
)}
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
)}
|
||||
className="Contractor__Contract"
|
||||
buttons={(
|
||||
<Box width="100%">
|
||||
{!!contractStatuses[contract.status] && (
|
||||
<Box
|
||||
color={contractStatuses[contract.status][1]}
|
||||
display="inline-block"
|
||||
mt={contract.status !== 1 && "0.125rem"}
|
||||
mr="0.25rem"
|
||||
lineHeight="20px">
|
||||
{contractStatuses[contract.status][0]}
|
||||
</Box>
|
||||
)}
|
||||
{contract.status === 1 && (
|
||||
<Button.Confirm
|
||||
icon="ban"
|
||||
color="bad"
|
||||
content="Abort"
|
||||
ml="0.5rem"
|
||||
onClick={() => act("abort")}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}>
|
||||
<Flex width="100%">
|
||||
<Flex.Item grow="2" mr="0.5rem">
|
||||
{contract.fluff_message}
|
||||
{!!contract.completed_time && (
|
||||
<Box color="good">
|
||||
<br />
|
||||
<Icon name="check" mr="0.5rem" />
|
||||
Contract completed at {contract.completed_time}
|
||||
</Box>
|
||||
)}
|
||||
{!!contract.dead_extraction && (
|
||||
<Box color="bad" mt="0.5rem" bold>
|
||||
<Icon name="exclamation-triangle" mr="0.5rem" />
|
||||
Telecrystals reward reduced drastically as the target was dead during extraction.
|
||||
</Box>
|
||||
)}
|
||||
{!!contract.fail_reason && (
|
||||
<Box color="bad">
|
||||
<br />
|
||||
<Icon name="times" mr="0.5rem" />
|
||||
Contract failed: {contract.fail_reason}
|
||||
</Box>
|
||||
)}
|
||||
</Flex.Item>
|
||||
<Flex.Item grow="1" flexBasis="100%">
|
||||
<Box mb="0.5rem" color="label">
|
||||
Extraction Zone:
|
||||
</Box>
|
||||
{contract.difficulties?.map((difficulty, key) => (
|
||||
<Fragment>
|
||||
<Button.Confirm
|
||||
disabled={!!contract_active}
|
||||
content={difficulty.name + " (" + difficulty.reward + " TC)"}
|
||||
onClick={() => act("activate", {
|
||||
uid: contract.uid,
|
||||
difficulty: key + 1,
|
||||
})}
|
||||
/>
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
{!!contract.objective && (
|
||||
<Box color="white" bold>
|
||||
{contract.objective.extraction_zone}<br />
|
||||
({(contract.objective.reward_tc || 0) + " TC"},
|
||||
{(contract.objective.reward_credits || 0) + " Credits"})
|
||||
</Box>
|
||||
)}
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Section>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Hub = (properties, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const {
|
||||
rep,
|
||||
buyables,
|
||||
} = data;
|
||||
return (
|
||||
<Section
|
||||
title="Available Purchases"
|
||||
overflow="auto"
|
||||
{...properties}>
|
||||
{buyables.map(buyable => (
|
||||
<Section
|
||||
key={buyable.uid}
|
||||
title={buyable.name}>
|
||||
{buyable.description}<br />
|
||||
<Button.Confirm
|
||||
disabled={rep < buyable.cost || buyable.stock === 0}
|
||||
icon="shopping-cart"
|
||||
content={"Buy (" + buyable.cost + " Rep)"}
|
||||
mt="0.5rem"
|
||||
onClick={() => act('purchase', {
|
||||
uid: buyable.uid,
|
||||
})}
|
||||
/>
|
||||
{buyable.stock > -1 && (
|
||||
<Box
|
||||
as="span"
|
||||
color={buyable.stock === 0 ? "bad" : "good"}
|
||||
ml="0.5rem">
|
||||
{buyable.stock} in stock
|
||||
</Box>
|
||||
)}
|
||||
</Section>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
// Lifted from /tg/station
|
||||
class FakeTerminal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.timer = null;
|
||||
this.state = {
|
||||
currentIndex: 0,
|
||||
currentDisplay: [],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
const { props, state } = this;
|
||||
if (state.currentIndex <= props.allMessages.length) {
|
||||
this.setState(prevState => {
|
||||
return ({
|
||||
currentIndex: prevState.currentIndex + 1,
|
||||
});
|
||||
});
|
||||
const { currentDisplay } = state;
|
||||
currentDisplay.push(props.allMessages[state.currentIndex]);
|
||||
} else {
|
||||
clearTimeout(this.timer);
|
||||
setTimeout(props.onFinished, props.finishedTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
linesPerSecond = 2.5,
|
||||
} = this.props;
|
||||
this.timer = setInterval(() => this.tick(), 1000 / linesPerSecond);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Box m={1}>
|
||||
{this.state.currentDisplay.map(value => (
|
||||
<Fragment key={value}>
|
||||
{value}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const PhotoZoom = (properties, context) => {
|
||||
const [viewingPhoto, setViewingPhoto] = useLocalState(context, "viewingPhoto", "");
|
||||
return (
|
||||
<Modal
|
||||
className="Contractor__photoZoom">
|
||||
<Box
|
||||
as="img"
|
||||
src={viewingPhoto}
|
||||
/>
|
||||
<Button
|
||||
icon="times"
|
||||
content="Close"
|
||||
color="grey"
|
||||
mt="1rem"
|
||||
onClick={() => setViewingPhoto("")}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
import { flow } from 'common/fp';
|
||||
import { createSearch, decodeHtmlEntities } from 'common/string';
|
||||
import { Fragment } from "inferno";
|
||||
import { useBackend, useLocalState } from "../backend";
|
||||
import { Button, Box, Section, Tabs, Icon, Flex, Input } from "../components";
|
||||
import { Box, Button, Flex, Icon, Input, Section, Tabs } from "../components";
|
||||
import { Countdown } from '../components/Countdown';
|
||||
import { FlexItem } from "../components/Flex";
|
||||
import { Window } from "../layouts";
|
||||
import { createSearch, decodeHtmlEntities } from 'common/string';
|
||||
import { flow } from 'common/fp';
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
import { ComplexModal, modalAnswer, modalOpen, modalRegisterBodyOverride } from './common/ComplexModal';
|
||||
|
||||
const PickTab = index => {
|
||||
switch (index) {
|
||||
@@ -25,6 +27,7 @@ export const Uplink = (props, context) => {
|
||||
|
||||
return (
|
||||
<Window theme="syndicate">
|
||||
<ComplexModal />
|
||||
<Window.Content scrollable>
|
||||
<Tabs>
|
||||
<Tabs.Tab
|
||||
@@ -41,6 +44,25 @@ export const Uplink = (props, context) => {
|
||||
icon="user">
|
||||
Exploitable Information
|
||||
</Tabs.Tab>
|
||||
{!!data.contractor && (
|
||||
<Tabs.Tab
|
||||
key="BecomeContractor"
|
||||
color={(!!data.contractor.available && !data.contractor.accepted) ? "yellow" : "transparent"}
|
||||
onClick={() => modalOpen(context, "become_contractor")}
|
||||
icon="suitcase">
|
||||
Contracting Opportunity
|
||||
{data.contractor.accepted
|
||||
? (
|
||||
<i> (Accepted)</i>
|
||||
) : (
|
||||
<Countdown
|
||||
timeLeft={data.contractor.time_left}
|
||||
format={(v, f) => " (" + f + ")"}
|
||||
bold
|
||||
/>
|
||||
)}
|
||||
</Tabs.Tab>
|
||||
)}
|
||||
<Tabs.Tab
|
||||
key="LockUplink"
|
||||
// This cant ever be selected. Its just a close button.
|
||||
@@ -190,3 +212,63 @@ const ExploitableInfoPage = (_properties, context) => {
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
modalRegisterBodyOverride("become_contractor", (modal, context) => {
|
||||
const { data } = useBackend(context);
|
||||
const {
|
||||
time_left,
|
||||
} = (data.contractor || {});
|
||||
const isAvailable = !!data?.contractor?.available;
|
||||
const isAffordable = !!data?.contractor?.affordable;
|
||||
const isAccepted = !!data?.contractor?.accepted;
|
||||
return (
|
||||
<Section
|
||||
level="2"
|
||||
m="-1rem"
|
||||
pb="1rem"
|
||||
title={(
|
||||
<Fragment>
|
||||
<Icon name="suitcase" />
|
||||
Contracting Opportunity
|
||||
</Fragment>
|
||||
)}>
|
||||
<Box mx="0.5rem" mb="0.5rem">
|
||||
<b>
|
||||
Your achievements for the Syndicate have not gone unnoticed, agent.
|
||||
We have decided to give you the rare opportunity of becoming a Contractor.
|
||||
</b><br /><br />
|
||||
For the small price of 20 telecrystals, we will upgrade your rank to that of a Contractor,
|
||||
allowing you to undertake kidnapping contracts for TC and credits.<br />
|
||||
In addition, you will be supplied with a Contractor Kit which contains a Contractor Uplink,
|
||||
standard issue contractor gear and three random low cost items.<br /><br />
|
||||
|
||||
More detailed instructions can be found within your kit, should you accept this offer.
|
||||
</Box>
|
||||
<Button.Confirm
|
||||
disabled={!isAvailable || isAccepted}
|
||||
italic={!isAvailable}
|
||||
bold={isAvailable}
|
||||
icon={(isAvailable && !isAccepted) && "check"}
|
||||
color="good"
|
||||
content={isAccepted ? (
|
||||
"Accepted"
|
||||
) : (
|
||||
isAvailable
|
||||
? [
|
||||
"Accept Offer",
|
||||
<Countdown
|
||||
key="countdown"
|
||||
timeLeft={time_left}
|
||||
format={(v, f) => " (" + f + ")"}
|
||||
/>,
|
||||
]
|
||||
: (!isAffordable ? "Insufficient TC" : "Offer expired")
|
||||
)}
|
||||
position="absolute"
|
||||
right="1rem"
|
||||
bottom="-0.75rem"
|
||||
onClick={() => modalAnswer(context, modal.id, 1)}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.Contractor {
|
||||
* {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.Section__titleText {
|
||||
display: inline-block;
|
||||
max-width: 70%;
|
||||
|
||||
& > .Flex {
|
||||
width: 100%;
|
||||
|
||||
& > .Flex__item:first-of-type {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Contractor__Contract {
|
||||
.Button {
|
||||
font-size: 11px;
|
||||
white-space: normal !important;
|
||||
}
|
||||
}
|
||||
|
||||
.Contractor__photoZoom {
|
||||
text-align: center;
|
||||
|
||||
& > img {
|
||||
width: 96px;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
}
|
||||
|
||||
& > .Button {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,12 +42,13 @@
|
||||
@include meta.load-css('./interfaces/AccountsUplinkTerminal.scss');
|
||||
@include meta.load-css('./interfaces/BrigCells.scss');
|
||||
@include meta.load-css('./interfaces/CameraConsole.scss');
|
||||
@include meta.load-css('./interfaces/Contractor.scss');
|
||||
@include meta.load-css('./interfaces/ExosuitFabricator.scss');
|
||||
@include meta.load-css('./interfaces/Newscaster.scss');
|
||||
@include meta.load-css('./interfaces/NuclearBomb.scss');
|
||||
@include meta.load-css('./interfaces/OreRedemption.scss');
|
||||
@include meta.load-css('./interfaces/PDA.scss');
|
||||
@include meta.load-css('./interfaces/PoolController.scss');
|
||||
@include meta.load-css('./interfaces/OreRedemption.scss');
|
||||
@include meta.load-css('./interfaces/RndConsole.scss');
|
||||
@include meta.load-css('./interfaces/Roulette.scss');
|
||||
@include meta.load-css('./interfaces/Safe.scss');
|
||||
|
||||