diff --git a/_maps/map_files/cyberiad/z2.dmm b/_maps/map_files/cyberiad/z2.dmm
index fe1c5fb8377..67b16b42f04 100644
--- a/_maps/map_files/cyberiad/z2.dmm
+++ b/_maps/map_files/cyberiad/z2.dmm
@@ -2901,6 +2901,12 @@
dir = 2
},
/area/syndicate_mothership)
+"ip" = (
+/obj/machinery/light/spot,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"iq" = (
/obj/structure/noticeboard{
pixel_x = -32
@@ -4451,6 +4457,28 @@
dir = 2
},
/area/centcom/control)
+"mF" = (
+/obj/machinery/light/spot,
+/obj/structure/chair{
+ dir = 1
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
+"mG" = (
+/obj/machinery/access_button{
+ command = "cycle_exterior";
+ frequency = 1441;
+ master_tag = "syndicate_jail_airlock_control";
+ name = "Syndicate Jail Access Button";
+ pixel_y = 24;
+ req_access_txt = "150"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership)
"mI" = (
/obj/structure/flora/ausbushes/brflowers,
/turf/unsimulated/floor{
@@ -11737,6 +11765,12 @@
},
/turf/unsimulated/floor/snow,
/area/syndicate_mothership)
+"Cf" = (
+/obj/structure/kitchenspike,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Cm" = (
/obj/structure/table/holotable,
/turf/simulated/floor/holofloor{
@@ -11744,6 +11778,12 @@
icon_state = "red"
},
/area/holodeck/source_thunderdomecourt)
+"CD" = (
+/obj/structure/table/wood,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"CF" = (
/turf/simulated/floor/holofloor{
dir = 4;
@@ -11770,9 +11810,39 @@
"Dv" = (
/turf/space,
/area/admin)
+"DG" = (
+/obj/machinery/embedded_controller/radio/airlock/access_controller{
+ frequency = 1441;
+ id_tag = "syndicate_jail_airlock_control";
+ name = "Syndicate Jail Access Controller";
+ pixel_x = 0;
+ pixel_y = 24;
+ req_access_txt = "150";
+ tag_exterior_door = "syndicate_jail_airlock_external";
+ tag_interior_door = "syndicate_jail_airlock_internal"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership)
+"DL" = (
+/turf/unsimulated/wall/fakeglass{
+ dir = 8;
+ icon_state = "fakewindows3";
+ tag = "icon-fakewindows (WEST)"
+ },
+/area/syndicate_mothership/jail)
"DX" = (
/turf/simulated/floor/mech_bay_recharge_floor,
/area/admin)
+"DY" = (
+/obj/machinery/door/airlock/hatch/syndicate{
+ name = "Torture Room"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Ei" = (
/obj/structure/holohoop{
dir = 1
@@ -11796,6 +11866,14 @@
dir = 4
},
/area/holodeck/source_meetinghall)
+"ED" = (
+/obj/structure/toilet{
+ dir = 8
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Fe" = (
/obj/structure/table/holotable,
/turf/simulated/floor/holofloor{
@@ -11833,6 +11911,12 @@
dir = 4
},
/area/holodeck/source_desert)
+"Gu" = (
+/obj/structure/bed,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Gx" = (
/turf/simulated/floor/holofloor{
tag = "icon-wood (EAST)";
@@ -11850,6 +11934,25 @@
icon_state = "gcircuit"
},
/area/admin)
+"GZ" = (
+/obj/machinery/door/airlock/hatch/syndicate{
+ autoclose = 0;
+ frequency = 1441;
+ id_tag = "syndicate_jail_airlock_internal";
+ locked = 1;
+ name = "Syndicate Jail Internal Airlock"
+ },
+/obj/machinery/door/airlock/hatch/syndicate{
+ autoclose = 0;
+ frequency = 1441;
+ id_tag = "syndicate_jail_airlock_internal";
+ locked = 1;
+ name = "Syndicate Jail Internal Airlock"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Hb" = (
/obj/structure/window/reinforced{
dir = 1
@@ -11887,6 +11990,9 @@
dir = 2
},
/area/holodeck/source_theatre)
+"Hr" = (
+/turf/unsimulated/wall,
+/area/syndicate_mothership/jail)
"Hu" = (
/obj/structure/rack,
/obj/item/clothing/gloves/space_ninja,
@@ -11930,6 +12036,16 @@
icon_state = "red"
},
/area/holodeck/source_knightarena)
+"HN" = (
+/obj/machinery/light/spot{
+ tag = "icon-tube1 (NORTH)";
+ icon_state = "tube1";
+ dir = 1
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"HR" = (
/obj/structure/closet/secure_closet/freezer/meat{
req_access_txt = "112"
@@ -11946,6 +12062,12 @@
icon_state = "white"
},
/area/tdome)
+"HU" = (
+/obj/effect/decal/remains/human,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"HY" = (
/obj/structure/table,
/obj/machinery/kitchen_machine/microwave/upgraded,
@@ -12010,6 +12132,36 @@
"Iw" = (
/turf/simulated/floor/bluegrid,
/area/tdome/arena)
+"Iz" = (
+/obj/structure/chair/stool,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
+"IA" = (
+/obj/structure/table/wood,
+/obj/machinery/door_control{
+ id = "syndicate_jail_cell";
+ name = "Bolt Cell Doors";
+ normaldoorcontrol = 1;
+ pixel_x = 6;
+ pixel_y = 0;
+ req_access_txt = "150";
+ specialfunctions = 4
+ },
+/obj/machinery/door_control{
+ id = "syndicate_jail";
+ name = "Bolt Jail Door";
+ normaldoorcontrol = 1;
+ pixel_x = -5;
+ pixel_y = 0;
+ req_access_txt = "150";
+ specialfunctions = 4
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"IE" = (
/obj/structure/holowindow{
dir = 1
@@ -12128,6 +12280,13 @@
icon_state = "floor4"
},
/area/shuttle/escape)
+"KB" = (
+/obj/structure/table/wood,
+/obj/machinery/kitchen_machine/microwave,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"KK" = (
/obj/machinery/tcomms/relay/cc,
/turf/unsimulated/floor{
@@ -12211,6 +12370,24 @@
icon_state = "dark"
},
/area/centcom/specops)
+"Mo" = (
+/obj/structure/table,
+/obj/item/circular_saw,
+/obj/item/surgicaldrill{
+ pixel_y = 5
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
+"MG" = (
+/obj/effect/landmark{
+ name = "syndieprisonwarp"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"MR" = (
/turf/simulated/floor/holofloor{
tag = "icon-asteroid1 (EAST)";
@@ -12218,6 +12395,16 @@
dir = 4
},
/area/holodeck/source_desert)
+"Nv" = (
+/obj/machinery/light/spot{
+ tag = "icon-tube1 (EAST)";
+ icon_state = "tube1";
+ dir = 4
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Nz" = (
/obj/effect/decal/warning_stripes/northwest,
/turf/unsimulated/floor{
@@ -12231,6 +12418,15 @@
name = "plating"
},
/area/centcom/control)
+"NG" = (
+/obj/effect/decal/cleanable/blood,
+/obj/effect/landmark{
+ name = "syndieprisonwarp"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"NI" = (
/obj/effect/decal/warning_stripes/north,
/turf/unsimulated/floor{
@@ -12243,6 +12439,12 @@
name = "plating"
},
/area/centcom/control)
+"NW" = (
+/turf/unsimulated/wall/fakeglass{
+ icon_state = "fakewindows";
+ dir = 1
+ },
+/area/syndicate_mothership/jail)
"Oh" = (
/obj/effect/decal/warning_stripes/southwest,
/turf/unsimulated/floor{
@@ -12256,6 +12458,18 @@
icon_state = "red"
},
/area/holodeck/source_basketball)
+"Om" = (
+/obj/machinery/door/airlock/hatch/syndicate{
+ autoclose = 0;
+ frequency = 1441;
+ id_tag = "syndicate_jail_airlock_internal";
+ locked = 1;
+ name = "Syndicate Jail Internal Airlock"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Oq" = (
/obj/effect/decal/warning_stripes/south,
/turf/unsimulated/floor{
@@ -12268,6 +12482,25 @@
name = "plating"
},
/area/centcom/control)
+"Ot" = (
+/obj/machinery/door/airlock/hatch/syndicate{
+ autoclose = 0;
+ frequency = 1441;
+ id_tag = "syndicate_jail_airlock_external";
+ locked = 1;
+ name = "Syndicate Jail External Airlock"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership)
+"Ov" = (
+/obj/structure/kitchenspike,
+/obj/effect/decal/cleanable/blood,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"OP" = (
/obj/effect/decal/warning_stripes/yellow/partial{
dir = 8
@@ -12293,12 +12526,25 @@
icon_state = "dark"
},
/area/centcom/specops)
+"Pr" = (
+/obj/structure/chair/office/dark,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"PJ" = (
/obj/effect/decal/warning_stripes/west,
/turf/unsimulated/floor{
name = "plating"
},
/area/centcom/specops)
+"Qc" = (
+/turf/unsimulated/wall/fakeglass{
+ tag = "icon-fakewindows (SOUTHEAST)";
+ icon_state = "fakewindows";
+ dir = 6
+ },
+/area/syndicate_mothership/jail)
"Qr" = (
/obj/effect/landmark/ai_multicam_room,
/turf/unsimulated/ai_visible,
@@ -12307,6 +12553,18 @@
/obj/machinery/poolcontroller/invisible,
/turf/unsimulated/beach/water,
/area/ninja/holding)
+"Qu" = (
+/obj/effect/decal/cleanable/blood,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
+"Qv" = (
+/obj/machinery/door/window/westleft,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"QB" = (
/obj/structure/table/holotable,
/obj/item/clothing/gloves/boxing/hologlove,
@@ -12327,6 +12585,17 @@
icon_state = "floor4"
},
/area/shuttle/escape)
+"QL" = (
+/obj/structure/table,
+/obj/item/kitchen/knife/butcher,
+/obj/item/melee/baseball_bat,
+/obj/item/clothing/mask/muzzle{
+ pixel_y = 8
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"QQ" = (
/turf/simulated/floor/holofloor{
tag = "icon-asteroid6";
@@ -12351,6 +12620,13 @@
icon_state = "dark"
},
/area/centcom/specops)
+"Ri" = (
+/turf/unsimulated/wall/fakeglass{
+ tag = "icon-fakewindows (NORTHEAST)";
+ icon_state = "fakewindows";
+ dir = 5
+ },
+/area/syndicate_mothership/jail)
"Rj" = (
/obj/structure/flora/ausbushes/sparsegrass,
/turf/simulated/floor/holofloor/grass,
@@ -12384,6 +12660,17 @@
icon_state = "red"
},
/area/holodeck/source_knightarena)
+"SI" = (
+/obj/machinery/door/airlock/hatch/syndicate{
+ damage_deflection = 75;
+ id_tag = "syndicate_jail";
+ locked = 1;
+ name = "Syndicate Jail"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"SY" = (
/obj/structure/table/holotable,
/obj/item/clothing/suit/armor/riot/knight/blue,
@@ -12525,9 +12812,22 @@
icon_state = "red"
},
/area/holodeck/source_thunderdomecourt)
+"UZ" = (
+/turf/unsimulated/wall/fakeglass{
+ icon_state = "fakewindows";
+ dir = 8
+ },
+/area/syndicate_mothership/jail)
"Vc" = (
/turf/unsimulated/ai_visible,
/area/ai_multicam_room)
+"Vm" = (
+/obj/structure/table/wood,
+/obj/item/storage/box/donkpockets,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Vz" = (
/turf/unsimulated/wall,
/area/ai_multicam_room)
@@ -12625,6 +12925,11 @@
icon_state = "floor4"
},
/area/centcom/evac)
+"WA" = (
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"WI" = (
/obj/structure/chair/comfy/shuttle{
dir = 8
@@ -12662,12 +12967,45 @@
icon_state = "floor"
},
/area/centcom)
+"Xq" = (
+/obj/structure/table,
+/obj/item/radio/electropack{
+ pixel_x = -5
+ },
+/obj/item/assembly/signaler{
+ code = 2;
+ frequency = 1449;
+ pixel_x = 8
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Xr" = (
/obj/structure/bed,
/turf/unsimulated/floor{
name = "plating"
},
/area/centcom)
+"XJ" = (
+/obj/machinery/light/spot{
+ tag = "icon-tube1 (WEST)";
+ icon_state = "tube1";
+ dir = 8
+ },
+/obj/machinery/access_button{
+ command = "cycle_interior";
+ frequency = 1441;
+ master_tag = "syndicate_jail_airlock_control";
+ name = "Syndicate Jail Access Button";
+ pixel_x = -24;
+ pixel_y = -8;
+ req_access_txt = "150"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"XL" = (
/obj/structure/table/abductor,
/obj/item/bonegel/alien,
@@ -12701,15 +13039,35 @@
"Yp" = (
/turf/simulated/floor/beach/water,
/area/holodeck/source_beach)
+"Yx" = (
+/obj/machinery/door/airlock/hatch/syndicate{
+ damage_deflection = 75;
+ id_tag = "syndicate_jail_cell";
+ locked = 1;
+ name = "Syndicate Jail Cell"
+ },
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
"Zv" = (
/turf/simulated/floor/beach/coastline,
/area/holodeck/source_beach)
+"ZE" = (
+/turf/unsimulated/wall/fakeglass,
+/area/syndicate_mothership/jail)
"ZR" = (
/turf/simulated/floor/holofloor{
dir = 10;
icon_state = "green"
},
/area/holodeck/source_emptycourt)
+"ZZ" = (
+/obj/structure/closet/secure_closet/contractor,
+/turf/unsimulated/floor{
+ icon_state = "dark"
+ },
+/area/syndicate_mothership/jail)
(1,1,1) = {"
ck
@@ -63927,8 +64285,8 @@ BG
kr
BG
nj
-kr
-kr
+mG
+mv
nj
aN
aN
@@ -64184,8 +64542,8 @@ nP
kr
nP
nj
-kr
-kr
+Ot
+Ot
nj
aN
aN
@@ -64441,8 +64799,8 @@ nj
nj
nj
nj
-nj
-nj
+kr
+kr
nj
aN
aN
@@ -64697,10 +65055,10 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
+nj
+DG
+mv
+nj
aN
aN
aN
@@ -64952,17 +65310,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Hr
+Hr
+Om
+GZ
+Hr
+Hr
+Hr
+Hr
+Hr
+Hr
aN
aN
aN
@@ -65209,17 +65567,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+WA
+XJ
+WA
+WA
+ZZ
+Hr
+Mo
+QL
+Xq
+Hr
aN
aN
aN
@@ -65466,17 +65824,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+WA
+WA
+WA
+WA
+WA
+Hr
+WA
+Qu
+WA
+Hr
aN
aN
aN
@@ -65723,17 +66081,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+CD
+Qv
+CD
+CD
+WA
+DY
+WA
+WA
+mF
+Hr
aN
aN
aN
@@ -65980,17 +66338,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Vm
+WA
+Pr
+CD
+WA
+Hr
+HU
+WA
+Qu
+Hr
aN
aN
aN
@@ -66237,17 +66595,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+KB
+WA
+Nv
+IA
+WA
+Hr
+Ov
+Cf
+Gu
+Hr
aN
aN
aN
@@ -66494,17 +66852,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Hr
+Hr
+Hr
+Hr
+SI
+Hr
+Hr
+Hr
+Hr
+Hr
aN
aN
aN
@@ -66751,17 +67109,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Gu
+MG
+Yx
+WA
+WA
+WA
+Yx
+MG
+Gu
+Hr
aN
aN
aN
@@ -67008,17 +67366,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+ED
+Qu
+UZ
+WA
+WA
+WA
+UZ
+WA
+ED
+Hr
aN
aN
aN
@@ -67265,17 +67623,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Hr
+NW
+Qc
+HN
+WA
+ip
+Ri
+ZE
+Hr
+Hr
aN
aN
aN
@@ -67522,17 +67880,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Gu
+MG
+Yx
+WA
+WA
+WA
+Yx
+NG
+Gu
+Hr
aN
aN
aN
@@ -67779,17 +68137,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+ED
+WA
+UZ
+WA
+WA
+WA
+UZ
+WA
+ED
+Hr
aN
aN
aN
@@ -68036,17 +68394,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Hr
+NW
+Qc
+HN
+WA
+ip
+Ri
+ZE
+Hr
+Hr
aN
aN
aN
@@ -68293,17 +68651,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Gu
+MG
+Yx
+Qu
+WA
+WA
+Yx
+MG
+Gu
+Hr
aN
aN
aN
@@ -68550,17 +68908,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+ED
+WA
+DL
+WA
+Iz
+WA
+DL
+WA
+ED
+Hr
aN
aN
aN
@@ -68807,17 +69165,17 @@ aN
aN
aN
aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
-aN
+Hr
+Hr
+Hr
+Hr
+Hr
+Hr
+Hr
+Hr
+Hr
+Hr
+Hr
aN
aN
aN
diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm
index b7e89e678ed..e50baddb7a0 100644
--- a/code/__DEFINES/combat.dm
+++ b/code/__DEFINES/combat.dm
@@ -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
diff --git a/code/__DEFINES/contractors.dm b/code/__DEFINES/contractors.dm
new file mode 100644
index 00000000000..f0390c4e91f
--- /dev/null
+++ b/code/__DEFINES/contractors.dm
@@ -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)
diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm
index 2de0a86c268..497d8349ce1 100644
--- a/code/__DEFINES/gamemode.dm
+++ b/code/__DEFINES/gamemode.dm
@@ -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))
diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm
index a1da66b1373..a5524d7c719 100644
--- a/code/__HELPERS/time.dm
+++ b/code/__HELPERS/time.dm
@@ -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)
diff --git a/code/_globalvars/mapping.dm b/code/_globalvars/mapping.dm
index e43062a18b7..2b435e0bede 100644
--- a/code/_globalvars/mapping.dm
+++ b/code/_globalvars/mapping.dm
@@ -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)
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 53e643f6e55..5f2b39d1aea 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -346,6 +346,57 @@
. += "traitor|NO"
. += _memory_edit_role_enabled(ROLE_TRAITOR)
+ // Contractor
+ . += "
contractor: "
+ var/datum/antagonist/traitor/contractor/C = has_antag_datum(/datum/antagonist/traitor/contractor)
+ if(C)
+ var/status
+ if(C.contractor_uplink) // Offer accepted
+ status = "CONTRACTOR"
+ else if(world.time >= C.offer_deadline)
+ status = "CONTRACTOR (EXPIRED)"
+ else
+ status = "CONTRACTOR (PENDING)"
+ . += "[status]|no"
+ // List all their contracts
+ if(C.contractor_uplink)
+ . += "
Contracts:"
+ if(C.contractor_uplink.hub.contracts)
+ var/count = 1
+ for(var/co in C.contractor_uplink.hub.contracts)
+ var/datum/syndicate_contract/CO = co
+ . += "
Contract #[count++]: "
+ . += "[CO.contract.target?.name || "Invalid target!"]|"
+ . += "locations|"
+ . += "more|"
+ switch(CO.status)
+ if(CONTRACT_STATUS_INVALID)
+ . += "INVALID"
+ if(CONTRACT_STATUS_INACTIVE)
+ . += "inactive"
+ if(CONTRACT_STATUS_ACTIVE)
+ . += "ACTIVE|"
+ . += "interrupt|"
+ . += "fail"
+ if(CONTRACT_STATUS_COMPLETED)
+ . += "COMPLETED"
+ if(CONTRACT_STATUS_FAILED)
+ . += "FAILED"
+ . += "
"
+ . += "Add Contract
"
+ . += "Claimable TC: [C.contractor_uplink.hub.reward_tc_available]
"
+ . += "Available Rep: [C.contractor_uplink.hub.rep]
"
+ else
+ . += "
"
+ . += "Has not logged in to contractor uplink"
+ else
+ if(has_antag_datum(/datum/antagonist/traitor))
+ if(find_syndicate_uplink())
+ . += "contractor|NO"
+ else
+ . += "contractor|NO|No Uplink"
+ else
+ . += "contractor|NO"
// Mindslave
. += "
mindslaved: "
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, "You are no longer a Contractor!")
+ 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")
diff --git a/code/game/area/ss13_areas.dm b/code/game/area/ss13_areas.dm
index 2d41414ebfc..4e75ae5c577 100644
--- a/code/game/area/ss13_areas.dm
+++ b/code/game/area/ss13_areas.dm
@@ -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
diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index f3d5f1f2737..dc7fd5e6239 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -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, "
You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!")
+ 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
diff --git a/code/game/gamemodes/traitor/traitor.dm b/code/game/gamemodes/traitor/traitor.dm
index e7c5b4541a8..c81758f62c8 100644
--- a/code/game/gamemodes/traitor/traitor.dm
+++ b/code/game/gamemodes/traitor/traitor.dm
@@ -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, "The current game mode is - Traitor!")
@@ -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 ? "[A.map_name]" : A.map_name)
+ var/display_locations = english_list(locations, and_text = " or ")
+ // Result
+ var/result = ""
+ if(C.status == CONTRACT_STATUS_COMPLETED)
+ result = "Success!"
+ else if(C.status != CONTRACT_STATUS_INACTIVE)
+ result = "Fail."
+ text += "
Contract #[count]: Kidnap and extract [C.target_name] at [display_locations]. [result]"
+ count++
+ text += "
[earned_tc] TC were earned from the contracts."
if(traitorwin)
text += "
The [special_role_text] was successful!
"
diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm
index 7e58cb992a7..342833c06f4 100644
--- a/code/game/machinery/cryopod.dm
+++ b/code/game/machinery/cryopod.dm
@@ -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, "
You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!")
- 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
diff --git a/code/game/objects/effects/decals/contraband.dm b/code/game/objects/effects/decals/contraband.dm
index da03aa51837..0f34aa95b71 100644
--- a/code/game/objects/effects/decals/contraband.dm
+++ b/code/game/objects/effects/decals/contraband.dm
@@ -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 ###########################
diff --git a/code/game/objects/effects/landmarks.dm b/code/game/objects/effects/landmarks.dm
index e90af21cca2..0f373bfe851 100644
--- a/code/game/objects/effects/landmarks.dm
+++ b/code/game/objects/effects/landmarks.dm
@@ -40,6 +40,10 @@
GLOB.prisonwarp += loc
qdel(src)
+ if("syndieprisonwarp")
+ GLOB.syndieprisonwarp += loc
+ qdel(src)
+
if("prisonsecuritywarp")
GLOB.prisonsecuritywarp += loc
qdel(src)
diff --git a/code/game/objects/items/devices/uplinks.dm b/code/game/objects/items/devices/uplinks.dm
index 2cdb80cc697..60abf1774cd 100644
--- a/code/game/objects/items/devices/uplinks.dm
+++ b/code/game/objects/items/devices/uplinks.dm
@@ -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, "[src] seems to be jammed - it cannot be used here!")
+ 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, "[src] seems to be jammed - it cannot be used here!")
+ 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.
diff --git a/code/game/objects/items/weapons/batons.dm b/code/game/objects/items/weapons/batons.dm
index 34d2bdd7ac8..4b369a9a552 100644
--- a/code/game/objects/items/weapons/batons.dm
+++ b/code/game/objects/items/weapons/batons.dm
@@ -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("[user] pulses [target]'s sensors with [src]!",\
+ "You pulse [target]'s sensors with [src]!")
+ 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("[user] has knocked down [target] with \the [src]!", \
- "[user] has knocked down [target] with \the [src]!")
- // 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("[user] knocks down [target] with [src]!",\
+ "You knock down [target] with [src]!")
+ 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, "You extend the baton.")
- item_state = "nullrod"
+ to_chat(user, "You extend [src].")
+ 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, "You collapse the baton.")
+ to_chat(user, "You collapse [src].")
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)
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 2d338825f22..d91a6c89532 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -270,6 +270,9 @@ GLOBAL_VAR_INIT(nologevent, 0)
Thunderdome 2 |
Thunderdome Admin |
Thunderdome Observer |
+ Stop Syndicate Jail Timer |
+ Start Syndicate Jail Timer |
+ Release now from Syndicate Jail |
"}
body += {"
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index cae09b224d2..8d33a760884 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -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, "This can only be used on instances of type /mob.")
+ return
+
+ var/datum/syndicate_contract/contract = LAZYACCESS(GLOB.prisoner_belongings.prisoners, M)
+ if(!contract)
+ to_chat(usr, "[M] is currently not imprisoned by the Syndicate.")
+ return
+ if(!contract.prisoner_timer_handle)
+ to_chat(usr, "[M] is already NOT scheduled to return from the Syndicate Jail.")
+ 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, "This can only be used on instances of type /mob.")
+ return
+
+ var/datum/syndicate_contract/contract = LAZYACCESS(GLOB.prisoner_belongings.prisoners, M)
+ if(!contract)
+ to_chat(usr, "[M] is currently not imprisoned by the Syndicate.")
+ return
+ if(contract.prisoner_timer_handle)
+ to_chat(usr, "[M] is already scheduled to return from the Syndicate Jail.")
+ 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, "This can only be used on instances of type /mob.")
+ return
+
+ var/datum/syndicate_contract/contract = LAZYACCESS(GLOB.prisoner_belongings.prisoners, M)
+ if(!contract)
+ to_chat(usr, "[M] is currently not imprisoned by the Syndicate.")
+ 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
diff --git a/code/modules/antagonists/traitor/contractor/datums/contractor.dm b/code/modules/antagonists/traitor/contractor/datums/contractor.dm
new file mode 100644
index 00000000000..d1e9d0c729b
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/contractor.dm
@@ -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.
"\
+ + "If you are interested, simply access your hidden uplink and select the \"Contracting Opportunity\" tab for more information.
"
+ to_chat(owner.current, "You have been offered a chance to become a Contractor.
")
+ to_chat(owner.current, "[greet_text]")
+ to_chat(owner.current, "This offer will expire in 10 minutes starting now (expiry time: [station_time_timestamp(time = offer_deadline)]).")
+
+/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, "You can no longer become a contractor as [reason].")
+ 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
diff --git a/code/modules/antagonists/traitor/contractor/datums/contractor_hub.dm b/code/modules/antagonists/traitor/contractor/datums/contractor_hub.dm
new file mode 100644
index 00000000000..1fb55a22ad2
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/contractor_hub.dm
@@ -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, "Your payment materializes into your hands!")
+ else
+ to_chat(M, "Your payment materializes on the floor.")
+ // 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)
diff --git a/code/modules/antagonists/traitor/contractor/datums/contractor_hub_ui.dm b/code/modules/antagonists/traitor/contractor/datums/contractor_hub_ui.dm
new file mode 100644
index 00000000000..d371db31d96
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/contractor_hub_ui.dm
@@ -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, "[error_message]")
+ 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
diff --git a/code/modules/antagonists/traitor/contractor/datums/objective_contract.dm b/code/modules/antagonists/traitor/contractor/datums/objective_contract.dm
new file mode 100644
index 00000000000..91188ae0cab
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/objective_contract.dm
@@ -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()
+ 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
diff --git a/code/modules/antagonists/traitor/contractor/datums/rep_purchases/_base.dm b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/_base.dm
new file mode 100644
index 00000000000..559ededd54c
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/_base.dm
@@ -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, "You were not recognized as this hub's original user.")
+ return
+ if(hub.rep < cost)
+ to_chat(user, "You do not have enough Rep.")
+ return
+ if(stock == 0)
+ to_chat(user, "This item is out of stock.")
+ 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)
diff --git a/code/modules/antagonists/traitor/contractor/datums/rep_purchases/balloon.dm b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/balloon.dm
new file mode 100644
index 00000000000..217136766ce
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/balloon.dm
@@ -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, "All of your contracts must be completed in the hardest location to be eligible for this item.")
+ 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"
diff --git a/code/modules/antagonists/traitor/contractor/datums/rep_purchases/blackout.dm b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/blackout.dm
new file mode 100644
index 00000000000..b1bab21d429
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/blackout.dm
@@ -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, "Another blackout may not be requested for [seconds_to_clock(timeleft)].")
+ 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()
diff --git a/code/modules/antagonists/traitor/contractor/datums/rep_purchases/fulton.dm b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/fulton.dm
new file mode 100644
index 00000000000..34b23c5b933
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/fulton.dm
@@ -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)
diff --git a/code/modules/antagonists/traitor/contractor/datums/rep_purchases/pinpointer.dm b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/pinpointer.dm
new file mode 100644
index 00000000000..94e35bbcae3
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/pinpointer.dm
@@ -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
diff --git a/code/modules/antagonists/traitor/contractor/datums/rep_purchases/reroll.dm b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/reroll.dm
new file mode 100644
index 00000000000..117b45ca31a
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/reroll.dm
@@ -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, "There are no inactive contracts that can be rerolled.")
+ 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.")
diff --git a/code/modules/antagonists/traitor/contractor/datums/rep_purchases/zippo.dm b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/zippo.dm
new file mode 100644
index 00000000000..adf3af170dd
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/rep_purchases/zippo.dm
@@ -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"
diff --git a/code/modules/antagonists/traitor/contractor/datums/syndicate_contract.dm b/code/modules/antagonists/traitor/contractor/datums/syndicate_contract.dm
new file mode 100644
index 00000000000..7a1354e57be
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/datums/syndicate_contract.dm
@@ -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, "You already have an ongoing contract!")
+ 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("[M] starts entering a cryptic series of characters on [U].",\
+ "You start entering an extraction signal to your handlers on [U]...")
+ 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("[M] enters a mysterious code on [U] and pulls a black and gold flare from [M.p_their()] belongings before lighting it.",\
+ "You finish entering the signal on [U] and light an extraction flare, initiating the extraction process.")
+ 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, "You feel strange...")
+ 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, "That portal did something to you...")
+
+ sleep(6.5 SECONDS)
+ to_chat(M, "Your head pounds... It feels like it's going to burst out your skull!")
+
+ sleep(3 SECONDS)
+ to_chat(M, "Your head pounds...")
+
+ sleep(10 SECONDS)
+ to_chat(M, "A million voices echo in your head... \"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...\"")
+
+/**
+ * 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, "Your captors left you a souvenir for your troubles!")
+ var/obj/item/souvenir = pick(souvenirs)
+ new souvenir(closet)
+ else if(prob(RETURN_BRUISE_CHANCE) && M.health >= 50)
+ to_chat(M, "You were roughed up a little by your captors before being sent back!")
+ M.adjustBruteLoss(RETURN_BRUISE_DAMAGE)
+
+ // Return them a bit confused.
+ M.visible_message("[M] vanishes...")
+ 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
diff --git a/code/modules/antagonists/traitor/contractor/items/contractor_baton.dm b/code/modules/antagonists/traitor/contractor/items/contractor_baton.dm
new file mode 100644
index 00000000000..cfbce2e69d5
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/items/contractor_baton.dm
@@ -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)
diff --git a/code/modules/antagonists/traitor/contractor/items/contractor_kit.dm b/code/modules/antagonists/traitor/contractor/items/contractor_kit.dm
new file mode 100644
index 00000000000..3dac9a97469
--- /dev/null
+++ b/code/modules/antagonists/traitor/contractor/items/contractor_kit.dm
@@ -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 = {"
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.
+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.
+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.
+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.
+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.
+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.
+Good luck agent. You can burn this document with the supplied lighter.
"} + + return ..() diff --git a/code/modules/antagonists/traitor/contractor/items/contractor_pinpointer.dm b/code/modules/antagonists/traitor/contractor/items/contractor_pinpointer.dm new file mode 100644 index 00000000000..205d954db90 --- /dev/null +++ b/code/modules/antagonists/traitor/contractor/items/contractor_pinpointer.dm @@ -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, "[src] refuses to do anything.") + return + else + owner = user + to_chat(user, "[src] now recognizes you as its sole user.") + return ..() diff --git a/code/modules/antagonists/traitor/contractor/items/contractor_uplink.dm b/code/modules/antagonists/traitor/contractor/items/contractor_uplink.dm new file mode 100644 index 00000000000..e3798896d9a --- /dev/null +++ b/code/modules/antagonists/traitor/contractor/items/contractor_uplink.dm @@ -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, "[bicon(src)] Incoming encrypted transmission from your handlers. Message as follows: