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.

+

Using the Contractor Uplink

+
    +
  1. Take the contractor uplink from this kit and activate it.
  2. +
  3. From there, you can accept a contract, and redeem your TC payments from completed contracts.
  4. +
  5. The payment number shown in brackets is the bonus you'll receive when bringing your target alive. You receive the + other number regardless of whether they were alive or not.
  6. +
  7. Contracts are completed by bringing the target to the designated extraction zone, calling for extraction, and putting them + inside the extraction portal.
  8. +
+

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.

+

Extracting

+
    +
  1. Make sure both yourself and your target are at the extraction zone.
  2. +
  3. Call the extraction, and stand back from the drop point.
  4. +
  5. If it fails, make sure your target is inside, and there's a free space for the extraction portal to appear.
  6. +
  7. Grab your target, and drag them into the extraction portal.
  8. +
+

Ransoms

+

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:
"\ + + "[text]") + if(sndfile) + M.playsound_local(get_turf(M), sndfile, 30, FALSE) diff --git a/code/modules/antagonists/traitor/contractor/items/extraction_items.dm b/code/modules/antagonists/traitor/contractor/items/extraction_items.dm new file mode 100644 index 00000000000..2b493fb6cd5 --- /dev/null +++ b/code/modules/antagonists/traitor/contractor/items/extraction_items.dm @@ -0,0 +1,120 @@ +/** + * # Contractor Extraction Flare + * + * Used to designate where the [/obj/effect/portal/redspace/contractor] should spawn during the extraction process. + */ +/obj/effect/contractor_flare + name = "contractor extraction flare" + icon = 'icons/obj/lighting.dmi' + icon_state = "flare-contractor-on" + +/obj/effect/contractor_flare/New() + ..() + playsound(loc, 'sound/goonstation/misc/matchstick_light.ogg', 50, TRUE) + set_light(8, l_color = "#FFD165") + +/obj/effect/contractor_flare/Destroy() + new /obj/effect/decal/cleanable/ash(loc) + return ..() + +/** + * # Contractor Extraction Portal + * + * Used to extract contract targets and send them to the Syndicate jail for a few minutes. + */ +/obj/effect/portal/redspace/contractor + name = "suspicious portal" + icon_state = "portal-syndicate" + /// The contract associated with this portal. + var/datum/syndicate_contract/contract = null + /// The mind of the contractor. Used to tell them they shouldn't be taking the portal. + var/datum/mind/contractor_mind = null + /// The mind of the kidnapping target. Prevents non-targets from taking the portal. + var/datum/mind/target_mind = null + +/obj/effect/portal/redspace/contractor/can_teleport(atom/movable/A) + var/mob/living/M = A + if(!istype(M)) + return FALSE + if(M == usr && M.mind == contractor_mind) + to_chat(M, "The portal is here to extract the contract target, not you!") + return FALSE + if(M.mind != target_mind) + if(usr?.mind == contractor_mind) // Contractor shoving a non-target into the portal + to_chat(M, "Somehow you are not sure [M] is the target you have to kidnap.") + return FALSE + else if(usr == M) // Non-target trying to enter the portal + to_chat(M, "Somehow you are not sure this is a good idea.") + return FALSE + return FALSE + return ..() + +/obj/effect/portal/redspace/contractor/teleport(atom/movable/M) + . = ..() + if(.) + contract.target_received(M, src) + +/** + * # Prisoner Belongings Closet + * + * Cannot be opened. Contains the belongings of all kidnapped targets. + * Any item added inside stops processing and starts again when removed. + */ +/obj/structure/closet/secure_closet/contractor + anchored = TRUE + can_be_emaged = FALSE + max_integrity = INFINITY + /// Lazy list of atoms which should process again when taken out. + var/list/atom/suspended_items = null + /// Lazy, associative list of prisoners being held as part of a contract. + /// Structure: [/mob/living] => [/datum/syndicate_contract] + var/list/prisoners = null + +/obj/structure/closet/secure_closet/contractor/New() + ..() + if(!GLOB.prisoner_belongings) + GLOB.prisoner_belongings = src + +/obj/structure/closet/secure_closet/contractor/allowed(mob/M) + return FALSE + +/** + * Tries to add an atom for temporary holding, suspending its processing. + * + * Arguments: + * * A - The atom to add. + */ +/obj/structure/closet/secure_closet/contractor/proc/give_item(atom/A) + if(ismob(A)) // No mobs allowed + return FALSE + var/obj/item/I = A + if(!istype(I)) + return FALSE + if(I.isprocessing) + LAZYSET(suspended_items, I.UID(), list(I, (I in SSfastprocess.processing))) + STOP_PROCESSING(SSobj, I) + I.loc = src // No forceMove because we don't want to trigger anything here + return TRUE + +/** + * Removes an atom from temporary holding. + * + * Arguments: + * * A - The atom to remove. + */ +/obj/structure/closet/secure_closet/contractor/proc/remove_item(atom/A) + if(!(A in contents)) + return + var/obj/item/I = A + if(!istype(I)) + return FALSE + // Resume processing if it was paused + var/list/tuple = LAZYACCESS(suspended_items, I.UID()) + if(tuple) + if(tuple[2]) + START_PROCESSING(SSfastprocess, I) + else + START_PROCESSING(SSobj, I) + suspended_items[I.UID()] = null + I.loc = loc // No forceMove because we don't want to trigger anything here + return I diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 0368957ac11..b1599d5dfd0 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -12,7 +12,8 @@ var/should_equip = TRUE var/traitor_kind = TRAITOR_HUMAN var/list/assigned_targets = list() // This includes assassinate as well as steal objectives. prevents duplicate objectives - + /// Whether the traitor can specialize into a contractor. + var/is_contractor = FALSE /datum/antagonist/traitor/on_gain() if(owner.current && isAI(owner.current)) @@ -410,3 +411,8 @@ The code responses were: [responses]
" return message + +/datum/antagonist/traitor/specialization(datum/mind/new_owner) + if(isAI(new_owner?.current) || !is_contractor) + return ..() + return new /datum/antagonist/traitor/contractor diff --git a/code/modules/clothing/spacesuits/syndi.dm b/code/modules/clothing/spacesuits/syndi.dm index 890c223d735..72d7bfc01a6 100644 --- a/code/modules/clothing/spacesuits/syndi.dm +++ b/code/modules/clothing/spacesuits/syndi.dm @@ -186,3 +186,18 @@ name = "Black Engineering Space Suit" icon_state = "syndicate-black-engie" item_state = "syndicate-black" + +/obj/item/clothing/head/helmet/space/syndicate/contractor + name = "contractor helmet" + desc = "A specialised black and gold helmet that's more compact than its standard Syndicate counterpart. Can be ultra-compressed into even the tightest of spaces." + w_class = WEIGHT_CLASS_SMALL + icon_state = "syndicate-helm-contractor" + item_state = "syndicate-helm-contractor" + +/obj/item/clothing/suit/space/syndicate/contractor + name = "contractor space suit" + desc = "A specialised black and gold space suit that's quicker, and more compact than its standard Syndicate counterpart. Can be ultra-compressed into even the tightest of spaces." + slowdown = 1 + w_class = WEIGHT_CLASS_SMALL + icon_state = "syndicate-contractor" + item_state = "syndicate-contractor" diff --git a/code/modules/mining/lavaland/loot/colossus_loot.dm b/code/modules/mining/lavaland/loot/colossus_loot.dm index 6ee649c549c..0835104cae6 100644 --- a/code/modules/mining/lavaland/loot/colossus_loot.dm +++ b/code/modules/mining/lavaland/loot/colossus_loot.dm @@ -306,7 +306,7 @@ cooldown_add = 50 activation_sound = 'sound/magic/timeparadox2.ogg' var/list/banned_items_typecache = list(/obj/item/storage, /obj/item/implant, /obj/item/implanter, /obj/item/disk/nuclear, - /obj/item/projectile, /obj/item/spellbook, /obj/item/clothing/mask/facehugger) + /obj/item/projectile, /obj/item/spellbook, /obj/item/clothing/mask/facehugger, /obj/item/contractor_uplink) /obj/machinery/anomalous_crystal/refresher/New() ..() diff --git a/code/modules/surgery/organs/augments_internal.dm b/code/modules/surgery/organs/augments_internal.dm index e4fad849d4b..53090e761cb 100644 --- a/code/modules/surgery/organs/augments_internal.dm +++ b/code/modules/surgery/organs/augments_internal.dm @@ -115,10 +115,9 @@ r_hand_obj.flags ^= NODROP /obj/item/organ/internal/cyberimp/brain/anti_drop/remove(var/mob/living/carbon/M, special = 0) - . = ..() if(active) ui_action_click() - + return ..() /obj/item/organ/internal/cyberimp/brain/anti_stun name = "CNS Rebooter implant" diff --git a/icons/mob/head.dmi b/icons/mob/head.dmi index b9b855a3800..aeeefde2961 100644 Binary files a/icons/mob/head.dmi and b/icons/mob/head.dmi differ diff --git a/icons/mob/hud.dmi b/icons/mob/hud.dmi index 712a05e87df..b49ec4af515 100644 Binary files a/icons/mob/hud.dmi and b/icons/mob/hud.dmi differ diff --git a/icons/mob/inhands/clothing_lefthand.dmi b/icons/mob/inhands/clothing_lefthand.dmi index 9fce742e0bf..b1f406da718 100644 Binary files a/icons/mob/inhands/clothing_lefthand.dmi and b/icons/mob/inhands/clothing_lefthand.dmi differ diff --git a/icons/mob/inhands/clothing_righthand.dmi b/icons/mob/inhands/clothing_righthand.dmi index 772f8af73e4..8e9bdfc6200 100644 Binary files a/icons/mob/inhands/clothing_righthand.dmi and b/icons/mob/inhands/clothing_righthand.dmi differ diff --git a/icons/mob/inhands/items_lefthand.dmi b/icons/mob/inhands/items_lefthand.dmi index f8195652dca..94a7f849a41 100644 Binary files a/icons/mob/inhands/items_lefthand.dmi and b/icons/mob/inhands/items_lefthand.dmi differ diff --git a/icons/mob/inhands/items_righthand.dmi b/icons/mob/inhands/items_righthand.dmi index 5520ac724b3..cfd70ebef1f 100644 Binary files a/icons/mob/inhands/items_righthand.dmi and b/icons/mob/inhands/items_righthand.dmi differ diff --git a/icons/mob/species/grey/helmet.dmi b/icons/mob/species/grey/helmet.dmi index 3e592369adc..e4ee286b4fd 100644 Binary files a/icons/mob/species/grey/helmet.dmi and b/icons/mob/species/grey/helmet.dmi differ diff --git a/icons/mob/species/tajaran/helmet.dmi b/icons/mob/species/tajaran/helmet.dmi index 00c3f1d2bdc..f9dc95207d8 100644 Binary files a/icons/mob/species/tajaran/helmet.dmi and b/icons/mob/species/tajaran/helmet.dmi differ diff --git a/icons/mob/species/tajaran/suit.dmi b/icons/mob/species/tajaran/suit.dmi index 61929dcceda..56072a30846 100644 Binary files a/icons/mob/species/tajaran/suit.dmi and b/icons/mob/species/tajaran/suit.dmi differ diff --git a/icons/mob/species/unathi/helmet.dmi b/icons/mob/species/unathi/helmet.dmi index 68f6171bb19..6b59a230bee 100644 Binary files a/icons/mob/species/unathi/helmet.dmi and b/icons/mob/species/unathi/helmet.dmi differ diff --git a/icons/mob/species/unathi/suit.dmi b/icons/mob/species/unathi/suit.dmi index eabd0f94672..fb63cff9d86 100644 Binary files a/icons/mob/species/unathi/suit.dmi and b/icons/mob/species/unathi/suit.dmi differ diff --git a/icons/mob/species/vox/helmet.dmi b/icons/mob/species/vox/helmet.dmi index 7aba8b0d3d8..8a68ef99955 100644 Binary files a/icons/mob/species/vox/helmet.dmi and b/icons/mob/species/vox/helmet.dmi differ diff --git a/icons/mob/species/vox/suit.dmi b/icons/mob/species/vox/suit.dmi index c0b9b07fea3..7ababa87c89 100644 Binary files a/icons/mob/species/vox/suit.dmi and b/icons/mob/species/vox/suit.dmi differ diff --git a/icons/mob/species/vulpkanin/helmet.dmi b/icons/mob/species/vulpkanin/helmet.dmi index 4f346411738..1bf2165c156 100644 Binary files a/icons/mob/species/vulpkanin/helmet.dmi and b/icons/mob/species/vulpkanin/helmet.dmi differ diff --git a/icons/mob/species/vulpkanin/suit.dmi b/icons/mob/species/vulpkanin/suit.dmi index f871c7c5374..a063a5e6c17 100644 Binary files a/icons/mob/species/vulpkanin/suit.dmi and b/icons/mob/species/vulpkanin/suit.dmi differ diff --git a/icons/mob/suit.dmi b/icons/mob/suit.dmi index 505358dd870..3e19b87cdd8 100644 Binary files a/icons/mob/suit.dmi and b/icons/mob/suit.dmi differ diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi index a8961290254..4113e4edc0c 100644 Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ diff --git a/icons/obj/clothing/suits.dmi b/icons/obj/clothing/suits.dmi index 75235804d21..457771bdfff 100644 Binary files a/icons/obj/clothing/suits.dmi and b/icons/obj/clothing/suits.dmi differ diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi index a5fdfa24d1d..6cf80471a0b 100644 Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ diff --git a/icons/obj/items.dmi b/icons/obj/items.dmi index e30f3e44392..6f831bdbd93 100644 Binary files a/icons/obj/items.dmi and b/icons/obj/items.dmi differ diff --git a/icons/obj/lighting.dmi b/icons/obj/lighting.dmi index 49b594c1656..19b4d85a1d8 100644 Binary files a/icons/obj/lighting.dmi and b/icons/obj/lighting.dmi differ diff --git a/icons/obj/stationobjs.dmi b/icons/obj/stationobjs.dmi index a912acc2fdf..9c3e072c74d 100755 Binary files a/icons/obj/stationobjs.dmi and b/icons/obj/stationobjs.dmi differ diff --git a/paradise.dme b/paradise.dme index 9f0ad5a0155..ff34ee438ef 100644 --- a/paradise.dme +++ b/paradise.dme @@ -31,6 +31,7 @@ #include "code\__DEFINES\colors.dm" #include "code\__DEFINES\combat.dm" #include "code\__DEFINES\construction.dm" +#include "code\__DEFINES\contractors.dm" #include "code\__DEFINES\contracts.dm" #include "code\__DEFINES\crafting.dm" #include "code\__DEFINES\criminal_status.dm" @@ -1269,6 +1270,23 @@ #include "code\modules\antagonists\survivalist\survivalist.dm" #include "code\modules\antagonists\traitor\datum_mindslave.dm" #include "code\modules\antagonists\traitor\datum_traitor.dm" +#include "code\modules\antagonists\traitor\contractor\datums\contractor.dm" +#include "code\modules\antagonists\traitor\contractor\datums\contractor_hub.dm" +#include "code\modules\antagonists\traitor\contractor\datums\contractor_hub_ui.dm" +#include "code\modules\antagonists\traitor\contractor\datums\objective_contract.dm" +#include "code\modules\antagonists\traitor\contractor\datums\syndicate_contract.dm" +#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\_base.dm" +#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\balloon.dm" +#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\blackout.dm" +#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\fulton.dm" +#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\pinpointer.dm" +#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\reroll.dm" +#include "code\modules\antagonists\traitor\contractor\datums\rep_purchases\zippo.dm" +#include "code\modules\antagonists\traitor\contractor\items\contractor_baton.dm" +#include "code\modules\antagonists\traitor\contractor\items\contractor_kit.dm" +#include "code\modules\antagonists\traitor\contractor\items\contractor_pinpointer.dm" +#include "code\modules\antagonists\traitor\contractor\items\contractor_uplink.dm" +#include "code\modules\antagonists\traitor\contractor\items\extraction_items.dm" #include "code\modules\antagonists\wishgranter\wishgranter.dm" #include "code\modules\arcade\arcade_base.dm" #include "code\modules\arcade\arcade_prize.dm" diff --git a/sound/effects/confirmdropoff.ogg b/sound/effects/confirmdropoff.ogg new file mode 100644 index 00000000000..835d9319921 Binary files /dev/null and b/sound/effects/confirmdropoff.ogg differ diff --git a/sound/effects/contractstartup.ogg b/sound/effects/contractstartup.ogg new file mode 100644 index 00000000000..7d866460507 Binary files /dev/null and b/sound/effects/contractstartup.ogg differ diff --git a/sound/weapons/contractorbatonextend.ogg b/sound/weapons/contractorbatonextend.ogg new file mode 100644 index 00000000000..c78281bf1f9 Binary files /dev/null and b/sound/weapons/contractorbatonextend.ogg differ diff --git a/sound/weapons/contractorbatonhit.ogg b/sound/weapons/contractorbatonhit.ogg new file mode 100644 index 00000000000..2377267cc71 Binary files /dev/null and b/sound/weapons/contractorbatonhit.ogg differ diff --git a/strings/syndicate_wanted_messages.json b/strings/syndicate_wanted_messages.json new file mode 100644 index 00000000000..d31ed01c658 --- /dev/null +++ b/strings/syndicate_wanted_messages.json @@ -0,0 +1,108 @@ +{ + "basemessage": [ + "Fugitive from the law due to", + "Needs to be interrogated for information about", + "Wanted by The Syndicate for", + "Ransomable to Nanotrasen for", + "Has exploitable information about" + ], + "verb": [ + "murdering", + "killing", + "accidentally destroying", + "destroying", + "knowing information about", + "stealing", + "slipping", + "sabotaging", + "robusting", + "collaborating with", + "being close friends with", + "cloning", + "befriending", + "bombing", + "kidnapping", + "pretending to be", + "seducing", + "ignoring", + "assassinating", + "drinking", + "hating", + "slapping", + "swatting", + "skreeing at", + "toolboxing", + "swirling", + "wearing", + "[REDACTED]" + ], + "noun": { + "secret plans": 50, + "the hand teleporter": 50, + "an NT CentCom uniform": 50, + "a supermatter shard": 50, + "internal Syndicate documents": 50, + "experimental Nanotrasen technology": 50, + "bluespace crystals": 50, + "a cult": 50, + "shapeshifting creatures": 50, + "a bluespace artillery": 50, + "a DNA vault": 50, + "a toolbox": 10, + "a bar of soap": 10, + "Ian": 10, + "a Diona": 10, + "a Drask": 10, + "a Golem": 10, + "a Grey": 10, + "a Human": 10, + "a Kidan": 10, + "a Monkey": 10, + "a Nucleation": 10, + "a Plasmaman": 10, + "a Shadow": 10, + "a Skeleton": 10, + "a Skrell": 10, + "a Slime": 10, + "a Slime Person": 10, + "a Tajaran": 10, + "a Vox": 10, + "a Vulpkanin": 10, + "a Wryn": 10, + "an IPC": 10, + "an Unathi": 10, + "[REDACTED]": 10, + "two million credits": 10, + "one billion credits": 5, + "research on floor cluwnes": 5, + "Officer Beepsky": 5, + "clown tears": 5, + "Nanotrasen's swimsuit calendar": 5, + "a suspicious bus": 5, + "cat ears": 5, + "the anime archive": 5, + "the CMO's cat": 5, + "WetSkrell.nt": 5, + "YALPER": 1, + "absolutely nothing": 1, + "Cuban Pete": 1, + "your mother": 1, + "four cups of coffee a day": 1, + "ducks": 1 + }, + "location": { + "on NSS Cyberiad": 50, + "on NSS Kerberos": 50, + "on NSS Cerebron": 50, + "somewhere in deep space": 50, + "on a remote syndicate base": 50, + "in a secure NT facility": 50, + "while on an escape shuttle": 10, + "while infiltrating CentCom headquarters": 10, + "deep in the necropolis": 10, + "in [REDACTED]": 10, + "during a drunken bar fight": 5, + "while stuck in a bathroom": 5, + "in a back alley on Mars": 5 + } +} diff --git a/tgui/packages/tgui/components/Section.js b/tgui/packages/tgui/components/Section.js index 5c0d9dd4748..6aa1dea9fd4 100644 --- a/tgui/packages/tgui/components/Section.js +++ b/tgui/packages/tgui/components/Section.js @@ -11,7 +11,6 @@ export const Section = props => { stretchContents, noTopPadding, children, - scrollable, ...rest } = props; const hasTitle = !isFalsy(title) || !isFalsy(buttons); diff --git a/tgui/packages/tgui/interfaces/Contractor.js b/tgui/packages/tgui/interfaces/Contractor.js new file mode 100644 index 00000000000..ba4feb76884 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Contractor.js @@ -0,0 +1,428 @@ +import { Component, Fragment } from 'inferno'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Flex, Icon, LabeledList, Modal, Section, Tabs } from '../components'; +import { Countdown } from '../components/Countdown'; +import { Window } from '../layouts'; + +const contractStatuses = { + 1: ["ACTIVE", "good"], + 2: ["COMPLETED", "good"], + 3: ["FAILED", "bad"], +}; + +// Lifted from /tg/station +const terminalMessages = [ + "Recording biometric data...", + "Analyzing embedded syndicate info...", + "STATUS CONFIRMED", + "Contacting Syndicate database...", + "Awaiting response...", + "Awaiting response...", + "Awaiting response...", + "Awaiting response...", + "Awaiting response...", + "Awaiting response...", + "Response received, ack 4851234...", + "CONFIRM ACC " + (Math.round(Math.random() * 20000)), + "Setting up private accounts...", + "CONTRACTOR ACCOUNT CREATED", + "Searching for available contracts...", + "Searching for available contracts...", + "Searching for available contracts...", + "Searching for available contracts...", + "CONTRACTS FOUND", + "WELCOME, AGENT", +]; + +export const Contractor = (properties, context) => { + const { act, data } = useBackend(context); + let body; + if (data.unauthorized) { + body = ( + + {}} + /> + + ); + } else if (!data.load_animation_completed) { + body = ( + + act('complete_load_animation')} + /> + + ); + } else { + body = ( + + + + + + + + + {data.page === 1 ? ( + + ) : ( + + )} + + + ); + } + const [viewingPhoto, _setViewingPhoto] = useLocalState(context, "viewingPhoto", ""); + return ( + + {viewingPhoto && ( + + )} + + + {body} + + + + ); +}; + +const Summary = (properties, context) => { + const { act, data } = useBackend(context); + const { + tc_available, + tc_paid_out, + completed_contracts, + rep, + } = data; + return ( +
+ {rep} Rep + + } + {...properties}> + + + + + + + {tc_available} TC + +
+ ); +}; + +const Navigation = (properties, context) => { + const { act, data } = useBackend(context); + const { + page, + } = data; + return ( + + act("page", { + page: 1, + })}> + + Contracts + + act("page", { + page: 2, + })}> + + Hub + + + ); +}; + +const Contracts = (properties, context) => { + const { act, data } = useBackend(context); + const { + contracts, + contract_active, + can_extract, + } = data; + const activeContract = !!contract_active && contracts.filter(c => c.status === 1)[0]; + const extractionCooldown = activeContract && activeContract.time_left > 0; + const [_viewingPhoto, setViewingPhoto] = useLocalState(context, "viewingPhoto", ""); + return ( +
" (" + f.substr(3) + ")"} + /> + ), + ]} + onClick={() => act("extract")} + /> + } + {...properties}> + {contracts.slice().sort((a, b) => { + if (a.status === 1) { + return -1; + } else if (b.status === 1) { + return 1; + } else { + return a.status - b.status; + } + }).map(contract => ( +
+ + {contract.target_name} + + + {contract.has_photo && ( +
+ ))} +
+ ); +}; + +const Hub = (properties, context) => { + const { act, data } = useBackend(context); + const { + rep, + buyables, + } = data; + return ( +
+ {buyables.map(buyable => ( +
+ {buyable.description}
+ act('purchase', { + uid: buyable.uid, + })} + /> + {buyable.stock > -1 && ( + + {buyable.stock} in stock + + )} +
+ ))} +
+ ); +}; + +// Lifted from /tg/station +class FakeTerminal extends Component { + constructor(props) { + super(props); + this.timer = null; + this.state = { + currentIndex: 0, + currentDisplay: [], + }; + } + + tick() { + const { props, state } = this; + if (state.currentIndex <= props.allMessages.length) { + this.setState(prevState => { + return ({ + currentIndex: prevState.currentIndex + 1, + }); + }); + const { currentDisplay } = state; + currentDisplay.push(props.allMessages[state.currentIndex]); + } else { + clearTimeout(this.timer); + setTimeout(props.onFinished, props.finishedTimeout); + } + } + + componentDidMount() { + const { + linesPerSecond = 2.5, + } = this.props; + this.timer = setInterval(() => this.tick(), 1000 / linesPerSecond); + } + + componentWillUnmount() { + clearTimeout(this.timer); + } + + render() { + return ( + + {this.state.currentDisplay.map(value => ( + + {value} +
+
+ ))} +
+ ); + } +} + +const PhotoZoom = (properties, context) => { + const [viewingPhoto, setViewingPhoto] = useLocalState(context, "viewingPhoto", ""); + return ( + + +