[READY] Syndicate Contractors (#14605)

* Syndicate Contractors initial PR

* Finalize initial version

DNP

Finalize initial version

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

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

* Address moxian, AA and Farie

* Fix mode.txt

* oops

* Address Farie 2

* Tweak baton to work around stam crits, address SteelSlayer

* Address TM issues, TP menu

* Fix wrongly merged DME

* Tick contractor DM files again

* Remove step_y

* TC rewards always higher than preceding difficulty's reward

* Address Farie

* Remove extraction_turf from handle_target_return

* fix doc

* Merge part2

* Address AA again
This commit is contained in:
dearmochi
2020-12-09 12:03:23 -05:00
committed by GitHub
parent 2897b82345
commit 5edee29a50
73 changed files with 3588 additions and 250 deletions
File diff suppressed because it is too large Load Diff
+3
View File
@@ -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
+28
View File
@@ -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)
+1
View File
@@ -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))
+2
View File
@@ -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)
+1
View File
@@ -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)
+281
View File
@@ -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")
+3
View File
@@ -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
+26
View File
@@ -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
+34
View File
@@ -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>"
+3 -18
View File
@@ -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 ###########################
+4
View File
@@ -40,6 +40,10 @@
GLOB.prisonwarp += loc
qdel(src)
if("syndieprisonwarp")
GLOB.syndieprisonwarp += loc
qdel(src)
if("prisonsecuritywarp")
GLOB.prisonsecuritywarp += loc
qdel(src)
+45 -1
View File
@@ -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.
+111 -31
View File
@@ -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)
+3
View File
@@ -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>
+71
View File
@@ -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
+15
View File
@@ -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"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 243 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

+18
View File
@@ -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"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+108
View File
@@ -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
}
}
-1
View File
@@ -11,7 +11,6 @@ export const Section = props => {
stretchContents,
noTopPadding,
children,
scrollable,
...rest
} = props;
const hasTitle = !isFalsy(title) || !isFalsy(buttons);
+428
View File
@@ -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"},&nbsp;
{(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>
);
};
+86 -4
View File
@@ -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>&nbsp;(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" />&nbsp;
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>
);
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;
}
}
+2 -1
View File
@@ -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');