[BOUNTY] Re-adds Contractors (#79737)

## About The Pull Request

Adds Contractors back to the game as a kit that Infiltrators (Only
midround/latejoin Traitors, not roundstart) can buy. Buying this will
give you access to the new Contractor items in the uplink (which
replaces the Contractor shop).
I've also refactored things about contractor and how it's handled, such
as removed its sleep calls, swapping its UI to tsx, making it actually
functional within more modern TG code and the reworked Traitors as a
whole, among other things.

I also fixed the contractor guide paper (the text was broken) and made
contractor kit boxes no longer empty.

Contractor items only appear once the contractor kit is purchased, but
they can't be bought until you make an account.

Video demonstration

https://github.com/tgstation/tgstation/assets/53777086/c6aed4e8-bbd4-4ba0-b6a2-9f58fd308d09

Regular Traitors can't access it

![image](https://github.com/tgstation/tgstation/assets/53777086/539b8121-a1fa-4f2d-8e69-ec3d9708152c)

The Contractor UI (Modified to not have a shop anymore) - Ignore the
Invalid Targets, those appear when there's no possible targets

![image](https://github.com/tgstation/tgstation/assets/53777086/e128aecc-0795-4a6b-b8c5-e6f431401330)

Roundend report

![image](https://github.com/tgstation/tgstation/assets/53777086/71b88262-61de-4652-862b-79a7d5ded979)

## Why It's Good For The Game

Contractor was a fun way of playing Traitor that isn't relying on RNG
objectives to pop up, this is instead constant stream of content for the
contractor that encourages non-lethal play, and is now a viable
alternative now that Infiltrators don't have Reputation anymore.

Link to bounty: https://tgstation13.org/phpBB/viewtopic.php?f=5&t=35160

Relevant image from bounty

![image](https://github.com/tgstation/tgstation/assets/53777086/b26a17b0-2f10-4e53-b8fc-25cf8130584e)

## Changelog

🆑
add: Infiltrators (Latejoin/Midround traitors) can now buy and use
Contract kits again.
del: Contractor baton can now only be purchased once.
/🆑

---------

Co-authored-by: Jacquerel <hnevard@gmail.com>
This commit is contained in:
John Willard
2023-11-18 13:38:37 -05:00
committed by GitHub
parent 722a3a5067
commit 6bb2175fa5
37 changed files with 1237 additions and 85 deletions
@@ -48,7 +48,7 @@
///Flag of the type of device the modular computer is, deciding what types of apps it can run.
var/hardware_flag = NONE
// Options: PROGRAM_ALL | PROGRAM_CONSOLE | PROGRAM_LAPTOP | PROGRAM_TABLET
// Options: PROGRAM_ALL | PROGRAM_CONSOLE | PROGRAM_LAPTOP | PROGRAM_PDA
///The theme, used for the main menu and file browser apps.
var/device_theme = PDA_THEME_NTOS
@@ -6,3 +6,6 @@
/obj/item/computer_disk/syndicate/camera_app
starting_programs = list(/datum/computer_file/program/secureye/syndicate)
/obj/item/computer_disk/syndicate/contractor
starting_programs = list(/datum/computer_file/program/contract_uplink)
@@ -18,7 +18,7 @@
icon_state_menu = "menu"
max_capacity = 64
allow_chunky = TRUE
hardware_flag = PROGRAM_TABLET
hardware_flag = PROGRAM_PDA
max_idle_programs = 2
w_class = WEIGHT_CLASS_SMALL
slot_flags = ITEM_SLOT_ID | ITEM_SLOT_BELT
@@ -272,6 +272,19 @@
if(msg)
msg.invisible = TRUE
/obj/item/modular_computer/pda/syndicate_contract_uplink
name = "contractor tablet"
device_theme = PDA_THEME_SYNDICATE
icon_state_menu = "contractor-assign"
comp_light_luminosity = 6.3
has_pda_programs = FALSE
greyscale_config = /datum/greyscale_config/tablet/stripe_double
greyscale_colors = "#696969#000000#FFA500"
starting_programs = list(
/datum/computer_file/program/contract_uplink,
)
/**
* Silicon PDA
*
@@ -44,7 +44,7 @@ This is how the base program is setup. the rest is mostly tgui stuff. I'll use t
/// ditto but on emagged syndie net. Use this for antag programs
available_on_syndinet = 0
/// Bitflags (PROGRAM_CONSOLE, PROGRAM_LAPTOP, PROGRAM_TABLET combination)
/// Bitflags (PROGRAM_CONSOLE, PROGRAM_LAPTOP, PROGRAM_PDA combination)
/// or PROGRAM_ALL. Use this to limit what kind of machines can run the
/// program. For example, comms program should be limited to consoles and laptops.
usage_flags = PROGRAM_ALL
@@ -28,7 +28,7 @@
var/requires_ntnet = FALSE
/// NTNet status, updated every tick by computer running this program. Don't use this for checks if NTNet works, computers do that. Use this for calculations, etc.
var/ntnet_status = 1
/// Bitflags (PROGRAM_CONSOLE, PROGRAM_LAPTOP, PROGRAM_TABLET combination) or PROGRAM_ALL
/// Bitflags (PROGRAM_CONSOLE, PROGRAM_LAPTOP, PROGRAM_PDA combination) or PROGRAM_ALL
var/usage_flags = PROGRAM_ALL
/// Whether the program can be downloaded from NTNet. Set to FALSE to disable.
var/available_on_ntnet = TRUE
@@ -0,0 +1,173 @@
/datum/computer_file/program/contract_uplink
filename = "contractor uplink"
filedesc = "Syndicate Contractor Uplink"
extended_desc = "A standard, Syndicate issued system for handling important contracts while on the field."
category = PROGRAM_CATEGORY_MISC
program_icon_state = "contractor-assign"
program_icon = "tasks"
size = 10
requires_ntnet = FALSE
available_on_ntnet = FALSE
available_on_syndinet = FALSE
usage_flags = PROGRAM_PDA //this is all we've got sprites for :sob:
unique_copy = TRUE
undeletable = TRUE
tgui_id = "SyndicateContractor"
///The traitor datum stored on the program. Starts off as null and is set by the player.
var/datum/antagonist/traitor/traitor_data
///The error screen sent to the UI so they can show the player.
var/error = ""
///Boolean on whether the UI is on the Information screen.
var/info_screen = TRUE
///Boolean on whether the program is being loaded for the first time, for a unique screen animation.
var/first_load = TRUE
/datum/computer_file/program/contract_uplink/clone()
var/datum/computer_file/program/contract_uplink/temp = ..()
temp.traitor_data = traitor_data
return temp
/datum/computer_file/program/contract_uplink/Destroy(force)
traitor_data = null
return ..()
/datum/computer_file/program/contract_uplink/ui_act(action, params)
. = ..()
if(.)
return
var/mob/living/user = usr
switch(action)
if("PRG_contract-accept")
var/contract_id = text2num(params["contract_id"])
traitor_data.uplink_handler.contractor_hub.assigned_contracts[contract_id].status = CONTRACT_STATUS_ACTIVE
traitor_data.uplink_handler.contractor_hub.current_contract = traitor_data.uplink_handler.contractor_hub.assigned_contracts[contract_id]
program_icon_state = "contractor-contract"
return TRUE
if("PRG_login")
var/datum/antagonist/traitor/traitor_user = user.mind.has_antag_datum(/datum/antagonist/traitor)
if(!traitor_user)
error = "UNAUTHORIZED USER"
return TRUE
traitor_data = traitor_user
if(!traitor_data.uplink_handler.contractor_hub)
traitor_data.uplink_handler.contractor_hub = new
traitor_data.uplink_handler.contractor_hub.create_contracts(traitor_user.owner)
user.playsound_local(user, 'sound/effects/contractstartup.ogg', 100, FALSE)
program_icon_state = "contractor-contractlist"
return TRUE
if("PRG_call_extraction")
if (traitor_data.uplink_handler.contractor_hub.current_contract.status != CONTRACT_STATUS_EXTRACTING)
if (traitor_data.uplink_handler.contractor_hub.current_contract.handle_extraction(user))
user.playsound_local(user, 'sound/effects/confirmdropoff.ogg', 100, TRUE)
traitor_data.uplink_handler.contractor_hub.current_contract.status = CONTRACT_STATUS_EXTRACTING
program_icon_state = "contractor-extracted"
else
user.playsound_local(user, 'sound/machines/uplinkerror.ogg', 50)
error = "Either both you or your target aren't at the dropoff location, or the pod hasn't got a valid place to land. Clear space, or make sure you're both inside."
else
user.playsound_local(user, 'sound/machines/uplinkerror.ogg', 50)
error = "Already extracting... Place the target into the pod. If the pod was destroyed, this contract is no longer possible."
return TRUE
if("PRG_contract_abort")
var/contract_id = traitor_data.uplink_handler.contractor_hub.current_contract.id
traitor_data.uplink_handler.contractor_hub.current_contract = null
traitor_data.uplink_handler.contractor_hub.assigned_contracts[contract_id].status = CONTRACT_STATUS_ABORTED
program_icon_state = "contractor-contractlist"
return TRUE
if("PRG_redeem_TC")
if (traitor_data.uplink_handler.contractor_hub.contract_TC_to_redeem)
var/obj/item/stack/telecrystal/crystals = new /obj/item/stack/telecrystal(get_turf(user), traitor_data.uplink_handler.contractor_hub.contract_TC_to_redeem)
if(ishuman(user))
var/mob/living/carbon/human/H = user
if(H.put_in_hands(crystals))
to_chat(H, span_notice("Your payment materializes into your hands!"))
else
to_chat(user, span_notice("Your payment materializes onto the floor."))
traitor_data.uplink_handler.contractor_hub.contract_TC_payed_out += traitor_data.uplink_handler.contractor_hub.contract_TC_to_redeem
traitor_data.uplink_handler.contractor_hub.contract_TC_to_redeem = 0
return TRUE
else
user.playsound_local(user, 'sound/machines/uplinkerror.ogg', 50)
return TRUE
if ("PRG_clear_error")
error = ""
return TRUE
if("PRG_set_first_load_finished")
first_load = FALSE
return TRUE
if("PRG_toggle_info")
info_screen = !info_screen
return TRUE
/datum/computer_file/program/contract_uplink/ui_data(mob/user)
var/list/data = list()
data["first_load"] = first_load
data["logged_in"] = !!traitor_data
data["station_name"] = GLOB.station_name
data["info_screen"] = info_screen
data["error"] = error
if(!traitor_data)
data["ongoing_contract"] = FALSE
data["extraction_enroute"] = FALSE
update_computer_icon()
return data
data["ongoing_contract"] = !!traitor_data.uplink_handler.contractor_hub.current_contract
if(traitor_data.uplink_handler.contractor_hub.current_contract)
program_icon_state = "contractor-contract"
if (traitor_data.uplink_handler.contractor_hub.current_contract.status == CONTRACT_STATUS_EXTRACTING)
data["extraction_enroute"] = TRUE
program_icon_state = "contractor-extracted"
else
data["extraction_enroute"] = FALSE
var/turf/curr = get_turf(user)
var/turf/dropoff_turf
data["current_location"] = "[get_area_name(curr, TRUE)]"
for (var/turf/content in traitor_data.uplink_handler.contractor_hub.current_contract.contract.dropoff.contents)
if (isturf(content))
dropoff_turf = content
break
var/direction
if(curr.z == dropoff_turf.z) //Direction calculations for same z-level only
direction = uppertext(dir2text(get_dir(curr, dropoff_turf))) //Direction text (East, etc). Not as precise, but still helpful.
if(get_area(user) == traitor_data.uplink_handler.contractor_hub.current_contract.contract.dropoff)
direction = "LOCATION CONFIRMED"
else
direction = "???"
data["dropoff_direction"] = direction
data["redeemable_tc"] = traitor_data.uplink_handler.contractor_hub.contract_TC_to_redeem
data["earned_tc"] = traitor_data.uplink_handler.contractor_hub.contract_TC_payed_out
data["contracts_completed"] = traitor_data.uplink_handler.contractor_hub.contracts_completed
for (var/datum/syndicate_contract/contract in traitor_data.uplink_handler.contractor_hub.assigned_contracts)
if(!contract.contract)
stack_trace("Syndiate contract with null contract objective found in [traitor_data.owner]'s contractor hub!")
contract.status = CONTRACT_STATUS_ABORTED
continue
data["contracts"] += list(list(
"target" = contract.contract.target,
"target_rank" = contract.target_rank,
"payout" = contract.contract.payout,
"payout_bonus" = contract.contract.payout_bonus,
"dropoff" = contract.contract.dropoff,
"id" = contract.id,
"status" = contract.status,
"message" = contract.wanted_message,
))
update_computer_icon()
return data
@@ -57,7 +57,7 @@
var/list/data = list()
var/turf/turf = get_turf(computer)
data["atmozphereMode"] = atmozphere_mode
data["clickAtmozphereCompatible"] = (computer.hardware_flag & PROGRAM_TABLET)
data["clickAtmozphereCompatible"] = (computer.hardware_flag & PROGRAM_PDA)
switch (atmozphere_mode) //Null air wont cause errors, don't worry.
if(ATMOZPHERE_SCAN_ENV)
var/datum/gas_mixture/air = turf?.return_air()
@@ -74,7 +74,7 @@
atmozphere_mode = ATMOZPHERE_SCAN_ENV
UnregisterSignal(computer, COMSIG_ITEM_ATTACK_SELF_SECONDARY)
return TRUE
if(!(computer.hardware_flag & PROGRAM_TABLET))
if(!(computer.hardware_flag & PROGRAM_PDA))
computer.say("Device incompatible for scanning objects!")
return FALSE
atmozphere_mode = ATMOZPHERE_SCAN_CLICK
@@ -5,7 +5,7 @@
program_icon_state = "request"
extended_desc = "Nanotrasen Internal Requisition Network interface for supply purchasing using a department budget account."
requires_ntnet = TRUE
usage_flags = PROGRAM_LAPTOP | PROGRAM_TABLET
usage_flags = PROGRAM_LAPTOP | PROGRAM_PDA
size = 10
tgui_id = "NtosCargo"
///Are you actually placing orders with it?
@@ -5,7 +5,7 @@
category = PROGRAM_CATEGORY_MISC
extended_desc = "This program allows the taking of pictures."
size = 4
usage_flags = PROGRAM_TABLET
usage_flags = PROGRAM_PDA
tgui_id = "NtosCamera"
program_icon = "camera"
@@ -4,7 +4,7 @@
category = PROGRAM_CATEGORY_MISC
extended_desc = "This program allows the tablet to scan physical objects and display a data output."
size = 2
usage_flags = PROGRAM_TABLET
usage_flags = PROGRAM_PDA
tgui_id = "NtosPhysScanner"
program_icon = "barcode"
/// Information from the last scanned person, to display on the app.
@@ -15,7 +15,7 @@
undeletable = TRUE // It comes by default in tablets, can't be downloaded, takes no space and should obviously not be able to be deleted.
header_program = TRUE
available_on_ntnet = FALSE
usage_flags = PROGRAM_TABLET
usage_flags = PROGRAM_PDA
ui_header = "ntnrc_idle.gif"
tgui_id = "NtosMessenger"
program_icon = "comment-alt"
@@ -6,7 +6,7 @@
program_icon_state = "radarntos"
requires_ntnet = TRUE
available_on_ntnet = FALSE
usage_flags = PROGRAM_LAPTOP | PROGRAM_TABLET
usage_flags = PROGRAM_LAPTOP | PROGRAM_PDA
size = 5
tgui_id = "NtosRadar"
///List of trackable entities. Updated by the scan() proc.
@@ -7,7 +7,7 @@
program_icon_state = "crew"
tgui_id = "NtosRecords"
size = 4
usage_flags = PROGRAM_TABLET | PROGRAM_LAPTOP
usage_flags = PROGRAM_PDA | PROGRAM_LAPTOP
available_on_ntnet = FALSE
detomatix_resistance = DETOMATIX_RESIST_MINOR
@@ -8,7 +8,7 @@
requires_ntnet = FALSE
available_on_ntnet = FALSE
undeletable = TRUE
usage_flags = PROGRAM_TABLET
usage_flags = PROGRAM_PDA
size = 5
tgui_id = "NtosRobotact"
program_icon = "terminal"
@@ -7,7 +7,7 @@
size = 2
tgui_id = "NtosSignaler"
program_icon = "satellite-dish"
usage_flags = PROGRAM_TABLET | PROGRAM_LAPTOP
usage_flags = PROGRAM_PDA | PROGRAM_LAPTOP
///What is the saved signal frequency?
var/signal_frequency = FREQ_SIGNALER
/// What is the saved signal code?
@@ -7,7 +7,7 @@
size = 2
tgui_id = "NtosSkillTracker"
program_icon = "medal"
usage_flags = PROGRAM_TABLET // Must be a handheld device to read read your chakras or whatever
usage_flags = PROGRAM_PDA // Must be a handheld device to read read your chakras or whatever
/datum/computer_file/program/skill_tracker/ui_data(mob/user)
var/list/data = list()