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 += "[syndicate_name] Operatives:" + + 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 = "
The syndicate operatives were:" + 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 += "" + 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
" + + 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 += "[syndicate_name] Operatives:" - - 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 = "
The syndicate operatives were:" - 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 += "" - 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
" - - 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} + + + + +
+ + +
+ + {selected_area} + + {(in_dropzone && ( + + )) || ( + + )} + + +
+
+ +
+ {' '} + 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)} + /> + + ))} + + +
+ ); +}; + +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 ( + <> +
+ +
+ + +
+ setSelectedName(value)} + /> +
+
+ +
+ { + return selected_hat.hat_name; + })} + onSelected={(selected) => + setSelectedHat(hatSelectionList[selected]) + } + /> +
+
+
+ + +
+ { + return possible_color.color_name; + })} + onSelected={(selected) => + setSelectedColor(possibleColorList[selected]) + } + /> +
+
+ +
+ + +
+
+
+
+ +
+ + ); +}; + +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} + + + + + + ))} + +
+ ); +}; + +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) => ( + + + + ))} + + +
+
+
+ ); +};