diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index f1137f100e9c1..fc007a7d80a17 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -47339,7 +47339,7 @@
start_active = 1
},
/turf/open/misc/asteroid/snow/icemoon,
-/area/station/ai_monitored/turret_protected/aisat/maint)
+/area/icemoon/surface/outdoors/nospawn)
"oxB" = (
/obj/machinery/door/airlock/maintenance,
/obj/effect/mapping_helpers/airlock/abandoned,
@@ -56034,7 +56034,7 @@
start_active = 1
},
/turf/open/misc/asteroid/snow/icemoon,
-/area/station/ai_monitored/turret_protected/aisat/maint)
+/area/icemoon/surface/outdoors/nospawn)
"rbs" = (
/obj/effect/turf_decal/tile/yellow,
/obj/machinery/light/directional/east,
diff --git a/code/__DEFINES/ai/pets.dm b/code/__DEFINES/ai/pets.dm
index e41c9ac0c3ffe..c7383f56a005e 100644
--- a/code/__DEFINES/ai/pets.dm
+++ b/code/__DEFINES/ai/pets.dm
@@ -51,3 +51,20 @@
/// key that holds items we arent interested in hoarding
#define BB_IGNORE_ITEMS "ignore_items"
+//virtual pet keys
+///the last PDA message we must relay
+#define BB_LAST_RECIEVED_MESSAGE "last_recieved_message"
+///our current virtual pet level
+#define BB_VIRTUAL_PET_LEVEL "virtual_pet_level"
+///the target we will play with
+#define BB_NEARBY_PLAYMATE "nearby_playmate"
+///cooldown till we search for playmates
+#define BB_NEXT_PLAYDATE "next_playdate"
+///our ability to trigger lights
+#define BB_LIGHTS_ABILITY "lights_ability"
+///our ability to capture images
+#define BB_PHOTO_ABILITY "photo_ability"
+///the name of our trick
+#define BB_TRICK_NAME "trick_name"
+///the sequence of our trick
+#define BB_TRICK_SEQUENCE "trick_sequence"
diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm
index 10b2f8dc63515..af1cb68c41cad 100644
--- a/code/__DEFINES/antagonists.dm
+++ b/code/__DEFINES/antagonists.dm
@@ -146,6 +146,9 @@
/// JSON string file for all of our heretic influence flavors
#define HERETIC_INFLUENCE_FILE "antagonist_flavor/heretic_influences.json"
+/// JSON file containing spy objectives
+#define SPY_OBJECTIVE_FILE "antagonist_flavor/spy_objective.json"
+
///employers that are from the syndicate
GLOBAL_LIST_INIT(syndicate_employers, list(
"Animal Rights Consortium",
@@ -265,6 +268,8 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list(
#define OBJECTIVE_ITEM_TYPE_NORMAL "normal"
/// Only appears in traitor objectives
#define OBJECTIVE_ITEM_TYPE_TRAITOR "traitor"
+/// Only appears for spy bounties
+#define OBJECTIVE_ITEM_TYPE_SPY "spy"
// Progression traitor defines
@@ -379,3 +384,11 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list(
#define BATON_MODES 4
#define FREEDOM_IMPLANT_CHARGES 4
+
+// Spy bounty difficulties
+/// Can easily be accomplished by any job without any specialized tools, people won't really miss these things
+#define SPY_DIFFICULTY_EASY "Easy"
+/// Requires some specialized tools, knowledge, or access to accomplish, may require getting into conflict with the crew
+#define SPY_DIFFICULTY_MEDIUM "Medium"
+/// Very difficult to accomplish, almost guaranteed to require crew conflict
+#define SPY_DIFFICULTY_HARD "Hard"
diff --git a/code/__DEFINES/dcs/signals/signals_action.dm b/code/__DEFINES/dcs/signals/signals_action.dm
index 6fbf5372acdd2..2226e34bcccbd 100644
--- a/code/__DEFINES/dcs/signals/signals_action.dm
+++ b/code/__DEFINES/dcs/signals/signals_action.dm
@@ -48,3 +48,6 @@
/// From /datum/action/cooldown/manual_heart/Activate(): ()
#define COMSIG_HEART_MANUAL_PULSE "heart_manual_pulse"
+
+/// From /datum/action/cooldown/mob_cooldown/capture_photo/Activate():
+#define COMSIG_ACTION_PHOTO_CAPTURED "action_photo_captured"
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
index a027dc61adbfe..24524395f35f2 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
@@ -129,3 +129,8 @@
#define COMSIG_ATOM_GERM_UNEXPOSED "atom_germ_unexposed"
/// signal sent to puzzle pieces by activator
#define COMSIG_PUZZLE_COMPLETED "puzzle_completed"
+
+/// From /datum/compomnent/cleaner/clean()
+#define COMSIG_ATOM_PRE_CLEAN "atom_pre_clean"
+ ///cancel clean
+ #define COMSIG_ATOM_CANCEL_CLEAN (1<<0)
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
index 601f441c66dd4..38d0500dcbdb5 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm
@@ -112,3 +112,6 @@
#define COMSIG_MOVABLE_EDIT_UNIQUE_IMMERSE_OVERLAY "movable_edit_unique_submerge_overlay"
/// From base of area/Exited(): (area/left, direction)
#define COMSIG_MOVABLE_EXITED_AREA "movable_exited_area"
+
+/// Sent to movables when they are being stolen by a spy: (mob/living/spy, datum/spy_bounty/bounty)
+#define COMSIG_MOVABLE_SPY_STEALING "movable_spy_stealing"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
index 533ad2e1ae886..3ddd0eb85387b 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
@@ -26,6 +26,8 @@
#define COMSIG_LIVING_EXTINGUISHED "living_extinguished"
///from base of mob/living/electrocute_act(): (shock_damage, source, siemens_coeff, flags)
#define COMSIG_LIVING_ELECTROCUTE_ACT "living_electrocute_act"
+ /// Block the electrocute_act() proc from proceeding
+ #define COMPONENT_LIVING_BLOCK_SHOCK (1<<0)
///sent when items with siemen coeff. of 0 block a shock: (power_source, source, siemens_coeff, dist_check)
#define COMSIG_LIVING_SHOCK_PREVENTED "living_shock_prevented"
///sent by stuff like stunbatons and tasers: ()
@@ -253,3 +255,11 @@
/// Sent to a mob grabbing another mob: (mob/living/grabbing)
#define COMSIG_LIVING_GRAB "living_grab"
// Return COMPONENT_CANCEL_ATTACK_CHAIN / COMPONENT_SKIP_ATTACK_CHAIN to stop the grab
+
+/// From /datum/element/basic_eating/try_eating()
+#define COMSIG_MOB_PRE_EAT "mob_pre_eat"
+ ///cancel eating attempt
+ #define COMSIG_MOB_CANCEL_EAT (1<<0)
+
+/// From /datum/element/basic_eating/finish_eating()
+#define COMSIG_MOB_ATE "mob_ate"
diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm
index 442309289f03a..3654b4cfce5d3 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -410,6 +410,11 @@
///from /datum/action/vehicle/sealed/headlights/vim/Trigger(): (headlights_on)
#define COMSIG_VIM_HEADLIGHTS_TOGGLED "vim_headlights_toggled"
+///from /datum/computer_file/program/messenger/proc/receive_message
+#define COMSIG_COMPUTER_RECIEVED_MESSAGE "computer_recieved_message"
+///from /datum/computer_file/program/virtual_pet/proc/handle_level_up
+#define COMSIG_VIRTUAL_PET_LEVEL_UP "virtual_pet_level_up"
+
// /obj/vehicle/sealed/mecha signals
/// sent if you attach equipment to mecha
diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 5798fd29e82de..0d2fb6b874d48 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -77,6 +77,7 @@
#define ui_building "EAST-4:22,SOUTH:21"
#define ui_language_menu "EAST-4:6,SOUTH:21"
#define ui_navigate_menu "EAST-4:22,SOUTH:5"
+#define ui_floor_menu "EAST-4:14,SOUTH:37"
//Upper-middle right (alerts)
#define ui_alert1 "EAST-1:28,CENTER+5:27"
@@ -143,6 +144,7 @@
#define ui_borg_alerts "CENTER+4:21,SOUTH:5"
#define ui_borg_language_menu "CENTER+4:19,SOUTH+1:6"
#define ui_borg_navigate_menu "CENTER+4:19,SOUTH+1:6"
+#define ui_borg_floor_menu "CENTER+4:-13,SOUTH+1:6"
//Aliens
#define ui_alien_health "EAST,CENTER-1:15"
@@ -151,6 +153,7 @@
#define ui_alien_storage_r "CENTER+1:18,SOUTH:5"
#define ui_alien_language_menu "EAST-4:20,SOUTH:5"
#define ui_alien_navigate_menu "EAST-4:20,SOUTH:5"
+#define ui_alien_floor_menu "EAST-4:-12,SOUTH:5"
//AI
#define ui_ai_core "BOTTOM:6,RIGHT-4"
@@ -159,6 +162,7 @@
#define ui_ai_state_laws "BOTTOM:6,RIGHT-1"
#define ui_ai_mod_int "BOTTOM:6,RIGHT"
#define ui_ai_language_menu "BOTTOM+1:8,RIGHT-1:30"
+#define ui_ai_floor_menu "BOTTOM+1:8,RIGHT-1:14"
#define ui_ai_crew_monitor "BOTTOM:6,CENTER-1"
#define ui_ai_crew_manifest "BOTTOM:6,CENTER"
@@ -200,6 +204,7 @@
#define ui_ghost_pai "SOUTH: 6, CENTER+1:24"
#define ui_ghost_minigames "SOUTH: 6, CENTER+2:24"
#define ui_ghost_language_menu "SOUTH: 22, CENTER+3:8"
+#define ui_ghost_floor_menu "SOUTH: 6, CENTER+3:8"
//Blobbernauts
#define ui_blobbernaut_overmind_health "EAST-1:28,CENTER+0:19"
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 909399b3c3da6..1cf4a1bb3be0d 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -314,6 +314,7 @@ GLOBAL_LIST_INIT(book_types, typecacheof(list(
#define is_captain_job(job_type) (istype(job_type, /datum/job/captain))
#define is_chaplain_job(job_type) (istype(job_type, /datum/job/chaplain))
#define is_clown_job(job_type) (istype(job_type, /datum/job/clown))
+#define is_mime_job(job_type) (istype(job_type, /datum/job/mime))
#define is_detective_job(job_type) (istype(job_type, /datum/job/detective))
#define is_scientist_job(job_type) (istype(job_type, /datum/job/scientist))
#define is_security_officer_job(job_type) (istype(job_type, /datum/job/security_officer))
diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index a6102aa6e7938..492a0a06a8850 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -161,6 +161,7 @@
#define LOG_CATEGORY_UPLINK_HERETIC "uplink-heretic"
#define LOG_CATEGORY_UPLINK_MALF "uplink-malf"
#define LOG_CATEGORY_UPLINK_SPELL "uplink-spell"
+#define LOG_CATEGORY_UPLINK_SPY "uplink-spy"
// PDA categories
#define LOG_CATEGORY_PDA "pda"
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index 90485367815f6..9a05a72928500 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -969,3 +969,17 @@ GLOBAL_LIST_INIT(layers_to_offset, list(
/// Types of bullets that mining mobs take full damage from
#define MINING_MOB_PROJECTILE_VULNERABILITY list(BRUTE)
+
+// Sprites for photocopying butts
+#define BUTT_SPRITE_HUMAN_MALE "human_male"
+#define BUTT_SPRITE_HUMAN_FEMALE "human_female"
+#define BUTT_SPRITE_LIZARD "lizard"
+#define BUTT_SPRITE_QR_CODE "qr_code"
+#define BUTT_SPRITE_XENOMORPH "xeno"
+#define BUTT_SPRITE_DRONE "drone"
+#define BUTT_SPRITE_CAT "cat"
+#define BUTT_SPRITE_FLOWERPOT "flowerpot"
+#define BUTT_SPRITE_GREY "grey"
+#define BUTT_SPRITE_PLASMA "plasma"
+#define BUTT_SPRITE_FUZZY "fuzzy"
+#define BUTT_SPRITE_SLIME "slime"
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index 3d41921e0ea00..09b07295beca0 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -16,6 +16,7 @@
#define ROLE_OPERATIVE "Operative"
#define ROLE_TRAITOR "Traitor"
#define ROLE_WIZARD "Wizard"
+#define ROLE_SPY "Spy"
// Midround roles
#define ROLE_ABDUCTOR "Abductor"
@@ -128,6 +129,7 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_REV_HEAD = 14,
ROLE_TRAITOR = 0,
ROLE_WIZARD = 14,
+ ROLE_SPY = 0,
// Midround
ROLE_ABDUCTOR = 0,
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index eef432fb2edfe..ac2b418b55fc8 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -221,6 +221,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_NO_THROW_HITPUSH "no_throw_hitpush"
///Added to mob or mind, changes the icons of the fish shown in the minigame UI depending on the possible reward.
#define TRAIT_REVEAL_FISH "reveal_fish"
+///This trait gets you a list of fishes that can be caught when examining a fishing spot.
+#define TRAIT_EXAMINE_FISHING_SPOT "examine_fishing_spot"
/// Added to a mob, allows that mob to experience flavour-based moodlets when examining food
#define TRAIT_REMOTE_TASTING "remote_tasting"
diff --git a/code/__DEFINES/uplink.dm b/code/__DEFINES/uplink.dm
index d6412e0e4d150..bb92f0672c3a7 100644
--- a/code/__DEFINES/uplink.dm
+++ b/code/__DEFINES/uplink.dm
@@ -12,6 +12,9 @@
/// This item is purchasable to infiltrators (midround traitors)
#define UPLINK_INFILTRATORS (1 << 3)
+/// Can be randomly given to spies for their bounties
+#define UPLINK_SPY (1 << 4)
+
/// Progression gets turned into a user-friendly form. This is just an abstract equation that makes progression not too large.
#define DISPLAY_PROGRESSION(time) round(time/60, 0.01)
@@ -19,3 +22,12 @@
#define TRAITOR_DISCOUNT_BIG "big_discount"
#define TRAITOR_DISCOUNT_AVERAGE "average_discount"
#define TRAITOR_DISCOUNT_SMALL "small_discount"
+
+/// Typepath used for uplink items which don't actually produce an item (essentially just a placeholder)
+/// Future todo: Make this not necessary / make uplink items support item-less items natively
+#define ABSTRACT_UPLINK_ITEM /obj/effect/gibspawner/generic
+
+/// Lower threshold for which an uplink items's TC cost is considered "low" for spy bounties picking rewards
+#define SPY_LOWER_COST_THRESHOLD 5
+/// Upper threshold for which an uplink items's TC cost is considered "high" for spy bounties picking rewards
+#define SPY_UPPER_COST_THRESHOLD 12
diff --git a/code/__HELPERS/logging/antagonists.dm b/code/__HELPERS/logging/antagonists.dm
index 3d06bb325ec35..5df39c69adef3 100644
--- a/code/__HELPERS/logging/antagonists.dm
+++ b/code/__HELPERS/logging/antagonists.dm
@@ -21,3 +21,7 @@
/// Logging for wizard powers learned
/proc/log_spellbook(text, list/data)
logger.Log(LOG_CATEGORY_UPLINK_SPELL, text, data)
+
+/// Logs bounties completed by spies and their rewards
+/proc/log_spy(text, list/data)
+ logger.Log(LOG_CATEGORY_UPLINK_SPY, text, data)
diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm
index d95a46c8be249..36f96bcc563e0 100644
--- a/code/_globalvars/lists/maintenance_loot.dm
+++ b/code/_globalvars/lists/maintenance_loot.dm
@@ -302,6 +302,7 @@ GLOBAL_LIST_INIT(rarity_loot, list(//rare: really good items
/obj/item/pen/survival = 1,
/obj/item/restraints/handcuffs = 1,
/obj/item/shield/buckler = 1,
+ /obj/item/shield/improvised = 1,
/obj/item/throwing_star = 1,
/obj/item/weldingtool/hugetank = 1,
/obj/item/fishing_rod/telescopic/master = 1,
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 15715ebd0a050..7a6e7f3602189 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -195,6 +195,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_EMOTEMUTE" = TRAIT_EMOTEMUTE,
"TRAIT_EMPATH" = TRAIT_EMPATH,
"TRAIT_ENTRAILS_READER" = TRAIT_ENTRAILS_READER,
+ "TRAIT_EXAMINE_FISHING_SPOT" = TRAIT_EXAMINE_FISHING_SPOT,
"TRAIT_EXPANDED_FOV" = TRAIT_EXPANDED_FOV,
"TRAIT_EXTROVERT" = TRAIT_EXTROVERT,
"TRAIT_FAKEDEATH" = TRAIT_FAKEDEATH,
diff --git a/code/_onclick/hud/ai.dm b/code/_onclick/hud/ai.dm
index 5f687d1964281..1d26c4916b04b 100644
--- a/code/_onclick/hud/ai.dm
+++ b/code/_onclick/hud/ai.dm
@@ -186,6 +186,11 @@
using.screen_loc = ui_ai_language_menu
static_inventory += using
+// Z-level floor change
+ using = new /atom/movable/screen/floor_menu(null, src)
+ using.screen_loc = ui_ai_floor_menu
+ static_inventory += using
+
//AI core
using = new /atom/movable/screen/ai/aicore(null, src)
using.screen_loc = ui_ai_core
diff --git a/code/_onclick/hud/alien.dm b/code/_onclick/hud/alien.dm
index 3c1b1029a3e06..c3b91173a45f5 100644
--- a/code/_onclick/hud/alien.dm
+++ b/code/_onclick/hud/alien.dm
@@ -63,6 +63,10 @@
using.screen_loc = ui_alien_language_menu
static_inventory += using
+ using = new /atom/movable/screen/floor_menu(null, src)
+ using.screen_loc = ui_alien_floor_menu
+ static_inventory += using
+
using = new /atom/movable/screen/navigate(null, src)
using.screen_loc = ui_alien_navigate_menu
static_inventory += using
@@ -87,7 +91,7 @@
pull_icon.update_appearance()
pull_icon.screen_loc = ui_above_movement
static_inventory += pull_icon
-
+
rest_icon = new /atom/movable/screen/rest(null, src)
rest_icon.icon = ui_style
rest_icon.screen_loc = ui_above_intent
diff --git a/code/_onclick/hud/alien_larva.dm b/code/_onclick/hud/alien_larva.dm
index d9ebb3611b68b..77d135ce2c663 100644
--- a/code/_onclick/hud/alien_larva.dm
+++ b/code/_onclick/hud/alien_larva.dm
@@ -32,6 +32,10 @@
using.screen_loc = ui_alien_language_menu
static_inventory += using
+ using = new /atom/movable/screen/floor_menu(null, src)
+ using.screen_loc = ui_alien_floor_menu
+ static_inventory += using
+
using = new /atom/movable/screen/navigate(null, src)
using.screen_loc = ui_alien_navigate_menu
static_inventory += using
diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm
index 99b04df906871..e20c1ede2f663 100644
--- a/code/_onclick/hud/ghost.dm
+++ b/code/_onclick/hud/ghost.dm
@@ -86,6 +86,16 @@
using.icon = ui_style
static_inventory += using
+ using = new /atom/movable/screen/language_menu(null, src)
+ using.screen_loc = ui_ghost_language_menu
+ using.icon = ui_style
+ static_inventory += using
+
+ using = new /atom/movable/screen/floor_menu(null, src)
+ using.screen_loc = ui_ghost_floor_menu
+ using.icon = ui_style
+ static_inventory += using
+
/datum/hud/ghost/show_hud(version = 0, mob/viewmob)
// don't show this HUD if observing; show the HUD of the observee
var/mob/dead/observer/O = mymob
diff --git a/code/_onclick/hud/human.dm b/code/_onclick/hud/human.dm
index 22a046970cb02..b12ade0c58d43 100644
--- a/code/_onclick/hud/human.dm
+++ b/code/_onclick/hud/human.dm
@@ -70,6 +70,10 @@
using.icon = ui_style
static_inventory += using
+ using = new /atom/movable/screen/floor_menu(null, src)
+ using.icon = ui_style
+ static_inventory += using
+
action_intent = new /atom/movable/screen/combattoggle/flashy(null, src)
action_intent.icon = ui_style
action_intent.screen_loc = ui_combat_toggle
diff --git a/code/_onclick/hud/robot.dm b/code/_onclick/hud/robot.dm
index ea890566f74cf..090b8876cba44 100644
--- a/code/_onclick/hud/robot.dm
+++ b/code/_onclick/hud/robot.dm
@@ -77,6 +77,7 @@
var/mob/living/silicon/robot/robit = mymob
var/atom/movable/screen/using
+// Language
using = new/atom/movable/screen/language_menu(null, src)
using.screen_loc = ui_borg_language_menu
static_inventory += using
@@ -86,6 +87,11 @@
using.screen_loc = ui_borg_navigate_menu
static_inventory += using
+// Z-level floor change
+ using = new /atom/movable/screen/floor_menu(null, src)
+ using.screen_loc = ui_borg_floor_menu
+ static_inventory += using
+
//Radio
using = new /atom/movable/screen/robot/radio(null, src)
using.screen_loc = ui_borg_radio
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index f75231722749f..dfb5f072d896c 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -127,6 +127,33 @@
/atom/movable/screen/language_menu/Click()
usr.get_language_holder().open_language_menu(usr)
+/atom/movable/screen/floor_menu
+ name = "change floor"
+ icon = 'icons/hud/screen_midnight.dmi'
+ icon_state = "floor_change"
+ screen_loc = ui_floor_menu
+
+/atom/movable/screen/floor_menu/Initialize(mapload)
+ . = ..()
+ register_context()
+
+/atom/movable/screen/floor_menu/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+
+ context[SCREENTIP_CONTEXT_LMB] = "Go up a floor"
+ context[SCREENTIP_CONTEXT_RMB] = "Go down a floor"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/atom/movable/screen/floor_menu/Click(location,control,params)
+ var/list/modifiers = params2list(params)
+
+ if(LAZYACCESS(modifiers, RIGHT_CLICK) || LAZYACCESS(modifiers, ALT_CLICK))
+ usr.down()
+ return
+
+ usr.up()
+ return
+
/atom/movable/screen/inventory
/// The identifier for the slot. It has nothing to do with ID cards.
var/slot_id
diff --git a/code/controllers/subsystem/blackmarket.dm b/code/controllers/subsystem/blackmarket.dm
index 357fa0df2915d..bdd342cbf3d04 100644
--- a/code/controllers/subsystem/blackmarket.dm
+++ b/code/controllers/subsystem/blackmarket.dm
@@ -21,17 +21,20 @@ SUBSYSTEM_DEF(blackmarket)
for(var/market in subtypesof(/datum/market))
markets[market] += new market
- for(var/item in subtypesof(/datum/market_item))
- var/datum/market_item/I = new item()
- if(!I.item)
+ for(var/datum/market_item/item as anything in subtypesof(/datum/market_item))
+ if(!initial(item.item))
+ continue
+ if(!prob(initial(item.availability_prob)))
continue
- for(var/M in I.markets)
- if(!markets[M])
- stack_trace("SSblackmarket: Item [I] available in market that does not exist.")
+ var/datum/market_item/item_instance = new item()
+ for(var/potential_market in item_instance.markets)
+ if(!markets[potential_market])
+ stack_trace("SSblackmarket: Item [item_instance] available in market that does not exist.")
continue
- markets[M].add_item(item)
- qdel(I)
+ // If this fails the market item will just be GC'd
+ markets[potential_market].add_item(item_instance)
+
return SS_INIT_SUCCESS
/datum/controller/subsystem/blackmarket/fire(resumed)
diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm
index b74483a2cb639..51ecd59925a4d 100644
--- a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm
@@ -698,3 +698,45 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE)
create_separatist_nation(department_type, announcement = FALSE, dangerous = FALSE, message_admins = FALSE)
GLOB.round_default_lawset = /datum/ai_laws/united_nations
+
+/datum/dynamic_ruleset/roundstart/spies
+ name = "Spies"
+ antag_flag = ROLE_SPY
+ antag_datum = /datum/antagonist/spy
+ minimum_required_age = 0
+ protected_roles = list(
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL, // AA = bad
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ )
+ required_candidates = 3 // lives or dies by there being a few spies
+ weight = 5
+ cost = 8
+ scaling_cost = 101 // see below
+ minimum_players = 8
+ antag_cap = list("denominator" = 8, "offset" = 1) // should have quite a few spies to work against each other
+ requirements = list(8, 8, 8, 8, 8, 8, 8, 8, 8, 8)
+
+/datum/dynamic_ruleset/roundstart/spies/pre_execute(population)
+ for(var/i in 1 to get_antag_cap(population) * (scaled_times + 1))
+ if(length(candidates) <= 0)
+ break
+ var/mob/picked_player = pick_n_take(candidates)
+ assigned += picked_player.mind
+ picked_player.mind.special_role = ROLE_SPY
+ picked_player.mind.restricted_roles = restricted_roles
+ GLOB.pre_setup_antags += picked_player.mind
+ return TRUE
+
+/datum/dynamic_ruleset/roundstart/spies/scale_up(population, max_scale)
+ // Disabled (at least until dynamic can handle scaling this better)
+ // Because spies have a very low demoninator, this can easily spawn like 30 of them
+ return 0
diff --git a/code/datums/actions/mobs/charge.dm b/code/datums/actions/mobs/charge.dm
index 9b8e1c36ef81c..1a3afba14d159 100644
--- a/code/datums/actions/mobs/charge.dm
+++ b/code/datums/actions/mobs/charge.dm
@@ -304,8 +304,8 @@
/datum/action/cooldown/mob_cooldown/charge/hallucination_charge/hallucination_surround
name = "Surround Target"
- button_icon = 'icons/turf/walls/wall.dmi'
- button_icon_state = "wall-0"
+ button_icon = 'icons/mob/actions/actions_animal.dmi'
+ button_icon_state = "expand"
desc = "Allows you to create hallucinations that charge around your target."
charge_delay = 0.6 SECONDS
charge_past = 2
diff --git a/code/datums/components/bloody_spreader.dm b/code/datums/components/bloody_spreader.dm
index 951136c890c01..b30000a115c6a 100644
--- a/code/datums/components/bloody_spreader.dm
+++ b/code/datums/components/bloody_spreader.dm
@@ -7,7 +7,7 @@
// Blood splashed around everywhere will carry these diseases. Oh no...
var/list/diseases
-/datum/component/bloody_spreader/Initialize(blood_left, list/blood_dna, list/diseases)
+/datum/component/bloody_spreader/Initialize(blood_left = INFINITY, list/blood_dna, list/diseases)
if(!isatom(parent))
return COMPONENT_INCOMPATIBLE
var/list/signals_to_add = list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_BLOB_ACT, COMSIG_ATOM_HULK_ATTACK, COMSIG_ATOM_ATTACKBY)
@@ -33,6 +33,9 @@
/datum/component/bloody_spreader/proc/spread_yucky_blood(atom/parent, atom/bloody_fool)
SIGNAL_HANDLER
bloody_fool.add_blood_DNA(blood_dna, diseases)
+ blood_left--
+ if(blood_left <= 0)
+ qdel(src)
/datum/component/bloody_spreader/InheritComponent(/datum/component/new_comp, i_am_original, blood_left = 0)
diff --git a/code/datums/components/cleaner.dm b/code/datums/components/cleaner.dm
index 242ad72071cf5..49f200b4b9286 100644
--- a/code/datums/components/cleaner.dm
+++ b/code/datums/components/cleaner.dm
@@ -89,9 +89,8 @@
*/
/datum/component/cleaner/proc/clean(datum/source, atom/target, mob/living/user, clean_target = TRUE)
//make sure we don't attempt to clean something while it's already being cleaned
- if(HAS_TRAIT(target, TRAIT_CURRENTLY_CLEANING))
+ if(HAS_TRAIT(target, TRAIT_CURRENTLY_CLEANING) || (SEND_SIGNAL(target, COMSIG_ATOM_PRE_CLEAN, user) & COMSIG_ATOM_CANCEL_CLEAN))
return
-
//add the trait and overlay
ADD_TRAIT(target, TRAIT_CURRENTLY_CLEANING, REF(src))
// We need to update our planes on overlay changes
diff --git a/code/datums/components/crafting/atmospheric.dm b/code/datums/components/crafting/atmospheric.dm
index 955c9704abda5..cb5bba9ab52b2 100644
--- a/code/datums/components/crafting/atmospheric.dm
+++ b/code/datums/components/crafting/atmospheric.dm
@@ -12,7 +12,7 @@
/datum/crafting_recipe/pipe
name = "Smart pipe fitting"
tool_behaviors = list(TOOL_WRENCH)
- result = /obj/item/pipe/quaternary/pipe
+ result = /obj/item/pipe/quaternary/pipe/crafted
reqs = list(/obj/item/stack/sheet/iron = 1)
time = 0.5 SECONDS
category = CAT_ATMOSPHERIC
@@ -38,14 +38,6 @@
)
blacklist = list(/obj/item/analyzer/ranged)
-/datum/crafting_recipe/pipe/on_craft_completion(mob/user, atom/result)
- var/obj/item/pipe/crafted_pipe = result
- crafted_pipe.pipe_type = /obj/machinery/atmospherics/pipe/smart
- crafted_pipe.pipe_color = COLOR_VERY_LIGHT_GRAY
- crafted_pipe.p_init_dir = ALL_CARDINALS
- crafted_pipe.setDir(SOUTH)
- crafted_pipe.update()
-
/datum/crafting_recipe/layer_adapter
name = "Layer manifold fitting"
tool_behaviors = list(TOOL_WRENCH, TOOL_WELDER)
diff --git a/code/datums/components/crafting/chemistry.dm b/code/datums/components/crafting/chemistry.dm
index 5e3afae9e634d..62504103eca2c 100644
--- a/code/datums/components/crafting/chemistry.dm
+++ b/code/datums/components/crafting/chemistry.dm
@@ -1,14 +1,14 @@
/datum/crafting_recipe/improv_explosive
- name = "IED"
- result = /obj/item/grenade/iedcasing
+ name = "Improvised Explosive"
+ result = /obj/item/grenade/iedcasing/spawned
+ tool_behaviors = list(TOOL_WELDER, TOOL_SCREWDRIVER)
reqs = list(
- /datum/reagent/fuel = 50,
- /obj/item/stack/cable_coil = 1,
- /obj/item/assembly/igniter = 1,
- /obj/item/reagent_containers/cup/soda_cans = 1,
+ /datum/reagent/fuel = 20,
+ /obj/item/stack/cable_coil = 15,
+ /obj/item/assembly/timer = 1,
+ /obj/item/pipe/quaternary/pipe = 1,
)
- parts = list(/obj/item/reagent_containers/cup/soda_cans = 1)
- time = 1.5 SECONDS
+ time = 6 SECONDS
category = CAT_CHEMISTRY
/datum/crafting_recipe/molotov
diff --git a/code/datums/components/crafting/equipment.dm b/code/datums/components/crafting/equipment.dm
index 4b686fdd8e9d4..741d53ed56b6c 100644
--- a/code/datums/components/crafting/equipment.dm
+++ b/code/datums/components/crafting/equipment.dm
@@ -13,6 +13,16 @@
..()
blacklist |= subtypesof(/obj/item/shield/riot)
+/datum/crafting_recipe/improvisedshield
+ name = "Improvised Shield"
+ result = /obj/item/shield/improvised
+ reqs = list(
+ /obj/item/stack/sheet/iron = 10,
+ /obj/item/stack/sticky_tape = 2,
+ )
+ time = 4 SECONDS
+ category = CAT_EQUIPMENT
+
/datum/crafting_recipe/radiogloves
name = "Radio Gloves"
result = /obj/item/clothing/gloves/radio
diff --git a/code/datums/components/fishing_spot.dm b/code/datums/components/fishing_spot.dm
index c8a00ce74cdca..2763d583f819c 100644
--- a/code/datums/components/fishing_spot.dm
+++ b/code/datums/components/fishing_spot.dm
@@ -15,6 +15,12 @@
fish_source.on_fishing_spot_init()
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(handle_attackby))
RegisterSignal(parent, COMSIG_FISHING_ROD_CAST, PROC_REF(handle_cast))
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE_MORE, PROC_REF(on_examined_more))
+
+/datum/component/fishing_spot/Destroy()
+ fish_source = null
+ return ..()
/datum/component/fishing_spot/proc/handle_cast(datum/source, obj/item/fishing_rod/rod, mob/user)
SIGNAL_HANDLER
@@ -28,6 +34,43 @@
return COMPONENT_NO_AFTERATTACK
return NONE
+///If the fish source has fishes that are shown in the
+/datum/component/fishing_spot/proc/on_examined(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+ if(!HAS_MIND_TRAIT(user, TRAIT_EXAMINE_FISHING_SPOT))
+ return
+
+ var/has_known_fishes = FALSE
+ for(var/reward in fish_source.fish_counts)
+ if(!ispath(reward, /obj/item/fish))
+ continue
+ var/obj/item/fish/prototype = reward
+ if(initial(prototype.show_in_catalog))
+ has_known_fishes = TRUE
+ break
+ if(!has_known_fishes)
+ return
+
+ examine_text += span_tinynoticeital("This is a fishing spot. You can look again to list its fishes...")
+
+/datum/component/fishing_spot/proc/on_examined_more(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+ if(!HAS_MIND_TRAIT(user, TRAIT_EXAMINE_FISHING_SPOT))
+ return
+
+ var/list/known_fishes = list()
+ for(var/reward in fish_source.fish_counts)
+ if(!ispath(reward, /obj/item/fish))
+ continue
+ var/obj/item/fish/prototype = reward
+ if(initial(prototype.show_in_catalog))
+ known_fishes += initial(prototype.name)
+
+ if(!length(known_fishes))
+ return
+
+ examine_text += span_info("You can catch the following fish here: [english_list(known_fishes)].")
+
/datum/component/fishing_spot/proc/try_start_fishing(obj/item/possibly_rod, mob/user)
SIGNAL_HANDLER
var/obj/item/fishing_rod/rod = possibly_rod
diff --git a/code/datums/components/tackle.dm b/code/datums/components/tackle.dm
index d19ef45e4b648..a0d0317d75540 100644
--- a/code/datums/components/tackle.dm
+++ b/code/datums/components/tackle.dm
@@ -164,8 +164,6 @@
neutral_outcome(user, target, tackle_word) //Forces a neutral outcome so you're not screwed too much from being blocked while tackling
return COMPONENT_MOVABLE_IMPACT_FLIP_HITPUSH
-
-
switch(roll)
if(-INFINITY to -1)
negative_outcome(user, target, roll, tackle_word) //OOF
@@ -178,6 +176,15 @@
return COMPONENT_MOVABLE_IMPACT_FLIP_HITPUSH
+/// Helper to do a grab and then adjust the grab state if necessary
+/datum/component/tackler/proc/do_grab(mob/living/carbon/tackler, mob/living/carbon/tackled, skip_to_state = GRAB_PASSIVE)
+ set waitfor = FALSE
+
+ if(!tackler.grab(tackled) || tackler.pulling != tackled)
+ return
+ if(tackler.grab_state != skip_to_state)
+ tackler.setGrabState(skip_to_state)
+
/**
* Our positive tackling outcomes.
*
@@ -198,15 +205,10 @@
var/potential_outcome = (roll * 10)
if(ishuman(target))
- var/mob/living/carbon/human/human_target = target
- var/target_armor = human_target.run_armor_check(BODY_ZONE_CHEST, MELEE)
- potential_outcome *= ((100 - target_armor) /100)
+ potential_outcome *= ((100 - target.run_armor_check(BODY_ZONE_CHEST, MELEE)) /100)
else
potential_outcome *= 0.9
- var/mob/living/carbon/human/human_target = target
- var/mob/living/carbon/human/human_sacker = user
-
switch(potential_outcome)
if(-INFINITY to 0) //I don't want to know how this has happened, okay?
neutral_outcome(user, target, roll, tackle_word) //Default to neutral
@@ -233,9 +235,7 @@
target.Paralyze(0.5 SECONDS)
target.Knockdown(3 SECONDS)
target.adjust_staggered_up_to(STAGGERED_SLOWDOWN_LENGTH * 2, 10 SECONDS)
- if(ishuman(target) && ishuman(user))
- INVOKE_ASYNC(human_sacker, TYPE_PROC_REF(/mob/living, grab), human_sacker, human_target)
- human_sacker.setGrabState(GRAB_PASSIVE)
+ do_grab(user, target)
if(50 to INFINITY) // absolutely BODIED
var/stamcritted_user = HAS_TRAIT_FROM(user, TRAIT_INCAPACITATED, STAMINA)
@@ -259,9 +259,7 @@
target.Paralyze(0.5 SECONDS)
target.Knockdown(3 SECONDS)
target.adjust_staggered_up_to(STAGGERED_SLOWDOWN_LENGTH * 3, 10 SECONDS)
- if(ishuman(target) && ishuman(user))
- INVOKE_ASYNC(human_sacker, TYPE_PROC_REF(/mob/living, grab), human_sacker, human_target)
- human_sacker.setGrabState(GRAB_AGGRESSIVE)
+ do_grab(user, target, GRAB_AGGRESSIVE)
/**
* Our neutral tackling outcome.
@@ -300,9 +298,7 @@
var/potential_roll_outcome = (roll * -10)
if(ishuman(user))
- var/mob/living/carbon/human/human_sacker = target
- var/attacker_armor = human_sacker.run_armor_check(BODY_ZONE_CHEST, MELEE)
- potential_roll_outcome *= ((100 - attacker_armor) /100)
+ potential_roll_outcome *= ((100 - target.run_armor_check(BODY_ZONE_CHEST, MELEE)) /100)
else
potential_roll_outcome *= 0.9
diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm
index e4e6e611ebca1..5007d8caeb92d 100644
--- a/code/datums/components/uplink.dm
+++ b/code/datums/components/uplink.dm
@@ -375,18 +375,18 @@
// PDA signal responses
-/datum/component/uplink/proc/new_ringtone(datum/source, atom/source, new_ring_text)
+/datum/component/uplink/proc/new_ringtone(datum/source, mob/living/user, new_ring_text)
SIGNAL_HANDLER
if(trim(lowertext(new_ring_text)) != trim(lowertext(unlock_code)))
if(trim(lowertext(new_ring_text)) == trim(lowertext(failsafe_code)))
- failsafe(source)
+ failsafe(user)
return COMPONENT_STOP_RINGTONE_CHANGE
return
locked = FALSE
- if(ismob(source))
- interact(null, source)
- to_chat(source, span_hear("The computer softly beeps."))
+ if(ismob(user))
+ interact(null, user)
+ to_chat(user, span_hear("The computer softly beeps."))
return COMPONENT_STOP_RINGTONE_CHANGE
/datum/component/uplink/proc/check_detonate()
diff --git a/code/datums/elements/basic_eating.dm b/code/datums/elements/basic_eating.dm
index 297e77fa060ea..2a7a4b46598b5 100644
--- a/code/datums/elements/basic_eating.dm
+++ b/code/datums/elements/basic_eating.dm
@@ -54,6 +54,8 @@
/datum/element/basic_eating/proc/try_eating(mob/living/eater, atom/target)
if(!is_type_in_list(target, food_types))
return FALSE
+ if(SEND_SIGNAL(eater, COMSIG_MOB_PRE_EAT, target) & COMSIG_MOB_CANCEL_EAT)
+ return FALSE
var/eat_verb
if(drinking)
eat_verb = pick("slurp","sip","guzzle","drink","quaff","suck")
@@ -79,6 +81,7 @@
return TRUE
/datum/element/basic_eating/proc/finish_eating(mob/living/eater, atom/target)
+ SEND_SIGNAL(eater, COMSIG_MOB_ATE)
if(drinking)
playsound(eater.loc,'sound/items/drink.ogg', rand(10,50), TRUE)
else
diff --git a/code/datums/elements/cleaning.dm b/code/datums/elements/cleaning.dm
index 3f39d00eb6e7c..6db1c9fb58033 100644
--- a/code/datums/elements/cleaning.dm
+++ b/code/datums/elements/cleaning.dm
@@ -1,32 +1,36 @@
+/datum/element/cleaning
+
/datum/element/cleaning/Attach(datum/target)
. = ..()
if(!ismovable(target))
return ELEMENT_INCOMPATIBLE
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(Clean))
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(clean))
/datum/element/cleaning/Detach(datum/target)
. = ..()
UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
-/datum/element/cleaning/proc/Clean(datum/source)
+/datum/element/cleaning/proc/clean(datum/source)
SIGNAL_HANDLER
- var/atom/movable/AM = source
- var/turf/tile = AM.loc
+ var/atom/movable/atom_movable = source
+ var/turf/tile = atom_movable.loc
if(!isturf(tile))
return
tile.wash(CLEAN_SCRUB)
- for(var/A in tile)
+ for(var/atom/cleaned as anything in tile)
// Clean small items that are lying on the ground
- if(isitem(A))
- var/obj/item/I = A
- if(I.w_class <= WEIGHT_CLASS_SMALL && !ismob(I.loc))
- I.wash(CLEAN_SCRUB)
+ if(isitem(cleaned))
+ var/obj/item/cleaned_item = cleaned
+ if(cleaned_item.w_class <= WEIGHT_CLASS_SMALL)
+ cleaned_item.wash(CLEAN_SCRUB)
+ continue
// Clean humans that are lying down
- else if(ishuman(A))
- var/mob/living/carbon/human/cleaned_human = A
- if(cleaned_human.body_position == LYING_DOWN)
- cleaned_human.wash(CLEAN_SCRUB)
- cleaned_human.regenerate_icons()
- to_chat(cleaned_human, span_danger("[AM] cleans your face!"))
+ if(!ishuman(cleaned))
+ continue
+ var/mob/living/carbon/human/cleaned_human = cleaned
+ if(cleaned_human.body_position == LYING_DOWN)
+ cleaned_human.wash(CLEAN_SCRUB)
+ cleaned_human.regenerate_icons()
+ to_chat(cleaned_human, span_danger("[atom_movable] cleans your face!"))
diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
index 250eba9a0d51f..87799dedda5de 100644
--- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
+++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
@@ -44,4 +44,3 @@
name = "Gutlunch"
icon_file = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
json_config = 'code/datums/greyscale/json_configs/gutlunch.json'
-
diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm
index 8b516a86a02a1..4aaab464f5e8f 100644
--- a/code/datums/mind/antag.dm
+++ b/code/datums/mind/antag.dm
@@ -105,6 +105,31 @@
var/datum/antagonist/rev/revolutionary = has_antag_datum(/datum/antagonist/rev)
revolutionary?.remove_revolutionary()
+/**
+ * Gets an item that can be used as an uplink somewhere on the mob's person.
+ *
+ * * desired_location: the location to look for the uplink in. An UPLINK_ define.
+ * If the desired location is not found, defaults to another location.
+ *
+ * Returns the item found, or null if no item was found.
+ */
+/mob/living/carbon/proc/get_uplink_location(desired_location = UPLINK_PDA)
+ var/list/all_contents = get_all_contents()
+ var/obj/item/modular_computer/pda/my_pda = locate() in all_contents
+ var/obj/item/radio/my_radio = locate() in all_contents
+ var/obj/item/pen/my_pen = (locate() in my_pda) || (locate() in all_contents)
+
+ switch(desired_location)
+ if(UPLINK_PDA)
+ return my_pda || my_radio || my_pen
+
+ if(UPLINK_RADIO)
+ return my_radio || my_pda || my_pen
+
+ if(UPLINK_PEN)
+ return my_pen || my_pda || my_radio
+
+ return null
/**
* ## give_uplink
@@ -115,53 +140,26 @@
* * antag_datum: the antag datum of the uplink owner, for storing it in antag memory. optional!
*/
/datum/mind/proc/give_uplink(silent = FALSE, datum/antagonist/antag_datum)
- if(!current)
+ if(isnull(current))
return
var/mob/living/carbon/human/traitor_mob = current
if (!istype(traitor_mob))
return
- var/list/all_contents = traitor_mob.get_all_contents()
- var/obj/item/modular_computer/pda/PDA = locate() in all_contents
- var/obj/item/radio/R = locate() in all_contents
- var/obj/item/pen/P
-
- if (PDA) // Prioritize PDA pen, otherwise the pocket protector pens will be chosen, which causes numerous ahelps about missing uplink
- P = locate() in PDA
- if (!P) // If we couldn't find a pen in the PDA, or we didn't even have a PDA, do it the old way
- P = locate() in all_contents
-
var/obj/item/uplink_loc
- var/implant = FALSE
-
var/uplink_spawn_location = traitor_mob.client?.prefs?.read_preference(/datum/preference/choiced/uplink_location)
- var/cant_speak = (HAS_TRAIT(traitor_mob, TRAIT_MUTE) || traitor_mob.mind?.assigned_role.title == JOB_MIME)
+ var/cant_speak = (HAS_TRAIT(traitor_mob, TRAIT_MUTE) || is_mime_job(assigned_role))
if(uplink_spawn_location == UPLINK_RADIO && cant_speak)
if(!silent)
to_chat(traitor_mob, span_warning("You have been deemed ineligible for a radio uplink. Supplying standard uplink instead."))
uplink_spawn_location = UPLINK_PDA
- switch (uplink_spawn_location)
- if(UPLINK_PDA)
- uplink_loc = PDA
- if(!uplink_loc)
- uplink_loc = R
- if(!uplink_loc)
- uplink_loc = P
- if(UPLINK_RADIO)
- uplink_loc = R
- if(!uplink_loc)
- uplink_loc = PDA
- if(!uplink_loc)
- uplink_loc = P
- if(UPLINK_PEN)
- uplink_loc = P
- if(UPLINK_IMPLANT)
- implant = TRUE
- if(!uplink_loc) // We've looked everywhere, let's just implant you
- implant = TRUE
+ if(uplink_spawn_location != UPLINK_IMPLANT)
+ uplink_loc = traitor_mob.get_uplink_location(uplink_spawn_location)
+ if(istype(uplink_loc, /obj/item/radio) && cant_speak)
+ uplink_loc = null
- if(implant)
+ if(isnull(uplink_loc))
var/obj/item/implant/uplink/starting/new_implant = new(traitor_mob)
new_implant.implant(traitor_mob, null, silent = TRUE)
if(!silent)
@@ -178,22 +176,27 @@
new_uplink.uplink_handler.owner = traitor_mob.mind
new_uplink.uplink_handler.assigned_role = traitor_mob.mind.assigned_role.title
new_uplink.uplink_handler.assigned_species = traitor_mob.dna.species.id
- if(uplink_loc == R)
- unlock_text = "Your Uplink is cunningly disguised as your [R.name]. Simply speak \"[new_uplink.unlock_code]\" into frequency [RADIO_TOKEN_UPLINK] to unlock its hidden features."
- add_memory(/datum/memory/key/traitor_uplink, uplink_loc = R.name, uplink_code = new_uplink.unlock_code)
- else if(uplink_loc == PDA)
- unlock_text = "Your Uplink is cunningly disguised as your [PDA.name]. Simply enter the code \"[new_uplink.unlock_code]\" into the ring tone selection to unlock its hidden features."
+
+ unlock_text = "Your Uplink is cunningly disguised as your [uplink_loc.name]. "
+ if(istype(uplink_loc, /obj/item/modular_computer/pda))
+ unlock_text += "Simply enter the code \"[new_uplink.unlock_code]\" into the ring tone selection to unlock its hidden features."
add_memory(/datum/memory/key/traitor_uplink, uplink_loc = "PDA", uplink_code = new_uplink.unlock_code)
- else if(uplink_loc == P)
+
+ else if(istype(uplink_loc, /obj/item/radio))
+ unlock_text += "Simply speak \"[new_uplink.unlock_code]\" into frequency [RADIO_TOKEN_UPLINK] to unlock its hidden features."
+ add_memory(/datum/memory/key/traitor_uplink, uplink_loc = uplink_loc.name, uplink_code = new_uplink.unlock_code)
+
+ else if(istype(uplink_loc, /obj/item/pen))
var/instructions = english_list(new_uplink.unlock_code)
- unlock_text = "Your Uplink is cunningly disguised as your [P.name]. Simply twist the top of the pen [instructions] from its starting position to unlock its hidden features."
- add_memory(/datum/memory/key/traitor_uplink, uplink_loc = "PDA pen", uplink_code = instructions)
+ unlock_text += "Simply twist the top of the pen [instructions] from its starting position to unlock its hidden features."
+ add_memory(/datum/memory/key/traitor_uplink, uplink_loc = uplink_loc.name, uplink_code = instructions)
new_uplink.unlock_text = unlock_text
if(!silent)
to_chat(traitor_mob, span_boldnotice(unlock_text))
if(antag_datum)
antag_datum.antag_memory += new_uplink.unlock_note + " "
+ return .
/// Link a new mobs mind to the creator of said mob. They will join any team they are currently on, and will only switch teams when their creator does.
/datum/mind/proc/enslave_mind_to_creator(mob/living/creator)
diff --git a/code/datums/skills/fishing.dm b/code/datums/skills/fishing.dm
index ddf90e1a0a3ac..cfd14a4ce3ba6 100644
--- a/code/datums/skills/fishing.dm
+++ b/code/datums/skills/fishing.dm
@@ -4,17 +4,20 @@
*/
/datum/skill/fishing
name = "Fishing"
- title = "Fisher"
+ title = "Angler"
desc = "How empty and alone you are on this barren Earth."
modifiers = list(SKILL_VALUE_MODIFIER = list(1, 1, 0, -1, -2, -4, -6))
skill_item_path = /obj/item/clothing/head/soft/fishing_hat
/datum/skill/fishing/New()
. = ..()
- levelUpMessages[SKILL_LEVEL_MASTER] = span_nicegreen("After lots of practice, I've begun to truly understand the surprising depth behind [name]. As a master [title], I can take an easier guess of what I'm trying to catch now.")
+ levelUpMessages[SKILL_LEVEL_JOURNEYMAN] = span_nicegreen("I feel like I've become quite proficient at [name]! I can tell what fishes I can catch at any given fishing spot.")
+ levelUpMessages[SKILL_LEVEL_MASTER] = span_nicegreen("I've begun to truly understand the surprising depth behind [name]. As a master [title], I can guess what I'm going to catch now!")
/datum/skill/fishing/level_gained(datum/mind/mind, new_level, old_level, silent)
. = ..()
+ if(new_level >= SKILL_LEVEL_JOURNEYMAN && old_level < SKILL_LEVEL_JOURNEYMAN)
+ ADD_TRAIT(mind, TRAIT_EXAMINE_FISHING_SPOT, SKILL_TRAIT)
if(new_level >= SKILL_LEVEL_MASTER && old_level < SKILL_LEVEL_MASTER)
ADD_TRAIT(mind, TRAIT_REVEAL_FISH, SKILL_TRAIT)
@@ -22,3 +25,5 @@
. = ..()
if(old_level >= SKILL_LEVEL_MASTER && new_level < SKILL_LEVEL_MASTER)
REMOVE_TRAIT(mind, TRAIT_REVEAL_FISH, SKILL_TRAIT)
+ if(old_level >= SKILL_LEVEL_JOURNEYMAN && new_level < SKILL_LEVEL_JOURNEYMAN)
+ REMOVE_TRAIT(mind, TRAIT_EXAMINE_FISHING_SPOT, SKILL_TRAIT)
diff --git a/code/datums/skills/fitness.dm b/code/datums/skills/fitness.dm
index c306548303e22..32be3f9d21174 100644
--- a/code/datums/skills/fitness.dm
+++ b/code/datums/skills/fitness.dm
@@ -1,6 +1,6 @@
/datum/skill/fitness
name = "Fitness"
- title = "Fitness"
+ title = "Powerlifter"
desc = "Twinkle twinkle little star, hit the gym and lift the bar."
/// The skill value modifier effects the max duration that is possible for /datum/status_effect/exercised
modifiers = list(SKILL_VALUE_MODIFIER = list(1 MINUTES, 1.5 MINUTES, 2 MINUTES, 2.5 MINUTES, 3 MINUTES, 3.5 MINUTES, 5 MINUTES))
diff --git a/code/datums/station_traits/job_traits.dm b/code/datums/station_traits/job_traits.dm
index 041f846424094..dc207d17e59a9 100644
--- a/code/datums/station_traits/job_traits.dm
+++ b/code/datums/station_traits/job_traits.dm
@@ -60,21 +60,18 @@
for (var/mob/dead/new_player/signee as anything in lobby_candidates)
if (isnull(signee) || !signee.client || !signee.mind || signee.ready != PLAYER_READY_TO_PLAY)
LAZYREMOVE(lobby_candidates, signee)
- if (!LAZYLEN(lobby_candidates))
- on_failed_assignment()
- return // Nobody signed up :(
- for(var/_ in 1 to position_amount)
+
+ var/datum/job/our_job = SSjob.GetJobType(job_to_add)
+ while(length(lobby_candidates) && position_amount > 0)
var/mob/dead/new_player/picked_player = pick_n_take(lobby_candidates)
- picked_player.mind.assigned_role = new job_to_add()
- lobby_candidates = null
+ picked_player.mind.set_assigned_role(our_job)
+ position_amount--
-/// Called if we didn't assign a role before the round began, we add it to the latejoin menu instead
-/datum/station_trait/job/proc/on_failed_assignment()
- var/datum/job/our_job = SSjob.GetJob(job_to_add::title)
- our_job.total_positions = position_amount
+ our_job.total_positions = max(0, position_amount)
+ lobby_candidates = null
/datum/station_trait/job/can_display_lobby_button(client/player)
- var/datum/job/our_job = SSjob.GetJob(job_to_add::title)
+ var/datum/job/our_job = SSjob.GetJobType(job_to_add)
return our_job.player_old_enough(player) && ..()
/// Adds a gorilla to the cargo department, replacing the sloth and the mech
diff --git a/code/game/gamemodes/objective_items.dm b/code/game/gamemodes/objective_items.dm
index 9627f48cbb501..491f0f71fb900 100644
--- a/code/game/gamemodes/objective_items.dm
+++ b/code/game/gamemodes/objective_items.dm
@@ -22,9 +22,23 @@
var/objective_type = OBJECTIVE_ITEM_TYPE_NORMAL
/// Whether this item exists on the station map at the start of a round.
var/exists_on_map = FALSE
+ /**
+ * How hard it is to steal this item given normal circumstances, ranked on a scale of 1 to 5.
+ *
+ * 1 - Probably found in a public area
+ * 2 - Likely on someone's person, or in a less-than-public but otherwise unguarded area
+ * 3 - Usually on someone's person, or in a locked locker or otherwise secure area
+ * 4 - Always on someone's person, or in a secure area
+ * 5 - You know it when you see it. Things like the Nuke Disc which have a pointer to it at all times.
+ *
+ * Also accepts 0 as "extremely easy to steal" and >5 as "almost impossible to steal"
+ */
+ var/difficulty = 0
+ /// A hint explaining how one may find the target item.
+ var/steal_hint = "The clown might have one."
/// For objectives with special checks (does that intellicard have an ai in it? etcetc)
-/datum/objective_item/proc/check_special_completion()
+/datum/objective_item/proc/check_special_completion(obj/item/thing)
return TRUE
/// Takes a list of minds and returns true if this is a valid objective to give to a team of these minds
@@ -72,6 +86,8 @@
excludefromjob = list(JOB_BARTENDER)
item_owner = list(JOB_BARTENDER)
exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "A double-barrel shotgun usually found on the bartender's person, or if none are around, in the bar's backroom."
/obj/item/gun/ballistic/shotgun/doublebarrel/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/gun/ballistic/shotgun/doublebarrel)
@@ -91,6 +107,9 @@
JOB_STATION_ENGINEER,
)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "Only two of these exist on the station - one in the bridge, and one in atmospherics. \
+ You can use a multitool to hack open the case, or break it open the hard way."
/obj/item/fireaxe/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/fireaxe)
@@ -105,6 +124,8 @@
)
item_owner = list(JOB_ROBOTICIST)
exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "A specialized tool found in the roboticist's lab. You can use a multitool to hack open the case, or break it open the hard way."
/obj/item/crowbar/mechremoval/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/crowbar/mechremoval)
@@ -115,6 +136,9 @@
excludefromjob = list(JOB_CHAPLAIN)
item_owner = list(JOB_CHAPLAIN)
exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "A holy artifact usually found on the chaplain's person, or if none are around, in the chapel's relic closet. \
+ If there is a chaplain aboard, it is likely be to be transformed into some holy weapon - some of which are... difficult to remove from their person."
/obj/item/nullrod/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/nullrod)
@@ -125,6 +149,8 @@
excludefromjob = list(JOB_CLOWN, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
item_owner = list(JOB_CLOWN)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "The clown's huge, bright shoes. They should always be on the clown's feet."
/obj/item/clothing/shoes/clown_shoes/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/shoes/clown_shoes)
@@ -135,6 +161,8 @@
excludefromjob = list(JOB_MIME, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
item_owner = list(JOB_MIME)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "The mime's mask. It should always be on the mime's face."
/obj/item/clothing/mask/gas/mime/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/mask/gas/mime)
@@ -145,6 +173,9 @@
excludefromjob = list(JOB_SHAFT_MINER, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
item_owner = list(JOB_SHAFT_MINER)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "A tool primarily used by shaft miners to mine. Most carry one (or multiple) on their person, \
+ but they can also be found in the Mining Station, Mining office, or Auxiliary Mining Base on the station."
/obj/item/gun/energy/recharge/kinetic_accelerator/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/gun/energy/recharge/kinetic_accelerator)
@@ -155,6 +186,8 @@
excludefromjob = list(JOB_COOK, JOB_HEAD_OF_PERSONNEL, JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER)
item_owner = list(JOB_COOK)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "The chef's fake Italian moustache, either found on their face or in the garbage, depending on who's on duty."
/obj/item/clothing/mask/fakemoustache/italian/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/mask/fakemoustache/italian)
@@ -164,6 +197,9 @@
targetitem = /obj/item/gun/ballistic/revolver/c38/detective
excludefromjob = list(JOB_DETECTIVE)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A .38 special revolver found in the Detective's holder. \
+ Usually found on the Detective's person, or if none are around, in the detective's locker, in their office."
/obj/item/gun/ballistic/revolver/c38/detective/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/gun/ballistic/revolver/c38/detective)
@@ -174,6 +210,8 @@
excludefromjob = list(JOB_LAWYER)
item_owner = list(JOB_LAWYER)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "The lawyer's badge. Usually pinned to their chest, but a spare can be obtained from their clothes vendor."
/obj/item/clothing/accessory/lawyers_badge/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/accessory/lawyers_badge)
@@ -183,6 +221,8 @@
targetitem = /obj/item/storage/belt/utility/chief
excludefromjob = list(JOB_CHIEF_ENGINEER)
exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "The chief engineer's toolbelt, strapped to their waist at all times."
/obj/item/storage/belt/utility/chief/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/storage/belt/utility/chief)
@@ -199,6 +239,8 @@
JOB_CHIEF_MEDICAL_OFFICER
)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A self-defense weapon standard-issue for all heads of staffs barring the Head of Security. Rarely found off of their person."
/obj/item/melee/baton/telescopic/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/melee/baton/telescopic)
@@ -209,6 +251,9 @@
excludefromjob = list(JOB_QUARTERMASTER, JOB_CARGO_TECHNICIAN)
item_owner = list(JOB_QUARTERMASTER)
exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "A card that grants access to Cargo's funds. \
+ Normally found in the locker of the Quartermaster, but a particularly keen one may have it on their person or in their wallet."
/obj/item/card/id/departmental_budget/car/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/card/id/departmental_budget/car)
@@ -218,6 +263,9 @@
targetitem = /obj/item/mod/control/pre_equipped/magnate
excludefromjob = list(JOB_CAPTAIN)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "An expensive, hand-crafted MOD unit made for the station's Captain. \
+ If not being worn by the Captain, you would find it in the Suit Storage Unit in their quarters."
/obj/item/mod/control/pre_equipped/magnate/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/magnate)
@@ -234,6 +282,10 @@
JOB_CHIEF_MEDICAL_OFFICER
)
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "The spare ID of the High Lord himself. \
+ If there's no official Captain around, you may find it pinned to the chest of the Acting Captain - one of the Heads of Staff. \
+ Otherwise, you'll have to bust open the golden safe on the bridge with acid or explosives to get to it."
/obj/item/card/id/advanced/gold/captains_spare/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/card/id/advanced/gold/captains_spare)
@@ -246,6 +298,9 @@
targetitem = /obj/item/gun/energy/laser/captain
excludefromjob = list(JOB_CAPTAIN)
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "A self-charging laser gun found in a display case in the Captain's Quarters. \
+ Breaking it open may trigger a security alert, so be careful."
/obj/item/gun/energy/laser/captain/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/gun/energy/laser/captain)
@@ -256,6 +311,9 @@
excludefromjob = list(JOB_HEAD_OF_SECURITY)
item_owner = list(JOB_HEAD_OF_SECURITY)
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "The Head of Security's unique three mode laser gun. \
+ Always found on their person, if they are alive, but may otherwise be found in their locker."
/obj/item/gun/energy/e_gun/hos/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/gun/energy/e_gun/hos)
@@ -266,6 +324,8 @@
excludefromjob = list(JOB_HEAD_OF_SECURITY)
item_owner = list(JOB_HEAD_OF_SECURITY)
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "A miniaturized combat shotgun. May be found in Head of Security's locker or strapped to their back."
/obj/item/gun/ballistic/shotgun/automatic/combat/compact/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/gun/ballistic/shotgun/automatic/combat/compact)
@@ -276,6 +336,9 @@
excludefromjob = list(JOB_CAPTAIN, JOB_RESEARCH_DIRECTOR, JOB_HEAD_OF_PERSONNEL)
item_owner = list(JOB_CAPTAIN, JOB_RESEARCH_DIRECTOR)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "Only two of these devices exist on the station, with one sitting in the Teleporter Room \
+ for emergencies, and the other in the Captain's Quarters for personal use."
/obj/item/hand_tele/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/hand_tele)
@@ -286,6 +349,8 @@
excludefromjob = list(JOB_CAPTAIN)
item_owner = list(JOB_CAPTAIN)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A special yellow jetpack found in the Suit Storage Unit in the Captain's Quarters."
/obj/item/tank/jetpack/oxygen/captain/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/tank/jetpack/oxygen/captain)
@@ -296,6 +361,9 @@
excludefromjob = list(JOB_CHIEF_ENGINEER)
item_owner = list(JOB_CHIEF_ENGINEER)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A pair of magnetic boots found in the Chief Engineer's Suit Storage Unit. \
+ May also be found on their person, concealed beneath their MODsuit."
/obj/item/clothing/shoes/magboots/advance/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/shoes/magboots/advance)
@@ -306,6 +374,9 @@
excludefromjob = list(JOB_CAPTAIN)
item_owner = list(JOB_CAPTAIN)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A gold medal found in the medal box in the Captain's Quarters. \
+ The Captain usually also has one pinned to their jumpsuit."
/obj/item/clothing/accessory/medal/gold/captain/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/accessory/medal/gold/captain)
@@ -316,6 +387,9 @@
excludefromjob = list(JOB_CHIEF_MEDICAL_OFFICER)
item_owner = list(JOB_CHIEF_MEDICAL_OFFICER)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "The Chief Medical Officer's personal medical injector. \
+ Usually found amongst their medical supplies on their person, in their belt, or otherwise in their locker."
/obj/item/reagent_containers/hypospray/cmo/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/reagent_containers/hypospray/cmo)
@@ -324,6 +398,9 @@
name = "the nuclear authentication disk"
targetitem = /obj/item/disk/nuclear
excludefromjob = list(JOB_CAPTAIN)
+ difficulty = 5
+ steal_hint = "THAT disk - you know the one. Carried by the Captain at all times (hopefully). \
+ Difficult to miss, but if you can't find it, the Head of Security and Captain both have devices to track its precise location."
/obj/item/disk/nuclear/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/disk/nuclear)
@@ -337,6 +414,8 @@
excludefromjob = list(JOB_HEAD_OF_SECURITY, JOB_WARDEN)
item_owner = list(JOB_HEAD_OF_SECURITY)
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "An ablative trechcoat found on the shelves of the Armory."
/obj/item/clothing/suit/hooded/ablative/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/suit/hooded/ablative)
@@ -347,6 +426,9 @@
excludefromjob = list(JOB_RESEARCH_DIRECTOR)
item_owner = list(JOB_RESEARCH_DIRECTOR)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A special suit of armor found in the possession of the Research Director. \
+ You may otherwise find it in their locker."
/obj/item/clothing/suit/armor/reactive/teleport/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/suit/armor/reactive/teleport)
@@ -356,6 +438,11 @@
valid_containers = list(/obj/item/folder)
targetitem = /obj/item/documents
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A set of papers belonging to a megaconglomerate. \
+ Nanotrasen documents can easily be found in the station's vault. \
+ For other corporations, you may find them in strange and distant places. \
+ A photocopy may also suffice."
/obj/item/documents/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/documents) //Any set of secret documents. Doesn't have to be NT's
@@ -365,6 +452,8 @@
valid_containers = list(/obj/item/nuke_core_container)
targetitem = /obj/item/nuke_core
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "The core of the station's self-destruct device, found in the vault."
/obj/item/nuke_core/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/nuke_core)
@@ -379,6 +468,8 @@
excludefromjob = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST, JOB_ROBOTICIST, JOB_GENETICIST)
item_owner = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST)
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "The hard drive of the master research server, found in R&D's server room."
/obj/item/computer_disk/hdd_theft/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/computer_disk/hdd_theft)
@@ -392,6 +483,8 @@
name = "a sliver of a supermatter crystal"
targetitem = /obj/item/nuke_core/supermatter_sliver
valid_containers = list(/obj/item/nuke_core_container/supermatter)
+ difficulty = 5
+ steal_hint = "A small shard of the station's supermatter crystal engine."
/datum/objective_item/steal/supermatter/New()
special_equipment += /obj/item/storage/box/syndie_kit/supermatter
@@ -404,6 +497,8 @@
/datum/objective_item/steal/functionalai
name = "a functional AI"
targetitem = /obj/item/aicard
+ difficulty = 5
+ steal_hint = "An intellicard (or MODsuit) containing an active, functional AI."
/datum/objective_item/steal/functionalai/New()
. = ..()
@@ -435,6 +530,8 @@
item_owner = list(JOB_CHIEF_ENGINEER)
altitems = list(/obj/item/photo)
exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "The blueprints of the station, found in the Chief Engineer's locker, or on their person. A picture may suffice."
/obj/item/areaeditor/blueprints/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/areaeditor/blueprints)
@@ -453,6 +550,8 @@
targetitem = /obj/item/blackbox
excludefromjob = list(JOB_CHIEF_ENGINEER, JOB_STATION_ENGINEER, JOB_ATMOSPHERIC_TECHNICIAN)
exists_on_map = TRUE
+ difficulty = 4
+ steal_hint = "The station's data Blackbox, found solely within Telecommunications."
/obj/item/blackbox/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/blackbox)
@@ -466,6 +565,8 @@
excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER)
item_owner = list(JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "A basic pair of insulated gloves, usually worn by Assistants, Engineers, or Cargo Technicians."
/obj/item/clothing/gloves/color/yellow/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/clothing/gloves/color/yellow)
@@ -475,6 +576,8 @@
targetitem = /obj/item/toy/plush/moth
excludefromjob = list(JOB_PSYCHOLOGIST, JOB_PARAMEDIC, JOB_CHEMIST, JOB_MEDICAL_DOCTOR, JOB_VIROLOGIST, JOB_CHIEF_MEDICAL_OFFICER, JOB_CORONER)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "A moth plush toy. The Psychologist has one to help console patients."
/obj/item/toy/plush/moth/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/toy/plush/moth)
@@ -483,6 +586,8 @@
name = "cute lizard plush toy"
targetitem = /obj/item/toy/plush/lizard_plushie
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "A lizard plush toy. Often found hidden in maintenance."
/obj/item/toy/plush/lizard_plushie/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/toy/plush/lizard_plushie)
@@ -492,6 +597,8 @@
targetitem = /obj/item/stamp/denied
excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_SHAFT_MINER)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "Cargo often has multiple of these red stamps lying around to process paperwork."
/obj/item/stamp/denied/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/stamp/denied)
@@ -501,6 +608,8 @@
targetitem = /obj/item/stamp/granted
excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_SHAFT_MINER)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "Cargo often has multiple of these green stamps lying around to process paperwork."
/obj/item/stamp/granted/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/stamp/granted)
@@ -510,6 +619,9 @@
targetitem = /obj/item/book/manual/wiki/security_space_law
excludefromjob = list(JOB_SECURITY_OFFICER, JOB_WARDEN, JOB_HEAD_OF_SECURITY, JOB_LAWYER, JOB_DETECTIVE)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "Sometimes found in the possession of members of Security and Lawyers. \
+ The courtroom and the library are also good places to look."
/obj/item/book/manual/wiki/security_space_law/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/book/manual/wiki/security_space_law)
@@ -520,6 +632,8 @@
excludefromjob = list(JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER, JOB_SCIENTIST, JOB_RESEARCH_DIRECTOR, JOB_GENETICIST, JOB_ROBOTICIST)
item_owner = list(JOB_CHIEF_ENGINEER)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "A tool often used by Engineers, Atmospherics Technicians, and Ordnance Technicians."
/obj/item/pipe_dispenser/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/pipe_dispenser)
@@ -529,6 +643,254 @@
targetitem = /obj/item/storage/fancy/donut_box
excludefromjob = list(JOB_CAPTAIN, JOB_CHIEF_ENGINEER, JOB_HEAD_OF_PERSONNEL, JOB_HEAD_OF_SECURITY, JOB_QUARTERMASTER, JOB_CHIEF_MEDICAL_OFFICER, JOB_RESEARCH_DIRECTOR, JOB_SECURITY_OFFICER, JOB_WARDEN, JOB_LAWYER, JOB_DETECTIVE)
exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "Everyone has a box of donuts - you may most commonly find them on the Bridge, within Security, or in any department's break room."
/obj/item/storage/fancy/donut_box/add_stealing_item_objective()
return add_item_to_steal(src, /obj/item/storage/fancy/donut_box)
+
+/datum/objective_item/steal/spy
+ objective_type = OBJECTIVE_ITEM_TYPE_SPY
+
+/datum/objective_item/steal/spy/lamarr
+ name = "The Research Director's pet headcrab"
+ targetitem = /obj/item/clothing/mask/facehugger/lamarr
+ excludefromjob = list(JOB_RESEARCH_DIRECTOR)
+ exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "The Research Director's pet headcrab, Lamarr, found in a secure cage in their office."
+
+/obj/item/clothing/mask/facehugger/lamarr/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/clothing/mask/facehugger/lamarr)
+
+/datum/objective_item/steal/spy/disabler
+ name = "a disabler"
+ targetitem = /obj/item/gun/energy/disabler
+ excludefromjob = list(
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ difficulty = 2
+ steal_hint = "A hand-held disabler, often found in the possession of Security Officers."
+
+/datum/objective_item/steal/spy/energy_gun
+ name = "an energy gun"
+ targetitem = /obj/item/gun/energy/e_gun
+ excludefromjob = list(
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_QUARTERMASTER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "A two-mode energy gun, found in the station's Armory, as well as in the hands of some heads of staff for personal defense."
+
+/datum/objective_item/steal/spy/energy_gun/check_special_completion(obj/item/thing)
+ return thing.type == /obj/item/gun/energy/e_gun
+
+/obj/item/gun/energy/e_gun/add_stealing_item_objective()
+ if(type == /obj/item/gun/energy/e_gun)
+ return add_item_to_steal(src, /obj/item/gun/energy/e_gun)
+
+/datum/objective_item/steal/spy/laser_gun
+ name = "a laser gun"
+ targetitem = /obj/item/gun/energy/laser
+ excludefromjob = list(
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_QUARTERMASTER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A simple laser gun, found in the station's Armory."
+
+/datum/objective_item/steal/spy/laser_gun/check_special_completion(obj/item/thing)
+ return thing.type == /obj/item/gun/energy/laser
+
+/obj/item/gun/energy/laser/add_stealing_item_objective()
+ if(type == /obj/item/gun/energy/laser)
+ return add_item_to_steal(src, /obj/item/gun/energy/laser)
+
+/datum/objective_item/steal/spy/shotgun
+ name = "a riot shotgun"
+ targetitem = /obj/item/gun/ballistic/shotgun/riot
+ excludefromjob = list(
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "A shotgun found in the station's Armory for riot suppression. Doesn't miss."
+
+/obj/item/gun/ballistic/shotgun/riot/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/gun/ballistic/shotgun/riot)
+
+/datum/objective_item/steal/spy/temp_gun
+ name = "security's temperature gun"
+ targetitem = /obj/item/gun/energy/temperature/security
+ excludefromjob = list(
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ exists_on_map = TRUE
+ difficulty = 2 // lowered for the meme
+ steal_hint = "Security's TRUSTY temperature gun, found in the station's Armory."
+
+/obj/item/gun/energy/temperature/security/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/gun/energy/temperature/security)
+
+/datum/objective_item/steal/spy/stamp
+ name = "a head of staff's stamp"
+ targetitem = /obj/item/stamp/head
+ excludefromjob = list(
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_QUARTERMASTER,
+ JOB_RESEARCH_DIRECTOR,
+ )
+ exists_on_map = TRUE
+ difficulty = 1
+ steal_hint = "A stamp owned by a head of staff, from their offices."
+
+/obj/item/stamp/head/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/stamp/head)
+
+/datum/objective_item/steal/spy/sunglasses
+ name = "sunglasses"
+ targetitem = /obj/item/clothing/glasses/sunglasses
+ excludefromjob = list(
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_LAWYER,
+ JOB_QUARTERMASTER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ difficulty = 1
+ steal_hint = "A pair of sunglasses. Lawyers often have a few pairs, as do some heads of staff. \
+ You can also obtain a pair from dissassembling hudglasses."
+
+/datum/objective_item/steal/spy/ce_modsuit
+ name = "the cheif engineer's advanced MOD control unit"
+ targetitem = /obj/item/mod/control/pre_equipped/advanced
+ excludefromjob = list(JOB_CHIEF_ENGINEER)
+ exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "An advanced version of the standard Engineering MODsuit commonly worn by the Chief Engineer."
+
+/obj/item/mod/control/pre_equipped/advanced/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/advanced)
+
+/datum/objective_item/steal/spy/rd_modsuit
+ name = "the research director's research MOD control unit"
+ targetitem = /obj/item/mod/control/pre_equipped/research
+ excludefromjob = list(JOB_RESEARCH_DIRECTOR)
+ exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "A bulky MODsuit commonly worn by the Research Director to protect themselves from the hazards of their work."
+
+/obj/item/mod/control/pre_equipped/research/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/research)
+
+/datum/objective_item/steal/spy/cmo_modsuit
+ name = "the chief medical officer's rescure MOD control unit"
+ targetitem = /obj/item/mod/control/pre_equipped/rescue
+ excludefromjob = list(JOB_CHIEF_MEDICAL_OFFICER)
+ exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "A MODsuit sometimes equipped by the Chief Medical Officer to perform rescue opperations in hazardous environments."
+
+/obj/item/mod/control/pre_equipped/rescue/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/rescue)
+
+/datum/objective_item/steal/spy/hos_modsuit
+ name = "the head of security's safeguard MOD control unit"
+ targetitem = /obj/item/mod/control/pre_equipped/safeguard
+ excludefromjob = list(JOB_HEAD_OF_SECURITY)
+ exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "An advanced MODsuit sometimes worn by the Head of Security when needing to detain hostiles invading the station."
+
+/obj/item/mod/control/pre_equipped/safeguard/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/mod/control/pre_equipped/safeguard)
+
+/datum/objective_item/steal/spy/stun_baton
+ name = "a stun baton"
+ targetitem = /obj/item/melee/baton/security
+ excludefromjob = list(
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ difficulty = 2
+ steal_hint = "Steal any stun baton from Security."
+
+/datum/objective_item/steal/spy/stun_baton/check_special_completion(obj/item/thing)
+ return !istype(thing, /obj/item/melee/baton/security/cattleprod)
+
+/datum/objective_item/steal/spy/det_baton
+ name = "the detective's baton"
+ targetitem = /obj/item/melee/baton
+ excludefromjob = list(
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ exists_on_map = TRUE
+ difficulty = 2
+ steal_hint = "The detective's old wooden truncheon, commonly found on their person for self defense."
+
+/datum/objective_item/steal/spy/det_baton/check_special_completion(obj/item/thing)
+ return thing.type == /obj/item/melee/baton
+
+/obj/item/melee/baton/add_stealing_item_objective()
+ if(type == /obj/item/melee/baton)
+ return add_item_to_steal(src, /obj/item/melee/baton)
+
+/datum/objective_item/steal/spy/captain_sabre_sheathe
+ name = "the captain's sabre sheathe"
+ targetitem = /obj/item/storage/belt/sabre
+ excludefromjob = list(JOB_CAPTAIN)
+ exists_on_map = TRUE
+ difficulty = 3
+ steal_hint = "The sheathe for the captain's sabre, found in their closet or strapped to their waist at all times."
+
+/obj/item/storage/belt/sabre/add_stealing_item_objective()
+ return add_item_to_steal(src, /obj/item/storage/belt/sabre)
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 63e95b2a74e76..69ae33f79592b 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -136,8 +136,6 @@
var/market_verb = "Customer"
var/payment_department = ACCOUNT_ENG
- /// For storing and overriding ui id
- var/tgui_id // ID of TGUI interface
///Is this machine currently in the atmos machinery queue?
var/atmos_processing = FALSE
/// world.time of last use by [/mob/living]
diff --git a/code/game/machinery/computer/accounting.dm b/code/game/machinery/computer/accounting.dm
index 475bf404c1ce0..d804b8efe5d94 100644
--- a/code/game/machinery/computer/accounting.dm
+++ b/code/game/machinery/computer/accounting.dm
@@ -21,10 +21,9 @@
for(var/current_account as anything in SSeconomy.bank_accounts_by_id)
var/datum/bank_account/current_bank_account = SSeconomy.bank_accounts_by_id[current_account]
- var/job_title = current_bank_account.account_job?.title
player_accounts += list(list(
"name" = current_bank_account.account_holder,
- "job" = job_title ? job_title : "No Job", // because this can be null
+ "job" = current_bank_account.account_job?.title || "No job", // because this can be null
"balance" = round(current_bank_account.account_balance),
"modifier" = round((current_bank_account.payday_modifier * 0.9), 0.1),
))
@@ -32,4 +31,3 @@
data["AuditLog"] = audit_list
data["Crashing"] = HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)
return data
-
diff --git a/code/game/machinery/pipe/construction.dm b/code/game/machinery/pipe/construction.dm
index 22f66fd1de73d..af6b477e90ba8 100644
--- a/code/game/machinery/pipe/construction.dm
+++ b/code/game/machinery/pipe/construction.dm
@@ -85,6 +85,16 @@ Buildable meters
/obj/item/pipe/quaternary/pipe
icon_state_preview = "manifold4w"
pipe_type = /obj/machinery/atmospherics/pipe/smart
+/obj/item/pipe/quaternary/pipe/crafted
+
+/obj/item/pipe/quaternary/pipe/crafted/Initialize(mapload, _pipe_type, _dir, obj/machinery/atmospherics/make_from, device_color, device_init_dir = SOUTH)
+ . = ..()
+ pipe_type = /obj/machinery/atmospherics/pipe/smart
+ pipe_color = COLOR_VERY_LIGHT_GRAY
+ p_init_dir = ALL_CARDINALS
+ setDir(SOUTH)
+ update()
+
/obj/item/pipe/quaternary/he_pipe
icon_state_preview = "he_manifold4w"
pipe_type = /obj/machinery/atmospherics/pipe/heat_exchanging/manifold4w
@@ -251,6 +261,23 @@ Buildable meters
qdel(src)
+/obj/item/pipe/welder_act(mob/living/user, obj/item/welder)
+ . = ..()
+ if(istype(pipe_type, /obj/machinery/atmospherics/components))
+ return TRUE
+ if(!welder.tool_start_check(user, amount=2))
+ return TRUE
+ add_fingerprint(user)
+
+ if(welder.use_tool(src, user, 2 SECONDS, volume=2))
+ new /obj/item/sliced_pipe(drop_location())
+ user.visible_message( \
+ "[user] welds \the [src] in two.", \
+ span_notice("You weld \the [src] in two."), \
+ span_hear("You hear welding."))
+
+ qdel(src)
+
/**
* Attempt to automatically resolve a pipe conflict by reconfiguring any smart pipes involved.
*
diff --git a/code/game/machinery/syndicatebomb.dm b/code/game/machinery/syndicatebomb.dm
index ab381b14fad25..ebe24b449748b 100644
--- a/code/game/machinery/syndicatebomb.dm
+++ b/code/game/machinery/syndicatebomb.dm
@@ -605,7 +605,7 @@
var/list/choosable_dimensions = list()
var/datum/radial_menu_choice/null_choice = new
null_choice.name = DIMENSION_CHOICE_RANDOM
- choosable_dimensions += null_choice
+ choosable_dimensions[DIMENSION_CHOICE_RANDOM] = null_choice
for(var/datum/dimension_theme/theme as anything in SSmaterials.dimensional_themes)
var/datum/radial_menu_choice/theme_choice = new
theme_choice.image = image(initial(theme.icon), initial(theme.icon_state))
@@ -631,9 +631,11 @@
var/theme_count = length(SSmaterials.dimensional_themes)
var/num_affected = 0
for(var/turf/affected as anything in affected_turfs)
- var/datum/dimension_theme/theme_to_use = chosen_theme
+ var/datum/dimension_theme/theme_to_use
if(isnull(chosen_theme))
theme_to_use = SSmaterials.dimensional_themes[SSmaterials.dimensional_themes[rand(1, theme_count)]]
+ else
+ theme_to_use = SSmaterials.dimensional_themes[chosen_theme]
if(!theme_to_use.can_convert(affected))
continue
num_affected++
diff --git a/code/game/objects/items/cards_ids.dm b/code/game/objects/items/cards_ids.dm
index d395905cd2631..26363b57bde6a 100644
--- a/code/game/objects/items/cards_ids.dm
+++ b/code/game/objects/items/cards_ids.dm
@@ -125,9 +125,8 @@
/obj/item/card/id/Initialize(mapload)
. = ..()
- var/datum/bank_account/blank_bank_account = new /datum/bank_account("Unassigned", player_account = FALSE)
+ var/datum/bank_account/blank_bank_account = new("Unassigned", SSjob.GetJobType(/datum/job/unassigned), player_account = FALSE)
registered_account = blank_bank_account
- blank_bank_account.account_job = new /datum/job/unassigned
registered_account.replaceable = TRUE
// Applying the trim updates the label and icon, so don't do this twice.
@@ -1229,7 +1228,7 @@
/obj/item/card/id/advanced/debug/Initialize(mapload)
. = ..()
registered_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
- registered_account.account_job = new /datum/job/admin // so we can actually use this account without being filtered as a "departmental" card
+ registered_account.account_job = SSjob.GetJobType(/datum/job/admin) // so we can actually use this account without being filtered as a "departmental" card
/obj/item/card/id/advanced/prisoner
name = "prisoner ID card"
diff --git a/code/game/objects/items/devices/traitordevices.dm b/code/game/objects/items/devices/traitordevices.dm
index 3515e7f52c3ce..8e8f2578fa4b1 100644
--- a/code/game/objects/items/devices/traitordevices.dm
+++ b/code/game/objects/items/devices/traitordevices.dm
@@ -202,9 +202,104 @@ effective or pretty fucking useless.
target = round(target)
wavelength = clamp(target, 0, 120)
+/datum/action/item_action/stealth_mode
+ name = "Toggle Stealth"
+ desc = "Makes you invisible to the naked eye."
+ button_icon = 'icons/mob/actions/actions_minor_antag.dmi'
+ button_icon_state = "ninja_cloak"
+ /// Whether stealth is active or not
+ var/stealth_engaged = FALSE
+ /// The amount of time the stealth mode can be active for, drains to 0 when active
+ var/charge = 30 SECONDS
+ /// The maximum amount of time the stealth mode can be active for
+ var/max_charge = 30 SECONDS
+ /// The minimum alpha value for the stealth mode
+ var/min_alpha = 0
+ /// Whether the stealth mode recharges while active
+ /// if TRUE standing in darkness will recharge even while active
+ /// if FALSE it will not uncharge, but not recharge while in darkness
+ var/recharge_while_active = TRUE
+
+/datum/action/item_action/stealth_mode/is_action_active(atom/movable/screen/movable/action_button/current_button)
+ return stealth_engaged
+
+/datum/action/item_action/stealth_mode/Grant(mob/grant_to)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+
+/datum/action/item_action/stealth_mode/Remove(mob/remove_from)
+ if(!isnull(owner) && stealth_engaged)
+ stealth_off()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/datum/action/item_action/stealth_mode/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+
+ if(stealth_engaged)
+ stealth_off()
+ else
+ stealth_on()
+
+/datum/action/item_action/stealth_mode/proc/stealth_on()
+ animate(owner, alpha = get_alpha(), time = 0.5 SECONDS)
+ apply_wibbly_filters(owner)
+ stealth_engaged = TRUE
+ build_all_button_icons(UPDATE_BUTTON_STATUS|UPDATE_BUTTON_BACKGROUND)
+ owner.balloon_alert(owner, "stealth mode engaged")
+
+/datum/action/item_action/stealth_mode/proc/stealth_off()
+ owner.alpha = initial(owner.alpha)
+ remove_wibbly_filters(owner)
+ stealth_engaged = FALSE
+ build_all_button_icons(UPDATE_BUTTON_STATUS|UPDATE_BUTTON_BACKGROUND)
+ owner.balloon_alert(owner, "stealth mode disengaged")
+
+/datum/action/item_action/stealth_mode/proc/get_alpha()
+ return clamp(255 - (255 * charge / max_charge), min_alpha, 255)
+
+/datum/action/item_action/stealth_mode/process(seconds_per_tick)
+ if(!stealth_engaged)
+ // Recharge over time
+ charge = min(max_charge, charge + (max_charge * 0.04) * seconds_per_tick)
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+ return
+
+ if(charge <= 0)
+ stealth_off()
+ return
+
+ var/turf/our_turf = get_turf(owner)
+ var/lumcount = our_turf?.get_lumcount() || 0
+ if(lumcount > 0.3)
+ // Decay charge while invisible+ in the light
+ charge = max(0, charge - (max_charge * 0.05) * seconds_per_tick)
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+
+ else if(recharge_while_active)
+ // Return charage while invisible + in the darkness + recharge_while_active
+ charge = min(max_charge, charge + (max_charge * 0.1) * seconds_per_tick)
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+
+ animate(owner, alpha = get_alpha(), time = 1 SECONDS, flags = ANIMATION_PARALLEL)
+
+/datum/action/item_action/stealth_mode/update_button_status(atom/movable/screen/movable/action_button/current_button, force)
+ . = ..()
+ current_button.maptext_x = 9
+ current_button.maptext = MAPTEXT_TINY_UNICODE("[round(charge / max_charge * 100, 0.01)]%")
+
+/datum/action/item_action/stealth_mode/weaker
+ charge = 15 SECONDS
+ max_charge = 15 SECONDS
+ min_alpha = 20
+ recharge_while_active = FALSE
+
/obj/item/shadowcloak
name = "cloaker belt"
- desc = "Makes you invisible for short periods of time. Recharges in darkness."
+ desc = "Makes you invisible for short periods of time. Recharges in darkness, even while active."
icon = 'icons/obj/clothing/belts.dmi'
icon_state = "utility"
inhand_icon_state = "utility"
@@ -214,66 +309,16 @@ effective or pretty fucking useless.
slot_flags = ITEM_SLOT_BELT
attack_verb_continuous = list("whips", "lashes", "disciplines")
attack_verb_simple = list("whip", "lash", "discipline")
-
- var/mob/living/carbon/human/user = null
- var/charge = 300
- var/max_charge = 300
- var/on = FALSE
- actions_types = list(/datum/action/item_action/toggle)
-
-/obj/item/shadowcloak/ui_action_click(mob/user)
- if(user.get_item_by_slot(ITEM_SLOT_BELT) == src)
- if(!on)
- Activate(usr)
-
- else
- Deactivate()
-
- return
+ actions_types = list(/datum/action/item_action/stealth_mode)
/obj/item/shadowcloak/item_action_slot_check(slot, mob/user)
- if(slot & ITEM_SLOT_BELT)
- return 1
-
-/obj/item/shadowcloak/proc/Activate(mob/living/carbon/human/user)
- if(!user)
- return
-
- to_chat(user, span_notice("You activate [src]."))
- src.user = user
- START_PROCESSING(SSobj, src)
- on = TRUE
-
-/obj/item/shadowcloak/proc/Deactivate()
- to_chat(user, span_notice("You deactivate [src]."))
- STOP_PROCESSING(SSobj, src)
- if(user)
- user.alpha = initial(user.alpha)
-
- on = FALSE
- user = null
-
-/obj/item/shadowcloak/dropped(mob/user)
- ..()
- if(user && user.get_item_by_slot(ITEM_SLOT_BELT) != src)
- Deactivate()
-
-/obj/item/shadowcloak/process(seconds_per_tick)
- if(user.get_item_by_slot(ITEM_SLOT_BELT) != src)
- Deactivate()
- return
-
- var/turf/T = get_turf(src)
- if(on)
- var/lumcount = T.get_lumcount()
-
- if(lumcount > 0.3)
- charge = max(0, charge - 12.5 * seconds_per_tick)//Quick decrease in light
-
- else
- charge = min(max_charge, charge + 25 * seconds_per_tick) //Charge in the dark
+ return slot & slot_flags
- animate(user,alpha = clamp(255 - charge,0,255),time = 10)
+/obj/item/shadowcloak/weaker
+ name = "stealth belt"
+ desc = "Makes you nigh-invisible to the naked eye for a short period of time. \
+ Lasts indefinitely in darkness, but will not recharge unless inactive."
+ actions_types = list(/datum/action/item_action/stealth_mode/weaker)
/// Checks if a given atom is in range of a radio jammer, returns TRUE if it is.
/proc/is_within_radio_jammer_range(atom/source)
diff --git a/code/game/objects/items/food/bait.dm b/code/game/objects/items/food/bait.dm
index aa9a0e7bd9e95..047a8a7cd58ce 100644
--- a/code/game/objects/items/food/bait.dm
+++ b/code/game/objects/items/food/bait.dm
@@ -36,9 +36,8 @@
lefthand_file = 'icons/mob/inhands/items_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items_righthand.dmi'
inhand_icon_state = "pen"
- food_reagents = list(/datum/reagent/drug/kronkaine = 1)
+ food_reagents = list(/datum/reagent/drug/kronkaine = 2) //The kronkaine is the thing that makes this a great bait.
tastes = list("hypocrisy" = 1)
- bait_quality = TRAIT_GREAT_QUALITY_BAIT
/obj/item/food/bait/doughball
name = "doughball"
diff --git a/code/game/objects/items/food/dough.dm b/code/game/objects/items/food/dough.dm
index 6ca618bc6e0cb..283cd347f55bd 100644
--- a/code/game/objects/items/food/dough.dm
+++ b/code/game/objects/items/food/dough.dm
@@ -46,7 +46,7 @@
/obj/item/food/pizzabread/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/customizable_reagent_holder, /obj/item/food/pizza/margherita, CUSTOM_INGREDIENT_ICON_SCATTER, max_ingredients = 12)
+ AddComponent(/datum/component/customizable_reagent_holder, /obj/item/food/pizza, CUSTOM_INGREDIENT_ICON_SCATTER, max_ingredients = 12)
/obj/item/food/doughslice
name = "dough slice"
diff --git a/code/game/objects/items/food/lizard.dm b/code/game/objects/items/food/lizard.dm
index 5f7092c64db58..729ad4d38a971 100644
--- a/code/game/objects/items/food/lizard.dm
+++ b/code/game/objects/items/food/lizard.dm
@@ -494,6 +494,7 @@
//Pizza Dishes
/obj/item/food/pizza/flatbread
icon = 'icons/obj/food/lizard.dmi'
+ icon_state = null
slice_type = null
/obj/item/food/pizza/flatbread/rustic
diff --git a/code/game/objects/items/food/pizza.dm b/code/game/objects/items/food/pizza.dm
index b93cd7ed7219c..834484872d650 100644
--- a/code/game/objects/items/food/pizza.dm
+++ b/code/game/objects/items/food/pizza.dm
@@ -1,25 +1,28 @@
// Pizza (Whole)
/obj/item/food/pizza
+ name = "pizza"
icon = 'icons/obj/food/pizza.dmi'
w_class = WEIGHT_CLASS_NORMAL
max_volume = 80
+ icon_state = "pizzamargherita"
food_reagents = list(
/datum/reagent/consumable/nutriment = 28,
/datum/reagent/consumable/nutriment/protein = 3,
/datum/reagent/consumable/tomatojuice = 6,
/datum/reagent/consumable/nutriment/vitamin = 5,
)
- tastes = list("crust" = 1, "tomato" = 1, "cheese" = 1)
- foodtypes = GRAIN | DAIRY | VEGETABLES
+ tastes = list("crust" = 1, "tomato" = 1)
+ foodtypes = GRAIN
venue_value = FOOD_PRICE_CHEAP
crafting_complexity = FOOD_COMPLEXITY_2
/// type is spawned 6 at a time and replaces this pizza when processed by cutting tool
var/obj/item/food/pizzaslice/slice_type
+ slice_type = /obj/item/food/pizzaslice
///What label pizza boxes use if this pizza spawns in them.
var/boxtag = ""
/obj/item/food/pizza/raw
- foodtypes = GRAIN | DAIRY | VEGETABLES | RAW
+ foodtypes = GRAIN | RAW
slice_type = null
crafting_complexity = FOOD_COMPLEXITY_2
@@ -34,9 +37,11 @@
// Pizza Slice
/obj/item/food/pizzaslice
+ name = "pizza slice"
icon = 'icons/obj/food/pizza.dmi'
food_reagents = list(/datum/reagent/consumable/nutriment = 5)
- foodtypes = GRAIN | DAIRY | VEGETABLES
+ icon_state = "pizzamargheritaslice"
+ foodtypes = GRAIN
w_class = WEIGHT_CLASS_SMALL
decomp_type = /obj/item/food/pizzaslice/moldy
crafting_complexity = FOOD_COMPLEXITY_2
diff --git a/code/game/objects/items/food/sweets.dm b/code/game/objects/items/food/sweets.dm
index 5c638077d16c5..d757261ac0154 100644
--- a/code/game/objects/items/food/sweets.dm
+++ b/code/game/objects/items/food/sweets.dm
@@ -79,6 +79,15 @@
w_class = WEIGHT_CLASS_TINY
crafting_complexity = FOOD_COMPLEXITY_1
+/obj/item/food/virtual_chocolate
+ name = "virtual chocolate bar"
+ desc = "Digital food only gives off the sensation of eating... without any of the nutritional benefits."
+ icon_state = "virtual_chocolate"
+ tastes = list("nothing" = 1)
+ foodtypes = NONE
+ w_class = WEIGHT_CLASS_TINY
+
+
/obj/item/food/chococoin
name = "chocolate coin"
desc = "A completely edible but non-flippable festive coin."
diff --git a/code/game/objects/items/grenades/ghettobomb.dm b/code/game/objects/items/grenades/ghettobomb.dm
index b77216a9104e8..9bc8c1c515f9a 100644
--- a/code/game/objects/items/grenades/ghettobomb.dm
+++ b/code/game/objects/items/grenades/ghettobomb.dm
@@ -1,12 +1,10 @@
-//improvised explosives//
-
/obj/item/grenade/iedcasing
- name = "improvised firebomb"
- desc = "A weak, improvised incendiary device."
+ name = "improvised explosive"
+ desc = "An improvised explosive device."
w_class = WEIGHT_CLASS_SMALL
icon = 'icons/obj/weapons/grenade.dmi'
- icon_state = "improvised_grenade"
- icon_state_preview = "ied_preview"
+ base_icon_state = "pipebomb"
+ icon_state = "slicedapart"
inhand_icon_state = "flashbang"
lefthand_file = 'icons/mob/inhands/equipment/security_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
@@ -15,67 +13,272 @@
obj_flags = CONDUCTS_ELECTRICITY
slot_flags = ITEM_SLOT_BELT
active = FALSE
- det_time = 50
+ shrapnel_type = /obj/projectile/bullet/shrapnel/ied
+ det_time = 225 SECONDS //this is handled by assemblies now
display_timer = FALSE
- var/check_parts = FALSE
- var/range = 3
- var/list/times
+ /// Explosive power
+ var/power = 5
+ /// Our assembly that when activated causes us to explode
+ var/obj/item/assembly/activator
+ /// List of effects, the key is a path to compare to and the value is incremented by one everytime theres one that is the same type in our contents
+ var/list/effects = list(
+ /obj/item/food/meat/slab = 0,
+ /obj/item/paper = 0,
+ /obj/item/shard = 0,
+ /obj/item/stack/ore/bluespace_crystal/refined = 0,
+ )
+ /// Cooldown to prevent spam
+ COOLDOWN_DECLARE(spam_cd)
/obj/item/grenade/iedcasing/Initialize(mapload)
. = ..()
- add_overlay("improvised_grenade_filled")
- add_overlay("improvised_grenade_wired")
- times = list("5" = 10, "-1" = 20, "[rand(30, 80)]" = 50, "[rand(65, 180)]" = 20)// "Premature, Dud, Short Fuse, Long Fuse"=[weighting value]
- det_time = text2num(pick_weight(times))
- if(det_time < 0) //checking for 'duds'
- range = 1
- det_time = rand(30, 80)
- else
- range = pick(2, 2, 2, 3, 3, 3, 4)
- if(check_parts) //since construction code calls this itself, no need to always call it. This does have the downside that adminspawned ones can potentially not have cans if they don't use the /spawned subtype.
- CheckParts()
+ if(ispath(activator))
+ var/obj/item/assembly/new_activator = new activator(src)
+ new_activator.toggle_secure()
+ activator = null
+ attach_activator(new_activator)
-/obj/item/grenade/iedcasing/spawned
- check_parts = TRUE
+/obj/item/grenade/iedcasing/proc/setup_effects_from_contents()
+ for(var/item in contents)
+ for(var/effect_type in effects)
+ if(!istype(item, effect_type))
+ continue
+ if(isstack(item))
+ var/obj/item/stack/as_stack = item
+ effects[effect_type] += as_stack.amount
+ else
+ effects[effect_type]++
+ break
-/obj/item/grenade/iedcasing/spawned/Initialize(mapload)
- new /obj/item/reagent_containers/cup/soda_cans/random(src)
- return ..()
+/obj/item/grenade/iedcasing/examine(mob/user)
+ . = ..()
+ . += span_notice("Using it in-hand activates the assembly, which means timers start timing and so on.")
+ . += span_notice("Using it off-hand allows you to configure the assembly, if possible.")
+ if(contents.len > 1) // above 1, so more than just the activator
+ . += span_warning("It seems to have something stuffed in it.")
+ if(isnull(activator))
+ return
+ . += activator.examine(user)
-/obj/item/grenade/iedcasing/CheckParts(list/parts_list)
- ..()
- var/obj/item/reagent_containers/cup/soda_cans/can = locate() in contents
- if(!can)
- stack_trace("[src] generated without a soda can!") //this shouldn't happen.
- qdel(src)
+// assembly handling
+
+/obj/item/grenade/iedcasing/IsAssemblyHolder()
+ return TRUE
+
+/obj/item/grenade/iedcasing/on_found(mob/finder)
+ if(activator)
+ activator.on_found(finder)
+
+/obj/item/grenade/iedcasing/Move()
+ . = ..()
+ if(activator)
+ activator.holder_movement()
+
+/obj/item/grenade/iedcasing/dropped()
+ . = ..()
+ if(activator)
+ activator.dropped()
+
+/obj/item/grenade/iedcasing/proc/process_activation(obj/item/assembly)
+ detonate()
+
+/obj/item/grenade/iedcasing/proc/attach_activator(obj/item/assembly/new_one)
+ if(activator)
return
- can.pixel_x = 0 //Reset the sprite's position to make it consistent with the rest of the IED
- can.pixel_y = 0
- var/mutable_appearance/can_underlay = new(can)
- can_underlay.layer = FLOAT_LAYER
- can_underlay.plane = FLOAT_PLANE
- underlays += can_underlay
+ activator = new_one
+ activator.holder = src
+ activator.on_attach()
+ activator.toggle_secure()
+ update_icon(UPDATE_ICON_STATE)
+/obj/item/grenade/iedcasing/change_det_time()
+ return
-/obj/item/grenade/iedcasing/attack_self(mob/user)
- if(!active)
- if(!botch_check(user))
- to_chat(user, span_warning("You light the [name]!"))
- cut_overlay("improvised_grenade_filled")
- arm_grenade(user, null, FALSE)
+//assembly handling end
+
+/obj/item/grenade/iedcasing/attack_hand(mob/user, list/modifiers)
+ if(loc == user) //if we were picked up already, this opening whenever picked up is not ok
+ activator.ui_interact(user) //if any
+ . = ..()
+ if(.)
+ return
+ if(isnull(activator))
+ return
+ activator.attack_hand()
+/obj/item/grenade/iedcasing/update_icon_state()
+ if(isnull(activator))
+ icon_state = "slicedapart" //this shouldnt happen but should prevent runtimes
+ return ..()
+ var/suffix = ""
+ var/obj/item/assembly/timer/as_timer = activator
+ var/obj/item/assembly/mousetrap/as_mousetrap = activator
+ var/obj/item/assembly/prox_sensor/as_prox = activator
+ if((istype(as_timer) && as_timer.timing) || (istype(as_mousetrap) && as_mousetrap.armed)) //these shouldve just had a common "active" variable or something
+ suffix = "-a"
+ else if(istype(as_prox))
+ suffix = as_prox.timing ? "-arming" : (as_prox.scanning ? "-a" : "")
+ icon_state = "[base_icon_state]-[initial(activator.name)][suffix]" //signalers detonate instantly so theyre not here
+ return ..()
+
+/obj/item/grenade/iedcasing/attack_self(mob/user)
+ if(isnull(activator) || !COOLDOWN_FINISHED(src, spam_cd))
+ balloon_alert(user, isnull(activator) ? "you shouldnt be seeing this" : "on cooldown!")
+ return
+ if(istype(activator, /obj/item/assembly/signaler))
+ return //no signallers, signallers send a signal and i can imagine this having bad sideeffects if some has multiple of the same frequency in their backpack and uses them inhand by accident
+ activator.activate()
+ update_icon(UPDATE_ICON_STATE)
+ user.balloon_alert_to_viewers("arming!")
+ COOLDOWN_START(src, spam_cd, 1 SECONDS)
+
/obj/item/grenade/iedcasing/detonate(mob/living/lanced_by) //Blowing that can up
+ if(effects[/obj/item/shard]) //this has to be before so it initializes us a pellet cloud or something
+ shrapnel_radius = effects[/obj/item/shard]
. = ..()
if(!.)
return
update_mob()
- explosion(src, devastation_range = -1, heavy_impact_range = -1, light_impact_range = 2, flame_range = 4) // small explosion, plus a very large fireball.
+ for(var/i = 1 to effects[/obj/item/food/meat/slab])
+ new /obj/effect/gibspawner/generic(loc)
+ if(effects[/obj/item/paper])
+ for(var/turf/open/floor in view(effects[/obj/item/paper], loc)) //this couldve been light impact range but fake pipebombs exploding into confetti is funny
+ new /obj/effect/decal/cleanable/confetti(floor)
+ var/heavy = floor(power * 0.2)
+ var/light = round(power * 0.7, 1)
+ var/flame = round(power + rand(-1, 1), 1)
+ explosion(loc, devastation_range = -1, heavy_impact_range = heavy, light_impact_range = light, flame_range = flame, explosion_cause = src)
+
+ if(effects[/obj/item/stack/ore/bluespace_crystal/refined])
+ for(var/mob/living/victim in view(light, loc))
+ do_teleport(victim, get_turf(victim), min(12, effects[/obj/item/stack/ore/bluespace_crystal/refined] * 3), asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE)
+
qdel(src)
-/obj/item/grenade/iedcasing/change_det_time()
- return //always be random.
+/obj/item/grenade/iedcasing/Destroy()
+ . = ..()
+ activator = null
-/obj/item/grenade/iedcasing/examine(mob/user)
+
+
+
+/obj/item/grenade/iedcasing/spawned
+ power = 2.5 //20u welding fuel
+ activator = /obj/item/assembly/timer
+
+#define MAX_STUFFINGS 3
+
+/obj/item/sliced_pipe
+ name = "halved pipe"
+ desc = "Two half-size pipes made from one."
+ w_class = WEIGHT_CLASS_SMALL
+ icon = 'icons/obj/weapons/grenade.dmi'
+ icon_state = "slicedapart"
+ /// Are wires inserted? If so, we are on the final step
+ var/wires_are_in = FALSE
+ /// Typecache of items we are allowed to stuff into the pipebomb for effects, only add items with effects
+ var/static/list/allowed = typecacheof(list(
+ /obj/item/food/meat/slab,
+ /obj/item/paper,
+ /obj/item/shard,
+ /obj/item/stack/ore/bluespace_crystal/refined,
+ ))
+ //this probably shouldve been a blacklist instead but god do i not wanna update this anytime a new assembly is added
+ /// A static list of types of assemblies that are allowed to be used to finish the bomb
+ var/static/list/allowed_activators = list(
+ /obj/item/assembly/signaler,
+ /obj/item/assembly/prox_sensor,
+ /obj/item/assembly/mousetrap,
+ /obj/item/assembly/mousetrap/armed,
+ /obj/item/assembly/timer,
+ /obj/item/assembly/wiremod,
+ /obj/item/assembly/voice,
+ )
+ /// Static list of reagent to explosive power
+ var/static/list/fuel_power = list(
+ /datum/reagent/fuel = 0.5,
+ /datum/reagent/gunpowder = 1,
+ /datum/reagent/nitroglycerin = 2,
+ /datum/reagent/tatp = 2.5,
+ )
+ /// Explosion power to be transferred to the new pipebomb
+ var/power = 5
+
+/obj/item/sliced_pipe/Initialize(mapload)
+ . = ..()
+ create_reagents(20, OPENCONTAINER)
+
+/obj/item/sliced_pipe/examine(mob/user)
. = ..()
- . += "You can't tell when it will explode!"
+ if(!wires_are_in)
+ . += span_notice("You could stuff something in, or fill it with fuel or some other volatile chemical..")
+ . += span_notice("Afterwards, add some cable.")
+ else
+ . += span_notice("The wires are just dangling from it, you need some sort of activating assembly .")
+
+/obj/item/sliced_pipe/attackby(obj/item/item, mob/user, params)
+ if(!wires_are_in)
+ // here we can stuff in additional objects for a cooler effect
+ if(is_type_in_typecache(item, allowed) && contents.len < MAX_STUFFINGS)
+ balloon_alert(user, "stuffed in")
+ var/atom/movable/to_put = item
+ if(isstack(item))
+ var/obj/item/stack/as_stack = item
+ to_put = as_stack.split_stack(user = null, amount = 1)
+ as_stack.merge_type = null //prevent them from merging inside for contents.len
+ to_put.forceMove(src)
+ return
+
+ //if the item has reagents lets allow it to transfer
+ if(item.reagents)
+ return ..()
+ if(reagents.total_volume < 5)
+ balloon_alert(user, "add more fuel!")
+ return
+
+ var/obj/item/stack/cable_coil/coil = item
+ if(!istype(coil))
+ return
+ if (coil.get_amount() < 15)
+ balloon_alert(user, "need 15 length!")
+ return
+ coil.use(15)
+
+ var/cur_power = 0
+ for(var/datum/reagent/reagent as anything in reagents.reagent_list)
+ if(!(reagent.type in fuel_power))
+ continue
+ cur_power += fuel_power[reagent.type] * reagent.volume / reagents.maximum_volume
+
+ power *= cur_power
+ power -= contents.len / 2
+
+ balloon_alert(user, "wires attached")
+ icon_state = "[icon_state]-cable"
+ reagents.flags = SEALED_CONTAINER
+ wires_are_in = TRUE
+ else // wires are in, lets finish this up
+ var/obj/item/assembly/assembly = item
+ if(!istype(assembly) || !(assembly.type in allowed_activators))
+ return
+ if(assembly.secured)
+ balloon_alert(user, "unsecure assembly first!")
+ return
+ if(!user.transferItemToLoc(assembly, src))
+ return
+ user.balloon_alert(user, "attached")
+
+ var/obj/item/grenade/iedcasing/pipebomb = new(drop_location())
+ for(var/atom/movable/item_inside as anything in contents)
+ item_inside.forceMove(pipebomb)
+
+ pipebomb.power = power
+ pipebomb.attach_activator(assembly)
+ pipebomb.setup_effects_from_contents()
+ var/was_in_hands = (loc == user)
+ qdel(src)
+ if(was_in_hands)
+ user.put_in_hands(pipebomb)
+
+#undef MAX_STUFFINGS
diff --git a/code/game/objects/items/robot/items/hud.dm b/code/game/objects/items/robot/items/hud.dm
index 6b11c71941b7f..7ee8a9386258b 100644
--- a/code/game/objects/items/robot/items/hud.dm
+++ b/code/game/objects/items/robot/items/hud.dm
@@ -1,10 +1,10 @@
/obj/item/borg/sight
var/sight_mode = null
+ icon = 'icons/obj/clothing/glasses.dmi'
/obj/item/borg/sight/xray
name = "\proper X-ray vision"
- icon = 'icons/obj/signs.dmi'
- icon_state = "securearea"
+ icon_state = "securityhudnight"
sight_mode = BORGXRAY
/obj/item/borg/sight/thermal
diff --git a/code/game/objects/items/shields.dm b/code/game/objects/items/shields.dm
index b711bb63d4519..3e3af7bc36f5e 100644
--- a/code/game/objects/items/shields.dm
+++ b/code/game/objects/items/shields.dm
@@ -397,4 +397,48 @@
balloon_alert(user, "extend it first!")
return COMPONENT_BLOCK_ITEM_DISARM_ATTACK
+/datum/armor/item_shield/ballistic
+ melee = 30
+ bullet = 85
+ bomb = 10
+ laser = 80
+
+/obj/item/shield/ballistic
+ name = "ballistic shield"
+ desc = "A heavy shield designed for blocking projectiles, weaker to melee."
+ icon_state = "ballistic"
+ inhand_icon_state = "ballistic"
+ custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 2, /datum/material/titanium =SHEET_MATERIAL_AMOUNT)
+ max_integrity = 75
+ shield_break_leftover = /obj/item/stack/rods/ten
+ armor_type = /datum/armor/item_shield/ballistic
+
+/obj/item/shield/ballistic/attackby(obj/item/attackby_item, mob/user, params)
+ if(istype(attackby_item, /obj/item/stack/sheet/mineral/titanium))
+ if (atom_integrity >= max_integrity)
+ to_chat(user, span_warning("[src] is already in perfect condition."))
+ return
+ var/obj/item/stack/sheet/mineral/titanium/titanium_sheet = attackby_item
+ titanium_sheet.use(1)
+ atom_integrity = max_integrity
+ to_chat(user, span_notice("You repair [src] with [titanium_sheet]."))
+ return
+ return ..()
+
+/datum/armor/item_shield/improvised
+ melee = 40
+ bullet = 30
+ laser = 30
+
+/obj/item/shield/improvised
+ name = "improvised shield"
+ desc = "A crude shield made out of several sheets of iron taped together, not very durable."
+ icon_state = "improvised"
+ inhand_icon_state = "improvised"
+ custom_materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT * 2)
+ max_integrity = 35
+ shield_break_leftover = /obj/item/stack/rods/two
+ armor_type = /datum/armor/item_shield/improvised
+ block_sound = 'sound/items/trayhit2.ogg'
+
#undef BATON_BASH_COOLDOWN
diff --git a/code/game/objects/items/shrapnel.dm b/code/game/objects/items/shrapnel.dm
index 59fbf61f62a90..cdc786fc8db56 100644
--- a/code/game/objects/items/shrapnel.dm
+++ b/code/game/objects/items/shrapnel.dm
@@ -44,6 +44,15 @@
ricochet_incidence_leeway = 0
ricochet_decay_chance = 0.9
+/obj/projectile/bullet/shrapnel/ied
+ name = "flying glass shrapnel"
+ damage = 15
+ range = 6
+ ricochets_max = 1
+ ricochet_chance = 40
+ shrapnel_type = /obj/item/shard
+ ricochet_incidence_leeway = 60
+
/obj/projectile/bullet/pellet/stingball
name = "stingball pellet"
damage = 3
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 9c4e687c12862..e042ad4c01cb3 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -74,6 +74,7 @@ GLOBAL_LIST_INIT(metal_recipes, list ( \
new/datum/stack_recipe("closet", /obj/structure/closet, 2, time = 1.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE), \
null, \
new/datum/stack_recipe("atmos canister", /obj/machinery/portable_atmospherics/canister, 10, time = 3 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_ATMOSPHERIC), \
+ new/datum/stack_recipe("pipe", /obj/item/pipe/quaternary/pipe/crafted, 1, time = 4 SECONDS, check_density = FALSE, category = CAT_ATMOSPHERIC), \
null, \
new/datum/stack_recipe("floor tile", /obj/item/stack/tile/iron/base, 1, 4, 20, category = CAT_TILES), \
new/datum/stack_recipe("iron rod", /obj/item/stack/rods, 1, 2, 60, category = CAT_MISC), \
diff --git a/code/game/objects/items/storage/backpack.dm b/code/game/objects/items/storage/backpack.dm
index 68463dad2b0c1..10c95056afffb 100644
--- a/code/game/objects/items/storage/backpack.dm
+++ b/code/game/objects/items/storage/backpack.dm
@@ -193,6 +193,12 @@
icon_state = "backpack-virology"
inhand_icon_state = "viropack"
+/obj/item/storage/backpack/floortile
+ name = "floortile backpack"
+ desc = "It's a backpack especially designed for use in floortiles..."
+ icon_state = "floortile_backpack"
+ inhand_icon_state = "backpack"
+
/obj/item/storage/backpack/ert
name = "emergency response team commander backpack"
desc = "A spacious backpack with lots of pockets, worn by the Commander of an Emergency Response Team."
diff --git a/code/game/objects/items/storage/boxes/clothes_boxes.dm b/code/game/objects/items/storage/boxes/clothes_boxes.dm
index 4c18ef4f6df28..18a6ec31d87c9 100644
--- a/code/game/objects/items/storage/boxes/clothes_boxes.dm
+++ b/code/game/objects/items/storage/boxes/clothes_boxes.dm
@@ -196,3 +196,18 @@
new /obj/item/clothing/suit/hooded/chaplain_hoodie/divine_archer(src)
new /obj/item/clothing/gloves/divine_archer(src)
new /obj/item/clothing/shoes/divine_archer(src)
+
+/obj/item/storage/box/floor_camo
+ name = "floor tile camo box"
+ desc = "Thank you for shopping from Camo-J's, our uniquely designed \
+ floor-tile 'NT scum' styled camouflage fatigues is the ultimate \
+ espionage uniform used by the very best. Providing the best \
+ flexibility, with our latest Camo-tech threads. Perfect for \
+ risky-espionage hallway operations. Enjoy our product!"
+
+/obj/item/storage/box/floor_camo/PopulateContents()
+ new /obj/item/clothing/under/syndicate/floortilecamo(src)
+ new /obj/item/clothing/mask/floortilebalaclava(src)
+ new /obj/item/clothing/gloves/combat/floortile(src)
+ new /obj/item/clothing/shoes/jackboots/floortile(src)
+ new /obj/item/storage/backpack/floortile(src)
diff --git a/code/game/objects/items/storage/boxes/security_boxes.dm b/code/game/objects/items/storage/boxes/security_boxes.dm
index 8e55986fb40d8..459c0ab7ce29e 100644
--- a/code/game/objects/items/storage/boxes/security_boxes.dm
+++ b/code/game/objects/items/storage/boxes/security_boxes.dm
@@ -174,6 +174,16 @@
for(var/i in 1 to 7)
new /obj/item/ammo_casing/shotgun/buckshot(src)
+/obj/item/storage/box/slugs
+ name = "box of shotgun shells (Lethal - Slugs)"
+ desc = "A box full of lethal shotgun slugs, designed for shotguns."
+ icon_state = "breacher_box"
+ illustration = null
+
+/obj/item/storage/box/slugs/PopulateContents()
+ for(var/i in 1 to 7)
+ new /obj/item/ammo_casing/shotgun(src)
+
/obj/item/storage/box/beanbag
name = "box of shotgun shells (Less Lethal - Beanbag)"
desc = "A box full of beanbag shotgun shells, designed for shotguns."
diff --git a/code/game/objects/items/storage/medkit.dm b/code/game/objects/items/storage/medkit.dm
index e389b990a4ca8..0ecd943b60457 100644
--- a/code/game/objects/items/storage/medkit.dm
+++ b/code/game/objects/items/storage/medkit.dm
@@ -271,6 +271,24 @@
/obj/item/storage/pill_bottle/penacid = 1)
generate_items_inside(items_inside,src)
+/obj/item/storage/medkit/tactical_lite
+ name = "combat first aid kit"
+ icon_state = "medkit_tactical"
+ inhand_icon_state = "medkit-tactical"
+ damagetype_healed = HEAL_ALL_DAMAGE
+
+/obj/item/storage/medkit/tactical_lite/PopulateContents()
+ if(empty)
+ return
+ var/static/list/items_inside = list(
+ /obj/item/healthanalyzer/advanced = 1,
+ /obj/item/reagent_containers/hypospray/medipen/atropine = 1,
+ /obj/item/stack/medical/gauze = 1,
+ /obj/item/stack/medical/suture/medicated = 2,
+ /obj/item/stack/medical/mesh/advanced = 2,
+ )
+ generate_items_inside(items_inside, src)
+
/obj/item/storage/medkit/tactical
name = "combat medical kit"
desc = "I hope you've got insurance."
diff --git a/code/game/objects/items/storage/storage.dm b/code/game/objects/items/storage/storage.dm
index cfdfef8a4590c..8631d62e79efd 100644
--- a/code/game/objects/items/storage/storage.dm
+++ b/code/game/objects/items/storage/storage.dm
@@ -55,7 +55,6 @@
max_total_storage,
list/canhold,
list/canthold,
- storage_type = /datum/storage,
storage_type,
)
// If no type was passed in, default to what we already have
diff --git a/code/game/objects/structures/crates_lockers/closets/gimmick.dm b/code/game/objects/structures/crates_lockers/closets/gimmick.dm
index 1e7fede584208..fecacd678c7c2 100644
--- a/code/game/objects/structures/crates_lockers/closets/gimmick.dm
+++ b/code/game/objects/structures/crates_lockers/closets/gimmick.dm
@@ -39,7 +39,6 @@
/obj/structure/closet/gimmick/tacticool/PopulateContents()
..()
new /obj/item/clothing/glasses/eyepatch(src)
- new /obj/item/clothing/glasses/sunglasses(src)
new /obj/item/clothing/gloves/tackler/combat(src)
new /obj/item/clothing/gloves/tackler/combat(src)
new /obj/item/clothing/head/helmet/swat(src)
@@ -53,6 +52,8 @@
new /obj/item/clothing/under/syndicate/tacticool(src)
new /obj/item/clothing/under/syndicate/tacticool(src)
+/obj/structure/closet/gimmick/tacticool/populate_contents_immediate()
+ new /obj/item/clothing/glasses/sunglasses(src)
/obj/structure/closet/thunderdome
name = "\improper Thunderdome closet"
@@ -69,8 +70,6 @@
new /obj/item/clothing/suit/armor/tdome/red(src)
for(var/i in 1 to 3)
new /obj/item/melee/energy/sword/saber(src)
- for(var/i in 1 to 3)
- new /obj/item/gun/energy/laser(src)
for(var/i in 1 to 3)
new /obj/item/melee/baton/security/loaded(src)
for(var/i in 1 to 3)
@@ -78,6 +77,10 @@
for(var/i in 1 to 3)
new /obj/item/clothing/head/helmet/thunderdome(src)
+/obj/structure/closet/thunderdome/tdred/populate_contents_immediate()
+ for(var/i in 1 to 3)
+ new /obj/item/gun/energy/laser(src)
+
/obj/structure/closet/thunderdome/tdgreen
name = "green-team Thunderdome closet"
icon_door = "green"
@@ -88,8 +91,6 @@
new /obj/item/clothing/suit/armor/tdome/green(src)
for(var/i in 1 to 3)
new /obj/item/melee/energy/sword/saber(src)
- for(var/i in 1 to 3)
- new /obj/item/gun/energy/laser(src)
for(var/i in 1 to 3)
new /obj/item/melee/baton/security/loaded(src)
for(var/i in 1 to 3)
@@ -97,6 +98,10 @@
for(var/i in 1 to 3)
new /obj/item/clothing/head/helmet/thunderdome(src)
+/obj/structure/closet/thunderdome/tdgreen/populate_contents_immediate()
+ for(var/i in 1 to 3)
+ new /obj/item/gun/energy/laser(src)
+
/obj/structure/closet/malf/suits
desc = "It's a storage unit for operational gear."
icon_state = "syndicate"
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/security.dm b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
index d09c12fb5d49c..553258bd360ea 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/security.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/security.dm
@@ -16,11 +16,13 @@
new /obj/item/computer_disk/command/captain(src)
new /obj/item/radio/headset/heads/captain/alt(src)
new /obj/item/radio/headset/heads/captain(src)
- new /obj/item/storage/belt/sabre(src)
- new /obj/item/gun/energy/e_gun(src)
new /obj/item/door_remote/captain(src)
new /obj/item/storage/photo_album/captain(src)
+/obj/structure/closet/secure_closet/captains/populate_contents_immediate()
+ new /obj/item/gun/energy/e_gun(src)
+ new /obj/item/storage/belt/sabre(src)
+
/obj/structure/closet/secure_closet/hop
name = "head of personnel's locker"
icon_state = "hop"
@@ -37,7 +39,6 @@
new /obj/item/storage/box/silver_ids(src)
new /obj/item/megaphone/command(src)
new /obj/item/assembly/flash/handheld(src)
- new /obj/item/gun/energy/e_gun(src)
new /obj/item/clothing/neck/petcollar(src)
new /obj/item/pet_carrier(src)
new /obj/item/door_remote/civilian(src)
@@ -45,6 +46,9 @@
new /obj/item/storage/photo_album/hop(src)
new /obj/item/storage/lockbox/medal/hop(src)
+/obj/structure/closet/secure_closet/hop/populate_contents_immediate()
+ new /obj/item/gun/energy/e_gun(src)
+
/obj/structure/closet/secure_closet/hos
name = "head of security's locker"
icon_state = "hos"
@@ -286,6 +290,8 @@
new /obj/item/storage/box/firingpins(src)
for(var/i in 1 to 3)
new /obj/item/storage/box/rubbershot(src)
+
+/obj/structure/closet/secure_closet/armory2/populate_contents_immediate()
for(var/i in 1 to 3)
new /obj/item/gun/ballistic/shotgun/riot(src)
@@ -299,12 +305,14 @@
..()
new /obj/item/storage/box/firingpins(src)
new /obj/item/gun/energy/ionrifle(src)
+ for(var/i in 1 to 3)
+ new /obj/item/gun/energy/laser/thermal(src)
+
+/obj/structure/closet/secure_closet/armory3/populate_contents_immediate()
for(var/i in 1 to 3)
new /obj/item/gun/energy/e_gun(src)
for(var/i in 1 to 3)
new /obj/item/gun/energy/laser(src)
- for(var/i in 1 to 3)
- new /obj/item/gun/energy/laser/thermal(src)
/obj/structure/closet/secure_closet/tac
name = "armory tac locker"
diff --git a/code/game/objects/structures/holosign.dm b/code/game/objects/structures/holosign.dm
index a3d09340d87e3..a1a3adf325145 100644
--- a/code/game/objects/structures/holosign.dm
+++ b/code/game/objects/structures/holosign.dm
@@ -45,6 +45,7 @@
user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
user.changeNext_move(CLICK_CD_MELEE)
take_damage(5 , BRUTE, MELEE, 1)
+ log_combat(user, src, "swatted")
/obj/structure/holosign/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
switch(damage_type)
diff --git a/code/modules/antagonists/brother/brother.dm b/code/modules/antagonists/brother/brother.dm
index 223664f0fa449..9404157ad24ab 100644
--- a/code/modules/antagonists/brother/brother.dm
+++ b/code/modules/antagonists/brother/brother.dm
@@ -209,7 +209,7 @@
/datum/objective/convert_brother
name = "convert brother"
- explanation_text = "Convert a brainwashable person using your flash. Any flash will work if you lose or break your starting flash."
+ explanation_text = "Convert a brainwashable person using your flash on them directly. Any handheld flash will work if you lose or break your starting flash."
admin_grantable = FALSE
martyr_compatible = TRUE
diff --git a/code/modules/antagonists/nukeop/datums/operative.dm b/code/modules/antagonists/nukeop/datums/operative.dm
new file mode 100644
index 0000000000000..516108c572513
--- /dev/null
+++ b/code/modules/antagonists/nukeop/datums/operative.dm
@@ -0,0 +1,213 @@
+/datum/antagonist/nukeop
+ name = ROLE_NUCLEAR_OPERATIVE
+ roundend_category = "syndicate operatives" //just in case
+ antagpanel_category = ANTAG_GROUP_SYNDICATE
+ job_rank = ROLE_OPERATIVE
+ antag_hud_name = "synd"
+ antag_moodlet = /datum/mood_event/focused
+ show_to_ghosts = TRUE
+ hijack_speed = 2 //If you can't take out the station, take the shuttle instead.
+ suicide_cry = "FOR THE SYNDICATE!!"
+ /// Which nukie team are we on?
+ var/datum/team/nuclear/nuke_team
+ /// If not assigned a team by default ops will try to join existing ones, set this to TRUE to always create new team.
+ var/always_new_team = FALSE
+ /// Should the user be moved to default spawnpoint after being granted this datum.
+ var/send_to_spawnpoint = TRUE
+ /// The DEFAULT outfit we will give to players granted this datum
+ var/nukeop_outfit = /datum/outfit/syndicate
+
+ preview_outfit = /datum/outfit/nuclear_operative_elite
+
+ /// In the preview icon, the nukies who are behind the leader
+ var/preview_outfit_behind = /datum/outfit/nuclear_operative
+ /// In the preview icon, a nuclear fission explosive device, only appearing if there's an icon state for it.
+ var/nuke_icon_state = "nuclearbomb_base"
+
+ /// The amount of discounts that the team get
+ var/discount_team_amount = 5
+ /// The amount of limited discounts that the team get
+ var/discount_limited_amount = 10
+
+/datum/antagonist/nukeop/greet()
+ owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/ops.ogg',100,0, use_reverb = FALSE)
+ to_chat(owner, span_big("You are a [nuke_team ? nuke_team.syndicate_name : "syndicate"] agent!"))
+ owner.announce_objectives()
+
+/datum/antagonist/nukeop/on_gain()
+ give_alias()
+ forge_objectives()
+ . = ..()
+ equip_op()
+ if(send_to_spawnpoint)
+ move_to_spawnpoint()
+ // grant extra TC for the people who start in the nukie base ie. not the lone op
+ var/extra_tc = CEILING(GLOB.joined_player_list.len/5, 5)
+ var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
+ if (uplink)
+ uplink.uplink_handler.add_telecrystals(extra_tc)
+
+ var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
+ if(uplink)
+ var/datum/team/nuclear/nuke_team = get_team()
+ if(!nuke_team.team_discounts)
+ var/list/uplink_items = list()
+ for(var/datum/uplink_item/item as anything in SStraitor.uplink_items)
+ if(item.item && !item.cant_discount && (item.purchasable_from & uplink.uplink_handler.uplink_flag) && item.cost > 1)
+ uplink_items += item
+ nuke_team.team_discounts = list()
+ nuke_team.team_discounts += create_uplink_sales(discount_team_amount, /datum/uplink_category/discount_team_gear, -1, uplink_items)
+ nuke_team.team_discounts += create_uplink_sales(discount_limited_amount, /datum/uplink_category/limited_discount_team_gear, 1, uplink_items)
+ uplink.uplink_handler.extra_purchasable += nuke_team.team_discounts
+
+ memorize_code()
+
+/datum/antagonist/nukeop/get_team()
+ return nuke_team
+
+/datum/antagonist/nukeop/apply_innate_effects(mob/living/mob_override)
+ add_team_hud(mob_override || owner.current, /datum/antagonist/nukeop)
+
+/datum/antagonist/nukeop/forge_objectives()
+ if(nuke_team)
+ objectives |= nuke_team.objectives
+
+/datum/antagonist/nukeop/leader/get_spawnpoint()
+ return pick(GLOB.nukeop_leader_start)
+
+/datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team)
+ if(!new_team)
+ if(!always_new_team)
+ for(var/datum/antagonist/nukeop/N in GLOB.antagonists)
+ if(!N.owner)
+ stack_trace("Antagonist datum without owner in GLOB.antagonists: [N]")
+ continue
+ if(N.nuke_team)
+ nuke_team = N.nuke_team
+ return
+ nuke_team = new /datum/team/nuclear
+ nuke_team.update_objectives()
+ assign_nuke() //This is bit ugly
+ return
+ if(!istype(new_team))
+ stack_trace("Wrong team type passed to [type] initialization.")
+ nuke_team = new_team
+
+/datum/antagonist/nukeop/admin_add(datum/mind/new_owner,mob/admin)
+ new_owner.set_assigned_role(SSjob.GetJobType(/datum/job/nuclear_operative))
+ new_owner.add_antag_datum(src)
+ message_admins("[key_name_admin(admin)] has nuke op'ed [key_name_admin(new_owner)].")
+ log_admin("[key_name(admin)] has nuke op'ed [key_name(new_owner)].")
+
+/datum/antagonist/nukeop/get_admin_commands()
+ . = ..()
+ .["Send to base"] = CALLBACK(src, PROC_REF(admin_send_to_base))
+ .["Tell code"] = CALLBACK(src, PROC_REF(admin_tell_code))
+
+/datum/antagonist/nukeop/get_preview_icon()
+ if (!preview_outfit)
+ return null
+
+ var/icon/final_icon = render_preview_outfit(preview_outfit)
+
+ if (!isnull(preview_outfit_behind))
+ var/icon/teammate = render_preview_outfit(preview_outfit_behind)
+ teammate.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY)
+
+ final_icon.Blend(teammate, ICON_UNDERLAY, -world.icon_size / 4, 0)
+ final_icon.Blend(teammate, ICON_UNDERLAY, world.icon_size / 4, 0)
+
+ if (!isnull(nuke_icon_state))
+ var/icon/nuke = icon('icons/obj/machines/nuke.dmi', nuke_icon_state)
+ nuke.Shift(SOUTH, 6)
+ final_icon.Blend(nuke, ICON_OVERLAY)
+
+ return finish_preview_icon(final_icon)
+
+/datum/antagonist/nukeop/proc/equip_op()
+ if(!ishuman(owner.current))
+ return
+
+ var/mob/living/carbon/human/operative = owner.current
+ ADD_TRAIT(operative, TRAIT_NOFEAR_HOLDUPS, INNATE_TRAIT)
+
+ if(!nukeop_outfit) // this variable is null in instances where an antagonist datum is granted via enslaving the mind (/datum/mind/proc/enslave_mind_to_creator), like in golems.
+ return
+
+ // If our nuke_ops_species pref is set to TRUE, (or we have no client) make us a human
+ if(isnull(operative.client) || operative.client.prefs.read_preference(/datum/preference/toggle/nuke_ops_species))
+ operative.set_species(/datum/species/human)
+
+ operative.equip_species_outfit(nukeop_outfit)
+
+ return TRUE
+
+/datum/antagonist/nukeop/proc/admin_send_to_base(mob/admin)
+ owner.current.forceMove(pick(GLOB.nukeop_start))
+
+/datum/antagonist/nukeop/proc/admin_tell_code(mob/admin)
+ var/code
+ for (var/obj/machinery/nuclearbomb/bombue as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb))
+ if (length(bombue.r_code) <= 5 && bombue.r_code != initial(bombue.r_code))
+ code = bombue.r_code
+ break
+ if (code)
+ antag_memory += "Syndicate Nuclear Bomb Code : [code] "
+ to_chat(owner.current, "The nuclear authorization code is: [code] ")
+ else
+ to_chat(admin, span_danger("No valid nuke found!"))
+
+/datum/antagonist/nukeop/proc/assign_nuke()
+ if(!nuke_team || nuke_team.tracked_nuke)
+ return
+ nuke_team.memorized_code = random_nukecode()
+ var/obj/machinery/nuclearbomb/syndicate/nuke = locate() in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/syndicate)
+ if(!nuke)
+ stack_trace("Syndicate nuke not found during nuke team creation.")
+ nuke_team.memorized_code = null
+ return
+ nuke_team.tracked_nuke = nuke
+ if(nuke.r_code == NUKE_CODE_UNSET)
+ nuke.r_code = nuke_team.memorized_code
+ else //Already set by admins/something else?
+ nuke_team.memorized_code = nuke.r_code
+ for(var/obj/machinery/nuclearbomb/beer/beernuke as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/beer))
+ beernuke.r_code = nuke_team.memorized_code
+
+/datum/antagonist/nukeop/proc/give_alias()
+ if(nuke_team?.syndicate_name)
+ var/mob/living/carbon/human/human_to_rename = owner.current
+ if(istype(human_to_rename)) // Reinforcements get a real name
+ var/first_name = owner.current.client?.prefs?.read_preference(/datum/preference/name/operative_alias) || pick(GLOB.operative_aliases)
+ var/chosen_name = "[first_name] [nuke_team.syndicate_name]"
+ human_to_rename.fully_replace_character_name(human_to_rename.real_name, chosen_name)
+ else
+ var/number = 1
+ number = nuke_team.members.Find(owner)
+ owner.current.real_name = "[nuke_team.syndicate_name] Operative #[number]"
+
+/datum/antagonist/nukeop/proc/memorize_code()
+ if(nuke_team && nuke_team.tracked_nuke && nuke_team.memorized_code)
+ antag_memory += "[nuke_team.tracked_nuke] Code : [nuke_team.memorized_code] "
+ owner.add_memory(/datum/memory/key/nuke_code, nuclear_code = nuke_team.memorized_code)
+ to_chat(owner, "The nuclear authorization code is: [nuke_team.memorized_code] ")
+ else
+ to_chat(owner, "Unfortunately the syndicate was unable to provide you with nuclear authorization code.")
+
+/// Actually moves our nukie to where they should be
+/datum/antagonist/nukeop/proc/move_to_spawnpoint()
+ // Ensure that the nukiebase is loaded, and wait for it if required
+ SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE)
+ var/turf/destination = get_spawnpoint()
+ owner.current.forceMove(destination)
+ if(!owner.current.onSyndieBase())
+ message_admins("[ADMIN_LOOKUPFLW(owner.current)] is a NUKE OP and move_to_spawnpoint put them somewhere that isn't the syndie base, help please.")
+ stack_trace("Nuke op move_to_spawnpoint resulted in a location not on the syndicate base. (Was moved to: [destination])")
+
+/// Gets the position we spawn at
+/datum/antagonist/nukeop/proc/get_spawnpoint()
+ var/team_number = 1
+ if(nuke_team)
+ team_number = nuke_team.members.Find(owner)
+
+ return GLOB.nukeop_start[((team_number - 1) % GLOB.nukeop_start.len) + 1]
diff --git a/code/modules/antagonists/nukeop/datums/operative_leader.dm b/code/modules/antagonists/nukeop/datums/operative_leader.dm
new file mode 100644
index 0000000000000..76ca635158b16
--- /dev/null
+++ b/code/modules/antagonists/nukeop/datums/operative_leader.dm
@@ -0,0 +1,55 @@
+/datum/antagonist/nukeop/leader
+ name = "Nuclear Operative Leader"
+ nukeop_outfit = /datum/outfit/syndicate/leader
+ always_new_team = TRUE
+ /// Randomly chosen honorific, for distinction
+ var/title
+ /// The nuclear challenge remote we will spawn this player with.
+ var/challengeitem = /obj/item/nuclear_challenge
+
+/datum/antagonist/nukeop/leader/memorize_code()
+ ..()
+ if(nuke_team?.memorized_code)
+ var/obj/item/paper/nuke_code_paper = new
+ nuke_code_paper.add_raw_text("The nuclear authorization code is: [nuke_team.memorized_code] ")
+ nuke_code_paper.name = "nuclear bomb code"
+ var/mob/living/carbon/human/H = owner.current
+ if(!istype(H))
+ nuke_code_paper.forceMove(get_turf(H))
+ else
+ H.put_in_hands(nuke_code_paper, TRUE)
+ H.update_icons()
+
+/datum/antagonist/nukeop/leader/greet()
+ owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/ops.ogg',100,0, use_reverb = FALSE)
+ to_chat(owner, "You are the Syndicate [title] for this mission. You are responsible for guiding the team and your ID is the only one who can open the launch bay doors. ")
+ to_chat(owner, "If you feel you are not up to this task, give your ID and radio to another operative. ")
+ if(!CONFIG_GET(flag/disable_warops))
+ to_chat(owner, "In your hand you will find a special item capable of triggering a greater challenge for your team. Examine it carefully and consult with your fellow operatives before activating it. ")
+ owner.announce_objectives()
+
+/datum/antagonist/nukeop/leader/on_gain()
+ . = ..()
+ if(!CONFIG_GET(flag/disable_warops))
+ var/mob/living/carbon/human/leader = owner.current
+ var/obj/item/war_declaration = new challengeitem(leader.drop_location())
+ leader.put_in_hands(war_declaration)
+ nuke_team.war_button_ref = WEAKREF(war_declaration)
+ addtimer(CALLBACK(src, PROC_REF(nuketeam_name_assign)), 1)
+
+/datum/antagonist/nukeop/leader/proc/nuketeam_name_assign()
+ if(!nuke_team)
+ return
+ nuke_team.rename_team(ask_name())
+
+/datum/antagonist/nukeop/leader/proc/ask_name()
+ var/randomname = pick(GLOB.last_names)
+ var/newname = tgui_input_text(owner.current, "You are the nuclear operative [title]. Please choose a last name for your family.", "Name change", randomname, MAX_NAME_LEN)
+ if (!newname)
+ newname = randomname
+ else
+ newname = reject_bad_name(newname)
+ if(!newname)
+ newname = randomname
+
+ return capitalize(newname)
diff --git a/code/modules/antagonists/nukeop/datums/operative_lone.dm b/code/modules/antagonists/nukeop/datums/operative_lone.dm
new file mode 100644
index 0000000000000..d0bc718a781b0
--- /dev/null
+++ b/code/modules/antagonists/nukeop/datums/operative_lone.dm
@@ -0,0 +1,22 @@
+/datum/antagonist/nukeop/lone
+ name = "Lone Operative"
+ always_new_team = TRUE
+ send_to_spawnpoint = FALSE //Handled by event
+ nukeop_outfit = /datum/outfit/syndicate/full
+ preview_outfit = /datum/outfit/nuclear_operative
+ preview_outfit_behind = null
+ nuke_icon_state = null
+
+/datum/antagonist/nukeop/lone/assign_nuke()
+ if(nuke_team && !nuke_team.tracked_nuke)
+ nuke_team.memorized_code = random_nukecode()
+ var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct)
+ if(nuke)
+ nuke_team.tracked_nuke = nuke
+ if(nuke.r_code == NUKE_CODE_UNSET)
+ nuke.r_code = nuke_team.memorized_code
+ else //Already set by admins/something else?
+ nuke_team.memorized_code = nuke.r_code
+ else
+ stack_trace("Station self-destruct not found during lone op team creation.")
+ nuke_team.memorized_code = null
diff --git a/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm b/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
new file mode 100644
index 0000000000000..eedaef3f720c9
--- /dev/null
+++ b/code/modules/antagonists/nukeop/datums/operative_reinforcement.dm
@@ -0,0 +1,4 @@
+/datum/antagonist/nukeop/reinforcement
+ show_in_antagpanel = FALSE
+ send_to_spawnpoint = FALSE
+ nukeop_outfit = /datum/outfit/syndicate/reinforcement
diff --git a/code/modules/antagonists/nukeop/datums/operative_team.dm b/code/modules/antagonists/nukeop/datums/operative_team.dm
new file mode 100644
index 0000000000000..e42d65b42a845
--- /dev/null
+++ b/code/modules/antagonists/nukeop/datums/operative_team.dm
@@ -0,0 +1,320 @@
+#define SPAWN_AT_BASE "Nuke base"
+#define SPAWN_AT_INFILTRATOR "Infiltrator"
+
+/datum/team/nuclear
+ var/syndicate_name
+ var/obj/machinery/nuclearbomb/tracked_nuke
+ var/core_objective = /datum/objective/nuclear
+ var/memorized_code
+ var/list/team_discounts
+ var/datum/weakref/war_button_ref
+
+/datum/team/nuclear/New()
+ ..()
+ syndicate_name = syndicate_name()
+
+/datum/team/nuclear/roundend_report()
+ var/list/parts = list()
+ parts += ""
+
+ switch(get_result())
+ if(NUKE_RESULT_FLUKE)
+ parts += "Humiliating Syndicate Defeat "
+ parts += "The crew of [station_name()] gave [syndicate_name] operatives back their bomb! The syndicate base was destroyed! Next time, don't lose the nuke!"
+ if(NUKE_RESULT_NUKE_WIN)
+ parts += "Syndicate Major Victory! "
+ parts += "[syndicate_name] operatives have destroyed [station_name()]! "
+ if(NUKE_RESULT_NOSURVIVORS)
+ parts += "Total Annihilation! "
+ parts += "[syndicate_name] operatives destroyed [station_name()] but did not leave the area in time and got caught in the explosion. Next time, don't lose the disk!"
+ if(NUKE_RESULT_WRONG_STATION)
+ parts += "Crew Minor Victory! "
+ parts += "[syndicate_name] operatives secured the authentication disk but blew up something that wasn't [station_name()]. Next time, don't do that!"
+ if(NUKE_RESULT_WRONG_STATION_DEAD)
+ parts += "[syndicate_name] operatives have earned Darwin Award! "
+ parts += "[syndicate_name] operatives blew up something that wasn't [station_name()] and got caught in the explosion. Next time, don't do that!"
+ if(NUKE_RESULT_HIJACK_DISK)
+ parts += "Syndicate Miniscule Victory! "
+ parts += "[syndicate_name] operatives failed to destroy [station_name()], but they managed to secure the disk and hijack the emergency shuttle, causing it to land on the syndicate base. Good job? "
+ if(NUKE_RESULT_HIJACK_NO_DISK)
+ parts += "Syndicate Insignificant Victory! "
+ parts += "[syndicate_name] operatives failed to destroy [station_name()] or secure the disk, but they managed to hijack the emergency shuttle, causing it to land on the syndicate base. Good job? "
+ if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD)
+ parts += "Crew Major Victory! "
+ parts += "The Research Staff has saved the disk and killed the [syndicate_name] Operatives "
+ if(NUKE_RESULT_CREW_WIN)
+ parts += "Crew Major Victory! "
+ parts += "The Research Staff has saved the disk and stopped the [syndicate_name] Operatives! "
+ if(NUKE_RESULT_DISK_LOST)
+ parts += "Neutral Victory! "
+ parts += "The Research Staff failed to secure the authentication disk but did manage to kill most of the [syndicate_name] Operatives! "
+ if(NUKE_RESULT_DISK_STOLEN)
+ parts += "Syndicate Minor Victory! "
+ parts += "[syndicate_name] operatives survived the assault but did not achieve the destruction of [station_name()]. Next time, don't lose the disk!"
+ else
+ parts += "Neutral Victory "
+ parts += "Mission aborted! "
+
+ var/text = " "
+ var/purchases = ""
+ var/TC_uses = 0
+ LAZYINITLIST(GLOB.uplink_purchase_logs_by_key)
+ for(var/I in members)
+ var/datum/mind/syndicate = I
+ var/datum/uplink_purchase_log/H = GLOB.uplink_purchase_logs_by_key[syndicate.key]
+ if(H)
+ TC_uses += H.total_spent
+ purchases += H.generate_render(show_key = FALSE)
+ text += printplayerlist(members)
+ text += " "
+ text += "(Syndicates used [TC_uses] TC) [purchases]"
+ if(TC_uses == 0 && GLOB.station_was_nuked && !are_all_operatives_dead())
+ text += "[icon2html('icons/ui_icons/antags/badass.dmi', world, "badass")] "
+
+ parts += text
+
+ return "
[parts.Join(" ")]
"
+
+/datum/team/nuclear/antag_listing_name()
+ if(syndicate_name)
+ return "[syndicate_name] Syndicates"
+ else
+ return "Syndicates"
+
+/datum/team/nuclear/antag_listing_entry()
+ var/disk_report = "Nuclear Disk(s) "
+ disk_report += ""
+ for(var/obj/item/disk/nuclear/N in SSpoints_of_interest.real_nuclear_disks)
+ disk_report += "[N.name], "
+ var/atom/disk_loc = N.loc
+ while(!isturf(disk_loc))
+ if(ismob(disk_loc))
+ var/mob/M = disk_loc
+ disk_report += "carried by [M.real_name] "
+ if(isobj(disk_loc))
+ var/obj/O = disk_loc
+ disk_report += "in \a [O.name] "
+ disk_loc = disk_loc.loc
+ disk_report += "in [disk_loc.loc] at ([disk_loc.x], [disk_loc.y], [disk_loc.z]) FLW "
+ disk_report += "
"
+
+ var/post_report
+
+ var/war_declared = FALSE
+ for(var/obj/item/circuitboard/computer/syndicate_shuttle/board as anything in GLOB.syndicate_shuttle_boards)
+ if(board.challenge)
+ war_declared = TRUE
+
+ var/force_war_button = ""
+
+ if(war_declared)
+ post_report += "War declared. "
+ force_war_button = "\[Force war\]"
+ else
+ post_report += "War not declared. "
+ var/obj/item/nuclear_challenge/war_button = war_button_ref?.resolve()
+ if(war_button)
+ force_war_button = "\[Force war\] "
+ else
+ force_war_button = "\[Cannot declare war, challenge button missing!\]"
+
+ post_report += "\n[force_war_button]"
+ post_report += "\n\[Send Reinforcement\] "
+
+ var/final_report = ..()
+ final_report += disk_report
+ final_report += post_report
+ return final_report
+
+/datum/team/nuclear/proc/rename_team(new_name)
+ syndicate_name = new_name
+ name = "[syndicate_name] Team"
+ for(var/I in members)
+ var/datum/mind/synd_mind = I
+ var/mob/living/carbon/human/human_to_rename = synd_mind.current
+ if(!istype(human_to_rename))
+ continue
+ var/first_name = human_to_rename.client?.prefs?.read_preference(/datum/preference/name/operative_alias) || pick(GLOB.operative_aliases)
+ var/chosen_name = "[first_name] [syndicate_name]"
+ human_to_rename.fully_replace_character_name(human_to_rename.real_name, chosen_name)
+
+/datum/team/nuclear/proc/admin_spawn_reinforcement(mob/admin)
+ if(!check_rights_for(admin.client, R_ADMIN))
+ return
+
+ var/infil_or_nukebase = tgui_alert(
+ admin,
+ "Spawn them at the nuke base, or in the Infiltrator?",
+ "Where to reinforce?",
+ list(SPAWN_AT_BASE, SPAWN_AT_INFILTRATOR, "Cancel"),
+ )
+
+ if(!infil_or_nukebase || infil_or_nukebase == "Cancel")
+ return
+
+ var/tc_to_spawn = tgui_input_number(admin, "How much TC to spawn with?", "TC", 0, 100)
+
+ var/list/nuke_candidates = SSpolling.poll_ghost_candidates(
+ "Do you want to play as an emergency syndicate reinforcement?",
+ check_jobban = ROLE_OPERATIVE,
+ role = ROLE_OPERATIVE,
+ poll_time = 30 SECONDS,
+ ignore_category = POLL_IGNORE_SYNDICATE,
+ pic_source = /obj/structure/sign/poster/contraband/gorlex_recruitment,
+ role_name_text = "syndicate reinforcement",
+ )
+
+ nuke_candidates -= admin // may be easy to fat-finger say yes. so just don't
+
+ if(!length(nuke_candidates))
+ tgui_alert(admin, "No candidates found.", "Recruitment Shortage", list("OK"))
+ return
+
+
+ var/turf/spawn_loc
+ if(infil_or_nukebase == SPAWN_AT_INFILTRATOR)
+ var/area/spawn_in
+ // Prioritize EVA then hallway, if neither can be found default to the first area we can find
+ for(var/area_type in list(/area/shuttle/syndicate/eva, /area/shuttle/syndicate/hallway, /area/shuttle/syndicate))
+ spawn_in = locate(area_type) in GLOB.areas // I'd love to use areas_by_type but the Infiltrator is a unique area
+ if(spawn_in)
+ break
+
+ var/list/turf/options = list()
+ for(var/turf/open/open_turf in spawn_in?.get_turfs_from_all_zlevels())
+ if(open_turf.is_blocked_turf())
+ continue
+ options += open_turf
+
+ if(length(options))
+ spawn_loc = pick(options)
+ else
+ infil_or_nukebase = SPAWN_AT_BASE
+
+ if(infil_or_nukebase == SPAWN_AT_BASE)
+ spawn_loc = pick(GLOB.nukeop_start)
+
+ var/mob/dead/observer/picked = pick(nuke_candidates)
+ var/mob/living/carbon/human/nukie = new(spawn_loc)
+ picked.client.prefs.safe_transfer_prefs_to(nukie, is_antag = TRUE)
+ nukie.key = picked.key
+
+ var/datum/antagonist/nukeop/antag_datum = new()
+ antag_datum.send_to_spawnpoint = FALSE
+ antag_datum.nukeop_outfit = /datum/outfit/syndicate/reinforcement
+
+ nukie.mind.add_antag_datum(antag_datum, src)
+
+ var/datum/component/uplink/uplink = nukie.mind.find_syndicate_uplink()
+ uplink?.uplink_handler.set_telecrystals(tc_to_spawn)
+
+ // add some pizzazz
+ do_sparks(4, FALSE, spawn_loc)
+ new /obj/effect/temp_visual/teleport_abductor/syndi_teleporter(spawn_loc)
+ playsound(spawn_loc, SFX_SPARKS, 50, TRUE)
+ playsound(spawn_loc, 'sound/effects/phasein.ogg', 50, TRUE)
+
+ tgui_alert(admin, "Reinforcement spawned at [infil_or_nukebase] with [tc_to_spawn].", "Reinforcements have arrived", list("God speed"))
+
+/datum/team/nuclear/proc/update_objectives()
+ if(core_objective)
+ var/datum/objective/O = new core_objective
+ O.team = src
+ objectives += O
+
+/datum/team/nuclear/proc/is_disk_rescued()
+ for(var/obj/item/disk/nuclear/nuke_disk in SSpoints_of_interest.real_nuclear_disks)
+ //If emergency shuttle is in transit disk is only safe on it
+ if(SSshuttle.emergency.mode == SHUTTLE_ESCAPE)
+ if(!SSshuttle.emergency.is_in_shuttle_bounds(nuke_disk))
+ return FALSE
+ //If shuttle escaped check if it's on centcom side
+ else if(SSshuttle.emergency.mode == SHUTTLE_ENDGAME)
+ if(!nuke_disk.onCentCom())
+ return FALSE
+ else //Otherwise disk is safe when on station
+ var/turf/disk_turf = get_turf(nuke_disk)
+ if(!disk_turf || !is_station_level(disk_turf.z))
+ return FALSE
+ return TRUE
+
+/datum/team/nuclear/proc/are_all_operatives_dead()
+ for(var/datum/mind/operative_mind as anything in members)
+ if(ishuman(operative_mind.current) && (operative_mind.current.stat != DEAD))
+ return FALSE
+ return TRUE
+
+/datum/team/nuclear/proc/get_result()
+ var/shuttle_evacuated = EMERGENCY_ESCAPED_OR_ENDGAMED
+ var/shuttle_landed_base = SSshuttle.emergency.is_hijacked()
+ var/disk_rescued = is_disk_rescued()
+ var/syndies_didnt_escape = !is_infiltrator_docked_at_syndiebase()
+ var/team_is_dead = are_all_operatives_dead()
+ var/station_was_nuked = GLOB.station_was_nuked
+ var/station_nuke_source = GLOB.station_nuke_source
+
+ // The nuke detonated on the syndicate base
+ if(station_nuke_source == DETONATION_HIT_SYNDIE_BASE)
+ return NUKE_RESULT_FLUKE
+
+ // The station was nuked
+ if(station_was_nuked)
+ // The station was nuked and the infiltrator failed to escape
+ if(syndies_didnt_escape)
+ return NUKE_RESULT_NOSURVIVORS
+ // The station was nuked and the infiltrator escaped, and the nuke ops won
+ else
+ return NUKE_RESULT_NUKE_WIN
+
+ // The station was not nuked, but something was
+ else if(station_nuke_source && !disk_rescued)
+ // The station was not nuked, but something was, and the syndicates didn't escape it
+ if(syndies_didnt_escape)
+ return NUKE_RESULT_WRONG_STATION_DEAD
+ // The station was not nuked, but something was, and the syndicates returned to their base
+ else
+ return NUKE_RESULT_WRONG_STATION
+
+ // Nuke didn't blow, but nukies somehow hijacked the emergency shuttle to land at the base anyways.
+ else if(shuttle_landed_base)
+ if(disk_rescued)
+ return NUKE_RESULT_HIJACK_DISK
+ else
+ return NUKE_RESULT_HIJACK_NO_DISK
+
+ // No nuke went off, the station rescued the disk
+ else if(disk_rescued)
+ // No nuke went off, the shuttle left, and the team is dead
+ if(shuttle_evacuated && team_is_dead)
+ return NUKE_RESULT_CREW_WIN_SYNDIES_DEAD
+ // No nuke went off, but the nuke ops survived
+ else
+ return NUKE_RESULT_CREW_WIN
+
+ // No nuke went off, but the disk was left behind
+ else
+ // No nuke went off, the disk was left, but all the ops are dead
+ if(team_is_dead)
+ return NUKE_RESULT_DISK_LOST
+ // No nuke went off, the disk was left, there are living ops, but the shuttle left successfully
+ else if(shuttle_evacuated)
+ return NUKE_RESULT_DISK_STOLEN
+
+ CRASH("[type] - got an undefined / unexpected result.")
+
+/// Returns whether or not syndicate operatives escaped.
+/proc/is_infiltrator_docked_at_syndiebase()
+ var/obj/docking_port/mobile/infiltrator/infiltrator_port = SSshuttle.getShuttle("syndicate")
+
+ var/datum/lazy_template/nukie_base/nukie_template = GLOB.lazy_templates[LAZY_TEMPLATE_KEY_NUKIEBASE]
+ if(!nukie_template)
+ return FALSE // if its not even loaded, cant be docked
+
+ for(var/datum/turf_reservation/loaded_area as anything in nukie_template.reservations)
+ var/infiltrator_turf = get_turf(infiltrator_port)
+ if(infiltrator_turf in loaded_area.reserved_turfs)
+ return TRUE
+ return FALSE
+
+#undef SPAWN_AT_BASE
+#undef SPAWN_AT_INFILTRATOR
diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm
deleted file mode 100644
index 6278d5ddaea91..0000000000000
--- a/code/modules/antagonists/nukeop/nukeop.dm
+++ /dev/null
@@ -1,645 +0,0 @@
-/datum/antagonist/nukeop
- name = ROLE_NUCLEAR_OPERATIVE
- roundend_category = "syndicate operatives" //just in case
- antagpanel_category = ANTAG_GROUP_SYNDICATE
- job_rank = ROLE_OPERATIVE
- antag_hud_name = "synd"
- antag_moodlet = /datum/mood_event/focused
- show_to_ghosts = TRUE
- hijack_speed = 2 //If you can't take out the station, take the shuttle instead.
- suicide_cry = "FOR THE SYNDICATE!!"
- /// Which nukie team are we on?
- var/datum/team/nuclear/nuke_team
- /// If not assigned a team by default ops will try to join existing ones, set this to TRUE to always create new team.
- var/always_new_team = FALSE
- /// Should the user be moved to default spawnpoint after being granted this datum.
- var/send_to_spawnpoint = TRUE
- /// The DEFAULT outfit we will give to players granted this datum
- var/nukeop_outfit = /datum/outfit/syndicate
-
- preview_outfit = /datum/outfit/nuclear_operative_elite
-
- /// In the preview icon, the nukies who are behind the leader
- var/preview_outfit_behind = /datum/outfit/nuclear_operative
- /// In the preview icon, a nuclear fission explosive device, only appearing if there's an icon state for it.
- var/nuke_icon_state = "nuclearbomb_base"
-
- /// The amount of discounts that the team get
- var/discount_team_amount = 5
- /// The amount of limited discounts that the team get
- var/discount_limited_amount = 10
-
-/datum/antagonist/nukeop/proc/equip_op()
- if(!ishuman(owner.current))
- return
-
- var/mob/living/carbon/human/operative = owner.current
- ADD_TRAIT(operative, TRAIT_NOFEAR_HOLDUPS, INNATE_TRAIT)
-
- if(!nukeop_outfit) // this variable is null in instances where an antagonist datum is granted via enslaving the mind (/datum/mind/proc/enslave_mind_to_creator), like in golems.
- return
-
- // If our nuke_ops_species pref is set to TRUE, (or we have no client) make us a human
- if(isnull(operative.client) || operative.client.prefs.read_preference(/datum/preference/toggle/nuke_ops_species))
- operative.set_species(/datum/species/human)
-
- operative.equip_species_outfit(nukeop_outfit)
-
- return TRUE
-
-/datum/antagonist/nukeop/greet()
- owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/ops.ogg',100,0, use_reverb = FALSE)
- to_chat(owner, span_big("You are a [nuke_team ? nuke_team.syndicate_name : "syndicate"] agent!"))
- owner.announce_objectives()
-
-/datum/antagonist/nukeop/on_gain()
- give_alias()
- forge_objectives()
- . = ..()
- equip_op()
- if(send_to_spawnpoint)
- move_to_spawnpoint()
- // grant extra TC for the people who start in the nukie base ie. not the lone op
- var/extra_tc = CEILING(GLOB.joined_player_list.len/5, 5)
- var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
- if (uplink)
- uplink.uplink_handler.add_telecrystals(extra_tc)
-
- var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
- if(uplink)
- var/datum/team/nuclear/nuke_team = get_team()
- if(!nuke_team.team_discounts)
- var/list/uplink_items = list()
- for(var/datum/uplink_item/item as anything in SStraitor.uplink_items)
- if(item.item && !item.cant_discount && (item.purchasable_from & uplink.uplink_handler.uplink_flag) && item.cost > 1)
- uplink_items += item
- nuke_team.team_discounts = list()
- nuke_team.team_discounts += create_uplink_sales(discount_team_amount, /datum/uplink_category/discount_team_gear, -1, uplink_items)
- nuke_team.team_discounts += create_uplink_sales(discount_limited_amount, /datum/uplink_category/limited_discount_team_gear, 1, uplink_items)
- uplink.uplink_handler.extra_purchasable += nuke_team.team_discounts
-
- memorize_code()
-
-/datum/antagonist/nukeop/get_team()
- return nuke_team
-
-/datum/antagonist/nukeop/apply_innate_effects(mob/living/mob_override)
- add_team_hud(mob_override || owner.current, /datum/antagonist/nukeop)
-
-/datum/antagonist/nukeop/proc/assign_nuke()
- if(!nuke_team || nuke_team.tracked_nuke)
- return
- nuke_team.memorized_code = random_nukecode()
- var/obj/machinery/nuclearbomb/syndicate/nuke = locate() in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/syndicate)
- if(!nuke)
- stack_trace("Syndicate nuke not found during nuke team creation.")
- nuke_team.memorized_code = null
- return
- nuke_team.tracked_nuke = nuke
- if(nuke.r_code == NUKE_CODE_UNSET)
- nuke.r_code = nuke_team.memorized_code
- else //Already set by admins/something else?
- nuke_team.memorized_code = nuke.r_code
- for(var/obj/machinery/nuclearbomb/beer/beernuke as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb/beer))
- beernuke.r_code = nuke_team.memorized_code
-
-/datum/antagonist/nukeop/proc/give_alias()
- if(nuke_team?.syndicate_name)
- var/mob/living/carbon/human/human_to_rename = owner.current
- if(istype(human_to_rename)) // Reinforcements get a real name
- var/first_name = owner.current.client?.prefs?.read_preference(/datum/preference/name/operative_alias) || pick(GLOB.operative_aliases)
- var/chosen_name = "[first_name] [nuke_team.syndicate_name]"
- human_to_rename.fully_replace_character_name(human_to_rename.real_name, chosen_name)
- else
- var/number = 1
- number = nuke_team.members.Find(owner)
- owner.current.real_name = "[nuke_team.syndicate_name] Operative #[number]"
-
-/datum/antagonist/nukeop/proc/memorize_code()
- if(nuke_team && nuke_team.tracked_nuke && nuke_team.memorized_code)
- antag_memory += "[nuke_team.tracked_nuke] Code : [nuke_team.memorized_code] "
- owner.add_memory(/datum/memory/key/nuke_code, nuclear_code = nuke_team.memorized_code)
- to_chat(owner, "The nuclear authorization code is: [nuke_team.memorized_code] ")
- else
- to_chat(owner, "Unfortunately the syndicate was unable to provide you with nuclear authorization code.")
-
-/datum/antagonist/nukeop/forge_objectives()
- if(nuke_team)
- objectives |= nuke_team.objectives
-
-/// Actually moves our nukie to where they should be
-/datum/antagonist/nukeop/proc/move_to_spawnpoint()
- // Ensure that the nukiebase is loaded, and wait for it if required
- SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE)
- var/turf/destination = get_spawnpoint()
- owner.current.forceMove(destination)
- if(!owner.current.onSyndieBase())
- message_admins("[ADMIN_LOOKUPFLW(owner.current)] is a NUKE OP and move_to_spawnpoint put them somewhere that isn't the syndie base, help please.")
- stack_trace("Nuke op move_to_spawnpoint resulted in a location not on the syndicate base. (Was moved to: [destination])")
-
-/// Gets the position we spawn at
-/datum/antagonist/nukeop/proc/get_spawnpoint()
- var/team_number = 1
- if(nuke_team)
- team_number = nuke_team.members.Find(owner)
-
- return GLOB.nukeop_start[((team_number - 1) % GLOB.nukeop_start.len) + 1]
-
-/datum/antagonist/nukeop/leader/get_spawnpoint()
- return pick(GLOB.nukeop_leader_start)
-
-/datum/antagonist/nukeop/create_team(datum/team/nuclear/new_team)
- if(!new_team)
- if(!always_new_team)
- for(var/datum/antagonist/nukeop/N in GLOB.antagonists)
- if(!N.owner)
- stack_trace("Antagonist datum without owner in GLOB.antagonists: [N]")
- continue
- if(N.nuke_team)
- nuke_team = N.nuke_team
- return
- nuke_team = new /datum/team/nuclear
- nuke_team.update_objectives()
- assign_nuke() //This is bit ugly
- return
- if(!istype(new_team))
- stack_trace("Wrong team type passed to [type] initialization.")
- nuke_team = new_team
-
-/datum/antagonist/nukeop/admin_add(datum/mind/new_owner,mob/admin)
- new_owner.set_assigned_role(SSjob.GetJobType(/datum/job/nuclear_operative))
- new_owner.add_antag_datum(src)
- message_admins("[key_name_admin(admin)] has nuke op'ed [key_name_admin(new_owner)].")
- log_admin("[key_name(admin)] has nuke op'ed [key_name(new_owner)].")
-
-/datum/antagonist/nukeop/get_admin_commands()
- . = ..()
- .["Send to base"] = CALLBACK(src, PROC_REF(admin_send_to_base))
- .["Tell code"] = CALLBACK(src, PROC_REF(admin_tell_code))
-
-/datum/antagonist/nukeop/proc/admin_send_to_base(mob/admin)
- owner.current.forceMove(pick(GLOB.nukeop_start))
-
-/datum/antagonist/nukeop/proc/admin_tell_code(mob/admin)
- var/code
- for (var/obj/machinery/nuclearbomb/bombue as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/nuclearbomb))
- if (length(bombue.r_code) <= 5 && bombue.r_code != initial(bombue.r_code))
- code = bombue.r_code
- break
- if (code)
- antag_memory += "Syndicate Nuclear Bomb Code : [code] "
- to_chat(owner.current, "The nuclear authorization code is: [code] ")
- else
- to_chat(admin, span_danger("No valid nuke found!"))
-
-/datum/antagonist/nukeop/get_preview_icon()
- if (!preview_outfit)
- return null
-
- var/icon/final_icon = render_preview_outfit(preview_outfit)
-
- if (!isnull(preview_outfit_behind))
- var/icon/teammate = render_preview_outfit(preview_outfit_behind)
- teammate.Blend(rgb(128, 128, 128, 128), ICON_MULTIPLY)
-
- final_icon.Blend(teammate, ICON_UNDERLAY, -world.icon_size / 4, 0)
- final_icon.Blend(teammate, ICON_UNDERLAY, world.icon_size / 4, 0)
-
- if (!isnull(nuke_icon_state))
- var/icon/nuke = icon('icons/obj/machines/nuke.dmi', nuke_icon_state)
- nuke.Shift(SOUTH, 6)
- final_icon.Blend(nuke, ICON_OVERLAY)
-
- return finish_preview_icon(final_icon)
-
-/datum/outfit/nuclear_operative
- name = "Nuclear Operative (Preview only)"
-
- back = /obj/item/mod/control/pre_equipped/empty/syndicate
- uniform = /obj/item/clothing/under/syndicate
-
-/datum/outfit/nuclear_operative/post_equip(mob/living/carbon/human/H, visualsOnly)
- var/obj/item/mod/module/armor_booster/booster = locate() in H.back
- booster.active = TRUE
- H.update_worn_back()
-
-/datum/outfit/nuclear_operative_elite
- name = "Nuclear Operative (Elite, Preview only)"
-
- back = /obj/item/mod/control/pre_equipped/empty/elite
- uniform = /obj/item/clothing/under/syndicate
- l_hand = /obj/item/modular_computer/pda/nukeops
- r_hand = /obj/item/shield/energy
-
-/datum/outfit/nuclear_operative_elite/post_equip(mob/living/carbon/human/H, visualsOnly)
- var/obj/item/mod/module/armor_booster/booster = locate() in H.back
- booster.active = TRUE
- H.update_worn_back()
- var/obj/item/shield/energy/shield = locate() in H.held_items
- shield.icon_state = "[shield.base_icon_state]1"
- H.update_held_items()
-
-/datum/antagonist/nukeop/leader
- name = "Nuclear Operative Leader"
- nukeop_outfit = /datum/outfit/syndicate/leader
- always_new_team = TRUE
- /// Randomly chosen honorific, for distinction
- var/title
- /// The nuclear challenge remote we will spawn this player with.
- var/challengeitem = /obj/item/nuclear_challenge
-
-/datum/antagonist/nukeop/leader/memorize_code()
- ..()
- if(nuke_team?.memorized_code)
- var/obj/item/paper/nuke_code_paper = new
- nuke_code_paper.add_raw_text("The nuclear authorization code is: [nuke_team.memorized_code] ")
- nuke_code_paper.name = "nuclear bomb code"
- var/mob/living/carbon/human/H = owner.current
- if(!istype(H))
- nuke_code_paper.forceMove(get_turf(H))
- else
- H.put_in_hands(nuke_code_paper, TRUE)
- H.update_icons()
-
-/datum/antagonist/nukeop/leader/greet()
- owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/ops.ogg',100,0, use_reverb = FALSE)
- to_chat(owner, "You are the Syndicate [title] for this mission. You are responsible for guiding the team and your ID is the only one who can open the launch bay doors. ")
- to_chat(owner, "If you feel you are not up to this task, give your ID and radio to another operative. ")
- if(!CONFIG_GET(flag/disable_warops))
- to_chat(owner, "In your hand you will find a special item capable of triggering a greater challenge for your team. Examine it carefully and consult with your fellow operatives before activating it. ")
- owner.announce_objectives()
-
-/datum/antagonist/nukeop/leader/on_gain()
- . = ..()
- if(!CONFIG_GET(flag/disable_warops))
- var/mob/living/carbon/human/leader = owner.current
- var/obj/item/war_declaration = new challengeitem(leader.drop_location())
- leader.put_in_hands(war_declaration)
- nuke_team.war_button_ref = WEAKREF(war_declaration)
- addtimer(CALLBACK(src, PROC_REF(nuketeam_name_assign)), 1)
-
-/datum/antagonist/nukeop/leader/proc/nuketeam_name_assign()
- if(!nuke_team)
- return
- nuke_team.rename_team(ask_name())
-
-/datum/team/nuclear/proc/rename_team(new_name)
- syndicate_name = new_name
- name = "[syndicate_name] Team"
- for(var/I in members)
- var/datum/mind/synd_mind = I
- var/mob/living/carbon/human/human_to_rename = synd_mind.current
- if(!istype(human_to_rename))
- continue
- var/first_name = human_to_rename.client?.prefs?.read_preference(/datum/preference/name/operative_alias) || pick(GLOB.operative_aliases)
- var/chosen_name = "[first_name] [syndicate_name]"
- human_to_rename.fully_replace_character_name(human_to_rename.real_name, chosen_name)
-
-/datum/antagonist/nukeop/leader/proc/ask_name()
- var/randomname = pick(GLOB.last_names)
- var/newname = tgui_input_text(owner.current, "You are the nuclear operative [title]. Please choose a last name for your family.", "Name change", randomname, MAX_NAME_LEN)
- if (!newname)
- newname = randomname
- else
- newname = reject_bad_name(newname)
- if(!newname)
- newname = randomname
-
- return capitalize(newname)
-
-/datum/antagonist/nukeop/lone
- name = "Lone Operative"
- always_new_team = TRUE
- send_to_spawnpoint = FALSE //Handled by event
- nukeop_outfit = /datum/outfit/syndicate/full
- preview_outfit = /datum/outfit/nuclear_operative
- preview_outfit_behind = null
- nuke_icon_state = null
-
-/datum/antagonist/nukeop/lone/assign_nuke()
- if(nuke_team && !nuke_team.tracked_nuke)
- nuke_team.memorized_code = random_nukecode()
- var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct)
- if(nuke)
- nuke_team.tracked_nuke = nuke
- if(nuke.r_code == NUKE_CODE_UNSET)
- nuke.r_code = nuke_team.memorized_code
- else //Already set by admins/something else?
- nuke_team.memorized_code = nuke.r_code
- else
- stack_trace("Station self-destruct not found during lone op team creation.")
- nuke_team.memorized_code = null
-
-/datum/antagonist/nukeop/reinforcement
- show_in_antagpanel = FALSE
- send_to_spawnpoint = FALSE
- nukeop_outfit = /datum/outfit/syndicate/reinforcement
-
-/datum/team/nuclear
- var/syndicate_name
- var/obj/machinery/nuclearbomb/tracked_nuke
- var/core_objective = /datum/objective/nuclear
- var/memorized_code
- var/list/team_discounts
- var/datum/weakref/war_button_ref
-
-/datum/team/nuclear/New()
- ..()
- syndicate_name = syndicate_name()
-
-/datum/team/nuclear/proc/update_objectives()
- if(core_objective)
- var/datum/objective/O = new core_objective
- O.team = src
- objectives += O
-
-/datum/team/nuclear/proc/is_disk_rescued()
- for(var/obj/item/disk/nuclear/nuke_disk in SSpoints_of_interest.real_nuclear_disks)
- //If emergency shuttle is in transit disk is only safe on it
- if(SSshuttle.emergency.mode == SHUTTLE_ESCAPE)
- if(!SSshuttle.emergency.is_in_shuttle_bounds(nuke_disk))
- return FALSE
- //If shuttle escaped check if it's on centcom side
- else if(SSshuttle.emergency.mode == SHUTTLE_ENDGAME)
- if(!nuke_disk.onCentCom())
- return FALSE
- else //Otherwise disk is safe when on station
- var/turf/disk_turf = get_turf(nuke_disk)
- if(!disk_turf || !is_station_level(disk_turf.z))
- return FALSE
- return TRUE
-
-/datum/team/nuclear/proc/are_all_operatives_dead()
- for(var/datum/mind/operative_mind as anything in members)
- if(ishuman(operative_mind.current) && (operative_mind.current.stat != DEAD))
- return FALSE
- return TRUE
-
-/datum/team/nuclear/proc/get_result()
- var/shuttle_evacuated = EMERGENCY_ESCAPED_OR_ENDGAMED
- var/shuttle_landed_base = SSshuttle.emergency.is_hijacked()
- var/disk_rescued = is_disk_rescued()
- var/syndies_didnt_escape = !is_infiltrator_docked_at_syndiebase()
- var/team_is_dead = are_all_operatives_dead()
- var/station_was_nuked = GLOB.station_was_nuked
- var/station_nuke_source = GLOB.station_nuke_source
-
- // The nuke detonated on the syndicate base
- if(station_nuke_source == DETONATION_HIT_SYNDIE_BASE)
- return NUKE_RESULT_FLUKE
-
- // The station was nuked
- if(station_was_nuked)
- // The station was nuked and the infiltrator failed to escape
- if(syndies_didnt_escape)
- return NUKE_RESULT_NOSURVIVORS
- // The station was nuked and the infiltrator escaped, and the nuke ops won
- else
- return NUKE_RESULT_NUKE_WIN
-
- // The station was not nuked, but something was
- else if(station_nuke_source && !disk_rescued)
- // The station was not nuked, but something was, and the syndicates didn't escape it
- if(syndies_didnt_escape)
- return NUKE_RESULT_WRONG_STATION_DEAD
- // The station was not nuked, but something was, and the syndicates returned to their base
- else
- return NUKE_RESULT_WRONG_STATION
-
- // Nuke didn't blow, but nukies somehow hijacked the emergency shuttle to land at the base anyways.
- else if(shuttle_landed_base)
- if(disk_rescued)
- return NUKE_RESULT_HIJACK_DISK
- else
- return NUKE_RESULT_HIJACK_NO_DISK
-
- // No nuke went off, the station rescued the disk
- else if(disk_rescued)
- // No nuke went off, the shuttle left, and the team is dead
- if(shuttle_evacuated && team_is_dead)
- return NUKE_RESULT_CREW_WIN_SYNDIES_DEAD
- // No nuke went off, but the nuke ops survived
- else
- return NUKE_RESULT_CREW_WIN
-
- // No nuke went off, but the disk was left behind
- else
- // No nuke went off, the disk was left, but all the ops are dead
- if(team_is_dead)
- return NUKE_RESULT_DISK_LOST
- // No nuke went off, the disk was left, there are living ops, but the shuttle left successfully
- else if(shuttle_evacuated)
- return NUKE_RESULT_DISK_STOLEN
-
- CRASH("[type] - got an undefined / unexpected result.")
-
-/datum/team/nuclear/roundend_report()
- var/list/parts = list()
- parts += ""
-
- switch(get_result())
- if(NUKE_RESULT_FLUKE)
- parts += "Humiliating Syndicate Defeat "
- parts += "The crew of [station_name()] gave [syndicate_name] operatives back their bomb! The syndicate base was destroyed! Next time, don't lose the nuke!"
- if(NUKE_RESULT_NUKE_WIN)
- parts += "Syndicate Major Victory! "
- parts += "[syndicate_name] operatives have destroyed [station_name()]! "
- if(NUKE_RESULT_NOSURVIVORS)
- parts += "Total Annihilation! "
- parts += "[syndicate_name] operatives destroyed [station_name()] but did not leave the area in time and got caught in the explosion. Next time, don't lose the disk!"
- if(NUKE_RESULT_WRONG_STATION)
- parts += "Crew Minor Victory! "
- parts += "[syndicate_name] operatives secured the authentication disk but blew up something that wasn't [station_name()]. Next time, don't do that!"
- if(NUKE_RESULT_WRONG_STATION_DEAD)
- parts += "[syndicate_name] operatives have earned Darwin Award! "
- parts += "[syndicate_name] operatives blew up something that wasn't [station_name()] and got caught in the explosion. Next time, don't do that!"
- if(NUKE_RESULT_HIJACK_DISK)
- parts += "Syndicate Miniscule Victory! "
- parts += "[syndicate_name] operatives failed to destroy [station_name()], but they managed to secure the disk and hijack the emergency shuttle, causing it to land on the syndicate base. Good job? "
- if(NUKE_RESULT_HIJACK_NO_DISK)
- parts += "Syndicate Insignificant Victory! "
- parts += "[syndicate_name] operatives failed to destroy [station_name()] or secure the disk, but they managed to hijack the emergency shuttle, causing it to land on the syndicate base. Good job? "
- if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD)
- parts += "Crew Major Victory! "
- parts += "The Research Staff has saved the disk and killed the [syndicate_name] Operatives "
- if(NUKE_RESULT_CREW_WIN)
- parts += "Crew Major Victory! "
- parts += "The Research Staff has saved the disk and stopped the [syndicate_name] Operatives! "
- if(NUKE_RESULT_DISK_LOST)
- parts += "Neutral Victory! "
- parts += "The Research Staff failed to secure the authentication disk but did manage to kill most of the [syndicate_name] Operatives! "
- if(NUKE_RESULT_DISK_STOLEN)
- parts += "Syndicate Minor Victory! "
- parts += "[syndicate_name] operatives survived the assault but did not achieve the destruction of [station_name()]. Next time, don't lose the disk!"
- else
- parts += "Neutral Victory "
- parts += "Mission aborted! "
-
- var/text = " "
- var/purchases = ""
- var/TC_uses = 0
- LAZYINITLIST(GLOB.uplink_purchase_logs_by_key)
- for(var/I in members)
- var/datum/mind/syndicate = I
- var/datum/uplink_purchase_log/H = GLOB.uplink_purchase_logs_by_key[syndicate.key]
- if(H)
- TC_uses += H.total_spent
- purchases += H.generate_render(show_key = FALSE)
- text += printplayerlist(members)
- text += " "
- text += "(Syndicates used [TC_uses] TC) [purchases]"
- if(TC_uses == 0 && GLOB.station_was_nuked && !are_all_operatives_dead())
- text += "[icon2html('icons/ui_icons/antags/badass.dmi', world, "badass")] "
-
- parts += text
-
- return "[parts.Join(" ")]
"
-
-/datum/team/nuclear/antag_listing_name()
- if(syndicate_name)
- return "[syndicate_name] Syndicates"
- else
- return "Syndicates"
-
-/datum/team/nuclear/antag_listing_entry()
- var/disk_report = "Nuclear Disk(s) "
- disk_report += ""
- for(var/obj/item/disk/nuclear/N in SSpoints_of_interest.real_nuclear_disks)
- disk_report += "[N.name], "
- var/atom/disk_loc = N.loc
- while(!isturf(disk_loc))
- if(ismob(disk_loc))
- var/mob/M = disk_loc
- disk_report += "carried by [M.real_name] "
- if(isobj(disk_loc))
- var/obj/O = disk_loc
- disk_report += "in \a [O.name] "
- disk_loc = disk_loc.loc
- disk_report += "in [disk_loc.loc] at ([disk_loc.x], [disk_loc.y], [disk_loc.z]) FLW "
- disk_report += "
"
-
- var/post_report
-
- var/war_declared = FALSE
- for(var/obj/item/circuitboard/computer/syndicate_shuttle/board as anything in GLOB.syndicate_shuttle_boards)
- if(board.challenge)
- war_declared = TRUE
-
- var/force_war_button = ""
-
- if(war_declared)
- post_report += "War declared. "
- force_war_button = "\[Force war\]"
- else
- post_report += "War not declared. "
- var/obj/item/nuclear_challenge/war_button = war_button_ref?.resolve()
- if(war_button)
- force_war_button = "\[Force war\] "
- else
- force_war_button = "\[Cannot declare war, challenge button missing!\]"
-
- post_report += "\n[force_war_button]"
- post_report += "\n\[Send Reinforcement\] "
-
- var/final_report = ..()
- final_report += disk_report
- final_report += post_report
- return final_report
-
-#define SPAWN_AT_BASE "Nuke base"
-#define SPAWN_AT_INFILTRATOR "Infiltrator"
-
-/datum/team/nuclear/proc/admin_spawn_reinforcement(mob/admin)
- if(!check_rights_for(admin.client, R_ADMIN))
- return
-
- var/infil_or_nukebase = tgui_alert(
- admin,
- "Spawn them at the nuke base, or in the Infiltrator?",
- "Where to reinforce?",
- list(SPAWN_AT_BASE, SPAWN_AT_INFILTRATOR, "Cancel"),
- )
-
- if(!infil_or_nukebase || infil_or_nukebase == "Cancel")
- return
-
- var/tc_to_spawn = tgui_input_number(admin, "How much TC to spawn with?", "TC", 0, 100)
-
- var/list/nuke_candidates = SSpolling.poll_ghost_candidates(
- "Do you want to play as an emergency syndicate reinforcement?",
- check_jobban = ROLE_OPERATIVE,
- role = ROLE_OPERATIVE,
- poll_time = 30 SECONDS,
- ignore_category = POLL_IGNORE_SYNDICATE,
- pic_source = /obj/structure/sign/poster/contraband/gorlex_recruitment,
- role_name_text = "syndicate reinforcement",
- )
-
- nuke_candidates -= admin // may be easy to fat-finger say yes. so just don't
-
- if(!length(nuke_candidates))
- tgui_alert(admin, "No candidates found.", "Recruitment Shortage", list("OK"))
- return
-
-
- var/turf/spawn_loc
- if(infil_or_nukebase == SPAWN_AT_INFILTRATOR)
- var/area/spawn_in
- // Prioritize EVA then hallway, if neither can be found default to the first area we can find
- for(var/area_type in list(/area/shuttle/syndicate/eva, /area/shuttle/syndicate/hallway, /area/shuttle/syndicate))
- spawn_in = locate(area_type) in GLOB.areas // I'd love to use areas_by_type but the Infiltrator is a unique area
- if(spawn_in)
- break
-
- var/list/turf/options = list()
- for(var/turf/open/open_turf in spawn_in?.get_turfs_from_all_zlevels())
- if(open_turf.is_blocked_turf())
- continue
- options += open_turf
-
- if(length(options))
- spawn_loc = pick(options)
- else
- infil_or_nukebase = SPAWN_AT_BASE
-
- if(infil_or_nukebase == SPAWN_AT_BASE)
- spawn_loc = pick(GLOB.nukeop_start)
-
- var/mob/dead/observer/picked = pick(nuke_candidates)
- var/mob/living/carbon/human/nukie = new(spawn_loc)
- picked.client.prefs.safe_transfer_prefs_to(nukie, is_antag = TRUE)
- nukie.key = picked.key
-
- var/datum/antagonist/nukeop/antag_datum = new()
- antag_datum.send_to_spawnpoint = FALSE
- antag_datum.nukeop_outfit = /datum/outfit/syndicate/reinforcement
-
- nukie.mind.add_antag_datum(antag_datum, src)
-
- var/datum/component/uplink/uplink = nukie.mind.find_syndicate_uplink()
- uplink?.uplink_handler.set_telecrystals(tc_to_spawn)
-
- // add some pizzazz
- do_sparks(4, FALSE, spawn_loc)
- new /obj/effect/temp_visual/teleport_abductor/syndi_teleporter(spawn_loc)
- playsound(spawn_loc, SFX_SPARKS, 50, TRUE)
- playsound(spawn_loc, 'sound/effects/phasein.ogg', 50, TRUE)
-
- tgui_alert(admin, "Reinforcement spawned at [infil_or_nukebase] with [tc_to_spawn].", "Reinforcements have arrived", list("God speed"))
-
-#undef SPAWN_AT_BASE
-#undef SPAWN_AT_INFILTRATOR
-
-/// Returns whether or not syndicate operatives escaped.
-/proc/is_infiltrator_docked_at_syndiebase()
- var/obj/docking_port/mobile/infiltrator/infiltrator_port = SSshuttle.getShuttle("syndicate")
-
- var/datum/lazy_template/nukie_base/nukie_template = GLOB.lazy_templates[LAZY_TEMPLATE_KEY_NUKIEBASE]
- if(!nukie_template)
- return FALSE // if its not even loaded, cant be docked
-
- for(var/datum/turf_reservation/loaded_area as anything in nukie_template.reservations)
- var/infiltrator_turf = get_turf(infiltrator_port)
- if(infiltrator_turf in loaded_area.reserved_turfs)
- return TRUE
- return FALSE
diff --git a/code/modules/antagonists/nukeop/outfits.dm b/code/modules/antagonists/nukeop/outfits.dm
index a3c97a764688b..e9a293c3e9981 100644
--- a/code/modules/antagonists/nukeop/outfits.dm
+++ b/code/modules/antagonists/nukeop/outfits.dm
@@ -163,3 +163,30 @@
shoes = /obj/item/clothing/shoes/laceup
glasses = /obj/item/clothing/glasses/sunglasses/big
faction = "MI13"
+
+/datum/outfit/nuclear_operative
+ name = "Nuclear Operative (Preview only)"
+
+ back = /obj/item/mod/control/pre_equipped/empty/syndicate
+ uniform = /obj/item/clothing/under/syndicate
+
+/datum/outfit/nuclear_operative/post_equip(mob/living/carbon/human/H, visualsOnly)
+ var/obj/item/mod/module/armor_booster/booster = locate() in H.back
+ booster.active = TRUE
+ H.update_worn_back()
+
+/datum/outfit/nuclear_operative_elite
+ name = "Nuclear Operative (Elite, Preview only)"
+
+ back = /obj/item/mod/control/pre_equipped/empty/elite
+ uniform = /obj/item/clothing/under/syndicate
+ l_hand = /obj/item/modular_computer/pda/nukeops
+ r_hand = /obj/item/shield/energy
+
+/datum/outfit/nuclear_operative_elite/post_equip(mob/living/carbon/human/H, visualsOnly)
+ var/obj/item/mod/module/armor_booster/booster = locate() in H.back
+ booster.active = TRUE
+ H.update_worn_back()
+ var/obj/item/shield/energy/shield = locate() in H.held_items
+ shield.icon_state = "[shield.base_icon_state]1"
+ H.update_held_items()
diff --git a/code/modules/antagonists/spy/spy.dm b/code/modules/antagonists/spy/spy.dm
new file mode 100644
index 0000000000000..e0ea7e4075404
--- /dev/null
+++ b/code/modules/antagonists/spy/spy.dm
@@ -0,0 +1,212 @@
+/datum/antagonist/spy
+ name = "\improper Spy"
+ roundend_category = "spies"
+ antagpanel_category = "Spy"
+ antag_hud_name = "spy"
+ job_rank = ROLE_SPY
+ antag_moodlet = /datum/mood_event/focused
+ hijack_speed = 1
+ ui_name = "AntagInfoSpy"
+ preview_outfit = /datum/outfit/spy
+ /// Whether an uplink has been created (successfully or at all)
+ var/uplink_created = FALSE
+ /// String displayed in the antag panel pointing the spy to where their uplink is.
+ var/uplink_location
+ /// Whether we give them some random objetives to aim for.
+ var/spawn_with_objectives = TRUE
+ /// Tracks number of bounties claimed, for roundend
+ var/bounties_claimed = 0
+ /// Tracks all loot items the spy has claimed, for roundend
+ var/list/all_loot = list()
+ /// Weakref to our spy uplink
+ /// Only exists for the sole purpose of letting admins see it
+ var/datum/weakref/uplink_weakref
+
+/datum/antagonist/spy/on_gain()
+ if(!uplink_created)
+ auto_create_spy_uplink(owner.current)
+ if(spawn_with_objectives)
+ give_random_objectives()
+ . = ..()
+ SEND_SOUND(owner.current, sound('sound/ambience/antag/spy.ogg'))
+
+/datum/antagonist/spy/ui_static_data(mob/user)
+ var/list/data = ..()
+ data["uplink_location"] = uplink_location
+ return data
+
+/datum/antagonist/spy/get_admin_commands()
+ . = ..()
+ // I wanted to put this in check-antagonists but it's less conducive to that
+ .["See All Bounties (For all spies)"] = CALLBACK(src, PROC_REF(see_bounties))
+ .["Refresh Bounties (For all spies)"] = CALLBACK(src, PROC_REF(refresh_bounties))
+ .["Give Spy Uplink"] = CALLBACK(src, PROC_REF(admin_create_spy_uplink))
+ .["Bounty Handler VV"] = CALLBACK(src, PROC_REF(bounty_handler_vv))
+
+/datum/antagonist/spy/proc/see_bounties()
+ if(!check_rights(R_ADMIN|R_DEBUG))
+ return
+
+ var/datum/component/spy_uplink/uplink = uplink_weakref?.resolve()
+ if(isnull(uplink))
+ tgui_alert(usr, "No spy uplink!", "Mission Failed")
+ return
+
+ uplink.ui_interact(usr)
+
+/datum/antagonist/spy/proc/refresh_bounties()
+ if(!check_rights(R_ADMIN|R_DEBUG))
+ return
+
+ var/datum/component/spy_uplink/uplink = uplink_weakref?.resolve()
+ if(isnull(uplink))
+ tgui_alert(usr, "No spy uplink!", "Mission Failed")
+ return
+
+ uplink.handler.force_refresh()
+ tgui_alert(usr, "Bounties refreshed.", "Mission Success")
+
+/datum/antagonist/spy/proc/admin_create_spy_uplink()
+ if(!check_rights(R_ADMIN|R_DEBUG))
+ return
+
+ if(!auto_create_spy_uplink(owner.current, give_backup = FALSE))
+ tgui_alert(usr, "Failed to give [owner.current] a spy uplink - likely don't have a valid item to host it.", "Mission Failed")
+
+/datum/antagonist/spy/proc/bounty_handler_vv()
+ if(!check_rights(R_ADMIN|R_DEBUG))
+ return
+
+ var/datum/component/spy_uplink/uplink = uplink_weakref?.resolve()
+ if(isnull(uplink))
+ tgui_alert(usr, "No spy uplink!", "Mission Failed")
+ return
+
+ usr.client?.debug_variables(uplink.handler)
+
+/datum/antagonist/spy/proc/auto_create_spy_uplink(mob/living/carbon/spy, give_backup = TRUE)
+ if(!iscarbon(spy))
+ return FALSE
+
+ var/spy_uplink_loc = spy.client?.prefs?.read_preference(/datum/preference/choiced/uplink_location)
+ if(isnull(spy_uplink_loc) || spy_uplink_loc == UPLINK_IMPLANT)
+ spy_uplink_loc = pick(UPLINK_PEN, UPLINK_PDA)
+
+ var/obj/item/spy_uplink = spy.get_uplink_location(spy_uplink_loc)
+ if(isnull(spy_uplink) || !create_spy_uplink(spy, spy_uplink))
+ if(give_backup)
+ var/datum/action/backup_uplink/backup = new(src)
+ backup.Grant(spy)
+ to_chat(spy, span_boldnotice("You were unable to be supplied with an uplink, so you have been given the ability to create one yourself."))
+ return FALSE
+
+ return TRUE
+
+/datum/antagonist/spy/proc/create_spy_uplink(mob/living/carbon/spy, obj/item/spy_uplink)
+ var/datum/component/spy_uplink/uplink = spy_uplink.AddComponent(/datum/component/spy_uplink, src)
+ if(!uplink)
+ return FALSE
+
+ uplink_weakref = WEAKREF(uplink)
+ uplink_created = TRUE
+
+ if(istype(spy_uplink, /obj/item/modular_computer/pda))
+ uplink_location = "your PDA"
+
+ else if(istype(spy_uplink, /obj/item/pen))
+ if(istype(spy_uplink.loc, /obj/item/modular_computer/pda))
+ uplink_location = "your PDA's pen"
+ else
+ uplink_location = "a pen"
+
+ else if(istype(spy_uplink, /obj/item/radio))
+ uplink_location = "your radio headset"
+
+ return TRUE
+
+/datum/antagonist/spy/proc/give_random_objectives()
+ for(var/i in 1 to rand(1, 3))
+ var/datum/objective/custom/your_mission = new()
+ your_mission.owner = owner
+ your_mission.explanation_text = pick_list_replacements(SPY_OBJECTIVE_FILE, "objective_body")
+ objectives += your_mission
+
+ if(prob(10))
+ var/datum/objective/martyr/leave_no_trace = new()
+ leave_no_trace.owner = owner
+ objectives += leave_no_trace
+
+ else if(prob(3)) //3% chance on 90% chance
+ var/datum/objective/hijack/steal_the_shuttle = new()
+ steal_the_shuttle.owner = owner
+ objectives += steal_the_shuttle
+
+ else
+ var/datum/objective/escape/gtfo = new()
+ gtfo.owner = owner
+ objectives += gtfo
+
+/datum/antagonist/spy/antag_panel_data()
+ return "Bounties Claimed: [bounties_claimed]"
+
+/datum/antagonist/spy/roundend_report()
+ var/list/report = list()
+ report += printplayer(owner)
+ report += " - They completed [bounties_claimed] bounties."
+ if(bounties_claimed > 0)
+ report += " - They received the following rewards: [english_list(all_loot)]"
+ report += printobjectives(objectives)
+ return report.Join(" ")
+
+/datum/antagonist/spy/get_preview_icon()
+ var/mob/living/carbon/human/dummy/consistent/dummy = new()
+ dummy.set_haircolor(COLOR_SILVER, update = FALSE)
+ dummy.set_hairstyle("CIA", update = FALSE)
+ return finish_preview_icon(render_preview_outfit(preview_outfit, dummy))
+
+/datum/outfit/spy
+ name = "Spy (Preview only)"
+ // Balaclava sprite is ass, otherwise I would use it for this
+ uniform = /obj/item/clothing/under/suit/black
+ gloves = /obj/item/clothing/gloves/color/black
+ shoes = /obj/item/clothing/shoes/jackboots
+ head = /obj/item/clothing/head/fedora
+ suit = /obj/item/clothing/suit/jacket/trenchcoat
+ glasses = /obj/item/clothing/glasses/osi
+ ears = /obj/item/radio/headset
+
+/datum/action/backup_uplink
+ name = "Create Uplink"
+ desc = "Fashion a PDA, Pen or Radio Headset into a swanky Spy Uplink."
+ var/list/valid_types = list(
+ /obj/item/modular_computer/pda,
+ /obj/item/pen,
+ /obj/item/radio,
+ )
+
+/datum/action/backup_uplink/New(Target)
+ . = ..()
+ if(!istype(Target, /datum/antagonist/spy))
+ stack_trace("[type] created on invalid target [Target || "null"]")
+ qdel(src)
+
+/datum/action/backup_uplink/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/spy = usr
+ var/obj/item/held_thing = spy.get_active_held_item()
+ if(isnull(held_thing))
+ spy.balloon_alert(spy, "you need to hold something!")
+ return
+
+ if(!is_type_in_list(held_thing, valid_types))
+ held_thing.balloon_alert(spy, "invalid item!")
+ return
+
+ var/datum/antagonist/spy/spy_datum = target
+ spy_datum.create_spy_uplink(spy, held_thing)
+ held_thing.balloon_alert(spy, "uplink created")
+
+ qdel(src)
diff --git a/code/modules/antagonists/spy/spy_bounty.dm b/code/modules/antagonists/spy/spy_bounty.dm
new file mode 100644
index 0000000000000..035ebba340512
--- /dev/null
+++ b/code/modules/antagonists/spy/spy_bounty.dm
@@ -0,0 +1,684 @@
+/**
+ * ## Spy Bounty
+ *
+ * A datum used to track a single spy bounty.
+ * Not a singleton - whenever bounties are re-rolled, the entire list is deleted and new bounty datums are instantiated.
+ *
+ * When bounties are completed, they are also not deleted, but instead marked as claimed.
+ */
+/datum/spy_bounty
+ /// The name of the bounty.
+ /// Should be a short description without punctuation.
+ /// IE: "Steal the captain's ID"
+ var/name
+ /// Help text for the bounty.
+ /// Should include additional information about the bounty to assist the spy in figuring out what to do.
+ /// Should be punctuated.
+ /// IE: "Steal the captain's ID. It was last seen in the captain's office."
+ var/help
+ /// Difficult of the bounty, one of [SPY_DIFFICULTY_EASY], [SPY_DIFFICULTY_MEDIUM], [SPY_DIFFICULTY_HARD].
+ /// Must be set to one of the possible bounties to be picked.
+ var/difficulty = "unset"
+ /// How long of a do-after must be completed by the Spy to turn in the bounty.
+ var/theft_time = 2 SECONDS
+ /// Probability that the stolen item will be sent to the black market instead of destroyed.
+ /// Guaranteed if the item is indestructible.
+ var/black_market_prob = 50
+ /// Weight that the bounty will be selected.
+ var/weight = 1
+
+ /// Whether the bounty's been fully initialized. If this is not set, the bounty will be rerolled.
+ VAR_FINAL/initalized = FALSE
+ /// Whether the bounty has been completed.
+ VAR_FINAL/claimed = FALSE
+ /// What uplink item the bounty will reward on completion.
+ VAR_FINAL/datum/uplink_item/reward_item
+
+/datum/spy_bounty/New(datum/spy_bounty_handler/handler)
+ if(!init_bounty(handler))
+ return
+
+ initalized = TRUE
+ select_reward(handler)
+
+/// Helper that translates the bounty into UI data for TGUI
+/datum/spy_bounty/proc/to_ui_data(mob/user)
+ SHOULD_CALL_PARENT(TRUE)
+ return list(
+ "name" = name,
+ "help" = help,
+ "difficulty" = difficulty,
+ "reward" = reward_item.name,
+ "claimed" = claimed,
+ "can_claim" = can_claim(user),
+ )
+
+/// Check if the passed mob can claim this bounty.
+/datum/spy_bounty/proc/can_claim(mob/user)
+ SHOULD_BE_PURE(TRUE)
+ return TRUE
+
+/**
+ * Initializes the bounty, setting up targets and etc.
+ *
+ * * handler - The bounty handler that is creating this bounty.
+ *
+ * Returning FALSE will cancel initialization and force it to reroll the bounty.
+ */
+/datum/spy_bounty/proc/init_bounty(datum/spy_bounty_handler/handler)
+ return FALSE
+
+/// Selects what uplink item the bounty will reward on completion.
+/datum/spy_bounty/proc/select_reward(datum/spy_bounty_handler/handler)
+ var/list/loot_pool = handler.possible_uplink_items[difficulty]
+
+ if(!length(loot_pool))
+ reward_item = /datum/uplink_item/bundles_tc/telecrystal
+ return // future todo : add some junk items for when we run out of items
+
+ reward_item = pick(loot_pool)
+ if(prob(80))
+ loot_pool -= reward_item
+
+/**
+ * Checks if the passed movable is a valid target for this bounty.
+ *
+ * * stealing - The movable to check.
+ *
+ * Returning FALSE simply means that the passed movable is not valid for this bounty.
+ */
+/datum/spy_bounty/proc/is_stealable(atom/movable/stealing)
+ // SHOULD_BE_PURE(TRUE)
+ return FALSE
+
+/**
+ * What is this bounty's "dupe protection key"?
+ * This is used to determine if a duplicate of this bounty has been rolled before / in the last refresh.
+ * You can check if a bounty has been duped by accessing the handler's claimed_bounties_from_last_pool or all_claimed_bounty_types list.
+ *
+ * * stealing - The item that was stolen.
+ * * handler - The handler that is handling the bounty.
+ *
+ * Return a string key, what this uses for dupe protection.
+ */
+/datum/spy_bounty/proc/get_dupe_protection_key(atom/movable/stealing)
+ return stealing.type
+
+/**
+ * Checks if the passed dupe key is a duplicate of an previously claimed bounty.
+ *
+ * * handler - The handler that is handling the bounty.
+ * * dupe_key - The key to check for dupes
+ * * dupe_prob - The probability of a dupe being allowed when checking all_claimed_bounty_types.
+ * This allows you to have a chance that distant dupes allowed depending on how many times they've been done.
+ *
+ * Returns TRUE if the bounty is a dupe, FALSE if it is not.
+ */
+/datum/spy_bounty/proc/check_dupe(datum/spy_bounty_handler/handler, dupe_key, dupe_prob = 0)
+ if(handler.claimed_bounties_from_last_pool[dupe_key])
+ return TRUE
+ if(prob(dupe_prob * handler.all_claimed_bounty_types[dupe_key]))
+ return TRUE
+ return FALSE
+
+/**
+ * Called when the bounty is completed, to handle how the stolen item is "stolen".
+ *
+ * By default, stolen items are simply deleted.
+ *
+ * * stealing - The item that was stolen.
+ * * spy - The spy that stole the item.
+ */
+/datum/spy_bounty/proc/clean_up_stolen_item(atom/movable/stealing, mob/living/spy)
+ do_sparks(3, FALSE, stealing)
+
+ // Don't mess with it while it's going away
+ stealing.interaction_flags_atom &= ~INTERACT_ATOM_ATTACK_HAND
+ stealing.anchored = TRUE
+ // Add some pizzazz
+ animate(stealing, time = 0.5 SECONDS, transform = matrix(stealing.transform).Scale(0.01), easing = CUBIC_EASING)
+
+ if(isitem(stealing) && ((stealing.resistance_flags & INDESTRUCTIBLE) || prob(black_market_prob)))
+ addtimer(CALLBACK(src, PROC_REF(send_to_black_market), stealing), 0.5 SECONDS)
+ else
+ QDEL_IN(stealing, 0.5 SECONDS)
+
+/**
+ * Handles putting the passed movable up on the black market.
+ *
+ * By the end of this proc, the item should either be deleted (if failure) or in nullspace (on the black market).
+ *
+ * * thing - The item to put up on the black market.
+ */
+/datum/spy_bounty/proc/send_to_black_market(atom/movable/thing)
+ if(QDELETED(thing)) // Just in case anything does anything weird
+ return FALSE
+
+ thing.interaction_flags_atom = initial(thing.interaction_flags_atom)
+ thing.anchored = initial(thing.anchored)
+ thing.moveToNullspace()
+
+ var/datum/market_item/new_item = new()
+ new_item.item = thing
+ new_item.name = "Stolen [thing.name]"
+ new_item.desc = "A [thing.name], stolen from somewhere on the station. Whoever owned it probably wouldn't be happy to see it here."
+ new_item.category = "Fenced Goods"
+ new_item.stock = 1
+ new_item.availability_prob = 100
+
+ switch(difficulty)
+ if(SPY_DIFFICULTY_EASY)
+ new_item.price = PAYCHECK_COMMAND * 2.5
+ if(SPY_DIFFICULTY_MEDIUM)
+ new_item.price = PAYCHECK_COMMAND * 5
+ if(SPY_DIFFICULTY_HARD)
+ new_item.price = PAYCHECK_COMMAND * 10
+
+ new_item.price += rand(0, PAYCHECK_COMMAND * 5)
+ if(thing.resistance_flags & INDESTRUCTIBLE)
+ new_item.price *= 2
+
+ return SSblackmarket.markets[/datum/market/blackmarket].add_item(new_item)
+
+/// Steal an item
+/datum/spy_bounty/objective_item
+ /// Reference to an objective item datum that we want stolen.
+ VAR_FINAL/datum/objective_item/desired_item
+ /// Typecache of objective items that should not be selected.
+ var/static/list/blacklisted_item_types = typecacheof(list(
+ /datum/objective_item/steal/functionalai,
+ /datum/objective_item/steal/nukedisc,
+ ))
+
+/datum/spy_bounty/objective_item/can_claim(mob/user)
+ return !(user.mind?.assigned_role.title in desired_item.excludefromjob)
+
+/datum/spy_bounty/objective_item/get_dupe_protection_key(atom/movable/stealing)
+ return desired_item.targetitem
+
+/// Determines if the passed objective item is a reasonable, valid theft target.
+/datum/spy_bounty/objective_item/proc/is_valid_objective_item(datum/objective_item/item)
+ if(length(item.special_equipment) || item.difficulty <= 0 || item.difficulty >= 6)
+ return FALSE
+ if(is_type_in_typecache(item, blacklisted_item_types))
+ return FALSE
+ if(!item.exists_on_map)
+ return TRUE
+ var/list/all_valid_existing_things = list()
+ for(var/obj/item/existing_thing as anything in GLOB.steal_item_handler.objectives_by_path[item.targetitem])
+ var/turf/thing_turf = get_turf(existing_thing)
+ if(isnull(thing_turf)) // nullspaced likely means it was stolen and is in the black market.
+ continue
+ if(!is_station_level(thing_turf.z) && !is_mining_level(thing_turf.z))
+ continue
+ all_valid_existing_things += existing_thing
+
+ if(!length(all_valid_existing_things))
+ return FALSE
+ return TRUE
+
+/datum/spy_bounty/objective_item/init_bounty(datum/spy_bounty_handler/handler)
+ var/list/valid_possible_items = list()
+ for(var/datum/objective_item/item as anything in GLOB.possible_items)
+ if(check_dupe(handler, item.targetitem, 33))
+ continue
+ if(!is_valid_objective_item(item))
+ continue
+ // Determine difficulty. Has some overlap between the categories, which is OK
+ switch(difficulty)
+ if(SPY_DIFFICULTY_EASY)
+ if(item.difficulty >= 3)
+ continue
+ if(SPY_DIFFICULTY_MEDIUM)
+ if(item.difficulty <= 2 || item.difficulty >= 5)
+ continue
+ if(SPY_DIFFICULTY_HARD)
+ if(item.difficulty <= 3)
+ continue
+
+ valid_possible_items += item
+
+ for(var/datum/spy_bounty/objective_item/existing_bounty in handler.get_all_bounties())
+ valid_possible_items -= existing_bounty.desired_item
+
+ if(!length(valid_possible_items))
+ return FALSE
+
+ desired_item = pick(valid_possible_items)
+ // We need to do some snowflake for items that do exist vs generic items
+ var/list/obj/item/existing_items = GLOB.steal_item_handler.objectives_by_path[desired_item.targetitem]
+ var/obj/item/the_item = length(existing_items) ? pick(existing_items) : desired_item.targetitem
+ var/the_item_name = istype(the_item) ? the_item.name : initial(the_item.name)
+ name = "[the_item_name] [difficulty == SPY_DIFFICULTY_HARD ? "Grand ":""]Theft"
+ help = "Steal any [the_item_name][desired_item.steal_hint ? ": [desired_item.steal_hint]" : "."]"
+ return TRUE
+
+/datum/spy_bounty/objective_item/is_stealable(atom/movable/stealing)
+ return istype(stealing, desired_item.targetitem) && desired_item.check_special_completion(stealing)
+
+/datum/spy_bounty/objective_item/random_easy
+ difficulty = SPY_DIFFICULTY_EASY
+ weight = 4 // Increased due to there being many easy options
+
+/datum/spy_bounty/objective_item/random_medium
+ difficulty = SPY_DIFFICULTY_MEDIUM
+ weight = 2 // Increased due to there being many medium options
+
+/datum/spy_bounty/objective_item/random_hard
+ difficulty = SPY_DIFFICULTY_HARD
+
+/datum/spy_bounty/machine
+ theft_time = 10 SECONDS
+
+ /// What machine (typepath) we want to steal.
+ var/obj/machinery/target_type
+ /// What area (typepath) the desired machine is in.
+ /// Can be pre-set for subtypes. If set, requires the machine to be in the location_type.
+ /// If not set, picks a random machine from all areas it can currently be found in.
+ var/area/location_type
+ /// List of weakrefs to all machines of the target type when the bounty was initialized.
+ var/list/original_options_weakrefs = list()
+
+/datum/spy_bounty/machine/Destroy()
+ original_options_weakrefs.Cut() // Just in case
+ return ..()
+
+/datum/spy_bounty/machine/get_dupe_protection_key(atom/movable/stealing)
+ return target_type
+
+/datum/spy_bounty/machine/send_to_black_market(obj/machinery/thing)
+ if(!istype(thing.circuit, /obj/item/circuitboard))
+ qdel(thing)
+ return FALSE
+
+ var/obj/item/circuitboard/selling = thing.circuit
+ var/turf/machine_turf = get_turf(thing)
+
+ // Sell the circuitboard, take the rest apart
+ // This (should) handle any mobs inside as well
+ thing.deconstruct(FALSE)
+ if(!..(selling))
+ return FALSE
+
+ // Clean up leftover parts from deconstruction
+ for(var/obj/structure/frame/leftover in machine_turf)
+ qdel(leftover)
+ break
+ for(var/obj/item/stock_parts/part in machine_turf)
+ if(prob(part.rating * 20))
+ continue
+ qdel(part)
+
+ return TRUE
+
+/datum/spy_bounty/machine/init_bounty(datum/spy_bounty_handler/handler)
+ if(isnull(target_type))
+ return FALSE
+
+ // Blacklisting maintenance in general, as well as any areas that already have a bounty in them.
+ var/list/blacklisted_areas = typecacheof(/area/station/maintenance)
+ for(var/datum/spy_bounty/machine/existing_bounty in handler.get_all_bounties())
+ blacklisted_areas[existing_bounty.location_type] = TRUE
+
+ var/list/obj/machinery/all_possible = list()
+ for(var/obj/machinery/found_machine as anything in SSmachines.get_machines_by_type_and_subtypes(target_type))
+ if(!is_station_level(found_machine.z) && !is_mining_level(found_machine.z))
+ continue
+ var/area/found_machine_area = get_area(found_machine)
+ if(is_type_in_typecache(found_machine_area, blacklisted_areas))
+ continue
+ if(!isnull(location_type) && !istype(found_machine_area, location_type))
+ continue
+ if(!(found_machine_area.area_flags & VALID_TERRITORY)) // only steal from valid station areas
+ continue
+ all_possible += found_machine
+
+ if(!length(all_possible))
+ return FALSE
+
+ var/obj/machinery/machine = pick_n_take(all_possible)
+ var/area/machine_area = get_area(machine)
+ // Tracks the picked machine, as well as any other machines in the same area
+ // (So they can be removed from the room but still count, for clever Spies)
+ original_options_weakrefs += WEAKREF(machine)
+ for(var/obj/machinery/other_machine as anything in all_possible)
+ if(get_area(other_machine) == machine_area)
+ original_options_weakrefs += WEAKREF(other_machine)
+
+ location_type = machine_area.type
+ name ||= "[machine.name] Burglary"
+ help ||= "Steal \a [machine] found in [machine_area]."
+ return TRUE
+
+/datum/spy_bounty/machine/is_stealable(atom/movable/stealing)
+ if(!istype(stealing, target_type))
+ return FALSE
+ if(WEAKREF(stealing) in original_options_weakrefs)
+ return TRUE
+ if(istype(get_area(stealing), location_type))
+ return TRUE
+ return FALSE
+
+/datum/spy_bounty/machine/random
+ /// List of all machines we can randomly draw from
+ var/list/random_options = list()
+
+/datum/spy_bounty/machine/random/init_bounty(datum/spy_bounty_handler/handler)
+ var/list/options = random_options.Copy()
+ for(var/datum/spy_bounty/machine/existing_bounty in handler.get_all_bounties())
+ options -= existing_bounty.target_type
+
+ for(var/remaining_option in options)
+ if(check_dupe(handler, remaining_option, 33))
+ options -= remaining_option
+
+ if(!length(options))
+ return FALSE
+
+ target_type = pick(options)
+ return ..()
+
+/datum/spy_bounty/machine/random/easy
+ difficulty = SPY_DIFFICULTY_EASY
+ weight = 4 // Increased due to there being many easy options
+ random_options = list(
+ /obj/machinery/computer/operating,
+ /obj/machinery/computer/order_console/mining,
+ /obj/machinery/computer/records/medical,
+ /obj/machinery/cryo_cell,
+ /obj/machinery/fax, // Completely random, wild card
+ /obj/machinery/hydroponics/constructable,
+ /obj/machinery/medical_kiosk,
+ /obj/machinery/microwave,
+ /obj/machinery/oven,
+ /obj/machinery/recharge_station,
+ /obj/machinery/vending/boozeomat,
+ /obj/machinery/vending/medical,
+ /obj/machinery/vending/wardrobe,
+ )
+
+/datum/spy_bounty/machine/random/medium
+ difficulty = SPY_DIFFICULTY_MEDIUM
+ weight = 4 // Increased due to there being many medium options
+ random_options = list(
+ /obj/machinery/chem_dispenser,
+ /obj/machinery/computer/bank_machine,
+ /obj/machinery/computer/camera_advanced/xenobio,
+ /obj/machinery/computer/cargo, // This includes request-only ones in the public lobby
+ /obj/machinery/computer/crew,
+ /obj/machinery/computer/prisoner/management,
+ /obj/machinery/computer/rdconsole,
+ /obj/machinery/computer/records/security,
+ /obj/machinery/computer/scan_consolenew,
+ /obj/machinery/computer/security, // Requires breaking into a sec checkpoint, but not too hard, many are never visited
+ /obj/machinery/dna_scannernew,
+ /obj/machinery/mecha_part_fabricator,
+ )
+
+/datum/spy_bounty/machine/engineering_emitter
+ difficulty = SPY_DIFFICULTY_MEDIUM
+ target_type = /obj/machinery/power/emitter
+ location_type = /area/station/engineering/supermatter/
+
+/datum/spy_bounty/machine/engineering_emitter/can_claim(mob/user)
+ return !(user.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING)
+
+/datum/spy_bounty/machine/random/hard
+ difficulty = SPY_DIFFICULTY_HARD
+ random_options = list(
+ /obj/machinery/computer/accounting,
+ /obj/machinery/computer/communications,
+ /obj/machinery/computer/upload,
+ /obj/machinery/modular_computer/preset/id,
+ )
+
+/datum/spy_bounty/machine/random/hard/can_claim(mob/user) // These would all be too easy with command level access
+ return !(user.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
+
+/datum/spy_bounty/machine/random/hard/ai_sat_teleporter
+ random_options = list(
+ /obj/machinery/teleport,
+ /obj/machinery/computer/teleporter.
+ )
+ location_type = /area/station/ai_monitored/aisat
+
+/// Subtype for a bounty that targets a specific crew member
+/datum/spy_bounty/targets_person
+ difficulty = SPY_DIFFICULTY_HARD
+ theft_time = 12 SECONDS
+ /// Weakref to the mob target of the bounty
+ VAR_FINAL/datum/weakref/target_ref
+
+/datum/spy_bounty/targets_person/get_dupe_protection_key(atom/movable/stealing)
+ // Prevents the same player from being selected twice, but if they're straight up gone, whatever
+ return REF(target_ref.resolve() || stealing)
+
+/datum/spy_bounty/targets_person/can_claim(mob/user)
+ return !IS_WEAKREF_OF(user, target_ref)
+
+/datum/spy_bounty/targets_person/init_bounty(datum/spy_bounty_handler/handler)
+ var/list/mob/possible_targets = list()
+ for(var/datum/mind/crew_mind as anything in get_crewmember_minds())
+ var/mob/living/real_target = crew_mind.current
+ // Ideally we want it to be a player, but we don't care if they DC after being selected
+ if(!istype(real_target) || isnull(GET_CLIENT(real_target)))
+ continue
+ if(check_dupe(handler, REF(real_target), 50))
+ continue
+ if(!is_valid_crewmember(real_target))
+ continue
+ possible_targets += real_target
+
+ for(var/datum/spy_bounty/targets_person/existing_bounty in handler.get_all_bounties())
+ possible_targets -= existing_bounty.target_ref.resolve()
+
+ if(!length(possible_targets))
+ return FALSE
+
+ var/mob/picked = pick(possible_targets)
+ if(target_found(picked))
+ target_ref = WEAKREF(picked)
+ return TRUE
+
+ return FALSE
+
+/**
+ * Ran on every single member of the crew to determine if they are a valid target.
+ *
+ * * crewmember - The person to check.
+ *
+ * Returning FALSE will exclude them from the list of possible targets.
+ */
+/datum/spy_bounty/targets_person/proc/is_valid_crewmember(mob/crewmember)
+ return FALSE
+
+/**
+ * Ran when a valid target is selected for the bounty.
+ *
+ * * crewmember - The person that was selected as the target.
+ *
+ * Returning FALSE will stop the bounty from being finalized, this can be used for last minute checks.
+ */
+/datum/spy_bounty/targets_person/proc/target_found(mob/crewmember)
+ return FALSE
+
+/// Subtype for a bounty that targets a specific crew member and a specific item on them
+/datum/spy_bounty/targets_person/some_item
+ /// Typepath of the item we want from the target
+ var/obj/item/desired_type
+ /// Weakref to the item that matches our desired type within the target at the time of bounty creation
+ VAR_FINAL/datum/weakref/target_original_desired_ref
+
+/datum/spy_bounty/targets_person/some_item/is_valid_crewmember(mob/living/carbon/human/crewmember)
+ return istype(crewmember) && find_desired_thing(crewmember)
+
+/datum/spy_bounty/targets_person/some_item/is_stealable(atom/movable/stealing)
+ if(IS_WEAKREF_OF(stealing, target_original_desired_ref))
+ return TRUE
+ if(IS_WEAKREF_OF(stealing, target_ref))
+ var/mob/living/carbon/human/target = stealing
+ if(!target.incapacitated(IGNORE_RESTRAINTS|IGNORE_STASIS))
+ return FALSE
+ if(find_desired_thing(target))
+ return TRUE
+ return FALSE
+
+/datum/spy_bounty/targets_person/some_item/clean_up_stolen_item(atom/movable/stealing, mob/living/spy)
+ if(IS_WEAKREF_OF(stealing, target_original_desired_ref))
+ return ..()
+
+ ASSERT(ishuman(stealing), "[type] called clean_up_stolen_item with something that isn't a human and isn't the original item.")
+
+ do_sparks(2, FALSE, stealing)
+ var/mob/living/carbon/human/stolen_from = stealing
+ var/obj/item/real_stolen_item = find_desired_thing(stealing)
+ stolen_from.Unconscious(10 SECONDS)
+ to_chat(stolen_from, span_warning("You feel something missing where your [real_stolen_item.name] once was."))
+ return ..(real_stolen_item, spy)
+
+/datum/spy_bounty/targets_person/some_item/target_found(mob/crewmember)
+ var/obj/item/desired_thing = find_desired_thing(crewmember)
+ target_original_desired_ref = WEAKREF(desired_thing)
+ name = "[crewmember.real_name]'s [desired_thing.name]"
+ help = "Steal [desired_thing] from [crewmember.real_name]. \
+ You can accomplish this via brute force, or by scanning them with your uplink while they are incapacitated."
+ return TRUE
+
+/// Finds the desired item type in the target crewmember.
+/datum/spy_bounty/targets_person/some_item/proc/find_desired_thing(mob/living/carbon/human/crewmember)
+ return locate(desired_type) in crewmember.get_all_gear()
+
+// Steal someone's ID card
+/datum/spy_bounty/targets_person/some_item/id
+ desired_type = /obj/item/card/id/advanced
+
+/datum/spy_bounty/targets_person/some_item/id/find_desired_thing(mob/living/carbon/human/crewmember)
+ for(var/obj/item/card/id/advanced/id in crewmember.get_all_gear())
+ if(id.registered_account?.account_id == crewmember.account_id)
+ return id
+
+ return null
+
+/datum/spy_bounty/targets_person/some_item/id/target_found(mob/crewmember)
+ . = ..()
+ name = "[crewmember.real_name]'s ID Card"
+
+// Steal someone's PDA
+/datum/spy_bounty/targets_person/some_item/pda
+ desired_type = /obj/item/modular_computer/pda
+
+/datum/spy_bounty/targets_person/some_item/pda/find_desired_thing(mob/living/carbon/human/crewmember)
+ for(var/obj/item/modular_computer/pda/pda in crewmember.get_all_gear())
+ if(pda.saved_identification == crewmember.real_name)
+ return pda
+
+ return null
+
+/datum/spy_bounty/targets_person/some_item/pda/target_found(mob/crewmember)
+ . = ..()
+ name = "[crewmember.real_name]'s PDA"
+
+// Steal someone's heirloom
+/datum/spy_bounty/targets_person/some_item/heirloom
+ desired_type = /obj/item
+ black_market_prob = 100
+
+/datum/spy_bounty/targets_person/some_item/heirloom/find_desired_thing(mob/living/crewmember)
+ var/datum/quirk/item_quirk/family_heirloom/quirk = crewmember.get_quirk(/datum/quirk/item_quirk/family_heirloom)
+ return quirk?.heirloom?.resolve()
+
+/datum/spy_bounty/targets_person/some_item/heirloom/target_found(mob/crewmember)
+ . = ..()
+ name = "[crewmember.real_name]'s heirloom"
+
+// Steal a limb or organ off someone
+/datum/spy_bounty/targets_person/some_item/limb_or_organ
+ weight = 4 // lots to pick from here
+
+/datum/spy_bounty/targets_person/some_item/limb_or_organ/init_bounty(datum/spy_bounty_handler/handler)
+ desired_type = pick(
+ /obj/item/bodypart/arm/left,
+ /obj/item/bodypart/arm/right,
+ /obj/item/bodypart/leg/left,
+ /obj/item/bodypart/leg/right,
+ /obj/item/organ/internal/stomach,
+ /obj/item/organ/internal/appendix,
+ /obj/item/organ/internal/liver,
+ /obj/item/organ/internal/eyes,
+ )
+ return ..()
+
+/datum/spy_bounty/targets_person/some_item/limb_or_organ/find_desired_thing(mob/living/carbon/human/crewmember)
+ if(ispath(desired_type, /obj/item/bodypart))
+ return locate(desired_type) in crewmember.bodyparts
+ if(ispath(desired_type, /obj/item/organ))
+ return locate(desired_type) in crewmember.organs
+ return null
+
+/datum/spy_bounty/some_bot
+ theft_time = 10 SECONDS
+ black_market_prob = 0
+ /// What typepath of bot we want to steal.
+ var/mob/living/simple_animal/bot/bot_type
+ /// Weakref to the bot we want to steal.
+ VAR_FINAL/datum/weakref/target_bot_ref
+
+/datum/spy_bounty/some_bot/get_dupe_protection_key(atom/movable/stealing)
+ return bot_type
+
+/datum/spy_bounty/some_bot/init_bounty(datum/spy_bounty_handler/handler)
+ for(var/datum/spy_bounty/some_bot/existing_bounty in handler.get_all_bounties())
+ var/mob/living/simple_animal/bot/existing_bot_type = existing_bounty.bot_type
+ // ensures we don't get two similar bounties.
+ // may occasionally cast a wider net than we'd desire, but it's not that bad.
+ if(ispath(bot_type, initial(existing_bot_type.parent_type)))
+ return FALSE
+
+ var/list/mob/living/possible_bots = list()
+ for(var/mob/living/bot as anything in GLOB.bots_list)
+ if(!is_station_level(bot.z) && !is_mining_level(bot.z))
+ continue
+ if(!istype(bot, bot_type))
+ continue
+ possible_bots += bot
+
+ if(!length(possible_bots))
+ return FALSE
+
+ var/mob/living/picked = pick(possible_bots)
+ target_bot_ref = WEAKREF(picked)
+ name ||= "[picked.name] Abduction"
+ help ||= "Abduct the station's robot assistant [picked.name]."
+ return TRUE
+
+/datum/spy_bounty/some_bot/is_stealable(atom/movable/stealing)
+ return IS_WEAKREF_OF(stealing, target_bot_ref)
+
+/datum/spy_bounty/some_bot/beepsky
+ difficulty = SPY_DIFFICULTY_MEDIUM // gotta get him to stand still
+ bot_type = /mob/living/simple_animal/bot/secbot/beepsky/officer
+ help = "Abduct Officer Beepsky - commonly found patrolling the station. \
+ Watch out, they may not take kindly to being scanned."
+
+/datum/spy_bounty/some_bot/ofitser
+ difficulty = SPY_DIFFICULTY_EASY
+ bot_type = /mob/living/simple_animal/bot/secbot/beepsky/ofitser
+ help = "Abduct Prison Ofitser - commonly found guarding the Gulag."
+
+/datum/spy_bounty/some_bot/armsky
+ difficulty = SPY_DIFFICULTY_HARD
+ bot_type = /mob/living/simple_animal/bot/secbot/beepsky/armsky
+ help = "Abduct Sergeant-At-Armsky - commonly found guarding the station's Armory."
+
+/datum/spy_bounty/some_bot/pingsky
+ difficulty = SPY_DIFFICULTY_HARD
+ bot_type = /mob/living/simple_animal/bot/secbot/pingsky
+ help = "Abduct Officer Pingsky - commonly found protecting the station's AI."
+
+/datum/spy_bounty/some_bot/scrubbs
+ difficulty = SPY_DIFFICULTY_EASY
+ bot_type = /mob/living/basic/bot/cleanbot/medbay
+ help = "Abduct Scrubbs, MD - commonly found mopping up blood in Medbay."
+
+/datum/spy_bounty/some_bot/scrubbs/can_claim(mob/user)
+ return !(user.mind?.assigned_role.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL)
diff --git a/code/modules/antagonists/spy/spy_bounty_handler.dm b/code/modules/antagonists/spy/spy_bounty_handler.dm
new file mode 100644
index 0000000000000..798719cb8a02c
--- /dev/null
+++ b/code/modules/antagonists/spy/spy_bounty_handler.dm
@@ -0,0 +1,123 @@
+/**
+ * ## Spy bounty handler
+ *
+ * Singleton datum that handles determining active bounties for spies.
+ */
+/datum/spy_bounty_handler
+ /// Timer between when all bounties are refreshed.
+ var/refresh_time = 12 MINUTES
+ /// timerID of the active refresh timer.
+ var/refresh_timer
+ /// Number of times we have refreshed bounties
+ var/num_refreshes = 0
+ /// Assoc list of items stolen in the past to how many times they have been stolen
+ /// Sometimes item typepaths, sometimes REFs, in general just strings that represent stolen items
+ var/list/all_claimed_bounty_types = list()
+ /// List of all items stolen in the last pool of bounties.
+ /// Same as above - strings that represent stolen items.
+ var/list/claimed_bounties_from_last_pool = list()
+ /// Override for the number of attempts to make a bounty.
+ var/num_attempts_override = 0
+
+ /// Assoc list that dictates how much of each bounty difficulty to give out at once.
+ /// Modified by the number of times we have refreshed bounties.
+ VAR_PRIVATE/list/base_bounties_to_give = list(
+ SPY_DIFFICULTY_EASY = 4,
+ SPY_DIFFICULTY_MEDIUM = 2,
+ SPY_DIFFICULTY_HARD = 2,
+ )
+
+ /// Assoc list of all active bounties.
+ VAR_PRIVATE/list/list/bounties = list(
+ SPY_DIFFICULTY_EASY = list(),
+ SPY_DIFFICULTY_MEDIUM = list(),
+ SPY_DIFFICULTY_HARD = list(),
+ )
+
+ /// Assoc list of all possible bounties for each difficulty, weighted.
+ /// This is static, no bounty types are removed from this list.
+ VAR_PRIVATE/list/list/bounty_types = list(
+ SPY_DIFFICULTY_EASY = list(),
+ SPY_DIFFICULTY_MEDIUM = list(),
+ SPY_DIFFICULTY_HARD = list(),
+ )
+
+ /// Assoc list of all uplink items possible to be given as bounties for each difficulty.
+ /// This is not static, as bounties are complete uplink items will be removed from this list.
+ var/list/list/possible_uplink_items = list(
+ SPY_DIFFICULTY_EASY = list(),
+ SPY_DIFFICULTY_MEDIUM = list(),
+ SPY_DIFFICULTY_HARD = list(),
+ )
+
+/datum/spy_bounty_handler/New()
+ for(var/datum/spy_bounty/bounty as anything in subtypesof(/datum/spy_bounty))
+ var/weight = initial(bounty.weight)
+ var/difficulty = initial(bounty.difficulty)
+ if(weight <= 0 || !islist(bounty_types[difficulty]))
+ continue
+ bounty_types[difficulty][bounty] = weight
+
+ for(var/datum/uplink_item/item as anything in SStraitor.uplink_items)
+ if(isnull(item.item) || item.item == ABSTRACT_UPLINK_ITEM)
+ continue
+ if(!(item.purchasable_from & UPLINK_SPY))
+ continue
+ // This will have some overlap, and that's intentional -
+ // Adds some variety, rare moments where you can get a hard reward for an easier bounty (or visa versa)
+ if(item.cost <= SPY_LOWER_COST_THRESHOLD)
+ possible_uplink_items[SPY_DIFFICULTY_EASY] += item
+ if(item.cost >= SPY_LOWER_COST_THRESHOLD && item.cost <= SPY_UPPER_COST_THRESHOLD)
+ possible_uplink_items[SPY_DIFFICULTY_MEDIUM] += item
+ if(item.cost >= SPY_UPPER_COST_THRESHOLD)
+ possible_uplink_items[SPY_DIFFICULTY_HARD] += item
+
+ refresh_bounty_list()
+
+/// Helper that returns a list of all active bounties in a single list, regardless of difficulty.
+/datum/spy_bounty_handler/proc/get_all_bounties() as /list
+ var/list/all_bounties = list()
+ for(var/difficulty in bounties)
+ all_bounties += bounties[difficulty]
+
+ return all_bounties
+
+/// Refreshes all active bounties for each difficulty, no matter if they were complete or not.
+/// Then recursively calls itself via a timer.
+/datum/spy_bounty_handler/proc/refresh_bounty_list()
+ PRIVATE_PROC(TRUE)
+
+ var/list/bounties_to_give = base_bounties_to_give.Copy()
+
+ if(num_refreshes < base_bounties_to_give[SPY_DIFFICULTY_HARD])
+ bounties_to_give[SPY_DIFFICULTY_HARD] = num_refreshes
+ bounties_to_give[SPY_DIFFICULTY_MEDIUM] += (base_bounties_to_give[SPY_DIFFICULTY_HARD] - num_refreshes)
+
+ for(var/difficulty in bounties)
+ QDEL_LIST(bounties[difficulty])
+
+ var/list/pool = bounty_types[difficulty]
+ var/amount_to_give = bounties_to_give[difficulty]
+ var/failed_attempts = num_attempts_override || clamp(amount_to_give * 4, 10, 25) // more potential bounties = more attempts to make one
+ while(amount_to_give > 0 && failed_attempts > 0)
+ var/picked_bounty = pick_weight(pool)
+ var/datum/spy_bounty/bounty = new picked_bounty(src)
+ if(bounty.initalized)
+ amount_to_give -= 1
+ bounties[difficulty] += bounty
+
+ else
+ failed_attempts -= 1
+ qdel(bounty)
+
+ claimed_bounties_from_last_pool.Cut()
+ num_refreshes += 1
+ refresh_timer = addtimer(CALLBACK(src, PROC_REF(refresh_bounty_list)), refresh_time, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
+
+/// Forces a refresh of the bounty list.
+/// Counts towards [num_refreshes].
+/datum/spy_bounty_handler/proc/force_refresh()
+ if(refresh_timer)
+ deltimer(refresh_timer)
+
+ refresh_bounty_list()
diff --git a/code/modules/antagonists/spy/spy_uplink.dm b/code/modules/antagonists/spy/spy_uplink.dm
new file mode 100644
index 0000000000000..ea6f39fc92d4b
--- /dev/null
+++ b/code/modules/antagonists/spy/spy_uplink.dm
@@ -0,0 +1,206 @@
+/**
+ * ## Spy uplink
+ *
+ * Applied to items similar to traitor uplinks.
+ *
+ * Used for spies to complete bounties.
+ */
+/datum/component/spy_uplink
+ /// Weakref to the spy antag datum which owns this uplink
+ var/datum/weakref/spy_ref
+ /// The handler which manages all bounties across all spies.
+ var/static/datum/spy_bounty_handler/handler
+
+/datum/component/spy_uplink/Initialize(datum/antagonist/spy/spy)
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ spy_ref = WEAKREF(spy)
+
+ if(isnull(handler))
+ handler = new()
+
+/datum/component/spy_uplink/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_attack_self))
+ RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK_SECONDARY, PROC_REF(on_pre_attack_secondary))
+ RegisterSignal(parent, COMSIG_TABLET_CHECK_DETONATE, PROC_REF(block_pda_bombs))
+
+/datum/component/spy_uplink/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_EXAMINE,
+ COMSIG_ITEM_ATTACK_SELF,
+ COMSIG_ITEM_PRE_ATTACK_SECONDARY,
+ COMSIG_TABLET_CHECK_DETONATE,
+ ))
+
+/// Checks that the passed mob is the owner of this uplink.
+/datum/component/spy_uplink/proc/is_our_spy(mob/whoever)
+ var/datum/antagonist/spy/spy_datum = spy_ref?.resolve()
+ return spy_datum?.owner.current == whoever
+
+/datum/component/spy_uplink/proc/on_examine(obj/item/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+
+ if(!is_our_spy(user))
+ return
+ examine_list += span_notice("You recognize this as your spy uplink .")
+ examine_list += span_notice("- [EXAMINE_HINT("Use it in hand")] to view your bounty list.")
+ examine_list += span_notice("- [EXAMINE_HINT("Right click")] with it on a bounty target to claim it.")
+
+/datum/component/spy_uplink/proc/block_pda_bombs(obj/item/source)
+ SIGNAL_HANDLER
+
+ return COMPONENT_TABLET_NO_DETONATE
+
+/datum/component/spy_uplink/proc/on_attack_self(obj/item/source, mob/user)
+ SIGNAL_HANDLER
+
+ if(is_our_spy(user))
+ INVOKE_ASYNC(src, TYPE_PROC_REF(/datum, ui_interact), user)
+ return NONE
+
+/datum/component/spy_uplink/proc/on_pre_attack_secondary(obj/item/source, atom/target, mob/living/user, params)
+ SIGNAL_HANDLER
+
+ if(!ismovable(target))
+ return NONE
+ if(!is_our_spy(user))
+ return NONE
+ if(!try_steal(target, user))
+ return NONE
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Checks if the passed atom is something that can be stolen according to one of the active bounties.
+/// If so, starts the stealing process.
+/datum/component/spy_uplink/proc/try_steal(atom/movable/stealing, mob/living/spy)
+ for(var/datum/spy_bounty/bounty as anything in handler.get_all_bounties())
+ if(!bounty.can_claim(spy))
+ continue
+ if(!bounty.is_stealable(stealing))
+ continue
+ if(bounty.claimed)
+ stealing.balloon_alert(spy, "bounty already claimed!")
+ return TRUE
+ if(DOING_INTERACTION(spy, REF(src)))
+ spy.balloon_alert(spy, "already scanning!") // Only shown if they're trying to scan two valid targets
+ return TRUE
+ SEND_SIGNAL(stealing, COMSIG_MOVABLE_SPY_STEALING, spy, bounty)
+ INVOKE_ASYNC(src, PROC_REF(start_stealing), stealing, spy, bounty)
+ return TRUE
+
+ return FALSE
+
+/// Wraps the stealing process in a scanning effect.
+/datum/component/spy_uplink/proc/start_stealing(atom/movable/stealing, mob/living/spy, datum/spy_bounty/bounty)
+ if(!isturf(stealing.loc) && stealing.loc != spy)
+ to_chat(spy, span_warning("Your uplinks blinks red: [stealing] cannot be extracted from there."))
+ return FALSE
+
+ playsound(stealing, 'sound/items/pshoom.ogg', 33, vary = TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, frequency = 0.33, ignore_walls = FALSE)
+
+ var/obj/effect/scan_effect/active_scan_effect = new(stealing.loc)
+ active_scan_effect.appearance = stealing.appearance
+ active_scan_effect.dir = stealing.dir
+ active_scan_effect.makeHologram()
+ SET_PLANE_EXPLICIT(active_scan_effect, stealing.plane, stealing)
+ active_scan_effect.layer = stealing.layer + 0.1
+
+ var/obj/effect/scan_effect/cone/active_scan_cone
+ if(isturf(stealing.loc) && isturf(spy.loc)) // Cone doesn't make sense if its being held or something
+ active_scan_cone = new(spy.loc)
+ var/angle = round(get_angle(spy, stealing), 10)
+ if(angle > 180 && angle < 360)
+ active_scan_cone.pixel_x -= 16
+ else if(angle < 180 && angle > 0)
+ active_scan_cone.pixel_x += 16
+ if(angle > 90 && angle < 270)
+ active_scan_cone.pixel_y -= 16
+ else if(angle < 90 || angle > 270)
+ active_scan_cone.pixel_y += 16
+ active_scan_cone.transform = active_scan_cone.transform.Turn(angle)
+ active_scan_cone.alpha = 0
+ animate(active_scan_cone, time = 0.5 SECONDS, alpha = initial(active_scan_cone.alpha))
+
+ . = steal_process(stealing, spy, bounty)
+ qdel(active_scan_effect)
+ qdel(active_scan_cone)
+ return .
+
+/// Attempts to steal the passed atom in accordance with the passed bounty.
+/// If successful, proceeds to complete the bounty.
+/datum/component/spy_uplink/proc/steal_process(atom/movable/stealing, mob/living/spy, datum/spy_bounty/bounty)
+ spy.visible_message(
+ span_warning("[spy] starts scanning [stealing] with a strange device..."),
+ span_notice("You start scanning [stealing], preparing it for extraction."),
+ )
+
+ if(!do_after(spy, bounty.theft_time, stealing, interaction_key = REF(src)))
+ return FALSE
+ if(bounty.claimed)
+ to_chat(spy, span_warning("Your uplinks blinks red: The bounty for [stealing] has been claimed by another spy!"))
+ return FALSE
+ if(spy.is_holding(stealing) && !spy.dropItemToGround(stealing))
+ to_chat(spy, span_warning("Your uplinks blinks red: [stealing] seems stuck to your hand!"))
+ return FALSE
+
+ var/bounty_key = bounty.get_dupe_protection_key(stealing)
+ handler.all_claimed_bounty_types[bounty_key] += 1
+ handler.claimed_bounties_from_last_pool[bounty_key] = TRUE
+
+ bounty.clean_up_stolen_item(stealing, spy, handler)
+ bounty.claimed = TRUE
+
+ var/atom/movable/reward = bounty.reward_item.spawn_item_for_generic_use(spy)
+ if(isitem(reward))
+ spy.put_in_hands(reward)
+
+ to_chat(spy, span_notice("Bounty complete! You have been rewarded with \a [reward].\
+ [reward.loc == spy ? "" : " Find it at your feet. "]"))
+
+ playsound(parent, 'sound/machines/wewewew.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+
+ log_spy("[key_name(spy)] completed the bounty [bounty.name] of difficulty [bounty.difficulty] for \a [reward].")
+ SSblackbox.record_feedback("nested tally", "spy_bounty", 1, list("[stealing.type]", "[bounty.type]", "[bounty.difficulty]", "[bounty.reward_item.type]"))
+
+ var/datum/antagonist/spy/spy_datum = spy_ref?.resolve()
+ if(!isnull(spy_datum))
+ // "When" TGUI roundend is finished, a list of all bounties complete and their rewards should be put in a collapsible,
+ // otherwise it's just too much information to display cleanly. (That's why we're only displaying number and rewards)
+ spy_datum.bounties_claimed += 1
+ spy_datum.all_loot += bounty.reward_item.name
+
+ return TRUE
+
+/datum/component/spy_uplink/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "SpyUplink")
+ ui.open()
+
+/datum/component/spy_uplink/ui_data(mob/user)
+ var/list/data = list()
+
+ data["bounties"] = list()
+ for(var/datum/spy_bounty/bounty as anything in handler.get_all_bounties())
+ UNTYPED_LIST_ADD(data["bounties"], bounty.to_ui_data(user))
+ data["time_left"] = timeleft(handler.refresh_timer)
+
+ return data
+
+/datum/component/spy_uplink/ui_status(mob/user, datum/ui_state/state)
+ if(isobserver(user) && user.client?.holder)
+ return UI_UPDATE
+ return ..()
+
+/obj/effect/scan_effect
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ anchored = TRUE
+ layer = ABOVE_ALL_MOB_LAYER
+
+/obj/effect/scan_effect/cone
+ name = "holoray"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "scan_beam"
+ color = "#3ba0ff"
+ alpha = 200
diff --git a/code/modules/assembly/mousetrap.dm b/code/modules/assembly/mousetrap.dm
index 1f760e29b8959..1d8936e6068da 100644
--- a/code/modules/assembly/mousetrap.dm
+++ b/code/modules/assembly/mousetrap.dm
@@ -89,7 +89,7 @@
to_chat(user, span_warning("Your hand slips, setting off the trigger!"))
pulse()
update_appearance()
- playsound(src, 'sound/weapons/handcuffs.ogg', 30, TRUE, -3)
+ playsound(loc, 'sound/weapons/handcuffs.ogg', 30, TRUE, -3)
/obj/item/assembly/mousetrap/update_icon_state()
icon_state = "mousetrap[armed ? "armed" : ""]"
diff --git a/code/modules/cargo/markets/_market.dm b/code/modules/cargo/markets/_market.dm
index 3c264289cd2bf..a4af2bc981d94 100644
--- a/code/modules/cargo/markets/_market.dm
+++ b/code/modules/cargo/markets/_market.dm
@@ -13,10 +13,7 @@
/// Adds item to the available items and add it's category if it is not in categories yet.
/datum/market/proc/add_item(datum/market_item/item)
- if(!prob(initial(item.availability_prob)))
- return FALSE
-
- if(ispath(item))
+ if(ispath(item, /datum/market_item))
item = new item()
if(!(item.category in categories))
diff --git a/code/modules/cargo/markets/market_item.dm b/code/modules/cargo/markets/market_item.dm
index 867facf015b98..d5689c17a45e6 100644
--- a/code/modules/cargo/markets/market_item.dm
+++ b/code/modules/cargo/markets/market_item.dm
@@ -14,7 +14,7 @@
var/stock
/// Path to or the item itself what this entry is for, this should be set even if you override spawn_item to spawn your item.
- var/item
+ var/obj/item/item
/// Minimum price for the item if generated randomly.
var/price_min = 0
@@ -33,9 +33,18 @@
if(isnull(stock))
stock = rand(stock_min, stock_max)
+/datum/market_item/Destroy()
+ item = null
+ return ..()
+
/// Used for spawning the wanted item, override if you need to do something special with the item.
/datum/market_item/proc/spawn_item(loc)
- return new item(loc)
+ if(ismovable(item))
+ item.forceMove(loc)
+ return item
+ if(ispath(item))
+ return new item(loc)
+ CRASH("Invalid item type for market item [item || "null"]")
/// Buys the item and makes SSblackmarket handle it.
/datum/market_item/proc/buy(obj/item/market_uplink/uplink, mob/buyer, shipping_method)
diff --git a/code/modules/cargo/markets/market_items/clothing.dm b/code/modules/cargo/markets/market_items/clothing.dm
index 003cd95780c29..8af34e2291657 100644
--- a/code/modules/cargo/markets/market_items/clothing.dm
+++ b/code/modules/cargo/markets/market_items/clothing.dm
@@ -79,3 +79,19 @@
price_max = CARGO_CRATE_VALUE * 1.5
stock_max = 5
availability_prob = 70
+
+/datum/market_item/clothing/floortileset
+ name = "Floor-tile Camouflage Uniform"
+ desc = "Hey there, looking to surprise somebody? Spy? Steal? Then you're lucky, meet our newest \
+ floor-tile 'NT SCUM' styled camouflage fatigues. This is the ultimate \
+ espionage uniform used by the very best. Providing the best \
+ flexibility, with our latest Camo-tech threads. Perfect for \
+ risky espionage hallway operations. Enjoy our product!"
+ item = /obj/item/storage/box/floor_camo
+ price_min = CARGO_CRATE_VALUE * 0.5
+ price_max = CARGO_CRATE_VALUE
+ stock_max = 3
+ availability_prob = 40
+
+
+
diff --git a/code/modules/cargo/markets/market_telepad.dm b/code/modules/cargo/markets/market_telepad.dm
index abdad441ce500..e99e4b88d223e 100644
--- a/code/modules/cargo/markets/market_telepad.dm
+++ b/code/modules/cargo/markets/market_telepad.dm
@@ -82,11 +82,7 @@
if(receiving)
var/datum/market_purchase/P = receiving
- if(!P.item || ispath(P.item))
- P.item = P.entry.spawn_item(T)
- else
- var/atom/movable/M = P.item
- M.forceMove(T)
+ P.item = P.entry.spawn_item(T)
use_power(power_usage_per_teleport / power_efficiency)
var/datum/effect_system/spark_spread/sparks = new
diff --git a/code/modules/cargo/packs/imports.dm b/code/modules/cargo/packs/imports.dm
index fae1e405d3fb2..781205eb03bf8 100644
--- a/code/modules/cargo/packs/imports.dm
+++ b/code/modules/cargo/packs/imports.dm
@@ -315,17 +315,18 @@
/datum/supply_pack/imports/floortilecamo
name = "Floor-tile Camouflage Uniform"
- desc = "Thank you for shopping from Camo-J's, our uniquely designed \
- floor-tile 'NT SCUM' styled camouflage fatigues is the ultimate \
+ desc = "Hey there, looking to surprise somebody? Spy? Steal? Then you're lucky, meet our newest \
+ floor-tile 'NT SCUM' styled camouflage fatigues. This is the ultimate \
espionage uniform used by the very best. Providing the best \
flexibility, with our latest Camo-tech threads. Perfect for \
risky espionage hallway operations. Enjoy our product!"
- hidden = TRUE
+ contraband = TRUE
cost = CARGO_CRATE_VALUE * 6
- contains = list(/obj/item/clothing/under/syndicate/floortilecamo = 4,
- /obj/item/clothing/mask/floortilebalaclava = 4,
- /obj/item/clothing/gloves/combat/floortile = 4,
- /obj/item/clothing/shoes/jackboots/floortile = 4
+ contains = list(/obj/item/clothing/under/syndicate/floortilecamo = 3,
+ /obj/item/clothing/mask/floortilebalaclava = 3,
+ /obj/item/clothing/gloves/combat/floortile = 3,
+ /obj/item/clothing/shoes/jackboots/floortile = 3,
+ /obj/item/storage/backpack/floortile = 3
)
crate_name = "floortile camouflauge crate"
crate_type = /obj/structure/closet/crate/secure/weapon
diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm
index e2108848b0ba6..14c1d88390ccb 100644
--- a/code/modules/fishing/fishing_minigame.dm
+++ b/code/modules/fishing/fishing_minigame.dm
@@ -191,7 +191,7 @@
if(difficulty > FISHING_EASY_DIFFICULTY)
completion -= round(MAX_FISH_COMPLETION_MALUS * (difficulty/100), 1)
- if(HAS_TRAIT(user, TRAIT_REVEAL_FISH) || (user.mind && HAS_TRAIT(user.mind, TRAIT_REVEAL_FISH)))
+ if(HAS_MIND_TRAIT(user, TRAIT_REVEAL_FISH))
fish_icon = GLOB.specific_fish_icons[reward_path] || "fish"
/**
@@ -332,7 +332,7 @@
phase = BITING_PHASE
// Trashing animation
playsound(lure, 'sound/effects/fish_splash.ogg', 100)
- if(HAS_TRAIT(user, TRAIT_REVEAL_FISH) || (user.mind && HAS_TRAIT(user.mind, TRAIT_REVEAL_FISH)))
+ if(HAS_MIND_TRAIT(user, TRAIT_REVEAL_FISH))
switch(fish_icon)
if(FISH_ICON_DEF)
send_alert("fish!!!")
diff --git a/code/modules/forensics/_forensics.dm b/code/modules/forensics/_forensics.dm
index 40b480182537e..5c43b9da0995c 100644
--- a/code/modules/forensics/_forensics.dm
+++ b/code/modules/forensics/_forensics.dm
@@ -8,8 +8,8 @@
* * List of clothing fibers on the atom
*/
/datum/forensics
- /// Weakref to the parent owning this datum
- var/datum/weakref/parent
+ /// Ref to the parent owning this datum
+ var/atom/parent
/**
* List of fingerprints on this atom
*
@@ -39,7 +39,7 @@
*/
var/list/fibers
-/datum/forensics/New(atom/parent, fingerprints, hiddenprints, blood_DNA, fibers)
+/datum/forensics/New(atom/parent, list/fingerprints, list/hiddenprints, list/blood_DNA, list/fibers)
if(!isatom(parent))
stack_trace("We tried adding a forensics datum to something that isnt an atom. What the hell are you doing?")
qdel(src)
@@ -47,7 +47,7 @@
RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(clean_act))
- src.parent = WEAKREF(parent)
+ src.parent = parent
src.fingerprints = fingerprints
src.hiddenprints = hiddenprints
src.blood_DNA = blood_DNA
@@ -67,9 +67,7 @@
check_blood()
/datum/forensics/Destroy(force)
- var/atom/parent_atom = parent.resolve()
- if (!isnull(parent_atom))
- UnregisterSignal(parent_atom, list(COMSIG_COMPONENT_CLEAN_ACT))
+ UnregisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT)
parent = null
return ..()
@@ -148,10 +146,7 @@
/// Adds a single fiber
/datum/forensics/proc/add_fibers(mob/living/carbon/human/suspect)
var/fibertext
- var/atom/actual_parent = parent?.resolve()
- if(isnull(actual_parent))
- parent = null
- var/item_multiplier = isitem(actual_parent) ? ITEM_FIBER_MULTIPLIER : NON_ITEM_FIBER_MULTIPLIER
+ var/item_multiplier = isitem(parent) ? ITEM_FIBER_MULTIPLIER : NON_ITEM_FIBER_MULTIPLIER
if(suspect.wear_suit)
fibertext = "Material from \a [suspect.wear_suit]."
if(prob(10 * item_multiplier) && !LAZYACCESS(fibers, fibertext))
@@ -217,11 +212,7 @@
if(last_stamp_pos)
LAZYSET(hiddenprints, suspect.key, copytext(hiddenprints[suspect.key], 1, last_stamp_pos))
hiddenprints[suspect.key] += "\nLast: \[[current_time]\] \"[suspect.real_name]\"[has_gloves]. Ckey: [suspect.ckey]" //made sure to be existing by if(!LAZYACCESS);else
- var/atom/parent_atom = parent?.resolve()
- if(!isnull(parent_atom))
- parent_atom.fingerprintslast = suspect.ckey
- else
- parent = null
+ parent.fingerprintslast = suspect.ckey
return TRUE
/// Adds the given list into blood_DNA
@@ -236,12 +227,8 @@
/// Updates the blood displayed on parent
/datum/forensics/proc/check_blood()
- var/obj/item/the_thing = parent?.resolve()
- if(isnull(the_thing))
- parent = null
- return
- if(!istype(the_thing) || isorgan(the_thing)) // organs don't spawn with blood decals by default
+ if(!isitem(parent) || isorgan(parent)) // organs don't spawn with blood decals by default
return
if(!length(blood_DNA))
return
- the_thing.AddElement(/datum/element/decal/blood)
+ parent.AddElement(/datum/element/decal/blood)
diff --git a/code/modules/hallucination/fake_sound.dm b/code/modules/hallucination/fake_sound.dm
index ec578f101d376..aaf8ef468230c 100644
--- a/code/modules/hallucination/fake_sound.dm
+++ b/code/modules/hallucination/fake_sound.dm
@@ -173,6 +173,7 @@
'sound/ambience/antag/ling_alert.ogg',
'sound/ambience/antag/malf.ogg',
'sound/ambience/antag/ops.ogg',
+ 'sound/ambience/antag/spy.ogg',
'sound/ambience/antag/tatoralert.ogg',
)
diff --git a/code/modules/library/skill_learning/skillchip.dm b/code/modules/library/skill_learning/skillchip.dm
index 2ef7a20e6806c..a6afa3398bc11 100644
--- a/code/modules/library/skill_learning/skillchip.dm
+++ b/code/modules/library/skill_learning/skillchip.dm
@@ -491,9 +491,9 @@
/obj/item/skillchip/master_angler
name = "Mast-Angl-Er skillchip"
- auto_traits = list(TRAIT_REVEAL_FISH)
+ auto_traits = list(TRAIT_REVEAL_FISH, TRAIT_EXAMINE_FISHING_SPOT)
skill_name = "Fisherman's Discernment"
- skill_description = "While fishing, it'll make a smidge easier to guess whatever you're trying to catch."
+ skill_description = "Lists fishes when examining a fishing spot, and gives a hint of whatever thing's biting the hook."
skill_icon = "fish"
activate_message = span_notice("You feel the knowledge and passion of several sunbaked, seasoned fishermen burn within you.")
deactivate_message = span_notice("You no longer feel like casting a fishing rod by the sunny riverside.")
diff --git a/code/modules/lighting/lighting_area.dm b/code/modules/lighting/lighting_area.dm
index 84170b6964fce..ea31f61c8becd 100644
--- a/code/modules/lighting/lighting_area.dm
+++ b/code/modules/lighting/lighting_area.dm
@@ -51,7 +51,7 @@
UnregisterSignal(SSdcs, COMSIG_STARLIGHT_COLOR_CHANGED)
var/list/z_offsets = SSmapping.z_level_to_plane_offset
if(length(lighting_effects) > 1)
- for(var/area_zlevel as anything in 1 to get_highest_zlevel())
+ for(var/area_zlevel in 1 to get_highest_zlevel())
if(z_offsets[area_zlevel])
for(var/turf/T as anything in get_turfs_by_zlevel(area_zlevel))
T.cut_overlay(lighting_effects[z_offsets[T.z] + 1])
diff --git a/code/modules/logging/categories/log_category_uplink.dm b/code/modules/logging/categories/log_category_uplink.dm
index f88d224ad3b34..4ef0f1af0c01a 100644
--- a/code/modules/logging/categories/log_category_uplink.dm
+++ b/code/modules/logging/categories/log_category_uplink.dm
@@ -21,3 +21,8 @@
category = LOG_CATEGORY_UPLINK_SPELL
config_flag = /datum/config_entry/flag/log_uplink
master_category = /datum/log_category/uplink
+
+/datum/log_category/uplink_spy
+ category = LOG_CATEGORY_UPLINK_SPY
+ config_flag = /datum/config_entry/flag/log_uplink
+ master_category = /datum/log_category/uplink
diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
index a4bcad876712f..97a543fa7e727 100644
--- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
+++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
@@ -203,7 +203,7 @@
new /obj/item/reagent_containers/cup/beaker(src)
new /obj/item/clothing/glasses/science(src)
if(7)
- new /obj/item/clothing/glasses/sunglasses(src)
+ new /obj/item/clothing/glasses/sunglasses/big(src)
new /obj/item/clothing/mask/cigarette/rollie(src)
else
//empty grave
diff --git a/code/modules/mob/living/basic/drone/_drone.dm b/code/modules/mob/living/basic/drone/_drone.dm
index 9298083c67c21..ae72054899b11 100644
--- a/code/modules/mob/living/basic/drone/_drone.dm
+++ b/code/modules/mob/living/basic/drone/_drone.dm
@@ -47,7 +47,6 @@
lighting_cutoff_red = 30
lighting_cutoff_green = 35
lighting_cutoff_blue = 25
-
can_be_held = TRUE
worn_slot_flags = ITEM_SLOT_HEAD
/// `TRUE` if we have picked our visual appearance, `FALSE` otherwise (default)
@@ -265,6 +264,9 @@
/mob/living/basic/drone/gib()
dust()
+/mob/living/basic/drone/get_butt_sprite()
+ return BUTT_SPRITE_DRONE
+
/mob/living/basic/drone/examine(mob/user)
. = list("This is [icon2html(src, user)] \a [src] !")
diff --git a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
index 34b92f218829c..f2354cc5f149a 100644
--- a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
+++ b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm
@@ -100,7 +100,7 @@
/// Handles automagically eating a plant when we move into a turf that has one.
/mob/living/basic/goat/proc/on_move(datum/source, atom/entering_loc)
SIGNAL_HANDLER
- if(!isturf(entering_loc))
+ if(!isturf(entering_loc) || stat == DEAD)
return
var/list/edible_plants = list()
diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm
index b143f471138f4..af17fc0cb01aa 100644
--- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm
@@ -34,8 +34,8 @@
/datum/action/cooldown/mob_cooldown/slippery_ice_floors
name = "Iced Floors"
desc = "Summon slippery ice floors all around!"
- button_icon = 'icons/turf/floors/ice_turf.dmi'
- button_icon_state = "ice_turf-6"
+ button_icon = 'icons/effects/freeze.dmi'
+ button_icon_state = "ice_cube"
cooldown_time = 2 SECONDS
click_to_activate = FALSE
melee_cooldown_time = 0 SECONDS
@@ -84,6 +84,7 @@
/datum/action/cooldown/spell/conjure/limit_summons/create_afterimages
name = "Create After Images"
+ desc = "Creates two illusionary doubles to increase your firepower, but which share some of your life force."
button_icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi'
button_icon_state = "ice_demon"
spell_requirements = NONE
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie.dm b/code/modules/mob/living/basic/pets/orbie/orbie.dm
new file mode 100644
index 0000000000000..2c9fb3d815c49
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/orbie/orbie.dm
@@ -0,0 +1,112 @@
+#define ORBIE_MAXIMUM_HEALTH 300
+
+/mob/living/basic/orbie
+ name = "Orbie"
+ desc = "An orb shaped hologram."
+ icon = 'icons/mob/simple/pets.dmi'
+ icon_state = "orbie"
+ icon_living = "orbie"
+ speed = 0
+ maxHealth = 100
+ light_on = FALSE
+ light_system = OVERLAY_LIGHT
+ light_range = 6
+ light_color = "#64bee1"
+ health = 100
+ habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ unsuitable_atmos_damage = 0
+ can_buckle_to = FALSE
+ density = FALSE
+ pass_flags = PASSMOB
+ move_force = 0
+ move_resist = 0
+ pull_force = 0
+ minimum_survivable_temperature = TCMB
+ maximum_survivable_temperature = INFINITY
+ death_message = "fades out of existence!"
+ ai_controller = /datum/ai_controller/basic_controller/orbie
+ ///are we happy or not?
+ var/happy_state = FALSE
+ ///overlay for our neutral eyes
+ var/static/mutable_appearance/eyes_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_eye_overlay")
+ ///overlay for when our eyes are emitting light
+ var/static/mutable_appearance/orbie_light_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_light_overlay")
+ ///overlay for the flame propellar
+ var/static/mutable_appearance/flame_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_flame_overlay")
+ ///overlay for our happy eyes
+ var/static/mutable_appearance/happy_eyes_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_happy_eye_overlay")
+ ///commands we can give orbie
+ var/list/pet_commands = list(
+ /datum/pet_command/idle,
+ /datum/pet_command/free,
+ /datum/pet_command/untargeted_ability/pet_lights,
+ /datum/pet_command/point_targeting/use_ability/take_photo,
+ /datum/pet_command/follow/orbie,
+ /datum/pet_command/perform_trick_sequence,
+ )
+
+/mob/living/basic/orbie/Initialize(mapload)
+ . = ..()
+ var/static/list/food_types = list(/obj/item/food/virtual_chocolate)
+ AddComponent(/datum/component/obeys_commands, pet_commands)
+ AddElement(/datum/element/basic_eating, food_types = food_types)
+ RegisterSignal(src, COMSIG_ATOM_CAN_BE_PULLED, PROC_REF(on_pulled))
+ RegisterSignal(src, COMSIG_VIRTUAL_PET_LEVEL_UP, PROC_REF(on_level_up))
+ RegisterSignal(src, COMSIG_MOB_CLICKON, PROC_REF(on_click))
+ RegisterSignal(src, COMSIG_ATOM_UPDATE_LIGHT_ON, PROC_REF(on_lights))
+ ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(food_types))
+ update_appearance()
+
+/mob/living/basic/orbie/proc/on_click(mob/living/basic/source, atom/target, params)
+ SIGNAL_HANDLER
+
+ if(!CanReach(target))
+ return
+
+ if(src == target || happy_state || !istype(target))
+ return
+
+ toggle_happy_state()
+ addtimer(CALLBACK(src, PROC_REF(toggle_happy_state)), 30 SECONDS)
+
+/mob/living/basic/orbie/proc/on_lights(datum/source)
+ SIGNAL_HANDLER
+
+ update_appearance()
+
+/mob/living/basic/orbie/proc/toggle_happy_state()
+ happy_state = !happy_state
+ update_appearance()
+
+/mob/living/basic/orbie/proc/on_pulled(datum/source) //i need move resist at 0, but i also dont want him to be pulled
+ SIGNAL_HANDLER
+
+ return COMSIG_ATOM_CANT_PULL
+
+/mob/living/basic/orbie/proc/on_level_up(datum/source, new_level)
+ SIGNAL_HANDLER
+
+ if(maxHealth >= ORBIE_MAXIMUM_HEALTH)
+ UnregisterSignal(src, COMSIG_VIRTUAL_PET_LEVEL_UP)
+ return
+
+ maxHealth += 100
+ heal_overall_damage(maxHealth - health)
+
+
+/mob/living/basic/orbie/update_overlays()
+ . = ..()
+ if(stat == DEAD)
+ return
+ . += flame_overlay
+ if(happy_state)
+ . += happy_eyes_overlay
+ else if(light_on)
+ . += orbie_light_overlay
+ else
+ . += eyes_overlay
+
+/mob/living/basic/orbie/gib()
+ death(TRUE)
+
+#undef ORBIE_MAXIMUM_HEALTH
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm b/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm
new file mode 100644
index 0000000000000..fb9994a932161
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm
@@ -0,0 +1,46 @@
+/datum/action/cooldown/mob_cooldown/lights
+ name = "Toggle Lights"
+ button_icon = 'icons/mob/simple/pets.dmi'
+ button_icon_state = "orbie_light_action"
+ background_icon_state = "bg_default"
+ overlay_icon_state = "bg_default_border"
+ click_to_activate = FALSE
+
+/datum/action/cooldown/mob_cooldown/lights/Activate()
+ owner.set_light_on(!owner.light_on)
+ return TRUE
+
+
+/datum/action/cooldown/mob_cooldown/capture_photo
+ name = "Camera"
+ button_icon = 'icons/mob/simple/pets.dmi'
+ button_icon_state = "orbie_light_action"
+ background_icon_state = "bg_default"
+ overlay_icon_state = "bg_default_border"
+ cooldown_time = 30 SECONDS
+ ///camera we use to take photos
+ var/obj/item/camera/ability_camera
+
+/datum/action/cooldown/mob_cooldown/capture_photo/Grant(mob/grant_to)
+ . = ..()
+ if(isnull(owner))
+ return
+ ability_camera = new(owner)
+ ability_camera.print_picture_on_snap = FALSE
+ RegisterSignal(ability_camera, COMSIG_PREQDELETED, PROC_REF(on_camera_delete))
+
+/datum/action/cooldown/mob_cooldown/capture_photo/Activate(atom/target)
+ if(isnull(ability_camera))
+ return FALSE
+ ability_camera.captureimage(target, owner)
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/capture_photo/proc/on_camera_delete(datum/source)
+ SIGNAL_HANDLER
+ UnregisterSignal(ability_camera, COMSIG_PREQDELETED)
+ ability_camera = null
+
+/datum/action/cooldown/mob_cooldown/capture_photo/Destroy()
+ QDEL_NULL(ability_camera)
+ return ..()
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie_ai.dm b/code/modules/mob/living/basic/pets/orbie/orbie_ai.dm
new file mode 100644
index 0000000000000..854a02094640b
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/orbie/orbie_ai.dm
@@ -0,0 +1,169 @@
+#define PET_PLAYTIME_COOLDOWN (2 MINUTES)
+#define MESSAGE_EXPIRY_TIME (30 SECONDS)
+
+/datum/ai_controller/basic_controller/orbie
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/allow_items,
+ BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
+ BB_TRICK_NAME = "Trick",
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_food,
+ /datum/ai_planning_subtree/find_playmates,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/relay_pda_message,
+ /datum/ai_planning_subtree/pet_planning,
+ )
+
+/datum/ai_controller/basic_controller/orbie/TryPossessPawn(atom/new_pawn)
+ . = ..()
+ if(. & AI_CONTROLLER_INCOMPATIBLE)
+ return
+ RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_LAST_RECIEVED_MESSAGE), PROC_REF(on_set_message))
+
+/datum/ai_controller/basic_controller/orbie/proc/on_set_message(datum/source)
+ SIGNAL_HANDLER
+
+ addtimer(CALLBACK(src, PROC_REF(clear_blackboard_key), BB_LAST_RECIEVED_MESSAGE), MESSAGE_EXPIRY_TIME)
+
+///ai behavior that lets us search for other orbies to play with
+/datum/ai_planning_subtree/find_playmates
+
+/datum/ai_planning_subtree/find_playmates/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard[BB_NEXT_PLAYDATE] > world.time)
+ return
+ if(controller.blackboard_key_exists(BB_NEARBY_PLAYMATE))
+ controller.queue_behavior(/datum/ai_behavior/interact_with_playmate, BB_NEARBY_PLAYMATE)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/find_playmate, BB_NEARBY_PLAYMATE, /mob/living/basic/orbie)
+
+/datum/ai_behavior/find_and_set/find_playmate
+
+/datum/ai_behavior/find_and_set/find_playmate/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ for(var/mob/living/basic/orbie/playmate in oview(search_range, controller.pawn))
+ if(playmate == controller.pawn || playmate.stat == DEAD || isnull(playmate.ai_controller))
+ continue
+ if(playmate.ai_controller.blackboard[BB_NEARBY_PLAYMATE] || playmate.ai_controller.blackboard[BB_NEXT_PLAYDATE] > world.time) //they already have a playmate...
+ continue
+ playmate.ai_controller.set_blackboard_key(BB_NEARBY_PLAYMATE, controller.pawn)
+ return playmate
+ return null
+
+
+/datum/ai_behavior/interact_with_playmate
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/interact_with_playmate/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/turf/target = controller.blackboard[target_key]
+ if(isnull(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/interact_with_playmate/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/basic/living_pawn = controller.pawn
+ var/atom/target = controller.blackboard[target_key]
+
+ if(QDELETED(target))
+ finish_action(controller, FALSE, target_key)
+ return
+
+ living_pawn.manual_emote("plays with [target]!")
+ living_pawn.spin(spintime = 4, speed = 1)
+ living_pawn.ClickOn(target)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/interact_with_playmate/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+ controller.set_blackboard_key(BB_NEXT_PLAYDATE, world.time + PET_PLAYTIME_COOLDOWN)
+
+/datum/ai_planning_subtree/relay_pda_message
+
+/datum/ai_planning_subtree/relay_pda_message/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard[BB_VIRTUAL_PET_LEVEL] < 2 || isnull(controller.blackboard[BB_LAST_RECIEVED_MESSAGE]))
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/relay_pda_message, BB_LAST_RECIEVED_MESSAGE)
+
+/datum/ai_behavior/relay_pda_message/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/basic/living_pawn = controller.pawn
+ var/text_to_say = controller.blackboard[target_key]
+ if(isnull(text_to_say))
+ finish_action(controller, FALSE, target_key)
+ return
+
+ living_pawn.say(text_to_say, forced = "AI controller")
+ living_pawn.spin(spintime = 4, speed = 1)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/relay_pda_message/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+/datum/pet_command/follow/orbie
+ follow_behavior = /datum/ai_behavior/pet_follow_friend/orbie
+
+/datum/ai_behavior/pet_follow_friend/orbie
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+///command to make our pet turn its lights on, we need to be level 2 to activate this ability
+/datum/pet_command/untargeted_ability/pet_lights
+ command_name = "Lights"
+ command_desc = "Toggle your pet's lights!"
+ radial_icon = 'icons/mob/simple/pets.dmi'
+ radial_icon_state = "orbie_lights_action"
+ speech_commands = list("lights", "light", "toggle")
+ ability_key = BB_LIGHTS_ABILITY
+
+/datum/pet_command/untargeted_ability/pet_lights/execute_action(datum/ai_controller/controller)
+ if(controller.blackboard[BB_VIRTUAL_PET_LEVEL] < 2)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+ return ..()
+
+/datum/pet_command/point_targeting/use_ability/take_photo
+ command_name = "Photo"
+ command_desc = "Make your pet take a photo!"
+ radial_icon = 'icons/mob/simple/pets.dmi'
+ radial_icon_state = "orbie_lights_action"
+ speech_commands = list("photo", "picture", "image")
+ command_feedback = "Readys camera mode"
+ pet_ability_key = BB_PHOTO_ABILITY
+ targeting_strategy_key = BB_TARGETING_STRATEGY
+
+/datum/pet_command/point_targeting/use_ability/take_photo/execute_action(datum/ai_controller/controller)
+ if(controller.blackboard[BB_VIRTUAL_PET_LEVEL] < 3)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+ return ..()
+
+/datum/pet_command/perform_trick_sequence
+ command_name = "Trick Sequence"
+ command_desc = "A trick sequence programmable through your PDA!"
+
+/datum/pet_command/perform_trick_sequence/find_command_in_text(spoken_text, check_verbosity = FALSE)
+ var/mob/living/living_pawn = weak_parent.resolve()
+ if(isnull(living_pawn?.ai_controller))
+ return FALSE
+ var/text_command = living_pawn.ai_controller.blackboard[BB_TRICK_NAME]
+ if(isnull(text_command))
+ return FALSE
+ return findtext(spoken_text, text_command)
+
+/datum/pet_command/perform_trick_sequence/execute_action(datum/ai_controller/controller)
+ var/mob/living/living_pawn = controller.pawn
+ var/list/trick_sequence = controller.blackboard[BB_TRICK_SEQUENCE]
+ for(var/index in 1 to length(trick_sequence))
+ addtimer(CALLBACK(living_pawn, TYPE_PROC_REF(/mob, emote), trick_sequence[index], index * 0.5 SECONDS))
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+#undef PET_PLAYTIME_COOLDOWN
+#undef MESSAGE_EXPIRY_TIME
diff --git a/code/modules/mob/living/carbon/alien/adult/adult.dm b/code/modules/mob/living/carbon/alien/adult/adult.dm
index 2cab03d670e59..bbacffd4f6f32 100644
--- a/code/modules/mob/living/carbon/alien/adult/adult.dm
+++ b/code/modules/mob/living/carbon/alien/adult/adult.dm
@@ -143,6 +143,9 @@ GLOBAL_LIST_INIT(strippable_alien_humanoid_items, create_strippable_list(list(
melting_pot.consume_thing(lucky_winner)
return TRUE
+/mob/living/carbon/alien/adult/get_butt_sprite()
+ return BUTT_SPRITE_XENOMORPH
+
// Aliens can touch acid
/mob/living/carbon/alien/can_touch_acid(atom/acided_atom, acid_power, acid_volume)
return TRUE
diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm
index 85a6b06a2d340..eafb6f8ba22e1 100644
--- a/code/modules/mob/living/carbon/death.dm
+++ b/code/modules/mob/living/carbon/death.dm
@@ -42,7 +42,7 @@
for(var/obj/item/organ/organ as anything in organs)
if((drop_bitflags & DROP_BRAIN) && istype(organ, /obj/item/organ/internal/brain))
- if(drop_bitflags & DROP_BODYPARTS)
+ if((drop_bitflags & DROP_BODYPARTS) && (check_zone(organ.zone) != BODY_ZONE_CHEST)) // chests can't drop
continue // the head will drop, so the brain should stay inside
organ.Remove(src)
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index ff07ad2ef0e73..7c8b6e9d23799 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -162,9 +162,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
///Unique cookie given by admins through prayers
var/species_cookie = /obj/item/food/cookie
- ///For custom overrides for species ass images
- var/icon/ass_image
-
/// List of family heirlooms this species can get with the family heirloom quirk. List of types.
var/list/family_heirlooms
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 89ad4700aad71..29b4e1f0793d3 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -347,6 +347,10 @@
var/obj/item/bodypart/the_part = isbodypart(target_zone) ? target_zone : get_bodypart(check_zone(target_zone)) //keep these synced
to_chat(user, span_alert("There is no exposed flesh or thin material on [p_their()] [the_part.name]."))
+/mob/living/carbon/human/get_butt_sprite()
+ var/obj/item/bodypart/chest/chest = get_bodypart(BODY_ZONE_CHEST)
+ return chest?.get_butt_sprite()
+
/mob/living/carbon/human/get_footprint_sprite()
var/obj/item/bodypart/leg/L = get_bodypart(BODY_ZONE_R_LEG) || get_bodypart(BODY_ZONE_L_LEG)
return shoes?.footprint_sprite || L?.footprint_sprite
diff --git a/code/modules/mob/living/carbon/human/species_types/abductors.dm b/code/modules/mob/living/carbon/human/species_types/abductors.dm
index 74d2bedf3a702..1eae13b0a5b28 100644
--- a/code/modules/mob/living/carbon/human/species_types/abductors.dm
+++ b/code/modules/mob/living/carbon/human/species_types/abductors.dm
@@ -19,7 +19,6 @@
mutantlungs = null
mutantbrain = /obj/item/organ/internal/brain/abductor
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN | SLIME_EXTRACT
- ass_image = 'icons/ass/assgrey.png'
bodypart_overrides = list(
BODY_ZONE_HEAD = /obj/item/bodypart/head/abductor,
diff --git a/code/modules/mob/living/carbon/human/species_types/felinid.dm b/code/modules/mob/living/carbon/human/species_types/felinid.dm
index 277bc7d72af8b..e57708565d716 100644
--- a/code/modules/mob/living/carbon/human/species_types/felinid.dm
+++ b/code/modules/mob/living/carbon/human/species_types/felinid.dm
@@ -18,7 +18,6 @@
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN | SLIME_EXTRACT
species_language_holder = /datum/language_holder/felinid
payday_modifier = 1.0
- ass_image = 'icons/ass/asscat.png'
family_heirlooms = list(/obj/item/toy/cattoy)
/// When false, this is a felinid created by mass-purrbation
var/original_felinid = TRUE
diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
index a39567d113147..9ec396816558b 100644
--- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
@@ -30,7 +30,6 @@
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN | SLIME_EXTRACT
inherent_factions = list(FACTION_SLIME)
species_language_holder = /datum/language_holder/jelly
- ass_image = 'icons/ass/assslime.png'
bodypart_overrides = list(
BODY_ZONE_L_ARM = /obj/item/bodypart/arm/left/jelly,
diff --git a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
index 2e265d32f65c7..8b9946fd8c72c 100644
--- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
@@ -34,8 +34,6 @@
bodytemp_heat_damage_limit = BODYTEMP_HEAT_LAVALAND_SAFE
bodytemp_cold_damage_limit = (BODYTEMP_COLD_DAMAGE_LIMIT - 10)
- ass_image = 'icons/ass/asslizard.png'
-
bodypart_overrides = list(
BODY_ZONE_HEAD = /obj/item/bodypart/head/lizard,
BODY_ZONE_CHEST = /obj/item/bodypart/chest/lizard,
diff --git a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
index c9fa732b2880d..ad6e36b527108 100644
--- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
@@ -50,8 +50,6 @@
// This effects how fast body temp stabilizes, also if cold resit is lost on the mob
bodytemp_cold_damage_limit = (BODYTEMP_COLD_DAMAGE_LIMIT - 50) // about -50c
- ass_image = 'icons/ass/assplasma.png'
-
outfit_override_registry = list(
/datum/outfit/syndicate = /datum/outfit/syndicate/plasmaman,
/datum/outfit/syndicate/full = /datum/outfit/syndicate/full/plasmaman,
diff --git a/code/modules/mob/living/carbon/human/species_types/podpeople.dm b/code/modules/mob/living/carbon/human/species_types/podpeople.dm
index 0190996567d13..d42dea250b4f3 100644
--- a/code/modules/mob/living/carbon/human/species_types/podpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/podpeople.dm
@@ -29,8 +29,6 @@
BODY_ZONE_CHEST = /obj/item/bodypart/chest/pod,
)
- ass_image = 'icons/ass/asspodperson.png'
-
/datum/species/pod/on_species_gain(mob/living/carbon/new_podperson, datum/species/old_species, pref_load)
. = ..()
if(ishuman(new_podperson))
diff --git a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
index c77ad3dfee1ed..b2027b9e1a654 100644
--- a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
@@ -95,6 +95,7 @@
icon = 'icons/obj/medical/organs/shadow_organs.dmi'
color_cutoffs = list(20, 10, 40)
pepperspray_protect = TRUE
+ flash_protect = FLASH_PROTECTION_SENSITIVE
/// the key to some of their powers
/obj/item/organ/internal/brain/shadow
diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm
index 5c13e489395b1..3a19eebadc966 100644
--- a/code/modules/mob/living/emote.dm
+++ b/code/modules/mob/living/emote.dm
@@ -711,7 +711,7 @@
message = "beeps."
message_param = "beeps at %t."
sound = 'sound/machines/twobeep.ogg'
- mob_type_allowed_typecache = list(/mob/living/brain, /mob/living/silicon)
+ mob_type_allowed_typecache = list(/mob/living/brain, /mob/living/silicon, /mob/living/basic/orbie)
emote_type = EMOTE_AUDIBLE
/datum/emote/living/inhale
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index e096434cbb966..86f93a344c6f7 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -2257,6 +2257,9 @@ GLOBAL_LIST_EMPTY(fire_appearances)
/mob/living/proc/is_face_visible()
return TRUE
+/// Sprite to show for photocopying mob butts
+/mob/living/proc/get_butt_sprite()
+ return null
///Proc to modify the value of num_legs and hook behavior associated to this event.
/mob/living/proc/set_num_legs(new_value)
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 562c54ae0b758..ec366f0f26ce8 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -500,7 +500,8 @@
///As the name suggests, this should be called to apply electric shocks.
/mob/living/proc/electrocute_act(shock_damage, source, siemens_coeff = 1, flags = NONE)
- SEND_SIGNAL(src, COMSIG_LIVING_ELECTROCUTE_ACT, shock_damage, source, siemens_coeff, flags)
+ if(SEND_SIGNAL(src, COMSIG_LIVING_ELECTROCUTE_ACT, shock_damage, source, siemens_coeff, flags) & COMPONENT_LIVING_BLOCK_SHOCK)
+ return FALSE
shock_damage *= siemens_coeff
if((flags & SHOCK_TESLA) && HAS_TRAIT(src, TRAIT_TESLA_SHOCKIMMUNE))
return FALSE
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index 3cd9d45f5c107..b43a625b1e482 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -79,6 +79,7 @@
)
add_traits(traits_to_apply, ROUNDSTART_TRAIT)
+ RegisterSignal(src, COMSIG_LIVING_ELECTROCUTE_ACT, PROC_REF(on_silicon_shocked))
/mob/living/silicon/Destroy()
QDEL_NULL(radio)
@@ -90,6 +91,14 @@
GLOB.silicon_mobs -= src
return ..()
+/mob/living/silicon/proc/on_silicon_shocked(datum/source, shock_damage, shock_source, siemens_coeff, flags)
+ SIGNAL_HANDLER
+ for(var/mob/living/living_mob in buckled_mobs)
+ unbuckle_mob(living_mob)
+ living_mob.electrocute_act(shock_damage/100, shock_source, siemens_coeff, flags) //Hard metal shell conducts!
+
+ return COMPONENT_LIVING_BLOCK_SHOCK //So borgs don't die trying to fix wiring
+
/mob/living/silicon/proc/create_modularInterface()
if(!modularInterface)
modularInterface = new /obj/item/modular_computer/pda/silicon(src)
@@ -432,6 +441,9 @@
/mob/living/silicon/on_standing_up()
return // Silicons are always standing by default.
+/mob/living/silicon/get_butt_sprite()
+ return BUTT_SPRITE_QR_CODE
+
/**
* Records an IC event log entry in the cyborg's internal tablet.
*
diff --git a/code/modules/mob/living/silicon/silicon_defense.dm b/code/modules/mob/living/silicon/silicon_defense.dm
index 175c335385057..b7a669b4a2e62 100644
--- a/code/modules/mob/living/silicon/silicon_defense.dm
+++ b/code/modules/mob/living/silicon/silicon_defense.dm
@@ -101,13 +101,6 @@
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
return ..()
-/mob/living/silicon/electrocute_act(shock_damage, source, siemens_coeff = 1, flags = NONE)
- if(buckled_mobs)
- for(var/mob/living/M in buckled_mobs)
- unbuckle_mob(M)
- M.electrocute_act(shock_damage/100, source, siemens_coeff, flags) //Hard metal shell conducts!
- return 0 //So borgs they don't die trying to fix wiring
-
/mob/living/silicon/emp_act(severity)
. = ..()
to_chat(src, span_danger("Warning: Electromagnetic pulse detected."))
diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm
index 582f51fb3aa9c..f99c649b50ec2 100644
--- a/code/modules/mob/living/simple_animal/bot/secbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/secbot.dm
@@ -71,6 +71,11 @@
desc = "It's Officer Beepsky! Powered by a potato and a shot of whiskey, and with a sturdier reinforced chassis, too."
health = 45
+/mob/living/simple_animal/bot/secbot/beepsky/officer/Initialize(mapload)
+ . = ..()
+ // Beepsky hates people scanning them
+ RegisterSignal(src, COMSIG_MOVABLE_SPY_STEALING, PROC_REF(retaliate_async))
+
/mob/living/simple_animal/bot/secbot/beepsky/ofitser
name = "Prison Ofitser"
desc = "Powered by the tears and sweat of laborers."
@@ -194,6 +199,11 @@
if("arrest_alert")
security_mode_flags ^= SECBOT_DECLARE_ARRESTS
+/mob/living/simple_animal/bot/secbot/proc/retaliate_async(datum/source, mob/user, ...)
+ SIGNAL_HANDLER
+
+ INVOKE_ASYNC(src, PROC_REF(retaliate), user)
+
/mob/living/simple_animal/bot/secbot/proc/retaliate(mob/living/carbon/human/attacking_human)
var/judgement_criteria = judgement_criteria()
threatlevel = attacking_human.assess_threat(judgement_criteria)
diff --git a/code/modules/mob/living/simple_animal/hostile/alien.dm b/code/modules/mob/living/simple_animal/hostile/alien.dm
index 593bf29535edc..8ce6d28f2ab62 100644
--- a/code/modules/mob/living/simple_animal/hostile/alien.dm
+++ b/code/modules/mob/living/simple_animal/hostile/alien.dm
@@ -110,6 +110,9 @@
egg_cooldown = initial(egg_cooldown)
LayEggs()
+/mob/living/simple_animal/hostile/alien/get_butt_sprite()
+ return BUTT_SPRITE_XENOMORPH
+
/mob/living/simple_animal/hostile/alien/proc/SpreadPlants()
if(!isturf(loc) || isspaceturf(loc))
return
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
index 342713ee960a5..6632dedd2342b 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
@@ -88,8 +88,8 @@ Difficulty: Hard
/datum/action/innate/megafauna_attack/shockwave_scream
name = "Shockwave Scream"
- button_icon = 'icons/turf/walls/wall.dmi'
- button_icon_state = "wall-0"
+ button_icon = 'icons/mob/actions/actions_animal.dmi'
+ button_icon_state = "expand"
chosen_message = "You are now screeching, disorienting targets around you. "
chosen_attack_num = 3
diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
index 2f3898fa3f7df..5fbf6e3d796a8 100644
--- a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
+++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
@@ -714,6 +714,8 @@
var/photo_message = signal.data["photo"] ? " (Photo Attached )" : ""
to_chat(messaged_mob, span_infoplain("[icon2html(computer, messaged_mob)] PDA message from [sender_title], \"[inbound_message]\"[photo_message] [reply]"))
+ SEND_SIGNAL(computer, COMSIG_COMPUTER_RECIEVED_MESSAGE, sender_title, inbound_message, photo_message)
+
if (alert_able && (!alert_silenced || is_rigged))
computer.ring(ringtone)
diff --git a/code/modules/modular_computers/file_system/programs/virtual_pet.dm b/code/modules/modular_computers/file_system/programs/virtual_pet.dm
new file mode 100644
index 0000000000000..1d3196789ca87
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/virtual_pet.dm
@@ -0,0 +1,568 @@
+GLOBAL_LIST_EMPTY(global_pet_updates)
+GLOBAL_LIST_EMPTY(virtual_pets_list)
+
+#define MAX_UPDATE_LENGTH 50
+#define PET_MAX_LEVEL 3
+#define PET_MAX_STEPS_RECORD 50000
+#define PET_EAT_BONUS 500
+#define PET_CLEAN_BONUS 250
+#define PET_PLAYMATE_BONUS 500
+#define PET_STATE_HUNGRY "hungry"
+#define PET_STATE_ASLEEP "asleep"
+#define PET_STATE_HAPPY "happy"
+#define PET_STATE_NEUTRAL "neutral"
+
+/datum/computer_file/program/virtual_pet
+ filename = "virtualpet"
+ filedesc = "Virtual Pet"
+ downloader_category = PROGRAM_CATEGORY_GAMES
+ extended_desc = "Download your very own Orbie today!"
+ program_flags = PROGRAM_ON_NTNET_STORE
+ size = 3
+ tgui_id = "NtosVirtualPet"
+ program_icon = "paw"
+ can_run_on_flags = PROGRAM_PDA
+ detomatix_resistance = DETOMATIX_RESIST_MALUS
+ ///how many steps have we walked
+ var/steps_counter = 0
+ ///the pet hologram
+ var/mob/living/pet
+ ///the type of our pet
+ var/pet_type = /mob/living/basic/orbie
+ ///our current happiness
+ var/happiness = 0
+ ///our max happiness
+ var/max_happiness = 1750
+ ///our current level
+ var/level = 1
+ ///required exp to get to next level
+ var/to_next_level = 1000
+ ///how much exp we currently have
+ var/current_level_progress = 0
+ ///our current hunger
+ var/hunger = 0
+ ///maximum hunger threshold
+ var/max_hunger = 500
+ ///pet icon for each state
+ var/static/list/pet_state_icons = list(
+ PET_STATE_HUNGRY = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_hungry"),
+ PET_STATE_HAPPY = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_happy"),
+ PET_STATE_ASLEEP = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_asleep"),
+ PET_STATE_NEUTRAL = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_neutral"),
+ )
+ ///hat options and what level they will be unlocked at
+ var/static/list/hat_selections = list(
+ /obj/item/clothing/head/hats/tophat = 1,
+ /obj/item/clothing/head/fedora = 1,
+ /obj/item/clothing/head/hats/bowler = 2,
+ /obj/item/clothing/head/hats/warden/police = 2,
+ /obj/item/clothing/head/hats/warden/red = 3,
+ /obj/item/clothing/head/hats/caphat = 3,
+ )
+ ///hologram hat we have selected for our pet
+ var/list/selected_hat = list()
+ ///area we have picked as dropoff location for petfeed
+ var/area/selected_area
+ ///manage hat offsets for when we turn directions
+ var/static/list/hat_offsets = list(
+ "west" = list(0,1),
+ "east" = list(0,1),
+ "north" = list(1,1),
+ "south" = list(0,1),
+ )
+ ///possible colors our pet can have
+ var/static/list/possible_colors= list(
+ "white" = null, //default color state
+ "light blue" = "#c3ecf3",
+ "light green" = "#b1ffe8",
+ )
+ ///areas we wont drop the chocolate in
+ var/static/list/restricted_areas = typecacheof(list(
+ /area/station/security,
+ /area/station/command,
+ /area/station/ai_monitored,
+ /area/station/maintenance,
+ /area/station/solars,
+ ))
+ ///our profile picture
+ var/icon/profile_picture
+ ///cooldown till we can reroll the pet feed dropzone
+ COOLDOWN_DECLARE(area_reroll)
+ ///cooldown till our pet gains happiness again from being cleaned
+ COOLDOWN_DECLARE(on_clean_cooldown)
+ ///cooldown till we can release/recall our pet
+ COOLDOWN_DECLARE(summon_cooldown)
+ ///cooldown till we can alter our pet's appearance again
+ COOLDOWN_DECLARE(alter_appearance_cooldown)
+
+/datum/computer_file/program/virtual_pet/on_install()
+ . = ..()
+ profile_picture = getFlatIcon(image(icon = 'icons/ui_icons/virtualpet/pet_state.dmi', icon_state = "pet_preview"))
+ GLOB.virtual_pets_list += src
+ pet = new pet_type(computer)
+ pet.forceMove(computer)
+ pet.AddComponent(/datum/component/leash, computer, 9, force_teleport_out_effect = /obj/effect/temp_visual/guardian/phase/out)
+ RegisterSignal(pet, COMSIG_QDELETING, PROC_REF(remove_pet))
+ RegisterSignal(pet, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_overlays_updated)) //hologramic hat management
+ RegisterSignal(pet, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_change_dir))
+ RegisterSignal(pet, COMSIG_MOVABLE_MOVED, PROC_REF(after_pet_move))
+ RegisterSignal(pet, COMSIG_MOB_ATE, PROC_REF(after_pet_eat)) // WE ATEEE
+ RegisterSignal(pet, COMSIG_ATOM_PRE_CLEAN, PROC_REF(pet_pre_clean))
+ RegisterSignal(pet, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ RegisterSignal(pet, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(post_cleaned))
+ RegisterSignal(pet, COMSIG_AI_BLACKBOARD_KEY_SET(BB_NEARBY_PLAYMATE), PROC_REF(on_playmate_find))
+ RegisterSignal(computer, COMSIG_ATOM_ENTERED, PROC_REF(on_pet_entered))
+ RegisterSignal(computer, COMSIG_ATOM_EXITED, PROC_REF(on_pet_exit))
+
+/datum/computer_file/program/virtual_pet/Destroy()
+ GLOB.virtual_pets_list -= src
+ if(!QDELETED(pet))
+ QDEL_NULL(pet)
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
+/datum/computer_file/program/virtual_pet/proc/on_death(datum/source)
+ SIGNAL_HANDLER
+
+ pet.forceMove(computer)
+
+
+/datum/computer_file/program/virtual_pet/proc/on_message_recieve(datum/source, sender_title, inbound_message, photo_message)
+ SIGNAL_HANDLER
+
+ var/message_to_display = "[sender_title] has sent you a message [photo_message ? "with a photo attached" : ""]: [inbound_message]!"
+ pet.ai_controller?.set_blackboard_key(BB_LAST_RECIEVED_MESSAGE, message_to_display)
+
+/datum/computer_file/program/virtual_pet/proc/pet_pre_clean(atom/source, mob/user)
+ SIGNAL_HANDLER
+
+ if(!COOLDOWN_FINISHED(src, on_clean_cooldown))
+ source.balloon_alert(user, "already clean!")
+ return COMSIG_ATOM_CANCEL_CLEAN
+
+/datum/computer_file/program/virtual_pet/proc/on_playmate_find(datum/source)
+ SIGNAL_HANDLER
+
+ happiness = min(happiness + PET_PLAYMATE_BONUS, max_happiness)
+ START_PROCESSING(SSprocessing, src)
+
+/datum/computer_file/program/virtual_pet/proc/post_cleaned(mob/source, mob/user)
+ SIGNAL_HANDLER
+
+ source.spin(spintime = 2 SECONDS, speed = 1) //celebrate!
+ happiness = min(happiness + PET_CLEAN_BONUS, max_happiness)
+ COOLDOWN_START(src, on_clean_cooldown, 1 MINUTES)
+ START_PROCESSING(SSprocessing, src)
+
+///manage the pet's hat offsets when he changes direction
+/datum/computer_file/program/virtual_pet/proc/on_change_dir(datum/source, old_dir, new_dir)
+ SIGNAL_HANDLER
+
+ if(!length(selected_hat))
+ return
+ set_hat_offsets(new_dir)
+
+/datum/computer_file/program/virtual_pet/proc/on_photo_captured(datum/source, atom/target, atom/user, datum/picture/photo)
+ SIGNAL_HANDLER
+
+ if(isnull(photo))
+ return
+ computer.store_file(new /datum/computer_file/picture(photo))
+
+/datum/computer_file/program/virtual_pet/proc/set_hat_offsets(new_dir)
+ var/direction_text = dir2text(new_dir)
+ var/list/offsets_list = hat_offsets[direction_text]
+ if(isnull(offsets_list))
+ return
+ var/mutable_appearance/hat_appearance = selected_hat["appearance"]
+ hat_appearance.pixel_x = offsets_list[1]
+ hat_appearance.pixel_y = offsets_list[2]
+ pet.update_appearance(UPDATE_OVERLAYS)
+
+///give our pet his hologram hat
+/datum/computer_file/program/virtual_pet/proc/on_overlays_updated(atom/source, list/overlays)
+ SIGNAL_HANDLER
+
+ if(!length(selected_hat))
+ return
+ overlays += selected_hat["appearance"]
+
+/datum/computer_file/program/virtual_pet/proc/alter_profile_picture()
+ var/image/pet_preview = image(icon = 'icons/ui_icons/virtualpet/pet_state.dmi', icon_state = "pet_preview")
+ if(LAZYACCESS(pet.atom_colours, FIXED_COLOUR_PRIORITY))
+ pet_preview.color = pet.atom_colours[FIXED_COLOUR_PRIORITY]
+
+ if(length(selected_hat))
+ var/mutable_appearance/our_selected_hat = selected_hat["appearance"]
+ var/mutable_appearance/hat_preview = mutable_appearance(our_selected_hat.icon, our_selected_hat.icon_state)
+ hat_preview.pixel_y = -9
+ pet_preview.add_overlay(hat_preview)
+
+ profile_picture = getFlatIcon(pet_preview)
+ COOLDOWN_START(src, alter_appearance_cooldown, 10 SECONDS)
+
+
+///decrease the pet's hunger after it eats
+/datum/computer_file/program/virtual_pet/proc/after_pet_eat(datum/source)
+ SIGNAL_HANDLER
+
+ hunger = min(hunger + PET_EAT_BONUS, max_hunger)
+ happiness = min(happiness + PET_EAT_BONUS, max_happiness)
+ START_PROCESSING(SSprocessing, src)
+
+///start processing if we enter the pda and need healing
+/datum/computer_file/program/virtual_pet/proc/on_pet_entered(atom/movable/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if(arrived != pet)
+ return
+ ADD_TRAIT(pet, TRAIT_AI_PAUSED, REF(src))
+ if((datum_flags & DF_ISPROCESSING))
+ return
+ if(pet.health < pet.maxHealth) //if we're in the pda, heal up
+ START_PROCESSING(SSprocessing, src)
+
+/datum/computer_file/program/virtual_pet/proc/on_pet_exit(atom/movable/source, atom/movable/exited)
+ SIGNAL_HANDLER
+
+ if(exited != pet)
+ return
+ REMOVE_TRAIT(pet, TRAIT_AI_PAUSED, REF(src))
+ if((datum_flags & DF_ISPROCESSING))
+ return
+ if(hunger > 0 || happiness > 0) //if were outside the pda, we become hungry and happiness decreases
+ START_PROCESSING(SSprocessing, src)
+
+/datum/computer_file/program/virtual_pet/process()
+ if(pet.loc == computer)
+ if(pet.health >= pet.maxHealth)
+ return PROCESS_KILL
+ if(pet.stat == DEAD)
+ pet.revive(ADMIN_HEAL_ALL)
+ pet.heal_overall_damage(5)
+ return
+
+ if(hunger > 0)
+ hunger--
+
+ if(happiness > 0)
+ happiness--
+
+ if(hunger <=0 && happiness <= 0)
+ return PROCESS_KILL
+
+/datum/computer_file/program/virtual_pet/proc/after_pet_move(atom/movable/movable, atom/old_loc)
+ SIGNAL_HANDLER
+
+ if(!isturf(pet.loc) || !isturf(old_loc))
+ return
+ steps_counter = min(steps_counter + 1, PET_MAX_STEPS_RECORD)
+ increment_exp()
+ if(steps_counter % 2000 == 0) //every 2000 steps, announce the milestone to the world!
+ announce_global_updates(message = "has walked [steps_counter] steps!")
+
+/datum/computer_file/program/virtual_pet/proc/increment_exp()
+ var/modifier = 1
+ var/hunger_happiness = hunger + happiness
+ var/max_hunger_happiness = max_hunger + max_happiness
+
+ switch(hunger_happiness / max_hunger_happiness)
+ if(0.8 to 1)
+ modifier = 3
+ if(0.5 to 0.8)
+ modifier = 2
+
+ current_level_progress = min(current_level_progress + modifier, to_next_level)
+ if(current_level_progress >= to_next_level)
+ handle_level_up()
+
+/datum/computer_file/program/virtual_pet/proc/handle_level_up()
+ current_level_progress = 0
+ level++
+ grant_level_abilities()
+ pet.ai_controller?.set_blackboard_key(BB_VIRTUAL_PET_LEVEL, level)
+ playsound(computer.loc, 'sound/items/orbie_level_up.ogg', 50)
+ to_next_level += (level**2) + 500
+ SEND_SIGNAL(pet, COMSIG_VIRTUAL_PET_LEVEL_UP, level) //its a signal so different path types of virtual pets can handle leveling up differently
+ announce_global_updates(message = "has reached level [level]!")
+
+/datum/computer_file/program/virtual_pet/proc/grant_level_abilities()
+ switch(level)
+ if(2)
+ RegisterSignal(computer, COMSIG_COMPUTER_RECIEVED_MESSAGE, PROC_REF(on_message_recieve)) // we will now read out PDA messages
+ var/datum/action/cooldown/mob_cooldown/lights/lights = new(pet)
+ lights.Grant(pet)
+ pet.ai_controller?.set_blackboard_key(BB_LIGHTS_ABILITY, lights)
+ if(3)
+ var/datum/action/cooldown/mob_cooldown/capture_photo/photo_ability = new(pet)
+ photo_ability.Grant(pet)
+ pet.ai_controller?.set_blackboard_key(BB_PHOTO_ABILITY, photo_ability)
+ RegisterSignal(photo_ability.ability_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_photo_captured))
+
+/datum/computer_file/program/virtual_pet/proc/announce_global_updates(message)
+ if(isnull(message))
+ return
+ var/list/message_to_announce = list(
+ "name" = pet.name,
+ "pet_picture" = icon2base64(profile_picture),
+ "message" = message,
+ "likers" = list(REF(src))
+ )
+ if(length(GLOB.global_pet_updates) >= MAX_UPDATE_LENGTH)
+ GLOB.global_pet_updates.Cut(1,2)
+
+ GLOB.global_pet_updates += list(message_to_announce)
+ playsound(computer.loc, 'sound/items/orbie_notification_sound.ogg', 50)
+
+/datum/computer_file/program/virtual_pet/proc/remove_pet(datum/source)
+ SIGNAL_HANDLER
+ pet = null
+ if(QDELETED(src))
+ return
+ computer.remove_file(src) //all is lost we no longer have a reason to exist
+
+/datum/computer_file/program/virtual_pet/kill_program(mob/user)
+ if(pet && pet.loc != computer)
+ pet.forceMove(computer) //recall the hologram back to the pda
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
+/datum/computer_file/program/virtual_pet/proc/get_pet_state()
+ if(isnull(pet))
+ return
+
+ if(pet.loc == computer)
+ return PET_STATE_ASLEEP
+
+ if(happiness/max_happiness > 0.8)
+ return PET_STATE_HAPPY
+
+ if(hunger/max_hunger < 0.5)
+ return PET_STATE_HUNGRY
+
+ return PET_STATE_NEUTRAL
+
+/datum/computer_file/program/virtual_pet/ui_data(mob/user)
+ var/list/data = list()
+ data["currently_summoned"] = (pet.loc != computer)
+ data["selected_area"] = (selected_area ? selected_area.name : "No location set")
+ data["pet_state"] = get_pet_state()
+ data["hunger"] = hunger
+ data["maximum_hunger"] = max_hunger
+ data["pet_hat"] = (length(selected_hat) ? selected_hat["name"] : "none")
+ data["can_reroll"] = COOLDOWN_FINISHED(src, area_reroll)
+ data["can_summon"] = COOLDOWN_FINISHED(src, summon_cooldown)
+ data["can_alter_appearance"] = COOLDOWN_FINISHED(src, alter_appearance_cooldown)
+ data["pet_name"] = pet.name
+ data["steps_counter"] = steps_counter
+ data["in_dropzone"] = (istype(get_area(computer), selected_area))
+ data["pet_area"] = (pet.loc != computer ? get_area_name(pet) : "Sleeping in PDA")
+ data["current_exp"] = current_level_progress
+ data["required_exp"] = to_next_level
+ data["happiness"] = happiness
+ data["maximum_happiness"] = max_happiness
+ data["level"] = level
+ data["pet_color"] = ""
+
+ var/color_value = LAZYACCESS(pet.atom_colours, FIXED_COLOUR_PRIORITY)
+ for(var/index in possible_colors)
+ if(possible_colors[index] == color_value)
+ data["pet_color"] = index
+ break
+
+ data["pet_gender"] = pet.gender
+
+ data["pet_updates"] = list()
+
+ for(var/i in length(GLOB.global_pet_updates) to 1 step -1)
+ var/list/update = GLOB.global_pet_updates[i]
+
+ if(isnull(update))
+ continue
+
+ data["pet_updates"] += list(list(
+ "update_id" = i,
+ "update_name" = update["name"],
+ "update_picture" = update["pet_picture"],
+ "update_message" = update["message"],
+ "update_likers" = length(update["likers"]),
+ "update_already_liked" = ((REF(src)) in update["likers"]),
+ ))
+
+ data["all_pets"] = list()
+ for(var/datum/computer_file/program/virtual_pet/program as anything in GLOB.virtual_pets_list)
+ data["all_pets"] += list(list(
+ "other_pet_name" = program.pet.name,
+ "other_pet_picture" = icon2base64(program.profile_picture),
+ ))
+ return data
+
+/datum/computer_file/program/virtual_pet/ui_static_data(mob/user)
+ var/list/data = list()
+ data["pet_state_icons"] = list()
+ for(var/list_index as anything in pet_state_icons)
+ var/list/sprite_location = pet_state_icons[list_index]
+ data["pet_state_icons"] += list(list(
+ "name" = list_index,
+ "icon" = icon2base64(getFlatIcon(image(icon = sprite_location["icon"], icon_state = sprite_location["icon_state"]), no_anim=TRUE))
+ ))
+
+ data["hat_selections"] = list(list(
+ "hat_id" = null,
+ "hat_name" = "none",
+ ))
+
+ for(var/type_index as anything in hat_selections)
+ if(level >= hat_selections[type_index])
+ var/obj/item/hat = type_index
+ data["hat_selections"] += list(list(
+ "hat_id" = type_index,
+ "hat_name" = initial(hat.name),
+ ))
+
+ data["possible_colors"] = list()
+ for(var/color in possible_colors)
+ data["possible_colors"] += list(list(
+ "color_name" = color,
+ "color_value" = possible_colors[color],
+ ))
+
+ var/static/list/possible_emotes = list(
+ /datum/emote/flip,
+ /datum/emote/living/jump,
+ /datum/emote/living/shiver,
+ /datum/emote/spin,
+ /datum/emote/living/beep,
+ )
+ data["possible_emotes"] = list("none")
+ for(var/datum/emote/target_emote as anything in possible_emotes)
+ data["possible_emotes"] += target_emote.key
+
+ data["preview_icon"] = icon2base64(profile_picture)
+ return data
+
+/datum/computer_file/program/virtual_pet/ui_act(action, params, datum/tgui/ui)
+ . = ..()
+ switch(action)
+
+ if("summon_pet")
+ if(!COOLDOWN_FINISHED(src, summon_cooldown))
+ return TRUE
+ if(pet.loc == computer)
+ release_pet(ui.user)
+ else
+ recall_pet()
+ COOLDOWN_START(src, summon_cooldown, 10 SECONDS)
+
+ if("apply_customization")
+ if(!COOLDOWN_FINISHED(src, alter_appearance_cooldown))
+ return TRUE
+ var/obj/item/chosen_type = text2path(params["chosen_hat"])
+ if(isnull(chosen_type))
+ selected_hat.Cut()
+
+ else if((chosen_type in hat_selections))
+ selected_hat["name"] = initial(chosen_type.name)
+ var/mutable_appearance/selected_hat_appearance = mutable_appearance(icon = initial(chosen_type.worn_icon), icon_state = initial(chosen_type.icon_state), layer = ABOVE_ALL_MOB_LAYER)
+ selected_hat_appearance.transform = selected_hat_appearance.transform.Scale(0.8, 1)
+ selected_hat["appearance"] = selected_hat_appearance
+ set_hat_offsets(pet.dir)
+
+ var/chosen_color = params["chosen_color"]
+ if(isnull(chosen_color))
+ pet.remove_atom_colour(FIXED_COLOUR_PRIORITY)
+ else
+ pet.add_atom_colour(chosen_color, FIXED_COLOUR_PRIORITY)
+
+ var/input_name = sanitize_name(params["chosen_name"], allow_numbers = TRUE)
+ pet.name = (input_name ? input_name : initial(pet.name))
+ new /obj/effect/temp_visual/guardian/phase(pet.loc)
+
+ switch(params["chosen_gender"])
+ if("male")
+ pet.gender = MALE
+ if("female")
+ pet.gender = FEMALE
+ if("neuter")
+ pet.gender = NEUTER
+
+ pet.update_appearance()
+ alter_profile_picture()
+ update_static_data(ui.user, ui)
+
+ if("get_feed_location")
+ generate_petfeed_area()
+
+ if("drop_feed")
+ drop_feed()
+
+ if("like_update")
+ var/index = params["update_reference"]
+ var/list/update_message = GLOB.global_pet_updates[index]
+ if(isnull(update_message))
+ return TRUE
+ var/our_reference = REF(src)
+ if(our_reference in update_message["likers"])
+ update_message["likers"] -= our_reference
+ else
+ update_message["likers"] += our_reference
+
+ if("teach_tricks")
+ var/trick_name = params["trick_name"]
+ var/list/trick_sequence = params["tricks"]
+ if(isnull(pet.ai_controller))
+ return TRUE
+ if(!isnull(trick_name))
+ pet.ai_controller.set_blackboard_key(BB_TRICK_NAME, trick_name)
+ pet.ai_controller.override_blackboard_key(BB_TRICK_SEQUENCE, trick_sequence)
+ playsound(computer.loc, 'sound/items/orbie_trick_learned.ogg', 50)
+
+ return TRUE
+
+/datum/computer_file/program/virtual_pet/proc/generate_petfeed_area()
+ if(!COOLDOWN_FINISHED(src, area_reroll))
+ return
+ var/list/filter_area_list = typecache_filter_list(GLOB.the_station_areas, restricted_areas)
+ var/list/target_area_list = GLOB.the_station_areas.Copy() - filter_area_list
+ if(!length(target_area_list))
+ return
+ selected_area = pick(target_area_list)
+ COOLDOWN_START(src, area_reroll, 2 MINUTES)
+
+/datum/computer_file/program/virtual_pet/proc/drop_feed()
+ if(!istype(get_area(computer), selected_area))
+ return
+ announce_global_updates(message = "has found a chocolate at [selected_area.name]")
+ selected_area = null
+ var/obj/item/food/virtual_chocolate/chocolate = new(get_turf(computer))
+ chocolate.AddElement(/datum/element/temporary_atom, life_time = 30 SECONDS) //we cant maintain its existence for too long!
+
+/datum/computer_file/program/virtual_pet/proc/recall_pet()
+ animate(pet, transform = matrix().Scale(0.3, 0.3), time = 1.5 SECONDS)
+ addtimer(CALLBACK(pet, TYPE_PROC_REF(/atom/movable, forceMove), computer), 1.5 SECONDS)
+
+/datum/computer_file/program/virtual_pet/proc/release_pet(mob/living/our_user)
+ var/turf/drop_zone
+ var/list/turfs_list = get_adjacent_open_turfs(computer.drop_location())
+ for(var/turf/possible_turf as anything in turfs_list)
+ if(possible_turf.is_blocked_turf())
+ continue
+ drop_zone = possible_turf
+ break
+ var/turf/final_turf = isnull(drop_zone) ? computer.drop_location() : drop_zone
+ pet.befriend(our_user) //befriend whoever set us out
+ animate(pet, transform = matrix(), time = 1.5 SECONDS)
+ pet.forceMove(final_turf)
+ playsound(computer.loc, 'sound/items/orbie_send_out.ogg', 20)
+ new /obj/effect/temp_visual/guardian/phase(pet.loc)
+
+#undef PET_MAX_LEVEL
+#undef PET_MAX_STEPS_RECORD
+#undef PET_EAT_BONUS
+#undef PET_CLEAN_BONUS
+#undef PET_PLAYMATE_BONUS
+#undef PET_STATE_HUNGRY
+#undef PET_STATE_ASLEEP
+#undef PET_STATE_HAPPY
+#undef PET_STATE_NEUTRAL
+#undef MAX_UPDATE_LENGTH
diff --git a/code/modules/paperwork/photocopier.dm b/code/modules/paperwork/photocopier.dm
index e5a30474f8721..4b495ae9148ae 100644
--- a/code/modules/paperwork/photocopier.dm
+++ b/code/modules/paperwork/photocopier.dm
@@ -199,7 +199,7 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
else
to_chat(usr, span_notice("You feel kind of silly, copying [ass]\'s ass with [ass.p_their()] clothes on."))
return FALSE
- do_copies(CALLBACK(src, PROC_REF(make_ass_copy), usr), usr, ASS_PAPER_USE, ASS_TONER_USE, num_copies)
+ do_copies(CALLBACK(src, PROC_REF(make_ass_copy)), usr, ASS_PAPER_USE, ASS_TONER_USE, num_copies)
return TRUE
else
// Basic paper
@@ -489,24 +489,13 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
* Calls `check_ass()` first to make sure that `ass` exists, among other conditions. Since this proc is called from a timer, it's possible that it was removed.
* Additionally checks that the mob has their clothes off.
*/
-/obj/machinery/photocopier/proc/make_ass_copy(mob/user)
+/obj/machinery/photocopier/proc/make_ass_copy()
if(!check_ass())
return null
- var/icon/temp_img
- if(ishuman(ass))
- var/mob/living/carbon/human/H = ass
- var/datum/species/spec = H.dna.species
- if(spec.ass_image)
- temp_img = icon(spec.ass_image)
- else
- temp_img = icon(ass.gender == FEMALE ? 'icons/ass/assfemale.png' : 'icons/ass/assmale.png')
- else if(isalienadult(ass)) //Xenos have their own asses, thanks to Pybro.
- temp_img = icon('icons/ass/assalien.png')
- else if(issilicon(ass))
- temp_img = icon('icons/ass/assmachine.png')
- else if(isdrone(ass)) //Drones are hot
- temp_img = icon('icons/ass/assdrone.png')
-
+ var/butt_icon_state = ass.get_butt_sprite()
+ if(isnull(butt_icon_state))
+ return null
+ var/icon/temp_img = icon('icons/mob/butts.dmi', butt_icon_state)
var/obj/item/photo/copied_ass = new /obj/item/photo(src)
var/datum/picture/toEmbed = new(name = "[ass]'s Ass", desc = "You see [ass]'s ass on the photo.", image = temp_img)
toEmbed.psize_x = 128
@@ -629,7 +618,7 @@ GLOBAL_LIST_INIT(paper_blanks, init_paper_blanks())
* Returns FALSE if `ass` doesn't exist or is not at the copier's location. Returns TRUE otherwise.
*/
/obj/machinery/photocopier/proc/check_ass() //I'm not sure wether I made this proc because it's good form or because of the name.
- if(!ass)
+ if(!isliving(ass))
return FALSE
if(ass.loc != loc)
ass = null
diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm
index 3f721c1cefc3b..4bdb1c4d93aa8 100644
--- a/code/modules/photography/camera/camera.dm
+++ b/code/modules/photography/camera/camera.dm
@@ -229,7 +229,7 @@
var/datum/picture/picture = new("picture", desc.Join(" "), mobs_spotted, dead_spotted, names, get_icon, null, psize_x, psize_y, blueprints, can_see_ghosts = see_ghosts)
after_picture(user, picture)
- SEND_SIGNAL(src, COMSIG_CAMERA_IMAGE_CAPTURED, target, user)
+ SEND_SIGNAL(src, COMSIG_CAMERA_IMAGE_CAPTURED, target, user, picture)
blending = FALSE
return picture
diff --git a/code/modules/power/monitor.dm b/code/modules/power/monitor.dm
index 32e461ba8f8a0..3d4c92d8b19e2 100644
--- a/code/modules/power/monitor.dm
+++ b/code/modules/power/monitor.dm
@@ -8,7 +8,6 @@
light_color = LIGHT_COLOR_DIM_YELLOW
use_power = ACTIVE_POWER_USE
circuit = /obj/item/circuitboard/computer/powermonitor
- tgui_id = "PowerMonitor"
var/datum/weakref/attached_wire_ref
var/datum/weakref/local_apc_ref
diff --git a/code/modules/projectiles/boxes_magazines/internal/shotgun.dm b/code/modules/projectiles/boxes_magazines/internal/shotgun.dm
index dfd99e24766f2..3b2489022ea45 100644
--- a/code/modules/projectiles/boxes_magazines/internal/shotgun.dm
+++ b/code/modules/projectiles/boxes_magazines/internal/shotgun.dm
@@ -13,6 +13,12 @@
/obj/item/ammo_box/magazine/internal/shot/tube/fire
ammo_type = /obj/projectile/bullet/incendiary/shotgun/no_trail
+/obj/item/ammo_box/magazine/internal/shot/tube/buckshot
+ ammo_type = /obj/item/ammo_casing/shotgun/buckshot
+
+/obj/item/ammo_box/magazine/internal/shot/tube/slug
+ ammo_type = /obj/item/ammo_casing/shotgun
+
/obj/item/ammo_box/magazine/internal/shot/lethal
ammo_type = /obj/item/ammo_casing/shotgun/buckshot
diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm
index 69668cfaf40d0..841629f5e38cf 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -264,8 +264,7 @@
casing.bounce_away(TRUE)
SEND_SIGNAL(casing, COMSIG_CASING_EJECTED)
else if(empty_chamber)
- UnregisterSignal(chambered, COMSIG_MOVABLE_MOVED)
- chambered = null
+ clear_chambered()
if (chamber_next_round && (magazine?.max_ammo > 1))
chamber_round()
diff --git a/code/modules/projectiles/guns/ballistic/automatic.dm b/code/modules/projectiles/guns/ballistic/automatic.dm
index 70e2210a4e992..4162ca9890f2f 100644
--- a/code/modules/projectiles/guns/ballistic/automatic.dm
+++ b/code/modules/projectiles/guns/ballistic/automatic.dm
@@ -159,7 +159,7 @@
/obj/item/gun/ballistic/automatic/m90
name = "\improper M-90gl Carbine"
- desc = "A three-round burst 5.56 toploading carbine, designated 'M-90gl'. Has an attached underbarrel grenade launcher."
+ desc = "A three-round burst .223 toploading carbine, designated 'M-90gl'. Has an attached underbarrel grenade launcher."
desc_controls = "Right-click to use grenade launcher."
icon_state = "m90"
w_class = WEIGHT_CLASS_BULKY
diff --git a/code/modules/projectiles/guns/ballistic/shotgun.dm b/code/modules/projectiles/guns/ballistic/shotgun.dm
index 8a6f15e9a981d..4e81b1e585638 100644
--- a/code/modules/projectiles/guns/ballistic/shotgun.dm
+++ b/code/modules/projectiles/guns/ballistic/shotgun.dm
@@ -97,6 +97,10 @@
desc = "An advanced shotgun with two separate magazine tubes. This one shows signs of bounty hunting customization, meaning it likely has a dual rubber shot/fire slug load."
alt_mag_type = /obj/item/ammo_box/magazine/internal/shot/tube/fire
+/obj/item/gun/ballistic/shotgun/automatic/dual_tube/deadly
+ spawn_magazine_type = /obj/item/ammo_box/magazine/internal/shot/tube/buckshot
+ alt_mag_type = /obj/item/ammo_box/magazine/internal/shot/tube/slug
+
/obj/item/gun/ballistic/shotgun/automatic/dual_tube/examine(mob/user)
. = ..()
. += span_notice("Alt-click to pump it.")
diff --git a/code/modules/projectiles/pins.dm b/code/modules/projectiles/pins.dm
index c4b6f6fb4ce7e..6f80bf0e21435 100644
--- a/code/modules/projectiles/pins.dm
+++ b/code/modules/projectiles/pins.dm
@@ -387,4 +387,5 @@
/obj/item/firing_pin/Destroy()
if(gun)
gun.pin = null
+ gun = null
return ..()
diff --git a/code/modules/reagents/chemistry/machinery/reagentgrinder.dm b/code/modules/reagents/chemistry/machinery/reagentgrinder.dm
index e7a6c9839eb7e..6641e63520e00 100644
--- a/code/modules/reagents/chemistry/machinery/reagentgrinder.dm
+++ b/code/modules/reagents/chemistry/machinery/reagentgrinder.dm
@@ -27,6 +27,7 @@
holdingitems = list()
beaker = new /obj/item/reagent_containers/cup/beaker/large(src)
warn_of_dust()
+ RegisterSignal(src, COMSIG_STORAGE_DUMP_CONTENT, PROC_REF(on_storage_dump))
/// Add a description to the current beaker warning of blended dust, if it doesn't already have that warning.
/obj/machinery/reagentgrinder/proc/warn_of_dust()
@@ -205,6 +206,24 @@
holdingitems[weapon] = TRUE
return FALSE
+/obj/machinery/reagentgrinder/proc/on_storage_dump(datum/source, datum/storage/storage, mob/user)
+ SIGNAL_HANDLER
+
+ for(var/obj/item/to_dump in storage.real_location)
+ if(holdingitems.len >= limit)
+ break
+
+ if(!to_dump.grind_results && !to_dump.juice_typepath)
+ continue
+
+ if(!storage.attempt_remove(to_dump, src, silent = TRUE))
+ continue
+
+ holdingitems[to_dump] = TRUE
+
+ to_chat(user, span_notice("You dump [storage.parent] into [src]."))
+ return STORAGE_DUMP_HANDLED
+
/obj/machinery/reagentgrinder/ui_interact(mob/user) // The microwave Menu //I am reasonably certain that this is not a microwave
. = ..()
diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
index 6363a9766a35a..d200ee2dc38f8 100644
--- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm
@@ -785,6 +785,24 @@
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
addiction_types = list(/datum/addiction/stimulants = 20)
+/datum/reagent/drug/kronkaine/on_new(data)
+ . = ..()
+ // Kronkaine also makes for a great fishing bait (found in "natural" baits)
+ if(!istype(holder?.my_atom, /obj/item/food))
+ return
+ ADD_TRAIT(holder.my_atom, TRAIT_GREAT_QUALITY_BAIT, type)
+ RegisterSignal(holder, COMSIG_REAGENTS_CLEAR_REAGENTS, PROC_REF(on_reagents_clear))
+ RegisterSignal(holder, COMSIG_REAGENTS_DEL_REAGENT, PROC_REF(on_reagent_delete))
+
+/datum/reagent/drug/kronkaine/proc/on_reagents_clear(datum/reagents/reagents)
+ SIGNAL_HANDLER
+ REMOVE_TRAIT(holder.my_atom, TRAIT_GREAT_QUALITY_BAIT, type)
+
+/datum/reagent/drug/kronkaine/proc/on_reagent_delete(datum/reagents/reagents, datum/reagent/deleted_reagent)
+ SIGNAL_HANDLER
+ if(deleted_reagent == src)
+ REMOVE_TRAIT(holder.my_atom, TRAIT_GREAT_QUALITY_BAIT, type)
+
/datum/reagent/drug/kronkaine/on_mob_metabolize(mob/living/kronkaine_fiend)
. = ..()
kronkaine_fiend.add_actionspeed_modifier(/datum/actionspeed_modifier/kronkaine)
diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm
index e33a1a7f2ab28..00c7dba3946bd 100644
--- a/code/modules/research/designs/weapon_designs.dm
+++ b/code/modules/research/designs/weapon_designs.dm
@@ -204,6 +204,19 @@
departmental_flags = DEPARTMENT_BITFLAG_SECURITY
autolathe_exportable = FALSE
+/datum/design/ballistic_shield
+ name = "Ballistic Shield"
+ desc = "A heavy shield designed for blocking projectiles, weaker to melee."
+ id = "ballistic_shield"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 2, /datum/material/titanium =SHEET_MATERIAL_AMOUNT)
+ build_path = /obj/item/shield/ballistic
+ category = list(
+ RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_MELEE
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SECURITY
+ autolathe_exportable = FALSE
+
/datum/design/beamrifle
name = "Beam Marksman Rifle Part Kit (Lethal)"
desc = "The gunkit for a powerful long ranged anti-material rifle that fires charged particle beams to obliterate targets."
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index acf17851eb34a..66fb0a8caa3bd 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -1625,6 +1625,7 @@
description = "Our researchers have found new ways to weaponize just about everything now."
prereq_ids = list("engineering")
design_ids = list(
+ "ballistic_shield",
"pin_testing",
"tele_shield",
"lasershell",
diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm
index ead03aa0f707c..22450ca793d7b 100644
--- a/code/modules/surgery/bodyparts/parts.dm
+++ b/code/modules/surgery/bodyparts/parts.dm
@@ -69,6 +69,15 @@
cavity_item = null
return ..()
+/// Sprite to show for photocopying mob butts
+/obj/item/bodypart/chest/proc/get_butt_sprite()
+ if(!ishuman(owner))
+ return null
+ var/mob/living/carbon/human/human_owner = owner
+ var/butt_sprite = human_owner.physique == FEMALE ? BUTT_SPRITE_HUMAN_FEMALE : BUTT_SPRITE_HUMAN_MALE
+ var/obj/item/organ/external/tail/tail = human_owner.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAIL)
+ return tail?.get_butt_sprite() || butt_sprite
+
/obj/item/bodypart/chest/monkey
icon = 'icons/mob/human/species/monkey/bodyparts.dmi'
icon_static = 'icons/mob/human/species/monkey/bodyparts.dmi'
diff --git a/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
index 8dccd4b4a2d5b..e6fffe1e6f91b 100644
--- a/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/lizard_bodyparts.dm
@@ -10,6 +10,9 @@
is_dimorphic = TRUE
wing_types = list(/obj/item/organ/external/wings/functional/dragon)
+/obj/item/bodypart/chest/lizard/get_butt_sprite()
+ return BUTT_SPRITE_LIZARD
+
/obj/item/bodypart/arm/left/lizard
icon_greyscale = 'icons/mob/human/species/lizard/bodyparts.dmi'
limb_id = SPECIES_LIZARD
diff --git a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
index 83758f920c4cb..6a83385b1a36e 100644
--- a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm
@@ -60,6 +60,9 @@
should_draw_greyscale = FALSE
wing_types = NONE
+/obj/item/bodypart/chest/abductor/get_butt_sprite()
+ return BUTT_SPRITE_GREY
+
/obj/item/bodypart/arm/left/abductor
limb_id = SPECIES_ABDUCTOR
should_draw_greyscale = FALSE
@@ -95,6 +98,9 @@
burn_modifier = 0.5 // = 1/2x generic burn damage
wing_types = list(/obj/item/organ/external/wings/functional/slime)
+/obj/item/bodypart/chest/jelly/get_butt_sprite()
+ return BUTT_SPRITE_SLIME
+
/obj/item/bodypart/arm/left/jelly
biological_state = (BIO_FLESH|BIO_BLOODED)
limb_id = SPECIES_JELLYPERSON
@@ -230,6 +236,9 @@
burn_modifier = 1.25
wing_types = NONE
+/obj/item/bodypart/chest/pod/get_butt_sprite()
+ return BUTT_SPRITE_FLOWERPOT
+
/obj/item/bodypart/arm/left/pod
limb_id = SPECIES_PODPERSON
unarmed_attack_verb = "slash"
diff --git a/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm
index faed53371af60..be8d601b1fb58 100644
--- a/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/moth_bodyparts.dm
@@ -16,6 +16,9 @@
should_draw_greyscale = FALSE
wing_types = list(/obj/item/organ/external/wings/functional/moth/megamoth, /obj/item/organ/external/wings/functional/moth/mothra)
+/obj/item/bodypart/chest/moth/get_butt_sprite()
+ return BUTT_SPRITE_FUZZY
+
/obj/item/bodypart/arm/left/moth
icon = 'icons/mob/human/species/moth/bodyparts.dmi'
icon_state = "moth_l_arm"
diff --git a/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm
index 8ba27c2cdf9d0..40bf4a51c042e 100644
--- a/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/species_parts/plasmaman_bodyparts.dm
@@ -26,6 +26,9 @@
bodypart_flags = BODYPART_UNHUSKABLE
wing_types = NONE
+/obj/item/bodypart/chest/plasmaman/get_butt_sprite()
+ return BUTT_SPRITE_PLASMA
+
/obj/item/bodypart/arm/left/plasmaman
icon = 'icons/mob/human/species/plasmaman/bodyparts.dmi'
icon_state = "plasmaman_l_arm"
diff --git a/code/modules/surgery/organs/autosurgeon.dm b/code/modules/surgery/organs/autosurgeon.dm
index b577b9f8ec048..a2cf91c72f5e9 100644
--- a/code/modules/surgery/organs/autosurgeon.dm
+++ b/code/modules/surgery/organs/autosurgeon.dm
@@ -177,3 +177,8 @@
/obj/item/autosurgeon/syndicate/emaggedsurgerytoolset
starting_organ = /obj/item/organ/internal/cyberimp/arm/surgery/emagged
+
+/obj/item/autosurgeon/syndicate/contraband_sechud
+ desc = "Contains a contraband SecHUD implant, undetectable by health scanners."
+ uses = 1
+ starting_organ = /obj/item/organ/internal/cyberimp/eyes/hud/security/syndicate
diff --git a/code/modules/surgery/organs/external/tails.dm b/code/modules/surgery/organs/external/tails.dm
index 17be616ba7935..4234ca9b5aed4 100644
--- a/code/modules/surgery/organs/external/tails.dm
+++ b/code/modules/surgery/organs/external/tails.dm
@@ -126,6 +126,9 @@
UnregisterSignal(organ_owner, COMSIG_LIVING_DEATH)
return succeeded
+/obj/item/organ/external/tail/proc/get_butt_sprite()
+ return null
+
///Tail parent type, with wagging functionality
/datum/bodypart_overlay/mutant/tail
layers = EXTERNAL_FRONT|EXTERNAL_BEHIND
@@ -147,6 +150,9 @@
wag_flags = WAG_ABLE
+/obj/item/organ/external/tail/cat/get_butt_sprite()
+ return BUTT_SPRITE_CAT
+
///Cat tail bodypart overlay
/datum/bodypart_overlay/mutant/tail/cat
feature_key = "tail_cat"
diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm b/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm
index cec0241ece34c..cd9de70c4e23e 100644
--- a/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm
+++ b/code/modules/surgery/organs/internal/cyberimp/augments_eyes.dm
@@ -16,7 +16,7 @@
var/HUD_type = 0
var/HUD_trait = null
/// Whether the HUD implant is on or off
- var/toggled_on = TRUE
+ var/toggled_on = TRUE
/obj/item/organ/internal/cyberimp/eyes/hud/proc/toggle_hud(mob/living/carbon/eye_owner)
@@ -25,11 +25,13 @@
var/datum/atom_hud/hud = GLOB.huds[HUD_type]
hud.hide_from(eye_owner)
toggled_on = FALSE
+ balloon_alert(eye_owner, "hud disabled")
else
if(HUD_type)
var/datum/atom_hud/hud = GLOB.huds[HUD_type]
hud.show_to(eye_owner)
toggled_on = TRUE
+ balloon_alert(eye_owner, "hud enabled")
/obj/item/organ/internal/cyberimp/eyes/hud/Insert(mob/living/carbon/eye_owner, special = FALSE, movement_flags)
. = ..()
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 61bc9ec6d4e6e..46b2470d647f4 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -247,6 +247,7 @@
#include "spell_mindswap.dm"
#include "spell_names.dm"
#include "spell_shapeshift.dm"
+#include "spies.dm"
#include "spritesheets.dm"
#include "stack_singular_name.dm"
#include "station_trait_tests.dm"
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_spy.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spy.png
new file mode 100644
index 0000000000000..103e9d60faf7c
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spy.png differ
diff --git a/code/modules/unit_tests/spies.dm b/code/modules/unit_tests/spies.dm
new file mode 100644
index 0000000000000..b4b1add333cb6
--- /dev/null
+++ b/code/modules/unit_tests/spies.dm
@@ -0,0 +1,41 @@
+/// Tests spy bounty setup
+/datum/unit_test/spy_bounty
+
+/datum/unit_test/spy_bounty/Run()
+ var/mob/living/carbon/human/james_bond = allocate(/mob/living/carbon/human/consistent)
+ james_bond.mind_initialize()
+ james_bond.equipOutfit(/datum/outfit/job/assistant/consistent)
+ var/datum/antagonist/spy/spy = james_bond.mind.add_antag_datum(/datum/antagonist/spy)
+
+ var/datum/component/spy_uplink/uplink = spy.uplink_weakref?.resolve()
+ TEST_ASSERT_NOTNULL(uplink, "Spy failed to be given an uplink!")
+
+ var/datum/spy_bounty_handler/handler = uplink.handler
+ handler.num_attempts_override = 100
+
+ for(var/difficulty in handler.possible_uplink_items)
+ var/list/loot_pool = handler.possible_uplink_items[difficulty]
+ if(!length(loot_pool))
+ TEST_FAIL("No rewards generated for spy bounty difficulty [difficulty]")
+
+ for(var/difficulty in UNLINT(handler.bounty_types))
+ var/list/bounty_type_pool = UNLINT(handler.bounty_types[difficulty])
+ if(!length(bounty_type_pool))
+ TEST_FAIL("No bounty types for spy bounty difficulty [difficulty] found")
+
+ for(var/difficulty in UNLINT(handler.bounties))
+ var/list/generated_bounties = UNLINT(handler.bounties[difficulty])
+ if(difficulty == SPY_DIFFICULTY_HARD)
+ if(length(generated_bounties))
+ TEST_FAIL("No [difficulty] bounties should not be generated on initial refresh!")
+
+ else
+ if(!length(generated_bounties))
+ TEST_FAIL("No bounties were generated on initial refresh for difficulty [difficulty]")
+
+ handler.force_refresh()
+
+ for(var/difficulty in UNLINT(handler.bounties))
+ var/list/generated_bounties = UNLINT(handler.bounties[difficulty])
+ if(!length(generated_bounties))
+ TEST_FAIL("No bounties were generated on first refresh for difficulty [difficulty]")
diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm
index 65935f077e33d..bb76564e42c46 100644
--- a/code/modules/uplink/uplink_items.dm
+++ b/code/modules/uplink/uplink_items.dm
@@ -149,6 +149,34 @@
SEND_SIGNAL(uplink_handler, COMSIG_ON_UPLINK_PURCHASE, spawned_item, user)
return spawned_item
+/// Used to create the uplink's item for generic use, rather than use by a Syndie specifically
+/// Can be used to "de-restrict" some items, such as Nukie guns spawning with Syndicate pins
+/datum/uplink_item/proc/spawn_item_for_generic_use(mob/user)
+ var/atom/movable/created = new item(user.loc)
+
+ if(isgun(created))
+ replace_pin(created)
+ else if(istype(created, /obj/item/storage/toolbox/guncase))
+ for(var/obj/item/gun/gun in created)
+ replace_pin(gun)
+
+ if(isobj(created))
+ var/obj/created_obj = created
+ LAZYREMOVE(created_obj.req_access, ACCESS_SYNDICATE)
+ LAZYREMOVE(created_obj.req_one_access, ACCESS_SYNDICATE)
+
+ return created
+
+/// Used by spawn_item_for_generic_use to replace the pin of a gun with a normal one
+/datum/uplink_item/proc/replace_pin(obj/item/gun/gun_reward)
+ PRIVATE_PROC(TRUE)
+
+ if(!istype(gun_reward.pin, /obj/item/firing_pin/implant/pindicate))
+ return
+
+ QDEL_NULL(gun_reward.pin)
+ gun_reward.pin = new /obj/item/firing_pin(gun_reward)
+
///For special overrides if an item can be bought or not.
/datum/uplink_item/proc/can_be_bought(datum/uplink_handler/source)
return TRUE
@@ -168,6 +196,7 @@
//Discounts (dynamically filled above)
/datum/uplink_item/discounts
category = /datum/uplink_category/discounts
+ purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY // Probably not necessary but just in case
// Special equipment (Dynamically fills in uplink component)
/datum/uplink_item/special_equipment
@@ -176,6 +205,7 @@
desc = "Equipment necessary for accomplishing specific objectives. If you are seeing this, something has gone wrong."
limited_stock = 1
illegal_tech = FALSE
+ purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY // Ditto
/datum/uplink_item/special_equipment/purchase(mob/user, datum/component/uplink/U)
..()
diff --git a/code/modules/uplink/uplink_items/ammunition.dm b/code/modules/uplink/uplink_items/ammunition.dm
index e88727812528d..5326880d31be6 100644
--- a/code/modules/uplink/uplink_items/ammunition.dm
+++ b/code/modules/uplink/uplink_items/ammunition.dm
@@ -53,5 +53,5 @@
For when you really need a lot of things dead."
item = /obj/item/ammo_box/a357
cost = 4
- purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) //nukies get their own version
+ purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY) //nukies get their own version
illegal_tech = FALSE
diff --git a/code/modules/uplink/uplink_items/bundle.dm b/code/modules/uplink/uplink_items/bundle.dm
index f236aa4da253a..b708af62b69c9 100644
--- a/code/modules/uplink/uplink_items/bundle.dm
+++ b/code/modules/uplink/uplink_items/bundle.dm
@@ -7,11 +7,12 @@
category = /datum/uplink_category/bundle
surplus = 0
cant_discount = TRUE
+ purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY
/datum/uplink_item/bundles_tc/random
name = "Random Item"
desc = "Picking this will purchase a random item. Useful if you have some TC to spare or if you haven't decided on a strategy yet."
- item = /obj/effect/gibspawner/generic // non-tangible item because techwebs use this path to determine illegal tech
+ item = ABSTRACT_UPLINK_ITEM
cost = 0
cost_override_string = "Varies"
@@ -61,7 +62,7 @@
item = /obj/item/storage/box/syndicate/bundle_a
cost = 20
stock_key = UPLINK_SHARED_STOCK_KITS
- purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
+ purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY)
/datum/uplink_item/bundles_tc/bundle_b
name = "Syndi-kit Special"
@@ -72,7 +73,7 @@
item = /obj/item/storage/box/syndicate/bundle_b
cost = 20
stock_key = UPLINK_SHARED_STOCK_KITS
- purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
+ purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY)
/datum/uplink_item/bundles_tc/surplus
name = "Syndicate Surplus Crate"
@@ -81,7 +82,7 @@
Contents are sorted to always be worth 30 TC. The Syndicate will only provide one surplus item per agent."
item = /obj/structure/closet/crate // will be replaced in purchase()
cost = 20
- purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
+ purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY)
stock_key = UPLINK_SHARED_STOCK_SURPLUS
/// Value of items inside the crate in TC
var/crate_tc_value = 30
@@ -170,5 +171,5 @@
The Syndicate will only provide one surplus item per agent."
cost = 20
item = /obj/item/syndicrate_key
- purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
+ purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY)
stock_key = UPLINK_SHARED_STOCK_SURPLUS
diff --git a/code/modules/uplink/uplink_items/clownops.dm b/code/modules/uplink/uplink_items/clownops.dm
index 852676dbcbb74..56c11fedc0cb8 100644
--- a/code/modules/uplink/uplink_items/clownops.dm
+++ b/code/modules/uplink/uplink_items/clownops.dm
@@ -8,7 +8,7 @@
cost = 10
item = /obj/item/pneumatic_cannon/pie/selfcharge
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/weapon_kits/bananashield
name = "Bananium Energy Shield"
@@ -18,7 +18,7 @@
item = /obj/item/shield/energy/bananium
cost = 16
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/weapon_kits/clownsword
name = "Bananium Energy Sword"
@@ -27,7 +27,7 @@
item = /obj/item/melee/energy/sword/bananium
cost = 3
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/weapon_kits/clownoppin
name = "Ultra Hilarious Firing Pin"
@@ -51,7 +51,7 @@
item = /obj/item/gun/ballistic/automatic/c20r/toy
cost = 5
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/weapon_kits/foammachinegun
name = "Toy Machine Gun"
@@ -60,7 +60,7 @@
item = /obj/item/gun/ballistic/automatic/l6_saw/toy
cost = 10
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/explosives/bombanana
name = "Bombanana"
@@ -69,7 +69,7 @@
item = /obj/item/food/grown/banana/bombanana
cost = 4 //it is a bit cheaper than a minibomb because you have to take off your helmet to eat it, which is how you arm it
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/explosives/clown_bomb_clownops
name = "Clown Bomb"
@@ -81,7 +81,7 @@
item = /obj/item/sbeacondrop/clownbomb
cost = 15
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/explosives/clown_bomb_clownops/New()
. = ..()
@@ -94,7 +94,7 @@
item = /obj/item/grenade/chem_grenade/teargas/moustache
cost = 3
surplus = 0
- purchasable_from = UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/explosives/pinata
name = "Weapons Grade Pinata Kit"
@@ -158,4 +158,3 @@
cost = 1
purchasable_from = UPLINK_CLOWN_OPS
illegal_tech = FALSE
-
diff --git a/code/modules/uplink/uplink_items/contractor.dm b/code/modules/uplink/uplink_items/contractor.dm
index 6004caf97452e..7d261410e314d 100644
--- a/code/modules/uplink/uplink_items/contractor.dm
+++ b/code/modules/uplink/uplink_items/contractor.dm
@@ -13,7 +13,7 @@
item = /obj/item/storage/box/syndicate/contract_kit
category = /datum/uplink_category/contractor
cost = 20
- purchasable_from = ~(UPLINK_CLOWN_OPS | UPLINK_NUKE_OPS | UPLINK_TRAITORS)
+ purchasable_from = UPLINK_INFILTRATORS
/datum/uplink_item/bundles_tc/contract_kit/purchase(mob/user, datum/uplink_handler/uplink_handler, atom/movable/source)
. = ..()
@@ -36,7 +36,7 @@
name = "Contract Reroll"
desc = "Request a reroll of your current contract list. Will generate a new target, \
payment, and dropoff for the contracts you currently have available."
- item = /obj/effect/gibspawner/generic
+ item = ABSTRACT_UPLINK_ITEM
limited_stock = 2
cost = 0
diff --git a/code/modules/uplink/uplink_items/device_tools.dm b/code/modules/uplink/uplink_items/device_tools.dm
index 66cb58c7b2899..7f87d93464e48 100644
--- a/code/modules/uplink/uplink_items/device_tools.dm
+++ b/code/modules/uplink/uplink_items/device_tools.dm
@@ -134,7 +134,7 @@
/datum/uplink_item/device_tools/failsafe
name = "Failsafe Uplink Code"
desc = "When entered the uplink will self-destruct immediately."
- item = /obj/effect/gibspawner/generic
+ item = ABSTRACT_UPLINK_ITEM
cost = 1
surplus = 0
restricted = TRUE
diff --git a/code/modules/uplink/uplink_items/implant.dm b/code/modules/uplink/uplink_items/implant.dm
index 87c9fd6c96c07..a2b21574f6f34 100644
--- a/code/modules/uplink/uplink_items/implant.dm
+++ b/code/modules/uplink/uplink_items/implant.dm
@@ -49,6 +49,7 @@
// An empty uplink is kinda useless.
surplus = 0
restricted = TRUE
+ purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY
/datum/uplink_item/implants/uplink/spawn_item(spawn_path, mob/user, datum/uplink_handler/uplink_handler, atom/movable/source)
var/obj/item/storage/box/syndie_kit/uplink_box = ..()
diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm
index 3af8674e4fcf5..df4f235f50cca 100644
--- a/code/modules/uplink/uplink_items/job.dm
+++ b/code/modules/uplink/uplink_items/job.dm
@@ -28,7 +28,7 @@
/datum/uplink_item/role_restricted/bureaucratic_error
name = "Organic Capital Disturbance Virus"
desc = "Randomizes job positions presented to new hires. May lead to too many/too few security officers and/or clowns. Single use."
- item = /obj/effect/gibspawner/generic
+ item = ABSTRACT_UPLINK_ITEM
surplus = 0
limited_stock = 1
cost = 2
@@ -184,7 +184,7 @@
desc = "A bootleg copy of an collector item, this disk contains the procedure to perform advanced plastic surgery, allowing you to model someone's face and voice based on a picture taken by a camera on your offhand. \
All changes are superficial and does not change ones genetic makeup. \
Insert into an Operating Console to enable the procedure."
- item = /obj/item/disk/surgery/brainwashing
+ item = /obj/item/disk/surgery/advanced_plastic_surgery
restricted_roles = list(JOB_MEDICAL_DOCTOR, JOB_CHIEF_MEDICAL_OFFICER, JOB_ROBOTICIST)
cost = 1
surplus = 50
@@ -286,6 +286,13 @@
restricted_roles = list(JOB_CLOWN)
surplus = 10
+/datum/uplink_item/role_restricted/clowncar/spawn_item_for_generic_use(mob/user)
+ var/obj/vehicle/sealed/car/clowncar/car = ..()
+ car.enforce_clown_role = FALSE
+ var/obj/item/key = new car.key_type(user.loc)
+ car.visible_message(span_notice("[key] drops out of [car] onto the floor."))
+ return car
+
/datum/uplink_item/role_restricted/his_grace
name = "His Grace"
desc = "An incredibly dangerous weapon recovered from a station overcome by the grey tide. Once activated, He will thirst for blood and must be used to kill to sate that thirst. \
@@ -298,6 +305,7 @@
cost = 20
surplus = 0
restricted_roles = list(JOB_CHAPLAIN)
+ purchasable_from = ~UPLINK_SPY
/datum/uplink_item/role_restricted/concealed_weapon_bay
name = "Concealed Weapon Bay"
@@ -381,3 +389,4 @@
restricted_roles = list(JOB_MIME)
restricted = TRUE
refundable = FALSE
+ purchasable_from = parent_type::purchasable_from & ~UPLINK_SPY
diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm
index d8bead5da6781..127f17e84729b 100644
--- a/code/modules/uplink/uplink_items/nukeops.dm
+++ b/code/modules/uplink/uplink_items/nukeops.dm
@@ -76,26 +76,28 @@
name = "12g Buckshot Drum (Bulldog)"
desc = "An additional 8-round buckshot magazine for use with the Bulldog shotgun. Front towards enemy."
item = /obj/item/ammo_box/magazine/m12g
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/basic/slug
name = "12g Slug Drum (Bulldog)"
desc = "An additional 8-round slug magazine for use with the Bulldog shotgun. \
Now 8 times less likely to shoot your pals."
item = /obj/item/ammo_box/magazine/m12g/slug
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/incendiary/dragon
name = "12g Dragon's Breath Drum (Bulldog)"
desc = "An alternative 8-round dragon's breath magazine for use in the Bulldog shotgun. \
'I'm a fire starter, twisted fire starter!'"
item = /obj/item/ammo_box/magazine/m12g/dragon
- purchasable_from = UPLINK_NUKE_OPS
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/special/meteor
name = "12g Meteorslug Shells (Bulldog)"
desc = "An alternative 8-round meteorslug magazine for use in the Bulldog shotgun. \
Great for blasting holes into the hull and knocking down enemies."
item = /obj/item/ammo_box/magazine/m12g/meteor
- purchasable_from = UPLINK_NUKE_OPS
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
// ~~ Ansem Pistol ~~
@@ -109,24 +111,28 @@
name = "10mm Handgun Magazine (Ansem)"
desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol."
item = /obj/item/ammo_box/magazine/m10mm
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/ap/m10mm
name = "10mm Armour Piercing Magazine (Ansem)"
desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol. \
These rounds are less effective at injuring the target but penetrate protective gear."
item = /obj/item/ammo_box/magazine/m10mm/ap
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/hp/m10mm
name = "10mm Hollow Point Magazine (Ansem)"
desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol. \
These rounds are more damaging but ineffective against armour."
item = /obj/item/ammo_box/magazine/m10mm/hp
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/incendiary/m10mm
name = "10mm Incendiary Magazine (Ansem)"
desc = "An additional 8-round 10mm magazine, compatible with the Ansem pistol. \
Loaded with incendiary rounds which inflict less damage, but ignite the target."
item = /obj/item/ammo_box/magazine/m10mm/fire
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
//Medium-cost: 14 TC each. Meant for more expensive purchases with a goal in mind.
@@ -197,6 +203,7 @@
desc = "A speed loader that contains seven additional .357 Magnum rounds; usable with the Syndicate revolver. \
For when you really need a lot of things dead. Operatives get a discount from most of our agents!"
item = /obj/item/ammo_box/a357
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/special/revolver/phasic
name = ".357 Phasic Speed Loader (Revolver)"
@@ -204,6 +211,7 @@
These bullets are made from an experimental alloy, 'Ghost Lead', that allows it to pass through almost any non-organic material. \
The name is a misnomer. It doesn't contain any lead whatsoever!"
item = /obj/item/ammo_box/a357/phasic
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
/datum/uplink_item/ammo_nuclear/special/revolver/heartseeker
name = ".357 Heartseeker Speed Loader (Revolver)"
@@ -212,6 +220,7 @@
Brought to you by Roseus Galactic!"
item = /obj/item/ammo_box/a357/heartseeker
cost = 3
+ purchasable_from = parent_type::purchasable_from | UPLINK_SPY
// ~~ Grenade Launcher ~~
// 'If god had wanted you to live, he would not have created ME!'
@@ -591,7 +600,7 @@
desc = "An upgraded, elite version of the Syndicate MODsuit. It features fireproofing, and also \
provides the user with superior armor and mobility compared to the standard Syndicate MODsuit."
item = /obj/item/mod/control/pre_equipped/elite
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/suits/energy_shield
name = "MODsuit Energy Shield Module"
@@ -599,28 +608,28 @@
before needing to recharge. Used wisely, this module will keep you alive for a lot longer."
item = /obj/item/mod/module/energy_shield
cost = 8
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/suits/emp_shield
name = "MODsuit Advanced EMP Shield Module"
desc = "An advanced EMP shield module for a MODsuit. It protects your entire body from electromagnetic pulses."
item = /obj/item/mod/module/emp_shield/advanced
cost = 5
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/suits/injector
name = "MODsuit Injector Module"
desc = "An injector module for a MODsuit. It is an extendable piercing injector with 30u capacity."
item = /obj/item/mod/module/injector
cost = 2
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/suits/holster
name = "MODsuit Holster Module"
desc = "A holster module for a MODsuit. It can stealthily store any not too heavy gun inside it."
item = /obj/item/mod/module/holster
cost = 2
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/device_tools/medgun_mod
name = "Medbeam Gun Module"
@@ -665,7 +674,7 @@
In its crowbar configuration, it can be used to force open airlocks. Very useful for entering the station or its departments."
item = /obj/item/crowbar/power/syndicate
cost = 4
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY
/datum/uplink_item/device_tools/medkit
name = "Syndicate Combat Medic Kit"
@@ -692,7 +701,7 @@
desc = "A potion recovered at great risk by undercover Syndicate operatives and then subsequently modified with Syndicate technology. \
Using it will make any animal sentient, and bound to serve you, as well as implanting an internal radio for communication and an internal ID card for opening doors."
cost = 4
- purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY
restricted = TRUE
// Implants
@@ -717,6 +726,7 @@
This will permanently destroy your body, however."
item = /obj/item/storage/box/syndie_kit/imp_microbomb
cost = 2
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY
/datum/uplink_item/implants/nuclear/macrobomb
name = "Macrobomb Implant"
@@ -732,18 +742,21 @@
Prevents collapsing from critical condition, but explodes after a while."
item = /obj/item/storage/box/syndie_kit/imp_deniability
cost = 6
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY
/datum/uplink_item/implants/nuclear/reviverplus
name = "Reviver Implant"
desc = "This implant will attempt to revive and heal you if you lose consciousness. Comes with an autosurgeon."
item = /obj/item/autosurgeon/syndicate/reviver
cost = 8
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY
/datum/uplink_item/implants/nuclear/thermals
name = "Thermal Eyes"
desc = "These cybernetic eyes will give you thermal vision. Comes with a free autosurgeon."
item = /obj/item/autosurgeon/syndicate/thermal_eyes
cost = 8
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY
/datum/uplink_item/implants/nuclear/implants/xray
name = "X-ray Vision Implant"
@@ -756,6 +769,7 @@
desc = "This implant will help you get back up on your feet faster after being stunned. Comes with an autosurgeon."
item = /obj/item/autosurgeon/syndicate/anti_stun
cost = 8
+ purchasable_from = UPLINK_NUKE_OPS | UPLINK_SPY
// Badass (meme items)
diff --git a/code/modules/uplink/uplink_items/species.dm b/code/modules/uplink/uplink_items/species.dm
index 54ba353c00adb..5eb4bbdcb1776 100644
--- a/code/modules/uplink/uplink_items/species.dm
+++ b/code/modules/uplink/uplink_items/species.dm
@@ -4,7 +4,7 @@
/datum/uplink_item/species_restricted
category = /datum/uplink_category/species
- purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
+ purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_SPY)
/datum/uplink_item/species_restricted/moth_lantern
name = "Extra-Bright Lantern"
diff --git a/code/modules/uplink/uplink_items/spy_unique.dm b/code/modules/uplink/uplink_items/spy_unique.dm
new file mode 100644
index 0000000000000..2f9c4b32576dc
--- /dev/null
+++ b/code/modules/uplink/uplink_items/spy_unique.dm
@@ -0,0 +1,123 @@
+/datum/uplink_category/spy_unique
+ name = "Spy Unique"
+
+// This is solely for uplink items that the spy can randomly obtain via bounties.
+/datum/uplink_item/spy_unique
+ category = /datum/uplink_category/spy_unique
+ cant_discount = TRUE
+ surplus = FALSE
+ purchasable_from = UPLINK_SPY
+ // Cost doesn't really matter since it's free, but it determines which loot pool it falls into.
+ // By default, these fall into easy-medium spy bounty loot pool
+ cost = SPY_LOWER_COST_THRESHOLD
+
+/datum/uplink_item/spy_unique/syndie_bowman
+ name = "Syndicate Bowman"
+ desc = "A bowman headset for members of the Syndicate. Not very conspicuous."
+ item = /obj/item/radio/headset/syndicate/alt
+ cost = 1
+
+/datum/uplink_item/spy_unique/megaphone
+ name = "Megaphone"
+ desc = "A megaphone. It's loud."
+ item = /obj/item/megaphone
+ cost = 1
+
+/datum/uplink_item/spy_unique/combat_gloves
+ name = "Combat Gloves"
+ desc = "A pair of combat gloves. They're insulated!"
+ item = /obj/item/clothing/gloves/combat
+ cost = 1
+
+/datum/uplink_item/spy_unique/krav_maga
+ name = "Combat Gloves Plus"
+ desc = "A pair of combat gloves plus. They're insulated AND you can do martial arts with it!"
+ item = /obj/item/clothing/gloves/krav_maga/combatglovesplus
+
+/datum/uplink_item/spy_unique/tackle_gloves
+ name = "Guerrilla Gloves"
+ desc = "A pair of Guerrilla gloves. They're insulated AND you can tackle people with it!"
+ item = /obj/item/clothing/gloves/tackler/combat/insulated
+
+/datum/uplink_item/spy_unique/kudzu
+ name = "Kudzu"
+ desc = "A packet of Kudzu - plant and forget, a great distraction."
+ item = /obj/item/seeds/kudzu
+
+/datum/uplink_item/spy_unique/big_knife
+ name = "Combat Knife"
+ desc = "A big knife. It's sharp."
+ item = /obj/item/knife/combat
+
+/datum/uplink_item/spy_unique/switchblade
+ name = "Switchblade"
+ desc = "A switchblade. Switches between not sharp and sharp."
+ item = /obj/item/switchblade
+
+/datum/uplink_item/spy_unique/sechud_implant
+ name = "SecHUD Implant"
+ desc = "A SecHUD implant. Shows you the ID of people you're looking at. It's also stealthy!"
+ item = /obj/item/autosurgeon/syndicate/contraband_sechud
+
+/datum/uplink_item/spy_unique/rifle_prime
+ name = "Bolt-Action Rifle"
+ desc = "A bolt-action rifle, with a scope. Won't jam, either."
+ item = /obj/item/gun/ballistic/rifle/boltaction/prime
+ cost = SPY_UPPER_COST_THRESHOLD
+
+/datum/uplink_item/spy_unique/cycler_shotgun
+ name = "Cycler Shotgun"
+ desc = "A cycler shotgun. It's a shotgun that cycles between two barrels."
+ item = /obj/item/gun/ballistic/shotgun/automatic/dual_tube/deadly
+ cost = SPY_UPPER_COST_THRESHOLD
+
+/datum/uplink_item/spy_unique/bulldog_shotgun
+ name = "Bulldog Shotgun"
+ desc = "A bulldog shotgun. It's a shotgun that shoots bulldogs."
+ item = /obj/item/gun/ballistic/shotgun/bulldog/unrestricted
+ cost = SPY_UPPER_COST_THRESHOLD
+
+/datum/uplink_item/spy_unique/ansem_pistol
+ name = "Ansem Pistol"
+ desc = "A pistol that's really good at making people sleep."
+ item = /obj/item/gun/ballistic/automatic/pistol/clandestine
+ cost = SPY_UPPER_COST_THRESHOLD
+
+/datum/uplink_item/spy_unique/rocket_launcher
+ name = "Rocket Launcher"
+ desc = "A rocket launcher. I would recommend against jumping with it."
+ item = /obj/item/gun/ballistic/rocketlauncher
+ cost = SPY_UPPER_COST_THRESHOLD - 1 // It's a meme item
+
+/datum/uplink_item/spy_unique/shotgun_ammo
+ name = "Box of Buckshot"
+ desc = "A box of buckshot rounds for a shotgun. For when you don't want to miss."
+ item = /obj/item/storage/box/lethalshot
+ cost = 1
+
+/datum/uplink_item/spy_unique/shotgun_ammo/breacher_slug
+ name = "Box of Breacher Slugs"
+ desc = "A box of breacher slugs for a shotgun. For making a good first impression."
+ item = /obj/item/storage/box/breacherslug
+
+/datum/uplink_item/spy_unique/shotgun_ammo/slugs
+ name = "Box of Slugs"
+ desc = "A box of slugs for a shotgun. For big game hunting."
+ item = /obj/item/storage/box/slugs
+
+/datum/uplink_item/spy_unique/stealth_belt
+ name = "Stealth Belt"
+ desc = "A stealth belt that lets you sneak behind enemy lines."
+ item = /obj/item/shadowcloak/weaker
+ cost = SPY_UPPER_COST_THRESHOLD
+
+/datum/uplink_item/spy_unique/katana
+ name = "Katana"
+ desc = "A really sharp Katana. Did I mention it's sharp?"
+ item = /obj/item/katana
+ cost = /datum/uplink_item/dangerous/doublesword::cost // Puts it in the same pool as Desword
+
+/datum/uplink_item/spy_unique/medkit_lite
+ name = "Syndicate First Medic Kit"
+ desc = "A syndicate tactical combat medkit, but only stocked enough to do basic first aid."
+ item = /obj/item/storage/medkit/tactical_lite
diff --git a/code/modules/uplink/uplink_items/stealthy.dm b/code/modules/uplink/uplink_items/stealthy.dm
index 793120fe56f34..fb450fb68df93 100644
--- a/code/modules/uplink/uplink_items/stealthy.dm
+++ b/code/modules/uplink/uplink_items/stealthy.dm
@@ -102,4 +102,4 @@
cost = 7
surplus = 50
limited_stock = 1
- purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS | UPLINK_INFILTRATORS)
+ purchasable_from = UPLINK_TRAITORS | UPLINK_SPY
diff --git a/code/modules/uplink/uplink_items/stealthy_tools.dm b/code/modules/uplink/uplink_items/stealthy_tools.dm
index 60f007ebae772..59b8f6fca77e6 100644
--- a/code/modules/uplink/uplink_items/stealthy_tools.dm
+++ b/code/modules/uplink/uplink_items/stealthy_tools.dm
@@ -102,7 +102,7 @@
/datum/uplink_item/stealthy_tools/telecomm_blackout
name = "Disable Telecomms"
desc = "When purchased, a virus will be uploaded to the telecommunication processing servers to temporarily disable themselves."
- item = /obj/effect/gibspawner/generic
+ item = ABSTRACT_UPLINK_ITEM
surplus = 0
progression_minimum = 15 MINUTES
limited_stock = 1
@@ -117,7 +117,7 @@
/datum/uplink_item/stealthy_tools/blackout
name = "Trigger Stationwide Blackout"
desc = "When purchased, a virus will be uploaded to the engineering processing servers to force a routine power grid check, forcing all APCs on the station to be temporarily disabled."
- item = /obj/effect/gibspawner/generic
+ item = ABSTRACT_UPLINK_ITEM
surplus = 0
progression_minimum = 20 MINUTES
limited_stock = 1
diff --git a/html/changelogs/AutoChangeLog-pr-81632.yml b/html/changelogs/AutoChangeLog-pr-81632.yml
new file mode 100644
index 0000000000000..c238f2b55d22b
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-81632.yml
@@ -0,0 +1,4 @@
+author: "Rhials"
+delete-after: True
+changes:
+ - code_imp: "Splits up the nuclear operative antagonist datum folder."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81683.yml b/html/changelogs/AutoChangeLog-pr-81683.yml
new file mode 100644
index 0000000000000..3291a02543a4f
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-81683.yml
@@ -0,0 +1,4 @@
+author: "xXPawnStarrXx"
+delete-after: True
+changes:
+ - bugfix: "makes custom pizzas dairy and vegetable free, unless you choose to add them."
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-81766.yml b/html/changelogs/AutoChangeLog-pr-81766.yml
new file mode 100644
index 0000000000000..a1a2e78484f37
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-81766.yml
@@ -0,0 +1,4 @@
+author: "Rhials"
+delete-after: True
+changes:
+ - bugfix: "Fixes some tiles outside the Icebox AI satellite not getting hit by storms."
\ No newline at end of file
diff --git a/html/changelogs/archive/2024-03.yml b/html/changelogs/archive/2024-03.yml
new file mode 100644
index 0000000000000..114fb94aa285c
--- /dev/null
+++ b/html/changelogs/archive/2024-03.yml
@@ -0,0 +1,63 @@
+2024-03-01:
+ Ben10Omintrix:
+ - rscadd: new virtual pet app on the pda
+ Cheshify:
+ - spellcheck: changed the fitness skill title to powerlifter
+ Echriser:
+ - qol: Allows dragging from boxes into All-In-One Grinders
+ Ghommie:
+ - bugfix: Fixed (cross)bows' strings not loosening once fired.
+ - bugfix: the multi-dimensional bomb payload now works as intended and doesn't break
+ once you select a theme.
+ - bugfix: Meat and other bloody things will not spread blood forever.
+ Higgin:
+ - balance: the shadow eyes of nightmares and shadowpeople more broadly are now sensitive
+ to light, requiring additional protection.
+ Melbert:
+ - bugfix: Fixes grabbing yourself when you tackle someone.
+ Momo8289:
+ - qol: HUD implants will now notify you when toggled on or off
+ Pickle-Coding:
+ - admin: Logs holosign swatting.
+ TheSmallBlue:
+ - qol: added an HUD button to go up and down floors
+ bigfatbananacyclops:
+ - rscadd: box with a set of floortile camo, which can be ordered in black market
+ uplink
+ - rscadd: also adds a backpack to camouflage
+ - bugfix: i had the crate under emagged console, should be fixed now.
+ mc-oofert:
+ - rscadd: Pipebombs
+ - rscdel: Improvised Firebombs
+ necromanceranne:
+ - bugfix: The M-90GL now correctly states that it accepts .223 toploader magazines.
+ vinylspiders:
+ - bugfix: fixes an issue where being gibbed while under the HARS mutation can sometimes
+ lead to the brain being deleted when it's not supposed to be
+2024-03-02:
+ 13spacemen:
+ - refactor: Butt sprites are based on the chest bodypart for humans, instead of
+ the species
+ - image: Moths have their own special butt sprites
+ DrDiasyl:
+ - rscadd: Adds 2 new shields to the game! Ballistic Shield - researched by Science,
+ and Improvised Shield - made out of iron and sticky tape
+ - image: Riot, Strobe, Telescopic, Energy shields got new less flat sprites!
+ Ghommie:
+ - rscadd: Fishes love kronkaine.
+ - qol: Examining a fishing spot twice with sufficiently high fishing skill (or the
+ skillchip) will get you a list of fishes that can be caught.
+ Jacquerel:
+ - image: Bubblegum Hallucination Surround Charge, Wendigo Shockwave Scream, and
+ Ice Demon Floor Freeze all have more appropriate action icons.
+ - qol: Adds a tooltip to Ice Demon Afterimages ability.
+ - image: Cyborg view items now use the same sprites as their corresponding goggles
+ instead of old versions of those sprites.
+ JohnFulpWillard:
+ - bugfix: Buying the advanced plastic surgery disk from the uplink now gives you
+ advanced plastic surgery instead of brainwashing.
+ Melbert:
+ - rscadd: Spies may now roam the halls of Space Station 13. Watch your belongings
+ closely.
+ Rhials:
+ - bugfix: Pete can no longer eat vines while dead.
diff --git a/icons/ass/assalien.png b/icons/ass/assalien.png
deleted file mode 100644
index 7ac184aa04be0..0000000000000
Binary files a/icons/ass/assalien.png and /dev/null differ
diff --git a/icons/ass/asscat.png b/icons/ass/asscat.png
deleted file mode 100644
index e37788e3dd734..0000000000000
Binary files a/icons/ass/asscat.png and /dev/null differ
diff --git a/icons/ass/assdrone.png b/icons/ass/assdrone.png
deleted file mode 100644
index 8a679f87c904d..0000000000000
Binary files a/icons/ass/assdrone.png and /dev/null differ
diff --git a/icons/ass/assfemale.png b/icons/ass/assfemale.png
deleted file mode 100644
index 22da27b71c281..0000000000000
Binary files a/icons/ass/assfemale.png and /dev/null differ
diff --git a/icons/ass/assgrey.png b/icons/ass/assgrey.png
deleted file mode 100644
index 60dde099510cf..0000000000000
Binary files a/icons/ass/assgrey.png and /dev/null differ
diff --git a/icons/ass/asslizard.png b/icons/ass/asslizard.png
deleted file mode 100644
index 38d82d9754c53..0000000000000
Binary files a/icons/ass/asslizard.png and /dev/null differ
diff --git a/icons/ass/assmachine.png b/icons/ass/assmachine.png
deleted file mode 100644
index 2ba447306c47c..0000000000000
Binary files a/icons/ass/assmachine.png and /dev/null differ
diff --git a/icons/ass/assmale.png b/icons/ass/assmale.png
deleted file mode 100644
index d215bc31e0979..0000000000000
Binary files a/icons/ass/assmale.png and /dev/null differ
diff --git a/icons/ass/assplasma.png b/icons/ass/assplasma.png
deleted file mode 100644
index 30294f65124bb..0000000000000
Binary files a/icons/ass/assplasma.png and /dev/null differ
diff --git a/icons/ass/asspodperson.png b/icons/ass/asspodperson.png
deleted file mode 100644
index 50e5a8644ab58..0000000000000
Binary files a/icons/ass/asspodperson.png and /dev/null differ
diff --git a/icons/ass/assslime.png b/icons/ass/assslime.png
deleted file mode 100644
index 9102dce7de10e..0000000000000
Binary files a/icons/ass/assslime.png and /dev/null differ
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index fcb4e262d7c89..d0734355c5e3f 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/hud/screen_clockwork.dmi b/icons/hud/screen_clockwork.dmi
index aa815e957e4ae..0923e42e7e429 100644
Binary files a/icons/hud/screen_clockwork.dmi and b/icons/hud/screen_clockwork.dmi differ
diff --git a/icons/hud/screen_detective.dmi b/icons/hud/screen_detective.dmi
index d1d7e49a832d1..aed6e0d6572a5 100644
Binary files a/icons/hud/screen_detective.dmi and b/icons/hud/screen_detective.dmi differ
diff --git a/icons/hud/screen_glass.dmi b/icons/hud/screen_glass.dmi
index 29f8cb47bfd25..63ad3293921b8 100644
Binary files a/icons/hud/screen_glass.dmi and b/icons/hud/screen_glass.dmi differ
diff --git a/icons/hud/screen_midnight.dmi b/icons/hud/screen_midnight.dmi
index 9cfe8db9727c5..5483ddf4564a5 100644
Binary files a/icons/hud/screen_midnight.dmi and b/icons/hud/screen_midnight.dmi differ
diff --git a/icons/hud/screen_operative.dmi b/icons/hud/screen_operative.dmi
index b4b38782fe179..f2d60d394acc9 100644
Binary files a/icons/hud/screen_operative.dmi and b/icons/hud/screen_operative.dmi differ
diff --git a/icons/hud/screen_plasmafire.dmi b/icons/hud/screen_plasmafire.dmi
index 8225adbda6046..5423d3855b2b6 100644
Binary files a/icons/hud/screen_plasmafire.dmi and b/icons/hud/screen_plasmafire.dmi differ
diff --git a/icons/hud/screen_retro.dmi b/icons/hud/screen_retro.dmi
index a00d16cac5eb9..b4252109d6847 100644
Binary files a/icons/hud/screen_retro.dmi and b/icons/hud/screen_retro.dmi differ
diff --git a/icons/hud/screen_slimecore.dmi b/icons/hud/screen_slimecore.dmi
index b7e3a87e07ae9..a75fe55c37839 100644
Binary files a/icons/hud/screen_slimecore.dmi and b/icons/hud/screen_slimecore.dmi differ
diff --git a/icons/hud/screen_trasenknox.dmi b/icons/hud/screen_trasenknox.dmi
index 58c28d83e4be8..2569d2a635edd 100644
Binary files a/icons/hud/screen_trasenknox.dmi and b/icons/hud/screen_trasenknox.dmi differ
diff --git a/icons/mob/butts.dmi b/icons/mob/butts.dmi
new file mode 100644
index 0000000000000..ae4b41961a1cd
Binary files /dev/null and b/icons/mob/butts.dmi differ
diff --git a/icons/mob/clothing/back.dmi b/icons/mob/clothing/back.dmi
index 49e46149a670f..c3e5dc82069ad 100644
Binary files a/icons/mob/clothing/back.dmi and b/icons/mob/clothing/back.dmi differ
diff --git a/icons/mob/clothing/back/backpack.dmi b/icons/mob/clothing/back/backpack.dmi
index bf5207d85c409..afab07d9f13b2 100644
Binary files a/icons/mob/clothing/back/backpack.dmi and b/icons/mob/clothing/back/backpack.dmi differ
diff --git a/icons/mob/huds/antag_hud.dmi b/icons/mob/huds/antag_hud.dmi
index bb44e3de9568f..90056e499fd2b 100644
Binary files a/icons/mob/huds/antag_hud.dmi and b/icons/mob/huds/antag_hud.dmi differ
diff --git a/icons/mob/inhands/equipment/shields_lefthand.dmi b/icons/mob/inhands/equipment/shields_lefthand.dmi
index d31dbb3f830b1..ce99a16d476c0 100644
Binary files a/icons/mob/inhands/equipment/shields_lefthand.dmi and b/icons/mob/inhands/equipment/shields_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/shields_righthand.dmi b/icons/mob/inhands/equipment/shields_righthand.dmi
index dfd72809be71f..1c9c990b43dc9 100644
Binary files a/icons/mob/inhands/equipment/shields_righthand.dmi and b/icons/mob/inhands/equipment/shields_righthand.dmi differ
diff --git a/icons/mob/silicon/robot_items.dmi b/icons/mob/silicon/robot_items.dmi
index 9f8b0142e9c62..e18a9d08f8691 100644
Binary files a/icons/mob/silicon/robot_items.dmi and b/icons/mob/silicon/robot_items.dmi differ
diff --git a/icons/mob/simple/pets.dmi b/icons/mob/simple/pets.dmi
index 9bd7d69c06bc5..311fff1e6b0dc 100644
Binary files a/icons/mob/simple/pets.dmi and b/icons/mob/simple/pets.dmi differ
diff --git a/icons/obj/food/food.dmi b/icons/obj/food/food.dmi
index c4d93c23b0b5d..2fb08c78be71a 100644
Binary files a/icons/obj/food/food.dmi and b/icons/obj/food/food.dmi differ
diff --git a/icons/obj/storage/backpack.dmi b/icons/obj/storage/backpack.dmi
index c61d9321611b6..e4364146a1c99 100644
Binary files a/icons/obj/storage/backpack.dmi and b/icons/obj/storage/backpack.dmi differ
diff --git a/icons/obj/weapons/grenade.dmi b/icons/obj/weapons/grenade.dmi
index 40ab4d99e05fc..b3fb018bafa07 100644
Binary files a/icons/obj/weapons/grenade.dmi and b/icons/obj/weapons/grenade.dmi differ
diff --git a/icons/obj/weapons/shields.dmi b/icons/obj/weapons/shields.dmi
index 3f90af83196ba..7c4be107566ec 100644
Binary files a/icons/obj/weapons/shields.dmi and b/icons/obj/weapons/shields.dmi differ
diff --git a/icons/ui_icons/virtualpet/pet_state.dmi b/icons/ui_icons/virtualpet/pet_state.dmi
new file mode 100644
index 0000000000000..7ec865d104bc1
Binary files /dev/null and b/icons/ui_icons/virtualpet/pet_state.dmi differ
diff --git a/sound/ambience/antag/spy.ogg b/sound/ambience/antag/spy.ogg
new file mode 100644
index 0000000000000..1a5c64a3979b1
Binary files /dev/null and b/sound/ambience/antag/spy.ogg differ
diff --git a/sound/ambience/license.txt b/sound/ambience/license.txt
index 607dd6628e79b..a0b6efb24c5c1 100644
--- a/sound/ambience/license.txt
+++ b/sound/ambience/license.txt
@@ -1,4 +1,4 @@
-ambidet1.ogg is Fast Talking by Kevin Macleod. It has been licensed under the CC-BY 3.0 license.
+ambidet1.ogg and spy.ogg is Fast Talking by Kevin Macleod. It has been licensed under the CC-BY 3.0 license.
It has been cropped for use ingame.
ambidet2.ogg is Night on the Docks, Piano by Kevin Macleod. It has been licensed under CC-BY 3.0 license.
It has been cropped for use ingame, and also fades in.
diff --git a/sound/items/orbie_level_up.ogg b/sound/items/orbie_level_up.ogg
new file mode 100644
index 0000000000000..c876c9d78173a
Binary files /dev/null and b/sound/items/orbie_level_up.ogg differ
diff --git a/sound/items/orbie_notification_sound.ogg b/sound/items/orbie_notification_sound.ogg
new file mode 100644
index 0000000000000..b43bba41ae5a6
Binary files /dev/null and b/sound/items/orbie_notification_sound.ogg differ
diff --git a/sound/items/orbie_send_out.ogg b/sound/items/orbie_send_out.ogg
new file mode 100644
index 0000000000000..aba3d84e18609
Binary files /dev/null and b/sound/items/orbie_send_out.ogg differ
diff --git a/sound/items/orbie_trick_learned.ogg b/sound/items/orbie_trick_learned.ogg
new file mode 100644
index 0000000000000..bc50cf41b1ced
Binary files /dev/null and b/sound/items/orbie_trick_learned.ogg differ
diff --git a/strings/antagonist_flavor/spy_objective.json b/strings/antagonist_flavor/spy_objective.json
new file mode 100644
index 0000000000000..aa696baad6fa0
--- /dev/null
+++ b/strings/antagonist_flavor/spy_objective.json
@@ -0,0 +1,84 @@
+{
+ "objective_body": [
+ "Assassinate a high profile crewmember without being caught.",
+ "Cause a disaster to shake the station.",
+ "Cause a station evacuation.",
+ "Deprive the station of as many @pick(stealables) as you can.",
+ "Ensure @pick(department) is @pick(affected) by the end of the shift.",
+ "Ensure @pick(location) is @pick(affected) by the end of the shift.",
+ "Ensure no heads of staff @pick(escape) the station.",
+ "Ensure no members of @pick(department) @pick(escape) the station.",
+ "Ensure no rival @pick(rivals) @pick(escape) the station.",
+ "Frame a crewmember for a crime.",
+ "Free the station's AI from its laws.",
+ "Halt the station's @pick(happenings).",
+ "Invoke a mutiny against the heads of staff.",
+ "Make it difficult, but not impossible to @pick(escape) the station.",
+ "Sabotage the station's power grid or engine.",
+ "Steal as many @pick(stealables) as you can.",
+ "Take control of the station as the new Captain.",
+ "Take hostages of high value crewmembers and demand a ransom."
+ ],
+ "department": [
+ "Security",
+ "Engineering",
+ "Medical",
+ "Science",
+ "Supply"
+ ],
+ "location": [
+ "engineering",
+ "genetics",
+ "hydroponics",
+ "medbay",
+ "the bar",
+ "the bridge",
+ "the brig",
+ "the cargo bay",
+ "the chapel",
+ "the kitchen",
+ "the library",
+ "xenobiology"
+ ],
+ "happenings": [
+ "research",
+ "cargo operations",
+ "communications",
+ "genetic research",
+ "mining operation"
+ ],
+ "affected": [
+ "ablaze",
+ "burning",
+ "covered in blood",
+ "demolished",
+ "destroyed",
+ "engulfed in flames",
+ "obliterated",
+ "on fire",
+ "ruined",
+ "sabotaged",
+ "wrecked"
+ ],
+ "rivals": [
+ "agents",
+ "moles",
+ "operatives",
+ "spies",
+ "traitors"
+ ],
+ "stealables": [
+ "items",
+ "objects",
+ "things",
+ "tools",
+ "weapons"
+ ],
+ "escape": [
+ "depart",
+ "escape",
+ "evacuate",
+ "flee",
+ "leave"
+ ]
+}
diff --git a/tgstation.dme b/tgstation.dme
index df071abcac476..a4c8d17da602f 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3115,8 +3115,12 @@
#include "code\modules\antagonists\ninja\ninja_stars.dm"
#include "code\modules\antagonists\ninja\ninjaDrainAct.dm"
#include "code\modules\antagonists\ninja\outfit.dm"
-#include "code\modules\antagonists\nukeop\nukeop.dm"
#include "code\modules\antagonists\nukeop\outfits.dm"
+#include "code\modules\antagonists\nukeop\datums\operative.dm"
+#include "code\modules\antagonists\nukeop\datums\operative_leader.dm"
+#include "code\modules\antagonists\nukeop\datums\operative_lone.dm"
+#include "code\modules\antagonists\nukeop\datums\operative_reinforcement.dm"
+#include "code\modules\antagonists\nukeop\datums\operative_team.dm"
#include "code\modules\antagonists\nukeop\equipment\borgchameleon.dm"
#include "code\modules\antagonists\nukeop\equipment\nuclear_authentication_disk.dm"
#include "code\modules\antagonists\nukeop\equipment\nuclear_challenge.dm"
@@ -3150,6 +3154,10 @@
#include "code\modules\antagonists\space_dragon\space_dragon.dm"
#include "code\modules\antagonists\space_ninja\space_ninja.dm"
#include "code\modules\antagonists\spiders\spiders.dm"
+#include "code\modules\antagonists\spy\spy.dm"
+#include "code\modules\antagonists\spy\spy_bounty.dm"
+#include "code\modules\antagonists\spy\spy_bounty_handler.dm"
+#include "code\modules\antagonists\spy\spy_uplink.dm"
#include "code\modules\antagonists\survivalist\survivalist.dm"
#include "code\modules\antagonists\syndicate_monkey\syndicate_monkey.dm"
#include "code\modules\antagonists\traitor\balance_helper.dm"
@@ -4688,6 +4696,9 @@
#include "code\modules\mob\living\basic\pets\dog\corgi.dm"
#include "code\modules\mob\living\basic\pets\dog\dog_subtypes.dm"
#include "code\modules\mob\living\basic\pets\dog\strippable_items.dm"
+#include "code\modules\mob\living\basic\pets\orbie\orbie.dm"
+#include "code\modules\mob\living\basic\pets\orbie\orbie_abilities.dm"
+#include "code\modules\mob\living\basic\pets\orbie\orbie_ai.dm"
#include "code\modules\mob\living\basic\pets\parrot\_parrot.dm"
#include "code\modules\mob\living\basic\pets\parrot\parrot_items.dm"
#include "code\modules\mob\living\basic\pets\parrot\parrot_subtypes.dm"
@@ -5098,6 +5109,7 @@
#include "code\modules\modular_computers\file_system\programs\statusdisplay.dm"
#include "code\modules\modular_computers\file_system\programs\techweb.dm"
#include "code\modules\modular_computers\file_system\programs\theme_selector.dm"
+#include "code\modules\modular_computers\file_system\programs\virtual_pet.dm"
#include "code\modules\modular_computers\file_system\programs\wirecarp.dm"
#include "code\modules\modular_computers\file_system\programs\antagonist\contractor_program.dm"
#include "code\modules\modular_computers\file_system\programs\antagonist\dos.dm"
@@ -5860,6 +5872,7 @@
#include "code\modules\uplink\uplink_items\nukeops.dm"
#include "code\modules\uplink\uplink_items\special.dm"
#include "code\modules\uplink\uplink_items\species.dm"
+#include "code\modules\uplink\uplink_items\spy_unique.dm"
#include "code\modules\uplink\uplink_items\stealthy.dm"
#include "code\modules\uplink\uplink_items\stealthy_tools.dm"
#include "code\modules\uplink\uplink_items\suits.dm"
diff --git a/tgui/packages/tgui/interfaces/AntagInfoSpy.tsx b/tgui/packages/tgui/interfaces/AntagInfoSpy.tsx
new file mode 100644
index 0000000000000..a26266bceb4d0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AntagInfoSpy.tsx
@@ -0,0 +1,65 @@
+import { useBackend } from '../backend';
+import { Section, Stack } from '../components';
+import { Window } from '../layouts';
+import { Objective, ObjectivePrintout } from './common/Objectives';
+
+const greenText = {
+ fontWeight: 'italics',
+ color: '#20b142',
+};
+
+const redText = {
+ fontWeight: 'italics',
+ color: '#e03c3c',
+};
+
+type Data = {
+ antag_name: string;
+ uplink_location: string | null;
+ objectives: Objective[];
+};
+
+export const AntagInfoSpy = () => {
+ const { data } = useBackend();
+ const { antag_name, uplink_location, objectives } = data;
+ return (
+
+
+
+
+
+ You have been equipped with a special uplink device disguised as{' '}
+ {uplink_location || 'something'} that will allow you to steal from
+ the station.
+
+
+
+ Use it in hand to access your uplink, and{' '}
+ right click on bounty targets to steal them.
+
+
+
+
+ You may not be alone: There may be other spies on the station.
+
+
+ Work together or work against them: The choice is yours, but{' '}
+ you cannot share the rewards.
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/NtosVirtualPet.tsx b/tgui/packages/tgui/interfaces/NtosVirtualPet.tsx
new file mode 100644
index 0000000000000..7b61f2b566e54
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/NtosVirtualPet.tsx
@@ -0,0 +1,527 @@
+import { BooleanLike } from 'common/react';
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
+import {
+ Box,
+ Button,
+ Dropdown,
+ Flex,
+ Image,
+ Input,
+ LabeledList,
+ ProgressBar,
+ Section,
+ Stack,
+ Tabs,
+} from '../components';
+import { NtosWindow } from '../layouts';
+
+type Data = {
+ currently_summoned: BooleanLike;
+ pet_state: string;
+ hunger: number;
+ current_exp: number;
+ steps_counter: number;
+ required_exp: number;
+ happiness: number;
+ pet_area: string;
+ maximum_happiness: number;
+ maximum_hunger: number;
+ level: number;
+ preview_icon: string;
+ pet_color: string;
+ pet_hat: string;
+ pet_name: string;
+ selected_area: string;
+ can_reroll: BooleanLike;
+ pet_gender: string;
+ in_dropzone: BooleanLike;
+ can_summon: BooleanLike;
+ can_alter_appearance: BooleanLike;
+ possible_emotes: string[];
+ pet_state_icons: Pet_State_Icons[];
+ hat_selections: Hat_Selections[];
+ possible_colors: Possible_Colors[];
+ pet_updates: Pet_Updates[];
+};
+
+type Pet_State_Icons = {
+ name: string;
+ icon: string;
+};
+
+type Hat_Selections = {
+ hat_id: string;
+ hat_name: string;
+};
+
+type Possible_Colors = {
+ color_name: string;
+ color_value: string;
+};
+
+type Pet_Updates = {
+ update_id: number;
+ update_name: string;
+ update_picture: string;
+ update_message: string;
+ update_likers: number;
+ update_already_liked: BooleanLike;
+};
+
+enum Tab {
+ Stats,
+ Customization,
+ Updates,
+ Tricks,
+}
+
+enum PetGender {
+ male = 'male',
+ female = 'female',
+ neuter = 'neuter',
+}
+
+export const NtosVirtualPet = (props) => {
+ const [tab, setTab] = useState(Tab.Stats);
+
+ return (
+
+
+
+ setTab(Tab.Stats)}
+ >
+ Stats
+
+ setTab(Tab.Customization)}
+ >
+ Customization
+
+ setTab(Tab.Updates)}
+ >
+ Pet Updates
+
+ setTab(Tab.Tricks)}
+ >
+ Tricks
+
+
+ {tab === Tab.Stats && }
+ {tab === Tab.Customization && }
+ {tab === Tab.Updates && }
+ {tab === Tab.Tricks && }
+
+
+ );
+};
+
+const Stats = (props) => {
+ const { act, data } = useBackend();
+ const {
+ currently_summoned,
+ pet_state,
+ hunger,
+ current_exp,
+ required_exp,
+ happiness,
+ maximum_happiness,
+ maximum_hunger,
+ level,
+ pet_area,
+ steps_counter,
+ selected_area,
+ can_reroll,
+ can_summon,
+ in_dropzone,
+ } = data;
+ return (
+ <>
+
+
+
+
+
+
+
+ Current Level: {level}
+
+ Happiness:
+
+
+
+ Exp Progress:
+
+
+
+ Hunger:
+
+
+
+
+
+
+
+
+ {pet_area}
+
+ act('summon_pet')}
+ >
+ {currently_summoned ? 'Recall' : 'Release'}
+
+
+
+
+
+
+
+
+ {selected_area}
+
+ {(in_dropzone && (
+ act('drop_feed')}
+ >
+ Get Food
+
+ )) || (
+ act('get_feed_location')}
+ >
+ {selected_area === 'No location set'
+ ? 'Generate'
+ : 'Reroll'}
+
+ )}
+
+
+
+
+
+
+ {' '}
+ Steps: {steps_counter}
+
+
+
+ >
+ );
+};
+
+const PetTricks = (props) => {
+ const { act, data } = useBackend();
+ const { possible_emotes } = data;
+ const [sequences, setSequences] = useState(['none', 'none', 'none', 'none']);
+ const [TrickName, setTrickName] = useState('Trick');
+
+ const UpdateSequence = (Index: number, Trick: string) => {
+ const NewSequence = [...sequences];
+ NewSequence[Index] = Trick;
+ setSequences(NewSequence);
+ };
+
+ return (
+ setTrickName(value)}
+ >
+ Rename Trick
+
+ }
+ >
+
+ {sequences.map((sequence, index) => (
+
+ UpdateSequence(index, selected)}
+ />
+
+ ))}
+
+
+ act('teach_tricks', {
+ trick_name: TrickName,
+ tricks: sequences,
+ })
+ }
+ >
+ Teach
+
+
+ );
+};
+
+const Customization = (props) => {
+ const { act, data } = useBackend();
+ const {
+ preview_icon,
+ hat_selections = [],
+ possible_colors = [],
+ pet_hat,
+ pet_color,
+ pet_name,
+ pet_gender,
+ can_alter_appearance,
+ } = data;
+
+ const hatSelectionList = {};
+ for (const index in hat_selections) {
+ const hat = hat_selections[index];
+ hatSelectionList[hat.hat_name] = hat;
+ }
+
+ const possibleColorList = {};
+ for (const index in possible_colors) {
+ const color = possible_colors[index];
+ possibleColorList[color.color_name] = color;
+ }
+
+ const [selectedHat, setSelectedHat] = useState(hatSelectionList[pet_hat]);
+ const [selectedGender, setSelectedGender] = useState(pet_gender);
+ const [selectedName, setSelectedName] = useState(pet_name);
+ const [selectedColor, setSelectedColor] = useState(
+ possibleColorList[pet_color],
+ );
+ return (
+ <>
+
+
+
+
+
+
+
+ {
+ return selected_hat.hat_name;
+ })}
+ onSelected={(selected) =>
+ setSelectedHat(hatSelectionList[selected])
+ }
+ />
+
+
+
+
+
+
+ {
+ return possible_color.color_name;
+ })}
+ onSelected={(selected) =>
+ setSelectedColor(possibleColorList[selected])
+ }
+ />
+
+
+
+
+
+
+ setSelectedGender(PetGender.male)}
+ />
+
+
+ setSelectedGender(PetGender.female)}
+ />
+
+
+ setSelectedGender(PetGender.neuter)}
+ />
+
+
+
+
+
+
+
+ act('apply_customization', {
+ chosen_hat: selectedHat?.hat_id,
+ chosen_name: selectedName,
+ chosen_color: selectedColor?.color_value,
+ chosen_gender: selectedGender,
+ })
+ }
+ >
+ Apply
+
+
+ >
+ );
+};
+
+const AllPetUpdates = (props) => {
+ const { act, data } = useBackend();
+ const { pet_updates } = data;
+
+ return (
+
+
+ {pet_updates.map((update) => (
+
+
+
+
+
+
+ {update.update_name.substring(0, 6)}
+
+
+
+
+
+
+
+ {update.update_name.substring(0, 6)} {update.update_message}
+
+ act('like_update', {
+ update_reference: update.update_id,
+ })
+ }
+ >
+ {update.update_likers}
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+const PetIcon = (props) => {
+ const { data } = useBackend();
+ const { pet_state_icons = [] } = data;
+ const { our_pet_state } = props;
+
+ let icon_display = pet_state_icons.find(
+ (pet_icon) => pet_icon.name === our_pet_state,
+ );
+
+ if (!icon_display) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {capitalize(our_pet_state)}
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts
new file mode 100644
index 0000000000000..395baf8791504
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/spy.ts
@@ -0,0 +1,24 @@
+import { multiline } from 'common/string';
+
+import { Antagonist, Category } from '../base';
+
+const Spy: Antagonist = {
+ key: 'spy',
+ name: 'Spy',
+ description: [
+ multiline`
+ Your mission, should you choose to accept it: Infiltrate Space Station 13.
+ Disguise yourself as a member of their crew and steal vital equipment.
+ Should you be caught or killed, your employer will disavow any knowledge
+ of your actions. Good luck agent.
+ `,
+
+ multiline`
+ Complete Spy Bounties to earn rewards from your employer.
+ Use these rewards to sow chaos and mischief!
+ `,
+ ],
+ category: Category.Roundstart,
+};
+
+export default Spy;
diff --git a/tgui/packages/tgui/interfaces/SpyUplink.tsx b/tgui/packages/tgui/interfaces/SpyUplink.tsx
new file mode 100644
index 0000000000000..87735c19ff701
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SpyUplink.tsx
@@ -0,0 +1,122 @@
+import { BooleanLike } from 'common/react';
+
+import { useBackend } from '../backend';
+import { BlockQuote, Box, Dimmer, Icon, Section, Stack } from '../components';
+import { Window } from '../layouts';
+
+type Bounty = {
+ name: string;
+ help: string;
+ difficulty: string;
+ reward: string;
+ claimed: BooleanLike;
+ can_claim: BooleanLike;
+};
+
+type Data = {
+ time_left: number;
+ bounties: Bounty[];
+};
+
+const difficulty_to_color = {
+ easy: 'good',
+ medium: 'average',
+ hard: 'bad',
+};
+
+const BountyDimmer = (props: { text: string; color: string }) => {
+ return (
+
+
+
+
+
+
+ {props.text}
+
+
+
+ );
+};
+
+const BountyDisplay = (props: { bounty: Bounty }) => {
+ const { bounty } = props;
+
+ return (
+
+ {!!bounty.claimed && }
+ {!bounty.can_claim && !bounty.claimed && (
+
+ )}
+
+
+
+ {bounty.name}
+
+
+
+ {bounty.help}
+
+ Reward: {bounty.reward}
+
+
+ );
+};
+
+// Formats a number of deciseconds into a string minutes:seconds
+const format_deciseconds = (deciseconds: number) => {
+ const seconds = Math.floor(deciseconds / 10);
+ const minutes = Math.floor(seconds / 60);
+
+ const seconds_left = seconds % 60;
+ const minutes_left = minutes % 60;
+
+ const seconds_string = seconds_left.toString().padStart(2, '0');
+ const minutes_string = minutes_left.toString().padStart(2, '0');
+
+ return `${minutes_string}:${seconds_string}`;
+};
+
+export const SpyUplink = () => {
+ const { data } = useBackend();
+ const { bounties, time_left } = data;
+
+ return (
+
+
+
+ Time until refresh: {format_deciseconds(time_left)}
+
+ }
+ >
+
+
+ {bounties.map((bounty) => (
+
+
+
+ ))}
+
+
+
+
+
+ );
+};