diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 9a778aafc7..11189cf1af 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -111,3 +111,12 @@ GLOBAL_LIST_EMPTY(living_heart_cache) //A list of all living hearts in existance #define BLOB_SPREAD_COST 4 #define BLOB_ATTACK_REFUND 2 //blob refunds this much if it attacks and doesn't spread #define BLOB_REFLECTOR_COST 15 + +/// How many telecrystals a normal traitor starts with +#define TELECRYSTALS_DEFAULT 20 +/// How many telecrystals mapper/admin only "precharged" uplink implant +#define TELECRYSTALS_PRELOADED_IMPLANT 10 +/// The normal cost of an uplink implant; used for calcuating how many +/// TC to charge someone if they get a free implant through choice or +/// because they have nothing else that supports an implant. +#define UPLINK_IMPLANT_TELECRYSTAL_COST 4 diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 04c91cfb9d..664b8c0c0d 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -1,62 +1,197 @@ // This is eventually for wjohn to add more color standardization stuff like I keep asking him >:( -#define COLOR_INPUT_DISABLED "#F0F0F0" -#define COLOR_INPUT_ENABLED "#D3B5B5" -#define COLOR_FLOORTILE_GRAY "#8D8B8B" -#define COLOR_ALMOST_BLACK "#333333" -#define COLOR_BLACK "#000000" -#define COLOR_RED "#FF0000" -#define COLOR_RED_LIGHT "#FF3333" -#define COLOR_MAROON "#800000" -#define COLOR_YELLOW "#FFFF00" -#define COLOR_OLIVE "#808000" -#define COLOR_LIME "#32CD32" -#define COLOR_GREEN "#008000" -#define COLOR_CYAN "#00FFFF" -#define COLOR_TEAL "#008080" -#define COLOR_BLUE "#0000FF" -#define COLOR_BLUE_LIGHT "#33CCFF" -#define COLOR_NAVY "#000080" -#define COLOR_PINK "#FFC0CB" -#define COLOR_MAGENTA "#FF00FF" -#define COLOR_PURPLE "#800080" -#define COLOR_ORANGE "#FF9900" -#define COLOR_BEIGE "#CEB689" -#define COLOR_BLUE_GRAY "#75A2BB" -#define COLOR_BROWN "#BA9F6D" -#define COLOR_SOFT_RED "#FA8282" -#define COLOR_DARK_BROWN "#997C4F" -#define COLOR_DARK_ORANGE "#C3630C" -#define COLOR_GREEN_GRAY "#99BB76" -#define COLOR_RED_GRAY "#B4696A" -#define COLOR_PALE_BLUE_GRAY "#98C5DF" -#define COLOR_PALE_GREEN_GRAY "#B7D993" -#define COLOR_PALE_ORANGE "#FFC066" -#define COLOR_PALE_RED_GRAY "#D59998" -#define COLOR_PALE_PURPLE_GRAY "#CBB1CA" -#define COLOR_PURPLE_GRAY "#AE8CA8" +#define COLOR_INPUT_DISABLED "#F0F0F0" +#define COLOR_INPUT_ENABLED "#D3B5B5" -// Color defines used by the assembly detailer. -#define COLOR_ASSEMBLY_BLACK "#545454" -#define COLOR_ASSEMBLY_BGRAY "#9497AB" -#define COLOR_ASSEMBLY_WHITE "#E2E2E2" -#define COLOR_ASSEMBLY_RED "#CC4242" -#define COLOR_ASSEMBLY_ORANGE "#E39751" -#define COLOR_ASSEMBLY_BEIGE "#AF9366" -#define COLOR_ASSEMBLY_BROWN "#97670E" -#define COLOR_ASSEMBLY_GOLD "#AA9100" -#define COLOR_ASSEMBLY_YELLOW "#CECA2B" -#define COLOR_ASSEMBLY_GURKHA "#999875" -#define COLOR_ASSEMBLY_LGREEN "#789876" -#define COLOR_ASSEMBLY_GREEN "#44843C" -#define COLOR_ASSEMBLY_LBLUE "#5D99BE" -#define COLOR_ASSEMBLY_BLUE "#38559E" -#define COLOR_ASSEMBLY_PURPLE "#6F6192" -#define COLOR_ASSEMBLY_PINK "#ff4adc" +#define COLOR_DARKMODE_BACKGROUND "#202020" +#define COLOR_DARKMODE_DARKBACKGROUND "#171717" +#define COLOR_DARKMODE_TEXT "#a4bad6" -#define COLOR_WHITE "#FFFFFF" -#define COLOR_VERY_LIGHT_GRAY "#EEEEEE" -#define COLOR_SILVER "#C0C0C0" -#define COLOR_GRAY "#808080" +#define COLOR_WHITE "#FFFFFF" +#define COLOR_VERY_LIGHT_GRAY "#EEEEEE" +#define COLOR_SILVER "#C0C0C0" +#define COLOR_GRAY "#808080" +#define COLOR_FLOORTILE_GRAY "#8D8B8B" +#define COLOR_DARK "#454545" +#define COLOR_ALMOST_BLACK "#333333" +#define COLOR_BLACK "#000000" #define COLOR_HALF_TRANSPARENT_BLACK "#0000007A" + +#define COLOR_RED "#FF0000" +#define COLOR_MOSTLY_PURE_RED "#FF3300" +#define COLOR_DARK_RED "#A50824" +#define COLOR_RED_LIGHT "#FF3333" +#define COLOR_MAROON "#800000" +#define COLOR_VIVID_RED "#FF3232" +#define COLOR_LIGHT_GRAYISH_RED "#E4C7C5" +#define COLOR_SOFT_RED "#FA8282" +#define COLOR_BUBBLEGUM_RED "#950A0A" + +#define COLOR_YELLOW "#FFFF00" +#define COLOR_VIVID_YELLOW "#FBFF23" +#define COLOR_VERY_SOFT_YELLOW "#FAE48E" + +#define COLOR_OLIVE "#808000" +#define COLOR_VIBRANT_LIME "#00FF00" +#define COLOR_LIME "#32CD32" +#define COLOR_DARK_LIME "#00aa00" +#define COLOR_VERY_PALE_LIME_GREEN "#DDFFD3" +#define COLOR_VERY_DARK_LIME_GREEN "#003300" +#define COLOR_GREEN "#008000" +#define COLOR_DARK_MODERATE_LIME_GREEN "#44964A" + +#define COLOR_CYAN "#00FFFF" +#define COLOR_DARK_CYAN "#00A2FF" +#define COLOR_TEAL "#008080" +#define COLOR_BLUE "#0000FF" +#define COLOR_STRONG_BLUE "#1919c8" #define COLOR_BRIGHT_BLUE "#2CB2E8" +#define COLOR_MODERATE_BLUE "#555CC2" +#define COLOR_AMETHYST "#822BFF" +#define COLOR_BLUE_LIGHT "#33CCFF" +#define COLOR_NAVY "#000080" +#define COLOR_BLUE_GRAY "#75A2BB" + +#define COLOR_PINK "#FFC0CB" +#define COLOR_LIGHT_PINK "#ff3cc8" +#define COLOR_MOSTLY_PURE_PINK "#E4005B" +#define COLOR_MAGENTA "#FF00FF" +#define COLOR_STRONG_MAGENTA "#B800B8" +#define COLOR_PURPLE "#800080" +#define COLOR_VIOLET "#B900F7" +#define COLOR_STRONG_VIOLET "#6927c5" + +#define COLOR_ORANGE "#FF9900" +#define COLOR_MOSTLY_PURE_ORANGE "#ff8000" +#define COLOR_TAN_ORANGE "#FF7B00" +#define COLOR_BRIGHT_ORANGE "#E2853D" +#define COLOR_LIGHT_ORANGE "#ffc44d" +#define COLOR_PALE_ORANGE "#FFBE9D" +#define COLOR_BEIGE "#CEB689" +#define COLOR_DARK_ORANGE "#C3630C" +#define COLOR_DARK_MODERATE_ORANGE "#8B633B" + +#define COLOR_BROWN "#BA9F6D" +#define COLOR_DARK_BROWN "#997C4F" +#define COLOR_ORANGE_BROWN "#a9734f" + +//Color defines used by the soapstone (based on readability against grey tiles) +#define COLOR_SOAPSTONE_PLASTIC "#a19d94" +#define COLOR_SOAPSTONE_IRON "#b2b2b2" +#define COLOR_SOAPSTONE_BRONZE "#FE8001" +#define COLOR_SOAPSTONE_SILVER "#FFFFFF" +#define COLOR_SOAPSTONE_GOLD "#FFD900" +#define COLOR_SOAPSTONE_DIAMOND "#00ffee" + +#define COLOR_GREEN_GRAY "#99BB76" +#define COLOR_RED_GRAY "#B4696A" +#define COLOR_PALE_BLUE_GRAY "#98C5DF" +#define COLOR_PALE_GREEN_GRAY "#B7D993" +#define COLOR_PALE_RED_GRAY "#D59998" +#define COLOR_PALE_PURPLE_GRAY "#CBB1CA" +#define COLOR_PURPLE_GRAY "#AE8CA8" + +//Color defines used by the assembly detailer. +#define COLOR_ASSEMBLY_BLACK "#545454" +#define COLOR_ASSEMBLY_BGRAY "#9497AB" +#define COLOR_ASSEMBLY_WHITE "#E2E2E2" +#define COLOR_ASSEMBLY_RED "#CC4242" +#define COLOR_ASSEMBLY_ORANGE "#E39751" +#define COLOR_ASSEMBLY_BEIGE "#AF9366" +#define COLOR_ASSEMBLY_BROWN "#97670E" +#define COLOR_ASSEMBLY_GOLD "#AA9100" +#define COLOR_ASSEMBLY_YELLOW "#CECA2B" +#define COLOR_ASSEMBLY_GURKHA "#999875" +#define COLOR_ASSEMBLY_LGREEN "#789876" +#define COLOR_ASSEMBLY_GREEN "#44843C" +#define COLOR_ASSEMBLY_LBLUE "#5D99BE" +#define COLOR_ASSEMBLY_BLUE "#38559E" +#define COLOR_ASSEMBLY_PURPLE "#6F6192" + +///Colors for xenobiology vatgrowing +#define COLOR_SAMPLE_YELLOW "#c0b823" +#define COLOR_SAMPLE_PURPLE "#342941" +#define COLOR_SAMPLE_GREEN "#98b944" +#define COLOR_SAMPLE_BROWN "#91542d" +#define COLOR_SAMPLE_GRAY "#5e5856" + +///Main colors for UI themes +#define COLOR_THEME_MIDNIGHT "#6086A0" +#define COLOR_THEME_PLASMAFIRE "#FFB200" +#define COLOR_THEME_RETRO "#24CA00" +#define COLOR_THEME_SLIMECORE "#4FB259" +#define COLOR_THEME_OPERATIVE "#B8221F" +#define COLOR_THEME_GLASS "#75A4C4" +#define COLOR_THEME_CLOCKWORK "#CFBA47" + +///Colors for eigenstates +#define COLOR_PERIWINKLEE "#9999FF" +/** + * Some defines to generalise colours used in lighting. + * + * Important note: colors can end up significantly different from the basic html picture, especially when saturated + */ +/// Bright but quickly dissipating neon green. rgb(100, 200, 100) +#define LIGHT_COLOR_GREEN "#64C864" +/// Electric green. rgb(0, 255, 0) +#define LIGHT_COLOR_ELECTRIC_GREEN "#00FF00" +/// Cold, diluted blue. rgb(100, 150, 250) +#define LIGHT_COLOR_BLUE "#6496FA" +/// Light blueish green. rgb(125, 225, 175) +#define LIGHT_COLOR_BLUEGREEN "#7DE1AF" +/// Diluted cyan. rgb(125, 225, 225) +#define LIGHT_COLOR_CYAN "#7DE1E1" +/// Electric cyan rgb(0, 255, 255) +#define LIGHT_COLOR_ELECTRIC_CYAN "#00FFFF" +/// More-saturated cyan. rgb(64, 206, 255) +#define LIGHT_COLOR_LIGHT_CYAN "#40CEFF" +/// Saturated blue. rgb(51, 117, 248) +#define LIGHT_COLOR_DARK_BLUE "#6496FA" +/// Diluted, mid-warmth pink. rgb(225, 125, 225) +#define LIGHT_COLOR_PINK "#E17DE1" +/// Dimmed yellow, leaning kaki. rgb(225, 225, 125) +#define LIGHT_COLOR_YELLOW "#E1E17D" +/// Clear brown, mostly dim. rgb(150, 100, 50) +#define LIGHT_COLOR_BROWN "#966432" +/// Mostly pure orange. rgb(250, 150, 50) +#define LIGHT_COLOR_ORANGE "#FA9632" +/// Light Purple. rgb(149, 44, 244) +#define LIGHT_COLOR_PURPLE "#952CF4" +/// Less-saturated light purple. rgb(155, 81, 255) +#define LIGHT_COLOR_LAVENDER "#9B51FF" +///slightly desaturated bright yellow. +#define LIGHT_COLOR_HOLY_MAGIC "#FFF743" +/// deep crimson +#define LIGHT_COLOR_BLOOD_MAGIC "#D00000" + +/* These ones aren't a direct colour like the ones above, because nothing would fit */ +/// Warm orange color, leaning strongly towards yellow. rgb(250, 160, 25) +#define LIGHT_COLOR_FIRE "#FAA019" +/// Very warm yellow, leaning slightly towards orange. rgb(196, 138, 24) +#define LIGHT_COLOR_LAVA "#C48A18" +/// Bright, non-saturated red. Leaning slightly towards pink for visibility. rgb(250, 100, 75) +#define LIGHT_COLOR_FLARE "#FA644B" +/// Weird color, between yellow and green, very slimy. rgb(175, 200, 75) +#define LIGHT_COLOR_SLIME_LAMP "#AFC84B" +/// Extremely diluted yellow, close to skin color (for some reason). rgb(250, 225, 175) +#define LIGHT_COLOR_TUNGSTEN "#FAE1AF" +/// Barely visible cyan-ish hue, as the doctor prescribed. rgb(240, 250, 250) +#define LIGHT_COLOR_HALOGEN "#F0FAFA" + +//The GAGS greyscale_colors for each department's computer/machine circuits +#define CIRCUIT_COLOR_GENERIC "#1A7A13" +#define CIRCUIT_COLOR_COMMAND "#1B4594" +#define CIRCUIT_COLOR_SECURITY "#9A151E" +#define CIRCUIT_COLOR_SCIENCE "#BC4A9B" +#define CIRCUIT_COLOR_SERVICE "#92DCBA" +#define CIRCUIT_COLOR_MEDICAL "#00CCFF" +#define CIRCUIT_COLOR_ENGINEERING "#F8D700" +#define CIRCUIT_COLOR_SUPPLY "#C47749" + +/// Colors for pride week +#define COLOR_PRIDE_RED "#FF6666" +#define COLOR_PRIDE_ORANGE "#FC9F3C" +#define COLOR_PRIDE_YELLOW "#EAFF51" +#define COLOR_PRIDE_GREEN "#41FC66" +#define COLOR_PRIDE_BLUE "#42FFF2" +#define COLOR_PRIDE_PURPLE "#5D5DFC" diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index d0d6f24343..ea527d19ab 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -25,6 +25,12 @@ /// Do not allow this random event to continue. #define CANCEL_PRE_RANDOM_EVENT (1<<0) +/// a weather event of some kind occured +#define COMSIG_WEATHER_TELEGRAPH(event_type) "!weather_telegraph [event_type]" +#define COMSIG_WEATHER_START(event_type) "!weather_start [event_type]" +#define COMSIG_WEATHER_WINDDOWN(event_type) "!weather_winddown [event_type]" +#define COMSIG_WEATHER_END(event_type) "!weather_end [event_type]" + /// called by auxgm add_gas: (gas_id) #define COMSIG_GLOB_NEW_GAS "!new_gas" @@ -48,6 +54,8 @@ #define COMSIG_PARENT_QDELETING "parent_qdeleting" /// generic topic handler (usr, href_list) #define COMSIG_TOPIC "handle_topic" +/// from datum ui_act (usr, action) +#define COMSIG_UI_ACT "COMSIG_UI_ACT" /// fires on the target datum when an element is attached to it (/datum/element) #define COMSIG_ELEMENT_ATTACH "element_attach" @@ -418,6 +426,12 @@ ///from /obj/machinery/obj_break(damage_flag): (damage_flag) #define COMSIG_MACHINERY_BROKEN "machinery_broken" +// /obj/machinery/power/supermatter_crystal signals +/// from /obj/machinery/power/supermatter_crystal/process_atmos(); when the SM delam reaches the point of sounding alarms +#define COMSIG_SUPERMATTER_DELAM_START_ALARM "sm_delam_start_alarm" +/// from /obj/machinery/power/supermatter_crystal/process_atmos(); when the SM sounds an audible alarm +#define COMSIG_SUPERMATTER_DELAM_ALARM "sm_delam_alarm" + // /obj/item signals #define COMSIG_ITEM_ATTACK "item_attack" //from base of obj/item/attack(): (/mob/living/target, /mob/living/user) #define COMSIG_ITEM_ATTACK_SELF "item_attack_self" //from base of obj/item/attack_self(): (/mob) diff --git a/code/__DEFINES/layers_planes.dm b/code/__DEFINES/layers_planes.dm index c7be89721e..a2d6bb4da6 100644 --- a/code/__DEFINES/layers_planes.dm +++ b/code/__DEFINES/layers_planes.dm @@ -38,6 +38,9 @@ #define CHAT_PLANE -1 //We don't want heard messages to be hidden by FoV. +/// Plane for balloon text (text that fades up) +#define BALLOON_CHAT_PLANE -1.2 + #define CHAT_LAYER 12.0001 // Do not insert layers between these two values #define CHAT_LAYER_MAX 12.9999 diff --git a/code/__DEFINES/lighting.dm b/code/__DEFINES/lighting.dm index 95d91da14a..e448cd23d4 100644 --- a/code/__DEFINES/lighting.dm +++ b/code/__DEFINES/lighting.dm @@ -30,31 +30,6 @@ //Important note on colors. Colors can end up significantly different from the basic html picture, especially when saturated #define LIGHT_COLOR_WHITE "#FFFFFF" #define LIGHT_COLOR_RED "#FA8282" //Warm but extremely diluted red. rgb(250, 130, 130) -#define LIGHT_COLOR_GREEN "#64C864" //Bright but quickly dissipating neon green. rgb(100, 200, 100) -#define LIGHT_COLOR_BLUE "#6496FA" //Cold, diluted blue. rgb(100, 150, 250) - -#define LIGHT_COLOR_BLUEGREEN "#7DE1AF" //Light blueish green. rgb(125, 225, 175) -#define LIGHT_COLOR_PALEBLUE "#7DAFE1" //A pale blue-ish color. rgb(125, 175, 225) -#define LIGHT_COLOR_CYAN "#7DE1E1" //Diluted cyan. rgb(125, 225, 225) -#define LIGHT_COLOR_LIGHT_CYAN "#40CEFF" //More-saturated cyan. rgb(64, 206, 255) -#define LIGHT_COLOR_DARK_BLUE "#6496FA" //Saturated blue. rgb(51, 117, 248) -#define LIGHT_COLOR_PINK "#E17DE1" //Diluted, mid-warmth pink. rgb(225, 125, 225) -#define LIGHT_COLOR_YELLOW "#E1E17D" //Dimmed yellow, leaning kaki. rgb(225, 225, 125) -#define LIGHT_COLOR_BROWN "#966432" //Clear brown, mostly dim. rgb(150, 100, 50) -#define LIGHT_COLOR_ORANGE "#FA9632" //Mostly pure orange. rgb(250, 150, 50) -#define LIGHT_COLOR_PURPLE "#952CF4" //Light Purple. rgb(149, 44, 244) -#define LIGHT_COLOR_LAVENDER "#9B51FF" //Less-saturated light purple. rgb(155, 81, 255) - -#define LIGHT_COLOR_HOLY_MAGIC "#FFF743" //slightly desaturated bright yellow. -#define LIGHT_COLOR_BLOOD_MAGIC "#D00000" //deep crimson - -//These ones aren't a direct colour like the ones above, because nothing would fit -#define LIGHT_COLOR_FIRE "#FAA019" //Warm orange color, leaning strongly towards yellow. rgb(250, 160, 25) -#define LIGHT_COLOR_LAVA "#C48A18" //Very warm yellow, leaning slightly towards orange. rgb(196, 138, 24) -#define LIGHT_COLOR_FLARE "#FA644B" //Bright, non-saturated red. Leaning slightly towards pink for visibility. rgb(250, 100, 75) -#define LIGHT_COLOR_SLIME_LAMP "#AFC84B" //Weird color, between yellow and green, very slimy. rgb(175, 200, 75) -#define LIGHT_COLOR_TUNGSTEN "#FAE1AF" //Extremely diluted yellow, close to skin color (for some reason). rgb(250, 225, 175) -#define LIGHT_COLOR_HALOGEN "#F0FAFA" //Barely visible cyan-ish hue, as the doctor prescribed. rgb(240, 250, 250) #define LIGHT_RANGE_FIRE 3 //How many tiles standard fires glow. diff --git a/code/__DEFINES/machines.dm b/code/__DEFINES/machines.dm index 762df42472..b25ef763cf 100644 --- a/code/__DEFINES/machines.dm +++ b/code/__DEFINES/machines.dm @@ -48,32 +48,39 @@ #define MC_CHARGE "CHARGE" #define MC_AI "AI" #define MC_SENSORS "SENSORS" +#define MC_SIGNALER "SIGNALER" //NTNet stuff, for modular computers // NTNet module-configuration values. Do not change these. If you need to add another use larger number (5..6..7 etc) -#define NTNET_SOFTWAREDOWNLOAD 1 // Downloads of software from NTNet -#define NTNET_PEERTOPEER 2 // P2P transfers of files between devices -#define NTNET_COMMUNICATION 3 // Communication (messaging) -#define NTNET_SYSTEMCONTROL 4 // Control of various systems, RCon, air alarm control, etc. +#define NTNET_SOFTWAREDOWNLOAD 1 // Downloads of software from NTNet +#define NTNET_PEERTOPEER 2 // P2P transfers of files between devices +#define NTNET_COMMUNICATION 3 // Communication (messaging) +#define NTNET_SYSTEMCONTROL 4 // Control of various systems, RCon, air alarm control, etc. //NTNet transfer speeds, used when downloading/uploading a file/program. -#define NTNETSPEED_LOWSIGNAL 0.5 // GQ/s transfer speed when the device is wirelessly connected and on Low signal -#define NTNETSPEED_HIGHSIGNAL 1 // GQ/s transfer speed when the device is wirelessly connected and on High signal -#define NTNETSPEED_ETHERNET 2 // GQ/s transfer speed when the device is using wired connection +#define NTNETSPEED_LOWSIGNAL 0.5 // GQ/s transfer speed when the device is wirelessly connected and on Low signal +#define NTNETSPEED_HIGHSIGNAL 1 // GQ/s transfer speed when the device is wirelessly connected and on High signal +#define NTNETSPEED_ETHERNET 2 // GQ/s transfer speed when the device is using wired connection //Caps for NTNet logging. Less than 10 would make logging useless anyway, more than 500 may make the log browser too laggy. Defaults to 100 unless user changes it. #define MAX_NTNET_LOGS 300 #define MIN_NTNET_LOGS 10 //Program bitflags -#define PROGRAM_ALL (~0) -#define PROGRAM_CONSOLE (1<<0) -#define PROGRAM_LAPTOP (1<<1) -#define PROGRAM_TABLET (1<<2) +#define PROGRAM_ALL (~0) +#define PROGRAM_CONSOLE (1<<0) +#define PROGRAM_LAPTOP (1<<1) +#define PROGRAM_TABLET (1<<2) //Program states #define PROGRAM_STATE_KILLED 0 #define PROGRAM_STATE_BACKGROUND 1 #define PROGRAM_STATE_ACTIVE 2 +//Program categories +#define PROGRAM_CATEGORY_CREW "Crew" +#define PROGRAM_CATEGORY_ENGI "Engineering" +#define PROGRAM_CATEGORY_ROBO "Robotics" +#define PROGRAM_CATEGORY_SUPL "Supply" +#define PROGRAM_CATEGORY_MISC "Other" #define FIREDOOR_OPEN 1 #define FIREDOOR_CLOSED 2 diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index f5b0a4fa3f..b0c548be32 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -330,6 +330,7 @@ GLOBAL_LIST_INIT(pda_reskins, list(PDA_SKIN_CLASSIC = 'icons/obj/pda.dmi', PDA_S #define FIRST_DIAG_STEP 1 #define SECOND_DIAG_STEP 2 +#define DEADCHAT_ANNOUNCEMENT "announcement" #define DEADCHAT_ARRIVALRATTLE "arrivalrattle" #define DEADCHAT_DEATHRATTLE "deathrattle" #define DEADCHAT_REGULAR "regular-deadchat" diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 30899c0ca2..91b7c2ac7b 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -29,10 +29,10 @@ * if the arguments to addtimer are the same as an existing timer, it doesn't create a new timer, * and returns the id of the existing timer */ -#define TIMER_UNIQUE (1<<0) +#define TIMER_UNIQUE (1<<0) ///For unique timers: Replace the old timer rather then not start this one -#define TIMER_OVERRIDE (1<<1) +#define TIMER_OVERRIDE (1<<1) /** * Timing should be based on how timing progresses on clients, not the server. @@ -41,20 +41,23 @@ * should only be used in conjuction with things that have to progress client side, such as * animate() or sound() */ -#define TIMER_CLIENT_TIME (1<<2) +#define TIMER_CLIENT_TIME (1<<2) ///Timer can be stopped using deltimer() -#define TIMER_STOPPABLE (1<<3) +#define TIMER_STOPPABLE (1<<3) ///prevents distinguishing identical timers with the wait variable /// ///To be used with TIMER_UNIQUE -#define TIMER_NO_HASH_WAIT (1<<4) +#define TIMER_NO_HASH_WAIT (1<<4) ///Loops the timer repeatedly until qdeleted /// ///In most cases you want a subsystem instead, so don't use this unless you have a good reason -#define TIMER_LOOP (1<<5) +#define TIMER_LOOP (1<<5) + +///Delete the timer on parent datum Destroy() and when deltimer'd +#define TIMER_DELETE_ME (1<<6) ///Empty ID define #define TIMER_ID_NULL -1 @@ -188,6 +191,7 @@ #define FIRE_PRIORITY_CALLBACKS 600 // #define FIRE_PRIORITY_EXPLOSIONS 666 #define FIRE_PRIORITY_TIMER 700 +#define FIRE_PRIORITY_SOUND_LOOPS 800 #define FIRE_PRIORITY_INPUT 1000 // This must always always be the max highest priority. Player input must never be lost. // SS runlevels diff --git a/code/__DEFINES/text.dm b/code/__DEFINES/text.dm new file mode 100644 index 0000000000..58c24747e0 --- /dev/null +++ b/code/__DEFINES/text.dm @@ -0,0 +1,5 @@ +/// Prepares a text to be used for maptext. Use this so it doesn't look hideous. +#define MAPTEXT(text) {"[##text]"} + +/// Macro from Lummox used to get height from a MeasureText proc +#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1)) diff --git a/code/__DEFINES/uplink.dm b/code/__DEFINES/uplink.dm new file mode 100644 index 0000000000..4aad3e6b88 --- /dev/null +++ b/code/__DEFINES/uplink.dm @@ -0,0 +1,10 @@ +// These are used in uplink_devices.dm to determine whether or not an item is purchasable. + +/// This item is purchasable to traitors +#define UPLINK_TRAITORS (1 << 0) + +/// This item is purchasable to nuke ops +#define UPLINK_NUKE_OPS (1 << 1) + +/// This item is purchasable to clown ops +#define UPLINK_CLOWN_OPS (1 << 2) diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index c858d30ba3..f045b5e9aa 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -1,8 +1,8 @@ /* * Holds procs to help with list operations * Contains groups: - * Misc - * Sorting + * Misc + * Sorting */ /* @@ -11,22 +11,29 @@ #define LAZYINITLIST(L) if (!L) { L = list(); } #define UNSETEMPTY(L) if (L && !length(L)) L = null -#define LAZYCOPY(L) (L ? L.Copy() : list() ) +///Like LAZYCOPY - copies an input list if the list has entries, If it doesn't the assigned list is nulled +#define LAZYLISTDUPLICATE(L) (L ? L.Copy() : null ) #define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } } #define LAZYADD(L, I) if(!L) { L = list(); } L += I; #define LAZYOR(L, I) if(!L) { L = list(); } L |= I; #define LAZYFIND(L, V) (L ? L.Find(V) : 0) +///returns L[I] if L exists and I is a valid index of L, runtimes if L is not a list #define LAZYACCESS(L, I) (L ? (isnum(I) ? (I > 0 && I <= length(L) ? L[I] : null) : L[I]) : null) #define LAZYSET(L, K, V) if(!L) { L = list(); } L[K] = V; #define LAZYLEN(L) length(L) -//Sets a list to null +///Sets a list to null #define LAZYNULL(L) L = null -#define LAZYCLEARLIST(L) if(L) L.Cut() -#define SANITIZE_LIST(L) ( islist(L) ? L : list() ) -#define reverseList(L) reverseRange(L.Copy()) +#define LAZYADDASSOC_TG(L, K, V) if(!L) { L = list(); } L[K] += V; +///This is used to add onto lazy assoc list when the value you're adding is a /list/. This one has extra safety over lazyaddassoc because the value could be null (and thus cant be used to += objects) #define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += list(V); #define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; } #define LAZYACCESSASSOC(L, I, K) L ? L[I] ? L[I][K] ? L[I][K] : null : null : null +#define QDEL_LAZYLIST(L) for(var/I in L) qdel(I); L = null; +//These methods don't null the list +#define LAZYCOPY(L) (L ? L.Copy() : list() ) //Use LAZYLISTDUPLICATE instead if you want it to null with no entries +#define LAZYCLEARLIST(L) if(L) L.Cut() // Consider LAZYNULL instead +#define SANITIZE_LIST(L) ( islist(L) ? L : list() ) +#define reverseList(L) reverseRange(L.Copy()) /// Performs an insertion on the given lazy list with the given key and value. If the value already exists, a new one will not be made. #define LAZYORASSOCLIST(lazy_list, key, value) \ @@ -75,7 +82,7 @@ } while(FALSE) //Returns a list in plain english as a string -/proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "") +/proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" ) var/total = length(input) switch(total) if (0) @@ -98,6 +105,7 @@ /** * English_list but associative supporting. Higher overhead. + * @depricated */ /proc/english_list_assoc(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "") var/total = length(input) @@ -125,6 +133,7 @@ return "[output][and_text][input[index]]" //Returns list element or null. Should prevent "index out of bounds" error. +/// @depricated /proc/listgetindex(list/L, index) if(LAZYLEN(L)) if(isnum(index) && ISINTEGER(index)) @@ -135,17 +144,19 @@ return //Return either pick(list) or null if list is not of type /list or is empty +/// @depricated /proc/safepick(list/L) if(LAZYLEN(L)) return pick(L) //Checks if the list is empty +/// @depricated /proc/isemptylist(list/L) if(!L.len) return TRUE return FALSE -//Checks for specific types in a list +//Checks for specific types in a listc /proc/is_type_in_list(atom/A, list/L) if(!LAZYLEN(L) || !A) return FALSE @@ -186,43 +197,45 @@ /proc/typecache_filter_list_reverse(list/atoms, list/typecache) RETURN_TYPE(/list) . = list() - for(var/atom/A as anything in atoms) - if(!typecache[A.type]) - . += A + for(var/atom/atom as anything in atoms) + if(!typecache[atom.type]) + . += atom /proc/typecache_filter_multi_list_exclusion(list/atoms, list/typecache_include, list/typecache_exclude) . = list() - for(var/atom/A as anything in atoms) - if(typecache_include[A.type] && !typecache_exclude[A.type]) - . += A + for(var/atom/atom as anything in atoms) + if(typecache_include[atom.type] && !typecache_exclude[atom.type]) + . += atom -//Like typesof() or subtypesof(), but returns a typecache instead of a list +///Like typesof() or subtypesof(), but returns a typecache instead of a list /proc/typecacheof(path, ignore_root_path, only_root_path = FALSE) if(ispath(path)) - var/list/types = list() + var/list/types + var/list/output = list() if(only_root_path) - types = list(path) + output[path] = TRUE else types = ignore_root_path ? subtypesof(path) : typesof(path) - var/list/L = list() - for(var/T in types) - L[T] = TRUE - return L + for(var/T in types) + output[T] = TRUE + return output else if(islist(path)) var/list/pathlist = path - var/list/L = list() + var/list/output = list() if(ignore_root_path) - for(var/P in pathlist) - for(var/T in subtypesof(P)) - L[T] = TRUE + for(var/current_path in pathlist) + for(var/subtype in subtypesof(current_path)) + output[subtype] = TRUE + return output + + if(only_root_path) + for(var/current_path in pathlist) + output[current_path] = TRUE else - for(var/P in pathlist) - if(only_root_path) - L[P] = TRUE - else - for(var/T in typesof(P)) - L[T] = TRUE - return L + for(var/current_path in pathlist) + for(var/subpath in typesof(current_path)) + output[subpath] = TRUE + return output /proc/typecacheof_assoc_list(list/pathlist, ignore_root_path = FALSE) . = list() @@ -281,9 +294,10 @@ //Picks a random element from a list based on a weighting system: //1. Adds up the total of weights for each element -//2. Gets the total from 0% to 100% of previous total value. +//2. Gets a number between 1 and that total //3. For each element in the list, subtracts its weighting from that number //4. If that makes the number 0 or less, return that element. +//Will output null sometimes if you use decimals (e.g. 0.1 instead of 10) as rand() uses integers, not floats /proc/pickweight(list/L, base_weight = 1) var/total = 0 var/item @@ -292,12 +306,14 @@ L[item] = base_weight total += L[item] - total = rand() * total + total = rand(base_weight, total) for (item in L) - total -= L[item] + total -=L [item] if (total <= 0) return item + return null + /proc/pickweightAllowZero(list/L) //The original pickweight proc will sometimes pick entries with zero weight. I'm not sure if changing the original will break anything, so I left it be. var/total = 0 var/item @@ -369,13 +385,13 @@ //Results will have a length of quality return results -//Pick a random element from the list and remove it from the list. +/// Pick a random element from the list and remove it from the list. /proc/pick_n_take(list/L) RETURN_TYPE(L[_].type) if(L.len) var/picked = rand(1,L.len) . = L[picked] - L.Cut(picked,picked+1) //Cut is far more efficient that Remove() + L.Cut(picked,picked+1) //Cut is far more efficient that Remove() //Pick a random element from the list by weight and remove it from the list. //Result is returned as a list in the format list(key, value) @@ -471,7 +487,7 @@ //uses sortList() but uses the var's name specifically. This should probably be using mergeAtom() instead /proc/sortNames(list/L, order=1) - return sortTim(L, order >= 0 ? /proc/cmp_name_asc : /proc/cmp_name_dsc) + return sortTim(L.Copy(), order >= 0 ? /proc/cmp_name_asc : /proc/cmp_name_dsc) //Converts a bitfield to a list of numbers (or words if a wordlist is provided) @@ -507,10 +523,12 @@ if (L[i] == val) .++ +/// Returns datum/data/record /proc/find_record(field, value, list/L) for(var/datum/data/record/R in L) if(R.fields[field] == value) return R + return null //Move a single element from position fromIndex within a list, to position toIndex @@ -520,10 +538,10 @@ //fromIndex and toIndex must be in the range [1,L.len+1] //This will preserve associations ~Carnie /proc/moveElement(list/L, fromIndex, toIndex) - if(fromIndex == toIndex || fromIndex+1 == toIndex) //no need to move + if(fromIndex == toIndex || fromIndex+1 == toIndex) //no need to move return if(fromIndex > toIndex) - ++fromIndex //since a null will be inserted before fromIndex, the index needs to be nudged right by one + ++fromIndex //since a null will be inserted before fromIndex, the index needs to be nudged right by one L.Insert(toIndex, null) L.Swap(fromIndex, toIndex) @@ -535,10 +553,10 @@ //This will preserve associations ~Carnie /proc/moveRange(list/L, fromIndex, toIndex, len=1) var/distance = abs(toIndex - fromIndex) - if(len >= distance) //there are more elements to be moved than the distance to be moved. Therefore the same result can be achieved (with fewer operations) by moving elements between where we are and where we are going. The result being, our range we are moving is shifted left or right by dist elements + if(len >= distance) //there are more elements to be moved than the distance to be moved. Therefore the same result can be achieved (with fewer operations) by moving elements between where we are and where we are going. The result being, our range we are moving is shifted left or right by dist elements if(fromIndex <= toIndex) - return //no need to move - fromIndex += len //we want to shift left instead of right + return //no need to move + fromIndex += len //we want to shift left instead of right for(var/i=0, i distance) //there is an overlap, therefore swapping each element will require more swaps than inserting new elements + if(len > distance) //there is an overlap, therefore swapping each element will require more swaps than inserting new elements if(fromIndex < toIndex) toIndex += len else @@ -604,6 +622,12 @@ if(D.vars[varname] == value) return D +//remove all nulls from a list +/proc/removeNullsFromList(list/L) + while(L.Remove(null)) + continue + return L + //Copies a list, and all lists inside it recusively //Does not copy any other reference type /proc/deepCopyList(list/l) @@ -636,11 +660,6 @@ used_key_list[input_key] = 1 return input_key -#if DM_VERSION > 514 -#error Remie said that lummox was adding a way to get a lists -#error contents via list.values, if that is true remove this -#error otherwise, update the version and bug lummox -#endif //Flattens a keyed list into a list of it's contents /proc/flatten_list(list/key_list) if(!islist(key_list)) @@ -717,6 +736,29 @@ ret += key return ret +/proc/compare_list(list/l,list/d) + if(!islist(l) || !islist(d)) + return FALSE + + if(l.len != d.len) + return FALSE + + for(var/i in 1 to l.len) + if(l[i] != d[i]) + return FALSE + + return TRUE + +#define LAZY_LISTS_OR(left_list, right_list)\ + ( length(left_list)\ + ? length(right_list)\ + ? (left_list | right_list)\ + : left_list.Copy()\ + : length(right_list)\ + ? right_list.Copy()\ + : null\ + ) + /proc/is_type_in_ref_list(path, list/L) if(!ispath(path))//not a path return diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index d61adf4337..4bc9ccaacd 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -88,6 +88,19 @@ if (CONFIG_GET(flag/log_access)) WRITE_LOG(GLOB.world_game_log, "ACCESS: [text]") +/** + * Writes to a special log file if the log_suspicious_login config flag is set, + * which is intended to contain all logins that failed under suspicious circumstances. + * + * Mirrors this log entry to log_access when access_log_mirror is TRUE, so this proc + * doesn't need to be used alongside log_access and can replace it where appropriate. + */ +/proc/log_suspicious_login(text, access_log_mirror = TRUE) + if (CONFIG_GET(flag/log_suspicious_login)) + WRITE_LOG(GLOB.world_suspicious_login_log, "SUSPICIOUS_ACCESS: [text]") + if(access_log_mirror) + log_access(text) + /proc/log_law(text) if (CONFIG_GET(flag/log_law)) WRITE_LOG(GLOB.world_game_log, "LAW: [text]") diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index fc579ebfd6..64311a011c 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -396,12 +396,16 @@ /proc/ScreenText(obj/O, maptext="", screen_loc="CENTER-7,CENTER-7", maptext_height=480, maptext_width=480) if(!isobj(O)) O = new /atom/movable/screen/text() - O.maptext = maptext + O.maptext = MAPTEXT(maptext) O.maptext_height = maptext_height O.maptext_width = maptext_width O.screen_loc = screen_loc return O +/// Removes an image from a client's `.images`. Useful as a callback. +/proc/remove_image_from_client(image/image, client/remove_from) + remove_from?.images -= image + /proc/remove_images_from_clients(image/I, list/show_to) for(var/client/C in show_to) C.images -= I diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm index a8d52ea085..e0354e2f5f 100644 --- a/code/__HELPERS/global_lists.dm +++ b/code/__HELPERS/global_lists.dm @@ -88,16 +88,6 @@ // Keybindings init_keybindings() - //Uplink Items - for(var/path in subtypesof(/datum/uplink_item)) - var/datum/uplink_item/I = path - if(!initial(I.item)) //We add categories to a separate list. - GLOB.uplink_categories |= initial(I.category) - continue - GLOB.uplink_items += path - //(sub)typesof entries are listed by the order they are loaded in the code, so we'll have to rearrange them here. - GLOB.uplink_items = sortList(GLOB.uplink_items, /proc/cmp_uplink_items_dsc) - init_subtypes(/datum/crafting_recipe, GLOB.crafting_recipes) INVOKE_ASYNC(GLOBAL_PROC, /proc/init_ref_coin_values) //so the current procedure doesn't sleep because of UNTIL() diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index fabe70c929..579126257f 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -992,7 +992,7 @@ world letter = lowertext(letter) var/image/text_image = new(loc = A) - text_image.maptext = "[letter]" + text_image.maptext = MAPTEXT("[letter]") text_image.pixel_x = 7 text_image.pixel_y = 5 qdel(atom_icon) diff --git a/code/__HELPERS/priority_announce.dm b/code/__HELPERS/priority_announce.dm index 374e89f715..1aaebc5f32 100644 --- a/code/__HELPERS/priority_announce.dm +++ b/code/__HELPERS/priority_announce.dm @@ -1,8 +1,10 @@ -/proc/priority_announce(text, title = "", sound = "attention", type , sender_override) +/proc/priority_announce(text, title = "", sound, type , sender_override, has_important_message) if(!text) return var/announcement + if(!sound) + sound = "attention" if(type == "Priority") announcement += "

Priority Announcement

" @@ -26,7 +28,11 @@ else GLOB.news_network.SubmitArticle(title + "

" + text, "Central Command", "Station Announcements", null) - announcement += "
[html_encode(text)]
" + ///If the announcer overrides alert messages, use that message. + // if(SSstation.announcer.custom_alert_message && !has_important_message) + // announcement += SSstation.announcer.custom_alert_message + // else + announcement += "
[span_alert("[html_encode(text)]")]
" announcement += "
" var/s = sound(get_announcer_sound(sound)) @@ -127,12 +133,44 @@ if("welcome") . = 'sound/announcer/medibot/welcome.ogg' +/** + * Summon the crew for an emergency meeting + * + * Teleports the crew to a specified area, and tells everyone (via an announcement) who called the meeting. Should only be used during april fools! + * Arguments: + * * user - Mob who called the meeting + * * button_zone - Area where the meeting was called and where everyone will get teleported to + */ +/proc/call_emergency_meeting(mob/living/user, area/button_zone) + var/meeting_sound = sound('sound/misc/emergency_meeting.ogg') + var/announcement + announcement += "

Captain Alert

" + announcement += "
[span_alert("[user] has called an Emergency Meeting!")]

" + + for(var/mob/mob_to_teleport in GLOB.player_list) //gotta make sure the whole crew's here! + if(isnewplayer(mob_to_teleport) || iscameramob(mob_to_teleport)) + continue + to_chat(mob_to_teleport, announcement) + SEND_SOUND(mob_to_teleport, meeting_sound) //no preferences here, you must hear the funny sound + mob_to_teleport.overlay_fullscreen("emergency_meeting", /atom/movable/screen/fullscreen/emergency_meeting, 1) + addtimer(CALLBACK(mob_to_teleport, /mob/.proc/clear_fullscreen, "emergency_meeting"), 3 SECONDS) + + if (is_station_level(mob_to_teleport.z)) //teleport the mob to the crew meeting + var/turf/target + var/list/turf_list = get_area_turfs(button_zone) + while (!target && turf_list.len) + target = pick_n_take(turf_list) + if (isclosedturf(target)) + target = null + continue + mob_to_teleport.forceMove(target) + /proc/print_command_report(text = "", title = null, announce=TRUE) if(!title) title = "Classified [command_name()] Update" if(announce) - priority_announce("A report has been downloaded and printed out at all communications consoles.", "Incoming Classified Message", "commandreport") + priority_announce("A report has been downloaded and printed out at all communications consoles.", "Incoming Classified Message", "commandreport", has_important_message = TRUE) var/datum/comm_message/M = new M.title = title @@ -140,13 +178,17 @@ SScommunications.send_message(M) -/proc/minor_announce(message, title = "Attention:", alert) +/proc/minor_announce(message, title = "Attention:", alert, html_encode = TRUE) if(!message) return + if (html_encode) + title = html_encode(title) + message = html_encode(message) + for(var/mob/M in GLOB.player_list) if(!isnewplayer(M) && M.can_hear()) - to_chat(M, "[html_encode(title)]
[html_encode(message)]

") + to_chat(M, "[span_minorannounce("[title]
[message]")]
") if(M.client.prefs.toggles & SOUND_ANNOUNCEMENTS) if(alert) SEND_SOUND(M, sound('sound/misc/notice1.ogg')) diff --git a/code/__HELPERS/radio.dm b/code/__HELPERS/radio.dm index dc52299025..e761a3a23c 100644 --- a/code/__HELPERS/radio.dm +++ b/code/__HELPERS/radio.dm @@ -1,4 +1,4 @@ -// Ensure the frequency is within bounds of what it should be sending/receiving at +/// Ensure the frequency is within bounds of what it should be sending/receiving at /proc/sanitize_frequency(frequency, free = FALSE) frequency = round(frequency) if(free) @@ -8,12 +8,26 @@ if(!(. % 2)) // Ensure the last digit is an odd number . += 1 -// Format frequency by moving the decimal. +/// Format frequency by moving the decimal. /proc/format_frequency(frequency) frequency = text2num(frequency) return "[round(frequency / 10)].[frequency % 10]" -//Opposite of format, returns as a number +///Opposite of format, returns as a number /proc/unformat_frequency(frequency) frequency = text2num(frequency) return frequency * 10 + +///returns a random unused frequency between MIN_FREE_FREQ & MAX_FREE_FREQ if free = TRUE, and MIN_FREQ & MAX_FREQ if FALSE +/proc/return_unused_frequency(free = FALSE) + var/start = free ? MIN_FREE_FREQ : MIN_FREQ + var/end = free ? MAX_FREE_FREQ : MAX_FREQ + + var/freq_to_check = 0 + do + freq_to_check = rand(start, end) + if(!(freq_to_check % 2)) // Ensure the last digit is an odd number + freq_to_check++ + while((freq_to_check == 0) || ("[freq_to_check]" in GLOB.reverseradiochannels)) + + return freq_to_check diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 9fee97bbb1..1a8852a721 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -663,10 +663,10 @@ Turf and target are separate in case you want to teleport some distance from a t return locate(final_x, final_y, T.z) //Finds the distance between two atoms, in pixels -//centered = 0 counts from turf edge to edge -//centered = 1 counts from turf center to turf center +//centered = FALSE counts from turf edge to edge +//centered = TRUE counts from turf center to turf center //of course mathematically this is just adding world.icon_size on again -/proc/getPixelDistance(atom/A, atom/B, centered = 1) +/proc/getPixelDistance(atom/A, atom/B, centered = TRUE) if(!istype(A)||!istype(B)) return 0 . = bounds_dist(A, B) + sqrt((((A.pixel_x+B.pixel_x)**2) + ((A.pixel_y+B.pixel_y)**2))) diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm index deb35a077d..f537703bbc 100644 --- a/code/_globalvars/lists/flavor_misc.dm +++ b/code/_globalvars/lists/flavor_misc.dm @@ -216,7 +216,13 @@ GLOBAL_LIST_INIT(jumpsuitlist, list(PREF_SUIT, PREF_SKIRT)) #define UPLINK_PDA "PDA" #define UPLINK_RADIO "Radio" #define UPLINK_PEN "Pen" //like a real spy! -GLOBAL_LIST_INIT(uplink_spawn_loc_list, list(UPLINK_PDA, UPLINK_RADIO, UPLINK_PEN)) +#define UPLINK_IMPLANT "Implant" +#define UPLINK_IMPLANT_WITH_PRICE "[UPLINK_IMPLANT] (-[UPLINK_IMPLANT_TELECRYSTAL_COST] TC)" +// What we show to the user +GLOBAL_LIST_INIT(uplink_spawn_loc_list, list(UPLINK_PDA, UPLINK_RADIO, UPLINK_PEN, UPLINK_IMPLANT_WITH_PRICE)) +// What is actually saved; if the uplink implant price changes, it won't affect save files then +GLOBAL_LIST_INIT(uplink_spawn_loc_list_save, list(UPLINK_PDA, UPLINK_RADIO, UPLINK_PEN, UPLINK_IMPLANT)) + //List of cached alpha masked icons. GLOBAL_LIST_EMPTY(alpha_masked_worn_icons) diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm index 9abc37c7e4..14969e6c92 100644 --- a/code/_globalvars/lists/objects.dm +++ b/code/_globalvars/lists/objects.dm @@ -19,7 +19,6 @@ GLOBAL_LIST(chemical_reactions_list) //list of all /datum/chemical_reaction d GLOBAL_LIST(chemical_reagents_list) //list of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff GLOBAL_LIST_EMPTY(tech_list) //list of all /datum/tech datums indexed by id. GLOBAL_LIST_EMPTY(surgeries_list) //list of all surgeries by name, associated with their path. -GLOBAL_LIST_EMPTY(uplink_items) //list of all uplink item typepaths, ascendingly sorted by their initial name. GLOBAL_LIST_EMPTY(uplink_categories) //list of all uplink categories, listed by the order they are loaded in code. Be careful. GLOBAL_LIST_EMPTY(crafting_recipes) //list of all table craft recipes GLOBAL_LIST_EMPTY(rcd_list) //list of Rapid Construction Devices. diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm index 78d802dbbf..843eab6fb0 100644 --- a/code/_globalvars/logging.dm +++ b/code/_globalvars/logging.dm @@ -2,6 +2,9 @@ GLOBAL_VAR(log_directory) GLOBAL_PROTECT(log_directory) GLOBAL_VAR(world_game_log) GLOBAL_PROTECT(world_game_log) +/// Log associated with [/proc/log_suspicious_login()] - Intended to hold all logins that failed due to suspicious circumstances such as ban detection, CID randomisation etc. +GLOBAL_VAR(world_suspicious_login_log) +GLOBAL_PROTECT(world_suspicious_login_log) GLOBAL_VAR(world_runtime_log) GLOBAL_PROTECT(world_runtime_log) GLOBAL_VAR(world_qdel_log) diff --git a/code/_onclick/hud/credits.dm b/code/_onclick/hud/credits.dm index 974f158323..aaf423ecbc 100644 --- a/code/_onclick/hud/credits.dm +++ b/code/_onclick/hud/credits.dm @@ -44,7 +44,7 @@ icon = I parent = P icon_state = credited - maptext = credited + maptext = MAPTEXT(credited) maptext_x = world.icon_size + 8 maptext_y = (world.icon_size / 2) - 4 maptext_width = world.icon_size * 3 diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm index a7247ccf09..81442b602f 100644 --- a/code/_onclick/hud/fullscreen.dm +++ b/code/_onclick/hud/fullscreen.dm @@ -82,6 +82,12 @@ severity = 0 . = ..() +/atom/movable/screen/fullscreen/emergency_meeting + icon_state = "emergency_meeting" + show_when_dead = TRUE + layer = CURSE_LAYER + plane = SPLASHSCREEN_PLANE + /atom/movable/screen/fullscreen/brute icon_state = "brutedamageoverlay" layer = UI_DAMAGE_LAYER diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 87d888e457..7646297163 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -56,16 +56,12 @@ maptext_width = 480 /atom/movable/screen/swap_hand - layer = HUD_LAYER plane = HUD_PLANE name = "swap hand" /atom/movable/screen/swap_hand/Click() // At this point in client Click() code we have passed the 1/10 sec check and little else // We don't even know if it's a middle click - // if(world.time <= usr.next_move) - // return 1 - if(usr.incapacitated()) return 1 @@ -74,6 +70,17 @@ M.swap_hand() return 1 +// /atom/movable/screen/skills +// name = "skills" +// icon = 'icons/mob/screen_midnight.dmi' +// icon_state = "skills" +// screen_loc = ui_skill_menu + +// /atom/movable/screen/skills/Click() +// if(ishuman(usr)) +// var/mob/living/carbon/human/H = usr +// H.mind.print_levels(H) + /atom/movable/screen/craft name = "crafting menu" icon = 'icons/mob/screen_midnight.dmi' @@ -91,7 +98,7 @@ return TRUE var/area/A = get_area(usr) if(!A.outdoors) - to_chat(usr, "There is already a defined structure here.") + to_chat(usr, span_warning("There is already a defined structure here.")) return TRUE create_area(usr) @@ -114,15 +121,12 @@ /// Icon when contains an item. For now used only by humans. var/icon_full /// The overlay when hovering over with an item in your hand - var/list/object_overlays = list() - layer = HUD_LAYER + var/image/object_overlay plane = HUD_PLANE /atom/movable/screen/inventory/Click(location, control, params) // At this point in client Click() code we have passed the 1/10 sec check and little else // We don't even know if it's a middle click - // if(world.time <= usr.next_move) - // return TRUE if(usr.incapacitated()) // ignore_stasis = TRUE return TRUE @@ -138,42 +142,23 @@ usr.update_inv_hands() return TRUE -/atom/movable/screen/inventory/MouseEntered() - ..() +/atom/movable/screen/inventory/MouseEntered(location, control, params) + . = ..() add_overlays() - //Apply the outline affect - add_stored_outline() /atom/movable/screen/inventory/MouseExited() ..() - cut_overlay(object_overlays) - object_overlays.Cut() - remove_stored_outline() - -/atom/movable/screen/inventory/proc/add_stored_outline() - if(hud?.mymob && slot_id) - var/obj/item/inv_item = hud.mymob.get_item_by_slot(slot_id) - if(inv_item) - if(hud?.mymob.incapacitated() || (slot_id in hud?.mymob.check_obscured_slots()) || !hud?.mymob.canUnEquip(inv_item)) - inv_item.apply_outline(_size = 3) - else - inv_item.apply_outline() - -/atom/movable/screen/inventory/proc/remove_stored_outline() - if(hud?.mymob && slot_id) - var/obj/item/inv_item = hud.mymob.get_item_by_slot(slot_id) - if(inv_item) - inv_item.remove_outline() + cut_overlay(object_overlay) + QDEL_NULL(object_overlay) /atom/movable/screen/inventory/update_icon_state() if(!icon_empty) icon_empty = icon_state - if(hud?.mymob && slot_id && icon_full) - if(hud.mymob.get_item_by_slot(slot_id)) - icon_state = icon_full - else - icon_state = icon_empty + if(!hud?.mymob || !slot_id || !icon_full) + return ..() + icon_state = hud.mymob.get_item_by_slot(slot_id) ? icon_full : icon_empty + return ..() /atom/movable/screen/inventory/proc/add_overlays() var/mob/user = hud?.mymob @@ -189,14 +174,14 @@ var/image/item_overlay = image(holding) item_overlay.alpha = 92 - if(!user.can_equip(holding, slot_id, TRUE, TRUE)) + if(!user.can_equip(holding, slot_id, disable_warning = TRUE, bypass_equip_delay_self = TRUE)) item_overlay.color = "#FF0000" else item_overlay.color = "#00ff00" - cut_overlay(object_overlays) - // object_overlay = item_overlay - add_overlay(object_overlays) + cut_overlay(object_overlay) + object_overlay = item_overlay + add_overlay(object_overlay) /atom/movable/screen/inventory/hand var/mutable_appearance/handcuff_overlay @@ -232,8 +217,6 @@ var/mob/user = hud?.mymob if(usr != user) return TRUE - // if(world.time <= user.next_move) - // return TRUE if(user.incapacitated()) return TRUE if (ismecha(user.loc)) // stops inventory actions in a mech @@ -247,12 +230,24 @@ user.swap_hand(held_index) return TRUE +// /atom/movable/screen/close +// name = "close" +// plane = ABOVE_HUD_PLANE +// icon_state = "backpack_close" + +// /atom/movable/screen/close/Initialize(mapload, new_master) +// . = ..() +// master = new_master + +// /atom/movable/screen/close/Click() +// var/datum/component/storage/S = master +// S.hide_from(usr) +// return TRUE /atom/movable/screen/drop name = "drop" icon = 'icons/mob/screen_midnight.dmi' icon_state = "act_drop" - layer = HUD_LAYER plane = HUD_PLANE /atom/movable/screen/drop/Click() @@ -308,12 +303,12 @@ if(C.internal) C.internal = null - to_chat(C, "You are no longer running on internals.") + to_chat(C, span_notice("You are no longer running on internals.")) icon_state = "internal0" else if(!C.getorganslot(ORGAN_SLOT_BREATHING_TUBE)) if(HAS_TRAIT(C, TRAIT_NO_INTERNALS)) - to_chat(C, "Due to cumbersome equipment or anatomy, you are currently unable to use internals!") + to_chat(C, span_warning("Due to cumbersome equipment or anatomy, you are currently unable to use internals!")) return var/obj/item/clothing/check var/internals = FALSE @@ -326,37 +321,37 @@ if((check.clothing_flags & ALLOWINTERNALS)) internals = TRUE if(!internals) - to_chat(C, "You are not wearing an internals mask!") + to_chat(C, span_warning("You are not wearing an internals mask!")) return var/obj/item/I = C.is_holding_item_of_type(/obj/item/tank) if(I) - to_chat(C, "You are now running on internals from [I] in your [C.get_held_index_name(C.get_held_index_of_item(I))].") + to_chat(C, span_notice("You are now running on internals from [I] in your [C.get_held_index_name(C.get_held_index_of_item(I))].")) C.internal = I else if(ishuman(C)) var/mob/living/carbon/human/H = C if(istype(H.s_store, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.s_store] on your [H.wear_suit.name].") + to_chat(H, span_notice("You are now running on internals from [H.s_store] on your [H.wear_suit.name].")) H.internal = H.s_store else if(istype(H.belt, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.belt] on your belt.") + to_chat(H, span_notice("You are now running on internals from [H.belt] on your belt.")) H.internal = H.belt else if(istype(H.l_store, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.l_store] in your left pocket.") + to_chat(H, span_notice("You are now running on internals from [H.l_store] in your left pocket.")) H.internal = H.l_store else if(istype(H.r_store, /obj/item/tank)) - to_chat(H, "You are now running on internals from [H.r_store] in your right pocket.") + to_chat(H, span_notice("You are now running on internals from [H.r_store] in your right pocket.")) H.internal = H.r_store //Separate so CO2 jetpacks are a little less cumbersome. if(!C.internal && istype(C.back, /obj/item/tank)) - to_chat(C, "You are now running on internals from [C.back] on your back.") + to_chat(C, span_notice("You are now running on internals from [C.back] on your back.")) C.internal = C.back if(C.internal) icon_state = "internal1" else - to_chat(C, "You don't have an oxygen tank!") + to_chat(C, span_warning("You don't have an oxygen tank!")) return C.update_action_buttons_icon() @@ -378,6 +373,7 @@ icon_state = CONFIG_GET(flag/sprint_enabled)? "walking" : "walking_nosprint" if(MOVE_INTENT_RUN) icon_state = CONFIG_GET(flag/sprint_enabled)? "running" : "running_nosprint" + return ..() /atom/movable/screen/mov_intent/proc/toggle(mob/user) if(isobserver(user)) @@ -399,12 +395,12 @@ icon_state = "pull" else icon_state = "pull0" + return ..() /atom/movable/screen/resist name = "resist" icon = 'icons/mob/screen_midnight.dmi' icon_state = "act_resist" - layer = HUD_LAYER plane = HUD_PLANE /atom/movable/screen/resist/Click() @@ -416,7 +412,6 @@ name = "rest" icon = 'icons/mob/screen_midnight.dmi' icon_state = "act_rest" - layer = HUD_LAYER plane = HUD_PLANE /atom/movable/screen/rest/Click() @@ -427,11 +422,12 @@ /atom/movable/screen/rest/update_icon_state() var/mob/living/user = hud?.mymob if(!istype(user)) - return + return ..() if(!user.resting) icon_state = "act_rest" else icon_state = "act_rest0" + return ..() /atom/movable/screen/throw_catch name = "throw/catch" @@ -455,9 +451,9 @@ if(isobserver(usr)) return - var/list/PL = params2list(params) - var/icon_x = text2num(PL["icon-x"]) - var/icon_y = text2num(PL["icon-y"]) + var/list/modifiers = params2list(params) + var/icon_x = text2num(LAZYACCESS(modifiers, "icon-x")) + var/icon_y = text2num(LAZYACCESS(modifiers, "icon-y")) var/choice = get_zone_at(icon_x, icon_y) if (!choice) return 1 @@ -465,15 +461,16 @@ return set_selected_zone(choice, usr) /atom/movable/screen/zone_sel/MouseEntered(location, control, params) + . = ..() MouseMove(location, control, params) /atom/movable/screen/zone_sel/MouseMove(location, control, params) if(isobserver(usr)) return - var/list/PL = params2list(params) - var/icon_x = text2num(PL["icon-x"]) - var/icon_y = text2num(PL["icon-y"]) + var/list/modifiers = params2list(params) + var/icon_x = text2num(LAZYACCESS(modifiers, "icon-x")) + var/icon_y = text2num(LAZYACCESS(modifiers, "icon-y")) var/choice = get_zone_at(icon_x, icon_y) if(hovering == choice) @@ -493,7 +490,6 @@ mouse_opacity = MOUSE_OPACITY_TRANSPARENT alpha = 128 anchored = TRUE - layer = ABOVE_HUD_LAYER plane = ABOVE_HUD_PLANE /atom/movable/screen/zone_sel/MouseExited(location, control, params) @@ -545,7 +541,7 @@ if(choice != hud.mymob.zone_selected) hud.mymob.zone_selected = choice - update_icon() + update_appearance() return TRUE @@ -562,7 +558,6 @@ /atom/movable/screen/zone_sel/robot icon = 'icons/mob/screen_cyborg.dmi' - /atom/movable/screen/flash name = "flash" icon_state = "blank" @@ -651,6 +646,11 @@ name = "health doll" screen_loc = ui_healthdoll +/atom/movable/screen/healthdoll/Click() + if (iscarbon(usr)) + var/mob/living/carbon/C = usr + C.check_self_for_injuries() + /atom/movable/screen/healthdoll/living icon_state = "fullhealth0" screen_loc = ui_living_healthdoll @@ -661,6 +661,9 @@ icon_state = "mood5" screen_loc = ui_mood +/atom/movable/screen/mood/attack_tk() + return + /atom/movable/screen/splash icon = 'icons/blank_title.png' icon_state = "" @@ -671,6 +674,8 @@ /atom/movable/screen/splash/New(client/C, visible, use_previous_title) //TODO: Make this use INITIALIZE_IMMEDIATE, except its not easy . = ..() + if(!istype(C)) + return holder = C diff --git a/code/controllers/configuration/entries/logging.dm b/code/controllers/configuration/entries/logging.dm index 25ca145939..54bd5cb713 100644 --- a/code/controllers/configuration/entries/logging.dm +++ b/code/controllers/configuration/entries/logging.dm @@ -4,6 +4,10 @@ /datum/config_entry/flag/log_access // log login/logout config_entry_value = TRUE +/// Config entry which special logging of failed logins under suspicious circumstances. +/datum/config_entry/flag/log_suspicious_login + config_entry_value = TRUE + /datum/config_entry/flag/log_say // log client say config_entry_value = TRUE diff --git a/code/controllers/subsystem/communications.dm b/code/controllers/subsystem/communications.dm index 718357b8bb..58a88890dd 100644 --- a/code/controllers/subsystem/communications.dm +++ b/code/controllers/subsystem/communications.dm @@ -1,33 +1,67 @@ -#define COMMUNICATION_COOLDOWN 300 -#define COMMUNICATION_COOLDOWN_AI 300 +#define COMMUNICATION_COOLDOWN (30 SECONDS) +#define COMMUNICATION_COOLDOWN_AI (30 SECONDS) +#define COMMUNICATION_COOLDOWN_MEETING (5 MINUTES) SUBSYSTEM_DEF(communications) name = "Communications" flags = SS_NO_INIT | SS_NO_FIRE - var/silicon_message_cooldown - var/nonsilicon_message_cooldown + COOLDOWN_DECLARE(silicon_message_cooldown) + COOLDOWN_DECLARE(nonsilicon_message_cooldown) + COOLDOWN_DECLARE(emergency_meeting_cooldown) /datum/controller/subsystem/communications/proc/can_announce(mob/living/user, is_silicon) - if(is_silicon && silicon_message_cooldown > world.time) - . = FALSE - else if(!is_silicon && nonsilicon_message_cooldown > world.time) - . = FALSE + if(is_silicon && COOLDOWN_FINISHED(src, silicon_message_cooldown)) + return TRUE + else if(!is_silicon && COOLDOWN_FINISHED(src, nonsilicon_message_cooldown)) + return TRUE else - . = TRUE + return FALSE /datum/controller/subsystem/communications/proc/make_announcement(mob/living/user, is_silicon, input) if(!can_announce(user, is_silicon)) return FALSE if(is_silicon) minor_announce(html_decode(input),"[user.name] Announces:") - silicon_message_cooldown = world.time + COMMUNICATION_COOLDOWN_AI + COOLDOWN_START(src, silicon_message_cooldown, COMMUNICATION_COOLDOWN_AI) else - priority_announce(html_decode(user.treat_message(input)), null, 'sound/misc/announce.ogg', "Captain") - nonsilicon_message_cooldown = world.time + COMMUNICATION_COOLDOWN + priority_announce(html_decode(user.treat_message(input)), null, 'sound/misc/announce.ogg', "Captain", has_important_message = TRUE) + COOLDOWN_START(src, nonsilicon_message_cooldown, COMMUNICATION_COOLDOWN) user.log_talk(input, LOG_SAY, tag="priority announcement") message_admins("[ADMIN_LOOKUPFLW(user)] has made a priority announcement.") +/** + * Check if a mob can call an emergency meeting + * + * Should only really happen during april fools. + * Checks to see that it's been at least 5 minutes since the last emergency meeting call. + * Arguments: + * * user - Mob who called the meeting + */ +/datum/controller/subsystem/communications/proc/can_make_emergency_meeting(mob/living/user) + if(!(SSevents.holidays && SSevents.holidays[APRIL_FOOLS])) + return FALSE + else if(COOLDOWN_FINISHED(src, emergency_meeting_cooldown)) + return TRUE + else + return FALSE + +/** + * Call an emergency meeting + * + * Communications subsystem wrapper for the call_emergency_meeting world proc. + * Checks to make sure the proc can be called, and handles + * relevant logging and timing. See that proc definition for more detail. + * Arguments: + * * user - Mob who called the meeting + */ +/datum/controller/subsystem/communications/proc/emergency_meeting(mob/living/user) + if(!can_make_emergency_meeting(user)) + return FALSE + call_emergency_meeting(user, get_area(user)) + COOLDOWN_START(src, emergency_meeting_cooldown, COMMUNICATION_COOLDOWN_MEETING) + message_admins("[ADMIN_LOOKUPFLW(user)] has called an emergency meeting.") + /datum/controller/subsystem/communications/proc/send_message(datum/comm_message/sending,print = TRUE,unique = FALSE) for(var/obj/machinery/computer/communications/C in GLOB.machines) if(!(C.stat & (BROKEN|NOPOWER)) && is_station_level(C.z)) @@ -40,7 +74,7 @@ SUBSYSTEM_DEF(communications) var/obj/item/paper/P = new /obj/item/paper(C.loc) P.name = "paper - '[sending.title]'" P.info = sending.content - P.update_icon() + P.update_appearance() #undef COMMUNICATION_COOLDOWN #undef COMMUNICATION_COOLDOWN_AI diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm index 661eafc2a5..058645da49 100644 --- a/code/controllers/subsystem/processing/processing.dm +++ b/code/controllers/subsystem/processing/processing.dm @@ -17,7 +17,6 @@ SUBSYSTEM_DEF(processing) /datum/controller/subsystem/processing/fire(resumed = FALSE) if (!resumed) currentrun = processing.Copy() - var/delta_time = (flags & SS_TICKER)? (wait * world.tick_lag * 0.1) : (wait * 0.1) //cache for sanic speed (lists are references anyways) var/list/current_run = currentrun @@ -26,7 +25,7 @@ SUBSYSTEM_DEF(processing) current_run.len-- if(QDELETED(thing)) processing -= thing - else if(thing.process(delta_time) == PROCESS_KILL) + else if(thing.process(wait * 0.1) == PROCESS_KILL) // fully stop so that a future START_PROCESSING will work STOP_PROCESSING(src, thing) if (MC_TICK_CHECK) @@ -47,5 +46,5 @@ SUBSYSTEM_DEF(processing) * If you override this do not call parent, as it will return PROCESS_KILL. This is done to prevent objects that dont override process() from staying in the processing list */ /datum/proc/process(delta_time) - SHOULD_NOT_SLEEP(TRUE) + set waitfor = FALSE return PROCESS_KILL diff --git a/code/controllers/subsystem/sound_loops.dm b/code/controllers/subsystem/sound_loops.dm new file mode 100644 index 0000000000..c1e6d097d6 --- /dev/null +++ b/code/controllers/subsystem/sound_loops.dm @@ -0,0 +1,3 @@ +PROCESSING_SUBSYSTEM_DEF(sound_loops) + name = "Sound Loops" + priority = FIRE_PRIORITY_SOUND_LOOPS diff --git a/code/controllers/subsystem/sounds.dm b/code/controllers/subsystem/sounds.dm index fa9ba3c472..7d94a3d21d 100644 --- a/code/controllers/subsystem/sounds.dm +++ b/code/controllers/subsystem/sounds.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(sounds) name = "Sounds" flags = SS_NO_FIRE init_order = INIT_ORDER_SOUNDS - var/static/using_channels_max = CHANNEL_HIGHEST_AVAILABLE //BYOND max channels + var/static/using_channels_max = CHANNEL_HIGHEST_AVAILABLE //BYOND max channels /// Amount of channels to reserve for random usage rather than reservations being allowed to reserve all channels. Also a nice safeguard for when someone screws up. var/static/random_channels_min = 50 @@ -42,7 +42,7 @@ SUBSYSTEM_DEF(sounds) var/text_channel = num2text(channel) var/using = using_channels[text_channel] using_channels -= text_channel - if(using != TRUE) // datum channel + if(using != TRUE) // datum channel using_channels_by_datum[using] -= channel if(!length(using_channels_by_datum[using])) using_channels_by_datum -= using @@ -65,7 +65,7 @@ SUBSYSTEM_DEF(sounds) /// NO AUTOMATIC CLEANUP - If you use this, you better manually free it later! Returns an integer for channel. /datum/controller/subsystem/sounds/proc/reserve_sound_channel_datumless() . = reserve_channel() - if(!.) //oh no.. + if(!.) //oh no.. return FALSE var/text_channel = num2text(.) using_channels[text_channel] = DATUMLESS @@ -74,7 +74,7 @@ SUBSYSTEM_DEF(sounds) /// Reserves a channel for a datum. Automatic cleanup only when the datum is deleted. Returns an integer for channel. /datum/controller/subsystem/sounds/proc/reserve_sound_channel(datum/D) - if(!D) //i don't like typechecks but someone will fuck it up + if(!D) //i don't like typechecks but someone will fuck it up CRASH("Attempted to reserve sound channel without datum using the managed proc.") .= reserve_channel() if(!.) @@ -89,7 +89,7 @@ SUBSYSTEM_DEF(sounds) */ /datum/controller/subsystem/sounds/proc/reserve_channel() PRIVATE_PROC(TRUE) - if(channel_reserve_high <= random_channels_min) // out of channels + if(channel_reserve_high <= random_channels_min) // out of channels return var/channel = channel_list[channel_reserve_high] reserved_channels[num2text(channel)] = channel_reserve_high-- @@ -112,7 +112,7 @@ SUBSYSTEM_DEF(sounds) // now, an existing reserved channel will likely (exception: unreserving last reserved channel) be at index // get it, and update position. var/text_reserved = num2text(channel_list[index]) - if(!reserved_channels[text_reserved]) //if it isn't already reserved make sure we don't accidently mistakenly put it on reserved list! + if(!reserved_channels[text_reserved]) //if it isn't already reserved make sure we don't accidently mistakenly put it on reserved list! return reserved_channels[text_reserved] = index diff --git a/code/datums/browser.dm b/code/datums/browser.dm index 2057cbb21a..e8c7615ecb 100644 --- a/code/datums/browser.dm +++ b/code/datums/browser.dm @@ -4,7 +4,7 @@ var/window_id // window_id is used as the window name for browse and onclose var/width = 0 var/height = 0 - var/atom/ref = null + var/datum/weakref/ref = null var/window_options = "can_close=1;can_minimize=1;can_maximize=0;can_resize=1;titlebar=1;" // window option is set using window_id var/stylesheets[0] var/scripts[0] @@ -16,8 +16,8 @@ /datum/browser/New(nuser, nwindow_id, ntitle = 0, nwidth = 0, nheight = 0, atom/nref = null) - user = nuser + RegisterSignal(user, COMSIG_PARENT_QDELETING, .proc/user_deleted) window_id = nwindow_id if (ntitle) title = format_text(ntitle) @@ -26,7 +26,11 @@ if (nheight) height = nheight if (nref) - ref = nref + ref = WEAKREF(nref) + +/datum/browser/proc/user_deleted(datum/source) + SIGNAL_HANDLER + user = null /datum/browser/proc/add_head_content(nhead_content) head_content = nhead_content @@ -35,7 +39,7 @@ window_options = nwindow_options /datum/browser/proc/add_stylesheet(name, file) - if(istype(name, /datum/asset/spritesheet)) + if (istype(name, /datum/asset/spritesheet)) var/datum/asset/spritesheet/sheet = name stylesheets["spritesheet_[sheet.name].css"] = "data/spritesheets/[sheet.name]" else @@ -94,27 +98,32 @@ "} /datum/browser/proc/open(use_onclose = TRUE) - if(isnull(window_id)) //null check because this can potentially nuke goonchat + if(isnull(window_id)) //null check because this can potentially nuke goonchat WARNING("Browser [title] tried to open with a null ID") - to_chat(user, "The [title] browser you tried to open failed a sanity check! Please report this on github!") + to_chat(user, span_userdanger("The [title] browser you tried to open failed a sanity check! Please report this on github!")) return var/window_size = "" - if(width && height) + if (width && height) window_size = "size=[width]x[height];" common_asset.send(user) - if(stylesheets.len) + if (stylesheets.len) SSassets.transport.send_assets(user, stylesheets) - if(scripts.len) + if (scripts.len) SSassets.transport.send_assets(user, scripts) user << browse(get_content(), "window=[window_id];[window_size][window_options]") - if(use_onclose) + if (use_onclose) setup_onclose() /datum/browser/proc/setup_onclose() set waitfor = 0 //winexists sleeps, so we don't need to. for (var/i in 1 to 10) - if (user && winexists(user, window_id)) - onclose(user, window_id, ref) + if (user?.client && winexists(user, window_id)) + var/atom/send_ref + if(ref) + send_ref = ref.resolve() + if(!send_ref) + ref = null + onclose(user, window_id, send_ref) break /datum/browser/proc/close() @@ -153,11 +162,35 @@ opentime = 0 close() -//designed as a drop in replacement for alert(); functions the same. (outside of needing User specified) -/proc/tgalert(mob/User, Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1, Timeout = 6000) +/** + * **DEPRECATED: USE tgui_alert(...) INSTEAD** + * + * Designed as a drop in replacement for alert(); functions the same. (outside of needing User specified) + * Arguments: + * * User - The user to show the alert to. + * * Message - The textual body of the alert. + * * Title - The title of the alert's window. + * * Button1 - The first button option. + * * Button2 - The second button option. + * * Button3 - The third button option. + * * StealFocus - Boolean operator controlling if the alert will steal the user's window focus. + * * Timeout - The timeout of the window, after which no responses will be valid. + */ +/proc/tgalert(mob/User, Message, Title, Button1="Ok", Button2, Button3, StealFocus = TRUE, Timeout = 6000) if (!User) User = usr - switch(askuser(User, Message, Title, Button1, Button2, Button3, StealFocus, Timeout)) + if (!istype(User)) + if (istype(User, /client)) + var/client/client = User + User = client.mob + else + return + + // Get user's response using a modal + var/datum/browser/modal/alert/A = new(User, Message, Title, Button1, Button2, Button3, StealFocus, Timeout) + A.open() + A.wait() + switch(A.selectedbutton) if (1) return Button1 if (2) @@ -197,8 +230,8 @@ .=..() opentime = 0 -/datum/browser/modal/open(use_onclose = 1) - set waitfor = 0 +/datum/browser/modal/open(use_onclose) + set waitfor = FALSE opentime = world.time if (stealfocus) @@ -277,7 +310,7 @@ opentime = 0 close() -/proc/presentpicker(var/mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/values, inputtype = "checkbox", width, height, slidecolor) +/proc/presentpicker(mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/values, inputtype = "checkbox", width, height, slidecolor) if (!istype(User)) if (istype(User, /client/)) var/client/C = User @@ -290,7 +323,7 @@ if (A.selectedbutton) return list("button" = A.selectedbutton, "values" = A.valueslist) -/proc/input_bitfield(var/mob/User, title, bitfield, current_value, nwidth = 350, nheight = 350, nslidecolor, allowed_edit_list = null) +/proc/input_bitfield(mob/User, title, bitfield, current_value, nwidth = 350, nheight = 350, nslidecolor, allowed_edit_list = null) if (!User || !(bitfield in GLOB.bitfields)) return var/list/pickerlist = list() @@ -401,7 +434,7 @@ opentime = 0 close() -/proc/presentpreflikepicker(var/mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/settings, width, height, slidecolor) +/proc/presentpreflikepicker(mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1,Timeout = 6000,list/settings, width, height, slidecolor) if (!istype(User)) if (istype(User, /client/)) var/client/C = User @@ -414,12 +447,6 @@ if (A.selectedbutton) return list("button" = A.selectedbutton, "settings" = A.settings) -// This will allow you to show an icon in the browse window -// This is added to mob so that it can be used without a reference to the browser object -// There is probably a better place for this... -/mob/proc/browse_rsc_icon(icon, icon_state, dir = -1) - - // Registers the on-close verb for a browse window (client/verb/.windowclose) // this will be called when the close-button of a window is pressed. // @@ -427,8 +454,8 @@ // e.g. canisters, timers, etc. // // windowid should be the specified window name -// e.g. code is : user << browse(text, "window=fred") -// then use : onclose(user, "fred") +// e.g. code is : user << browse(text, "window=fred") +// then use : onclose(user, "fred") // // Optionally, specify the "ref" parameter as the controlled atom (usually src) // to pass a "close=1" parameter to the atom's Topic() proc for special handling. @@ -451,18 +478,18 @@ // otherwise, just reset the client mob's machine var. // /client/verb/windowclose(atomref as text) - set hidden = TRUE // hide this verb from the user's panel - set name = ".windowclose" // no autocomplete on cmd line + set hidden = TRUE // hide this verb from the user's panel + set name = ".windowclose" // no autocomplete on cmd line - if(atomref!="null") // if passed a real atomref - var/hsrc = locate(atomref) // find the reffed atom + if(atomref!="null") // if passed a real atomref + var/hsrc = locate(atomref) // find the reffed atom var/href = "close=1" if(hsrc) usr = src.mob - src.Topic(href, params2list(href), hsrc) // this will direct to the atom's - return // Topic() proc via client.Topic() + src.Topic(href, params2list(href), hsrc) // this will direct to the atom's + return // Topic() proc via client.Topic() // no atomref specified (or not found) // so just reset the user mob's machine var - if(src && src.mob) + if(src?.mob) src.mob.unset_machine() diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index b0ff5f8ee0..b6f9a65115 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -20,8 +20,6 @@ #define CHAT_LAYER_Z_STEP 0.0001 /// The number of z-layer 'slices' usable by the chat message layering #define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP -/// Macro from Lummox used to get height from a MeasureText proc -#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1)) /** * # Chat Message Overlay @@ -201,7 +199,7 @@ message.maptext_width = CHAT_MESSAGE_WIDTH message.maptext_height = mheight message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5 - message.maptext = complete_text + message.maptext = MAPTEXT(complete_text) // View the message LAZYADDASSOC(owned_by.seen_messages, message_loc, src) diff --git a/code/datums/components/area_sound_manager.dm b/code/datums/components/area_sound_manager.dm new file mode 100644 index 0000000000..50bb77772f --- /dev/null +++ b/code/datums/components/area_sound_manager.dm @@ -0,0 +1,77 @@ +///Allows you to set a theme for a set of areas without tying them to looping sounds explicitly +/datum/component/area_sound_manager + ///area -> looping sound type + var/list/area_to_looping_type = list() + ///Current sound loop + var/datum/looping_sound/our_loop + ///A list of "acceptable" z levels to be on. If you leave this, we're gonna delete ourselves + var/list/accepted_zs + ///The timer id of our current start delay, if it exists + var/timerid + +/datum/component/area_sound_manager/Initialize(area_loop_pairs, change_on, remove_on, acceptable_zs) + if(!ismovable(parent)) + return + area_to_looping_type = area_loop_pairs + accepted_zs = acceptable_zs + change_the_track() + + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/react_to_move) + RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, .proc/react_to_z_move) + RegisterSignal(parent, change_on, .proc/handle_change) + RegisterSignal(parent, remove_on, .proc/handle_removal) + +/datum/component/area_sound_manager/Destroy(force, silent) + QDEL_NULL(our_loop) + . = ..() + +/datum/component/area_sound_manager/proc/react_to_move(datum/source, atom/oldloc, dir, forced) + SIGNAL_HANDLER + var/list/loop_lookup = area_to_looping_type + if(loop_lookup[get_area(oldloc)] == loop_lookup[get_area(parent)]) + return + change_the_track(TRUE) + +/datum/component/area_sound_manager/proc/react_to_z_move(datum/source, old_z, new_z) + SIGNAL_HANDLER + if(!length(accepted_zs) || (new_z in accepted_zs)) + return + qdel(src) + +/datum/component/area_sound_manager/proc/handle_removal(datum/source) + SIGNAL_HANDLER + qdel(src) + +/datum/component/area_sound_manager/proc/handle_change(datum/source) + SIGNAL_HANDLER + change_the_track() + +/datum/component/area_sound_manager/proc/change_the_track(skip_start = FALSE) + var/time_remaining = 0 + + if(our_loop) + var/our_id = our_loop.timerid || timerid + if(our_id) + time_remaining = timeleft(our_id, SSsound_loops) || 0 + //Time left will sometimes return negative values, just ignore them and start a new sound loop now + time_remaining = max(time_remaining, 0) + QDEL_NULL(our_loop) + + var/area/our_area = get_area(parent) + var/new_loop_type = area_to_looping_type[our_area] + if(!new_loop_type) + return + + our_loop = new new_loop_type(parent, FALSE, TRUE, skip_start) + + //If we're still playing, wait a bit before changing the sound so we don't double up + if(time_remaining) + timerid = addtimer(CALLBACK(src, .proc/start_looping_sound), time_remaining, TIMER_UNIQUE | TIMER_CLIENT_TIME | TIMER_STOPPABLE | TIMER_NO_HASH_WAIT | TIMER_DELETE_ME, SSsound_loops) + return + timerid = null + our_loop.start() + +/datum/component/area_sound_manager/proc/start_looping_sound() + timerid = null + if(our_loop) + our_loop.start() diff --git a/code/datums/components/storage/concrete/_concrete.dm b/code/datums/components/storage/concrete/_concrete.dm index bb226bbfdf..6f08959dd2 100644 --- a/code/datums/components/storage/concrete/_concrete.dm +++ b/code/datums/components/storage/concrete/_concrete.dm @@ -66,7 +66,7 @@ /datum/component/storage/concrete/_insert_physical_item(obj/item/I, override = FALSE) . = TRUE var/atom/real_location = real_location() - if(I.loc != real_location && real_location) + if(real_location && I.loc != real_location) I.forceMove(real_location) refresh_mob_views() @@ -95,16 +95,19 @@ return FALSE /datum/component/storage/concrete/proc/on_contents_del(datum/source, atom/A) + SIGNAL_HANDLER var/atom/real_location = parent if(A in real_location) usr = null remove_from_storage(A, null) /datum/component/storage/concrete/proc/on_deconstruct(datum/source, disassembled) + SIGNAL_HANDLER if(drop_all_on_deconstruct) do_quick_empty() /datum/component/storage/concrete/proc/on_break(datum/source, damage_flag) + SIGNAL_HANDLER if(drop_all_on_break) do_quick_empty() if(unlock_on_break) @@ -131,14 +134,16 @@ var/list/seeing_mobs = can_see_contents() for(var/mob/M in seeing_mobs) M.client.screen -= AM - if(ismob(parent.loc) && isitem(AM)) - var/obj/item/I = AM - var/mob/M = parent.loc - I.dropped(M) - I.item_flags &= ~IN_STORAGE - I.remove_outline() + if(isitem(AM)) + var/obj/item/removed_item = AM + removed_item.item_flags &= ~IN_STORAGE + if(ismob(parent.loc)) + var/mob/carrying_mob = parent.loc + removed_item.dropped(carrying_mob, TRUE) if(new_location) - AM.forceMove(new_location) // exited comsig will handle removal reset. + //Reset the items values + _removal_reset(AM) + AM.forceMove(new_location) //We don't want to call this if the item is being destroyed AM.on_exit_storage(src) else @@ -147,7 +152,7 @@ refresh_mob_views() if(isobj(parent)) var/obj/O = parent - O.update_icon() + O.update_appearance() return TRUE /datum/component/storage/concrete/proc/slave_can_insert_object(datum/component/storage/slave, obj/item/I, stop_messages = FALSE, mob/M) @@ -158,7 +163,7 @@ if(. && !prevent_warning) slave.mob_item_insertion_feedback(usr, M, I) -/datum/component/storage/concrete/handle_item_insertion(obj/item/I, prevent_warning = FALSE, mob/M, datum/component/storage/remote) //Remote is null or the slave datum +/datum/component/storage/concrete/handle_item_insertion(obj/item/I, prevent_warning = FALSE, mob/M, datum/component/storage/remote) //Remote is null or the slave datum var/datum/component/storage/concrete/master = master() var/atom/parent = src.parent var/moved = FALSE @@ -168,7 +173,7 @@ if(!M.temporarilyRemoveItemFromInventory(I)) return FALSE else - moved = TRUE //At this point if the proc fails we need to manually move the object back to the turf/mob/whatever. + moved = TRUE //At this point if the proc fails we need to manually move the object back to the turf/mob/whatever. if(I.pulledby) I.pulledby.stop_pulling() if(silent) @@ -203,7 +208,7 @@ /datum/component/storage/concrete/update_icon() if(isobj(parent)) var/obj/O = parent - O.update_icon() + O.update_appearance() for(var/i in slaves) var/datum/component/storage/slave = i slave.update_icon() diff --git a/code/datums/components/storage/storage.dm b/code/datums/components/storage/storage.dm index 56ac899ec6..8f0bd008b4 100644 --- a/code/datums/components/storage/storage.dm +++ b/code/datums/components/storage/storage.dm @@ -419,39 +419,38 @@ return TRUE /datum/component/storage/proc/mousedrop_onto(datum/source, atom/over_object, mob/M) + SIGNAL_HANDLER + set waitfor = FALSE . = COMPONENT_NO_MOUSEDROP + if(!ismob(M)) + return + if(!over_object) + return + if(ismecha(M.loc)) // stops inventory actions in a mech + return + if(M.incapacitated() || !M.canUseStorage()) + return var/atom/A = parent - if(ismob(M)) //all the check for item manipulation are in other places, you can safely open any storages as anything and its not buggy, i checked - A.add_fingerprint(M) - if(istype(A, /obj/item)) - var/obj/item/I = A - I.remove_outline() //Removes the outline when we drag - if(!over_object) - return FALSE - if(ismecha(M.loc)) // stops inventory actions in a mech - return FALSE - // this must come before the screen objects only block, dunno why it wasn't before - if(over_object == M) - user_show_to_mob(M, trigger_on_found = TRUE) - return - if(isrevenant(M)) - RevenantThrow(over_object, M, source) - return - if(!M.incapacitated()) - if(!istype(over_object, /atom/movable/screen)) - dump_content_at(over_object, M) - return - if(A.loc != M) - return - if(rustle_sound) - playsound(A, "rustle", 50, 1, -5) - A.do_jiggle() - if(istype(over_object, /atom/movable/screen/inventory/hand)) - var/atom/movable/screen/inventory/hand/H = over_object - M.putItemFromInventoryInHandIfPossible(A, H.held_index) - return - A.add_fingerprint(M) + A.add_fingerprint(M) + // this must come before the screen objects only block, dunno why it wasn't before + if(over_object == M) + user_show_to_mob(M, trigger_on_found = TRUE) + if(isrevenant(M)) + INVOKE_ASYNC(GLOBAL_PROC, .proc/RevenantThrow, over_object, M, source) + return + if(!istype(over_object, /atom/movable/screen)) + INVOKE_ASYNC(src, .proc/dump_content_at, over_object, M) + return + if(A.loc != M) + return + playsound(A, "rustle", 50, TRUE, -5) + A.do_jiggle() + if(istype(over_object, /atom/movable/screen/inventory/hand)) + var/atom/movable/screen/inventory/hand/H = over_object + M.putItemFromInventoryInHandIfPossible(A, H.held_index) + return + A.add_fingerprint(M) /datum/component/storage/proc/user_show_to_mob(mob/M, force = FALSE, trigger_on_found = FALSE) var/atom/A = parent diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm index 433e57abbe..c0ec25dfc2 100644 --- a/code/datums/components/uplink.dm +++ b/code/datums/components/uplink.dm @@ -1,4 +1,5 @@ GLOBAL_LIST_EMPTY(uplinks) +#define PEN_ROTATIONS 2 /** * Uplinks @@ -17,7 +18,7 @@ GLOBAL_LIST_EMPTY(uplinks) var/telecrystals var/selected_cat var/owner = null - var/datum/game_mode/gamemode + var/uplink_flag var/datum/uplink_purchase_log/purchase_log var/list/uplink_items var/hidden_crystals = 0 @@ -26,11 +27,11 @@ GLOBAL_LIST_EMPTY(uplinks) var/failsafe_code var/compact_mode = FALSE var/debug = FALSE - var/saved_player_population = 0 - var/list/filters = list() + ///Instructions on how to access the uplink based on location + var/unlock_text + var/list/previous_attempts - -/datum/component/uplink/Initialize(_owner, _lockable = TRUE, _enabled = FALSE, datum/game_mode/_gamemode, starting_tc = 20, datum/traitor_class/traitor_class) +/datum/component/uplink/Initialize(_owner, _lockable = TRUE, _enabled = FALSE, uplink_flag = UPLINK_TRAITORS, starting_tc = TELECRYSTALS_DEFAULT) if(!isitem(parent)) return COMPONENT_INCOMPATIBLE @@ -44,16 +45,13 @@ GLOBAL_LIST_EMPTY(uplinks) RegisterSignal(parent, COMSIG_IMPLANT_EXISTING_UPLINK, .proc/new_implant) else if(istype(parent, /obj/item/pda)) RegisterSignal(parent, COMSIG_PDA_CHANGE_RINGTONE, .proc/new_ringtone) + // RegisterSignal(parent, COMSIG_PDA_CHECK_DETONATE, .proc/check_detonate) else if(istype(parent, /obj/item/radio)) RegisterSignal(parent, COMSIG_RADIO_NEW_FREQUENCY, .proc/new_frequency) else if(istype(parent, /obj/item/pen)) RegisterSignal(parent, COMSIG_PEN_ROTATED, .proc/pen_rotation) - GLOB.uplinks += src - if(istype(traitor_class)) - filters = traitor_class.uplink_filters - starting_tc = traitor_class.TC - uplink_items = get_uplink_items(gamemode, TRUE, allow_restricted, filters) + GLOB.uplinks |= src if(_owner) owner = _owner @@ -64,44 +62,58 @@ GLOBAL_LIST_EMPTY(uplinks) purchase_log = new(owner, src) lockable = _lockable active = _enabled - gamemode = _gamemode + src.uplink_flag = uplink_flag + update_items() telecrystals = starting_tc if(!lockable) active = TRUE locked = FALSE - saved_player_population = GLOB.joined_player_list.len + + previous_attempts = list() /datum/component/uplink/InheritComponent(datum/component/uplink/U) lockable |= U.lockable active |= U.active - if(!gamemode) - gamemode = U.gamemode + uplink_flag |= U.uplink_flag telecrystals += U.telecrystals if(purchase_log && U.purchase_log) purchase_log.MergeWithAndDel(U.purchase_log) /datum/component/uplink/Destroy() GLOB.uplinks -= src - gamemode = null purchase_log = null return ..() +/datum/component/uplink/proc/update_items() + var/updated_items + updated_items = get_uplink_items(uplink_flag, TRUE, allow_restricted) + update_sales(updated_items) + uplink_items = updated_items + +/datum/component/uplink/proc/update_sales(updated_items) + var/discount_categories = list("Discounted Gear", "Discounted Team Gear", "Limited Stock Team Gear") + if (uplink_items == null) + return + for (var/category in discount_categories) // Makes sure discounted items aren't renewed or replaced + if (uplink_items[category] != null && updated_items[category] != null) + updated_items[category] = uplink_items[category] + /datum/component/uplink/proc/LoadTC(mob/user, obj/item/stack/telecrystal/TC, silent = FALSE) if(!silent) - to_chat(user, "You slot [TC] into [parent] and charge its internal uplink.") + to_chat(user, span_notice("You slot [TC] into [parent] and charge its internal uplink.")) var/amt = TC.amount telecrystals += amt TC.use(amt) - -/datum/component/uplink/proc/set_gamemode(_gamemode) - gamemode = _gamemode - uplink_items = get_uplink_items(gamemode, TRUE, allow_restricted) + // log_uplink("[key_name(user)] loaded [amt] telecrystals into [parent]'s uplink") /datum/component/uplink/proc/OnAttackBy(datum/source, obj/item/I, mob/user) + SIGNAL_HANDLER + if(!active) - return //no hitting everyone/everything just to try to slot tcs in! + return //no hitting everyone/everything just to try to slot tcs in! if(istype(I, /obj/item/stack/telecrystal)) LoadTC(user, I) + // CIT SPECIFIC: STEALING from unlocked uplink. if(active) if(I.GetComponent(/datum/component/uplink)) var/datum/component/uplink/hidden_uplink = I.GetComponent(/datum/component/uplink) @@ -118,31 +130,26 @@ GLOBAL_LIST_EMPTY(uplinks) var/cost = UI.refund_amount || UI.cost if(I.type == path && UI.refundable && I.check_uplink_validity()) telecrystals += cost - purchase_log.total_spent -= cost - to_chat(user, "[I] refunded.") + // log_uplink("[key_name(user)] refunded [UI] for [cost] telecrystals using [parent]'s uplink") + if(purchase_log) + purchase_log.total_spent -= cost + to_chat(user, span_notice("[I] refunded.")) qdel(I) return /datum/component/uplink/proc/interact(datum/source, mob/user) + SIGNAL_HANDLER + if(locked) return active = TRUE + update_items() if(user) - //update the saved population - var/previous_player_population = saved_player_population - saved_player_population = GLOB.joined_player_list.len - //if population has changed, update uplink items - if(saved_player_population != previous_player_population) - //make sure discounts are not rerolled - var/old_discounts = uplink_items["Discounted Gear"] - uplink_items = get_uplink_items(gamemode, FALSE, allow_restricted, filters) - if(old_discounts) - uplink_items["Discounted Gear"] = old_discounts - ui_interact(user) - + INVOKE_ASYNC(src, .proc/ui_interact, user) // an unlocked uplink blocks also opening the PDA or headset menu return COMPONENT_NO_INTERACT + /datum/component/uplink/ui_state(mob/user) if(istype(parent, /obj/item/implant/uplink)) return GLOB.not_incapacitated_state @@ -178,15 +185,10 @@ GLOBAL_LIST_EMPTY(uplinks) var/datum/uplink_item/I = uplink_items[category][item] if(I.limited_stock == 0) continue - if(I.restricted_roles.len) - var/is_inaccessible = TRUE - for(var/R in I.restricted_roles) - if(R == user.mind.assigned_role || debug) - is_inaccessible = FALSE - if(is_inaccessible) + if(length(I.restricted_roles)) + if(!debug && !(user.mind.assigned_role in I.restricted_roles)) continue - /* - if(I.restricted_species) //catpeople specfic gloves. + if(I.restricted_species) if(ishuman(user)) var/is_inaccessible = TRUE var/mob/living/carbon/human/H = user @@ -196,7 +198,6 @@ GLOBAL_LIST_EMPTY(uplinks) break if(is_inaccessible) continue - */ cat["items"] += list(list( "name" = I.name, "cost" = I.cost, @@ -255,25 +256,41 @@ GLOBAL_LIST_EMPTY(uplinks) // Implant signal responses /datum/component/uplink/proc/implant_activation() + SIGNAL_HANDLER + var/obj/item/implant/implant = parent locked = FALSE interact(null, implant.imp_in) /datum/component/uplink/proc/implanting(datum/source, list/arguments) + SIGNAL_HANDLER + var/mob/user = arguments[2] - owner = "[user.key]" + owner = user?.key + if(owner && !purchase_log) + LAZYINITLIST(GLOB.uplink_purchase_logs_by_key) + if(GLOB.uplink_purchase_logs_by_key[owner]) + purchase_log = GLOB.uplink_purchase_logs_by_key[owner] + else + purchase_log = new(owner, src) /datum/component/uplink/proc/old_implant(datum/source, list/arguments, obj/item/implant/new_implant) + SIGNAL_HANDLER + // It kinda has to be weird like this until implants are components return SEND_SIGNAL(new_implant, COMSIG_IMPLANT_EXISTING_UPLINK, src) /datum/component/uplink/proc/new_implant(datum/source, datum/component/uplink/uplink) + SIGNAL_HANDLER + uplink.telecrystals += telecrystals return COMPONENT_DELETE_NEW_IMPLANT // PDA signal responses /datum/component/uplink/proc/new_ringtone(datum/source, mob/living/user, new_ring_text) + SIGNAL_HANDLER + var/obj/item/pda/master = parent if(trim(lowertext(new_ring_text)) != trim(lowertext(unlock_code))) if(trim(lowertext(new_ring_text)) == trim(lowertext(failsafe_code))) @@ -282,14 +299,21 @@ GLOBAL_LIST_EMPTY(uplinks) return locked = FALSE interact(null, user) - to_chat(user, "The PDA softly beeps.") + to_chat(user, span_hear("The PDA softly beeps.")) user << browse(null, "window=pda") master.mode = 0 return COMPONENT_STOP_RINGTONE_CHANGE +/datum/component/uplink/proc/check_detonate() + SIGNAL_HANDLER + + // return COMPONENT_PDA_NO_DETONATE + // Radio signal responses /datum/component/uplink/proc/new_frequency(datum/source, list/arguments) + SIGNAL_HANDLER + var/obj/item/radio/master = parent var/frequency = arguments[1] if(frequency != unlock_code) @@ -303,15 +327,22 @@ GLOBAL_LIST_EMPTY(uplinks) // Pen signal responses /datum/component/uplink/proc/pen_rotation(datum/source, degrees, mob/living/carbon/user) + SIGNAL_HANDLER + var/obj/item/pen/master = parent - if(degrees != unlock_code) - if(degrees == failsafe_code) //Getting failsafes on pens is risky business - failsafe() - return - locked = FALSE - master.degrees = 0 - interact(null, user) - to_chat(user, "Your pen makes a clicking noise, before quickly rotating back to 0 degrees!") + previous_attempts += degrees + if(length(previous_attempts) > PEN_ROTATIONS) + popleft(previous_attempts) + + if(compare_list(previous_attempts, unlock_code)) + locked = FALSE + previous_attempts.Cut() + master.degrees = 0 + interact(null, user) + to_chat(user, span_warning("Your pen makes a clicking noise, before quickly rotating back to 0 degrees!")) + + else if(compare_list(previous_attempts, failsafe_code)) + failsafe(user) /datum/component/uplink/proc/setup_unlock_code() unlock_code = generate_code() @@ -321,15 +352,18 @@ GLOBAL_LIST_EMPTY(uplinks) else if(istype(parent,/obj/item/radio)) unlock_note = "Radio Frequency: [format_frequency(unlock_code)] ([P.name])." else if(istype(parent,/obj/item/pen)) - unlock_note = "Uplink Degrees: [unlock_code] ([P.name])." + unlock_note = "Uplink Degrees: [english_list(unlock_code)] ([P.name])." /datum/component/uplink/proc/generate_code() if(istype(parent,/obj/item/pda)) return "[rand(100,999)] [pick(GLOB.phonetic_alphabet)]" else if(istype(parent,/obj/item/radio)) - return sanitize_frequency(rand(MIN_FREQ, MAX_FREQ)) + return return_unused_frequency() else if(istype(parent,/obj/item/pen)) - return rand(1, 360) + var/list/L = list() + for(var/i in 1 to PEN_ROTATIONS) + L += rand(1, 360) + return L /datum/component/uplink/proc/failsafe(mob/living/carbon/user) if(!parent) @@ -339,5 +373,5 @@ GLOBAL_LIST_EMPTY(uplinks) return message_admins("[ADMIN_LOOKUPFLW(user)] has triggered an uplink failsafe explosion at [AREACOORD(T)] The owner of the uplink was [ADMIN_LOOKUPFLW(owner)].") log_game("[key_name(user)] triggered an uplink failsafe explosion. The owner of the uplink was [key_name(owner)].") - explosion(T,1,2,3) + explosion(parent, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3) qdel(parent) //Alternatively could brick the uplink. diff --git a/code/datums/datum.dm b/code/datums/datum.dm index 164cda63e0..5f9bf37040 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -106,7 +106,7 @@ active_timers = null for(var/thing in timers) var/datum/timedevent/timer = thing - if (timer.spent) + if (timer.spent && !(timer.flags & TIMER_DELETE_ME)) continue qdel(timer) diff --git a/code/datums/elements/weather_listener.dm b/code/datums/elements/weather_listener.dm new file mode 100644 index 0000000000..7cea61b640 --- /dev/null +++ b/code/datums/elements/weather_listener.dm @@ -0,0 +1,44 @@ +///This element just handles creating and destroying an area sound manager that's hooked into weather stuff +/datum/element/weather_listener + element_flags = ELEMENT_BESPOKE + id_arg_index = 2 + var/weather_type + //What events to change the track on + var/list/sound_change_signals + //The weather type we're working with + var/weather_trait + //The playlist of sounds to draw from. Pass by ref + var/list/playlist + + +/datum/element/weather_listener/Attach(datum/target, w_type, trait, weather_playlist) + . = ..() + if(!weather_type) + weather_type = w_type + sound_change_signals = list( + COMSIG_WEATHER_TELEGRAPH(weather_type), + COMSIG_WEATHER_START(weather_type), + COMSIG_WEATHER_WINDDOWN(weather_type), + COMSIG_WEATHER_END(weather_type) + ) + weather_trait = trait + playlist = weather_playlist + + RegisterSignal(target, COMSIG_MOVABLE_Z_CHANGED, .proc/handle_z_level_change, override = TRUE) + RegisterSignal(target, COMSIG_MOB_CLIENT_LOGOUT, .proc/handle_logout, override = TRUE) + +/datum/element/weather_listener/Detach(datum/source) + . = ..() + UnregisterSignal(source, COMSIG_MOVABLE_Z_CHANGED, COMSIG_MOB_CLIENT_LOGOUT) + +/datum/element/weather_listener/proc/handle_z_level_change(datum/source, old_z, new_z) + SIGNAL_HANDLER + var/list/fitting_z_levels = SSmapping.levels_by_trait(weather_trait) + if(!(new_z in fitting_z_levels)) + return + var/datum/component/our_comp = source.AddComponent(/datum/component/area_sound_manager, playlist, list(), COMSIG_MOB_CLIENT_LOGOUT, fitting_z_levels) + our_comp.RegisterSignal(SSdcs, sound_change_signals, /datum/component/area_sound_manager/proc/handle_change) + +/datum/element/weather_listener/proc/handle_logout(datum/source, client/this_is_a_null_ref) + SIGNAL_HANDLER + source.RemoveElement(/datum/element/weather_listener, weather_type, weather_trait, playlist) diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm index 6af3b3c993..e8c6bc4d22 100644 --- a/code/datums/looping_sounds/_looping_sound.dm +++ b/code/datums/looping_sounds/_looping_sound.dm @@ -1,21 +1,21 @@ /* - output_atoms (list of atoms) The destination(s) for the sounds + output_atoms (list of atoms) The destination(s) for the sounds - mid_sounds (list or soundfile) Since this can be either a list or a single soundfile you can have random sounds. May contain further lists but must contain a soundfile at the end. - mid_length (num) The length to wait between playing mid_sounds + mid_sounds (list or soundfile) Since this can be either a list or a single soundfile you can have random sounds. May contain further lists but must contain a soundfile at the end. + mid_length (num) The length to wait between playing mid_sounds - start_sound (soundfile) Played before starting the mid_sounds loop - start_length (num) How long to wait before starting the main loop after playing start_sound + start_sound (soundfile) Played before starting the mid_sounds loop + start_length (num) How long to wait before starting the main loop after playing start_sound - end_sound (soundfile) The sound played after the main loop has concluded + end_sound (soundfile) The sound played after the main loop has concluded - chance (num) Chance per loop to play a mid_sound - volume (num) Sound output volume - max_loops (num) The max amount of loops to run for. - direct (bool) If true plays directly to provided atoms instead of from them + chance (num) Chance per loop to play a mid_sound + volume (num) Sound output volume + max_loops (num) The max amount of loops to run for. + direct (bool) If true plays directly to provided atoms instead of from them */ /datum/looping_sound - var/list/atom/output_atoms + var/atom/parent var/mid_sounds var/mid_length ///Override for volume of start sound @@ -34,38 +34,46 @@ var/falloff_exponent var/timerid var/falloff_distance + var/skip_starting_sounds = FALSE + var/loop_started = FALSE -/datum/looping_sound/New(list/_output_atoms=list(), start_immediately=FALSE, _direct=FALSE) +/datum/looping_sound/New(_parent, start_immediately=FALSE, _direct=FALSE, _skip_starting_sounds = FALSE) if(!mid_sounds) WARNING("A looping sound datum was created without sounds to play.") return - output_atoms = _output_atoms + set_parent(_parent) direct = _direct + skip_starting_sounds = _skip_starting_sounds if(start_immediately) start() /datum/looping_sound/Destroy() - stop() - output_atoms = null + stop(TRUE) return ..() -/datum/looping_sound/proc/start(atom/add_thing) - if(add_thing) - output_atoms |= add_thing +/datum/looping_sound/proc/start(on_behalf_of) + if(on_behalf_of) + set_parent(on_behalf_of) if(timerid) return on_start() -/datum/looping_sound/proc/stop(atom/remove_thing) - if(remove_thing) - output_atoms -= remove_thing +/datum/looping_sound/proc/stop(null_parent) + if(null_parent) + set_parent(null) if(!timerid) return on_stop() - deltimer(timerid) + deltimer(timerid, SSsound_loops) timerid = null + loop_started = FALSE + +/datum/looping_sound/proc/start_sound_loop() + loop_started = TRUE + sound_loop() + timerid = addtimer(CALLBACK(src, .proc/sound_loop, world.time), mid_length, TIMER_CLIENT_TIME | TIMER_STOPPABLE | TIMER_LOOP | TIMER_DELETE_ME, SSsound_loops) /datum/looping_sound/proc/sound_loop(starttime) if(max_loops && world.time >= starttime + mid_length * max_loops) @@ -73,21 +81,15 @@ return if(!chance || prob(chance)) play(get_sound(starttime)) - if(!timerid) - timerid = addtimer(CALLBACK(src, .proc/sound_loop, world.time), mid_length, TIMER_CLIENT_TIME | TIMER_STOPPABLE | TIMER_LOOP) /datum/looping_sound/proc/play(soundfile, volume_override) - var/list/atoms_cache = output_atoms var/sound/S = sound(soundfile) if(direct) S.channel = SSsounds.random_available_channel() S.volume = volume_override || volume //Use volume as fallback if theres no override - for(var/i in 1 to atoms_cache.len) - var/atom/thing = atoms_cache[i] - if(direct) - SEND_SOUND(thing, S) - else - playsound(thing, S, volume, vary, extra_range, falloff_exponent = falloff_exponent, falloff_distance = falloff_distance) + SEND_SOUND(parent, S) + else + playsound(parent, S, volume, vary, extra_range, falloff_exponent = falloff_exponent, falloff_distance = falloff_distance) /datum/looping_sound/proc/get_sound(starttime, _mid_sounds) . = _mid_sounds || mid_sounds @@ -96,11 +98,22 @@ /datum/looping_sound/proc/on_start() var/start_wait = 0 - if(start_sound) + if(start_sound && !skip_starting_sounds) play(start_sound, start_volume) start_wait = start_length - addtimer(CALLBACK(src, .proc/sound_loop), start_wait, TIMER_CLIENT_TIME) + timerid = addtimer(CALLBACK(src, .proc/start_sound_loop), start_wait, TIMER_CLIENT_TIME | TIMER_DELETE_ME | TIMER_STOPPABLE, SSsound_loops) /datum/looping_sound/proc/on_stop() - if(end_sound) + if(end_sound && loop_started) play(end_sound, end_volume) + +/datum/looping_sound/proc/set_parent(new_parent) + if(parent) + UnregisterSignal(parent, COMSIG_PARENT_QDELETING) + parent = new_parent + if(parent) + RegisterSignal(parent, COMSIG_PARENT_QDELETING, .proc/handle_parent_del) + +/datum/looping_sound/proc/handle_parent_del(datum/source) + SIGNAL_HANDLER + set_parent(null) diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm index e729ed22dd..d8b72be76c 100644 --- a/code/datums/looping_sounds/item_sounds.dm +++ b/code/datums/looping_sounds/item_sounds.dm @@ -1,4 +1,4 @@ -#define RAD_GEIGER_LOW 100 // Geiger counter sound thresholds +#define RAD_GEIGER_LOW 100 // Geiger counter sound thresholds #define RAD_GEIGER_MEDIUM 500 #define RAD_GEIGER_HIGH 1000 diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm index 658f22a107..dbd976b2f7 100644 --- a/code/datums/materials/_material.dm +++ b/code/datums/materials/_material.dm @@ -120,7 +120,7 @@ Simple datum which is instanced once per type and is used for every object of sa return I.hitsound = item_sound_override I.usesound = item_sound_override - I.throwhitsound = item_sound_override + I.mob_throw_hit_sound = item_sound_override // I.mob_throw_hit_sound = item_sound_override // I.equip_sound = item_sound_override // I.pickup_sound = item_sound_override diff --git a/code/datums/mind.dm b/code/datums/mind.dm index fe8a7db23d..839552a208 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -268,11 +268,17 @@ remove_rev() SSticker.mode.update_cult_icons_removed(src) -/datum/mind/proc/equip_traitor(datum/traitor_class/traitor_class, silent = FALSE, datum/antagonist/uplink_owner) +/** + * ## give_uplink + * + * A mind proc for giving anyone an uplink. + * arguments: + * * silent: if this should send a message to the mind getting the uplink. traitors do not use this silence, but the silence var on their antag datum. + * * antag_datum: the antag datum of the uplink owner, for storing it in antag memory. optional! + */ +/datum/mind/proc/equip_traitor(silent = FALSE, datum/antagonist/antag_datum) if(!current) return - if(!traitor_class) - traitor_class = GLOB.traitor_classes[TRAITOR_HUMAN] var/mob/living/carbon/human/traitor_mob = current if (!istype(traitor_mob)) return @@ -286,16 +292,9 @@ 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 - if(!P) // I do not have a pen. - var/obj/item/pen/inowhaveapen - if(istype(traitor_mob.back,/obj/item/storage)) //ok buddy you better have a backpack! - inowhaveapen = new /obj/item/pen(traitor_mob.back) - else - inowhaveapen = new /obj/item/pen(traitor_mob.loc) - traitor_mob.put_in_hands(inowhaveapen) // I hope you don't have arms and your traitor pen gets stolen for all this trouble you've caused. - P = inowhaveapen var/obj/item/uplink_loc + var/implant = FALSE if(traitor_mob.client && traitor_mob.client.prefs) switch(traitor_mob.client.prefs.uplink_spawn_loc) @@ -313,33 +312,38 @@ uplink_loc = P if(UPLINK_PEN) uplink_loc = P - if(!uplink_loc) - uplink_loc = PDA - if(!uplink_loc) - uplink_loc = R + if(UPLINK_IMPLANT) + implant = TRUE - if (!uplink_loc) - if(!silent) - to_chat(traitor_mob, "Unfortunately, [traitor_class.employer] wasn't able to get you an Uplink.") - . = 0 - else - . = uplink_loc - var/datum/component/uplink/U = uplink_loc.AddComponent(/datum/component/uplink, traitor_mob.key,traitor_class) - if(!U) - CRASH("Uplink creation failed.") - U.setup_unlock_code() - if(!silent) - if(uplink_loc == R) - to_chat(traitor_mob, "[traitor_class.employer] has cunningly disguised a Syndicate Uplink as your [R.name]. Simply dial the frequency [format_frequency(U.unlock_code)] to unlock its hidden features.") - else if(uplink_loc == PDA) - to_chat(traitor_mob, "[traitor_class.employer] has cunningly disguised a Syndicate Uplink as your [PDA.name]. Simply enter the code \"[U.unlock_code]\" into the ringtone select to unlock its hidden features.") - else if(uplink_loc == P) - to_chat(traitor_mob, "[traitor_class.employer] has cunningly disguised a Syndicate Uplink as your [P.name]. Simply twist the top of the pen [U.unlock_code] from its starting position to unlock its hidden features.") + if(!uplink_loc) // We've looked everywhere, let's just implant you + implant = TRUE - if(uplink_owner) - uplink_owner.antag_memory += U.unlock_note + "
" - else - traitor_mob.mind.store_memory(U.unlock_note) + if(implant) + var/obj/item/implant/uplink/starting/new_implant = new(traitor_mob) + new_implant.implant(traitor_mob, null, silent = TRUE) + if(!silent) + to_chat(traitor_mob, span_boldnotice("Your Syndicate Uplink has been cunningly implanted in you, for a small TC fee. Simply trigger the uplink to access it.")) + return new_implant + + . = uplink_loc + var/unlock_text + var/datum/component/uplink/new_uplink = uplink_loc.AddComponent(/datum/component/uplink, traitor_mob.key) + if(!new_uplink) + CRASH("Uplink creation failed.") + new_uplink.setup_unlock_code() + if(uplink_loc == R) + unlock_text = "Your Uplink is cunningly disguised as your [R.name]. Simply dial the frequency [format_frequency(new_uplink.unlock_code)] to unlock its hidden features." + 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 ringtone select to unlock its hidden features." + else if(uplink_loc == P) + unlock_text = "Your Uplink is cunningly disguised as your [P.name]. Simply twist the top of the pen [english_list(new_uplink.unlock_code)] from its starting position to unlock its hidden features." + new_uplink.unlock_text = unlock_text + if(!silent) + to_chat(traitor_mob, span_boldnotice(unlock_text)) + if(!antag_datum) + traitor_mob.mind.store_memory(new_uplink.unlock_note) + return + antag_datum.antag_memory += new_uplink.unlock_note + "
" //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. diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm index 6248be0de5..14b514b30a 100644 --- a/code/datums/weather/weather_types/ash_storm.dm +++ b/code/datums/weather/weather_types/ash_storm.dm @@ -1,3 +1,5 @@ +//A reference to this list is passed into area sound managers, and it's modified in a manner that preserves that reference in ash_storm.dm +GLOBAL_LIST_EMPTY(ash_storm_sounds) //Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside. /datum/weather/ash_storm name = "ash storm" @@ -25,55 +27,41 @@ probability = 90 barometer_predictable = TRUE - - var/datum/looping_sound/active_outside_ashstorm/sound_ao = new(list(), FALSE, TRUE) - var/datum/looping_sound/active_inside_ashstorm/sound_ai = new(list(), FALSE, TRUE) - var/datum/looping_sound/weak_outside_ashstorm/sound_wo = new(list(), FALSE, TRUE) - var/datum/looping_sound/weak_inside_ashstorm/sound_wi = new(list(), FALSE, TRUE) + var/list/weak_sounds = list() + var/list/strong_sounds = list() /datum/weather/ash_storm/telegraph() - . = ..() - var/list/inside_areas = list() - var/list/outside_areas = list() var/list/eligible_areas = list() for (var/z in impacted_z_levels) eligible_areas += SSmapping.areas_in_z["[z]"] for(var/i in 1 to eligible_areas.len) var/area/place = eligible_areas[i] if(place.outdoors) - outside_areas += place + weak_sounds[place] = /datum/looping_sound/weak_outside_ashstorm + strong_sounds[place] = /datum/looping_sound/active_outside_ashstorm else - inside_areas += place + weak_sounds[place] = /datum/looping_sound/weak_inside_ashstorm + strong_sounds[place] = /datum/looping_sound/active_inside_ashstorm CHECK_TICK - sound_ao.output_atoms = outside_areas - sound_ai.output_atoms = inside_areas - sound_wo.output_atoms = outside_areas - sound_wi.output_atoms = inside_areas - - sound_wo.start() - sound_wi.start() + //We modify this list instead of setting it to weak/stron sounds in order to preserve things that hold a reference to it + //It's essentially a playlist for a bunch of components that chose what sound to loop based on the area a player is in + GLOB.ash_storm_sounds += weak_sounds + return ..() /datum/weather/ash_storm/start() - . = ..() - sound_wo.stop() - sound_wi.stop() - - sound_ao.start() - sound_ai.start() + GLOB.ash_storm_sounds -= weak_sounds + GLOB.ash_storm_sounds += strong_sounds + return ..() /datum/weather/ash_storm/wind_down() - . = ..() - sound_ao.stop() - sound_ai.stop() - - sound_wo.start() - sound_wi.start() + GLOB.ash_storm_sounds -= strong_sounds + GLOB.ash_storm_sounds += weak_sounds + return ..() /datum/weather/ash_storm/end() - . = ..() - sound_wo.stop() - sound_wi.stop() + GLOB.ash_storm_sounds -= weak_sounds + return ..() /datum/weather/ash_storm/proc/is_ash_immune(atom/L) while (L && !isturf(L)) @@ -88,6 +76,11 @@ var/mob/living/the_mob = L if("ash" in the_mob.weather_immunities) return TRUE + // if(istype(L, /obj/structure/closet)) + // var/obj/structure/closet/the_locker = L + // if(the_locker.weather_protection) + // if("ash" in the_locker.weather_protection) + // return TRUE L = L.loc //Check parent items immunities (recurses up to the turf) return FALSE //RIP you diff --git a/code/game/atoms.dm b/code/game/atoms.dm index f107243879..e7a0aa1c68 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1312,9 +1312,7 @@ /obj/item/update_filters() . = ..() - for(var/X in actions) - var/datum/action/A = X - A.UpdateButtonIcon() + update_action_buttons() /atom/proc/get_filter(name) if(filter_data && filter_data[name]) @@ -1425,3 +1423,15 @@ */ /atom/proc/setClosed() return + +//Update the screentip to reflect what we're hoverin over +/atom/MouseEntered(location, control, params) + . = ..() + // Statusbar + // status_bar_set_text(usr, name) + // Screentips + // if(usr?.hud_used) + // if(!usr.client?.prefs.screentip_pref || (flags_1 & NO_SCREENTIPS_1)) + // usr.hud_used.screentip_text.maptext = "" + // else + // usr.hud_used.screentip_text.maptext = MAPTEXT("[name]") diff --git a/code/game/atoms_movement.dm b/code/game/atoms_movement.dm index 4f07ff6f95..43a22c9df0 100644 --- a/code/game/atoms_movement.dm +++ b/code/game/atoms_movement.dm @@ -48,8 +48,17 @@ continue var/atom/movable/thing = i thing.Crossed(src) -// -//////////////////////////////////////// + +/** + * meant for movement with zero side effects. only use for objects that are supposed to move "invisibly" (like camera mobs or ghosts) + * if you want something to move onto a tile with a beartrap or recycler or tripmine or mouse without that object knowing about it at all, use this + * most of the time you want forceMove() + */ +/atom/movable/proc/abstract_move(atom/new_loc) + var/atom/old_loc = loc + // move_stacks++ + loc = new_loc + Moved(old_loc) /atom/movable/Move(atom/newloc, direct, glide_size_override = 0) var/atom/movable/pullee = pulling diff --git a/code/game/communications.dm b/code/game/communications.dm index 696b942434..2b45fc9469 100644 --- a/code/game/communications.dm +++ b/code/game/communications.dm @@ -193,7 +193,9 @@ GLOBAL_LIST_INIT(reverseradiochannels, list( var/frequency = 0 var/transmission_method var/list/data + var/logging_data -/datum/signal/New(data, transmission_method = TRANSMISSION_RADIO) +/datum/signal/New(data, transmission_method = TRANSMISSION_RADIO, logging_data = null) src.data = data || list() src.transmission_method = transmission_method + src.logging_data = logging_data diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index d2785a1a76..f40adcd00b 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -247,61 +247,94 @@ Class Procs: /obj/machinery/proc/auto_use_power() if(!powered(power_channel)) - return 0 + return FALSE if(use_power == 1) use_power(idle_power_usage,power_channel) else if(use_power >= 2) use_power(active_power_usage,power_channel) - return 1 + return TRUE /obj/machinery/proc/is_operational() return !(stat & (NOPOWER|BROKEN|MAINT)) /obj/machinery/can_interact(mob/user) - var/silicon = hasSiliconAccessInArea(user) || IsAdminGhost(user) - if((stat & (NOPOWER|BROKEN)) && !(interaction_flags_machine & INTERACT_MACHINE_OFFLINE)) + if((stat & (NOPOWER|BROKEN)) && !(interaction_flags_machine & INTERACT_MACHINE_OFFLINE)) // Check if the machine is broken, and if we can still interact with it if so return FALSE - if(panel_open && !(interaction_flags_machine & INTERACT_MACHINE_OPEN)) - if(!silicon || !(interaction_flags_machine & INTERACT_MACHINE_OPEN_SILICON)) - return FALSE - if(silicon) + if(IsAdminGhost(user)) + return TRUE //if you're an admin, you probably know what you're doing (or at least have permission to do what you're doing) + + if(!isliving(user)) + return FALSE //no ghosts in the machine allowed, sorry + + // if(SEND_SIGNAL(user, COMSIG_TRY_USE_MACHINE, src) & COMPONENT_CANT_USE_MACHINE_INTERACT) + // return FALSE + + var/mob/living/living_user = user + + var/is_dextrous = FALSE + if(isanimal(user)) + var/mob/living/simple_animal/user_as_animal = user + if (user_as_animal.dextrous) + is_dextrous = TRUE + + if(!issilicon(user) && !is_dextrous && !user.can_hold_items()) + return FALSE //spiders gtfo + + if(issilicon(user)) // If we are a silicon, make sure the machine allows silicons to interact with it if(!(interaction_flags_machine & INTERACT_MACHINE_ALLOW_SILICON)) return FALSE - else - if(interaction_flags_machine & INTERACT_MACHINE_REQUIRES_SILICON) + + if(panel_open && !(interaction_flags_machine & INTERACT_MACHINE_OPEN) && !(interaction_flags_machine & INTERACT_MACHINE_OPEN_SILICON)) return FALSE - if(!Adjacent(user)) - var/mob/living/carbon/H = user - if(!(istype(H) && H.has_dna() && H.dna.check_mutation(TK))) - return FALSE - return TRUE + + return TRUE //silicons don't care about petty mortal concerns like needing to be next to a machine to use it + + if(living_user.incapacitated()) //idk why silicons aren't supposed to care about incapacitation when interacting with machines, but it was apparently like this before + return FALSE + + // TODO: nerf blind people + // if((interaction_flags_machine & INTERACT_MACHINE_REQUIRES_SIGHT) && user.is_blind()) + // to_chat(user, span_warning("This machine requires sight to use.")) + // return FALSE + + if(panel_open && !(interaction_flags_machine & INTERACT_MACHINE_OPEN)) + return FALSE + + if(interaction_flags_machine & INTERACT_MACHINE_REQUIRES_SILICON) //if the user was a silicon, we'd have returned out earlier, so the user must not be a silicon + return FALSE + + if(!Adjacent(user)) // Next make sure we are next to the machine unless we have telekinesis + var/mob/living/carbon/carbon_user = living_user + if(!istype(carbon_user) || !carbon_user.has_dna() || !carbon_user.dna.check_mutation(TK)) + return FALSE + + return TRUE // If we passed all of those checks, woohoo! We can interact with this machine. /obj/machinery/proc/check_nap_violations() if(!SSeconomy.full_ancap) return TRUE if(occupant && !state_open) - if(ishuman(occupant)) - var/mob/living/carbon/human/H = occupant - var/obj/item/card/id/I = H.get_idcard() - if(I) - var/datum/bank_account/insurance = I.registered_account - if(!insurance) - say("[market_verb] NAP Violation: No bank account found.") - nap_violation() - return FALSE - else - if(!insurance.adjust_money(-fair_market_price)) - say("[market_verb] NAP Violation: Unable to pay.") - nap_violation() - return FALSE - var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department) - if(D) - D.adjust_money(fair_market_price) - else - say("[market_verb] NAP Violation: No ID card found.") - nap_violation() + var/mob/living/L = occupant + var/obj/item/card/id/I = L.get_idcard(TRUE) + if(I) + var/datum/bank_account/insurance = I.registered_account + if(!insurance) + say("[market_verb] NAP Violation: No bank account found.") + nap_violation(L) return FALSE + else + if(!insurance.adjust_money(-fair_market_price)) + say("[market_verb] NAP Violation: Unable to pay.") + nap_violation(L) + return FALSE + var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department) + if(D) + D.adjust_money(fair_market_price) + else + say("[market_verb] NAP Violation: No ID card found.") + nap_violation(L) + return FALSE return TRUE /obj/machinery/proc/nap_violation(mob/violator) @@ -322,11 +355,11 @@ Class Procs: /obj/machinery/Topic(href, href_list) ..() if(!can_interact(usr)) - return 1 + return TRUE if(!usr.canUseTopic(src)) - return 1 + return TRUE add_fingerprint(usr) - return 0 + return FALSE //////////////////////////////////////////////////////////////////////////////////////////// diff --git a/code/game/machinery/colormate.dm b/code/game/machinery/colormate.dm index 281538e7a8..1528142a3b 100644 --- a/code/game/machinery/colormate.dm +++ b/code/game/machinery/colormate.dm @@ -3,6 +3,7 @@ desc = "A machine to give your apparel a fresh new color! Recommended to use with white items for best results." icon = 'icons/obj/vending.dmi' icon_state = "colormate" + // light_mask = "colormate-light-mask" density = TRUE anchored = TRUE circuit = /obj/item/circuitboard/machine/colormate diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index 822302789f..b81507024c 100755 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -44,6 +44,7 @@ /obj/machinery/computer/communications/Initialize() . = ..() GLOB.shuttle_caller_list += src + AddComponent(/datum/component/gps, "Secured Communications Signal") /// Are we NOT a silicon, AND we're logged in as the captain? /obj/machinery/computer/communications/proc/authenticated_as_non_silicon_captain(mob/user) @@ -79,13 +80,11 @@ if (obj_flags & EMAGGED) return obj_flags |= EMAGGED - SSshuttle.shuttle_purchase_requirements_met |= "emagged" if (authenticated) authorize_access = get_all_accesses() - to_chat(user, "You scramble the communication routing circuits!") + to_chat(user, span_danger("You scramble the communication routing circuits!")) playsound(src, 'sound/machines/terminal_alert.ogg', 50, FALSE) SSshuttle.shuttle_purchase_requirements_met["emagged"] = TRUE - return /obj/machinery/computer/communications/ui_act(action, list/params) var/static/list/approved_states = list(STATE_BUYING_SHUTTLE, STATE_CHANGING_STATUS, STATE_MAIN, STATE_MESSAGES) @@ -104,6 +103,7 @@ if ("answerMessage") if (!authenticated(usr)) return + var/answer_index = params["answer"] var/message_index = params["message"] @@ -136,11 +136,11 @@ var/obj/item/held_item = usr.get_active_held_item() var/obj/item/card/id/id_card = held_item?.GetID() if (!istype(id_card)) - to_chat(usr, "You need to swipe your ID!") + to_chat(usr, span_warning("You need to swipe your ID!")) playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE) return if (!(ACCESS_CAPTAIN in id_card.access)) - to_chat(usr, "You are not authorized to do this!") + to_chat(usr, span_warning("You are not authorized to do this!")) playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE) return @@ -152,26 +152,28 @@ set_security_level(new_sec_level) - to_chat(usr, "Authorization confirmed. Modifying security level.") + to_chat(usr, span_notice("Authorization confirmed. Modifying security level.")) playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE) // Only notify people if an actual change happened log_game("[key_name(usr)] has changed the security level to [params["newSecurityLevel"]] with [src] at [AREACOORD(usr)].") message_admins("[ADMIN_LOOKUPFLW(usr)] has changed the security level to [params["newSecurityLevel"]] with [src] at [AREACOORD(usr)].") - deadchat_broadcast(" has changed the security level to [params["newSecurityLevel"]] with [src] at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr) + deadchat_broadcast(" has changed the security level to [params["newSecurityLevel"]] with [src] at [span_name("[get_area_name(usr, TRUE)]")].", usr, src.loc, message_type=DEADCHAT_ANNOUNCEMENT) alert_level_tick += 1 if ("deleteMessage") if (!authenticated(usr)) return - var/message_index = params["message"] - - if(!isnum(message_index)) - message_admins("[ADMIN_LOOKUPFLW(usr)] provided an invalid index type when deleting a message on [src] [ADMIN_JMP(src)]. This should not happen. Please check with a maintainer and/or consult tgui logs.") - CRASH("Non-numeric index provided when deleting comms console message.") + var/message_index = text2num(params["message"]) if (!message_index) return LAZYREMOVE(messages, LAZYACCESS(messages, message_index)) + if ("emergency_meeting") + if(!(SSevents.holidays && SSevents.holidays[APRIL_FOOLS])) + return + if (!authenticated_as_silicon_or_captain(usr)) + return + emergency_meeting(usr) if ("makePriorityAnnouncement") if (!authenticated_as_silicon_or_captain(usr)) return @@ -188,32 +190,36 @@ var/emagged = obj_flags & EMAGGED if (emagged) message_syndicate(message, usr) - to_chat(usr, "SYSERR @l(19833)of(transmit.dm): !@$ MESSAGE TRANSMITTED TO SYNDICATE COMMAND.") + to_chat(usr, span_danger("SYSERR @l(19833)of(transmit.dm): !@$ MESSAGE TRANSMITTED TO SYNDICATE COMMAND.")) else message_centcom(message, usr) - to_chat(usr, "Message transmitted to Central Command.") + to_chat(usr, span_notice("Message transmitted to Central Command.")) var/associates = emagged ? "the Syndicate": "CentCom" usr.log_talk(message, LOG_SAY, tag = "message to [associates]") - deadchat_broadcast(" has messaged [associates], \"[message]\" at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr) + deadchat_broadcast(" has messaged [associates], \"[message]\" at [span_name("[get_area_name(usr, TRUE)]")].", usr, src.loc, message_type = DEADCHAT_ANNOUNCEMENT) COOLDOWN_START(src, important_action_cooldown, IMPORTANT_ACTION_COOLDOWN) if ("purchaseShuttle") var/can_buy_shuttles_or_fail_reason = can_buy_shuttles(usr) if (can_buy_shuttles_or_fail_reason != TRUE) if (can_buy_shuttles_or_fail_reason != FALSE) - to_chat(usr, "[can_buy_shuttles_or_fail_reason]") + to_chat(usr, span_alert("[can_buy_shuttles_or_fail_reason]")) return var/list/shuttles = flatten_list(SSmapping.shuttle_templates) var/datum/map_template/shuttle/shuttle = locate(params["shuttle"]) in shuttles if (!istype(shuttle)) return + // if (!can_purchase_this_shuttle(shuttle)) + // return if (!shuttle.prerequisites_met()) - to_chat(usr, "You have not met the requirements for purchasing this shuttle.") + to_chat(usr, span_alert("You have not met the requirements for purchasing this shuttle.")) return var/datum/bank_account/bank_account = SSeconomy.get_dep_account(ACCOUNT_CAR) if (bank_account.account_balance < shuttle.credit_cost) return SSshuttle.shuttle_purchased = SHUTTLEPURCHASE_PURCHASED + // for(var/datum/round_event_control/shuttle_insurance/insurance_event in SSevents.control) + // insurance_event.weight *= 20 SSshuttle.unload_preview() SSshuttle.existing_shuttle = SSshuttle.emergency SSshuttle.action_load(shuttle, replace = TRUE) @@ -235,7 +241,7 @@ return var/reason = trim(html_encode(params["reason"]), MAX_MESSAGE_LEN) nuke_request(reason, usr) - to_chat(usr, "Request sent.") + to_chat(usr, span_notice("Request sent.")) usr.log_message("has requested the nuclear codes from CentCom with reason \"[reason]\"", LOG_SAY) priority_announce("The codes for the on-station nuclear self-destruct have been requested by [usr]. Confirmation or denial of this request will be sent shortly.", "Nuclear Self-Destruct Codes Requested", "commandreport") playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE) @@ -245,7 +251,7 @@ return if (!(obj_flags & EMAGGED)) return - to_chat(usr, "Backup routing data restored.") + to_chat(usr, span_notice("Backup routing data restored.")) playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE) obj_flags &= ~EMAGGED if ("sendToOtherSector") @@ -256,7 +262,7 @@ if (!COOLDOWN_FINISHED(src, important_action_cooldown)) return - var/message = trim(html_encode(params["message"]), MAX_MESSAGE_LEN) + var/message = trim(params["message"], MAX_MESSAGE_LEN) if (!message) return @@ -268,12 +274,13 @@ var/network_name = CONFIG_GET(string/cross_comms_network) if (network_name) payload["network"] = network_name + payload["sender_ckey"] = usr.ckey - send2otherserver(station_name(), message, "Comms_Console", destination == "all" ? null : list(destination), additional_data = payload) + send2otherserver(html_decode(station_name()), message, "Comms_Console", destination == "all" ? null : list(destination), additional_data = payload) minor_announce(message, title = "Outgoing message to allied station") usr.log_talk(message, LOG_SAY, tag = "message to the other server") message_admins("[ADMIN_LOOKUPFLW(usr)] has sent a message to the other server\[s].") - deadchat_broadcast(" has sent an outgoing message to the other station(s).", "[usr.real_name]", usr) + deadchat_broadcast(" has sent an outgoing message to the other station(s).", usr, src.loc, message_type = DEADCHAT_ANNOUNCEMENT) COOLDOWN_START(src, important_action_cooldown, IMPORTANT_ACTION_COOLDOWN) if ("setState") @@ -315,7 +322,7 @@ authenticated = TRUE authorize_access = get_all_accesses() authorize_name = "Unknown" - to_chat(usr, "[src] lets out a quiet alarm as its login is overridden.") + to_chat(usr, span_warning("[src] lets out a quiet alarm as its login is overridden.")) playsound(src, 'sound/machines/terminal_alert.ogg', 25, FALSE) else if(isliving(usr)) var/mob/living/L = usr @@ -334,22 +341,36 @@ revoke_maint_all_access() log_game("[key_name(usr)] disabled emergency maintenance access.") message_admins("[ADMIN_LOOKUPFLW(usr)] disabled emergency maintenance access.") - deadchat_broadcast(" disabled emergency maintenance access at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr) + deadchat_broadcast(" disabled emergency maintenance access at [span_name("[get_area_name(usr, TRUE)]")].", usr, src.loc, message_type = DEADCHAT_ANNOUNCEMENT) else make_maint_all_access() log_game("[key_name(usr)] enabled emergency maintenance access.") message_admins("[ADMIN_LOOKUPFLW(usr)] enabled emergency maintenance access.") - deadchat_broadcast(" enabled emergency maintenance access at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr) + deadchat_broadcast(" enabled emergency maintenance access at [span_name("[get_area_name(usr, TRUE)]")].", usr, src.loc, message_type = DEADCHAT_ANNOUNCEMENT) /obj/machinery/computer/communications/ui_data(mob/user) var/list/data = list( "authenticated" = FALSE, "emagged" = FALSE, - "hasConnection" = has_communication(), ) var/ui_state = issilicon(user) ? cyborg_state : state + var/has_connection = has_communication() + data["hasConnection"] = has_connection + + // if(!SSjob.assigned_captain && !SSjob.safe_code_requested && SSid_access.spare_id_safe_code && has_connection) + // data["canRequestSafeCode"] = TRUE + // data["safeCodeDeliveryWait"] = 0 + // else + // data["canRequestSafeCode"] = FALSE + // if(SSjob.safe_code_timer_id && has_connection) + // data["safeCodeDeliveryWait"] = timeleft(SSjob.safe_code_timer_id) + // data["safeCodeDeliveryArea"] = get_area(SSjob.safe_code_request_loc) + // else + // data["safeCodeDeliveryWait"] = 0 + // data["safeCodeDeliveryArea"] = null + if (authenticated || issilicon(user)) data["authenticated"] = TRUE data["canLogOut"] = !issilicon(user) @@ -371,16 +392,17 @@ data["importantActionReady"] = COOLDOWN_FINISHED(src, important_action_cooldown) data["shuttleCalled"] = FALSE data["shuttleLastCalled"] = FALSE - + data["aprilFools"] = SSevents.holidays && SSevents.holidays[APRIL_FOOLS] data["alertLevel"] = NUM2SECLEVEL(GLOB.security_level) data["authorizeName"] = authorize_name data["canLogOut"] = !issilicon(user) - data["shuttleCanEvacOrFailReason"] = SSshuttle.canEvac(user, TRUE) + data["shuttleCanEvacOrFailReason"] = SSshuttle.canEvac(user) if (authenticated_as_non_silicon_captain(user)) data["canRequestNuke"] = TRUE if (authenticated_as_non_silicon_command(user)) data["canMessageAssociates"] = TRUE + if (can_send_messages_to_other_sectors(user)) data["canSendToSectors"] = TRUE @@ -428,8 +450,13 @@ for (var/shuttle_id in SSmapping.shuttle_templates) var/datum/map_template/shuttle/shuttle_template = SSmapping.shuttle_templates[shuttle_id] - if (!shuttle_template.can_be_bought || shuttle_template.credit_cost == INFINITY) + + if (shuttle_template.credit_cost == INFINITY) continue + + if (!shuttle_template.can_be_bought) + continue + shuttles += list(list( "name" = shuttle_template.name, "description" = shuttle_template.description, @@ -478,6 +505,7 @@ return FALSE if (!authenticated_as_non_silicon_captain(user)) return FALSE + if (SSshuttle.emergency.mode != SHUTTLE_RECALL && SSshuttle.emergency.mode != SHUTTLE_IDLE) return "The shuttle is already in transit." if (SSshuttle.shuttle_purchased == SHUTTLEPURCHASE_PURCHASED) @@ -492,21 +520,38 @@ return length(CONFIG_GET(keyed_list/cross_server)) > 0 +/** + * Call an emergency meeting + * + * Comm Console wrapper for the Communications subsystem wrapper for the call_emergency_meeting world proc. + * Checks to make sure the proc can be called, and handles relevant feedback, logging and timing. + * See the SScommunications proc definition for more detail, in short, teleports the entire crew to + * the bridge for a meetup. Should only really happen during april fools. + * Arguments: + * * user - Mob who called the meeting + */ +/obj/machinery/computer/communications/proc/emergency_meeting(mob/living/user) + if(!SScommunications.can_make_emergency_meeting(user)) + to_chat(user, span_alert("The emergency meeting button doesn't seem to work right now. Please stand by.")) + return + SScommunications.emergency_meeting(user) + deadchat_broadcast(" called an emergency meeting from [span_name("[get_area_name(usr, TRUE)]")].", span_name("[user.real_name]"), user, message_type=DEADCHAT_ANNOUNCEMENT) + /obj/machinery/computer/communications/proc/make_announcement(mob/living/user) var/is_ai = issilicon(user) if(!SScommunications.can_announce(user, is_ai)) - to_chat(user, "Intercomms recharging. Please stand by.") + to_chat(user, span_alert("Intercomms recharging. Please stand by.")) return var/input = stripped_input(user, "Please choose a message to announce to the station crew.", "What?") if(!input || !user.canUseTopic(src, !issilicon(usr))) return if(!(user.can_speak())) //No more cheating, mime/random mute guy! input = "..." - to_chat(user, "You find yourself unable to speak.") + to_chat(user, span_warning("You find yourself unable to speak.")) else input = user.treat_message(input) //Adds slurs and so on. Someone should make this use languages too. SScommunications.make_announcement(user, is_ai, input) - deadchat_broadcast(" made a priority announcement from [get_area_name(usr, TRUE)].", "[user.real_name]", user) + deadchat_broadcast(" made a priority announcement from [span_name("[get_area_name(usr, TRUE)]")].", user, src.loc, message_type=DEADCHAT_ANNOUNCEMENT) /obj/machinery/computer/communications/proc/post_status(command, data1, data2) diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm index 04e8915e18..3386229fc8 100644 --- a/code/game/machinery/cryopod.dm +++ b/code/game/machinery/cryopod.dm @@ -1,3 +1,5 @@ +#define AHELP_FIRST_MESSAGE "Please adminhelp before leaving the round, even if there are no administrators online!" + /* * Cryogenic refrigeration unit. Basically a despawner. * Stealing a lot of concepts/code from sleepers due to massive laziness. @@ -5,6 +7,7 @@ * since time_entered, which is world.time when the occupant moves in. * ~ Zuhayr */ +GLOBAL_LIST_EMPTY(cryopod_computers) //Main cryopod console. @@ -13,159 +16,113 @@ desc = "An interface between crew and the cryogenic storage/teleporter storage oversight systems." icon = 'icons/obj/Cryogenic2.dmi' icon_state = "cellconsole_1" + icon_keyboard = null circuit = /obj/item/circuitboard/cryopodcontrol density = FALSE interaction_flags_machine = INTERACT_MACHINE_OFFLINE - req_one_access = list(ACCESS_HEADS, ACCESS_ARMORY) //Heads of staff or the warden can go here to claim recover items from their department that people went were cryodormed with. + req_one_access = list(ACCESS_HEADS, ACCESS_ARMORY) // Heads of staff or the warden can go here to claim recover items from their department that people went were cryodormed with. + var/mode = null - var/menu = 1 //Which menu screen to display - - //Used for logging people entering cryosleep and important items they are carrying. + // Used for logging people entering cryosleep and important items they are carrying. var/list/frozen_crew = list() - var/list/obj/stored_packages = list() - + var/list/stored_packages = list() + /// Does this console store items? if NOT, will dump all items when the user cryo's instead var/allow_items = TRUE + var/storage_type = "crewmembers" + var/storage_name = "Cryogenic/Teleporter Oversight Control" + /obj/machinery/computer/cryopod/deconstruct() . = ..() for(var/i in stored_packages) var/obj/O = i O.forceMove(drop_location()) -/obj/machinery/computer/cryopod/attack_ai() - attack_hand() +/obj/machinery/computer/cryopod/Initialize() + . = ..() + GLOB.cryopod_computers += src -/obj/machinery/computer/cryopod/ui_interact(mob/user = usr) - if(!is_operational()) +/obj/machinery/computer/cryopod/Destroy() + GLOB.cryopod_computers -= src + return ..() + +/obj/machinery/computer/cryopod/update_icon_state() + if(stat & (NOPOWER|BROKEN)) + icon_state = "cellconsole" + return ..() + icon_state = "cellconsole_1" + return ..() + +/obj/machinery/computer/cryopod/ui_interact(mob/user, datum/tgui/ui) + if(stat & (NOPOWER|BROKEN)) return - user.set_machine(src) add_fingerprint(user) - var/dat = "" + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "CryopodConsole", name) + ui.open() - dat += "

Welcome, [user.real_name].


" - dat += "

" +/obj/machinery/computer/cryopod/ui_data(mob/user) + var/list/data = list() + data["frozen_crew"] = frozen_crew + var/list/item_meta = list() - switch(src.menu) - if(1) - dat += "View crew storage log

" - if(allow_items) - dat += "View objects storage log

" - dat += "Recover object

" - dat += "Recover all objects
" - if(2) - dat += "<< Back

" - dat += "

Recently stored/teleported Crew




" - if(!frozen_crew.len) - dat += "There has been no storage usage at this terminal.
" - else - for(var/person in frozen_crew) - dat += "[person]
" - dat += "
" - if(3) - dat += "<< Back

" - dat += "

Recently stored objects




" - if(!stored_packages.len) - dat += "There has been no storage usage at this terminal.
" - else - for(var/obj/O in stored_packages) - dat += "[O.name]
" - dat += "
" + for(var/obj/item/storage/box/blue/cryostorage_items/O as anything in stored_packages) + item_meta += list(list("name" = O.real_name, "ref" = REF(O))) // i am truely livid about byond lists + data["item_meta"] = item_meta - var/datum/browser/popup = new(user, "cryopod_console", "General Storage System Control") - popup.set_content(dat) - popup.open() + var/obj/item/card/id/id_card + var/datum/bank_account/current_user + if(isliving(user)) + var/mob/living/person = user + id_card = person.get_idcard() + if(id_card?.registered_account) + current_user = id_card.registered_account + if(current_user) + data["account_name"] = current_user.account_holder // i do not know why but this uses budget? -/obj/machinery/computer/cryopod/Topic(href, href_list) + return data + +/obj/machinery/computer/cryopod/ui_act(action, params) if(..()) - return TRUE + return - var/mob/user = usr - - add_fingerprint(user) - - if(href_list["item"]) - if(!allowed(user) && !(obj_flags & EMAGGED)) - to_chat(user, "Access Denied.") + if(action == "item") + if(!allowed(usr) && !(obj_flags & EMAGGED)) + to_chat(usr, "Access Denied.") playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0) - updateUsrDialog() return if(!allow_items) return if(stored_packages.len == 0) - to_chat(user, "There is nothing to recover from storage.") + to_chat(usr, "There is nothing to recover from storage.") playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0) - updateUsrDialog() return - var/obj/I = input(user, "Please choose which object to retrieve.","Object recovery",null) as null|anything in stored_packages + var/obj/I = locate(params["item"]) playsound(src, "terminal_type", 25, 0) + if(!I) return if(!(I in stored_packages)) - to_chat(user, "\The [I] is no longer in storage.") + to_chat(usr, "\The [I] is no longer in storage.") playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0) - updateUsrDialog() return visible_message("The console beeps happily as it disgorges \the [I].") playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, 0) I.forceMove(drop_location()) - if(user && Adjacent(user) && user.can_hold_items()) - user.put_in_hands(I) + if(usr && Adjacent(usr) && usr.can_hold_items()) + usr.put_in_hands(I) stored_packages -= I - updateUsrDialog() - else if(href_list["allitems"]) - playsound(src, "terminal_type", 25, 0) - if(!allowed(user) && !(obj_flags & EMAGGED)) - to_chat(user, "Access Denied.") - playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0) - updateUsrDialog() - return - - if(!allow_items) - return - - if(stored_packages.len == 0) - to_chat(user, "There is nothing to recover from storage.") - playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0) - return - - visible_message("The console beeps happily as it disgorges the desired objects.") - playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, 0) - - for(var/obj/O in stored_packages) - O.forceMove(get_turf(src)) - stored_packages.Cut() - updateUsrDialog() - - else if (href_list["menu"]) - src.menu = text2num(href_list["menu"]) - playsound(src, "terminal_type", 25, 0) - updateUsrDialog() - - ui_interact(usr) - updateUsrDialog() - return - -/obj/item/circuitboard/cryopodcontrol - name = "Circuit board (Cryogenic Oversight Console)" - build_path = "/obj/machinery/computer/cryopod" - -/obj/machinery/computer/cryopod/contents_explosion(severity, target, origin) - return - -/// The box -/obj/item/storage/box/blue/cryostorage_items - w_class = WEIGHT_CLASS_HUGE - -//Cryopods themselves. +// Cryopods themselves. /obj/machinery/cryopod name = "cryogenic freezer" desc = "Suited for Cyborgs and Humanoids, the pod is a safe place for personnel affected by the Space Sleep Disorder to get some rest." @@ -179,47 +136,55 @@ var/on_store_message = "has entered long-term storage." var/on_store_name = "Cryogenic Oversight" - // 15 minutes-ish safe period before being despawned. - var/time_till_despawn = 15 * 600 // This is reduced by 90% if a player manually enters cryo - var/despawn_world_time = null // Used to keep track of the safe period. + /// Time until despawn when a mob enters a cryopod. You can cryo other people in pods. + var/time_till_despawn = 30 SECONDS + /// Cooldown for when it's now safe to try an despawn the player. + COOLDOWN_DECLARE(despawn_world_time) - var/obj/machinery/computer/cryopod/control_computer - var/item_storage_type = /obj/item/storage/box/blue/cryostorage_items //with how storage components work this can be anything the player can open or anything with a storage component. - var/last_no_computer_message = 0 + ///Weakref to our controller + var/datum/weakref/control_computer_weakref + COOLDOWN_DECLARE(last_no_computer_message) -/obj/machinery/cryopod/Initialize(mapload) - . = ..() +/obj/machinery/cryopod/Initialize() + ..() + return INITIALIZE_HINT_LATELOAD //Gotta populate the cryopod computer GLOB first + +/obj/machinery/cryopod/LateInitialize() update_icon() - find_control_computer(mapload) + find_control_computer() + +// This is not a good situation +/obj/machinery/cryopod/Destroy() + control_computer_weakref = null + return ..() /obj/machinery/cryopod/proc/find_control_computer(urgent = FALSE) - for(var/obj/machinery/computer/cryopod/C in get_area(src)) - control_computer = C - if(C) - return C - break + for(var/cryo_console as anything in GLOB.cryopod_computers) + var/obj/machinery/computer/cryopod/console = cryo_console + if(get_area(console) == get_area(src)) + control_computer_weakref = WEAKREF(console) + break // Don't send messages unless we *need* the computer, and less than five minutes have passed since last time we messaged - if(!control_computer && urgent && last_no_computer_message + 5*60*10 < world.time) + if(!control_computer_weakref && urgent && COOLDOWN_FINISHED(src, last_no_computer_message)) + COOLDOWN_START(src, last_no_computer_message, 5 MINUTES) log_admin("\The [src] in [get_area(src)] could not find control computer!") message_admins("\The [src] in [get_area(src)] could not find control computer!") last_no_computer_message = world.time - return control_computer != null + return control_computer_weakref != null -/obj/machinery/cryopod/close_machine(mob/user) - if(!control_computer) +/obj/machinery/cryopod/close_machine(atom/movable/target) + if(!control_computer_weakref) find_control_computer(TRUE) - if((isnull(user) || istype(user)) && state_open && !panel_open) - ..(user) + if((isnull(target) || isliving(target)) && state_open && !panel_open) + ..(target) var/mob/living/mob_occupant = occupant - investigate_log("[src] closed with occupant [key_name(occupant)] by user [key_name(user)].", INVESTIGATE_CRYOGENICS) - if(mob_occupant && mob_occupant.stat != DEAD && !(tele)) - to_chat(occupant, "You feel cool air surround you. You go numb as your senses turn inward.") - if(mob_occupant.client)//if they're logged in - despawn_world_time = world.time + (time_till_despawn * 0.1) - else - despawn_world_time = world.time + time_till_despawn + investigate_log("\The [src] closed with occupant [key_name(occupant)] by user [key_name(target)].", INVESTIGATE_CRYOGENICS) + if(mob_occupant && mob_occupant.stat != DEAD) + to_chat(occupant, span_notice("You feel cool air surround you. You go numb as your senses turn inward.")) + + COOLDOWN_START(src, despawn_world_time, time_till_despawn) icon_state = "cryopod" /obj/machinery/cryopod/open_machine() @@ -231,9 +196,9 @@ name = initial(name) /obj/machinery/cryopod/container_resist(mob/living/user) - investigate_log("[src] resisted by [key_name(user)] with occupant [key_name(occupant)].", INVESTIGATE_CRYOGENICS) - visible_message("[occupant] emerges from \the [src]!", - "You climb out of \the [src]!") + investigate_log("\The [src] container resisted by [key_name(user)] with occupant [key_name(occupant)].", INVESTIGATE_CRYOGENICS) + visible_message(span_notice("[occupant] emerges from [src]!"), + span_notice("You climb out of [src]!")) open_machine() /obj/machinery/cryopod/relaymove(mob/user) @@ -244,41 +209,125 @@ return var/mob/living/mob_occupant = occupant - if(mob_occupant) - // Eject dead people - if(mob_occupant.stat == DEAD) - open_machine() + if(mob_occupant.stat == DEAD) + open_machine() - if(!(world.time > despawn_world_time + 100))//+ 10 seconds - return + if(!mob_occupant.client && COOLDOWN_FINISHED(src, despawn_world_time)) + if(!control_computer_weakref) + find_control_computer(urgent = TRUE) - if(!mob_occupant.client && mob_occupant.stat < 2) //Occupant is living and has no client. - if(!control_computer) - find_control_computer(urgent = TRUE)//better hope you found it this time + despawn_occupant() - despawn_occupant() +/obj/machinery/cryopod/proc/handle_objectives() + var/mob/living/mob_occupant = occupant + // Update any existing objectives involving this mob. + for(var/datum/objective/objective in GLOB.objectives) + // We don't want revs to get objectives that aren't for heads of staff. Letting + // them win or lose based on cryo is silly so we remove the objective. + if(istype(objective,/datum/objective/mutiny) && objective.target == mob_occupant.mind) + objective.team.objectives -= objective + qdel(objective) + for(var/datum/mind/mind in objective.team.members) + to_chat(mind.current, "
[span_userdanger("Your target is no longer within reach. Objective removed!")]") + mind.announce_objectives() + else if(istype(objective.target) && objective.target == mob_occupant.mind) + if(istype(objective, /datum/objective/contract)) + var/datum/antagonist/traitor/affected_traitor = objective.owner.has_antag_datum(/datum/antagonist/traitor) + var/datum/contractor_hub/affected_contractor_hub = affected_traitor.contractor_hub + for(var/datum/syndicate_contract/affected_contract as anything in affected_contractor_hub.assigned_contracts) + if(affected_contract.contract == objective) + affected_contract.generate(affected_contractor_hub.assigned_targets) + affected_contractor_hub.assigned_targets.Add(affected_contract.contract.target) + to_chat(objective.owner.current, "
[span_userdanger("Contract target out of reach. Contract rerolled.")]") + break + else + var/old_target = objective.target + objective.target = null + if(!objective) + return + objective.find_target() + if(!objective.target && objective.owner) + to_chat(objective.owner.current, "
[span_userdanger("Your target is no longer within reach. Objective removed!")]") + for(var/datum/antagonist/antag in objective.owner.antag_datums) + antag.objectives -= objective + if (!objective.team) + objective.update_explanation_text() + objective.owner.announce_objectives() + to_chat(objective.owner.current, "
[span_userdanger("You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!")]") + else + var/list/objectivestoupdate + for(var/datum/mind/objective_owner in objective.get_owners()) + to_chat(objective_owner.current, "
[span_userdanger("You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!")]") + for(var/datum/objective/update_target_objective in objective_owner.get_all_objectives()) + LAZYADD(objectivestoupdate, update_target_objective) + objectivestoupdate += objective.team.objectives + for(var/datum/objective/update_objective in objectivestoupdate) + if(update_objective.target != old_target || !istype(update_objective,objective.type)) + continue + update_objective.target = objective.target + update_objective.update_explanation_text() + to_chat(objective.owner.current, "
[span_userdanger("You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!")]") + update_objective.owner.announce_objectives() + qdel(objective) - if(tele) - despawn_occupant() - do_fake_sparks(2, TRUE, src) - playsound(src, 'sound/weapons/emitter2.ogg', 25, 1, extrarange = 3, falloff_exponent = 5) +/obj/machinery/cryopod/proc/should_preserve_item(obj/item/item) + for(var/datum/objective_item/steal/possible_item in GLOB.possible_items) + if(istype(item, possible_item.targetitem)) + return TRUE + return FALSE // This function can not be undone; do not call this unless you are sure /obj/machinery/cryopod/proc/despawn_occupant() - if(!control_computer) - find_control_computer() - var/mob/living/mob_occupant = occupant + var/list/crew_member = list() + crew_member["name"] = mob_occupant.real_name + + if(mob_occupant.mind) + // Handle job slot/tater cleanup. + var/job = mob_occupant.mind.assigned_role + crew_member["job"] = job + SSjob.FreeRole(job) + // if(LAZYLEN(mob_occupant.mind.objectives)) + // mob_occupant.mind.objectives.Cut() + mob_occupant.mind.special_role = null + else + crew_member["job"] = "N/A" + + // Delete them from datacore. + var/announce_rank = null + for(var/datum/data/record/medical_record as anything in GLOB.data_core.medical) + if(medical_record.fields["name"] == mob_occupant.real_name) + qdel(medical_record) + for(var/datum/data/record/security_record as anything in GLOB.data_core.security) + if(security_record.fields["name"] == mob_occupant.real_name) + qdel(security_record) + for(var/datum/data/record/general_record as anything in GLOB.data_core.general) + if(general_record.fields["name"] == mob_occupant.real_name) + announce_rank = general_record.fields["rank"] + qdel(general_record) + + var/obj/machinery/computer/cryopod/control_computer = control_computer_weakref?.resolve() + if(!control_computer) + control_computer_weakref = null + else + control_computer.frozen_crew += list(crew_member) + + // Make an announcement and log the person entering storage. + if(GLOB.announcement_systems.len) + var/obj/machinery/announcement_system/announcer = pick(GLOB.announcement_systems) + announcer.announce(tele ? "CRYOSTORAGE_TELE" : "CRYOSTORAGE", mob_occupant.real_name, announce_rank, list()) + + visible_message(span_notice("\The [src] hums and hisses as it [tele ? "teleports" : "moves"] [mob_occupant.real_name] [tele ? "to centcom" : "into storage"].")) + if(tele) + do_fake_sparks(2, TRUE, src) // Oh yeah plasmafire time. + playsound(src, 'sound/weapons/emitter2.ogg', 25, 1, extrarange = 3, falloff_exponent = 5) + + /* ============================= */ var/list/obj/item/storing = list() var/list/obj/item/destroying = list() var/list/obj/item/destroy_later = list() - - investigate_log("Despawning [key_name(mob_occupant)].", INVESTIGATE_CRYOGENICS) - - var/atom/target_store = (control_computer?.allow_items && control_computer) || src //the double control computer check makes it return the control computer. - var/drop_to_ground = !istype(target_store, /obj/machinery/computer/cryopod) - + var/drop_to_ground = !istype(control_computer, /obj/machinery/computer/cryopod) || !control_computer.allow_items var/mind_identity = mob_occupant.mind?.name var/occupant_identity = mob_occupant.real_name @@ -299,7 +348,6 @@ AM.forceMove(src) R.module.remove_module(I, TRUE) else - if(ishuman(mob_occupant)) var/mob/living/carbon/human/H = mob_occupant if(H.mind && H.client && H.client.prefs && H == H.mind.original_character) @@ -309,145 +357,89 @@ if(iscarbon(mob_occupant)) // sorry simp-le-mobs deserve no mercy var/mob/living/carbon/C = mob_occupant gear = C.get_all_gear() - for(var/i in gear) - var/obj/item/I = i - I.forceMove(src) - if(!istype(I)) - destroying += I + + for(var/obj/item/item_content as anything in gear) + if(!istype(item_content) || HAS_TRAIT(item_content, TRAIT_NODROP)) + destroying += item_content continue - if(I.item_flags & (DROPDEL | ABSTRACT)) - destroying += I - continue - if(HAS_TRAIT(I, TRAIT_NODROP)) - destroying += I + if(item_content.item_flags & (DROPDEL | ABSTRACT)) + destroying += item_content continue + + // destroying stays in mob for a bit + item_content.forceMove(src) + // WEE WOO SNOWFLAKE TIME - if(istype(I, /obj/item/pda)) - var/obj/item/pda/P = I + if(istype(item_content, /obj/item/pda)) + var/obj/item/pda/P = item_content if((P.owner == mind_identity) || (P.owner == occupant_identity)) destroying += P else storing += P - else if(istype(I, /obj/item/card/id)) - var/obj/item/card/id/idcard = I + else if(istype(item_content, /obj/item/card/id)) + var/obj/item/card/id/idcard = item_content if((idcard.registered_name == mind_identity) || (idcard.registered_name == occupant_identity)) destroying += idcard else storing += idcard else - storing += I + storing += item_content // get rid of mobs for(var/mob/living/L in mob_occupant.GetAllContents() - mob_occupant) L.forceMove(drop_location()) if(storing.len) - var/obj/O = new item_storage_type - O.name = "cryogenic retrieval package: [mob_occupant.real_name]" + var/obj/item/storage/box/blue/cryostorage_items/O = new /obj/item/storage/box/blue/cryostorage_items + O.name = "[tele ? "early leave" : "cryogenic"] retrieval package: [mob_occupant.real_name]" + O.real_name = mob_occupant.real_name for(var/i in storing) var/obj/item/I = i I.forceMove(O) - O.forceMove(drop_to_ground? target_store.drop_location() : target_store) - if((target_store == control_computer) && !drop_to_ground) + O.forceMove(drop_to_ground ? control_computer.drop_location() : control_computer) + if((control_computer == control_computer) && !drop_to_ground) control_computer.stored_packages += O - - QDEL_LIST(destroying) - - //Update any existing objectives involving this mob. - for(var/i in GLOB.objectives) - var/datum/objective/O = i - // We don't want revs to get objectives that aren't for heads of staff. Letting - // them win or lose based on cryo is silly so we remove the objective. - if(istype(O,/datum/objective/mutiny) && O.target == mob_occupant.mind) - qdel(O) - else if(O.target && istype(O.target, /datum/mind)) - if(O.target != mob_occupant.mind) - continue - if(O.check_midround_completion()) - continue - if(O.owner && O.owner.current) - to_chat(O.owner.current, "
You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!") - O.target = null - spawn(10) //This should ideally fire after the occupant is deleted. - if(!O) - return - O.find_target() - O.update_explanation_text() - if(!(O.target)) - qdel(O) - - if(mob_occupant.mind) - //Handle job slot/tater cleanup. - if(mob_occupant.mind.assigned_role) - var/job = mob_occupant.mind.assigned_role - SSjob.FreeRole(job) - mob_occupant.mind.special_role = null - - // Delete them from datacore. - - var/announce_rank = null - for(var/datum/data/record/R in GLOB.data_core.medical) - if((R.fields["name"] == mob_occupant.real_name)) - qdel(R) - for(var/datum/data/record/T in GLOB.data_core.security) - if((T.fields["name"] == mob_occupant.real_name)) - qdel(T) - for(var/datum/data/record/G in GLOB.data_core.general) - if((G.fields["name"] == mob_occupant.real_name)) - announce_rank = G.fields["rank"] - qdel(G) - - for(var/obj/machinery/computer/cloning/cloner in world) - for(var/datum/data/record/R in cloner.records) - if(R.fields["name"] == mob_occupant.real_name) - cloner.records.Remove(R) - - //Make an announcement and log the person entering storage. - if(control_computer) - control_computer.frozen_crew += "[mob_occupant.real_name]" - - if(GLOB.announcement_systems.len) - var/obj/machinery/announcement_system/announcer = pick(GLOB.announcement_systems) - announcer.announce("[tele ? "CRYOSTORAGE_TELE" : "CRYOSTORAGE"]", mob_occupant.real_name, announce_rank, list()) - visible_message("\The [src] hums and hisses as it [tele ? "teleports" : "moves"] [mob_occupant.real_name] [tele ? "to centcom" : "into storage"].") + /* ============================= */ // Ghost and delete the mob. + // they already did ghost verb var/mob/dead/observer/G = mob_occupant.get_ghost(TRUE) if(G) G.voluntary_ghosted = TRUE + // they did not ghost verb else mob_occupant.ghostize(FALSE, penalize = TRUE, voluntary = TRUE, cryo = TRUE) + QDEL_LIST(destroying) + handle_objectives() QDEL_NULL(occupant) QDEL_LIST(destroy_later) open_machine() name = initial(name) /obj/machinery/cryopod/MouseDrop_T(mob/living/target, mob/user) - if(!istype(target) || user.incapacitated() || !target.Adjacent(user) || !Adjacent(user) || !ismob(target) || (!ishuman(user) && !iscyborg(user)) || !istype(user.loc, /turf) || target.buckled) + if(!istype(target) || !can_interact(user) || !target.Adjacent(user) || !ismob(target) || isanimal(target) || !istype(user.loc, /turf) || target.buckled) return if(occupant) - to_chat(user, "\The [src] is already occupied!") + to_chat(user, span_notice("\The [src] is already occupied!")) return if(target.stat == DEAD) - to_chat(user, "Dead people can not be put into [tele ? "teleportation process" : "cryo"].") + to_chat(user, span_notice("Dead people can not be put into [tele ? "teleportation process" : "cryo"].")) return - if(target.client && user != target) + if(user != target && target.client) if(iscyborg(target)) - to_chat(user, "You can't put [target] into [src]. They're online.") + to_chat(user, span_danger("You can't put [target] into [src]. [target.p_theyre(capitalized = TRUE)] online.")) else - to_chat(user, "You can't put [target] into [src]. They're conscious.") + to_chat(user, span_danger("You can't put [target] into [src]. [target.p_theyre(capitalized = TRUE)] conscious.")) return - else if(target.client) - if(alert(target,"Would you like to [tele ? "be teleported out" : "enter cryosleep"]?",,"Yes","No") == "No") + else if(target.client) // mob has client + if(tgalert(target, "Would you like to [tele ? "be teleported out" : "enter cryosleep"]?", "Enter Cryopod?", "Yes", "No") != "Yes") return - var/generic_plsnoleave_message = " Please adminhelp before leaving the round, even if there are no administrators online!" - - if(target == user && world.time - target.client.cryo_warned > 5 MINUTES)//if we haven't warned them in the last 5 minutes + if(target == user && world.time - target.client.cryo_warned > 5 MINUTES) var/list/caught_string var/addendum = "" if(target.mind.assigned_role in GLOB.command_positions) @@ -465,30 +457,48 @@ LAZYADD(caught_string, "Revolutionary") if(caught_string) - alert(target, "You're a [english_list(caught_string)]![generic_plsnoleave_message][addendum]") + tgui_alert(target, "You're a [english_list(caught_string)]! [AHELP_FIRST_MESSAGE][addendum]") target.client.cryo_warned = world.time - return - if(!target || user.incapacitated() || !target.Adjacent(user) || !Adjacent(user) || (!ishuman(user) && !iscyborg(user)) || !istype(user.loc, /turf) || target.buckled) + if(!istype(target) || !can_interact(user) || !target.Adjacent(user) || !ismob(target) || isanimal(target) || !istype(user.loc, /turf) || target.buckled) return - //rerun the checks in case of shenanigans - - if(target == user) - visible_message("[user] starts climbing into \the [src].") - else - visible_message("[user] starts putting [target] into \the [src].") + // rerun the checks in case of shenanigans if(occupant) - to_chat(user, "\The [src] is in use.") + to_chat(user, span_notice("[src] is already occupied!")) return - close_machine(target) - to_chat(target, "If you ghost, log out or close your client now, your character will shortly be permanently removed from the round.") - name = "[name] ([occupant.name])" - log_admin("[key_name(target)] entered a [src].") - message_admins("[key_name_admin(target)] entered a [src]. (JMP)") + if(target == user) + visible_message("[user] starts climbing into \the [src].") + else + visible_message("[user] starts putting [target] into \the [src].") + + to_chat(target, span_warning("If you ghost, log out or close your client now, your character will shortly be permanently removed from the round.")) + + log_admin("[key_name(target)] entered a stasis pod.") + message_admins("[key_name_admin(target)] entered a stasis pod. [ADMIN_JMP(src)]") add_fingerprint(target) -//Attacks/effects. + close_machine(target) + name = "[name] ([target.name])" + +// Attacks/effects. /obj/machinery/cryopod/blob_act() - return //Sorta gamey, but we don't really want these to be destroyed. + return // Sorta gamey, but we don't really want these to be destroyed. + +#undef AHELP_FIRST_MESSAGE + +/obj/item/circuitboard/cryopodcontrol + name = "Circuit board (Cryogenic Oversight Console)" + build_path = /obj/machinery/computer/cryopod + +/obj/machinery/computer/cryopod/contents_explosion() + return + +/obj/machinery/computer/cryopod/contents_explosion() + return //don't blow everyone's shit up. + +/// The box +/obj/item/storage/box/blue/cryostorage_items + w_class = WEIGHT_CLASS_HUGE + var/real_name = "fire coderbus" diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm index 853bd73eac..d83819a1b3 100644 --- a/code/game/machinery/hologram.dm +++ b/code/game/machinery/hologram.dm @@ -1,8 +1,8 @@ /* Holograms! * Contains: - * Holopad - * Hologram - * Other stuff + * Holopad + * Hologram + * Other stuff */ /* @@ -24,7 +24,6 @@ Possible to do for anyone motivated enough: * Holopad */ -GLOBAL_LIST_EMPTY(network_holopads) #define HOLOPAD_PASSIVE_POWER_USAGE 1 #define HOLOGRAM_POWER_USAGE 2 @@ -32,6 +31,7 @@ GLOBAL_LIST_EMPTY(network_holopads) name = "holopad" desc = "It's a floor-mounted device for projecting holographic images." icon_state = "holopad0" + base_icon_state = "holopad" layer = LOW_OBJ_LAYER plane = FLOOR_PLANE flags_1 = HEAR_1 @@ -70,7 +70,7 @@ GLOBAL_LIST_EMPTY(network_holopads) var/obj/effect/overlay/holo_pad_hologram/replay_holo /// Calls will be automatically answered after a couple rings, here for debugging var/static/force_answer_call = FALSE - // var/static/list/holopads = list() + var/static/list/holopads = list() var/obj/effect/overlay/holoray/ray var/ringing = FALSE var/offset = FALSE @@ -107,26 +107,47 @@ GLOBAL_LIST_EMPTY(network_holopads) new_disk.forceMove(src) disk = new_disk -/obj/machinery/holopad/tutorial/on_attack_hand(mob/user, act_intent = user.a_intent, unarmed_attack_flags) +/obj/machinery/holopad/Moved(atom/OldLoc, Dir) + . = ..() + if(!loc) + return + // move any relevant holograms, basically non-AI, and rays with the pad + if(replay_holo) + replay_holo.abstract_move(loc) + for(var/i in holorays) + var/obj/effect/overlay/holoray/ray = holorays[i] + ray.abstract_move(loc) + var/list/non_call_masters = masters?.Copy() + for(var/datum/holocall/holocall as anything in holo_calls) + if(!holocall.user || !LAZYACCESS(masters, holocall.user)) + continue + non_call_masters -= holocall.user + // moving the eye moves the holo which updates the ray too + holocall.eye.setLoc(locate(clamp(x + (holocall.hologram.x - OldLoc.x), 1, world.maxx), clamp(y + (holocall.hologram.y - OldLoc.y), 1, world.maxy), z)) + for(var/mob/living/holo_master as anything in non_call_masters) + var/obj/effect/holo = masters[holo_master] + update_holoray(holo_master, holo.loc) + +/obj/machinery/holopad/tutorial/attack_hand(mob/user, list/modifiers) if(!istype(user)) return if(user.incapacitated() || !is_operational()) return if(replay_mode) replay_stop() - else if(disk && disk.record) + else if(disk?.record) replay_start() /obj/machinery/holopad/tutorial/HasProximity(atom/movable/AM) if (!isliving(AM)) return - if(!replay_mode && (disk && disk.record)) + if(!replay_mode && (disk?.record)) replay_start() /obj/machinery/holopad/Initialize() . = ..() if(on_network) - GLOB.network_holopads += src + holopads += src /obj/machinery/holopad/Destroy() if(outgoing_call) @@ -146,7 +167,7 @@ GLOBAL_LIST_EMPTY(network_holopads) QDEL_NULL(disk) - GLOB.network_holopads -= src + holopads -= src return ..() /obj/machinery/holopad/power_change() @@ -172,8 +193,10 @@ GLOBAL_LIST_EMPTY(network_holopads) /obj/machinery/holopad/examine(mob/user) . = ..() - if(in_range(user, src) || isobserver(user)) - . += "The status display reads: Current projection range: [holo_range] units." + if(isAI(user)) + . += span_notice("The status display reads: Current projection range: [holo_range] units. Use :h to speak through the projection. Right-click to project or cancel a projection. Alt-click to hangup all active and incomming calls. Ctrl-click to end projection without jumping to your last location.") + else if(in_range(user, src) || isobserver(user)) + . += span_notice("The status display reads: Current projection range: [holo_range] units.") /obj/machinery/holopad/attackby(obj/item/P, mob/user, params) if(default_deconstruction_screwdriver(user, "holopad_open", "holopad0", P)) @@ -190,11 +213,11 @@ GLOBAL_LIST_EMPTY(network_holopads) if(istype(P,/obj/item/disk/holodisk)) if(disk) - to_chat(user,"There's already a disk inside [src]!") + to_chat(user,span_warning("There's already a disk inside [src]!")) return if (!user.transferItemToLoc(P,src)) return - to_chat(user,"You insert [P] into [src].") + to_chat(user,span_notice("You insert [P] into [src].")) disk = P return @@ -242,24 +265,29 @@ GLOBAL_LIST_EMPTY(network_holopads) switch(action) if("AIrequest") + if(isAI(usr)) + var/mob/living/silicon/ai/ai_user = usr + ai_user.eyeobj.setLoc(get_turf(src)) + to_chat(usr, span_info("AIs can not request AI presence. Jumping instead.")) + return if(last_request + 200 < world.time) last_request = world.time - to_chat(usr, "You requested an AI's presence.") + to_chat(usr, span_info("You requested an AI's presence.")) var/area/area = get_area(src) for(var/mob/living/silicon/ai/AI in GLOB.silicon_mobs) if(!AI.client) continue - to_chat(AI, "Your presence is requested at \the [area].") + to_chat(AI, span_info("Your presence is requested at \the [area].
")) // Project Hologram?")) return TRUE else - to_chat(usr, "A request for AI presence was already sent recently.") + to_chat(usr, span_info("A request for AI presence was already sent recently.")) return if("holocall") if(outgoing_call) return if(usr.loc == loc) var/list/callnames = list() - for(var/I in GLOB.network_holopads) + for(var/I in holopads) var/area/A = get_area(I) if(A) LAZYADD(callnames[A], I) @@ -274,7 +302,7 @@ GLOBAL_LIST_EMPTY(network_holopads) calling = TRUE return TRUE else - to_chat(usr, "You must stand on the holopad to make a call!") + to_chat(usr, span_warning("You must stand on the holopad to make a call!")) if("connectcall") var/datum/holocall/call_to_connect = locate(params["holopad"]) in holo_calls if(!QDELETED(call_to_connect)) @@ -285,6 +313,12 @@ GLOBAL_LIST_EMPTY(network_holopads) if(!QDELETED(call_to_disconnect)) call_to_disconnect.Disconnect(src) return TRUE + if("rejectall") + for(var/datum/holocall/call_to_reject as anything in holo_calls) + if(call_to_reject.connected_holopad == src) // do not kill the current connection + continue + call_to_reject.Disconnect(src) + return TRUE if("disk_eject") if(disk && !replay_mode) disk.forceMove(drop_location()) @@ -327,14 +361,13 @@ GLOBAL_LIST_EMPTY(network_holopads) return TRUE /** - * hangup_all_calls: Disconnects all current holocalls from the holopad - */ + * hangup_all_calls: Disconnects all current holocalls from the holopad + */ /obj/machinery/holopad/proc/hangup_all_calls() for(var/I in holo_calls) var/datum/holocall/HC = I HC.Disconnect(src) -//do not allow AIs to answer calls or people will use it to meta the AI sattelite /obj/machinery/holopad/attack_ai(mob/living/silicon/ai/user) if (!istype(user)) return @@ -343,12 +376,25 @@ GLOBAL_LIST_EMPTY(network_holopads) /*There are pretty much only three ways to interact here. I don't need to check for client since they're clicking on an object. This may change in the future but for now will suffice.*/ - if(user.eyeobj.loc != src.loc)//Set client eye on the object if it's not already. - user.eyeobj.setLoc(get_turf(src)) - else if(!LAZYLEN(masters) || !masters[user])//If there is no hologram, possibly make one. + if(!LAZYLEN(masters) || !masters[user])//If there is no hologram, possibly make one. activate_holo(user) - else//If there is a hologram, remove it. + else//If there is a hologram, remove it, and jump to your last location. clear_holo(user) + // if(user.lastloc)//only jump to your last location if your lastloc is set, which only sets if you projected from a request message. + // user.eyeobj.setLoc(user.lastloc) + // user.lastloc = null + +/obj/machinery/holopad/AICtrlClick(mob/living/silicon/ai/user) + if (!istype(user)) + return + if (!on_network) + return + if(!LAZYLEN(masters) || !masters[user])//If there is no hologram, then this button does nothing. + return + else//If there is a hologram, remove it, but dont jump to your last location. + // user.lastloc = null + clear_holo(user) + return /obj/machinery/holopad/process() if(LAZYLEN(masters)) @@ -378,25 +424,26 @@ GLOBAL_LIST_EMPTY(network_holopads) if(outgoing_call) HC.Disconnect(src)//can't answer calls while calling else - playsound(src, 'sound/machines/twobeep.ogg', 100) //bring, bring! + playsound(src, 'sound/machines/twobeep.ogg', 100) //bring, bring! ringing = TRUE - update_icon() + update_appearance() /obj/machinery/holopad/proc/activate_holo(mob/living/user) var/mob/living/silicon/ai/AI = user if(!istype(AI)) AI = null - if(is_operational() && (!AI || AI.eyeobj.loc == loc))//If the projector has power and client eye is on it - if (AI && istype(AI.current, /obj/machinery/holopad)) - to_chat(user, "ERROR: \black Image feed in progress.") + if(is_operational())//If the projector has power + if(AI && istype(AI.current, /obj/machinery/holopad)) + to_chat(user, "[span_danger("ERROR:")] \black Image feed in progress.") return var/obj/effect/overlay/holo_pad_hologram/Hologram = new(loc)//Spawn a blank effect at the location. if(AI) Hologram.icon = AI.holo_icon - else //make it like real life + AI.eyeobj.setLoc(get_turf(src)) //ensure the AI camera moves to the holopad + else //make it like real life Hologram.icon = user.icon Hologram.icon_state = user.icon_state Hologram.copy_overlays(user, TRUE) @@ -407,17 +454,17 @@ GLOBAL_LIST_EMPTY(network_holopads) Hologram.mouse_opacity = MOUSE_OPACITY_TRANSPARENT//So you can't click on it. Hologram.layer = FLY_LAYER//Above all the other objects/mobs. Or the vast majority of them. - Hologram.setAnchored(TRUE)//So space wind cannot drag it. + Hologram.set_anchored(TRUE)//So space wind cannot drag it. Hologram.name = "[user.name] (Hologram)"//If someone decides to right click. - Hologram.set_light(2) //hologram lighting + Hologram.set_light(2) //hologram lighting move_hologram() set_holo(user, Hologram) - visible_message("A holographic image of [user] flickers to life before your eyes!") + visible_message(span_notice("A holographic image of [user] flickers to life before your eyes!")) return Hologram else - to_chat(user, "ERROR: Unable to project hologram.") + to_chat(user, "[span_danger("ERROR:")] Unable to project hologram.") /*This is the proc for special two-way communication between AI and holopad/people talking near holopad. For the other part of the code, check silicon say.dm. Particularly robot talk.*/ @@ -430,10 +477,13 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ for(var/I in holo_calls) var/datum/holocall/HC = I - if(HC.connected_holopad == src && speaker != HC.hologram) - HC.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) + if(HC.connected_holopad == src) + if(speaker == HC.hologram && HC.user.client?.prefs.chat_on_map) + HC.user.create_chat_message(speaker, message_language, raw_message, spans) + else + HC.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) - if(outgoing_call && speaker == outgoing_call.user) + if(outgoing_call?.hologram && speaker == outgoing_call.user) outgoing_call.hologram.say(raw_message) if(record_mode && speaker == record_user) @@ -447,16 +497,15 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ set_light(2) else set_light(0) - update_icon() + update_appearance() /obj/machinery/holopad/update_icon_state() var/total_users = LAZYLEN(masters) + LAZYLEN(holo_calls) if(ringing) - icon_state = "holopad_ringing" - else if(total_users || replay_mode) - icon_state = "holopad1" - else - icon_state = "holopad0" + icon_state = "[base_icon_state]_ringing" + return ..() + icon_state = "[base_icon_state][(total_users || replay_mode) ? 1 : 0]" + return ..() /obj/machinery/holopad/proc/set_holo(mob/living/user, obj/effect/overlay/holo_pad_hologram/h) LAZYSET(masters, user, h) @@ -488,7 +537,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ var/obj/effect/overlay/holo_pad_hologram/h = masters[holo_owner] if(!h || h.HC) //Holocalls can't change source. return FALSE - for(var/pad in GLOB.network_holopads) + for(var/pad in holopads) var/obj/machinery/holopad/another = pad if(another == src) continue @@ -524,7 +573,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ else transfered = TRUE //All is good. - holo.forceMove(new_turf) + holo.abstract_move(new_turf) if(!transfered) update_holoray(user,new_turf) return TRUE @@ -565,10 +614,10 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ holder.selected_language = record.language Hologram.mouse_opacity = MOUSE_OPACITY_TRANSPARENT//So you can't click on it. Hologram.layer = FLY_LAYER//Above all the other objects/mobs. Or the vast majority of them. - Hologram.setAnchored(TRUE)//So space wind cannot drag it. + Hologram.set_anchored(TRUE)//So space wind cannot drag it. Hologram.name = "[record.caller_name] (Hologram)"//If someone decides to right click. - Hologram.set_light(2) //hologram lighting - visible_message("A holographic image of [record.caller_name] flickers to life before your eyes!") + Hologram.set_light(2) //hologram lighting + visible_message(span_notice("A holographic image of [record.caller_name] flickers to life before your eyes!")) return Hologram /obj/machinery/holopad/proc/replay_start() @@ -661,7 +710,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ record_user = null /obj/machinery/holopad/proc/record_clear() - if(disk && disk.record) + if(disk?.record) QDEL_NULL(disk.record) /obj/effect/overlay/holo_pad_hologram @@ -673,6 +722,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ Impersonation = null if(!QDELETED(HC)) HC.Disconnect(HC.calling_holopad) + HC = null return ..() /obj/effect/overlay/holo_pad_hologram/Process_Spacemove(movement_dir = 0) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 0dbfcd257c..1c6c6686e8 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -46,11 +46,23 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb max_integrity = 200 obj_flags = NONE + ///Item flags for the item var/item_flags = NONE - var/hitsound = null - var/usesound = null - var/throwhitsound = null + ///Sound played when you hit something with the item + var/hitsound + ///Played when the item is used, for example tools + var/usesound + ///Used when yate into a mob + var/mob_throw_hit_sound + ///Sound used when equipping the item into a valid slot + var/equip_sound + ///Sound uses when picking the item up (into your hands) + var/pickup_sound + ///Sound uses when dropping the item, or when its thrown. + var/drop_sound + ///Whether or not we use stealthy audio levels for this item's attack sounds + var/stealthy_audio = FALSE /// Weight class for how much storage capacity it uses and how big it physically is meaning storages can't hold it if their maximum weight class isn't as high as it. var/w_class = WEIGHT_CLASS_NORMAL @@ -129,7 +141,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb var/datum/dog_fashion/dog_fashion = null //Tooltip vars - var/force_string //string form of an item's force. Edit this var only to set a custom force string + ///string form of an item's force. Edit this var only to set a custom force string + var/force_string var/last_force_string_check = 0 var/trigger_guard = TRIGGER_GUARD_NONE @@ -388,7 +401,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb if(!user.temporarilyRemoveItemFromInventory(I = src)) return - remove_outline() + . = FALSE pickup(user) add_fingerprint(user) if(!user.put_in_active_hand(src, FALSE, FALSE)) @@ -461,9 +474,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb item_flags &= ~(IN_INVENTORY) item_flags &= ~(IN_STORAGE) SEND_SIGNAL(src, COMSIG_ITEM_DROPPED,user) - remove_outline() - // if(!silent) - // playsound(src, drop_sound, DROP_SOUND_VOLUME, ignore_walls = FALSE) + if(!silent) + playsound(src, drop_sound, DROP_SOUND_VOLUME, ignore_walls = FALSE) user?.update_equipment_speed_mods() // called just as an item is picked up (loc is not yet changed) @@ -710,8 +722,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb if(isliving(hit_atom)) //Living mobs handle hit sounds differently. var/volume = get_volume_by_throwforce_and_or_w_class() if (throwforce > 0) - if (throwhitsound) - playsound(hit_atom, throwhitsound, volume, TRUE, -1) + if (mob_throw_hit_sound) + playsound(hit_atom, mob_throw_hit_sound, volume, TRUE, -1) else if(hitsound) playsound(hit_atom, hitsound, volume, TRUE, -1) else @@ -719,8 +731,8 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb else playsound(hit_atom, 'sound/weapons/throwtap.ogg', 1, volume, -1) - // else - // playsound(src, drop_sound, YEET_SOUND_VOLUME, ignore_walls = FALSE) + else if (drop_sound) + playsound(src, drop_sound, YEET_SOUND_VOLUME, ignore_walls = FALSE) return hit_atom.hitby(src, 0, itempush, throwingdatum=throwingdatum) /obj/item/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, messy_throw = TRUE) @@ -906,47 +918,55 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb openToolTip(user,src,params,title = name,content = "[desc]
Force: [force_string]",theme = "") /obj/item/MouseEntered(location, control, params) + . = ..() SEND_SIGNAL(src, COMSIG_ITEM_MOUSE_ENTER, location, control, params) - if((item_flags & IN_INVENTORY || item_flags & IN_STORAGE) && usr?.client.prefs.enable_tips && !QDELETED(src)) - var/timedelay = max(usr.client.prefs.tip_delay * 0.01, 0.01) // I heard multiplying is faster, also runtimes from very low/negative numbers - usr.client.tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, usr), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. - var/mob/living/L = usr - if(istype(L) && (L.incapacitated() || (current_equipped_slot in L.check_obscured_slots()) || !L.canUnEquip(src))) - apply_outline(_size = 3) - else - apply_outline() + if(get(src, /mob) == usr && !QDELETED(src)) + var/mob/living/L = usr + if(usr.client.prefs.enable_tips) + var/timedelay = usr.client.prefs.tip_delay/100 + usr.client.tip_timer = addtimer(CALLBACK(src, .proc/openTip, location, control, params, usr), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. + if(usr.client.prefs.outline_enabled) + if(istype(L) && L.incapacitated()) + apply_outline(COLOR_RED_GRAY) //if they're dead or handcuffed, let's show the outline as red to indicate that they can't interact with that right now + else + apply_outline(usr.client.prefs.outline_color) //if the player's alive and well we send the command with no color set, so it uses the theme's color /obj/item/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params) . = ..() - remove_outline() + remove_filter("hover_outline") //get rid of the hover effect in case the mouse exit isn't called if someone drags and drops an item and somthing goes wrong -/obj/item/MouseExited(location,control,params) +/obj/item/MouseExited(location, control, params) SEND_SIGNAL(src, COMSIG_ITEM_MOUSE_EXIT, location, control, params) + deltimer(usr.client.tip_timer) //delete any in-progress timer if the mouse is moved off the item before it finishes closeToolTip(usr) - remove_outline() + remove_filter("hover_outline") -/obj/item/proc/apply_outline(colour = null, _size=1) - if(!(item_flags & IN_INVENTORY || item_flags & IN_STORAGE) || QDELETED(src) || isobserver(usr)) +/obj/item/proc/apply_outline(outline_color = null) + if(get(src, /mob) != usr || QDELETED(src) || isobserver(usr)) //cancel if the item isn't in an inventory, is being deleted, or if the person hovering is a ghost (so that people spectating you don't randomly make your items glow) return - if(usr.client) - if(!usr.client.prefs.outline_enabled) - return - if(!colour) - if(usr.client) - colour = usr.client.prefs.outline_color - if(!colour) - colour = COLOR_BLUE_GRAY - else - colour = COLOR_BLUE_GRAY - if(outline_filter) - filters -= outline_filter - outline_filter = filter(type="outline", size=_size, color=colour) - filters += outline_filter + var/theme = lowertext(usr.client.prefs.UI_style) + if(!outline_color) //if we weren't provided with a color, take the theme's color + switch(theme) //yeah it kinda has to be this way + if("midnight") + outline_color = COLOR_THEME_MIDNIGHT + if("plasmafire") + outline_color = COLOR_THEME_PLASMAFIRE + if("retro") + outline_color = COLOR_THEME_RETRO //just as garish as the rest of this theme + if("slimecore") + outline_color = COLOR_THEME_SLIMECORE + if("operative") + outline_color = COLOR_THEME_OPERATIVE + if("clockwork") + outline_color = COLOR_THEME_CLOCKWORK //if you want free gbp go fix the fact that clockwork's tooltip css is glass' + if("glass") + outline_color = COLOR_THEME_GLASS + else //this should never happen, hopefully + outline_color = COLOR_WHITE + if(color) + outline_color = COLOR_WHITE //if the item is recolored then the outline will be too, let's make the outline white so it becomes the same color instead of some ugly mix of the theme and the tint -/obj/item/proc/remove_outline() - if(outline_filter) - filters -= outline_filter - outline_filter = null + add_filter("hover_outline", 1, list("type" = "outline", "size" = 1, "color" = outline_color)) // Called when a mob tries to use the item as a tool. // Handles most checks. @@ -1228,3 +1248,14 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb pain_stam_pct = (!isnull(embedding["pain_stam_pct"]) ? embedding["pain_stam_pct"] : EMBEDDED_PAIN_STAM_PCT),\ embed_chance_turf_mod = (!isnull(embedding["embed_chance_turf_mod"]) ? embedding["embed_chance_turf_mod"] : EMBED_CHANCE_TURF_MOD)) return TRUE + +/** + * Updates all action buttons associated with this item + * + * Arguments: + * * status_only - Update only current availability status of the buttons to show if they are ready or not to use + * * force - Force buttons update even if the given button icon state has not changed + */ +/obj/item/proc/update_action_buttons(status_only = FALSE, force = FALSE) + for(var/datum/action/current_action as anything in actions) + current_action.UpdateButtonIcon(status_only, force) diff --git a/code/game/objects/items/devices/geiger_counter.dm b/code/game/objects/items/devices/geiger_counter.dm index 2413a3fb9a..aa54ee6219 100644 --- a/code/game/objects/items/devices/geiger_counter.dm +++ b/code/game/objects/items/devices/geiger_counter.dm @@ -19,6 +19,7 @@ w_class = WEIGHT_CLASS_SMALL slot_flags = ITEM_SLOT_BELT rad_flags = RAD_NO_CONTAMINATE + item_flags = NOBLUDGEON custom_materials = list(/datum/material/iron = 150, /datum/material/glass = 150) var/grace = RAD_GRACE_PERIOD @@ -35,17 +36,15 @@ . = ..() START_PROCESSING(SSobj, src) - soundloop = new(list(src), FALSE) + soundloop = new(src, FALSE) /obj/item/geiger_counter/Destroy() STOP_PROCESSING(SSobj, src) QDEL_NULL(soundloop) + return ..() -/obj/item/geiger_counter/process() - update_icon() - update_sound() - +/obj/item/geiger_counter/process(delta_time) if(!scanning) current_tick_amount = 0 return @@ -64,49 +63,55 @@ current_tick_amount = 0 + update_appearance() + update_sound() + /obj/item/geiger_counter/examine(mob/user) . = ..() if(!scanning) return - . += "Alt-click it to clear stored radiation levels." + . += span_info("Alt-click it to clear stored radiation levels.") if(obj_flags & EMAGGED) - . += "The display seems to be incomprehensible." + . += span_warning("The display seems to be incomprehensible.") return switch(radiation_count) if(-INFINITY to RAD_LEVEL_NORMAL) - . += "Ambient radiation level count reports that all is well." + . += span_notice("Ambient radiation level count reports that all is well.") if(RAD_LEVEL_NORMAL + 1 to RAD_LEVEL_MODERATE) - . += "Ambient radiation levels slightly above average." + . += span_alert("Ambient radiation levels slightly above average.") if(RAD_LEVEL_MODERATE + 1 to RAD_LEVEL_HIGH) - . += "Ambient radiation levels above average." + . += span_warning("Ambient radiation levels above average.") if(RAD_LEVEL_HIGH + 1 to RAD_LEVEL_VERY_HIGH) - . += "Ambient radiation levels highly above average." + . += span_danger("Ambient radiation levels highly above average.") if(RAD_LEVEL_VERY_HIGH + 1 to RAD_LEVEL_CRITICAL) - . += "Ambient radiation levels nearing critical level." + . += span_suicide("Ambient radiation levels nearing critical level.") if(RAD_LEVEL_CRITICAL + 1 to INFINITY) - . += "Ambient radiation levels above critical level!" + . += span_boldannounce("Ambient radiation levels above critical level!") - . += "The last radiation amount detected was [last_tick_amount]" + . += span_notice("The last radiation amount detected was [last_tick_amount]") /obj/item/geiger_counter/update_icon_state() if(!scanning) icon_state = "geiger_off" - else if(obj_flags & EMAGGED) + return ..() + if(obj_flags & EMAGGED) icon_state = "geiger_on_emag" - else - switch(radiation_count) - if(-INFINITY to RAD_LEVEL_NORMAL) - icon_state = "geiger_on_1" - if(RAD_LEVEL_NORMAL + 1 to RAD_LEVEL_MODERATE) - icon_state = "geiger_on_2" - if(RAD_LEVEL_MODERATE + 1 to RAD_LEVEL_HIGH) - icon_state = "geiger_on_3" - if(RAD_LEVEL_HIGH + 1 to RAD_LEVEL_VERY_HIGH) - icon_state = "geiger_on_4" - if(RAD_LEVEL_VERY_HIGH + 1 to RAD_LEVEL_CRITICAL) - icon_state = "geiger_on_4" - if(RAD_LEVEL_CRITICAL + 1 to INFINITY) - icon_state = "geiger_on_5" + return ..() + + switch(radiation_count) + if(-INFINITY to RAD_LEVEL_NORMAL) + icon_state = "geiger_on_1" + if(RAD_LEVEL_NORMAL + 1 to RAD_LEVEL_MODERATE) + icon_state = "geiger_on_2" + if(RAD_LEVEL_MODERATE + 1 to RAD_LEVEL_HIGH) + icon_state = "geiger_on_3" + if(RAD_LEVEL_HIGH + 1 to RAD_LEVEL_VERY_HIGH) + icon_state = "geiger_on_4" + if(RAD_LEVEL_VERY_HIGH + 1 to RAD_LEVEL_CRITICAL) + icon_state = "geiger_on_4" + if(RAD_LEVEL_CRITICAL + 1 to INFINITY) + icon_state = "geiger_on_5" + return ..() /obj/item/geiger_counter/proc/update_sound() var/datum/looping_sound/geiger/loop = soundloop @@ -124,21 +129,21 @@ if(amount <= RAD_BACKGROUND_RADIATION || !scanning) return current_tick_amount += amount - update_icon() + update_appearance() /obj/item/geiger_counter/attack_self(mob/user) scanning = !scanning - update_icon() - to_chat(user, "[icon2html(src, user)] You switch [scanning ? "on" : "off"] [src].") + update_appearance() + to_chat(user, span_notice("[icon2html(src, user)] You switch [scanning ? "on" : "off"] [src].")) -/obj/item/geiger_counter/afterattack(atom/target, mob/user) +/obj/item/geiger_counter/afterattack(atom/target, mob/living/user) . = ..() if(user.a_intent == INTENT_HELP) if(!(obj_flags & EMAGGED)) - user.visible_message("[user] scans [target] with [src].", "You scan [target]'s radiation levels with [src]...") + user.visible_message(span_notice("[user] scans [target] with [src]."), span_notice("You scan [target]'s radiation levels with [src]...")) addtimer(CALLBACK(src, .proc/scan, target, user), 20, TIMER_UNIQUE) // Let's not have spamming GetAllContents else - user.visible_message("[user] scans [target] with [src].", "You project [src]'s stored radiation into [target]!") + user.visible_message(span_notice("[user] scans [target] with [src]."), span_danger("You project [src]'s stored radiation into [target]!")) target.rad_act(radiation_count) radiation_count = 0 return TRUE @@ -156,57 +161,61 @@ if(isliving(A)) var/mob/living/M = A if(!M.radiation) - to_chat(user, "[icon2html(src, user)] Radiation levels within normal boundaries.") + to_chat(user, span_notice("[icon2html(src, user)] Radiation levels within normal boundaries.")) else - to_chat(user, "[icon2html(src, user)] Subject is irradiated. Radiation levels: [M.radiation] rad.") + to_chat(user, span_boldannounce("[icon2html(src, user)] Subject is irradiated. Radiation levels: [M.radiation].")) if(rad_strength) - to_chat(user, "[icon2html(src, user)] Target contains radioactive contamination. Radioactive strength: [rad_strength]") + to_chat(user, span_boldannounce("[icon2html(src, user)] Target contains radioactive contamination. Radioactive strength: [rad_strength]")) else - to_chat(user, "[icon2html(src, user)] Target is free of radioactive contamination.") + to_chat(user, span_notice("[icon2html(src, user)] Target is free of radioactive contamination.")) /obj/item/geiger_counter/attackby(obj/item/I, mob/user, params) if(I.tool_behaviour == TOOL_SCREWDRIVER && (obj_flags & EMAGGED)) if(scanning) - to_chat(user, "Turn off [src] before you perform this action!") - return 0 - user.visible_message("[user] unscrews [src]'s maintenance panel and begins fiddling with its innards...", "You begin resetting [src]...") + to_chat(user, span_warning("Turn off [src] before you perform this action!")) + return FALSE + user.visible_message(span_notice("[user] unscrews [src]'s maintenance panel and begins fiddling with its innards..."), span_notice("You begin resetting [src]...")) if(!I.use_tool(src, user, 40, volume=50)) - return 0 - user.visible_message("[user] refastens [src]'s maintenance panel!", "You reset [src] to its factory settings!") + return FALSE + user.visible_message(span_notice("[user] refastens [src]'s maintenance panel!"), span_notice("You reset [src] to its factory settings!")) obj_flags &= ~EMAGGED radiation_count = 0 - update_icon() - return 1 + update_appearance() + return TRUE else return ..() /obj/item/geiger_counter/AltClick(mob/living/user) - . = ..() if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) - return + return ..() if(!scanning) - to_chat(usr, "[src] must be on to reset its radiation level!") - return TRUE + to_chat(usr, span_warning("[src] must be on to reset its radiation level!")) + return radiation_count = 0 - to_chat(usr, "You flush [src]'s radiation counts, resetting it to normal.") - update_icon() - return TRUE + to_chat(usr, span_notice("You flush [src]'s radiation counts, resetting it to normal.")) + update_appearance() /obj/item/geiger_counter/emag_act(mob/user) . = ..() if(obj_flags & EMAGGED) return if(scanning) - to_chat(user, "Turn off [src] before you perform this action!") + to_chat(user, span_warning("Turn off [src] before you perform this action!")) return - to_chat(user, "You override [src]'s radiation storing protocols. It will now generate small doses of radiation, and stored rads are now projected into creatures you scan.") + to_chat(user, span_warning("You override [src]'s radiation storing protocols. It will now generate small doses of radiation, and stored rads are now projected into creatures you scan.")) obj_flags |= EMAGGED return TRUE /obj/item/geiger_counter/cyborg var/mob/listeningTo +/obj/item/geiger_counter/cyborg/cyborg_unequip(mob/user) + if(!scanning) + return + scanning = FALSE + update_appearance() + /obj/item/geiger_counter/cyborg/equipped(mob/user) . = ..() if(listeningTo == user) @@ -217,6 +226,7 @@ listeningTo = user /obj/item/geiger_counter/cyborg/proc/redirect_rad_act(datum/source, amount) + SIGNAL_HANDLER rad_act(amount) /obj/item/geiger_counter/cyborg/dropped(mob/user) diff --git a/code/game/objects/items/devices/reverse_bear_trap.dm b/code/game/objects/items/devices/reverse_bear_trap.dm index f2a0ea5450..de7e3d2812 100644 --- a/code/game/objects/items/devices/reverse_bear_trap.dm +++ b/code/game/objects/items/devices/reverse_bear_trap.dm @@ -24,8 +24,8 @@ /obj/item/reverse_bear_trap/Initialize() . = ..() - soundloop = new(list(src)) - soundloop2 = new(list(src)) + soundloop = new(src) + soundloop2 = new(src) /obj/item/reverse_bear_trap/Destroy() QDEL_NULL(soundloop) @@ -33,16 +33,16 @@ STOP_PROCESSING(SSprocessing, src) return ..() -/obj/item/reverse_bear_trap/process() +/obj/item/reverse_bear_trap/process(delta_time) if(!ticking) return - time_left-- + time_left -= delta_time soundloop2.mid_length = max(0.5, time_left - 5) //beepbeepbeepbeepbeep - if(!time_left || !isliving(loc)) + if(time_left <= 0 || !isliving(loc)) playsound(src, 'sound/machines/microwave/microwave-end.ogg', 100, FALSE) soundloop.stop() soundloop2.stop() - to_chat(loc, "*ding*") + to_chat(loc, span_userdanger("*ding*")) addtimer(CALLBACK(src, .proc/snap), 2) /obj/item/reverse_bear_trap/on_attack_hand(mob/user, act_intent = user.a_intent, unarmed_attack_flags) @@ -116,13 +116,22 @@ /obj/item/reverse_bear_trap/proc/reset() ticking = FALSE + update_appearance(UPDATE_OVERLAYS) REMOVE_TRAIT(src, TRAIT_NODROP, REVERSE_BEAR_TRAP_TRAIT) soundloop.stop() soundloop2.stop() STOP_PROCESSING(SSprocessing, src) +/obj/item/reverse_bear_trap/update_overlays() + . = ..() + if(ticking != TRUE) + return + /// note: this timer overlay increments one frame every second (to simulate a clock ticking). If you want to instead have it do a full cycle in a minute, set the 'delay' of each frame of the icon overlay to 75 rather than 10, and the worn overlay to twice that. + // . += "rbt_ticking" + /obj/item/reverse_bear_trap/proc/arm() //hulen ticking = TRUE + update_appearance(UPDATE_OVERLAYS) escape_chance = initial(escape_chance) //we keep these vars until re-arm, for tracking purposes time_left = initial(time_left) ADD_TRAIT(src, TRAIT_NODROP, REVERSE_BEAR_TRAP_TRAIT) diff --git a/code/game/objects/items/implants/implant_uplink.dm b/code/game/objects/items/implants/implant_uplink.dm index 0cac8f838a..bd861013dd 100644 --- a/code/game/objects/items/implants/implant_uplink.dm +++ b/code/game/objects/items/implants/implant_uplink.dm @@ -20,4 +20,7 @@ imp_type = /obj/item/implant/uplink/precharged /obj/item/implant/uplink/precharged - starting_tc = 10 + starting_tc = TELECRYSTALS_PRELOADED_IMPLANT + +/obj/item/implant/uplink/starting + starting_tc = TELECRYSTALS_DEFAULT - UPLINK_IMPLANT_TELECRYSTAL_COST diff --git a/code/game/objects/structures/artstuff.dm b/code/game/objects/structures/artstuff.dm index 9b2f305b42..b751724313 100644 --- a/code/game/objects/structures/artstuff.dm +++ b/code/game/objects/structures/artstuff.dm @@ -52,6 +52,8 @@ var/author_ckey var/icon_generated = FALSE var/icon/generated_icon + ///boolean that blocks persistence from saving it. enabled from printing copies, because we do not want to save copies. + var/no_save = FALSE // Painting overlay offset when framed var/framed_offset_x = 11 @@ -370,7 +372,7 @@ update_icon() /obj/structure/sign/painting/proc/save_persistent() - if(!persistence_id || !current_canvas) + if(!persistence_id || !current_canvas || current_canvas.no_save) return if(sanitize_filename(persistence_id) != persistence_id) stack_trace("Invalid persistence_id - [persistence_id]") diff --git a/code/game/objects/structures/manned_turret.dm b/code/game/objects/structures/manned_turret.dm index 3d0b28d2f3..cdbf19367b 100644 --- a/code/game/objects/structures/manned_turret.dm +++ b/code/game/objects/structures/manned_turret.dm @@ -25,7 +25,7 @@ /obj/machinery/manned_turret/Destroy() target = null target_turf = null - ..() + return ..() //BUCKLE HOOKS diff --git a/code/game/objects/structures/noticeboard.dm b/code/game/objects/structures/noticeboard.dm index 68da53aa6c..34fbdb3029 100644 --- a/code/game/objects/structures/noticeboard.dm +++ b/code/game/objects/structures/noticeboard.dm @@ -1,3 +1,5 @@ +#define MAX_NOTICES 5 + /obj/structure/noticeboard name = "notice board" desc = "A board for pinning important notices upon." @@ -7,8 +9,25 @@ density = FALSE anchored = TRUE max_integrity = 150 + /// Current number of a pinned notices var/notices = 0 +/obj/structure/noticeboard/directional/north + dir = SOUTH + pixel_y = 32 + +/obj/structure/noticeboard/directional/south + dir = NORTH + pixel_y = -32 + +/obj/structure/noticeboard/directional/east + dir = WEST + pixel_x = 32 + +/obj/structure/noticeboard/directional/west + dir = EAST + pixel_x = -32 + /obj/structure/noticeboard/Initialize(mapload) . = ..() @@ -16,7 +35,7 @@ return for(var/obj/item/I in loc) - if(notices > 4) + if(notices >= MAX_NOTICES) break if(istype(I, /obj/item/paper)) I.forceMove(src) @@ -27,67 +46,84 @@ /obj/structure/noticeboard/attackby(obj/item/O, mob/user, params) if(istype(O, /obj/item/paper) || istype(O, /obj/item/photo)) if(!allowed(user)) - to_chat(user, "You are not authorized to add notices") + to_chat(user, span_warning("You are not authorized to add notices!")) return - if(notices < 5) + if(notices < MAX_NOTICES) if(!user.transferItemToLoc(O, src)) return notices++ icon_state = "nboard0[notices]" - to_chat(user, "You pin the [O] to the noticeboard.") + to_chat(user, span_notice("You pin the [O] to the noticeboard.")) else - to_chat(user, "The notice board is full") + to_chat(user, span_warning("The notice board is full!")) else return ..() -/obj/structure/noticeboard/interact(mob/user) - ui_interact(user) +/obj/structure/noticeboard/ui_state(mob/user) + return GLOB.physical_state -/obj/structure/noticeboard/ui_interact(mob/user) +/obj/structure/noticeboard/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "NoticeBoard", name) + ui.open() + +/obj/structure/noticeboard/ui_data(mob/user) + var/list/data = list() + data["allowed"] = allowed(user) + data["items"] = list() + for(var/obj/item/content in contents) + var/list/content_data = list( + name = content.name, + ref = REF(content) + ) + data["items"] += list(content_data) + return data + +/obj/structure/noticeboard/ui_act(action, params) . = ..() - var/auth = allowed(user) - var/dat = "[name]
" - for(var/obj/item/P in src) - if(istype(P, /obj/item/paper)) - dat += "[P.name] [auth ? "Write Remove" : ""]
" - else - dat += "[P.name] [auth ? "Remove" : ""]
" - user << browse("Notices[dat]","window=noticeboard") - onclose(user, "noticeboard") + if(.) + return -/obj/structure/noticeboard/Topic(href, href_list) - ..() - usr.set_machine(src) - if(href_list["remove"]) - if((usr.stat || usr.restrained())) //For when a player is handcuffed while they have the notice window open - return - var/obj/item/I = locate(href_list["remove"]) in contents - if(istype(I) && I.loc == src) - I.forceMove(usr.loc) - usr.put_in_hands(I) - notices-- - icon_state = "nboard0[notices]" + var/obj/item/item = locate(params["ref"]) in contents + if(!istype(item) || item.loc != src) + return - if(href_list["write"]) - if((usr.stat || usr.restrained())) //For when a player is handcuffed while they have the notice window open - return - var/obj/item/P = locate(href_list["write"]) in contents - if(istype(P) && P.loc == src) - var/obj/item/I = usr.is_holding_item_of_type(/obj/item/pen) - if(I) - add_fingerprint(usr) - P.attackby(I, usr) + var/mob/user = usr + + switch(action) + if("examine") + if(istype(item, /obj/item/paper)) + item.ui_interact(user) else - to_chat(usr, "You'll need something to write with!") + user.examinate(item) + return TRUE + if("remove") + if(!allowed(user)) + return + remove_item(item, user) + return TRUE - if(href_list["read"]) - var/obj/item/I = locate(href_list["read"]) in contents - if(istype(I) && I.loc == src) - usr.examinate(I) +/** + * Removes an item from the notice board + * + * Arguments: + * * item - The item that is to be removed + * * user - The mob that is trying to get the item removed, if there is one + */ +/obj/structure/noticeboard/proc/remove_item(obj/item/item, mob/user) + item.forceMove(drop_location()) + if(user) + user.put_in_hands(item) + balloon_alert(user, "removed from board") + notices-- + icon_state = "nboard0[notices]" /obj/structure/noticeboard/deconstruct(disassembled = TRUE) if(!(flags_1 & NODECONSTRUCT_1)) new /obj/item/stack/sheet/metal (loc, 1) + for(var/obj/item/content in contents) + remove_item(content) qdel(src) // Notice boards for the heads of staff (plus the qm) @@ -131,3 +167,5 @@ name = "Staff Notice Board" desc = "Important notices from the heads of staff." req_access = list(ACCESS_HEADS) + +#undef MAX_NOTICES diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm index 5a206186c0..fea31177d7 100644 --- a/code/game/objects/structures/watercloset.dm +++ b/code/game/objects/structures/watercloset.dm @@ -257,7 +257,7 @@ /obj/machinery/shower/Initialize() . = ..() - soundloop = new(list(src), FALSE) + soundloop = new(src, FALSE) /obj/machinery/shower/Destroy() QDEL_NULL(soundloop) diff --git a/code/game/turfs/open/floor/catwalk_plating.dm b/code/game/turfs/open/floor/catwalk_plating.dm index c6bfdc0448..cbcbade5d4 100644 --- a/code/game/turfs/open/floor/catwalk_plating.dm +++ b/code/game/turfs/open/floor/catwalk_plating.dm @@ -34,11 +34,11 @@ /turf/open/floor/plating/catwalk_floor/screwdriver_act(mob/living/user, obj/item/tool) . = ..() covered = !covered - to_chat(user, span_notice("[!covered ? "You removed the cover!" : "You added the cover!"]")) + user.balloon_alert(user, "[!covered ? "cover removed" : "cover added"]") update_icon(UPDATE_OVERLAYS) /turf/open/floor/plating/catwalk_floor/pry_tile(obj/item/crowbar, mob/user, silent) if(covered) - to_chat(user, span_notice("You need to remove the cover first!")) + user.balloon_alert(user, "remove cover first!") return FALSE . = ..() diff --git a/code/game/world.dm b/code/game/world.dm index e8780442e0..286f8622c2 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -111,6 +111,7 @@ GLOBAL_LIST(topic_status_cache) GLOB.picture_log_directory = "data/picture_logs/[override_dir]" GLOB.world_game_log = "[GLOB.log_directory]/game.log" + GLOB.world_suspicious_login_log = "[GLOB.log_directory]/suspicious_logins.log" GLOB.world_virus_log = "[GLOB.log_directory]/virus.log" GLOB.world_asset_log = "[GLOB.log_directory]/asset.log" GLOB.world_attack_log = "[GLOB.log_directory]/attack.log" diff --git a/code/modules/admin/verbs/possess.dm b/code/modules/admin/verbs/possess.dm index ee4d12656a..d10ec67d13 100644 --- a/code/modules/admin/verbs/possess.dm +++ b/code/modules/admin/verbs/possess.dm @@ -3,7 +3,7 @@ set category = "Object" if((O.obj_flags & DANGEROUS_POSSESSION) && CONFIG_GET(flag/forbid_singulo_possession)) - to_chat(usr, "[O] is too powerful for you to possess.") + to_chat(usr, "[O] is too powerful for you to possess.", confidential = TRUE) return var/turf/T = get_turf(O) @@ -18,19 +18,22 @@ if(!usr.control_object) //If you're not already possessing something... usr.name_archive = usr.real_name - usr.loc = O + usr.forceMove(O) usr.real_name = O.name usr.name = O.name usr.reset_perspective(O) usr.control_object = O + O.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) SSblackbox.record_feedback("tally", "admin_verb", 1, "Possess Object") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /proc/release() set name = "Release Obj" set category = "Object" - //usr.loc = get_turf(usr) - if(usr.control_object && usr.name_archive) //if you have a name archived and if you are actually relassing an object + if(!usr.control_object) //lest we are banished to the nullspace realm. + return + + if(usr.name_archive) //if you have a name archived usr.real_name = usr.name_archive usr.name_archive = "" usr.name = usr.real_name @@ -38,8 +41,8 @@ var/mob/living/carbon/human/H = usr H.name = H.get_visible_name() - - usr.loc = get_turf(usr.control_object) + usr.control_object.RemoveElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + usr.forceMove(get_turf(usr.control_object)) usr.reset_perspective() usr.control_object = null SSblackbox.record_feedback("tally", "admin_verb", 1, "Release Object") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/admin/view_variables/filterrific.dm b/code/modules/admin/view_variables/filterrific.dm index e651028cbe..6cef9b178a 100644 --- a/code/modules/admin/view_variables/filterrific.dm +++ b/code/modules/admin/view_variables/filterrific.dm @@ -79,7 +79,7 @@ . = TRUE if("mass_apply") if(!check_rights_for(usr.client, R_FUN)) - to_chat(usr, "[message]") - else - H.dna.add_mutation(CLOWNMUT) // We're removing their antag status, add back clumsy + if(!ishuman(mob_override) || owner.assigned_role != "Clown") + return + var/mob/living/carbon/human/human_override = mob_override + if(removing) // They're a clown becoming an antag, remove clumsy + human_override.dna.remove_mutation(CLOWNMUT) + if(!silent && message) + to_chat(human_override, span_boldnotice("[message]")) + else + human_override.dna.add_mutation(CLOWNMUT) // We're removing their antag status, add back clumsy + //Assign default team and creates one for one of a kind team antagonists /datum/antagonist/proc/create_team(datum/team/team) return - ///Called by the add_antag_datum() mind proc after the instanced datum is added to the mind's antag_datums list. +///Called by the add_antag_datum() mind proc after the instanced datum is added to the mind's antag_datums list. /datum/antagonist/proc/on_gain() SHOULD_CALL_PARENT(TRUE) - set waitfor = FALSE - if(!(owner?.current)) - return + if(!owner) + CRASH("[src] ran on_gain() without a mind") + if(!owner.current) + CRASH("[src] ran on_gain() on a mind without a mob") + if(ui_name)//in the future, this should entirely replace greet. + info_button = new(owner.current, src) + info_button.Grant(owner.current) if(!silent) greet() + if(ui_name) + to_chat(owner.current, span_big("You are \a [src].")) + to_chat(owner.current, span_boldnotice("For more info, read the panel. you can always come back to it using the button in the top left.")) + info_button.Trigger() apply_innate_effects() give_antag_moodies() remove_blacklisted_quirks() + // RegisterSignal(owner, COMSIG_PRE_MINDSHIELD_IMPLANT, .proc/pre_mindshield) + // RegisterSignal(owner, COMSIG_MINDSHIELD_IMPLANTED, .proc/on_mindshield) if(is_banned(owner.current) && replace_banned) replace_banned_player() + // else if(owner.current.client?.holder && (CONFIG_GET(flag/auto_deadmin_antagonists) || owner.current.client.prefs?.toggles & DEADMIN_ANTAGONIST)) + // owner.current.client.holder.auto_deadmin() + if(!soft_antag && owner.current.stat != DEAD) + owner.current.add_to_current_living_antags() + + // cit skill if(skill_modifiers) for(var/A in skill_modifiers) ADD_SINGLETON_SKILL_MODIFIER(owner, A, type) @@ -120,61 +189,95 @@ GLOBAL_LIST_EMPTY(antagonists) if(istype(M)) M.name = "[name] Training" owner.current.AddComponent(/datum/component/activity) - if(owner.current.stat != DEAD) - owner.current.add_to_current_living_antags() SEND_SIGNAL(owner.current, COMSIG_MOB_ANTAG_ON_GAIN, src) + +/** + * Proc that checks the sent mob aganst the banlistfor this antagonist. + * Returns FALSE if no mob is sent, or the mob is not found to be banned. + * + * * mob/M: The mob that you are looking for on the banlist. + */ /datum/antagonist/proc/is_banned(mob/M) if(!M) return FALSE . = (jobban_isbanned(M, ROLE_SYNDICATE) || QDELETED(M) || (job_rank && (jobban_isbanned(M,job_rank) || QDELETED(M)))) +/** + * Proc that replaces a player who cannot play a specific antagonist due to being banned via a poll, and alerts the player of their being on the banlist. + */ /datum/antagonist/proc/replace_banned_player() set waitfor = FALSE - var/list/mob/candidates = pollCandidatesForMob("Do you want to play as a [name]?", "[name]", null, job_rank, 50, owner.current) + var/list/mob/dead/observer/candidates = pollCandidatesForMob("Do you want to play as a [name]?", "[name]", job_rank, 50, owner.current) if(LAZYLEN(candidates)) - var/mob/C = pick(candidates) + var/mob/dead/observer/C = pick(candidates) to_chat(owner, "Your mob has been taken over by a ghost! Appeal your job ban if you want to avoid this in the future!") - message_admins("[key_name_admin(C)] has taken control of ([key_name_admin(owner.current)]) to replace a jobbaned player.") + message_admins("[key_name_admin(C)] has taken control of ([key_name_admin(owner)]) to replace a jobbanned player.") owner.current.ghostize(0) C.transfer_ckey(owner.current, FALSE) -///Called by the remove_antag_datum() and remove_all_antag_datums() mind procs for the antag datum to handle its own removal and deletion. +/** + * Called by the remove_antag_datum() and remove_all_antag_datums() mind procs for the antag datum to handle its own removal and deletion. + */ /datum/antagonist/proc/on_removal() SHOULD_CALL_PARENT(TRUE) + if(!owner) + CRASH("Antag datum with no owner.") + remove_innate_effects() clear_antag_moodies() - if(owner) - LAZYREMOVE(owner.antag_datums, src) - for(var/A in skill_modifiers) - owner.remove_skill_modifier(GET_SKILL_MOD_ID(A, type)) - if(!LAZYLEN(owner.antag_datums)) - owner.current.remove_from_current_living_antags() - if(!silent && owner.current) - farewell() + LAZYREMOVE(owner.antag_datums, src) + // cit skill + for(var/A in skill_modifiers) + owner.remove_skill_modifier(GET_SKILL_MOD_ID(A, type)) + // end + if(!LAZYLEN(owner.antag_datums) && !soft_antag) + owner.current.remove_from_current_living_antags() + if(info_button) + QDEL_NULL(info_button) + if(!silent && owner.current) + farewell() + // UnregisterSignal(owner, COMSIG_PRE_MINDSHIELD_IMPLANT) + // UnregisterSignal(owner, COMSIG_MINDSHIELD_IMPLANTED) var/datum/team/team = get_team() if(team) team.remove_member(owner) - // we don't remove the activity component on purpose--no real point to it qdel(src) +/** + * Proc that sends fluff or instructional messages to the player when they are given this antag datum. + * Use this proc for playing sounds, sending alerts, or helping to setup non-gameplay influencing aspects of the antagonist type. + */ /datum/antagonist/proc/greet() return +/** + * Proc that sends fluff or instructional messages to the player when they lose this antag datum. + * Use this proc for playing sounds, sending alerts, or otherwise informing the player that they're no longer a specific antagonist type. + */ /datum/antagonist/proc/farewell() return +/** + * Proc that assigns this antagonist's ascribed moodlet to the player. + */ /datum/antagonist/proc/give_antag_moodies() if(!antag_moodlet) return SEND_SIGNAL(owner.current, COMSIG_ADD_MOOD_EVENT, "antag_moodlet", antag_moodlet) +/** + * Proc that removes this antagonist's ascribed moodlet from the player. + */ /datum/antagonist/proc/clear_antag_moodies() if(!antag_moodlet) return SEND_SIGNAL(owner.current, COMSIG_CLEAR_MOOD_EVENT, "antag_moodlet") +/** + * Removes invalid quirks. + */ /datum/antagonist/proc/remove_blacklisted_quirks() var/mob/living/L = owner.current if(istype(L)) @@ -185,16 +288,22 @@ GLOBAL_LIST_EMPTY(antagonists) to_chat(L, "[initial(Q.antag_removal_text)]") qdel(Q) -//Returns the team antagonist belongs to if any. +/** + * Proc that will return the team this antagonist belongs to, when called. Helpful with antagonists that may belong to multiple potential teams in a single round, like families. + */ /datum/antagonist/proc/get_team() return -//Individual roundend report +/** + * Proc that sends string information for the end-round report window to the server. + * This runs on every instance of every antagonist that exists at the end of the round. + * This is the body of the message, sandwiched between roundend_report_header and roundend_report_footer. + */ /datum/antagonist/proc/roundend_report() var/list/report = list() if(!owner) - CRASH("antagonist datum without owner") + CRASH("Antagonist datum without owner") report += printplayer(owner) @@ -213,11 +322,19 @@ GLOBAL_LIST_EMPTY(antagonists) return report.Join("
") -//Displayed at the start of roundend_category section, default to roundend_category header +/** + * Proc that sends string data for the round-end report. + * Displayed before roundend_report and roundend_report_footer. + * Appears at start of roundend_catagory section. + */ /datum/antagonist/proc/roundend_report_header() - return "The [roundend_category] were:
" + return "The [roundend_category] were:
" -//Displayed at the end of roundend_category section +/** + * Proc that sends string data for the round-end report. + * Displayed after roundend_report and roundend_report_footer. + * Appears at the end of the roundend_catagory section. + */ /datum/antagonist/proc/roundend_report_footer() return @@ -226,22 +343,24 @@ GLOBAL_LIST_EMPTY(antagonists) //Called when using admin tools to give antag status /datum/antagonist/proc/admin_add(datum/mind/new_owner,mob/admin) - message_admins("[key_name_admin(admin)] made [new_owner.current] into [name].") - log_admin("[key_name(admin)] made [new_owner.current] into [name].") + message_admins("[key_name_admin(admin)] made [key_name_admin(new_owner)] into [name].") + log_admin("[key_name(admin)] made [key_name(new_owner)] into [name].") new_owner.add_antag_datum(src) //Called when removing antagonist using admin tools /datum/antagonist/proc/admin_remove(mob/user) if(!user) return - message_admins("[key_name_admin(user)] has removed [name] antagonist status from [owner.current].") - log_admin("[key_name(user)] has removed [name] antagonist status from [owner.current].") + message_admins("[key_name_admin(user)] has removed [name] antagonist status from [key_name_admin(owner)].") + log_admin("[key_name(user)] has removed [name] antagonist status from [key_name(owner)].") on_removal() //gamemode/proc/is_mode_antag(antagonist/A) => TRUE/FALSE -//Additional data to display in antagonist panel section -//nuke disk code, genome count, etc +/** + * Additional data to display in the antagonist panel section. + * For example, nuke disk code, genome count, etc + */ /datum/antagonist/proc/antag_panel_data() return "" @@ -253,7 +372,7 @@ GLOBAL_LIST_EMPTY(antagonists) return FALSE return TRUE -// List if ["Command"] = CALLBACK(), user will be appeneded to callback arguments on execution +/// List of ["Command"] = CALLBACK(), user will be appeneded to callback arguments on execution /datum/antagonist/proc/get_admin_commands() . = list() @@ -283,14 +402,19 @@ GLOBAL_LIST_EMPTY(antagonists) return antag_memory = new_memo -/// Gets how fast we can hijack the shuttle, return 0 for can not hijack. Defaults to hijack_speed var, override for custom stuff like buffing hijack speed for hijack objectives or something. +/** + * Gets how fast we can hijack the shuttle, return 0 for can not hijack. + * Defaults to hijack_speed var, override for custom stuff like buffing hijack speed for hijack objectives or something. + */ /datum/antagonist/proc/hijack_speed() var/datum/objective/hijack/H = locate() in objectives if(!isnull(H?.hijack_speed_override)) return H.hijack_speed_override return hijack_speed -/// Gets our threat level. Defaults to threat var, override for custom stuff like different traitor goals having different threats. +/** + * Gets our threat level. Override this proc for custom functionality/dynamic threat level. + */ /datum/antagonist/proc/threat() . = CONFIG_GET(keyed_list/antag_threat)[lowertext(name)] if(. == null) @@ -300,6 +424,13 @@ GLOBAL_LIST_EMPTY(antagonists) /datum/antagonist/custom antagpanel_category = "Custom" show_name_in_check_antagonists = TRUE //They're all different + var/datum/team/custom_team + +/datum/antagonist/custom/create_team(datum/team/team) + custom_team = team + +/datum/antagonist/custom/get_team() + return custom_team /datum/antagonist/custom/admin_add(datum/mind/new_owner,mob/admin) var/custom_name = stripped_input(admin, "Custom antagonist name:", "Custom antag", "Antagonist") @@ -308,3 +439,48 @@ GLOBAL_LIST_EMPTY(antagonists) else return ..() + +///ANTAGONIST UI STUFF + +/datum/antagonist/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, ui_name, name) + ui.open() + +/datum/antagonist/ui_state(mob/user) + return GLOB.always_state + +///generic helper to send objectives as data through tgui. supports smart objectives too! +/datum/antagonist/proc/get_objectives() + var/objective_count = 1 + var/list/objective_data = list() + //all obj + for(var/datum/objective/objective in objectives) + objective_data += list(list( + "count" = objective_count, + "name" = objective.name, + "explanation" = objective.explanation_text, + "complete" = objective.completed, + )) + objective_count++ + return objective_data + +//button for antags to review their descriptions/info + +/datum/action/antag_info + name = "Open Antag Information:" + button_icon_state = "round_end" + var/datum/antagonist/antag_datum + +/datum/action/antag_info/New(Target, datum/antagonist/antag_datum) + . = ..() + src.antag_datum = antag_datum + name += " [antag_datum.name]" + +/datum/action/antag_info/Trigger() + if(antag_datum) + antag_datum.ui_interact(owner) + +/datum/action/antag_info/IsAvailable() + return TRUE diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm index 491ee9d2e5..bd7a9a43cb 100644 --- a/code/modules/antagonists/brainwashing/brainwashing.dm +++ b/code/modules/antagonists/brainwashing/brainwashing.dm @@ -17,11 +17,13 @@ B.objectives += objective M.add_antag_datum(B) - var/begin_message = "[L] has been brainwashed with the following objectives: " + var/begin_message = " has been brainwashed with the following objectives: " var/obj_message = english_list(directives) - var/end_message = "." + var/end_message = "." var/rendered = begin_message + obj_message + end_message - deadchat_broadcast(rendered, follow_target = L, turf_target = get_turf(L), message_type=DEADCHAT_REGULAR) + deadchat_broadcast(rendered, "[L]", follow_target = L, turf_target = get_turf(L), message_type=DEADCHAT_ANNOUNCEMENT) + if(prob(1) || SSevents.holidays && SSevents.holidays[APRIL_FOOLS]) + L.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in.") /datum/antagonist/brainwashed name = "Brainwashed Victim" @@ -30,19 +32,21 @@ show_in_antagpanel = TRUE antagpanel_category = "Other" show_name_in_check_antagonists = TRUE + ui_name = "AntagInfoBrainwashed" + suicide_cry = "FOR... SOMEONE!!" -/datum/antagonist/brainwashed/greet() - to_chat(owner, "Your mind reels as it begins focusing on a single purpose...") - to_chat(owner, "Follow the Directives, at any cost!") - var/i = 1 - for(var/X in objectives) - var/datum/objective/O = X - to_chat(owner, "[i]. [O.explanation_text]") - i++ +/datum/antagonist/brainwashed/ui_static_data(mob/user) + . = ..() + var/list/data = list() + data["objectives"] = get_objectives() + return data /datum/antagonist/brainwashed/farewell() - to_chat(owner, "Your mind suddenly clears...") - to_chat(owner, "You feel the weight of the Directives disappear! You no longer have to obey them.") + to_chat(owner, span_warning("Your mind suddenly clears...")) + to_chat(owner, "[span_warning("You feel the weight of the Directives disappear! You no longer have to obey them.")]") + if(owner.current) + var/mob/living/owner_mob = owner.current + owner_mob.log_message("is no longer brainwashed with the objectives: [english_list(objectives)].", LOG_ATTACK) owner.announce_objectives() /datum/antagonist/brainwashed/admin_add(datum/mind/new_owner,mob/admin) @@ -54,9 +58,9 @@ var/objective = stripped_input(admin, "Add an objective, or leave empty to finish.", "Brainwashing", null, MAX_MESSAGE_LEN) if(objective) objectives += objective - while(alert(admin,"Add another objective?","More Brainwashing","Yes","No") == "Yes") + while(tgui_alert(admin,"Add another objective?","More Brainwashing",list("Yes","No")) == "Yes") - if(alert(admin,"Confirm Brainwashing?","Are you sure?","Yes","No") == "No") + if(tgui_alert(admin,"Confirm Brainwashing?","Are you sure?",list("Yes","No")) == "No") return if(!LAZYLEN(objectives)) @@ -69,6 +73,7 @@ brainwash(C, objectives) var/obj_list = english_list(objectives) message_admins("[key_name_admin(admin)] has brainwashed [key_name_admin(C)] with the following objectives: [obj_list].") + C.log_message("has been force-brainwashed with the objective '[obj_list]' by admin [key_name(admin)]", LOG_ATTACK, log_globally = FALSE) log_admin("[key_name(admin)] has brainwashed [key_name(C)] with the following objectives: [obj_list].") /datum/objective/brainwashing diff --git a/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm b/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm index 8060b7b0cd..d5c9b2cd2f 100644 --- a/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm +++ b/code/modules/antagonists/clockcult/clock_items/clockwork_slab.dm @@ -264,7 +264,9 @@ ui.open() /obj/item/clockwork/slab/ui_data(mob/user) //we display a lot of data via TGUI - . = list() + . = ..() + if(!.) + return .["recollection"] = recollecting .["power"] = DisplayPower(get_clockwork_power()) .["power_unformatted"] = get_clockwork_power() @@ -275,6 +277,7 @@ if(S.tier == SCRIPTURE_PERIPHERAL) // This tier is skiped because this contains basetype stuff continue + // FUTURE IMPL: cache these perhaps? var/list/data = list() data["name"] = S.name data["descname"] = S.descname diff --git a/code/modules/antagonists/clockcult/clockcult.dm b/code/modules/antagonists/clockcult/clockcult.dm index 0db3f9e6df..4e993763e7 100644 --- a/code/modules/antagonists/clockcult/clockcult.dm +++ b/code/modules/antagonists/clockcult/clockcult.dm @@ -6,6 +6,7 @@ job_rank = ROLE_SERVANT_OF_RATVAR antag_moodlet = /datum/mood_event/cult skill_modifiers = list(/datum/skill_modifier/job/level/wiring, /datum/skill_modifier/job/level/dwarfy/blacksmithing) + ui_name = "AntagInfoClockwork" var/datum/action/innate/hierophant/hierophant_network = new threat = 3 var/datum/team/clockcult/clock_team @@ -14,6 +15,12 @@ var/ignore_eligibility_check = FALSE var/ignore_holy_water = FALSE +/datum/antagonist/clockcult/ui_data(mob/user) + . = ..() + if(!.) + return + .["HONOR_RATVAR"] = GLOB.ratvar_awakens + /datum/antagonist/clockcult/silent name = "Silent Clock Cultist" silent = TRUE @@ -22,6 +29,8 @@ /datum/antagonist/clockcult/neutered name = "Neutered Clock Cultist" neutered = TRUE + soft_antag = TRUE + ui_name = null // no. /datum/antagonist/clockcult/neutered/traitor name = "Traitor Clock Cultist" @@ -57,14 +66,6 @@ if(. && !ignore_eligibility_check) . = is_eligible_servant(new_owner.current) -/datum/antagonist/clockcult/greet() - if(!owner.current || silent) - return - owner.current.visible_message("[owner.current]'s eyes glow a blazing yellow!", null, null, 7, owner.current) //don't show the owner this message - to_chat(owner.current, "Assist your new companions in their righteous efforts. Your goal is theirs, and theirs yours. You serve the Clockwork \ - Justiciar above all else. Perform his every whim without hesitation.") - owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/clockcultalr.ogg', 70, FALSE, pressure_affected = FALSE) - /datum/antagonist/clockcult/on_gain() var/mob/living/current = owner.current SSticker.mode.servants_of_ratvar += owner diff --git a/code/modules/antagonists/morph/morph_antag.dm b/code/modules/antagonists/morph/morph_antag.dm index 07781ce60a..928236c053 100644 --- a/code/modules/antagonists/morph/morph_antag.dm +++ b/code/modules/antagonists/morph/morph_antag.dm @@ -3,5 +3,6 @@ show_name_in_check_antagonists = TRUE show_in_antagpanel = FALSE threat = 2 + ui_name = "AntagInfoMorph" //It does nothing! (Besides tracking) diff --git a/code/modules/antagonists/nightmare/nightmare.dm b/code/modules/antagonists/nightmare/nightmare.dm index f5b10de5c2..185a2be9a7 100644 --- a/code/modules/antagonists/nightmare/nightmare.dm +++ b/code/modules/antagonists/nightmare/nightmare.dm @@ -4,3 +4,5 @@ show_name_in_check_antagonists = TRUE threat = 5 show_to_ghosts = TRUE + ui_name = "AntagInfoNightmare" + suicide_cry = "FOR THE DARKNESS!!" diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm index 652b19a8e7..9875eb1581 100644 --- a/code/modules/antagonists/nukeop/nukeop.dm +++ b/code/modules/antagonists/nukeop/nukeop.dm @@ -236,6 +236,8 @@ 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() ..() diff --git a/code/modules/antagonists/overthrow/overthrow.dm b/code/modules/antagonists/overthrow/overthrow.dm index 8fa5517b4f..f086f5db97 100644 --- a/code/modules/antagonists/overthrow/overthrow.dm +++ b/code/modules/antagonists/overthrow/overthrow.dm @@ -121,7 +121,7 @@ return var/mob/living/carbon/human/H = owner.current // Give uplink - var/obj/item/uplink_holder = owner.equip_traitor(uplink_owner = src) + var/obj/item/uplink_holder = owner.equip_traitor() var/datum/component/uplink/uplink = uplink_holder.GetComponent(/datum/component/uplink) uplink.telecrystals = INITIAL_CRYSTALS // Give AI hacking board diff --git a/code/modules/antagonists/traitor/IAA/internal_affairs.dm b/code/modules/antagonists/traitor/IAA/internal_affairs.dm index 4414fe8257..796365a459 100644 --- a/code/modules/antagonists/traitor/IAA/internal_affairs.dm +++ b/code/modules/antagonists/traitor/IAA/internal_affairs.dm @@ -7,37 +7,51 @@ /datum/antagonist/traitor/internal_affairs name = "Internal Affairs Agent" employer = "Nanotrasen" - special_role = "internal affairs agent" + suicide_cry = "FOR THE COMPANY!!" antagpanel_category = "IAA" + var/special_role = "internal affairs agent" var/syndicate = FALSE var/last_man_standing = FALSE var/list/datum/mind/targets_stolen +/datum/antagonist/traitor/internal_affairs/New() + . = ..() + LAZYADD(targets_stolen, src) /datum/antagonist/traitor/internal_affairs/proc/give_pinpointer() - if(owner && owner.current) + if(!owner) + CRASH("Antag datum with no owner.") + + if(owner.current) owner.current.apply_status_effect(/datum/status_effect/agent_pinpointer) /datum/antagonist/traitor/internal_affairs/apply_innate_effects() - .=..() //in case the base is used in future - if(owner && owner.current) + . = ..() + + if(!owner) + CRASH("Antag datum with no owner.") + + if(owner.current) give_pinpointer(owner.current) /datum/antagonist/traitor/internal_affairs/remove_innate_effects() - .=..() - if(owner && owner.current) + . = ..() + + if(!owner) + CRASH("Antag datum with no owner.") + + if(owner.current) owner.current.remove_status_effect(/datum/status_effect/agent_pinpointer) /datum/antagonist/traitor/internal_affairs/on_gain() START_PROCESSING(SSprocessing, src) - .=..() + . = ..() /datum/antagonist/traitor/internal_affairs/on_removal() STOP_PROCESSING(SSprocessing,src) - .=..() + . = ..() /datum/antagonist/traitor/internal_affairs/process() iaa_process() - /datum/status_effect/agent_pinpointer id = "agent_pinpointer" duration = -1 @@ -46,6 +60,8 @@ var/minimum_range = PINPOINTER_MINIMUM_RANGE var/range_fuzz_factor = PINPOINTER_EXTRA_RANDOM_RANGE var/mob/scan_target = null + var/range_mid = 8 + var/range_far = 16 /atom/movable/screen/alert/status_effect/agent_pinpointer name = "Internal Affairs Integrated Pinpointer" @@ -66,13 +82,13 @@ linked_alert.icon_state = "pinondirect" else linked_alert.setDir(get_dir(here, there)) - switch(get_dist(here, there)) - if(1 to 8) - linked_alert.icon_state = "pinonclose" - if(9 to 16) - linked_alert.icon_state = "pinonmedium" - if(16 to INFINITY) - linked_alert.icon_state = "pinonfar" + var/dist = (get_dist(here, there)) + if(dist >= 1 && dist <= range_mid) + linked_alert.icon_state = "pinonclose" + else if(dist > range_mid && dist <= range_far) + linked_alert.icon_state = "pinonmedium" + else if(dist > range_far) + linked_alert.icon_state = "pinonfar" /datum/status_effect/agent_pinpointer/proc/scan_for_target() scan_target = null @@ -99,37 +115,42 @@ return (istype(O, /datum/objective/assassinate/internal)||istype(O, /datum/objective/destroy/internal)) /datum/antagonist/traitor/proc/replace_escape_objective() - if(!owner || !objectives.len) + if(!owner) + CRASH("Antag datum with no owner.") + if(!objectives.len) return - for (var/objective_ in objectives) - if(!(istype(objective_, /datum/objective/escape)||istype(objective_, /datum/objective/survive))) + for (var/objective in objectives) + if(!(istype(objective, /datum/objective/escape) || istype(objective, /datum/objective/survive))) continue - remove_objective(objective_) + objectives -= objective var/datum/objective/martyr/martyr_objective = new martyr_objective.owner = owner add_objective(martyr_objective) /datum/antagonist/traitor/proc/reinstate_escape_objective() - if(!owner||!objectives.len) + if(!owner) + CRASH("Antag datum with no owner.") + if(!objectives.len) return - for (var/objective_ in objectives) - if(!istype(objective_, /datum/objective/martyr)) + for (var/objective in objectives) + if(!istype(objective, /datum/objective/martyr)) continue - remove_objective(objective_) + remove_objective(objective) /datum/antagonist/traitor/internal_affairs/reinstate_escape_objective() ..() - var/objtype = !istype(traitor_kind,TRAITOR_AI) ? /datum/objective/escape : /datum/objective/survive - var/datum/objective/escape_objective = new objtype + for (var/datum/objective/martyr/martyr_objective in objectives) + objectives -= martyr_objective + + var/datum/objective/escape_objective = new /datum/objective/escape() escape_objective.owner = owner - add_objective(escape_objective) + objectives += escape_objective /datum/antagonist/traitor/internal_affairs/proc/steal_targets(datum/mind/victim) if(!owner.current||owner.current.stat==DEAD) return - to_chat(owner.current, " Target eliminated: [victim.name]") - LAZYINITLIST(targets_stolen) + to_chat(owner.current, span_userdanger("Target eliminated: [victim.name]")) for(var/objective_ in victim.get_all_objectives()) if(istype(objective_, /datum/objective/assassinate/internal)) var/datum/objective/assassinate/internal/objective = objective_ @@ -143,7 +164,7 @@ add_objective(new_objective) targets_stolen += objective.target var/status_text = objective.check_completion() ? "neutralised" : "active" - to_chat(owner.current, " New target added to database: [objective.target.name] ([status_text]) ") + to_chat(owner.current, span_userdanger("New target added to database: [objective.target.name] ([status_text])")) else if(istype(objective_, /datum/objective/destroy/internal)) var/datum/objective/destroy/internal/objective = objective_ var/datum/objective/destroy/internal/new_objective = new @@ -156,7 +177,7 @@ add_objective(new_objective) targets_stolen += objective.target var/status_text = objective.check_completion() ? "neutralised" : "active" - to_chat(owner.current, " New target added to database: [objective.target.name] ([status_text]) ") + to_chat(owner.current, span_userdanger("New target added to database: [objective.target.name] ([status_text])")) last_man_standing = TRUE for(var/objective_ in objectives) if(!is_internal_objective(objective_)) @@ -167,13 +188,15 @@ return if(last_man_standing) if(syndicate) - to_chat(owner.current,"All the suspected agents are dead, and no more is required of you. Die a glorious death, agent.") - replace_escape_objective(owner) + to_chat(owner.current,span_userdanger("All the suspected agents are dead, and no more is required of you. Die a glorious death, agent.")) else - to_chat(owner.current,"All the other agents are dead. You have done us all a great service and shall be honorably exiled upon returning to base.") + to_chat(owner.current,span_userdanger("All the other agents are dead. You have done us all a great service and shall be honorably exiled upon returning to base.")) + replace_escape_objective(owner) /datum/antagonist/traitor/internal_affairs/proc/iaa_process() - if(owner&&owner.current&&owner.current.stat!=DEAD) + if(!owner) + CRASH("Antag datum with no owner.") + if(owner.current && owner.current.stat != DEAD) for(var/objective_ in objectives) if(!is_internal_objective(objective_)) continue @@ -188,12 +211,12 @@ objective.stolen = TRUE else if(objective.stolen) - var/fail_msg = "Your sensors tell you that [objective.target.current.real_name], one of the targets you were meant to have killed, pulled one over on you, and is still alive - do the job properly this time! " + var/fail_msg = span_userdanger("Your sensors tell you that [objective.target.current.real_name], one of the targets you were meant to have killed, pulled one over on you, and is still alive - do the job properly this time! ") if(last_man_standing) if(syndicate) - fail_msg += " You no longer have permission to die. " + fail_msg += span_userdanger(" You no longer have permission to die. ") else - fail_msg += " The truth could still slip out! Cease any terrorist actions as soon as possible, unneeded property damage or loss of employee life will lead to great shame." + fail_msg += span_userdanger(" The truth could still slip out! Cease any terrorist actions as soon as possible, unneeded property damage or loss of employee life will lead to your contract being terminated.") reinstate_escape_objective(owner) last_man_standing = FALSE to_chat(owner.current, fail_msg) @@ -220,29 +243,24 @@ /datum/antagonist/traitor/internal_affairs/forge_traitor_objectives() forge_iaa_objectives() - var/objtype = !istype(traitor_kind,TRAITOR_AI) ? /datum/objective/escape : /datum/objective/survive - var/datum/objective/escape_objective = new objtype + var/datum/objective/escape_objective = new /datum/objective/escape() escape_objective.owner = owner add_objective(escape_objective) /datum/antagonist/traitor/internal_affairs/proc/greet_iaa() - var/crime = pick("distribution of contraband" , "embezzlement", "piloting under the influence", "dereliction of duty", "syndicate collaboration", "mutiny", "multiple homicides", "corporate espionage", "receiving bribes", "malpractice", "worship of prohibited life forms", "possession of profane texts", "murder", "arson", "insulting their manager", "grand theft", "conspiracy", "attempting to unionize", "vandalism", "gross incompetence") + var/crime = pick("distribution of contraband", "embezzlement", "piloting under the influence", "dereliction of duty", "syndicate collaboration", "mutiny", "multiple homicides", "corporate espionage", "receiving bribes", "malpractice", "worship of prohibited life forms", "possession of profane texts", "murder", "arson", "insulting their manager", "grand theft", "conspiracy", "attempting to unionize", "vandalism", "gross incompetence") - to_chat(owner.current, "You are the [special_role].") + to_chat(owner.current, span_userdanger("You are the [special_role].")) if(syndicate) - to_chat(owner.current, "GREAT LEADER IS DEAD. NANOTRASEN MUST FALL.") - to_chat(owner.current, "Your have infiltrated this vessel to cause chaos and assassinate targets known to have conspired against the Syndicate.") - to_chat(owner.current, "Any damage you cause will be a further embarrassment to Nanotrasen, so you have no limits on collateral damage.") - to_chat(owner.current, "You have been provided with a standard uplink to accomplish your task. ") - to_chat(owner.current, "By no means reveal that you are a Syndicate agent. By no means reveal that your targets are being hunted.") + to_chat(owner.current, span_userdanger("Your target has been framed for [crime], and you have been tasked with eliminating them to prevent them defending themselves in court.")) + to_chat(owner.current, "Any damage you cause will be a further embarrassment to Nanotrasen, so you have no limits on collateral damage.") + to_chat(owner.current, span_userdanger("You have been provided with a standard uplink to accomplish your task.")) else - to_chat(owner.current, "CAUTION: Your legal status as a citizen of NanoTrasen will be permanently revoked upon completion of your first contract.") - to_chat(owner.current, "Your target has been suspected of [crime], and must be removed from this plane.") - to_chat(owner.current, "While you have a license to kill, you are to eliminate your targets with no collateral or unrelated deaths.") - to_chat(owner.current, "For the sake of plausable deniability, you have been equipped with captured Syndicate equipment via uplink.") - to_chat(owner.current, "By no means reveal that you, or any other NT employees, are undercover agents.") + to_chat(owner.current, span_userdanger("Your target is suspected of [crime], and you have been tasked with eliminating them by any means necessary to avoid a costly and embarrassing public trial.")) + to_chat(owner.current, "While you have a license to kill, unneeded property damage or loss of employee life will lead to your contract being terminated.") + to_chat(owner.current, span_userdanger("For the sake of plausible deniability, you have been equipped with an array of captured Syndicate weaponry available via uplink.")) - to_chat(owner.current, "Finally, watch your back. Your target has friends in high places, and intel suggests someone may have taken out a contract of their own to protect them.") + to_chat(owner.current, span_userdanger("Finally, watch your back. Your target has friends in high places, and intel suggests someone may have taken out a contract of their own to protect them.")) owner.announce_objectives() /datum/antagonist/traitor/internal_affairs/greet() diff --git a/code/modules/antagonists/traitor/classes/martyr.dm b/code/modules/antagonists/traitor/classes/martyr.dm index 5c407b70fd..b0a6494de7 100644 --- a/code/modules/antagonists/traitor/classes/martyr.dm +++ b/code/modules/antagonists/traitor/classes/martyr.dm @@ -5,7 +5,7 @@ chaos = 5 threat = 5 min_players = 20 - uplink_filters = list(/datum/uplink_item/stealthy_weapons/romerol_kit,/datum/uplink_item/bundles_TC/contract_kit) + uplink_filters = list(/datum/uplink_item/stealthy_weapons/romerol_kit,/datum/uplink_item/bundles_tc/contract_kit) /datum/traitor_class/human/martyr/forge_objectives(datum/antagonist/traitor/T) var/datum/objective/martyr/O = new diff --git a/code/modules/antagonists/traitor/classes/traitor_class.dm b/code/modules/antagonists/traitor/classes/traitor_class.dm index bcc6fc18dd..bc24aecd9c 100644 --- a/code/modules/antagonists/traitor/classes/traitor_class.dm +++ b/code/modules/antagonists/traitor/classes/traitor_class.dm @@ -11,6 +11,8 @@ GLOBAL_LIST_EMPTY(traitor_classes) /// Minimum players for this to randomly roll via get_random_traitor_kind(). var/min_players = 0 var/list/uplink_filters + /// Specific tgui theme for the player's antag info panel. + var/tgui_theme = "syndicate" /datum/traitor_class/New() ..() diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index b6fc397233..78403d4e27 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -5,14 +5,22 @@ job_rank = ROLE_TRAITOR antag_moodlet = /datum/mood_event/focused skill_modifiers = list(/datum/skill_modifier/job/level/wiring/basic) - var/special_role = ROLE_TRAITOR + hijack_speed = 0.5 //10 seconds per hijack stage by default + ui_name = "AntagInfoTraitor" + suicide_cry = "FOR THE SYNDICATE!!" var/employer = "The Syndicate" var/give_objectives = TRUE var/should_give_codewords = TRUE var/should_equip = TRUE + + ///special datum about what kind of employer the trator has var/datum/traitor_class/traitor_kind + + ///reference to the uplink this traitor was given, if they were. + var/datum/component/uplink/uplink + var/datum/contractor_hub/contractor_hub - hijack_speed = 0.5 //10 seconds per hijack stage by default + threat = 5 /datum/antagonist/traitor/New() @@ -45,7 +53,7 @@ /datum/antagonist/traitor/process() traitor_kind.on_process(src) -/proc/get_random_traitor_kind(var/list/blacklist = list()) +/proc/get_random_traitor_kind(list/blacklist = list()) var/list/weights = list() for(var/C in GLOB.traitor_classes) if(!(C in blacklist)) @@ -62,23 +70,24 @@ return choice /datum/antagonist/traitor/on_gain() + owner.special_role = job_rank if(owner.current && isAI(owner.current)) set_traitor_kind(TRAITOR_AI) else set_traitor_kind(get_random_traitor_kind()) SSticker.mode.traitors += owner - owner.special_role = special_role finalize_traitor() - ..() + uplink = owner.find_syndicate_uplink() + return ..() /datum/antagonist/traitor/on_removal() + if(!silent && owner.current) + to_chat(owner.current,span_userdanger("You are no longer the [job_rank]!")) //Remove malf powers. traitor_kind.on_removal(src) SSticker.mode.traitors -= owner - if(!silent && owner.current) - to_chat(owner.current," You are no longer the [special_role]! ") owner.special_role = null - . = ..() + return ..() /datum/antagonist/traitor/proc/handle_hearing(datum/source, list/hearing_args) var/message = hearing_args[HEARING_RAW_MESSAGE] @@ -93,6 +102,7 @@ /datum/antagonist/traitor/proc/remove_objective(datum/objective/O) objectives -= O +/// Generates a complete set of traitor objectives up to the traitor objective limit, including non-generic objectives such as martyr and hijack. /datum/antagonist/traitor/proc/forge_traitor_objectives() traitor_kind.forge_objectives(src) @@ -151,21 +161,42 @@ H.dna.add_mutation(CLOWNMUT) UnregisterSignal(M, COMSIG_MOVABLE_HEAR) + +/datum/antagonist/traitor/ui_static_data(mob/user) + var/list/data = list() + data["phrases"] = jointext(GLOB.syndicate_code_phrase, ", ") + data["responses"] = jointext(GLOB.syndicate_code_response, ", ") + data["theme"] = traitor_kind.tgui_theme //traitor_flavor["ui_theme"] + data["code"] = uplink.unlock_code + data["intro"] = "You are from [traitor_kind.employer]." //traitor_flavor["introduction"] + data["allies"] = "Most other syndicate operatives are not to be trusted (but try not to rat them out), as they might have been assigned opposing objectives." //traitor_flavor["allies"] + data["goal"] = "We do not approve of mindless killing of innocent workers; \"get in, get done, get out\" is our motto." //traitor_flavor["goal"] + data["has_uplink"] = uplink ? TRUE : FALSE + if(uplink) + data["uplink_intro"] = "You have been provided with a standard uplink to accomplish your task." //traitor_flavor["uplink"] + data["uplink_unlock_info"] = uplink.unlock_text + data["objectives"] = get_objectives() + return data + +/// Outputs this shift's codewords and responses to the antag's chat and copies them to their memory. /datum/antagonist/traitor/proc/give_codewords() if(!owner.current) return - var/mob/traitor_mob=owner.current + + var/mob/traitor_mob = owner.current var/phrases = jointext(GLOB.syndicate_code_phrase, ", ") var/responses = jointext(GLOB.syndicate_code_response, ", ") - var/dat = "The Syndicate have provided you with the following codewords to identify fellow agents:\n" - dat += "Code Phrase: [phrases]\n" - dat += "Code Response: [responses]" - to_chat(traitor_mob, dat) + to_chat(traitor_mob, "The Syndicate have provided you with the following codewords to identify fellow agents:") + to_chat(traitor_mob, "Code Phrase: [span_blue("[phrases]")]") + to_chat(traitor_mob, "Code Response: [span_red("[responses]")]") - antag_memory += "Code Phrase: [phrases]
" - antag_memory += "Code Response: [responses]
" + antag_memory += "Code Phrase: [span_blue("[phrases]")]
" + antag_memory += "Code Response: [span_red("[responses]")]
" + + to_chat(traitor_mob, "Use the codewords during regular conversation to identify other agents. Proceed with caution, however, as everyone is a potential foe.") + to_chat(traitor_mob, span_alertwarning("You memorize the codewords, allowing you to recognise them when heard.")) /datum/antagonist/traitor/proc/add_law_zero() var/mob/living/silicon/ai/killer = owner.current @@ -220,44 +251,43 @@ where = "In your [equipped_slot]" to_chat(mob, "

[where] is a folder containing secret documents that another Syndicate group wants. We have set up a meeting with one of their agents on station to make an exchange. Exercise extreme caution as they cannot be trusted and may be hostile.
") -//TODO Collate /datum/antagonist/traitor/roundend_report() var/list/result = list() - var/traitorwin = TRUE + var/traitor_won = TRUE result += printplayer(owner) - var/TC_uses = 0 - var/uplink_true = FALSE + var/used_telecrystals = 0 + var/uplink_owned = FALSE var/purchases = "" + LAZYINITLIST(GLOB.uplink_purchase_logs_by_key) - var/datum/uplink_purchase_log/H = GLOB.uplink_purchase_logs_by_key[owner.key] - if(H) - TC_uses = H.total_spent - uplink_true = TRUE - purchases += H.generate_render(FALSE) + // Uplinks add an entry to uplink_purchase_logs_by_key on init. + var/datum/uplink_purchase_log/purchase_log = GLOB.uplink_purchase_logs_by_key[owner.key] + if(purchase_log) + used_telecrystals = purchase_log.total_spent + uplink_owned = TRUE + purchases += purchase_log.generate_render(FALSE) var/objectives_text = "" - if(objectives.len)//If the traitor had no objectives, don't need to process this. + if(objectives.len) //If the traitor had no objectives, don't need to process this. var/count = 1 for(var/datum/objective/objective in objectives) - if(objective.completable) - var/completion = objective.check_completion() - if(completion >= 1) - objectives_text += "
Objective #[count]: [objective.explanation_text] Success!" - else if(completion <= 0) - objectives_text += "
Objective #[count]: [objective.explanation_text] Fail." - traitorwin = FALSE - else - objectives_text += "
Objective #[count]: [objective.explanation_text] [completion*100]%" + var/completion = objective.check_completion() + if(completion >= 1) + objectives_text += "
Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" + else if(completion <= 0) + objectives_text += "
Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" + traitor_won = FALSE else - objectives_text += "
Objective #[count]: [objective.explanation_text]" + objectives_text += "
Objective #[count]: [objective.explanation_text] [completion*100]%" + count++ - if(uplink_true) - var/uplink_text = "(used [TC_uses] TC) [purchases]" - if(TC_uses==0 && traitorwin) + if(uplink_owned) + var/uplink_text = "(used [used_telecrystals] TC) [purchases]" + if((used_telecrystals == 0) && traitor_won) var/static/icon/badass = icon('icons/badass.dmi', "badass") uplink_text += "[icon2html(badass, world)]" result += uplink_text @@ -266,55 +296,59 @@ var/special_role_text = lowertext(name) - if(contractor_hub) + if (contractor_hub) result += contractor_round_end() - if(traitorwin) - result += "The [special_role_text] was successful!" + if(traitor_won) + result += span_greentext("The [special_role_text] was successful!") else - result += "The [special_role_text] has failed!" + result += span_redtext("The [special_role_text] has failed!") SEND_SOUND(owner.current, 'sound/ambience/ambifailure.ogg') return result.Join("
") /// Proc detailing contract kit buys/completed contracts/additional info /datum/antagonist/traitor/proc/contractor_round_end() - var result = "" - var total_spent_rep = 0 + var/result = "" + var/total_spent_rep = 0 - var/completed_contracts = 0 + var/completed_contracts = contractor_hub.contracts_completed var/tc_total = contractor_hub.contract_TC_payed_out + contractor_hub.contract_TC_to_redeem - for(var/datum/syndicate_contract/contract in contractor_hub.assigned_contracts) - if(contract.status == CONTRACT_STATUS_COMPLETE) - completed_contracts++ var/contractor_item_icons = "" // Icons of purchases var/contractor_support_unit = "" // Set if they had a support unit - and shows appended to their contracts completed - for(var/datum/contractor_item/contractor_purchase in contractor_hub.purchased_items) // Get all the icons/total cost for all our items bought + /// Get all the icons/total cost for all our items bought + for (var/datum/contractor_item/contractor_purchase in contractor_hub.purchased_items) contractor_item_icons += "\[ [contractor_purchase.name] - [contractor_purchase.cost] Rep

[contractor_purchase.desc]
\]
" + total_spent_rep += contractor_purchase.cost - if(istype(contractor_purchase, /datum/contractor_item/contractor_partner)) // Special case for reinforcements, we want to show their ckey and name on round end. + + /// Special case for reinforcements, we want to show their ckey and name on round end. + if (istype(contractor_purchase, /datum/contractor_item/contractor_partner)) var/datum/contractor_item/contractor_partner/partner = contractor_purchase contractor_support_unit += "
[partner.partner_mind.key] played [partner.partner_mind.current.name], their contractor support unit." + if (contractor_hub.purchased_items.len) - result += "
(used [total_spent_rep] Rep)" + result += "
(used [total_spent_rep] Rep) " result += contractor_item_icons result += "
" - if(completed_contracts > 0) + if (completed_contracts > 0) var/pluralCheck = "contract" - if(completed_contracts > 1) + if (completed_contracts > 1) pluralCheck = "contracts" - result += "Completed [completed_contracts] [pluralCheck] for a total of \ - [tc_total] TC!
" + + result += "Completed [span_greentext("[completed_contracts]")] [pluralCheck] for a total of \ + [span_greentext("[tc_total] TC")]![contractor_support_unit]
" + return result /datum/antagonist/traitor/roundend_report_footer() var/phrases = jointext(GLOB.syndicate_code_phrase, ", ") var/responses = jointext(GLOB.syndicate_code_response, ", ") - var message = "
The code phrases were: [phrases]
\ - The code responses were: [responses]
" + var/message = "
The code phrases were: [phrases]
\ + The code responses were: [span_redtext("[responses]")]
" return message diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index 69bd52c739..88a2b82ab9 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -3,12 +3,15 @@ roundend_category = "wizards/witches" antagpanel_category = "Wizard" job_rank = ROLE_WIZARD + antag_hud_type = ANTAG_HUD_WIZ + antag_hud_name = "wizard" antag_moodlet = /datum/mood_event/focused - threat = 30 + hijack_speed = 0.5 + ui_name = "AntagInfoWizard" + suicide_cry = "FOR THE FEDERATION!!" var/give_objectives = TRUE var/strip = TRUE //strip before equipping var/allow_rename = TRUE - var/hud_version = "wizard" var/datum/team/wizard/wiz_team //Only created if wizard summons apprentices var/move_to_lair = TRUE var/outfit_type = /datum/outfit/wizard @@ -50,16 +53,17 @@ wiz_team = new(owner) wiz_team.name = "[owner.current.real_name] team" wiz_team.master_wizard = src - update_wiz_icons_added(owner.current) + add_antag_hud(antag_hud_type, antag_hud_name, owner.current) /datum/antagonist/wizard/proc/send_to_lair() - if(!owner || !owner.current) + if(!owner) + CRASH("Antag datum with no owner.") + if(!owner.current) return if(!GLOB.wizardstart.len) SSjob.SendToLateJoin(owner.current) to_chat(owner, "HOT INSERTION, GO GO GO") - else - owner.current.forceMove(pick(GLOB.wizardstart)) + owner.current.forceMove(pick(GLOB.wizardstart)) /datum/antagonist/wizard/proc/create_objectives() var/datum/objective/flavor/wizard/new_objective = new @@ -79,7 +83,7 @@ /datum/antagonist/wizard/proc/equip_wizard() if(!owner) - return + CRASH("Antag datum with no owner.") var/mob/living/carbon/human/H = owner.current if(!istype(H)) return @@ -91,18 +95,14 @@ H.age = wiz_age H.equipOutfit(outfit_type) -/datum/antagonist/wizard/greet() - to_chat(owner, "You are the Space Wizard!") - to_chat(owner, "The Space Wizards Federation has given you the following tasks:") - owner.announce_objectives() - to_chat(owner, "These are merely guidelines! The federation are your masters, but you forge your own path!") - to_chat(owner, "You will find a list of available spells in your spell book. Choose your magic arsenal carefully.") - to_chat(owner, "The spellbook is bound to you, and others cannot use it.") - to_chat(owner, "In your pockets you will find a teleport scroll. Use it as needed.") - to_chat(owner,"Remember: do not forget to prepare your spells.") +/datum/antagonist/wizard/ui_static_data(mob/user) + . = ..() + var/list/data = list() + data["objectives"] = get_objectives() + return data /datum/antagonist/wizard/farewell() - to_chat(owner, "You have been brainwashed! You are no longer a wizard!") + to_chat(owner, span_userdanger("You have been brainwashed! You are no longer a wizard!")) /datum/antagonist/wizard/proc/rename_wizard() set waitfor = FALSE @@ -111,7 +111,7 @@ var/wizard_name_second = pick(GLOB.wizard_second) var/randomname = "[wizard_name_first] [wizard_name_second]" var/mob/living/wiz_mob = owner.current - var/newname = reject_bad_name(stripped_input(wiz_mob, "You are the [name]. Would you like to change your name to something else?", "Name change", randomname, MAX_NAME_LEN)) + var/newname = sanitize_name(reject_bad_text(stripped_input(wiz_mob, "You are the [name]. Would you like to change your name to something else?", "Name change", randomname, MAX_NAME_LEN))) if (!newname) newname = randomname @@ -120,12 +120,12 @@ /datum/antagonist/wizard/apply_innate_effects(mob/living/mob_override) var/mob/living/M = mob_override || owner.current - update_wiz_icons_added(M, wiz_team ? TRUE : FALSE) //Don't bother showing the icon if you're solo wizard + add_antag_hud(antag_hud_type, antag_hud_name, M) M.faction |= ROLE_WIZARD /datum/antagonist/wizard/remove_innate_effects(mob/living/mob_override) var/mob/living/M = mob_override || owner.current - update_wiz_icons_removed(M) + remove_antag_hud(antag_hud_type, M) M.faction -= ROLE_WIZARD @@ -138,7 +138,7 @@ /datum/antagonist/wizard/apprentice name = "Wizard Apprentice" - hud_version = "apprentice" + antag_hud_name = "apprentice" var/datum/mind/master var/school = APPRENTICE_DESTRUCTION outfit_type = /datum/outfit/wizard/apprentice @@ -157,7 +157,7 @@ /datum/antagonist/wizard/apprentice/equip_wizard() . = ..() if(!owner) - return + CRASH("Antag datum with no owner.") var/mob/living/carbon/human/H = owner.current if(!istype(H)) return @@ -169,12 +169,12 @@ if(APPRENTICE_BLUESPACE) owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/turf_teleport/blink(null)) owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/ethereal_jaunt(null)) - to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned reality bending mobility spells. You are able to cast teleport and ethereal jaunt.") + to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned reality-bending mobility spells. You are able to cast teleport and ethereal jaunt.") if(APPRENTICE_HEALING) owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/charge(null)) owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/forcewall(null)) H.put_in_hands(new /obj/item/gun/magic/staff/healing(H)) - to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned livesaving survival spells. You are able to cast charge and forcewall.") + to_chat(owner, "Your service has not gone unrewarded, however. Studying under [master.current.real_name], you have learned life-saving survival spells. You are able to cast charge and forcewall.") if(APPRENTICE_ROBELESS) owner.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/knock(null)) owner.AddSpell(new /obj/effect/proc_holder/spell/pointed/mind_transfer(null)) @@ -194,6 +194,7 @@ //Random event wizard /datum/antagonist/wizard/apprentice/imposter name = "Wizard Imposter" + show_in_antagpanel = FALSE allow_rename = FALSE move_to_lair = FALSE @@ -224,20 +225,11 @@ owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/turf_teleport/blink(null)) owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/ethereal_jaunt(null)) -/datum/antagonist/wizard/proc/update_wiz_icons_added(mob/living/wiz,join = TRUE) - var/datum/atom_hud/antag/wizhud = GLOB.huds[ANTAG_HUD_WIZ] - wizhud.join_hud(wiz) - set_antag_hud(wiz, hud_version) - -/datum/antagonist/wizard/proc/update_wiz_icons_removed(mob/living/wiz) - var/datum/atom_hud/antag/wizhud = GLOB.huds[ANTAG_HUD_WIZ] - wizhud.leave_hud(wiz) - set_antag_hud(wiz, null) - - /datum/antagonist/wizard/academy name = "Academy Teacher" + show_in_antagpanel = FALSE outfit_type = /datum/outfit/wizard/academy + move_to_lair = FALSE /datum/antagonist/wizard/academy/equip_wizard() . = ..() @@ -250,7 +242,7 @@ if(!istype(M)) return - var/obj/item/implant/exile/Implant = new + var/obj/item/implant/exile/Implant = new/obj/item/implant/exile(M) Implant.implant(M) /datum/antagonist/wizard/academy/create_objectives() @@ -265,25 +257,25 @@ parts += printplayer(owner) var/count = 1 - var/wizardwin = 1 + var/wizardwin = TRUE for(var/datum/objective/objective in objectives) if(objective.completable) var/completion = objective.check_completion() if(completion >= 1) - parts += "Objective #[count]: [objective.explanation_text] Success!" + parts += "
Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" else if(completion <= 0) - parts += "Objective #[count]: [objective.explanation_text] Fail." + parts += "
Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" wizardwin = FALSE else - parts += "Objective #[count]: [objective.explanation_text] [completion*100]%" + parts += "
Objective #[count]: [objective.explanation_text] [completion*100]%" else parts += "Objective #[count]: [objective.explanation_text]" count++ if(wizardwin) - parts += "The wizard was successful!" + parts += span_greentext("The wizard was successful!") else - parts += "The wizard has failed!" + parts += span_redtext("The wizard has failed!") if(owner.spell_list.len>0) parts += "[owner.name] used the following spells: " diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index 94c7630bd9..f88f57eacc 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -13,9 +13,6 @@ GLOBAL_LIST_EMPTY(asset_datums) var/_abstract = /datum/asset var/cached_url_mappings - /// Whether or not this asset should be loaded in the "early assets" SS - var/early = FALSE - /datum/asset/New() GLOB.asset_datums[type] = src register() @@ -368,28 +365,3 @@ GLOBAL_LIST_EMPTY(asset_datums) /datum/asset/simple/namespaced/proc/get_htmlloader(filename) return url2htmlloader(SSassets.transport.get_asset_url(filename, assets[filename])) -/// A subtype to generate a JSON file from a list -/datum/asset/json - _abstract = /datum/asset/json - /// The filename, will be suffixed with ".json" - var/name - -/datum/asset/json/send(client) - return SSassets.transport.send_assets(client, "data/[name].json") - -/datum/asset/json/get_url_mappings() - return list( - "[name].json" = SSassets.transport.get_asset_url("data/[name].json"), - ) - -/datum/asset/json/register() - var/filename = "data/[name].json" - fdel(filename) - text2file(json_encode(generate()), filename) - SSassets.transport.register_asset(filename, fcopy_rsc(filename)) - fdel(filename) - -/// Returns the data that will be JSON encoded -/datum/asset/json/proc/generate() - SHOULD_CALL_PARENT(FALSE) - CRASH("generate() not implemented for [type]!") diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 04725695a7..73c94ac559 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -1,11 +1,5 @@ //DEFINITIONS FOR ASSET DATUMS START HERE. -/datum/asset/simple/tgui_common - keep_local_name = TRUE - assets = list( - "tgui-common.bundle.js" = file("tgui/public/tgui-common.bundle.js"), - ) - /datum/asset/simple/tgui keep_local_name = TRUE assets = list( @@ -54,9 +48,14 @@ /datum/asset/simple/radar_assets assets = list( - "ntosradarbackground.png" = 'icons/UI_Icons/tgui/ntosradar_background.png', - "ntosradarpointer.png" = 'icons/UI_Icons/tgui/ntosradar_pointer.png', - "ntosradarpointerS.png" = 'icons/UI_Icons/tgui/ntosradar_pointer_S.png' + "ntosradarbackground.png" = 'icons/ui_icons/tgui/ntosradar_background.png', + "ntosradarpointer.png" = 'icons/ui_icons/tgui/ntosradar_pointer.png', + "ntosradarpointerS.png" = 'icons/ui_icons/tgui/ntosradar_pointer_S.png' + ) + +/datum/asset/simple/circuit_assets + assets = list( + "grid_background.png" = 'icons/ui_icons/tgui/grid_background.png' ) /datum/asset/spritesheet/simple/pda @@ -91,6 +90,7 @@ "status" = 'icons/pda_icons/pda_status.png', "dronephone" = 'icons/pda_icons/pda_dronephone.png', "emoji" = 'icons/pda_icons/pda_emoji.png' + // "droneblacklist" = 'icons/pda_icons/pda_droneblacklist.png', ) /datum/asset/spritesheet/simple/paper @@ -116,7 +116,7 @@ /datum/asset/simple/irv assets = list( - "jquery-ui.custom-core-widgit-mouse-sortable-min.js" = 'html/IRV/jquery-ui.custom-core-widgit-mouse-sortable-min.js', + "jquery-ui.custom-core-widgit-mouse-sortable-min.js" = 'html/jquery/jquery-ui.custom-core-widgit-mouse-sortable.min.js', ) /datum/asset/group/irv @@ -149,11 +149,10 @@ ) parents = list("changelog.html" = 'html/changelog.html') - /datum/asset/simple/jquery legacy = TRUE assets = list( - "jquery.min.js" = 'html/jquery.min.js', + "jquery.min.js" = 'html/jquery/jquery.min.js', ) /datum/asset/simple/namespaced/fontawesome @@ -223,102 +222,102 @@ /datum/asset/simple/arcade assets = list( - "boss1.gif" = 'icons/UI_Icons/Arcade/boss1.gif', - "boss2.gif" = 'icons/UI_Icons/Arcade/boss2.gif', - "boss3.gif" = 'icons/UI_Icons/Arcade/boss3.gif', - "boss4.gif" = 'icons/UI_Icons/Arcade/boss4.gif', - "boss5.gif" = 'icons/UI_Icons/Arcade/boss5.gif', - "boss6.gif" = 'icons/UI_Icons/Arcade/boss6.gif', + "boss1.gif" = 'icons/ui_icons/arcade/boss1.gif', + "boss2.gif" = 'icons/ui_icons/arcade/boss2.gif', + "boss3.gif" = 'icons/ui_icons/arcade/boss3.gif', + "boss4.gif" = 'icons/ui_icons/arcade/boss4.gif', + "boss5.gif" = 'icons/ui_icons/arcade/boss5.gif', + "boss6.gif" = 'icons/ui_icons/arcade/boss6.gif', ) /datum/asset/spritesheet/simple/achievements name ="achievements" assets = list( - "default" = 'icons/UI_Icons/Achievements/default.png', - "basemisc" = 'icons/UI_Icons/Achievements/basemisc.png', - "baseboss" = 'icons/UI_Icons/Achievements/baseboss.png', - "baseskill" = 'icons/UI_Icons/Achievements/baseskill.png', - "bbgum" = 'icons/UI_Icons/Achievements/Boss/bbgum.png', - "colossus" = 'icons/UI_Icons/Achievements/Boss/colossus.png', - "hierophant" = 'icons/UI_Icons/Achievements/Boss/hierophant.png', - "drake" = 'icons/UI_Icons/Achievements/Boss/drake.png', - "legion" = 'icons/UI_Icons/Achievements/Boss/legion.png', - "miner" = 'icons/UI_Icons/Achievements/Boss/miner.png', - "swarmer" = 'icons/UI_Icons/Achievements/Boss/swarmer.png', - "tendril" = 'icons/UI_Icons/Achievements/Boss/tendril.png', - "featofstrength" = 'icons/UI_Icons/Achievements/Misc/featofstrength.png', - "helbital" = 'icons/UI_Icons/Achievements/Misc/helbital.png', - "jackpot" = 'icons/UI_Icons/Achievements/Misc/jackpot.png', - "meteors" = 'icons/UI_Icons/Achievements/Misc/meteors.png', - "timewaste" = 'icons/UI_Icons/Achievements/Misc/timewaste.png', - "upgrade" = 'icons/UI_Icons/Achievements/Misc/upgrade.png', - "clownking" = 'icons/UI_Icons/Achievements/Misc/clownking.png', - "clownthanks" = 'icons/UI_Icons/Achievements/Misc/clownthanks.png', - "rule8" = 'icons/UI_Icons/Achievements/Misc/rule8.png', - "longshift" = 'icons/UI_Icons/Achievements/Misc/longshift.png', - "snail" = 'icons/UI_Icons/Achievements/Misc/snail.png', - "ascension" = 'icons/UI_Icons/Achievements/Misc/ascension.png', - "ashascend" = 'icons/UI_Icons/Achievements/Misc/ashascend.png', - "fleshascend" = 'icons/UI_Icons/Achievements/Misc/fleshascend.png', - "rustascend" = 'icons/UI_Icons/Achievements/Misc/rustascend.png', - "voidascend" = 'icons/UI_Icons/Achievements/Misc/voidascend.png', - "toolbox_soul" = 'icons/UI_Icons/Achievements/Misc/toolbox_soul.png', - "chem_tut" = 'icons/UI_Icons/Achievements/Misc/chem_tut.png', - "mining" = 'icons/UI_Icons/Achievements/Skills/mining.png', - "mafia" = 'icons/UI_Icons/Achievements/Mafia/mafia.png', - "town" = 'icons/UI_Icons/Achievements/Mafia/town.png', - "neutral" = 'icons/UI_Icons/Achievements/Mafia/neutral.png', - "hated" = 'icons/UI_Icons/Achievements/Mafia/hated.png', - "basemafia" ='icons/UI_Icons/Achievements/basemafia.png', - "frenching" = 'icons/UI_Icons/Achievements/Misc/frenchingthebubble.png' + "default" = 'icons/ui_icons/achievements/default.png', + "basemisc" = 'icons/ui_icons/achievements/basemisc.png', + "baseboss" = 'icons/ui_icons/achievements/baseboss.png', + "baseskill" = 'icons/ui_icons/achievements/baseskill.png', + "bbgum" = 'icons/ui_icons/achievements/Boss/bbgum.png', + "colossus" = 'icons/ui_icons/achievements/Boss/colossus.png', + "hierophant" = 'icons/ui_icons/achievements/Boss/hierophant.png', + "drake" = 'icons/ui_icons/achievements/Boss/drake.png', + "legion" = 'icons/ui_icons/achievements/Boss/legion.png', + "miner" = 'icons/ui_icons/achievements/Boss/miner.png', + "swarmer" = 'icons/ui_icons/achievements/Boss/swarmer.png', + "tendril" = 'icons/ui_icons/achievements/Boss/tendril.png', + "featofstrength" = 'icons/ui_icons/achievements/Misc/featofstrength.png', + "helbital" = 'icons/ui_icons/achievements/Misc/helbital.png', + "jackpot" = 'icons/ui_icons/achievements/Misc/jackpot.png', + "meteors" = 'icons/ui_icons/achievements/Misc/meteors.png', + "timewaste" = 'icons/ui_icons/achievements/Misc/timewaste.png', + "upgrade" = 'icons/ui_icons/achievements/Misc/upgrade.png', + "clownking" = 'icons/ui_icons/achievements/Misc/clownking.png', + "clownthanks" = 'icons/ui_icons/achievements/Misc/clownthanks.png', + "rule8" = 'icons/ui_icons/achievements/Misc/rule8.png', + "longshift" = 'icons/ui_icons/achievements/Misc/longshift.png', + "snail" = 'icons/ui_icons/achievements/Misc/snail.png', + "ascension" = 'icons/ui_icons/achievements/Misc/ascension.png', + "ashascend" = 'icons/ui_icons/achievements/Misc/ashascend.png', + "fleshascend" = 'icons/ui_icons/achievements/Misc/fleshascend.png', + "rustascend" = 'icons/ui_icons/achievements/Misc/rustascend.png', + "voidascend" = 'icons/ui_icons/achievements/Misc/voidascend.png', + "toolbox_soul" = 'icons/ui_icons/achievements/Misc/toolbox_soul.png', + "chem_tut" = 'icons/ui_icons/achievements/Misc/chem_tut.png', + "mining" = 'icons/ui_icons/achievements/Skills/mining.png', + "mafia" = 'icons/ui_icons/achievements/Mafia/mafia.png', + "town" = 'icons/ui_icons/achievements/Mafia/town.png', + "neutral" = 'icons/ui_icons/achievements/Mafia/neutral.png', + "hated" = 'icons/ui_icons/achievements/Mafia/hated.png', + "basemafia" ='icons/ui_icons/achievements/basemafia.png', + "frenching" = 'icons/ui_icons/achievements/Misc/frenchingthebubble.png' ) /datum/asset/spritesheet/simple/pills name = "pills" assets = list( - "pill1" = 'icons/UI_Icons/Pills/pill1.png', - "pill2" = 'icons/UI_Icons/Pills/pill2.png', - "pill3" = 'icons/UI_Icons/Pills/pill3.png', - "pill4" = 'icons/UI_Icons/Pills/pill4.png', - "pill5" = 'icons/UI_Icons/Pills/pill5.png', - "pill6" = 'icons/UI_Icons/Pills/pill6.png', - "pill7" = 'icons/UI_Icons/Pills/pill7.png', - "pill8" = 'icons/UI_Icons/Pills/pill8.png', - "pill9" = 'icons/UI_Icons/Pills/pill9.png', - "pill10" = 'icons/UI_Icons/Pills/pill10.png', - "pill11" = 'icons/UI_Icons/Pills/pill11.png', - "pill12" = 'icons/UI_Icons/Pills/pill12.png', - "pill13" = 'icons/UI_Icons/Pills/pill13.png', - "pill14" = 'icons/UI_Icons/Pills/pill14.png', - "pill15" = 'icons/UI_Icons/Pills/pill15.png', - "pill16" = 'icons/UI_Icons/Pills/pill16.png', - "pill17" = 'icons/UI_Icons/Pills/pill17.png', - "pill18" = 'icons/UI_Icons/Pills/pill18.png', - "pill19" = 'icons/UI_Icons/Pills/pill19.png', - "pill20" = 'icons/UI_Icons/Pills/pill20.png', - "pill21" = 'icons/UI_Icons/Pills/pill21.png', - "pill22" = 'icons/UI_Icons/Pills/pill22.png', + "pill1" = 'icons/ui_icons/pills/pill1.png', + "pill2" = 'icons/ui_icons/pills/pill2.png', + "pill3" = 'icons/ui_icons/pills/pill3.png', + "pill4" = 'icons/ui_icons/pills/pill4.png', + "pill5" = 'icons/ui_icons/pills/pill5.png', + "pill6" = 'icons/ui_icons/pills/pill6.png', + "pill7" = 'icons/ui_icons/pills/pill7.png', + "pill8" = 'icons/ui_icons/pills/pill8.png', + "pill9" = 'icons/ui_icons/pills/pill9.png', + "pill10" = 'icons/ui_icons/pills/pill10.png', + "pill11" = 'icons/ui_icons/pills/pill11.png', + "pill12" = 'icons/ui_icons/pills/pill12.png', + "pill13" = 'icons/ui_icons/pills/pill13.png', + "pill14" = 'icons/ui_icons/pills/pill14.png', + "pill15" = 'icons/ui_icons/pills/pill15.png', + "pill16" = 'icons/ui_icons/pills/pill16.png', + "pill17" = 'icons/ui_icons/pills/pill17.png', + "pill18" = 'icons/ui_icons/pills/pill18.png', + "pill19" = 'icons/ui_icons/pills/pill19.png', + "pill20" = 'icons/ui_icons/pills/pill20.png', + "pill21" = 'icons/ui_icons/pills/pill21.png', + "pill22" = 'icons/ui_icons/pills/pill22.png', ) // /datum/asset/spritesheet/simple/condiments // name = "condiments" // assets = list( -// CONDIMASTER_STYLE_FALLBACK = 'icons/UI_Icons/Condiments/emptycondiment.png', -// "enzyme" = 'icons/UI_Icons/Condiments/enzyme.png', -// "flour" = 'icons/UI_Icons/Condiments/flour.png', -// "mayonnaise" = 'icons/UI_Icons/Condiments/mayonnaise.png', -// "milk" = 'icons/UI_Icons/Condiments/milk.png', -// "blackpepper" = 'icons/UI_Icons/Condiments/peppermillsmall.png', -// "rice" = 'icons/UI_Icons/Condiments/rice.png', -// "sodiumchloride" = 'icons/UI_Icons/Condiments/saltshakersmall.png', -// "soymilk" = 'icons/UI_Icons/Condiments/soymilk.png', -// "soysauce" = 'icons/UI_Icons/Condiments/soysauce.png', -// "sugar" = 'icons/UI_Icons/Condiments/sugar.png', -// "ketchup" = 'icons/UI_Icons/Condiments/ketchup.png', -// "capsaicin" = 'icons/UI_Icons/Condiments/hotsauce.png', -// "frostoil" = 'icons/UI_Icons/Condiments/coldsauce.png', -// "bbqsauce" = 'icons/UI_Icons/Condiments/bbqsauce.png', -// "cornoil" = 'icons/UI_Icons/Condiments/oliveoil.png', +// CONDIMASTER_STYLE_FALLBACK = 'icons/ui_icons/condiments/emptycondiment.png', +// "enzyme" = 'icons/ui_icons/condiments/enzyme.png', +// "flour" = 'icons/ui_icons/condiments/flour.png', +// "mayonnaise" = 'icons/ui_icons/condiments/mayonnaise.png', +// "milk" = 'icons/ui_icons/condiments/milk.png', +// "blackpepper" = 'icons/ui_icons/condiments/peppermillsmall.png', +// "rice" = 'icons/ui_icons/condiments/rice.png', +// "sodiumchloride" = 'icons/ui_icons/condiments/saltshakersmall.png', +// "soymilk" = 'icons/ui_icons/condiments/soymilk.png', +// "soysauce" = 'icons/ui_icons/condiments/soysauce.png', +// "sugar" = 'icons/ui_icons/condiments/sugar.png', +// "ketchup" = 'icons/ui_icons/condiments/ketchup.png', +// "capsaicin" = 'icons/ui_icons/condiments/hotsauce.png', +// "frostoil" = 'icons/ui_icons/condiments/coldsauce.png', +// "bbqsauce" = 'icons/ui_icons/condiments/bbqsauce.png', +// "cornoil" = 'icons/ui_icons/condiments/oliveoil.png', // ) //this exists purely to avoid meta by pre-loading all language icons. @@ -410,9 +409,15 @@ if (machine) item = machine + // Check for GAGS support where necessary + // var/greyscale_config = initial(item.greyscale_config) + // var/greyscale_colors = initial(item.greyscale_colors) + // if (greyscale_config && greyscale_colors) + // icon_file = SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) + // else icon_file = initial(item.icon) - icon_state = initial(item.icon_state) + icon_state = initial(item.icon_state) if(!(icon_state in icon_states(icon_file))) warning("design [D] with icon '[icon_file]' missing state '[icon_state]'") continue @@ -441,7 +446,11 @@ if (!ispath(item, /atom)) continue - var/icon_file = initial(item.icon) + var/icon_file + // if (initial(item.greyscale_colors) && initial(item.greyscale_config)) + // icon_file = SSgreyscale.GetColoredIconByType(initial(item.greyscale_config), initial(item.greyscale_colors)) + // else + icon_file = initial(item.icon) var/icon_state = initial(item.icon_state) var/icon/I @@ -543,32 +552,40 @@ // Insert(id, fish_icon, fish_icon_state) // ..() +/datum/asset/simple/adventure + assets = list( + "default" = 'icons/ui_icons/adventure/default.png', + "grue" = 'icons/ui_icons/adventure/grue.png', + "signal_lost" ='icons/ui_icons/adventure/signal_lost.png', + "trade" = 'icons/ui_icons/adventure/trade.png', + ) + /datum/asset/simple/inventory assets = list( - "inventory-glasses.png" = 'icons/UI_Icons/inventory/glasses.png', - "inventory-head.png" = 'icons/UI_Icons/inventory/head.png', - "inventory-neck.png" = 'icons/UI_Icons/inventory/neck.png', - "inventory-mask.png" = 'icons/UI_Icons/inventory/mask.png', - "inventory-ears.png" = 'icons/UI_Icons/inventory/ears.png', - "inventory-uniform.png" = 'icons/UI_Icons/inventory/uniform.png', - "inventory-suit.png" = 'icons/UI_Icons/inventory/suit.png', - "inventory-gloves.png" = 'icons/UI_Icons/inventory/gloves.png', - "inventory-hand_l.png" = 'icons/UI_Icons/inventory/hand_l.png', - "inventory-hand_r.png" = 'icons/UI_Icons/inventory/hand_r.png', - "inventory-shoes.png" = 'icons/UI_Icons/inventory/shoes.png', - "inventory-suit_storage.png" = 'icons/UI_Icons/inventory/suit_storage.png', - "inventory-id.png" = 'icons/UI_Icons/inventory/id.png', - "inventory-belt.png" = 'icons/UI_Icons/inventory/belt.png', - "inventory-back.png" = 'icons/UI_Icons/inventory/back.png', - "inventory-pocket.png" = 'icons/UI_Icons/inventory/pocket.png', - "inventory-collar.png" = 'icons/UI_Icons/inventory/collar.png', + "inventory-glasses.png" = 'icons/ui_icons/inventory/glasses.png', + "inventory-head.png" = 'icons/ui_icons/inventory/head.png', + "inventory-neck.png" = 'icons/ui_icons/inventory/neck.png', + "inventory-mask.png" = 'icons/ui_icons/inventory/mask.png', + "inventory-ears.png" = 'icons/ui_icons/inventory/ears.png', + "inventory-uniform.png" = 'icons/ui_icons/inventory/uniform.png', + "inventory-suit.png" = 'icons/ui_icons/inventory/suit.png', + "inventory-gloves.png" = 'icons/ui_icons/inventory/gloves.png', + "inventory-hand_l.png" = 'icons/ui_icons/inventory/hand_l.png', + "inventory-hand_r.png" = 'icons/ui_icons/inventory/hand_r.png', + "inventory-shoes.png" = 'icons/ui_icons/inventory/shoes.png', + "inventory-suit_storage.png" = 'icons/ui_icons/inventory/suit_storage.png', + "inventory-id.png" = 'icons/ui_icons/inventory/id.png', + "inventory-belt.png" = 'icons/ui_icons/inventory/belt.png', + "inventory-back.png" = 'icons/ui_icons/inventory/back.png', + "inventory-pocket.png" = 'icons/ui_icons/inventory/pocket.png', + "inventory-collar.png" = 'icons/ui_icons/inventory/collar.png', ///Sandstorm content - "inventory-ears_extra.png" = 'modular_sand/icons/UI_Icons/inventory/ears_extra.png', - "inventory-underwear.png" = 'modular_sand/icons/UI_Icons/inventory/underwear.png', - "inventory-socks.png" = 'modular_sand/icons/UI_Icons/inventory/socks.png', - "inventory-undershirt.png" = 'modular_sand/icons/UI_Icons/inventory/undershirt.png', - "inventory-wrists.png" = 'modular_sand/icons/UI_Icons/inventory/wrists.png', + "inventory-ears_extra.png" = 'modular_sand/icons/ui_icons/inventory/ears_extra.png', + "inventory-underwear.png" = 'modular_sand/icons/ui_icons/inventory/underwear.png', + "inventory-socks.png" = 'modular_sand/icons/ui_icons/inventory/socks.png', + "inventory-undershirt.png" = 'modular_sand/icons/ui_icons/inventory/undershirt.png', + "inventory-wrists.png" = 'modular_sand/icons/ui_icons/inventory/wrists.png', ) /// Removes all non-alphanumerics from the text, keep in mind this can lead to id conflicts @@ -578,5 +595,34 @@ /datum/asset/simple/tutorial_advisors assets = list( - "chem_help_advisor.gif" = 'icons/UI_Icons/Advisors/chem_help_advisor.gif', + "chem_help_advisor.gif" = 'icons/ui_icons/advisors/chem_help_advisor.gif', ) + +// /datum/asset/spritesheet/moods +// name = "moods" +// var/iconinserted = 1 + +// /datum/asset/spritesheet/moods/register() +// for(var/i in 1 to 9) +// var/target_to_insert = "mood"+"[iconinserted]" +// Insert(target_to_insert, 'icons/hud/screen_gen.dmi', target_to_insert) +// iconinserted++ +// ..() + +// /datum/asset/spritesheet/moods/ModifyInserted(icon/pre_asset) +// var/blended_color +// switch(iconinserted) +// if(1) +// blended_color = "#f15d36" +// if(2 to 3) +// blended_color = "#f38943" +// if(4) +// blended_color = "#dfa65b" +// if(5) +// blended_color = "#4b96c4" +// if(6) +// blended_color = "#86d656" +// else +// blended_color = "#2eeb9a" +// pre_asset.Blend(blended_color, ICON_MULTIPLY) +// return pre_asset diff --git a/code/modules/asset_cache/transports/asset_transport.dm b/code/modules/asset_cache/transports/asset_transport.dm index f5a1af4f05..b2da2602ae 100644 --- a/code/modules/asset_cache/transports/asset_transport.dm +++ b/code/modules/asset_cache/transports/asset_transport.dm @@ -115,7 +115,7 @@ if (unreceived.len) if (unreceived.len >= ASSET_CACHE_TELL_CLIENT_AMOUNT) - to_chat(client, "Sending Resources...") + to_chat(client, "Sending Resources...") for (var/asset_name in unreceived) var/new_asset_name = asset_name diff --git a/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm b/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm index 56a7d78288..ac8ed537b5 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/passive_gate.dm @@ -29,13 +29,16 @@ Passive gate is similar to the regular pump except: /obj/machinery/atmospherics/components/binary/passive_gate/CtrlClick(mob/user) if(can_interact(user)) on = !on + investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS) update_icon() return ..() /obj/machinery/atmospherics/components/binary/passive_gate/AltClick(mob/user) if(can_interact(user)) target_pressure = MAX_OUTPUT_PRESSURE - update_icon() + investigate_log("was set to [target_pressure] kPa by [key_name(user)]", INVESTIGATE_ATMOS) + balloon_alert(user, "pressure output set to [target_pressure] kPa") + update_appearance() return ..() /obj/machinery/atmospherics/components/binary/passive_gate/Destroy() diff --git a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm index dc5a6eccd4..a8d82282d6 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm @@ -33,25 +33,19 @@ . += "You can hold Alt and click on it to maximize its pressure." /obj/machinery/atmospherics/components/binary/pump/CtrlClick(mob/user) - var/area/A = get_area(src) - var/turf/T = get_turf(src) - if(user.canUseTopic(src, BE_CLOSE, FALSE,)) + if(can_interact(user)) on = !on - update_icon() - investigate_log("Pump, [src.name], turned on by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) - message_admins("Pump, [src.name], turned [on ? "on" : "off"] by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") - return ..() + investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS) + update_appearance() + return ..() /obj/machinery/atmospherics/components/binary/pump/AltClick(mob/user) - . = ..() - var/area/A = get_area(src) - var/turf/T = get_turf(src) - if(user.canUseTopic(src, BE_CLOSE, FALSE,)) + if(can_interact(user)) target_pressure = MAX_OUTPUT_PRESSURE - to_chat(user,"You maximize the pressure on the [src].") - investigate_log("Pump, [src.name], was maximized by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) - message_admins("Pump, [src.name], was maximized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") - return TRUE + investigate_log("was set to [target_pressure] kPa by [key_name(user)]", INVESTIGATE_ATMOS) + balloon_alert(user, "pressure output set to [target_pressure] kPa") + update_appearance() + return ..() /obj/machinery/atmospherics/components/binary/pump/Destroy() SSradio.remove_object(src,frequency) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm index 46d584339b..f5f0064558 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm @@ -33,15 +33,20 @@ . += "You can hold Alt and click on it to maximize its pressure." /obj/machinery/atmospherics/components/binary/volume_pump/CtrlClick(mob/user) - var/area/A = get_area(src) - var/turf/T = get_turf(src) if(user.canUseTopic(src, BE_CLOSE, FALSE,)) on = !on - update_icon() - investigate_log("Volume Pump, [src.name], turned on by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) - message_admins("Volume Pump, [src.name], turned [on ? "on" : "off"] by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") + investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS) + update_appearance() return ..() +/obj/machinery/atmospherics/components/binary/volume_pump/AltClick(mob/user) + if(can_interact(user)) + transfer_rate = MAX_TRANSFER_RATE + investigate_log("was set to [transfer_rate] L/s by [key_name(user)]", INVESTIGATE_ATMOS) + balloon_alert(user, "volume output set to [transfer_rate] L/s") + update_appearance() + return ..() + /obj/machinery/atmospherics/components/binary/volume_pump/Destroy() SSradio.remove_object(src,frequency) return ..() diff --git a/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm b/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm index b6d03bd0a7..fd89e26792 100644 --- a/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm +++ b/code/modules/atmospherics/machinery/components/trinary_devices/filter.dm @@ -20,25 +20,19 @@ . += "You can hold Alt and click on it to maximize its flow rate." /obj/machinery/atmospherics/components/trinary/filter/CtrlClick(mob/user) - var/area/A = get_area(src) - var/turf/T = get_turf(src) if(user.canUseTopic(src, BE_CLOSE, FALSE,)) on = !on - update_icon() - investigate_log("Filter, [src.name], turned on by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) - message_admins("Filter, [src.name], turned [on ? "on" : "off"] by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") + investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS) + update_appearance() return ..() /obj/machinery/atmospherics/components/trinary/filter/AltClick(mob/user) - . = ..() - var/area/A = get_area(src) - var/turf/T = get_turf(src) - if(user.canUseTopic(src, BE_CLOSE, FALSE,)) + if(can_interact(user)) transfer_rate = MAX_TRANSFER_RATE - to_chat(user,"You maximize the flow rate on the [src].") - investigate_log("Filter, [src.name], was maximized by [key_name(usr)] at [x], [y], [z], [A]", INVESTIGATE_ATMOS) - message_admins("Filter, [src.name], was maximized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") - return TRUE + investigate_log("was set to [transfer_rate] L/s by [key_name(user)]", INVESTIGATE_ATMOS) + balloon_alert(user, "volume output set to [transfer_rate] L/s") + update_appearance() + return ..() /obj/machinery/atmospherics/components/trinary/filter/proc/set_frequency(new_frequency) SSradio.remove_object(src, frequency) diff --git a/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm b/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm index 3296981e5e..49d6a71f78 100644 --- a/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm +++ b/code/modules/atmospherics/machinery/components/trinary_devices/mixer.dm @@ -17,35 +17,27 @@ //node 3 is the outlet, nodes 1 & 2 are intakes /obj/machinery/atmospherics/components/trinary/mixer/CtrlClick(mob/user) - if(user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) + if(can_interact(user)) on = !on - update_icon() + investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS) + update_appearance() return ..() /obj/machinery/atmospherics/components/trinary/mixer/AltClick(mob/user) - if(user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) + if(can_interact(user)) target_pressure = MAX_OUTPUT_PRESSURE - update_icon() + investigate_log("was set to [target_pressure] kPa by [key_name(user)]", INVESTIGATE_ATMOS) + balloon_alert(user, "pressure output on set to [target_pressure] kPa") + update_appearance() return ..() - //node 3 is the outlet, nodes 1 & 2 are intakes - -/obj/machinery/atmospherics/components/trinary/mixer/update_icon() - cut_overlays() +/obj/machinery/atmospherics/components/trinary/mixer/update_overlays() + . = ..() for(var/direction in GLOB.cardinals) if(!(direction & initialize_directions)) continue - var/obj/machinery/atmospherics/node = findConnecting(direction) - var/image/cap - if(node) - cap = getpipeimage(icon, "cap", direction, node.pipe_color, piping_layer = piping_layer) - else - cap = getpipeimage(icon, "cap", direction, piping_layer = piping_layer) - - add_overlay(cap) - - return ..() + . += getpipeimage(icon, "cap", direction, pipe_color, piping_layer, TRUE) /obj/machinery/atmospherics/components/trinary/mixer/update_icon_nopipes() var/on_state = on && nodes[1] && nodes[2] && nodes[3] && is_operational() diff --git a/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm b/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm index b2fa26edba..c1990a0ffe 100644 --- a/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm +++ b/code/modules/atmospherics/machinery/components/unary_devices/outlet_injector.dm @@ -24,13 +24,16 @@ /obj/machinery/atmospherics/components/unary/outlet_injector/CtrlClick(mob/user) if(can_interact(user)) on = !on - update_icon() + investigate_log("was turned [on ? "on" : "off"] by [key_name(user)]", INVESTIGATE_ATMOS) + update_appearance() return ..() /obj/machinery/atmospherics/components/unary/outlet_injector/AltClick(mob/user) if(can_interact(user)) volume_rate = MAX_TRANSFER_RATE - update_icon() + investigate_log("was set to [volume_rate] L/s by [key_name(user)]", INVESTIGATE_ATMOS) + balloon_alert(user, "volume output set to [volume_rate] L/s") + update_appearance() return ..() /obj/machinery/atmospherics/components/unary/outlet_injector/Destroy() diff --git a/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm b/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm index cff08d0d02..80a8ee4bf3 100644 --- a/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm +++ b/code/modules/atmospherics/machinery/components/unary_devices/thermomachine.dm @@ -217,16 +217,11 @@ min_temperature = max(T0C - (initial(min_temperature) + L * 15), TCMB) //73.15K with T1 stock parts /obj/machinery/atmospherics/components/unary/thermomachine/freezer/AltClick(mob/living/user) - . = ..() - var/area/A = get_area(src) - var/turf/T = get_turf(src) - if(!istype(user) || !user.canUseTopic(src, BE_CLOSE)) + if(!can_interact(user)) return target_temperature = min_temperature - to_chat(user,"You minimize the temperature on the [src].") - investigate_log("was set to [target_temperature] K by [key_name(usr)]", INVESTIGATE_ATMOS) - message_admins("[src.name] was minimized by [ADMIN_LOOKUPFLW(usr)] at [ADMIN_COORDJMP(T)], [A]") - return TRUE + investigate_log("was set to [target_temperature] K by [key_name(user)]", INVESTIGATE_ATMOS) + balloon_alert(user, "temperature reset to [target_temperature] K") /obj/machinery/atmospherics/components/unary/thermomachine/heater name = "heater" diff --git a/code/modules/balloon_alert/balloon_alert.dm b/code/modules/balloon_alert/balloon_alert.dm new file mode 100644 index 0000000000..d6f17d6ffe --- /dev/null +++ b/code/modules/balloon_alert/balloon_alert.dm @@ -0,0 +1,89 @@ +#define BALLOON_TEXT_WIDTH 200 +#define BALLOON_TEXT_SPAWN_TIME (0.2 SECONDS) +#define BALLOON_TEXT_FADE_TIME (0.1 SECONDS) +#define BALLOON_TEXT_FULLY_VISIBLE_TIME (0.7 SECONDS) +#define BALLOON_TEXT_TOTAL_LIFETIME(mult) (BALLOON_TEXT_SPAWN_TIME + BALLOON_TEXT_FULLY_VISIBLE_TIME*mult + BALLOON_TEXT_FADE_TIME) +/// The increase in duration per character in seconds +#define BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MULT (0.05) +/// The amount of characters needed before this increase takes into effect +#define BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MIN 10 + +/// Creates text that will float from the atom upwards to the viewer. +/atom/proc/balloon_alert(mob/viewer, text) + SHOULD_NOT_SLEEP(TRUE) + + INVOKE_ASYNC(src, .proc/balloon_alert_perform, viewer, text) + +/// Create balloon alerts (text that floats up) to everything within range. +/// Will only display to people who can see. +/atom/proc/balloon_alert_to_viewers(message, self_message, vision_distance = DEFAULT_MESSAGE_RANGE, list/ignored_mobs) + SHOULD_NOT_SLEEP(TRUE) + + var/list/hearers = get_hearers_in_view(vision_distance, src) + hearers -= ignored_mobs + + for (var/mob/hearer in hearers) + if (hearer.is_blind()) + continue + + balloon_alert(hearer, (hearer == src && self_message) || message) + +// Do not use. +// MeasureText blocks. I have no idea for how long. +// I would've made the maptext_height update on its own, but I don't know +// if this would look bad on laggy clients. +/atom/proc/balloon_alert_perform(mob/viewer, text) + var/client/viewer_client = viewer.client + if (isnull(viewer_client)) + return + + var/bound_width = world.icon_size + if (ismovable(src)) + var/atom/movable/movable_source = src + bound_width = movable_source.bound_width + + var/image/balloon_alert = image(loc = get_atom_on_turf(src), layer = ABOVE_MOB_LAYER) + balloon_alert.plane = BALLOON_CHAT_PLANE + balloon_alert.alpha = 0 + balloon_alert.maptext = MAPTEXT("[text]") + balloon_alert.maptext_x = (BALLOON_TEXT_WIDTH - bound_width) * -0.5 + balloon_alert.maptext_height = WXH_TO_HEIGHT(viewer_client?.MeasureText(text, null, BALLOON_TEXT_WIDTH)) + balloon_alert.maptext_width = BALLOON_TEXT_WIDTH + + viewer_client?.images += balloon_alert + + var/duration_mult = 1 + var/duration_length = length(text) - BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MIN + + if(duration_length > 0) + duration_mult += duration_length*BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MULT + + animate( + balloon_alert, + pixel_y = world.icon_size * 1.2, + time = BALLOON_TEXT_TOTAL_LIFETIME(1), + easing = SINE_EASING | EASE_OUT, + ) + + animate( + alpha = 255, + time = BALLOON_TEXT_SPAWN_TIME, + easing = CUBIC_EASING | EASE_OUT, + flags = ANIMATION_PARALLEL, + ) + + animate( + alpha = 0, + time = BALLOON_TEXT_FULLY_VISIBLE_TIME*duration_mult, + easing = CUBIC_EASING | EASE_IN, + ) + + addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_image_from_client, balloon_alert, viewer_client), BALLOON_TEXT_TOTAL_LIFETIME(duration_mult)) + +#undef BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MIN +#undef BALLOON_TEXT_CHAR_LIFETIME_INCREASE_MULT +#undef BALLOON_TEXT_FADE_TIME +#undef BALLOON_TEXT_FULLY_VISIBLE_TIME +#undef BALLOON_TEXT_SPAWN_TIME +#undef BALLOON_TEXT_TOTAL_LIFETIME +#undef BALLOON_TEXT_WIDTH diff --git a/code/modules/cargo/packs/vending.dm b/code/modules/cargo/packs/vending.dm index 6d978d629f..f0f89a49e6 100644 --- a/code/modules/cargo/packs/vending.dm +++ b/code/modules/cargo/packs/vending.dm @@ -52,7 +52,7 @@ desc = "Packs of tools waiting to be used for repairing. Contains a tool and engineering vending machine refill. Requires CE access." cost = 5500 //Powerfull access = ACCESS_CE - contains = list(/obj/item/vending_refill/tool, + contains = list(/obj/item/vending_refill/youtool, /obj/item/vending_refill/engivend) crate_name = "engineering supply crate" crate_type = /obj/structure/closet/crate/secure/engineering diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 950b2b5b25..70aab57e0a 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -34,6 +34,9 @@ var/last_move = 0 var/area = null + /// Timers are now handled by clients, not by doing a mess on the item and multiple people overwriting a single timer on the object, have fun. + var/tip_timer = null + /// Last time we Click()ed. No clicking twice in one tick! var/last_click = 0 diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index fde56d78ab..0b412b9ec4 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -81,19 +81,16 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( // Tgui Topic middleware if(tgui_Topic(href_list)) - if(CONFIG_GET(flag/emergency_tgui_logging)) - log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]") return if(href_list["reload_tguipanel"]) nuke_chat() if(href_list["reload_statbrowser"]) src << browse(file('html/statbrowser.html'), "window=statbrowser") + // Log all hrefs + log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]") last_activity = world.time - //Logs all hrefs - log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]") - //byond bug ID:2256651 if (asset_cache_job && (asset_cache_job in completed_asset_jobs)) to_chat(src, "An error has been detected in how your client is receiving resources. Attempting to correct.... (If you keep seeing these messages you might want to close byond and reconnect)") @@ -362,13 +359,19 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( return // Initialize tgui panel - tgui_panel.initialize() src << browse(file('html/statbrowser.html'), "window=statbrowser") addtimer(CALLBACK(src, .proc/check_panel_loaded), 30 SECONDS) + tgui_panel.initialize() - if(alert_mob_dupe_login) - spawn() - alert(mob, "You have logged in already with another key this round, please log out of this one NOW or risk being banned!") + if(alert_mob_dupe_login && !holder) + var/dupe_login_message = "Your ComputerID has already logged in with another key this round, please log out of this one NOW or risk being banned!" + // if (alert_admin_multikey) + // dupe_login_message += "\nAdmins have been informed." + // message_admins(span_danger("MULTIKEYING: [key_name_admin(src)] has a matching CID+IP with another player and is clearly multikeying. They have been warned to leave the server or risk getting banned.")) + // log_admin_private("MULTIKEYING: [key_name(src)] has a matching CID+IP with another player and is clearly multikeying. They have been warned to leave the server or risk getting banned.") + spawn(0.5 SECONDS) //needs to run during world init, do not convert to add timer + alert(mob, dupe_login_message) //players get banned if they don't see this message, do not convert to tgui_alert (or even tg_alert) please. + to_chat(mob, span_danger(dupe_login_message)) connection_time = world.time connection_realtime = world.realtime @@ -1040,18 +1043,27 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(prefs && prefs.chat_toggles & CHAT_PULLR) to_chat(src, announcement) -/client/proc/show_character_previews(mutable_appearance/MA) +/client/proc/show_character_previews(mutable_appearance/source) + LAZYINITLIST(char_render_holders) + if(!LAZYLEN(char_render_holders)) + for(var/plane_master_path as anything in subtypesof(/atom/movable/screen/plane_master)) + var/atom/movable/screen/plane_master/plane_master = new plane_master_path() + char_render_holders["plane_master-[plane_master.plane]"] = plane_master + plane_master.backdrop(mob) + screen |= plane_master + plane_master.screen_loc = "character_preview_map:0,CENTER" + var/pos = 0 - for(var/D in GLOB.cardinals) + for(var/dir in GLOB.cardinals) pos++ - var/atom/movable/screen/O = LAZYACCESS(char_render_holders, "[D]") - if(!O) - O = new - LAZYSET(char_render_holders, "[D]", O) - screen |= O - O.appearance = MA - O.dir = D - O.screen_loc = "character_preview_map:0,[pos]" + var/atom/movable/screen/preview = char_render_holders["preview-[dir]"] + if(!preview) + preview = new + char_render_holders["preview-[dir]"] = preview + screen |= preview + preview.appearance = source + preview.dir = dir + preview.screen_loc = "character_preview_map:0,[pos]" /client/proc/clear_character_previews() for(var/index in char_render_holders) @@ -1087,8 +1099,14 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if(IsAdminAdvancedProcCall()) return var/list/verblist = list() - verb_tabs.Cut() - for(var/thing in (verbs + mob?.verbs)) + var/list/verbstoprocess = verbs.Copy() + if(mob) + verbstoprocess += mob.verbs + for(var/AM in mob.contents) + var/atom/movable/thing = AM + verbstoprocess += thing.verbs + panel_tabs.Cut() // panel_tabs get reset in init_verbs on JS side anyway + for(var/thing in verbstoprocess) var/procpath/verb_to_init = thing if(!verb_to_init) continue @@ -1096,9 +1114,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( continue if(!istext(verb_to_init.category)) continue - verb_tabs |= verb_to_init.category + panel_tabs |= verb_to_init.category verblist[++verblist.len] = list(verb_to_init.category, verb_to_init.name) - src << output("[url_encode(json_encode(verb_tabs))];[url_encode(json_encode(verblist))]", "statbrowser:init_verbs") + src << output("[url_encode(json_encode(panel_tabs))];[url_encode(json_encode(verblist))]", "statbrowser:init_verbs") /client/proc/check_panel_loaded() if(statbrowser_ready) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index c75c2ea9a8..7b63a63b4c 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -61,7 +61,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/UI_style = null var/outline_enabled = TRUE - var/outline_color = COLOR_BLUE_GRAY + var/outline_color = COLOR_THEME_MIDNIGHT var/buttons_locked = FALSE var/hotkeys = FALSE @@ -821,7 +821,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "

General Settings

" dat += "UI Style: [UI_style]
" dat += "Outline: [outline_enabled ? "Enabled" : "Disabled"]
" - dat += "Outline Color:     Change
" + dat += "Outline Color: [outline_color ? "" : "Theme-based (null)"]    Change
" dat += "tgui Monitors: [(tgui_lock) ? "Primary" : "All"]
" dat += "tgui Style: [(tgui_fancy) ? "Fancy" : "No Frills"]
" dat += "Show Runechat Chat Bubbles: [chat_on_map ? "Enabled" : "Disabled"]
" @@ -2907,8 +2907,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) outline_enabled = !outline_enabled if("outline_color") var/pickedOutlineColor = input(user, "Choose your outline color.", "General Preference", outline_color) as color|null - if(pickedOutlineColor) - outline_color = pickedOutlineColor + if(pickedOutlineColor != pickedOutlineColor) + outline_color = pickedOutlineColor // nullable if("tgui_lock") tgui_lock = !tgui_lock if("winflash") diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index ee52c1ad8d..00ffb8cf97 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -47,7 +47,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car addtimer(CALLBACK(src, .proc/force_reset_keybindings), 30) //No mob available when this is run, timer allows user choice. if(current_version < 30) outline_enabled = TRUE - outline_color = COLOR_BLUE_GRAY + outline_color = COLOR_THEME_MIDNIGHT /datum/preferences/proc/update_character(current_version, savefile/S) if(current_version < 19) diff --git a/code/modules/client/verbs/etips.dm b/code/modules/client/verbs/etips.dm index d4ebc899ae..7452a98317 100644 --- a/code/modules/client/verbs/etips.dm +++ b/code/modules/client/verbs/etips.dm @@ -18,3 +18,12 @@ prefs.tip_delay = max(indelay, 0.01) prefs.save_preferences() to_chat(usr, "Tooltip delay set to [indelay] milliseconds.") + +/client/verb/toggle_hover_outline() + set name = "Toggle hover outline" + set desc = "Toggles hover-over item outline" + set category = "Preferences" + + prefs.outline_enabled = !prefs.outline_enabled + prefs.save_preferences() + to_chat(usr, "Item outline [prefs.outline_enabled ? "en" : "dis"]abled.") diff --git a/code/modules/clothing/spacesuits/hardsuit.dm b/code/modules/clothing/spacesuits/hardsuit.dm index 33204f377b..18138bf4b6 100644 --- a/code/modules/clothing/spacesuits/hardsuit.dm +++ b/code/modules/clothing/spacesuits/hardsuit.dm @@ -20,12 +20,16 @@ /obj/item/clothing/head/helmet/space/hardsuit/Initialize() . = ..() - soundloop = new(list(), FALSE, TRUE) + soundloop = new(src, FALSE, TRUE) soundloop.volume = 5 START_PROCESSING(SSobj, src) /obj/item/clothing/head/helmet/space/hardsuit/Destroy() . = ..() + if(!QDELETED(suit)) + qdel(suit) + suit = null + QDEL_NULL(soundloop) STOP_PROCESSING(SSobj, src) /obj/item/clothing/head/helmet/space/hardsuit/attack_self(mob/user) diff --git a/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm b/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm index ebde504ec0..a41970ea2b 100644 --- a/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm +++ b/code/modules/food_and_drinks/kitchen_machinery/deep_fryer.dm @@ -57,7 +57,11 @@ God bless America. component_parts += new /obj/item/circuitboard/machine/deep_fryer(null) component_parts += new /obj/item/stock_parts/micro_laser(null) RefreshParts() - fry_loop = new(list(src), FALSE) + fry_loop = new(src, FALSE) + +/obj/machinery/deepfryer/Destroy() + QDEL_NULL(fry_loop) + return ..() /obj/machinery/deepfryer/RefreshParts() var/oil_efficiency diff --git a/code/modules/food_and_drinks/kitchen_machinery/grill.dm b/code/modules/food_and_drinks/kitchen_machinery/grill.dm index 09e1d7b1c6..02b24402db 100644 --- a/code/modules/food_and_drinks/kitchen_machinery/grill.dm +++ b/code/modules/food_and_drinks/kitchen_machinery/grill.dm @@ -13,15 +13,21 @@ /obj/machinery/grill/Initialize() . = ..() - grill_loop = new(list(src), FALSE) + grill_loop = new(src, FALSE) + +/obj/machinery/grill/Destroy() + QDEL_NULL(grill_loop) + return ..() /obj/machinery/grill/update_icon_state() if(grilled_item) icon_state = "grill" - else if(grill_fuel) + return ..() + if(grill_fuel > 0) icon_state = "grill_on" - else - icon_state = "grill_open" + return ..() + icon_state = "grill_open" + return ..() /obj/machinery/grill/attackby(obj/item/I, mob/user) if(istype(I, /obj/item/stack/sheet/mineral/coal) || istype(I, /obj/item/stack/sheet/mineral/wood)) @@ -60,21 +66,21 @@ return ..() -/obj/machinery/grill/process() +/obj/machinery/grill/process(delta_time) ..() - update_icon() - if(!grill_fuel) + update_appearance() + if(grill_fuel <= 0) return else - grill_fuel -= 1 - if(prob(1)) + grill_fuel -= 0.5 * delta_time + if(DT_PROB(0.5, delta_time)) var/datum/effect_system/smoke_spread/bad/smoke = new smoke.set_up(1, loc) smoke.start() if(grilled_item) - grill_time += 1 + grill_time += delta_time grilled_item.reagents.add_reagent("char", 1) - grill_fuel -= 10 + grill_fuel -= 5 * delta_time grilled_item.AddComponent(/datum/component/sizzle) /obj/machinery/grill/Exited(atom/movable/AM) @@ -109,9 +115,9 @@ /obj/machinery/grill/on_attack_hand(mob/user, act_intent = user.a_intent, unarmed_attack_flags) if(grilled_item) - to_chat(user, "You take out [grilled_item] from [src].") + to_chat(user, span_notice("You take out [grilled_item] from [src].")) grilled_item.forceMove(drop_location()) - update_icon() + update_appearance() return return ..() diff --git a/code/modules/food_and_drinks/kitchen_machinery/microwave.dm b/code/modules/food_and_drinks/kitchen_machinery/microwave.dm index 5954788d5c..c4ac4c2dcf 100644 --- a/code/modules/food_and_drinks/kitchen_machinery/microwave.dm +++ b/code/modules/food_and_drinks/kitchen_machinery/microwave.dm @@ -36,12 +36,13 @@ . = ..() wires = new /datum/wires/microwave(src) create_reagents(100) - soundloop = new(list(src), FALSE) + soundloop = new(src, FALSE) /obj/machinery/microwave/Destroy() eject() if(wires) QDEL_NULL(wires) + QDEL_NULL(soundloop) . = ..() /obj/machinery/microwave/RefreshParts() diff --git a/code/modules/holiday/halloween/jacqueen.dm b/code/modules/holiday/halloween/jacqueen.dm index 0f37e41f11..c20b99d684 100644 --- a/code/modules/holiday/halloween/jacqueen.dm +++ b/code/modules/holiday/halloween/jacqueen.dm @@ -154,7 +154,7 @@ pop_areas += A var/list/targets = list() - for(var/H in GLOB.network_holopads) + for(var/H in GLOB.the_station_areas) var/area/A = get_area(H) if(!A || findtextEx(A.name, "AI") || !(A in pop_areas) || !is_station_level(H)) continue diff --git a/code/modules/integrated_electronics/core/detailer.dm b/code/modules/integrated_electronics/core/detailer.dm index 653992c0d3..b48cc74769 100644 --- a/code/modules/integrated_electronics/core/detailer.dm +++ b/code/modules/integrated_electronics/core/detailer.dm @@ -26,7 +26,7 @@ "light blue" = COLOR_ASSEMBLY_LBLUE, "blue" = COLOR_ASSEMBLY_BLUE, "purple" = COLOR_ASSEMBLY_PURPLE, - "pink" = COLOR_ASSEMBLY_PINK, + "pink" = LIGHT_COLOR_PINK, "custom" = COLOR_ASSEMBLY_WHITE ) diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index 2e530226c8..a3c6294aec 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -115,13 +115,13 @@ . = ..() if(!.) return - if(HAS_TRAIT(src, TRAIT_NOGUNS)) - to_chat(src, "You can't bring yourself to use a ranged weapon!") - return FALSE if(G.trigger_guard == TRIGGER_GUARD_NORMAL) if(HAS_TRAIT(src, TRAIT_CHUNKYFINGERS)) - to_chat(src, "Your meaty finger is much too large for the trigger guard!") + balloon_alert(src, "fingers are too big!") return FALSE + if(HAS_TRAIT(src, TRAIT_NOGUNS)) + to_chat(src, span_warning("You can't bring yourself to use a ranged weapon!")) + return FALSE /mob/living/carbon/human/proc/get_bank_account() RETURN_TYPE(/datum/bank_account) diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 8f74d3b45f..7d2daac326 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -1,4 +1,31 @@ +/** + * Run when a client is put in this mob or reconnets to byond and their client was on this mob + * + * Things it does: + * * Adds player to player_list + * * sets lastKnownIP + * * sets computer_id + * * logs the login + * * tells the world to update it's status (for player count) + * * create mob huds for the mob if needed + * * reset next_move to 1 + * * parent call + * * if the client exists set the perspective to the mob loc + * * call on_log on the loc (sigh) + * * reload the huds for the mob + * * reload all full screen huds attached to this mob + * * load any global alternate apperances + * * sync the mind datum via sync_mind() + * * call any client login callbacks that exist + * * grant any actions the mob has to the client + * * calls [auto_deadmin_on_login](mob.html#proc/auto_deadmin_on_login) + * * send signal COMSIG_MOB_CLIENT_LOGIN + * * attaches the ash listener element so clients can hear weather + * client can be deleted mid-execution of this proc, chiefly on parent calls, with lag + */ /mob/Login() + if(!client) + return FALSE add_to_player_list() lastKnownIP = client.address computer_id = client.computer_id @@ -15,6 +42,14 @@ . = ..() + if(!client) + return FALSE + + // SEND_SIGNAL(src, COMSIG_MOB_LOGIN) + + if (key != client.key) + key = client.key + reset_perspective(loc) if(loc) @@ -53,6 +88,12 @@ log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP) SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client) + client.init_verbs() if(has_field_of_vision && CONFIG_GET(flag/use_field_of_vision)) LoadComponent(/datum/component/field_of_vision, field_of_vision_type) + + AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + + // optimized area sound effects. Enable during events (compile flag when 😳) + // AddElement(/datum/element/weather_listener, /datum/weather/long_rain, ZTRAIT_STATION, GLOB.rain_sounds) diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index 2e2eeeb4fb..d26d562a46 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -4,7 +4,6 @@ SStgui.on_logout(src) unset_machine() remove_from_player_list() - ..() if(loc) diff --git a/code/modules/modular_computers/NTNet/NTNRC/conversation.dm b/code/modules/modular_computers/NTNet/NTNRC/conversation.dm index b5f3bae53d..fada2229e5 100644 --- a/code/modules/modular_computers/NTNet/NTNRC/conversation.dm +++ b/code/modules/modular_computers/NTNet/NTNRC/conversation.dm @@ -5,7 +5,14 @@ var/title = "Untitled Conversation" var/datum/computer_file/program/chatclient/operator // "Administrator" of this channel. Creator starts as channel's operator, var/list/messages = list() - var/list/clients = list() + ///chat clients who are active or minimized + var/list/active_clients = list() + ///chat clients who have exited out of the program. + var/list/offline_clients = list() + ///clients muted by operator + var/list/muted_clients = list() + //if a channel is strong, it cannot be renamed or deleted. + var/strong = FALSE var/password var/static/ntnrc_uid = 0 @@ -22,6 +29,8 @@ /datum/ntnet_conversation/Destroy() if(SSnetworks.station_network) SSnetworks.station_network.chat_channels.Remove(src) + for(var/datum/computer_file/program/chatclient/chatterbox in (active_clients | offline_clients | muted_clients)) + purge_client(chatterbox) return ..() /datum/ntnet_conversation/proc/add_message(message, username) @@ -38,39 +47,70 @@ return messages = messages.Copy(messages.len-50 ,0) -/datum/ntnet_conversation/proc/add_client(datum/computer_file/program/chatclient/C) - if(!istype(C)) +/datum/ntnet_conversation/proc/add_client(datum/computer_file/program/chatclient/new_user, silent = FALSE) + if(!istype(new_user)) return - clients.Add(C) - add_status_message("[C.username] has joined the channel.") + new_user.conversations |= src + active_clients.Add(new_user) + if(!silent) + add_status_message("[new_user.username] has joined the channel.") // No operator, so we assume the channel was empty. Assign this user as operator. if(!operator) - changeop(C) + changeop(new_user) -/datum/ntnet_conversation/proc/remove_client(datum/computer_file/program/chatclient/C) - if(!istype(C) || !(C in clients)) +//Clear all of our references to a client, used for client deletion +/datum/ntnet_conversation/proc/purge_client(datum/computer_file/program/chatclient/forget) + remove_client(forget) + muted_clients -= forget + offline_clients -= forget + forget.conversations -= src + +/datum/ntnet_conversation/proc/remove_client(datum/computer_file/program/chatclient/leaving) + if(!istype(leaving)) return - clients.Remove(C) - add_status_message("[C.username] has left the channel.") + if(leaving in active_clients) + active_clients.Remove(leaving) + add_status_message("[leaving.username] has left the channel.") // Channel operator left, pick new operator - if(C == operator) + if(leaving == operator) operator = null - if(clients.len) - var/datum/computer_file/program/chatclient/newop = pick(clients) + if(active_clients.len) + var/datum/computer_file/program/chatclient/newop = pick(active_clients) changeop(newop) +/datum/ntnet_conversation/proc/go_offline(datum/computer_file/program/chatclient/offline) + if(!istype(offline) || !(offline in active_clients)) + return + active_clients.Remove(offline) + offline_clients.Add(offline) + +/datum/ntnet_conversation/proc/mute_user(datum/computer_file/program/chatclient/op, datum/computer_file/program/chatclient/muted) + if(operator != op) //sanity even if the person shouldn't be able to see the mute button + return + if(muted in muted_clients) + muted_clients.Remove(muted) + muted.computer.alert_call(muted, "You have been unmuted from [title]!", 'sound/machines/ping.ogg') + else + muted_clients.Add(muted) + muted.computer.alert_call(muted, "You have been muted from [title]!") + +/datum/ntnet_conversation/proc/ping_user(datum/computer_file/program/chatclient/pinger, datum/computer_file/program/chatclient/pinged) + if(pinger in muted_clients) //oh my god fuck off + return + add_status_message("[pinger.username] pinged [pinged.username].") + pinged.computer.alert_call(pinged, "You have been pinged in [title] by [pinger.username]!", 'sound/machines/ping.ogg') /datum/ntnet_conversation/proc/changeop(datum/computer_file/program/chatclient/newop) if(istype(newop)) operator = newop add_status_message("Channel operator status transferred to [newop.username].") -/datum/ntnet_conversation/proc/change_title(newtitle, datum/computer_file/program/chatclient/client) - if(operator != client) - return FALSE // Not Authorised +/datum/ntnet_conversation/proc/change_title(newtitle, datum/computer_file/program/chatclient/renamer) + if(operator != renamer || strong) + return FALSE // Not Authorised or channel cannot be editted - add_status_message("[client.username] has changed channel title from [title] to [newtitle]") + add_status_message("[renamer.username] has changed channel title from [title] to [newtitle]") title = newtitle #undef MAX_CHANNELS diff --git a/code/modules/modular_computers/computers/_modular_computer_shared.dm b/code/modules/modular_computers/computers/_modular_computer_shared.dm index 0aca92f9d8..1a2ba822d1 100644 --- a/code/modules/modular_computers/computers/_modular_computer_shared.dm +++ b/code/modules/modular_computers/computers/_modular_computer_shared.dm @@ -39,7 +39,7 @@ . += "It has a slot installed for an intelliCard which contains: [ai_slot.stored_card.name]" else . += "It has a slot installed for an intelliCard, which appears to be occupied." - . += "Alt-click to eject the intelliCard." + . += span_info("Alt-click to eject the intelliCard.") else . += "It has a slot installed for an intelliCard." @@ -55,7 +55,7 @@ . += "It has [multiple_slots ? "two slots" : "a slot"] for identification cards installed[multiple_cards ? " which contain [first_ID] and [second_ID]" : ", one of which contains [first_ID ? first_ID : second_ID]"]." else . += "It has [multiple_slots ? "two slots" : "a slot"] for identification cards installed, [multiple_cards ? "both of which appear" : "and one of them appears"] to be occupied." - . += "Alt-click [src] to eject the identification card[multiple_cards ? "s":""]." + . += span_info("Alt-click [src] to eject the identification card[multiple_cards ? "s":""].") else . += "It has [multiple_slots ? "two slots" : "a slot"] installed for identification cards." @@ -63,4 +63,4 @@ if(printer_slot) . += "It has a printer installed." if(user_is_adjacent) - . += "The printer's paper levels are at: [printer_slot.stored_paper]/[printer_slot.max_paper].
" + . += "The printer's paper levels are at: [printer_slot.stored_paper]/[printer_slot.max_paper].]" diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 635ec2e54c..1d90c3a651 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -12,13 +12,13 @@ rad_flags = RAD_PROTECT_CONTENTS armor = list("melee" = 0, "bullet" = 20, "laser" = 20, "energy" = 100, "bomb" = 0, "bio" = 100, "rad" = 100, "fire" = 0, "acid" = 0) - var/enabled = 0 // Whether the computer is turned on. - var/screen_on = 1 // Whether the computer is active/opened/it's screen is on. - var/device_theme = "ntos" // Sets the theme for the main menu, hardware config, and file browser apps. Overridden by certain non-NT devices. - var/datum/computer_file/program/active_program = null // A currently active program running on the computer. - var/hardware_flag = 0 // A flag that describes this device type + var/enabled = 0 // Whether the computer is turned on. + var/screen_on = 1 // Whether the computer is active/opened/it's screen is on. + var/device_theme = "ntos" // Sets the theme for the main menu, hardware config, and file browser apps. Overridden by certain non-NT devices. + var/datum/computer_file/program/active_program = null // A currently active program running on the computer. + var/hardware_flag = 0 // A flag that describes this device type var/last_power_usage = 0 - var/last_battery_percent = 0 // Used for deciding if battery percentage has chandged + var/last_battery_percent = 0 // Used for deciding if battery percentage has chandged var/last_world_time = "00:00" var/list/last_header_icons ///Looping sound for when the computer is on @@ -26,19 +26,19 @@ ///Whether or not this modular computer uses the looping sound var/looping_sound = TRUE - var/base_active_power_usage = 50 // Power usage when the computer is open (screen is active) and can be interacted with. Remember hardware can use power too. - var/base_idle_power_usage = 5 // Power usage when the computer is idle and screen is off (currently only applies to laptops) + var/base_active_power_usage = 50 // Power usage when the computer is open (screen is active) and can be interacted with. Remember hardware can use power too. + var/base_idle_power_usage = 5 // Power usage when the computer is idle and screen is off (currently only applies to laptops) // Modular computers can run on various devices. Each DEVICE (Laptop, Console, Tablet,..) // must have it's own DMI file. Icon states must be called exactly the same in all files, but may look differently // If you create a program which is limited to Laptops and Consoles you don't have to add it's icon_state overlay for Tablets too, for example. - var/icon_state_unpowered = null // Icon state when the computer is turned off. - var/icon_state_powered = null // Icon state when the computer is turned on. - var/icon_state_menu = "menu" // Icon state overlay when the computer is turned on, but no program is loaded that would override the screen. - var/display_overlays = TRUE // If FALSE, don't draw overlays on this device at all - var/max_hardware_size = 0 // Maximal hardware w_class. Tablets/PDAs have 1, laptops 2, consoles 4. - var/steel_sheet_cost = 5 // Amount of steel sheets refunded when disassembling an empty frame of this computer. + var/icon_state_unpowered = null // Icon state when the computer is turned off. + var/icon_state_powered = null // Icon state when the computer is turned on. + var/icon_state_menu = "menu" // Icon state overlay when the computer is turned on, but no program is loaded that would override the screen. + var/display_overlays = TRUE // If FALSE, don't draw overlays on this device at all + var/max_hardware_size = 0 // Maximal hardware w_class. Tablets/PDAs have 1, laptops 2, consoles 4. + var/steel_sheet_cost = 5 // Amount of steel sheets refunded when disassembling an empty frame of this computer. /// List of "connection ports" in this computer and the components with which they are plugged var/list/all_components = list() @@ -47,11 +47,11 @@ /// Number of total expansion bays this computer has available. var/max_bays = 0 - var/list/idle_threads // Idle programs on background. They still receive process calls but can't be interacted with. - var/obj/physical = null // Object that represents our computer. It's used for Adjacent() and UI visibility checks. - var/has_light = FALSE //If the computer has a flashlight/LED light/what-have-you installed - var/comp_light_luminosity = 3 //The brightness of that light - var/comp_light_color //The color of that light + var/list/idle_threads // Idle programs on background. They still receive process calls but can't be interacted with. + var/obj/physical = null // Object that represents our computer. It's used for Adjacent() and UI visibility checks. + var/has_light = FALSE //If the computer has a flashlight/LED light/what-have-you installed + var/comp_light_luminosity = 3 //The brightness of that light + var/comp_light_color //The color of that light /obj/item/modular_computer/Initialize() @@ -62,13 +62,12 @@ comp_light_color = "#FFFFFF" idle_threads = list() if(looping_sound) - soundloop = new(list(src), enabled) - update_icon() + soundloop = new(src, enabled) + update_appearance() /obj/item/modular_computer/Destroy() kill_program(forced = TRUE) STOP_PROCESSING(SSobj, src) - QDEL_NULL(soundloop) for(var/H in all_components) var/obj/item/computer_hardware/CH = all_components[H] if(CH.holder == src) @@ -76,9 +75,19 @@ CH.holder = null all_components.Remove(CH.device_type) qdel(CH) + //Some components will actually try and interact with this, so let's do it later + QDEL_NULL(soundloop) physical = null return ..() +/** + * Plays a ping sound. + * + * Timers runtime if you try to make them call playsound. Yep. + */ +/obj/item/modular_computer/proc/play_ping() + playsound(loc, 'sound/machines/ping.ogg', get_clamped_volume(), FALSE, -1) + /obj/item/modular_computer/AltClick(mob/user) ..() if(issilicon(user)) @@ -98,14 +107,34 @@ /obj/item/modular_computer/GetID() var/obj/item/computer_hardware/card_slot/card_slot = all_components[MC_CARD] - if(card_slot) - return card_slot.GetID() + var/obj/item/computer_hardware/card_slot/card_slot2 = all_components[MC_CARD2] + + var/obj/item/card/id/first_id = card_slot?.GetID() + var/obj/item/card/id/second_id = card_slot2?.GetID() + + // If we don't have both ID slots filled, pick the one that is filled. + if(first_id) + return first_id + if(second_id) + return second_id + + // Otherwise, we have no ID at all. return ..() /obj/item/modular_computer/RemoveID() var/obj/item/computer_hardware/card_slot/card_slot2 = all_components[MC_CARD2] var/obj/item/computer_hardware/card_slot/card_slot = all_components[MC_CARD] - return (card_slot2?.try_eject() || card_slot?.try_eject()) //Try the secondary one first. + + var/removed_id = (card_slot2?.try_eject() || card_slot?.try_eject()) + if(removed_id) + if(ishuman(loc)) + var/mob/living/carbon/human/human_wearer = loc + if(human_wearer.wear_id == src) + human_wearer.sec_hud_set_ID() + update_slot_icon() + return removed_id + + return ..() /obj/item/modular_computer/InsertID(obj/item/inserting_item) var/obj/item/computer_hardware/card_slot/card_slot = all_components[MC_CARD] @@ -118,7 +147,13 @@ return FALSE if((card_slot?.try_insert(inserting_id)) || (card_slot2?.try_insert(inserting_id))) + if(ishuman(loc)) + var/mob/living/carbon/human/human_wearer = loc + if(human_wearer.wear_id == src) + human_wearer.sec_hud_set_ID() + update_slot_icon() return TRUE + return FALSE /obj/item/modular_computer/MouseDrop(obj/over_object, src_location, over_location) @@ -142,9 +177,8 @@ turn_on(user) /obj/item/modular_computer/emag_act(mob/user) - . = ..() if(!enabled) - to_chat(user, "You'd need to turn the [src] on first.") + to_chat(user, span_warning("You'd need to turn the [src] on first.")) return FALSE obj_flags |= EMAGGED //Mostly for consistancy purposes; the programs will do their own emag handling var/newemag = FALSE @@ -155,36 +189,31 @@ if(app.run_emag()) newemag = TRUE if(newemag) - to_chat(user, "You swipe \the [src]. A console window momentarily fills the screen, with white text rapidly scrolling past.") + to_chat(user, span_notice("You swipe \the [src]. A console window momentarily fills the screen, with white text rapidly scrolling past.")) return TRUE - to_chat(user, "You swipe \the [src]. A console window fills the screen, but it quickly closes itself after only a few lines are written to it.") + to_chat(user, span_notice("You swipe \the [src]. A console window fills the screen, but it quickly closes itself after only a few lines are written to it.")) return FALSE /obj/item/modular_computer/examine(mob/user) . = ..() if(obj_integrity <= integrity_failure * max_integrity) - . += "It is heavily damaged!" + . += span_danger("It is heavily damaged!") else if(obj_integrity < max_integrity) - . += "It is damaged." + . += span_warning("It is damaged.") . += get_modular_computer_parts_examine(user) /obj/item/modular_computer/update_icon_state() - if(!enabled) - icon_state = icon_state_unpowered - else - icon_state = icon_state_powered + icon_state = enabled ? icon_state_powered : icon_state_unpowered + return ..() /obj/item/modular_computer/update_overlays() . = ..() if(!display_overlays) return - if(enabled) - if(active_program) - . += active_program.program_icon_state ? active_program.program_icon_state : icon_state_menu - else - . += icon_state_menu + if(enabled) + . += active_program?.program_icon_state || icon_state_menu if(obj_integrity <= integrity_failure * max_integrity) . += "bsod" . += "broken" @@ -201,9 +230,9 @@ var/issynth = issilicon(user) // Robots and AIs get different activation messages. if(obj_integrity <= integrity_failure * max_integrity) if(issynth) - to_chat(user, "You send an activation signal to \the [src], but it responds with an error code. It must be damaged.") + to_chat(user, span_warning("You send an activation signal to \the [src], but it responds with an error code. It must be damaged.")) else - to_chat(user, "You press the power button, but the computer fails to boot up, displaying variety of errors before shutting down again.") + to_chat(user, span_warning("You press the power button, but the computer fails to boot up, displaying variety of errors before shutting down again.")) return FALSE // If we have a recharger, enable it automatically. Lets computer without a battery work. @@ -213,20 +242,20 @@ if(all_components[MC_CPU] && use_power()) // use_power() checks if the PC is powered if(issynth) - to_chat(user, "You send an activation signal to \the [src], turning it on.") + to_chat(user, span_notice("You send an activation signal to \the [src], turning it on.")) else - to_chat(user, "You press the power button and start up \the [src].") + to_chat(user, span_notice("You press the power button and start up \the [src].")) if(looping_sound) soundloop.start() enabled = 1 - update_icon() + update_appearance() ui_interact(user) return TRUE else // Unpowered if(issynth) - to_chat(user, "You send an activation signal to \the [src] but it does not respond.") + to_chat(user, span_warning("You send an activation signal to \the [src] but it does not respond.")) else - to_chat(user, "You press the power button but \the [src] does not respond.") + to_chat(user, span_warning("You press the power button but \the [src] does not respond.")) return FALSE // Process currently calls handle_power(), may be expanded in future if more things are added. @@ -282,10 +311,10 @@ if(!caller || !caller.alert_able || caller.alert_silenced || !alerttext) //Yeah, we're checking alert_able. No, you don't get to make alerts that the user can't silence. return playsound(src, sound, 50, TRUE) - visible_message("The [src] displays a [caller.filedesc] notification: [alerttext]") + visible_message(span_notice("The [src] displays a [caller.filedesc] notification: [alerttext]")) var/mob/living/holder = loc if(istype(holder)) - to_chat(holder, "[icon2html(src)] The [src] displays a [caller.filedesc] notification: [alerttext]") + to_chat(holder, "[icon2html(src)] [span_notice("The [src] displays a [caller.filedesc] notification: [alerttext]")]") // Function used by NanoUI's to obtain data for header. All relevant entries begin with "PC_" /obj/item/modular_computer/proc/get_header_data() @@ -349,14 +378,13 @@ // Relays kill program request to currently active program. Use this to quit current program. /obj/item/modular_computer/proc/kill_program(forced = FALSE) - set waitfor = FALSE if(active_program) active_program.kill_program(forced) active_program = null var/mob/user = usr if(user && istype(user)) ui_interact(user) // Re-open the UI on this computer. It should show the main screen now. - update_icon() + update_appearance() // Returns 0 for No Signal, 1 for Low Signal and 2 for Good Signal. 3 is for wired connection (always-on) /obj/item/modular_computer/proc/get_ntnet_status(specific_action = 0) @@ -370,6 +398,7 @@ if(!get_ntnet_status()) return FALSE var/obj/item/computer_hardware/network_card/network_card = all_components[MC_NET] + return SSnetworks.station_network.add_log(text, network_card) /obj/item/modular_computer/proc/shutdown_computer(loud = 1) @@ -380,9 +409,9 @@ if(looping_sound) soundloop.stop() if(loud) - physical.visible_message("\The [src] shuts down.") + physical.visible_message(span_notice("\The [src] shuts down.")) enabled = 0 - update_icon() + update_appearance() /** * Toggles the computer's flashlight, if it has one. @@ -412,13 +441,13 @@ if(!has_light || !color) return FALSE comp_light_color = color - // set_light_color(color) + set_light_color(color) update_light() return TRUE /obj/item/modular_computer/screwdriver_act(mob/user, obj/item/tool) if(!all_components.len) - to_chat(user, "This device doesn't have any components installed.") + to_chat(user, span_warning("This device doesn't have any components installed.")) return var/list/component_names = list() for(var/h in all_components) @@ -460,28 +489,34 @@ if(W.tool_behaviour == TOOL_WRENCH) if(all_components.len) - to_chat(user, "Remove all components from \the [src] before disassembling it.") + to_chat(user, span_warning("Remove all components from \the [src] before disassembling it.")) return new /obj/item/stack/sheet/metal( get_turf(src.loc), steel_sheet_cost ) - physical.visible_message("\The [src] is disassembled by [user].") + physical.visible_message(span_notice("\The [src] is disassembled by [user].")) relay_qdel() qdel(src) return if(W.tool_behaviour == TOOL_WELDER) if(obj_integrity == max_integrity) - to_chat(user, "\The [src] does not require repairs.") + to_chat(user, span_warning("\The [src] does not require repairs.")) return if(!W.tool_start_check(user, amount=1)) return - to_chat(user, "You begin repairing damage to \the [src]...") + to_chat(user, span_notice("You begin repairing damage to \the [src]...")) if(W.use_tool(src, user, 20, volume=50, amount=1)) obj_integrity = max_integrity - to_chat(user, "You repair \the [src].") + to_chat(user, span_notice("You repair \the [src].")) return + var/obj/item/computer_hardware/card_slot/card_slot = all_components[MC_CARD] + // Check to see if we have an ID inside, and a valid input for money + if(card_slot?.GetID() && iscash(W)) + var/obj/item/card/id/id = card_slot.GetID() + id.attackby(W, user) // If we do, try and put that attacking object in + return ..() // Used by processor to relay qdel() to machinery type. diff --git a/code/modules/modular_computers/computers/item/computer_components.dm b/code/modules/modular_computers/computers/item/computer_components.dm index 8668b279cf..91c369581f 100644 --- a/code/modules/modular_computers/computers/item/computer_components.dm +++ b/code/modules/modular_computers/computers/item/computer_components.dm @@ -3,19 +3,19 @@ return FALSE if(H.w_class > max_hardware_size) - to_chat(user, "This component is too large for \the [src]!") + to_chat(user, span_warning("This component is too large for \the [src]!")) return FALSE if(H.expansion_hw) if(LAZYLEN(expansion_bays) >= max_bays) - to_chat(user, "All of the computer's expansion bays are filled.") + to_chat(user, span_warning("All of the computer's expansion bays are filled.")) return FALSE if(LAZYACCESS(expansion_bays, H.device_type)) - to_chat(user, "The computer immediately ejects /the [H] and flashes an error: \"Hardware Address Conflict\".") + to_chat(user, span_warning("The computer immediately ejects /the [H] and flashes an error: \"Hardware Address Conflict\".")) return FALSE if(all_components[H.device_type]) - to_chat(user, "This computer's hardware slot is already occupied by \the [all_components[H.device_type]].") + to_chat(user, span_warning("This computer's hardware slot is already occupied by \the [all_components[H.device_type]].")) return FALSE return TRUE @@ -32,7 +32,7 @@ LAZYSET(expansion_bays, H.device_type, H) all_components[H.device_type] = H - to_chat(user, "You install \the [H] into \the [src].") + to_chat(user, span_notice("You install \the [H] into \the [src].")) H.holder = src H.forceMove(src) H.on_install(src, user) @@ -47,14 +47,14 @@ LAZYREMOVE(expansion_bays, H.device_type) all_components.Remove(H.device_type) - to_chat(user, "You remove \the [H] from \the [src].") + to_chat(user, span_notice("You remove \the [H] from \the [src].")) H.forceMove(get_turf(src)) H.holder = null H.on_remove(src, user) if(enabled && !use_power()) shutdown_computer() - update_icon() + update_appearance() return TRUE diff --git a/code/modules/modular_computers/computers/item/computer_damage.dm b/code/modules/modular_computers/computers/item/computer_damage.dm index b510f8aded..9053aebcd5 100644 --- a/code/modules/modular_computers/computers/item/computer_damage.dm +++ b/code/modules/modular_computers/computers/item/computer_damage.dm @@ -18,7 +18,7 @@ /obj/item/modular_computer/proc/break_apart() if(!(flags_1 & NODECONSTRUCT_1)) - physical.visible_message("\The [src] breaks apart!") + physical.visible_message(span_notice("\The [src] breaks apart!")) var/turf/newloc = get_turf(src) new /obj/item/stack/sheet/metal(newloc, round(steel_sheet_cost/2)) for(var/C in all_components) diff --git a/code/modules/modular_computers/computers/item/computer_power.dm b/code/modules/modular_computers/computers/item/computer_power.dm index 92d4a812a2..f31c3c82cc 100644 --- a/code/modules/modular_computers/computers/item/computer_power.dm +++ b/code/modules/modular_computers/computers/item/computer_power.dm @@ -16,7 +16,7 @@ if(cell.use(amount * GLOB.CELLRATE)) return TRUE else // Discharge the cell anyway. - cell.use(min(amount*GLOB.CELLRATE, cell.charge)) + cell.use(min(amount * GLOB.CELLRATE, cell.charge)) return FALSE return FALSE diff --git a/code/modules/modular_computers/computers/item/computer_ui.dm b/code/modules/modular_computers/computers/item/computer_ui.dm index d9d9e5c876..93324bffe8 100644 --- a/code/modules/modular_computers/computers/item/computer_ui.dm +++ b/code/modules/modular_computers/computers/item/computer_ui.dm @@ -13,6 +13,10 @@ ui.close() return + // if(HAS_TRAIT(user, TRAIT_CHUNKYFINGERS)) + // to_chat(user, span_warning("Your fingers are too big to use this right now!")) + // return + // Robots don't really need to see the screen, their wireless connection works as long as computer is on. if(!screen_on && !issilicon(user)) if(ui) @@ -30,7 +34,7 @@ // This screen simply lists available programs and user may select them. var/obj/item/computer_hardware/hard_drive/hard_drive = all_components[MC_HDD] if(!hard_drive || !hard_drive.stored_files || !hard_drive.stored_files.len) - to_chat(user, "\The [src] beeps three times, it's screen displaying a \"DISK ERROR\" warning.") + to_chat(user, span_danger("\The [src] beeps three times, it's screen displaying a \"DISK ERROR\" warning.")) return // No HDD, No HDD files list or no stored files. Something is very broken. ui = SStgui.try_update_ui(user, src, ui) @@ -111,15 +115,9 @@ active_program.program_state = PROGRAM_STATE_BACKGROUND // Should close any existing UIs active_program = null - update_icon() + update_appearance() if(user && istype(user)) ui_interact(user) // Re-open the UI on this computer. It should show the main screen now. - if("eject_pen") - if(istype(src, /obj/item/modular_computer/tablet)) - var/obj/item/modular_computer/tablet/self = src - if(self.can_have_pen) - self.remove_pen() - return if("PC_killprogram") var/prog = params["name"] @@ -132,7 +130,7 @@ return P.kill_program(forced = TRUE) - to_chat(user, "Program [P.filename].[P.filetype] with PID [rand(100,999)] has been killed.") + to_chat(user, span_notice("Program [P.filename].[P.filetype] with PID [rand(100,999)] has been killed.")) if("PC_runprogram") var/prog = params["name"] @@ -142,7 +140,7 @@ P = hard_drive.find_file_by_name(prog) if(!P || !istype(P)) // Program not found or it's not executable program. - to_chat(user, "\The [src]'s screen shows \"I/O ERROR - Unable to run program\" warning.") + to_chat(user, span_danger("\The [src]'s screen shows \"I/O ERROR - Unable to run program\" warning.")) return P.computer = src @@ -156,22 +154,22 @@ active_program = P P.alert_pending = FALSE idle_threads.Remove(P) - update_icon() + update_appearance() return var/obj/item/computer_hardware/processor_unit/PU = all_components[MC_CPU] if(idle_threads.len > PU.max_idle_programs) - to_chat(user, "\The [src] displays a \"Maximal CPU load reached. Unable to run another program.\" error.") + to_chat(user, span_danger("\The [src] displays a \"Maximal CPU load reached. Unable to run another program.\" error.")) return if(P.requires_ntnet && !get_ntnet_status(P.requires_ntnet_feature)) // The program requires NTNet connection, but we are not connected to NTNet. - to_chat(user, "\The [src]'s screen shows \"Unable to connect to NTNet. Please retry. If problem persists contact your system administrator.\" warning.") + to_chat(user, span_danger("\The [src]'s screen shows \"Unable to connect to NTNet. Please retry. If problem persists contact your system administrator.\" warning.")) return if(P.run_program(user)) active_program = P P.alert_pending = FALSE - update_icon() + update_appearance() return 1 if("PC_toggle_light") @@ -185,7 +183,7 @@ if(!new_color) return if(color_hex2num(new_color) < 200) //Colors too dark are rejected - to_chat(user, "That color is too dark! Choose a lighter one.") + to_chat(user, span_warning("That color is too dark! Choose a lighter one.")) new_color = null return set_flashlight_color(new_color) diff --git a/code/modules/modular_computers/computers/item/laptop.dm b/code/modules/modular_computers/computers/item/laptop.dm index 5686dec8d0..b4669c72f8 100644 --- a/code/modules/modular_computers/computers/item/laptop.dm +++ b/code/modules/modular_computers/computers/item/laptop.dm @@ -17,8 +17,8 @@ // No running around with open laptops in hands. item_flags = SLOWS_WHILE_IN_HAND - screen_on = FALSE // Starts closed - var/start_open = TRUE // unless this var is set to 1 + screen_on = FALSE // Starts closed + var/start_open = TRUE // unless this var is set to 1 var/icon_state_closed = "laptop-closed" var/w_class_open = WEIGHT_CLASS_BULKY var/slowdown_open = TRUE @@ -44,15 +44,14 @@ /obj/item/modular_computer/laptop/update_icon_state() if(!screen_on) icon_state = icon_state_closed - else - . = ..() + return + return ..() /obj/item/modular_computer/laptop/update_overlays() - if(screen_on) - return ..() - else + if(!screen_on) cut_overlays() - icon_state = icon_state_closed + return + return ..() /obj/item/modular_computer/laptop/attack_self(mob/user) if(!screen_on) @@ -68,7 +67,8 @@ try_toggle_open(usr) /obj/item/modular_computer/laptop/MouseDrop(obj/over_object, src_location, over_location) - if(istype(over_object, /atom/movable/screen/inventory/hand) || over_object == usr) + . = ..() + if(istype(over_object, /atom/movable/screen/inventory/hand)) var/atom/movable/screen/inventory/hand/H = over_object var/mob/M = usr @@ -103,17 +103,17 @@ /obj/item/modular_computer/laptop/proc/toggle_open(mob/living/user=null) if(screen_on) - to_chat(user, "You close \the [src].") + to_chat(user, span_notice("You close \the [src].")) slowdown = initial(slowdown) w_class = initial(w_class) else - to_chat(user, "You open \the [src].") + to_chat(user, span_notice("You open \the [src].")) slowdown = slowdown_open w_class = w_class_open screen_on = !screen_on display_overlays = screen_on - update_icon() + update_appearance() diff --git a/code/modules/modular_computers/computers/item/processor.dm b/code/modules/modular_computers/computers/item/processor.dm index 970dc8bd1d..d569ad24fd 100644 --- a/code/modules/modular_computers/computers/item/processor.dm +++ b/code/modules/modular_computers/computers/item/processor.dm @@ -38,7 +38,7 @@ integrity_failure = machinery_computer.integrity_failure base_active_power_usage = machinery_computer.base_active_power_usage base_idle_power_usage = machinery_computer.base_idle_power_usage - machinery_computer.RegisterSignal(src, COMSIG_ATOM_UPDATED_ICON, /atom/proc/update_icon) //when we update_icon, also update the computer + machinery_computer.RegisterSignal(src, COMSIG_ATOM_UPDATED_ICON, /obj/machinery/modular_computer/proc/relay_icon_update) //when we update_icon, also update the computer /obj/item/modular_computer/processor/relay_qdel() qdel(machinery_computer) @@ -47,7 +47,7 @@ if(!machinery_computer) return ..() - machinery_computer.update_icon() + machinery_computer.update_appearance() return /obj/item/modular_computer/processor/attack_ghost(mob/user) @@ -57,4 +57,4 @@ if(!caller || !caller.alert_able || caller.alert_silenced || !alerttext) return playsound(src, 'sound/machines/twobeep_high.ogg', 50, TRUE) - machinery_computer.visible_message("The [src] displays a [caller.filedesc] notification: [alerttext]") + machinery_computer.visible_message(span_notice("The [src] displays a [caller.filedesc] notification: [alerttext]")) diff --git a/code/modules/modular_computers/computers/item/tablet.dm b/code/modules/modular_computers/computers/item/tablet.dm index d4d3ef52c2..8741c468d2 100644 --- a/code/modules/modular_computers/computers/item/tablet.dm +++ b/code/modules/modular_computers/computers/item/tablet.dm @@ -2,9 +2,10 @@ name = "tablet computer" icon = 'icons/obj/modular_tablet.dmi' icon_state = "tablet-red" - icon_state_unpowered = "tablet" - icon_state_powered = "tablet" + icon_state_unpowered = "tablet-red" + icon_state_powered = "tablet-red" icon_state_menu = "menu" + base_icon_state = "tablet" // worn_icon_state = "tablet" hardware_flag = PROGRAM_TABLET max_hardware_size = 1 @@ -80,12 +81,12 @@ /obj/item/modular_computer/tablet/ui_data(mob/user) . = ..() .["PC_showpeneject"] = inserted_item ? 1 : 0 - /obj/item/modular_computer/tablet/update_icon_state() if(has_variants) if(!finish_color) - finish_color = pick("red","blue","brown","green","black") - icon_state = icon_state_powered = icon_state_unpowered = "tablet-[finish_color]" + finish_color = pick("red", "blue", "brown", "green", "black") + icon_state = icon_state_powered = icon_state_unpowered = "[base_icon_state]-[finish_color]" + return ..() /obj/item/modular_computer/tablet/syndicate_contract_uplink name = "contractor tablet" @@ -102,6 +103,8 @@ /// Given to Nuke Ops members. /obj/item/modular_computer/tablet/nukeops icon_state = "tablet-syndicate" + icon_state_powered = "tablet-syndicate" + icon_state_unpowered = "tablet-syndicate" comp_light_luminosity = 6.3 has_variants = FALSE device_theme = "syndicate" @@ -109,15 +112,18 @@ /obj/item/modular_computer/tablet/nukeops/emag_act(mob/user) if(!enabled) - to_chat(user, "You'd need to turn the [src] on first.") + to_chat(user, span_warning("You'd need to turn the [src] on first.")) return FALSE - to_chat(user, "You swipe \the [src]. It's screen briefly shows a message reading \"MEMORY CODE INJECTION DETECTED AND SUCCESSFULLY QUARANTINED\".") + to_chat(user, span_notice("You swipe \the [src]. It's screen briefly shows a message reading \"MEMORY CODE INJECTION DETECTED AND SUCCESSFULLY QUARANTINED\".")) return FALSE /// Borg Built-in tablet interface /obj/item/modular_computer/tablet/integrated name = "modular interface" icon_state = "tablet-silicon" + icon_state_powered = "tablet-silicon" + icon_state_unpowered = "tablet-silicon" + base_icon_state = "tablet-silicon" has_light = FALSE //tablet light button actually enables/disables the borg lamp comp_light_luminosity = 0 has_variants = FALSE @@ -198,11 +204,13 @@ if(!caller || !caller.alert_able || caller.alert_silenced || !alerttext) //Yeah, we're checking alert_able. No, you don't get to make alerts that the user can't silence. return borgo.playsound_local(src, sound, 50, TRUE) - to_chat(borgo, "The [src] displays a [caller.filedesc] notification: [alerttext]") + to_chat(borgo, span_notice("The [src] displays a [caller.filedesc] notification: [alerttext]")) /obj/item/modular_computer/tablet/integrated/syndicate icon_state = "tablet-silicon-syndicate" + icon_state_powered = "tablet-silicon-syndicate" + icon_state_unpowered = "tablet-silicon-syndicate" device_theme = "syndicate" diff --git a/code/modules/modular_computers/computers/item/tablet_presets.dm b/code/modules/modular_computers/computers/item/tablet_presets.dm index 90dd149825..2c760a4b6c 100644 --- a/code/modules/modular_computers/computers/item/tablet_presets.dm +++ b/code/modules/modular_computers/computers/item/tablet_presets.dm @@ -20,6 +20,17 @@ install_component(new /obj/item/computer_hardware/card_slot) install_component(new /obj/item/computer_hardware/printer/mini) +/obj/item/modular_computer/tablet/preset/science/Initialize() + . = ..() + var/obj/item/computer_hardware/hard_drive/small/hard_drive = new + install_component(new /obj/item/computer_hardware/processor_unit/small) + install_component(new /obj/item/computer_hardware/battery(src, /obj/item/stock_parts/cell/computer)) + install_component(hard_drive) + install_component(new /obj/item/computer_hardware/card_slot) + install_component(new /obj/item/computer_hardware/network_card) + install_component(new /obj/item/computer_hardware/radio_card) + hard_drive.store_file(new /datum/computer_file/program/signaler) + /obj/item/modular_computer/tablet/preset/cargo/Initialize() . = ..() var/obj/item/computer_hardware/hard_drive/small/hard_drive = new @@ -30,17 +41,38 @@ install_component(new /obj/item/computer_hardware/network_card) install_component(new /obj/item/computer_hardware/printer/mini) // hard_drive.store_file(new /datum/computer_file/program/shipping) + var/datum/computer_file/program/chatclient/chatprogram + chatprogram = new + hard_drive.store_file(chatprogram) + chatprogram.username = get_cargochat_username() + +/obj/item/modular_computer/tablet/preset/cargo/proc/get_cargochat_username() + return "cargonian_[rand(1,999)]" + +/obj/item/modular_computer/tablet/preset/cargo/quartermaster/get_cargochat_username() + return "quartermaster" /obj/item/modular_computer/tablet/preset/advanced/atmos/Initialize() //This will be defunct and will be replaced when NtOS PDAs are done . = ..() install_component(new /obj/item/computer_hardware/sensorpackage) +/obj/item/modular_computer/tablet/preset/advanced/engineering/Initialize() + . = ..() + var/obj/item/computer_hardware/hard_drive/small/hard_drive = find_hardware_by_name("solid state drive") + hard_drive.store_file(new /datum/computer_file/program/supermatter_monitor) + /obj/item/modular_computer/tablet/preset/advanced/command/Initialize() . = ..() var/obj/item/computer_hardware/hard_drive/small/hard_drive = find_hardware_by_name("solid state drive") install_component(new /obj/item/computer_hardware/sensorpackage) install_component(new /obj/item/computer_hardware/card_slot/secondary) hard_drive.store_file(new /datum/computer_file/program/budgetorders) + // hard_drive.store_file(new /datum/computer_file/program/science) + +/obj/item/modular_computer/tablet/preset/advanced/command/engineering/Initialize() + . = ..() + var/obj/item/computer_hardware/hard_drive/small/hard_drive = find_hardware_by_name("solid state drive") + hard_drive.store_file(new /datum/computer_file/program/supermatter_monitor) /// Given by the syndicate as part of the contract uplink bundle - loads in the Contractor Uplink. /obj/item/modular_computer/tablet/syndicate_contract_uplink/preset/uplink/Initialize() diff --git a/code/modules/modular_computers/computers/machinery/console_presets.dm b/code/modules/modular_computers/computers/machinery/console_presets.dm index 12b2f6d25a..9da6b35ff9 100644 --- a/code/modules/modular_computers/computers/machinery/console_presets.dm +++ b/code/modules/modular_computers/computers/machinery/console_presets.dm @@ -26,8 +26,6 @@ /obj/machinery/modular_computer/console/preset/proc/install_programs() return - - // ===== ENGINEERING CONSOLE ===== /obj/machinery/modular_computer/console/preset/engineering console_department = "Engineering" @@ -45,6 +43,7 @@ console_department = "Research" name = "research director's console" desc = "A stationary computer. This one comes preloaded with research programs." + _has_second_id_slot = TRUE _has_ai = TRUE /obj/machinery/modular_computer/console/preset/research/install_programs() @@ -84,6 +83,18 @@ hard_drive.store_file(new/datum/computer_file/program/job_management()) hard_drive.store_file(new/datum/computer_file/program/crew_manifest()) +/obj/machinery/modular_computer/console/preset/id/centcom + desc = "A stationary computer. This one comes preloaded with CentCom identification modification programs." + +/obj/machinery/modular_computer/console/preset/id/centcom/install_programs() + var/obj/item/computer_hardware/hard_drive/hard_drive = cpu.all_components[MC_HDD] + var/datum/computer_file/program/card_mod/card_mod_centcom = new /datum/computer_file/program/card_mod() + card_mod_centcom.is_centcom = TRUE + hard_drive.store_file(new /datum/computer_file/program/chatclient()) + hard_drive.store_file(card_mod_centcom) + hard_drive.store_file(new /datum/computer_file/program/job_management()) + hard_drive.store_file(new /datum/computer_file/program/crew_manifest()) + // ===== CIVILIAN CONSOLE ===== /obj/machinery/modular_computer/console/preset/civilian console_department = "Civilian" @@ -94,3 +105,79 @@ var/obj/item/computer_hardware/hard_drive/hard_drive = cpu.all_components[MC_HDD] hard_drive.store_file(new/datum/computer_file/program/chatclient()) hard_drive.store_file(new/datum/computer_file/program/arcade()) + +// curator +/obj/machinery/modular_computer/console/preset/curator + console_department = "Civilian" + name = "curator console" + desc = "A stationary computer. This one comes preloaded with art programs." + _has_printer = TRUE + +/obj/machinery/modular_computer/console/preset/curator/install_programs() + var/obj/item/computer_hardware/hard_drive/hard_drive = cpu.all_components[MC_HDD] + hard_drive.store_file(new/datum/computer_file/program/portrait_printer()) + +// ===== CARGO CHAT CONSOLES ===== +/obj/machinery/modular_computer/console/preset/cargochat + name = "cargo chatroom console" + desc = "A stationary computer. This one comes preloaded with a chatroom for your cargo requests." + ///chat client installed on this computer, just helpful for linking all the computers + var/datum/computer_file/program/chatclient/chatprogram + +/obj/machinery/modular_computer/console/preset/cargochat/install_programs() + var/obj/item/computer_hardware/hard_drive/hard_drive = cpu.all_components[MC_HDD] + chatprogram = new + chatprogram.computer = cpu + hard_drive.store_file(chatprogram) + chatprogram.username = "[lowertext(console_department)]_department" + chatprogram.program_state = PROGRAM_STATE_ACTIVE + cpu.active_program = chatprogram + +//ONE PER MAP PLEASE, IT MAKES A CARGOBUS FOR EACH ONE OF THESE +/obj/machinery/modular_computer/console/preset/cargochat/cargo + console_department = "Cargo" + name = "department chatroom console" + desc = "A stationary computer. This one comes preloaded with a chatroom for incoming cargo requests. You may moderate it from this computer." + +/obj/machinery/modular_computer/console/preset/cargochat/cargo/install_programs() + var/obj/item/computer_hardware/hard_drive/hard_drive = cpu.all_components[MC_HDD] + + //adding chat, setting it as the active window immediately + chatprogram = new + chatprogram.computer = cpu + hard_drive.store_file(chatprogram) + chatprogram.program_state = PROGRAM_STATE_ACTIVE + cpu.active_program = chatprogram + + //setting up chat + chatprogram.username = "cargo_requests_operator" + var/datum/ntnet_conversation/cargochat = new + cargochat.operator = chatprogram //adding operator before joining the chat prevents an unnecessary message about switching op from showing + cargochat.add_client(chatprogram) + cargochat.title = "#cargobus" + cargochat.strong = TRUE + chatprogram.active_channel = cargochat.id + +/obj/machinery/modular_computer/console/preset/cargochat/cargo/LateInitialize() + . = ..() + var/datum/ntnet_conversation/cargochat = SSnetworks.station_network.get_chat_channel_by_id(chatprogram.active_channel) + for(var/obj/machinery/modular_computer/console/preset/cargochat/cargochat_console in GLOB.machines) + if(cargochat_console == src) + continue + cargochat_console.chatprogram.active_channel = chatprogram.active_channel + cargochat.add_client(cargochat_console.chatprogram, silent = TRUE) + +/obj/machinery/modular_computer/console/preset/cargochat/service + console_department = "Service" + +/obj/machinery/modular_computer/console/preset/cargochat/engineering + console_department = "Engineering" + +/obj/machinery/modular_computer/console/preset/cargochat/science + console_department = "Science" + +/obj/machinery/modular_computer/console/preset/cargochat/security + console_department = "Security" + +/obj/machinery/modular_computer/console/preset/cargochat/medical + console_department = "Medical" diff --git a/code/modules/modular_computers/computers/machinery/modular_computer.dm b/code/modules/modular_computers/computers/machinery/modular_computer.dm index da60657f7e..dd78a985e1 100644 --- a/code/modules/modular_computers/computers/machinery/modular_computer.dm +++ b/code/modules/modular_computers/computers/machinery/modular_computer.dm @@ -4,28 +4,41 @@ name = "modular computer" desc = "An advanced computer." - use_power = IDLE_POWER_USE - idle_power_usage = 5 - var/hardware_flag = 0 // A flag that describes this device type - var/last_power_usage = 0 // Power usage during last tick - // Modular computers can run on various devices. Each DEVICE (Laptop, Console, Tablet,..) // must have it's own DMI file. Icon states must be called exactly the same in all files, but may look differently // If you create a program which is limited to Laptops and Consoles you don't have to add it's icon_state overlay for Tablets too, for example. - icon = null icon_state = null - var/icon_state_unpowered = null // Icon state when the computer is turned off. - var/icon_state_powered = null // Icon state when the computer is turned on. - var/screen_icon_state_menu = "menu" // Icon state overlay when the computer is turned on, but no program is loaded that would override the screen. - var/screen_icon_screensaver = "standby" // Icon state overlay when the computer is powered, but not 'switched on'. - var/max_hardware_size = 0 // Maximal hardware size. Currently, tablets have 1, laptops 2 and consoles 3. Limits what hardware types can be installed. - var/steel_sheet_cost = 10 // Amount of steel sheets refunded when disassembling an empty frame of this computer. - var/light_strength = 0 // Light luminosity when turned on - var/base_active_power_usage = 100 // Power usage when the computer is open (screen is active) and can be interacted with. Remember hardware can use power too. - var/base_idle_power_usage = 10 // Power usage when the computer is idle and screen is off (currently only applies to laptops) - var/obj/item/modular_computer/processor/cpu = null // CPU that handles most logic while this type only handles power and other specific things. + use_power = IDLE_POWER_USE + idle_power_usage = 5 + ///A flag that describes this device type + var/hardware_flag = 0 + ///Power usage during last tick + var/last_power_usage = 0 + + + ///Icon state when the computer is turned off. + var/icon_state_unpowered = null + ///Icon state when the computer is turned on. + var/icon_state_powered = null + ///Icon state overlay when the computer is turned on, but no program is loaded that would override the screen. + var/screen_icon_state_menu = "menu" + ///Icon state overlay when the computer is powered, but not 'switched on'. + var/screen_icon_screensaver = "standby" + ///Maximal hardware size. Currently, tablets have 1, laptops 2 and consoles 3. Limits what hardware types can be installed. + var/max_hardware_size = 0 + ///Amount of steel sheets refunded when disassembling an empty frame of this computer. + var/steel_sheet_cost = 10 + ///Light luminosity when turned on + var/light_strength = 0 + ///Power usage when the computer is open (screen is active) and can be interacted with. Remember hardware can use power too. + var/base_active_power_usage = 100 + ///Power usage when the computer is idle and screen is off (currently only applies to laptops) + var/base_idle_power_usage = 10 + + ///CPU that handles most logic while this type only handles power and other specific things. + var/obj/item/modular_computer/processor/cpu = null /obj/machinery/modular_computer/Initialize() . = ..() @@ -48,32 +61,35 @@ cpu.attack_ghost(user) /obj/machinery/modular_computer/emag_act(mob/user) - . = ..() if(!cpu) - to_chat(user, "You'd need to turn the [src] on first.") + to_chat(user, span_warning("You'd need to turn the [src] on first.")) return FALSE return (cpu.emag_act(user)) -/obj/machinery/modular_computer/update_icon() - cut_overlays() - icon_state = icon_state_powered +/obj/machinery/modular_computer/update_appearance(updates) + . = ..() + set_light(cpu?.enabled ? light_strength : 0) - if(!cpu || !cpu.enabled) +/obj/machinery/modular_computer/update_icon_state() + icon_state = (cpu?.enabled || (!(stat & NOPOWER) && cpu?.use_power())) ? icon_state_powered : icon_state_unpowered + return ..() + +/obj/machinery/modular_computer/update_overlays() + . = ..() + if(!cpu?.enabled) if (!(stat & NOPOWER) && (cpu?.use_power())) - add_overlay(screen_icon_screensaver) - else - icon_state = icon_state_unpowered - set_light(0) + . += screen_icon_screensaver else - set_light(light_strength) - if(cpu.active_program) - add_overlay(cpu.active_program.program_icon_state ? cpu.active_program.program_icon_state : screen_icon_state_menu) - else - add_overlay(screen_icon_state_menu) + . += cpu.active_program?.program_icon_state || screen_icon_state_menu if(cpu && cpu.obj_integrity <= cpu.integrity_failure * cpu.max_integrity) - add_overlay("bsod") - add_overlay("broken") + . += "bsod" + . += "broken" + +/// Eats the "source" arg because update_icon actually expects args now. +/obj/machinery/modular_computer/proc/relay_icon_update(datum/source, updates, updated) + SIGNAL_HANDLER + return update_icon(updates) /obj/machinery/modular_computer/AltClick(mob/user) if(cpu) @@ -98,17 +114,17 @@ /obj/machinery/modular_computer/proc/power_failure(malfunction = 0) var/obj/item/computer_hardware/battery/battery_module = cpu.all_components[MC_CELL] if(cpu?.enabled) // Shut down the computer - visible_message("\The [src]'s screen flickers [battery_module ? "\"BATTERY [malfunction ? "MALFUNCTION" : "CRITICAL"]\"" : "\"EXTERNAL POWER LOSS\""] warning as it shuts down unexpectedly.") + visible_message(span_danger("\The [src]'s screen flickers [battery_module ? "\"BATTERY [malfunction ? "MALFUNCTION" : "CRITICAL"]\"" : "\"EXTERNAL POWER LOSS\""] warning as it shuts down unexpectedly.")) if(cpu) cpu.shutdown_computer(0) - stat |= NOPOWER - update_icon() + set_machine_stat(stat | NOPOWER) + update_appearance() // Modular computers can have battery in them, we handle power in previous proc, so prevent this from messing it up for us. /obj/machinery/modular_computer/power_change() if(cpu?.use_power()) // If MC_CPU still has a power source, PC wouldn't go offline. - stat &= ~NOPOWER - update_icon() + set_machine_stat(stat & ~NOPOWER) + update_appearance() return . = ..() @@ -116,7 +132,7 @@ if(cpu) return cpu.screwdriver_act(user, tool) -/obj/machinery/modular_computer/attackby(obj/item/W as obj, mob/user) +/obj/machinery/modular_computer/attackby(obj/item/W as obj, mob/living/user) if (user.a_intent == INTENT_HELP && cpu && !(flags_1 & NODECONSTRUCT_1)) return cpu.attackby(W, user) return ..() @@ -126,15 +142,16 @@ // Minor explosions are mostly mitigitated by casing. /obj/machinery/modular_computer/ex_act(severity, target, origin) if(cpu) - cpu.ex_act(severity, target, origin) - // switch(severity) - // if(EXPLODE_DEVASTATE) - // SSexplosions.high_mov_atom += cpu - // if(EXPLODE_HEAVY) - // SSexplosions.med_mov_atom += cpu - // if(EXPLODE_LIGHT) - // SSexplosions.low_mov_atom += cpu - ..() + return cpu.ex_act(severity) + + // switch(severity) + // if(EXPLODE_DEVASTATE) + // SSexplosions.high_mov_atom += cpu + // if(EXPLODE_HEAVY) + // SSexplosions.med_mov_atom += cpu + // if(EXPLODE_LIGHT) + // SSexplosions.low_mov_atom += cpu + return ..() // EMPs are similar to explosions, but don't cause physical damage to the casing. Instead they screw up the components /obj/machinery/modular_computer/emp_act(severity) diff --git a/code/modules/modular_computers/computers/machinery/modular_console.dm b/code/modules/modular_computers/computers/machinery/modular_console.dm index 0e27d81305..aa6108fcbd 100644 --- a/code/modules/modular_computers/computers/machinery/modular_console.dm +++ b/code/modules/modular_computers/computers/machinery/modular_console.dm @@ -16,7 +16,8 @@ light_strength = 2 max_integrity = 300 integrity_failure = 0.5 - var/console_department = "" // Used in New() to set network tag according to our area. + ///Used in New() to set network tag according to our area. + var/console_department = "" /obj/machinery/modular_computer/console/buildable/Initialize() . = ..() @@ -52,4 +53,4 @@ network_card.identification_string = "Unknown Console" if(cpu) cpu.screen_on = 1 - update_icon() + update_appearance() diff --git a/code/modules/modular_computers/file_system/computer_file.dm b/code/modules/modular_computers/file_system/computer_file.dm index 4e862c4ae3..08e77b8a9b 100644 --- a/code/modules/modular_computers/file_system/computer_file.dm +++ b/code/modules/modular_computers/file_system/computer_file.dm @@ -1,11 +1,11 @@ /datum/computer_file - var/filename = "NewFile" // Placeholder. No spacebars - var/filetype = "XXX" // File full names are [filename].[filetype] so like NewFile.XXX in this case - var/size = 1 // File size in GQ. Integers only! - var/obj/item/computer_hardware/hard_drive/holder // Holder that contains this file. - var/unsendable = FALSE // Whether the file may be sent to someone via NTNet transfer or other means. - var/undeletable = FALSE // Whether the file may be deleted. Setting to TRUE prevents deletion/renaming/etc. - var/uid // UID of this file + var/filename = "NewFile" // Placeholder. No spacebars + var/filetype = "XXX" // File full names are [filename].[filetype] so like NewFile.XXX in this case + var/size = 1 // File size in GQ. Integers only! + var/obj/item/computer_hardware/hard_drive/holder // Holder that contains this file. + var/unsendable = FALSE // Whether the file may be sent to someone via NTNet transfer or other means. + var/undeletable = FALSE // Whether the file may be deleted. Setting to TRUE prevents deletion/renaming/etc. + var/uid // UID of this file var/static/file_uid = 0 /datum/computer_file/New() diff --git a/code/modules/modular_computers/file_system/data.dm b/code/modules/modular_computers/file_system/data.dm index 32ef6f53dd..e0404e787f 100644 --- a/code/modules/modular_computers/file_system/data.dm +++ b/code/modules/modular_computers/file_system/data.dm @@ -1,10 +1,10 @@ // /data/ files store data in string format. // They don't contain other logic for now. /datum/computer_file/data - var/stored_data = "" // Stored data in string format. + var/stored_data = "" // Stored data in string format. filetype = "DAT" var/block_size = 250 - var/do_not_edit = 0 // Whether the user will be reminded that the file probably shouldn't be edited. + var/do_not_edit = 0 // Whether the user will be reminded that the file probably shouldn't be edited. /datum/computer_file/data/clone() var/datum/computer_file/data/temp = ..() diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm index a86405f882..dc0cdda5d1 100644 --- a/code/modules/modular_computers/file_system/program.dm +++ b/code/modules/modular_computers/file_system/program.dm @@ -15,6 +15,8 @@ var/filedesc = "Unknown Program" /// Short description of this program's function. var/extended_desc = "N/A" + /// Category in the NTDownloader. + var/category = PROGRAM_CATEGORY_MISC /// Program-specific screen icon state var/program_icon_state = null /// Set to 1 for program to require nonstop NTNet connection to run. If NTNet connection is lost program crashes. @@ -25,10 +27,10 @@ var/ntnet_status = 1 /// Bitflags (PROGRAM_CONSOLE, PROGRAM_LAPTOP, PROGRAM_TABLET combination) or PROGRAM_ALL var/usage_flags = PROGRAM_ALL - /// Whether the program can be downloaded from NTNet. Set to 0 to disable. - var/available_on_ntnet = 1 - /// Whether the program can be downloaded from SyndiNet (accessible via emagging the computer). Set to 1 to enable. - var/available_on_syndinet = 0 + /// Whether the program can be downloaded from NTNet. Set to FALSE to disable. + var/available_on_ntnet = TRUE + /// Whether the program can be downloaded from SyndiNet (accessible via emagging the computer). Set to TRUE to enable. + var/available_on_syndinet = FALSE /// Name of the tgui interface var/tgui_id /// Example: "something.gif" - a header image that will be rendered in computer's UI when this program is running at background. Images are taken from /icons/program_icons. Be careful not to use too large images! @@ -64,7 +66,7 @@ // Relays icon update to the computer. /datum/computer_file/program/proc/update_computer_icon() if(computer) - computer.update_icon() + computer.update_appearance() // Attempts to create a log in global ntnet datum. Returns 1 on success, 0 on fail. /datum/computer_file/program/proc/generate_network_log(text) @@ -72,10 +74,25 @@ return computer.add_log(text) return 0 +/** + *Runs when the device is used to attack an atom in non-combat mode. + * + *Simulates using the device to read or scan something. Tap is called by the computer during pre_attack + *and sends us all of the related info. If we return TRUE, the computer will stop the attack process + *there. What we do with the info is up to us, but we should only return TRUE if we actually perform + *an action of some sort. + *Arguments: + *A is the atom being tapped + *user is the person making the attack action + *params is anything the pre_attack() proc had in the same-named variable. +*/ +/datum/computer_file/program/proc/tap(atom/A, mob/living/user, params) + return FALSE + /datum/computer_file/program/proc/is_supported_by_hardware(hardware_flag = 0, loud = 0, mob/user = null) if(!(hardware_flag & usage_flags)) if(loud && computer && user) - to_chat(user, "\The [computer] flashes a \"Hardware Error - Incompatible software\" warning.") + to_chat(user, span_danger("\The [computer] flashes a \"Hardware Error - Incompatible software\" warning.")) return FALSE return TRUE @@ -109,7 +126,7 @@ if(!access_to_check) // No required_access, allow it. return TRUE - if(!transfer && computer && (computer.obj_flags & EMAGGED)) //emags can bypass the execution locks but not the download ones. + if(!transfer && computer && (computer.obj_flags & EMAGGED)) //emags can bypass the execution locks but not the download ones. return TRUE if(IsAdminGhost(user)) @@ -127,14 +144,14 @@ if(!D) if(loud) - to_chat(user, "\The [computer] flashes an \"RFID Error - Unable to scan ID\" warning.") + to_chat(user, span_danger("\The [computer] flashes an \"RFID Error - Unable to scan ID\" warning.")) return FALSE access = D.GetAccess() if(access_to_check in access) return TRUE if(loud) - to_chat(user, "\The [computer] flashes an \"Access Denied\" warning.") + to_chat(user, span_danger("\The [computer] flashes an \"Access Denied\" warning.")) return FALSE // This attempts to retrieve header data for UIs. If implementing completely new device of different type than existing ones @@ -219,7 +236,7 @@ program_state = PROGRAM_STATE_BACKGROUND // Should close any existing UIs computer.active_program = null - computer.update_icon() + computer.update_appearance() ui.close() if(user && istype(user)) diff --git a/code/modules/modular_computers/file_system/program_events.dm b/code/modules/modular_computers/file_system/program_events.dm index 1cb74a227b..c11b1066bd 100644 --- a/code/modules/modular_computers/file_system/program_events.dm +++ b/code/modules/modular_computers/file_system/program_events.dm @@ -13,6 +13,6 @@ /datum/computer_file/program/proc/event_networkfailure(background) kill_program(forced = TRUE) if(background) - computer.visible_message("\The [computer]'s screen displays a \"Process [filename].[filetype] (PID [rand(100,999)]) terminated - Network Error\" error") + computer.visible_message(span_danger("\The [computer]'s screen displays a \"Process [filename].[filetype] (PID [rand(100,999)]) terminated - Network Error\" error")) else - computer.visible_message("\The [computer]'s screen briefly freezes and then shows \"NETWORK ERROR - NTNet connection lost. Please retry. If problem persists contact your system administrator.\" error.") + computer.visible_message(span_danger("\The [computer]'s screen briefly freezes and then shows \"NETWORK ERROR - NTNet connection lost. Please retry. If problem persists contact your system administrator.\" error.")) diff --git a/code/modules/modular_computers/file_system/programs/airestorer.dm b/code/modules/modular_computers/file_system/programs/airestorer.dm index faf2831ca1..4f181c0e34 100644 --- a/code/modules/modular_computers/file_system/programs/airestorer.dm +++ b/code/modules/modular_computers/file_system/programs/airestorer.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/aidiag filename = "aidiag" filedesc = "NT FRK" + category = PROGRAM_CATEGORY_ROBO program_icon_state = "generic" extended_desc = "Firmware Restoration Kit, capable of reconstructing damaged AI systems. Requires direct AI connection via intellicard slot." size = 12 @@ -55,7 +56,7 @@ /datum/computer_file/program/aidiag/process_tick() . = ..() - if(!restoring) //Put the check here so we don't check for an ai all the time + if(!restoring) //Put the check here so we don't check for an ai all the time return var/obj/item/aicard/cardhold = get_ai(2) @@ -64,7 +65,7 @@ var/mob/living/silicon/ai/A = get_ai() if(!A || !cardhold) - restoring = FALSE // If the AI was removed, stop the restoration sequence. + restoring = FALSE // If the AI was removed, stop the restoration sequence. if(ai_slot) ai_slot.locked = FALSE return @@ -84,7 +85,7 @@ if(A.health >= 0 && A.stat == DEAD) A.revive(full_heal = FALSE, admin_revive = FALSE) - cardhold.update_icon() + cardhold.update_appearance() // Finished restoring if(A.health >= 100) diff --git a/code/modules/modular_computers/file_system/programs/antagonist/contract_uplink.dm b/code/modules/modular_computers/file_system/programs/antagonist/contract_uplink.dm index 8709526de6..1cad0e46f8 100644 --- a/code/modules/modular_computers/file_system/programs/antagonist/contract_uplink.dm +++ b/code/modules/modular_computers/file_system/programs/antagonist/contract_uplink.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/contract_uplink filename = "contractor uplink" filedesc = "Syndicate Contractor Uplink" + category = PROGRAM_CATEGORY_MISC program_icon_state = "assign" extended_desc = "A standard, Syndicate issued system for handling important contracts while on the field." size = 10 @@ -91,9 +92,9 @@ if(ishuman(user)) var/mob/living/carbon/human/H = user if(H.put_in_hands(crystals)) - to_chat(H, "Your payment materializes into your hands!") + to_chat(H, span_notice("Your payment materializes into your hands!")) else - to_chat(user, "Your payment materializes onto the floor.") + to_chat(user, span_notice("Your payment materializes onto the floor.")) hard_drive.traitor_data.contractor_hub.contract_TC_payed_out += hard_drive.traitor_data.contractor_hub.contract_TC_to_redeem hard_drive.traitor_data.contractor_hub.contract_TC_to_redeem = 0 @@ -164,6 +165,11 @@ )) for (var/datum/syndicate_contract/contract in traitor_data.contractor_hub.assigned_contracts) + if(!contract.contract) + stack_trace("Syndiate contract with null contract objective found in [traitor_data.owner]'s contractor hub!") + contract.status = CONTRACT_STATUS_ABORTED + continue + data["contracts"] += list(list( "target" = contract.contract.target, "target_rank" = contract.target_rank, diff --git a/code/modules/modular_computers/file_system/programs/antagonist/dos.dm b/code/modules/modular_computers/file_system/programs/antagonist/dos.dm index bb3c62cac2..90510bacdb 100644 --- a/code/modules/modular_computers/file_system/programs/antagonist/dos.dm +++ b/code/modules/modular_computers/file_system/programs/antagonist/dos.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/ntnet_dos filename = "ntn_dos" filedesc = "DoS Traffic Generator" + category = PROGRAM_CATEGORY_MISC program_icon_state = "hostile" extended_desc = "This advanced script can perform denial of service attacks against NTNet quantum relays. The system administrator will probably notice this. Multiple devices can run this program together against same relay for increased effect" size = 20 diff --git a/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm b/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm index ba24a5ab3e..4e5afa32a7 100644 --- a/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm +++ b/code/modules/modular_computers/file_system/programs/antagonist/revelation.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/revelation filename = "revelation" filedesc = "Revelation" + category = PROGRAM_CATEGORY_MISC program_icon_state = "hostile" extended_desc = "This virus can destroy hard drive of system it is executed on. It may be obfuscated to look like another non-malicious program. Once armed, it will destroy the system upon next execution." size = 13 @@ -20,13 +21,13 @@ if(computer) if(istype(computer, /obj/item/modular_computer/tablet/integrated)) //If this is a borg's integrated tablet var/obj/item/modular_computer/tablet/integrated/modularInterface = computer - to_chat(modularInterface.borgo,"SYSTEM PURGE DETECTED/") + to_chat(modularInterface.borgo,span_userdanger("SYSTEM PURGE DETECTED/")) addtimer(CALLBACK(modularInterface.borgo, /mob/living/silicon/robot/.proc/death), 2 SECONDS, TIMER_UNIQUE) return - computer.visible_message("\The [computer]'s screen brightly flashes and loud electrical buzzing is heard.") + computer.visible_message(span_notice("\The [computer]'s screen brightly flashes and loud electrical buzzing is heard.")) computer.enabled = FALSE - computer.update_icon() + computer.update_appearance() var/obj/item/computer_hardware/hard_drive/hard_drive = computer.all_components[MC_HDD] var/obj/item/computer_hardware/battery/battery_module = computer.all_components[MC_CELL] var/obj/item/computer_hardware/recharger/recharger = computer.all_components[MC_CHARGE] @@ -34,13 +35,13 @@ computer.take_damage(25, BRUTE, 0, 0) if(battery_module && prob(25)) qdel(battery_module) - computer.visible_message("\The [computer]'s battery explodes in rain of sparks.") + computer.visible_message(span_notice("\The [computer]'s battery explodes in rain of sparks.")) var/datum/effect_system/spark_spread/spark_system = new /datum/effect_system/spark_spread spark_system.start() if(recharger && prob(50)) qdel(recharger) - computer.visible_message("\The [computer]'s recharger explodes in rain of sparks.") + computer.visible_message(span_notice("\The [computer]'s recharger explodes in rain of sparks.")) var/datum/effect_system/spark_spread/spark_system = new /datum/effect_system/spark_spread spark_system.start() diff --git a/code/modules/modular_computers/file_system/programs/arcade.dm b/code/modules/modular_computers/file_system/programs/arcade.dm index c330cdcbe8..3fe38cdf6f 100644 --- a/code/modules/modular_computers/file_system/programs/arcade.dm +++ b/code/modules/modular_computers/file_system/programs/arcade.dm @@ -32,7 +32,7 @@ game_active = FALSE program_icon_state = "arcade_off" if(istype(computer)) - computer.update_icon() + computer.update_appearance() ticket_count += 1 // user?.mind?.adjust_experience(/datum/skill/gaming, 50) sleep(10) @@ -42,7 +42,7 @@ game_active = FALSE program_icon_state = "arcade_off" if(istype(computer)) - computer.update_icon() + computer.update_appearance() // user?.mind?.adjust_experience(/datum/skill/gaming, 10) sleep(10) @@ -150,20 +150,20 @@ return TRUE if("Dispense_Tickets") if(!printer) - to_chat(usr, "Hardware error: A printer is required to redeem tickets.") + to_chat(usr, span_notice("Hardware error: A printer is required to redeem tickets.")) return if(printer.stored_paper <= 0) - to_chat(usr, "Hardware error: Printer is out of paper.") + to_chat(usr, span_notice("Hardware error: Printer is out of paper.")) return else - computer.visible_message("\The [computer] prints out paper.") + computer.visible_message(span_notice("\The [computer] prints out paper.")) if(ticket_count >= 1) new /obj/item/stack/arcadeticket((get_turf(computer)), 1) - to_chat(usr, "[src] dispenses a ticket!") + to_chat(usr, span_notice("[computer] dispenses a ticket!")) ticket_count -= 1 printer.stored_paper -= 1 else - to_chat(usr, "You don't have any stored tickets!") + to_chat(usr, span_notice("You don't have any stored tickets!")) return TRUE if("Start_Game") game_active = TRUE @@ -175,4 +175,4 @@ boss_id = rand(1,6) pause_state = FALSE if(istype(computer)) - computer.update_icon() + computer.update_appearance() diff --git a/code/modules/modular_computers/file_system/programs/atmosscan.dm b/code/modules/modular_computers/file_system/programs/atmosscan.dm index 1576a5b4b7..db9f0d85b4 100644 --- a/code/modules/modular_computers/file_system/programs/atmosscan.dm +++ b/code/modules/modular_computers/file_system/programs/atmosscan.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/atmosscan filename = "atmosscan" filedesc = "AtmoZphere" + category = PROGRAM_CATEGORY_ENGI program_icon_state = "air" extended_desc = "A small built-in sensor reads out the atmospheric conditions around the device." size = 4 @@ -12,7 +13,7 @@ if (!.) return if(!computer?.get_modular_computer_part(MC_SENSORS)) //Giving a clue to users why the program is spitting out zeros. - to_chat(user, "\The [computer] flashes an error: \"hardware\\sensorpackage\\startup.bin -- file not found\".") + to_chat(user, span_warning("\The [computer] flashes an error: \"hardware\\sensorpackage\\startup.bin -- file not found\".")) /datum/computer_file/program/atmosscan/ui_data(mob/user) diff --git a/code/modules/modular_computers/file_system/programs/borg_monitor.dm b/code/modules/modular_computers/file_system/programs/borg_monitor.dm index 46e1f89ee4..10f6561087 100644 --- a/code/modules/modular_computers/file_system/programs/borg_monitor.dm +++ b/code/modules/modular_computers/file_system/programs/borg_monitor.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/borg_monitor filename = "siliconnect" filedesc = "SiliConnect" + category = PROGRAM_CATEGORY_ROBO ui_header = "borg_mon.gif" program_icon_state = "generic" extended_desc = "This program allows for remote monitoring of station cyborgs." @@ -9,6 +10,70 @@ size = 5 tgui_id = "NtosCyborgRemoteMonitor" program_icon = "project-diagram" + var/emagged = FALSE ///Bool of if this app has already been emagged + var/list/loglist = list() ///A list to copy a borg's IC log list into + var/mob/living/silicon/robot/DL_source ///reference of a borg if we're downloading a log, or null if not. + var/DL_progress = -1 ///Progress of current download, 0 to 100, -1 for no current download + +/datum/computer_file/program/borg_monitor/Destroy() + loglist = null + DL_source = null + return ..() + +/datum/computer_file/program/borg_monitor/kill_program(forced = FALSE) + loglist = null //Not everything is saved if you close an app + DL_source = null + DL_progress = 0 + return ..() + +/datum/computer_file/program/borg_monitor/run_emag() + if(emagged) + return FALSE + emagged = TRUE + return TRUE + +/datum/computer_file/program/borg_monitor/tap(atom/A, mob/living/user, params) + var/mob/living/silicon/robot/borgo = A + if(!istype(borgo) || !borgo.modularInterface) + return FALSE + DL_source = borgo + DL_progress = 0 + + var/username = "unknown user" + var/obj/item/card/id/stored_card = computer.GetID() + if(istype(stored_card) && stored_card.registered_name) + username = "user [stored_card.registered_name]" + to_chat(borgo, span_userdanger("Request received from [username] for the system log file. Upload in progress."))//Damning evidence may be contained, so warn the borg + borgo.logevent("File request by [username]: /var/logs/syslog") + return TRUE + +/datum/computer_file/program/borg_monitor/process_tick() + if(!DL_source) + DL_progress = -1 + return + + var/turf/here = get_turf(computer) + var/turf/there = get_turf(DL_source) + if(!here.Adjacent(there))//If someone walked away, cancel the download + to_chat(DL_source, span_danger("Log upload failed: general connection error."))//Let the borg know the upload stopped + DL_source = null + DL_progress = -1 + return + + if(DL_progress == 100) + if(!DL_source || !DL_source.modularInterface) //sanity check, in case the borg or their modular tablet poofs somehow + loglist = list("System log of unit [DL_source.name]") + loglist += "Error -- Download corrupted." + else + loglist = DL_source.modularInterface.borglog.Copy() + loglist.Insert(1,"System log of unit [DL_source.name]") + DL_progress = -1 + DL_source = null + for(var/datum/tgui/window in SStgui.open_uis_by_src[REF(src)]) + window.send_full_update() + return + + DL_progress += 25 /datum/computer_file/program/borg_monitor/ui_data(mob/user) var/list/data = get_header_data() @@ -32,15 +97,22 @@ var/list/cyborg_data = list( name = R.name, + integ = round((R.health + 100) / 2), //mob heath is -100 to 100, we want to scale that to 0 - 100 locked_down = R.locked_down, status = R.stat, shell_discon = shell, charge = R.cell ? round(R.cell.percent()) : null, - module = R.module ? "[R.module.name] Module" : "No Module Detected", + module = R.module ? "[R.module.name] Model" : "No Model Detected", upgrades = upgrade, ref = REF(R) ) data["cyborgs"] += list(cyborg_data) + data["DL_progress"] = DL_progress + return data + +/datum/computer_file/program/borg_monitor/ui_static_data(mob/user) + var/list/data = list() + data["borglog"] = loglist return data /datum/computer_file/program/borg_monitor/ui_act(action, params) @@ -57,16 +129,16 @@ if(!ID) return if(R.stat == DEAD) //Dead borgs will listen to you no longer - to_chat(usr, "Error -- Could not open a connection to unit:[R]") + to_chat(usr, span_warning("Error -- Could not open a connection to unit:[R]")) var/message = stripped_input(usr, message = "Enter message to be sent to remote cyborg.", title = "Send Message") if(!message) return - to_chat(R, "

Message from [ID] -- \"[message]\"
") + to_chat(R, "

[span_notice("Message from [ID] -- \"[message]\"")]
") to_chat(usr, "Message sent to [R]: [message]") R.logevent("Message from [ID] -- \"[message]\"") SEND_SOUND(R, 'sound/machines/twobeep_high.ogg') if(R.connected_ai) - to_chat(R.connected_ai, "

Message from [ID] to [R] -- \"[message]\"
") + to_chat(R.connected_ai, "

[span_notice("Message from [ID] to [R] -- \"[message]\"")]
") SEND_SOUND(R.connected_ai, 'sound/machines/twobeep_high.ogg') usr.log_talk(message, LOG_PDA, tag="Cyborg Monitor Program: ID name \"[ID]\" to [R]") @@ -82,12 +154,15 @@ /datum/computer_file/program/borg_monitor/proc/checkID() var/obj/item/card/id/ID = computer.GetID() if(!ID) + if(emagged) + return "STDERR:UNDF" return FALSE return ID.registered_name /datum/computer_file/program/borg_monitor/syndicate filename = "roboverlord" filedesc = "Roboverlord" + category = PROGRAM_CATEGORY_ROBO ui_header = "borg_mon.gif" program_icon_state = "generic" extended_desc = "This program allows for remote monitoring of mission-assigned cyborgs." @@ -97,6 +172,9 @@ transfer_access = null tgui_id = "NtosCyborgRemoteMonitorSyndicate" +/datum/computer_file/program/borg_monitor/syndicate/run_emag() + return FALSE + /datum/computer_file/program/borg_monitor/syndicate/evaluate_borg(mob/living/silicon/robot/R) if((get_turf(computer)).z != (get_turf(R)).z) return FALSE diff --git a/code/modules/modular_computers/file_system/programs/bounty_board.dm b/code/modules/modular_computers/file_system/programs/bounty_board.dm index 9c42a28a9b..d008d5e72b 100644 --- a/code/modules/modular_computers/file_system/programs/bounty_board.dm +++ b/code/modules/modular_computers/file_system/programs/bounty_board.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/bounty_board filename = "bountyboard" filedesc = "Bounty Board Request Network" + category = PROGRAM_CATEGORY_SUPL program_icon_state = "bountyboard" extended_desc = "A multi-platform network for placing requests across the station, with payment across the network being possible.." requires_ntnet = TRUE diff --git a/code/modules/modular_computers/file_system/programs/budgetordering.dm b/code/modules/modular_computers/file_system/programs/budgetordering.dm index 4fda99864c..1f8fd6f7a6 100644 --- a/code/modules/modular_computers/file_system/programs/budgetordering.dm +++ b/code/modules/modular_computers/file_system/programs/budgetordering.dm @@ -1,41 +1,50 @@ /datum/computer_file/program/budgetorders filename = "orderapp" filedesc = "NT IRN" - // category = PROGRAM_CATEGORY_SUPL + category = PROGRAM_CATEGORY_SUPL program_icon_state = "request" extended_desc = "Nanotrasen Internal Requisition Network interface for supply purchasing using a department budget account." requires_ntnet = TRUE - transfer_access = ACCESS_HEADS usage_flags = PROGRAM_LAPTOP | PROGRAM_TABLET size = 20 tgui_id = "NtosCargo" ///Are you actually placing orders with it? var/requestonly = TRUE ///Can the tablet see or buy illegal stuff? - var/contraband_view = FALSE + var/contraband = FALSE ///Is it being bought from a personal account, or is it being done via a budget/cargo? var/self_paid = FALSE ///Can this console approve purchase requests? var/can_approve_requests = FALSE ///What do we say when the shuttle moves with living beings on it. - var/safety_warning = "For safety reasons, the automated supply shuttle \ - cannot transport live organisms, human remains, classified nuclear weaponry, \ - homing beacons or machinery housing any form of artificial intelligence." + var/safety_warning = "For safety and ethical reasons, the automated supply shuttle \ + cannot transport live organisms, human remains, classified nuclear weaponry, mail, \ + homing beacons, unstable eigenstates or machinery housing any form of artificial intelligence." ///If you're being raided by pirates, what do you tell the crew? var/blockade_warning = "Bluespace instability detected. Shuttle movement impossible." + ///The name of the shuttle template being used as the cargo shuttle. 'supply' is default and contains critical code. Don't change this unless you know what you're doing. + var/cargo_shuttle = "supply" + ///The docking port called when returning to the station. + var/docking_home = "supply_home" + ///The docking port called when leaving the station. + var/docking_away = "supply_away" + ///If this console can loan the cargo shuttle. Set to false to disable. + var/stationcargo = TRUE + ///The account this console processes and displays. Independent from the account the shuttle processes. + var/cargo_account = ACCOUNT_CAR /datum/computer_file/program/budgetorders/proc/get_export_categories() . = EXPORT_CARGO /datum/computer_file/program/budgetorders/run_emag() - if(!contraband_view) - contraband_view = TRUE + if(!contraband) + contraband = TRUE return TRUE /datum/computer_file/program/budgetorders/proc/is_visible_pack(mob/user, paccess_to_check, list/access, contraband) if(issilicon(user)) //Borgs can't buy things. return FALSE - if((computer.obj_flags & EMAGGED) || contraband_view) + if(computer.obj_flags & EMAGGED) return TRUE else if(contraband) //Hide contrband when non-emagged. return FALSE @@ -64,11 +73,11 @@ . = ..() var/list/data = get_header_data() data["location"] = SSshuttle.supply.getStatusText() - var/datum/bank_account/buyer = SSeconomy.get_dep_account(ACCOUNT_CAR) + var/datum/bank_account/buyer = SSeconomy.get_dep_account(cargo_account) var/obj/item/computer_hardware/card_slot/card_slot = computer.all_components[MC_CARD] var/obj/item/card/id/id_card = card_slot?.GetID() if(id_card?.registered_account) - if(ACCESS_HEADS in id_card.access) + if((ACCESS_HEADS in id_card.access) || (ACCESS_QM in id_card.access)) requestonly = FALSE buyer = SSeconomy.get_dep_account(id_card.registered_account.account_job.paycheck_department) can_approve_requests = TRUE @@ -85,14 +94,14 @@ data["supplies"] = list() for(var/pack in SSshuttle.supply_packs) var/datum/supply_pack/P = SSshuttle.supply_packs[pack] - if(!is_visible_pack(usr, P.access , null, P.contraband)) + if(!is_visible_pack(usr, P.access , null, P.contraband) || P.hidden) continue if(!data["supplies"][P.group]) data["supplies"][P.group] = list( "name" = P.group, "packs" = list() ) - if(((P.hidden || P.contraband) && !contraband_view) || (P.special && !P.special_enabled) || P.DropPodOnly) + if((P.hidden && (P.contraband && !contraband) || (P.special && !P.special_enabled) || P.DropPodOnly)) continue data["supplies"][P.group]["packs"] += list(list( "name" = P.name, @@ -105,7 +114,7 @@ //Data regarding the User's capability to buy things. data["has_id"] = id_card - data["away"] = SSshuttle.supply.getDockedId() == "supply_away" + data["away"] = SSshuttle.supply.getDockedId() == docking_away data["self_paid"] = self_paid data["docked"] = SSshuttle.supply.mode == SHUTTLE_IDLE data["loan"] = !!SSshuttle.shuttle_loan @@ -153,15 +162,15 @@ if(SSshuttle.supplyBlocked) computer.say(blockade_warning) return - if(SSshuttle.supply.getDockedId() == "supply_home") + if(SSshuttle.supply.getDockedId() == docking_home) SSshuttle.supply.export_categories = get_export_categories() - SSshuttle.moveShuttle("supply", "supply_away", TRUE) + SSshuttle.moveShuttle(cargo_shuttle, docking_away, TRUE) computer.say("The supply shuttle is departing.") computer.investigate_log("[key_name(usr)] sent the supply shuttle away.", INVESTIGATE_CARGO) else computer.investigate_log("[key_name(usr)] called the supply shuttle.", INVESTIGATE_CARGO) computer.say("The supply shuttle has been called and will arrive in [SSshuttle.supply.timeLeft(600)] minutes.") - SSshuttle.moveShuttle("supply", "supply_home", TRUE) + SSshuttle.moveShuttle(cargo_shuttle, docking_home, TRUE) . = TRUE if("loan") if(!SSshuttle.shuttle_loan) @@ -171,7 +180,9 @@ return else if(SSshuttle.supply.mode != SHUTTLE_IDLE) return - else if(SSshuttle.supply.getDockedId() != "supply_away") + else if(SSshuttle.supply.getDockedId() != docking_away) + return + else if(stationcargo != TRUE) return else SSshuttle.shuttle_loan.loan_shuttle() @@ -184,7 +195,7 @@ var/datum/supply_pack/pack = SSshuttle.supply_packs[id] if(!istype(pack)) return - if(((pack.hidden || pack.contraband) && !contraband_view) || pack.DropPodOnly) + if((pack.hidden && (pack.contraband && !contraband) || pack.DropPodOnly)) return var/name = "*None Provided*" @@ -273,7 +284,7 @@ self_paid = !self_paid . = TRUE if(.) - post_signal("supply") + post_signal(cargo_shuttle) /datum/computer_file/program/budgetorders/proc/post_signal(command) diff --git a/code/modules/modular_computers/file_system/programs/card.dm b/code/modules/modular_computers/file_system/programs/card.dm index 65bb5f2343..18e717191e 100644 --- a/code/modules/modular_computers/file_system/programs/card.dm +++ b/code/modules/modular_computers/file_system/programs/card.dm @@ -9,6 +9,7 @@ /datum/computer_file/program/card_mod filename = "plexagonidwriter" filedesc = "Plexagon Access Management" + category = PROGRAM_CATEGORY_CREW program_icon_state = "id" extended_desc = "Program for programming employee ID cards to access parts of the station." transfer_access = ACCESS_HEADS diff --git a/code/modules/modular_computers/file_system/programs/cargoship.dm b/code/modules/modular_computers/file_system/programs/cargoship.dm index 89a3b3247d..672c0de470 100644 --- a/code/modules/modular_computers/file_system/programs/cargoship.dm +++ b/code/modules/modular_computers/file_system/programs/cargoship.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/shipping filename = "shipping" filedesc = "GrandArk Exporter" + category = PROGRAM_CATEGORY_SUPL program_icon_state = "shipping" extended_desc = "A combination printer/scanner app that enables modular computers to print barcodes for easy scanning and shipping." size = 6 @@ -8,8 +9,12 @@ program_icon = "tags" ///Account used for creating barcodes. var/datum/bank_account/payments_acc - ///The amount which the tagger will receive for the sale. - var/percent_cut = 20 + ///The person who tagged this will receive the sale value multiplied by this number. + var/cut_multiplier = 0.5 + ///Maximum value for cut_multiplier. + var/cut_max = 0.5 + ///Minimum value for cut_multiplier. + var/cut_min = 0.01 /datum/computer_file/program/shipping/ui_data(mob/user) var/list/data = get_header_data() @@ -22,7 +27,7 @@ data["paperamt"] = printer ? "[printer.stored_paper] / [printer.max_paper]" : null data["card_owner"] = card_slot?.stored_card ? id_card.registered_name : "No Card Inserted." data["current_user"] = payments_acc ? payments_acc.account_holder : null - data["barcode_split"] = percent_cut + data["barcode_split"] = cut_multiplier * 100 return data /datum/computer_file/program/shipping/ui_act(action, list/params) @@ -54,20 +59,20 @@ if("resetid") payments_acc = null if("setsplit") - var/potential_cut = input("How much would you like to payout to the registered card?","Percentage Profit") as num|null - percent_cut = potential_cut ? clamp(round(potential_cut, 1), 1, 50) : 20 + var/potential_cut = input("How much would you like to pay out to the registered card?","Percentage Profit ([round(cut_min*100)]% - [round(cut_max*100)]%)") as num|null + cut_multiplier = potential_cut ? clamp(round(potential_cut/100, cut_min), cut_min, cut_max) : initial(cut_multiplier) if("print") if(!printer) - to_chat(usr, "Hardware error: A printer is required to print barcodes.") + to_chat(usr, span_notice("Hardware error: A printer is required to print barcodes.")) return if(printer.stored_paper <= 0) - to_chat(usr, "Hardware error: Printer is out of paper.") + to_chat(usr, span_notice("Hardware error: Printer is out of paper.")) return if(!payments_acc) - to_chat(usr, "Software error: Please set a current user first.") + to_chat(usr, span_notice("Software error: Please set a current user first.")) return var/obj/item/barcode/barcode = new /obj/item/barcode(get_turf(ui_host())) barcode.payments_acc = payments_acc - barcode.percent_cut = percent_cut + barcode.cut_multiplier = cut_multiplier printer.stored_paper-- - to_chat(usr, "The computer prints out a barcode.") + to_chat(usr, span_notice("The computer prints out a barcode.")) diff --git a/code/modules/modular_computers/file_system/programs/crewmanifest.dm b/code/modules/modular_computers/file_system/programs/crewmanifest.dm index debe87259d..6837f315a4 100644 --- a/code/modules/modular_computers/file_system/programs/crewmanifest.dm +++ b/code/modules/modular_computers/file_system/programs/crewmanifest.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/crew_manifest filename = "plexagoncrew" filedesc = "Plexagon Crew List" + category = PROGRAM_CATEGORY_CREW program_icon_state = "id" extended_desc = "Program for viewing and printing the current crew manifest" transfer_access = ACCESS_HEADS @@ -44,7 +45,7 @@ [GLOB.data_core ? GLOB.data_core.get_manifest() : ""] "} if(!printer.print_text(contents,text("crew manifest ([])", STATION_TIME_TIMESTAMP("hh:mm:ss", world.time)))) - to_chat(usr, "Hardware error: Printer was unable to print the file. It may be out of paper.") + to_chat(usr, span_notice("Hardware error: Printer was unable to print the file. It may be out of paper.")) return else - computer.visible_message("\The [computer] prints out a paper.") + computer.visible_message(span_notice("\The [computer] prints out a paper.")) diff --git a/code/modules/modular_computers/file_system/programs/file_browser.dm b/code/modules/modular_computers/file_system/programs/file_browser.dm index 97a71496ea..60a9536543 100644 --- a/code/modules/modular_computers/file_system/programs/file_browser.dm +++ b/code/modules/modular_computers/file_system/programs/file_browser.dm @@ -38,14 +38,27 @@ return RHDD.remove_file(file) return TRUE - if("PRG_rename") + if("PRG_renamefile") if(!HDD) return var/datum/computer_file/file = HDD.find_file_by_name(params["name"]) if(!file) return - var/newname = params["new_name"] - if(!newname) + var/newname = reject_bad_name(params["new_name"]) + if(!newname || newname != params["new_name"]) + playsound(computer, 'sound/machines/terminal_error.ogg', 25, FALSE) + return + file.filename = newname + return TRUE + if("PRG_usbrenamefile") + if(!RHDD) + return + var/datum/computer_file/file = RHDD.find_file_by_name(params["name"]) + if(!file) + return + var/newname = reject_bad_name(params["new_name"]) + if(!newname || newname != params["new_name"]) + playsound(computer, 'sound/machines/terminal_error.ogg', 25, FALSE) return file.filename = newname return TRUE diff --git a/code/modules/modular_computers/file_system/programs/jobmanagement.dm b/code/modules/modular_computers/file_system/programs/jobmanagement.dm index 0babfcdddd..29f9a37433 100644 --- a/code/modules/modular_computers/file_system/programs/jobmanagement.dm +++ b/code/modules/modular_computers/file_system/programs/jobmanagement.dm @@ -1,6 +1,10 @@ +/// The time since the last job opening was created +// GLOBAL_VAR_INIT(time_last_changed_position, 0) + /datum/computer_file/program/job_management filename = "plexagoncore" filedesc = "Plexagon HR Core" + category = PROGRAM_CATEGORY_CREW program_icon_state = "id" extended_desc = "Program for viewing and changing job slot avalibility." transfer_access = ACCESS_HEADS @@ -14,6 +18,7 @@ var/list/blacklisted = list( "AI", "Assistant", + "Prisoner", "Cyborg", "Captain", "Head of Personnel", @@ -35,21 +40,25 @@ change_position_cooldown = CONFIG_GET(number/id_console_jobslot_delay) /datum/computer_file/program/job_management/proc/can_open_job(datum/job/job) - if(!(job?.title in blacklisted)) - if((job.total_positions <= length(GLOB.player_list) * (max_relative_positions / 100))) - var/delta = (world.time / 10) - GLOB.time_last_changed_position - if((change_position_cooldown < delta) || (opened_positions[job.title] < 0)) - return TRUE + if(job?.title in blacklisted) + return FALSE + if((job.total_positions <= length(GLOB.player_list) * (max_relative_positions / 100))) + var/delta = (world.time / 10) - GLOB.time_last_changed_position + if((change_position_cooldown < delta) || (opened_positions[job.title] < 0)) + return TRUE return FALSE + /datum/computer_file/program/job_management/proc/can_close_job(datum/job/job) - if(!(job?.title in blacklisted)) - if(job.total_positions > job.current_positions) - var/delta = (world.time / 10) - GLOB.time_last_changed_position - if((change_position_cooldown < delta) || (opened_positions[job.title] > 0)) - return TRUE + if(job?.title in blacklisted) + return FALSE + if(job.total_positions > job.current_positions) + var/delta = (world.time / 10) - GLOB.time_last_changed_position + if((change_position_cooldown < delta) || (opened_positions[job.title] > 0)) + return TRUE return FALSE + /datum/computer_file/program/job_management/ui_act(action, params, datum/tgui/ui) . = ..() if(.) @@ -68,9 +77,10 @@ if(!j || !can_open_job(j)) return if(opened_positions[edit_job_target] >= 0) - GLOB.time_last_changed_position = world.time / 10 // global cd + GLOB.time_last_changed_position = world.time / 10 j.total_positions++ opened_positions[edit_job_target]++ + log_game("[key_name(usr)] opened a [j.title] job position, for a total of [j.total_positions] open job slots.") playsound(computer, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE) return TRUE if("PRG_close_job") @@ -83,21 +93,23 @@ GLOB.time_last_changed_position = world.time / 10 j.total_positions-- opened_positions[edit_job_target]-- + log_game("[key_name(usr)] closed a [j.title] job position, leaving [j.total_positions] open job slots.") playsound(computer, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE) return TRUE if("PRG_priority") - if(length(SSjob.prioritized_jobs) >= 5) - return var/priority_target = params["target"] var/datum/job/j = SSjob.GetJob(priority_target) - if(!j) + if(!j || (j?.title in blacklisted)) return if(j.total_positions <= j.current_positions) return if(j in SSjob.prioritized_jobs) SSjob.prioritized_jobs -= j else - SSjob.prioritized_jobs += j + if(length(SSjob.prioritized_jobs) < 5) + SSjob.prioritized_jobs += j + else + computer.say("Error: CentCom employment protocols restrict prioritising more than 5 jobs.") playsound(computer, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE) return TRUE diff --git a/code/modules/modular_computers/file_system/programs/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/ntdownloader.dm index f3fa6df2b3..56283e3c8b 100644 --- a/code/modules/modular_computers/file_system/programs/ntdownloader.dm +++ b/code/modules/modular_computers/file_system/programs/ntdownloader.dm @@ -22,6 +22,13 @@ var/emagged = FALSE var/list/main_repo var/list/antag_repo + var/list/show_categories = list( + PROGRAM_CATEGORY_CREW, + PROGRAM_CATEGORY_ENGI, + PROGRAM_CATEGORY_ROBO, + PROGRAM_CATEGORY_SUPL, + PROGRAM_CATEGORY_MISC, + ) /datum/computer_file/program/ntnetdownload/run_program() . = ..() @@ -146,39 +153,29 @@ var/obj/item/computer_hardware/hard_drive/hard_drive = my_computer.all_components[MC_HDD] data["disk_size"] = hard_drive.max_capacity data["disk_used"] = hard_drive.used_capacity - var/list/all_entries[0] - for(var/A in main_repo) - var/datum/computer_file/program/P = A - // Only those programs our user can run will show in the list - if(hard_drive.find_file_by_name(P.filename)) - continue - all_entries.Add(list(list( + data["emagged"] = emagged + + var/list/repo = antag_repo | main_repo + var/list/program_categories = list() + + for(var/I in repo) + var/datum/computer_file/program/P = I + if(!(P.category in program_categories)) + program_categories.Add(P.category) + data["programs"] += list(list( + "icon" = P.program_icon, "filename" = P.filename, "filedesc" = P.filedesc, "fileinfo" = P.extended_desc, - "compatibility" = check_compatibility(P), + "category" = P.category, + "installed" = !!hard_drive.find_file_by_name(P.filename), + "compatible" = check_compatibility(P), "size" = P.size, - "access" = P.can_run(user,transfer = 1, access = access) - ))) - data["hackedavailable"] = FALSE - if(emagged) // If we are running on emagged computer we have access to some "bonus" software - var/list/hacked_programs[0] - for(var/S in antag_repo) - var/datum/computer_file/program/P = S - if(hard_drive.find_file_by_name(P.filename)) - continue - data["hackedavailable"] = TRUE - hacked_programs.Add(list(list( - "filename" = P.filename, - "filedesc" = P.filedesc, - "fileinfo" = P.extended_desc, - "compatibility" = check_compatibility(P), - "size" = P.size, - "access" = TRUE, - ))) - data["hacked_programs"] = hacked_programs + "access" = emagged && P.available_on_syndinet ? TRUE : P.can_run(user,transfer = 1, access = access), + "verifiedsource" = P.available_on_ntnet, + )) - data["downloadable_programs"] = all_entries + data["categories"] = show_categories & program_categories return data @@ -186,8 +183,8 @@ var/hardflag = computer.hardware_flag if(P?.is_supported_by_hardware(hardflag,0)) - return "Compatible" - return "Incompatible!" + return TRUE + return FALSE /datum/computer_file/program/ntnetdownload/kill_program(forced) abort_file_download() diff --git a/code/modules/modular_computers/file_system/programs/ntmonitor.dm b/code/modules/modular_computers/file_system/programs/ntmonitor.dm index 63f0b18a74..aeb8106040 100644 --- a/code/modules/modular_computers/file_system/programs/ntmonitor.dm +++ b/code/modules/modular_computers/file_system/programs/ntmonitor.dm @@ -1,11 +1,12 @@ /datum/computer_file/program/ntnetmonitor filename = "wirecarp" filedesc = "WireCarp" + category = PROGRAM_CATEGORY_MISC program_icon_state = "comm_monitor" extended_desc = "This program monitors stationwide NTNet network, provides access to logging systems, and allows for configuration changes" size = 12 requires_ntnet = TRUE - required_access = ACCESS_NETWORK //NETWORK CONTROL IS A MORE SECURE PROGRAM. + required_access = ACCESS_NETWORK //NETWORK CONTROL IS A MORE SECURE PROGRAM. available_on_ntnet = TRUE tgui_id = "NtosNetMonitor" program_icon = "network-wired" diff --git a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm index 19172f130a..29a4418d2c 100644 --- a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm +++ b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm @@ -1,25 +1,36 @@ + /datum/computer_file/program/chatclient filename = "ntnrc_client" filedesc = "Chat Client" + category = PROGRAM_CATEGORY_MISC program_icon_state = "command" extended_desc = "This program allows communication over NTNRC network" size = 8 - requires_ntnet = 1 + requires_ntnet = TRUE requires_ntnet_feature = NTNET_COMMUNICATION ui_header = "ntnrc_idle.gif" - available_on_ntnet = 1 + available_on_ntnet = TRUE tgui_id = "NtosNetChat" program_icon = "comment-alt" - var/last_message // Used to generate the toolbar icon + alert_able = TRUE + var/last_message // Used to generate the toolbar icon var/username var/active_channel var/list/channel_history = list() - var/operator_mode = FALSE // Channel operator mode - var/netadmin_mode = FALSE // Administrator mode (invisible to other users + bypasses passwords) + var/operator_mode = FALSE // Channel operator mode + var/netadmin_mode = FALSE // Administrator mode (invisible to other users + bypasses passwords) + //A list of all the converstations we're a part of + var/list/datum/ntnet_conversation/conversations = list() /datum/computer_file/program/chatclient/New() username = "DefaultUser[rand(100, 999)]" +/datum/computer_file/program/chatclient/Destroy() + for(var/datum/ntnet_conversation/discussion as anything in conversations) + discussion.purge_client(src) + conversations.Cut() + return ..() + /datum/computer_file/program/chatclient/ui_act(action, params) . = ..() if(.) @@ -36,7 +47,7 @@ var/message = reject_bad_text(params["message"]) if(!message) return - if(channel.password && !(src in channel.clients)) + if(channel.password && (!(src in channel.active_clients) && !(src in channel.offline_clients))) if(channel.password == message) channel.add_client(src) return TRUE @@ -56,7 +67,7 @@ active_channel = new_target channel = SSnetworks.station_network.get_chat_channel_by_id(new_target) - if(!(src in channel.clients) && !channel.password) + if((!(src in channel.active_clients) && !(src in channel.offline_clients)) && !channel.password) channel.add_client(src) return TRUE if("PRG_leavechannel") @@ -89,12 +100,12 @@ return TRUE if("PRG_changename") var/newname = sanitize(params["new_name"]) - if(!newname) + newname = replacetext(newname, " ", "_") + if(!newname || newname == username) return - for(var/C in SSnetworks.station_network.chat_channels) - var/datum/ntnet_conversation/chan = C - if(src in chan.clients) - chan.add_status_message("[username] is now known as [newname].") + for(var/datum/ntnet_conversation/anychannel as anything in SSnetworks.station_network.chat_channels) + if(src in anychannel.active_clients) + anychannel.add_status_message("[username] is now known as [newname].") username = newname return TRUE if("PRG_savelog") @@ -117,9 +128,9 @@ // This program shouldn't even be runnable without computer. CRASH("Var computer is null!") if(!hard_drive) - computer.visible_message("\The [computer] shows an \"I/O Error - Hard drive connection error\" warning.") - else // In 99.9% cases this will mean our HDD is full - computer.visible_message("\The [computer] shows an \"I/O Error - Hard drive may be full. Please free some space and try again. Required space: [logfile.size]GQ\" warning.") + computer.visible_message(span_warning("\The [computer] shows an \"I/O Error - Hard drive connection error\" warning.")) + else // In 99.9% cases this will mean our HDD is full + computer.visible_message(span_warning("\The [computer] shows an \"I/O Error - Hard drive may be full. Please free some space and try again. Required space: [logfile.size]GQ\" warning.")) return TRUE if("PRG_renamechannel") if(!authed) @@ -145,6 +156,18 @@ channel.password = new_password return TRUE + if("PRG_mute_user") + if(!authed) + return + var/datum/computer_file/program/chatclient/muted = locate(params["ref"]) in channel.active_clients + channel.offline_clients + channel.mute_user(src, muted) + return TRUE + if("PRG_ping_user") + if(!authed) + return + var/datum/computer_file/program/chatclient/pinged = locate(params["ref"]) in channel.active_clients + channel.offline_clients + channel.ping_user(src, pinged) + return TRUE /datum/computer_file/program/chatclient/process_tick() . = ..() @@ -162,10 +185,19 @@ else ui_header = "ntnrc_idle.gif" +/datum/computer_file/program/chatclient/run_program(mob/living/user) + . = ..() + if(!.) + return + for(var/datum/ntnet_conversation/channel as anything in SSnetworks.station_network.chat_channels) + if(src in channel.offline_clients) + channel.offline_clients.Remove(src) + channel.active_clients.Add(src) + /datum/computer_file/program/chatclient/kill_program(forced = FALSE) - for(var/C in SSnetworks.station_network.chat_channels) - var/datum/ntnet_conversation/channel = C - channel.remove_client(src) + for(var/datum/ntnet_conversation/channel as anything in SSnetworks.station_network.chat_channels) + channel.go_offline(src) + active_channel = null ..() /datum/computer_file/program/chatclient/ui_static_data(mob/user) @@ -192,6 +224,7 @@ data["all_channels"] = all_channels data["active_channel"] = active_channel + data["selfref"] = REF(src) //used to verify who is you, as usernames can be copied. data["username"] = username data["adminmode"] = netadmin_mode var/datum/ntnet_conversation/channel = SSnetworks.station_network.get_chat_channel_by_id(active_channel) @@ -203,21 +236,25 @@ if(netadmin_mode) authed = TRUE var/list/clients = list() - for(var/C in channel.clients) - if(C == src) + for(var/datum/computer_file/program/chatclient/channel_client as anything in channel.active_clients + channel.offline_clients) + if(channel_client == src) authed = TRUE - var/datum/computer_file/program/chatclient/cl = C clients.Add(list(list( - "name" = cl.username + "name" = channel_client.username, + "status" = channel_client.program_state, + "muted" = (channel_client in channel.muted_clients), + "operator" = channel.operator == channel_client, + "ref" = REF(channel_client) ))) data["authed"] = authed //no fishing for ui data allowed if(authed) + data["strong"] = channel.strong data["clients"] = clients var/list/messages = list() - for(var/M in channel.messages) + for(var/message in channel.messages) messages.Add(list(list( - "msg" = M + "msg" = message ))) data["messages"] = messages data["is_operator"] = (channel.operator == src) || netadmin_mode diff --git a/code/modules/modular_computers/file_system/programs/portrait_printer.dm b/code/modules/modular_computers/file_system/programs/portrait_printer.dm new file mode 100644 index 0000000000..ac3b7e5a88 --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/portrait_printer.dm @@ -0,0 +1,83 @@ + +///how much paper it takes from the printer to create a canvas. +#define CANVAS_PAPER_COST 10 + +/** + * ## portrait printer! + * + * Program that lets the curator browse all of the portraits in the database + * They are free to print them out as they please. + */ +/datum/computer_file/program/portrait_printer + filename = "PortraitPrinter" + filedesc = "Marlowe Treeby's Art Galaxy" + category = PROGRAM_CATEGORY_CREW + program_icon_state = "dummy" + extended_desc = "This program connects to a Spinward Sector community art site for viewing and printing art." + transfer_access = ACCESS_LIBRARY + usage_flags = PROGRAM_CONSOLE + requires_ntnet = TRUE + size = 9 + tgui_id = "NtosPortraitPrinter" + program_icon = "paint-brush" + +/datum/computer_file/program/portrait_printer/ui_data(mob/user) + var/list/data = list() + data["library"] = SSpersistence.paintings["library"] ? SSpersistence.paintings["library"] : 0 + data["library_secure"] = SSpersistence.paintings["library_secure"] ? SSpersistence.paintings["library_secure"] : 0 + data["library_private"] = SSpersistence.paintings["library_private"] ? SSpersistence.paintings["library_private"] : 0 //i'm gonna regret this, won't i? Yes you should. + return data + +/datum/computer_file/program/portrait_printer/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/simple/portraits/library), + get_asset_datum(/datum/asset/simple/portraits/library_secure), + get_asset_datum(/datum/asset/simple/portraits/library_private) + ) + +/datum/computer_file/program/portrait_printer/ui_act(action, params) + . = ..() + if(.) + return + + //printer check! + var/obj/item/computer_hardware/printer/printer + if(computer) + printer = computer.all_components[MC_PRINT] + if(!printer) + to_chat(usr, span_notice("Hardware error: A printer is required to print a canvas.")) + return + if(printer.stored_paper < CANVAS_PAPER_COST) + to_chat(usr, span_notice("Printing error: Your printer needs at least [CANVAS_PAPER_COST] paper to print a canvas.")) + return + printer.stored_paper -= CANVAS_PAPER_COST + + //canvas printing! + var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private") + var/folder = tab2key[params["tab"]] + var/list/current_list = SSpersistence.paintings[folder] + var/list/chosen_portrait = current_list[params["selected"]] + var/author = chosen_portrait["author"] + var/title = chosen_portrait["title"] + var/png = "data/paintings/[folder]/[chosen_portrait["md5"]].png" + var/icon/art_icon = new(png) + var/obj/item/canvas/printed_canvas + var/art_width = art_icon.Width() + var/art_height = art_icon.Height() + for(var/canvas_type in typesof(/obj/item/canvas)) + printed_canvas = canvas_type + if(initial(printed_canvas.width) == art_width && initial(printed_canvas.height) == art_height) + printed_canvas = new canvas_type(get_turf(computer.physical)) + break + printed_canvas.fill_grid_from_icon(art_icon) + printed_canvas.generated_icon = art_icon + printed_canvas.icon_generated = TRUE + printed_canvas.finalized = TRUE + printed_canvas.painting_name = title + printed_canvas.author_ckey = author + printed_canvas.name = "painting - [title]" + ///this is a copy of something that is already in the database- it should not be able to be saved. + printed_canvas.no_save = TRUE + printed_canvas.update_icon() + to_chat(usr, span_notice("You have printed [title] onto a new canvas.")) + playsound(computer.physical, 'sound/items/poster_being_created.ogg', 100, TRUE) diff --git a/code/modules/modular_computers/file_system/programs/powermonitor.dm b/code/modules/modular_computers/file_system/programs/powermonitor.dm index 78a14ff1ad..933c9410a2 100644 --- a/code/modules/modular_computers/file_system/programs/powermonitor.dm +++ b/code/modules/modular_computers/file_system/programs/powermonitor.dm @@ -3,6 +3,7 @@ /datum/computer_file/program/power_monitor filename = "ampcheck" filedesc = "AmpCheck" + category = PROGRAM_CATEGORY_ENGI program_icon_state = "power_monitor" extended_desc = "This program connects to sensors around the station to provide information about electrical systems" ui_header = "power_norm.gif" diff --git a/code/modules/modular_computers/file_system/programs/radar.dm b/code/modules/modular_computers/file_system/programs/radar.dm index 0bf5eb2118..4b14a61152 100644 --- a/code/modules/modular_computers/file_system/programs/radar.dm +++ b/code/modules/modular_computers/file_system/programs/radar.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/radar //generic parent that handles most of the process filename = "genericfinder" filedesc = "debug_finder" + category = PROGRAM_CATEGORY_CREW ui_header = "borg_mon.gif" //DEBUG -- new icon before PR program_icon_state = "radarntos" requires_ntnet = TRUE @@ -15,7 +16,7 @@ var/atom/selected ///Used to store when the next scan is available. Updated by the scan() proc. var/next_scan = 0 - ///Used to keep track of the last value program_icon_state was set to, to prevent constant unnecessary update_icon() calls + ///Used to keep track of the last value program_icon_state was set to, to prevent constant unnecessary update_appearance() calls var/last_icon_state = "" ///Used by the tgui interface, themed NT or Syndicate. var/arrowstyle = "ntosradarpointer.png" @@ -174,7 +175,7 @@ if(!trackable(signal)) program_icon_state = "[initial(program_icon_state)]lost" if(last_icon_state != program_icon_state) - computer.update_icon() + computer.update_appearance() last_icon_state = program_icon_state return @@ -192,7 +193,7 @@ program_icon_state = "[initial(program_icon_state)]far" if(last_icon_state != program_icon_state) - computer.update_icon() + computer.update_appearance() last_icon_state = program_icon_state computer.setDir(get_dir(here_turf, target_turf)) @@ -241,13 +242,12 @@ /datum/computer_file/program/radar/lifeline/trackable(mob/living/carbon/human/humanoid) if(!humanoid || !istype(humanoid)) return FALSE - if(..() && istype(humanoid.w_uniform, /obj/item/clothing/under)) - - var/obj/item/clothing/under/uniform = humanoid.w_uniform - if(!uniform.has_sensor || (uniform.sensor_mode < SENSOR_COORDS)) // Suit sensors must be on maximum. - return FALSE - - return TRUE + if(..()) + if (istype(humanoid.w_uniform, /obj/item/clothing/under)) + var/obj/item/clothing/under/uniform = humanoid.w_uniform + if(uniform.has_sensor && uniform.sensor_mode >= SENSOR_COORDS) // Suit sensors must be on maximum + return TRUE + return FALSE //////////////////////// //Nuke Disk Finder App// @@ -257,6 +257,7 @@ /datum/computer_file/program/radar/fission360 filename = "fission360" filedesc = "Fission360" + category = PROGRAM_CATEGORY_MISC program_icon_state = "radarsyndicate" extended_desc = "This program allows for tracking of nuclear authorization disks and warheads." requires_ntnet = FALSE diff --git a/code/modules/modular_computers/file_system/programs/robocontrol.dm b/code/modules/modular_computers/file_system/programs/robocontrol.dm index 8c41ea6c38..7fe5a09ab2 100644 --- a/code/modules/modular_computers/file_system/programs/robocontrol.dm +++ b/code/modules/modular_computers/file_system/programs/robocontrol.dm @@ -2,6 +2,7 @@ /datum/computer_file/program/robocontrol filename = "botkeeper" filedesc = "BotKeeper" + category = PROGRAM_CATEGORY_ROBO program_icon_state = "robot" extended_desc = "A remote controller used for giving basic commands to non-sentient robots." transfer_access = null diff --git a/code/modules/modular_computers/file_system/programs/robotact.dm b/code/modules/modular_computers/file_system/programs/robotact.dm index b25332d027..d4dce42204 100644 --- a/code/modules/modular_computers/file_system/programs/robotact.dm +++ b/code/modules/modular_computers/file_system/programs/robotact.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/robotact filename = "robotact" filedesc = "RoboTact" + category = PROGRAM_CATEGORY_ROBO extended_desc = "A built-in app for cyborg self-management and diagnostics." ui_header = "robotact.gif" //DEBUG -- new icon before PR program_icon_state = "command" @@ -22,7 +23,7 @@ /datum/computer_file/program/robotact/run_program(mob/living/user) if(!istype(computer, /obj/item/modular_computer/tablet/integrated)) - to_chat(user, "A warning flashes across \the [computer]: Device Incompatible.") + to_chat(user, span_warning("A warning flashes across \the [computer]: Device Incompatible.")) return FALSE . = ..() if(.) @@ -39,7 +40,7 @@ var/mob/living/silicon/robot/borgo = tablet.borgo data["name"] = borgo.name - data["designation"] = borgo.designation //Borgo module type + data["designation"] = borgo.designation //Borgo model type data["masterAI"] = borgo.connected_ai //Master AI var/charge = 0 @@ -62,7 +63,7 @@ data["cover"] = "[borgo.locked? "LOCKED":"UNLOCKED"]" //Ability to move. FAULT if lockdown wire is cut, DISABLED if borg locked, ENABLED otherwise data["locomotion"] = "[borgo.wires.is_cut(WIRE_LOCKDOWN)?"FAULT":"[borgo.locked_down?"DISABLED":"ENABLED"]"]" - //Module wire. FAULT if cut, NOMINAL otherwise + //Model wire. FAULT if cut, NOMINAL otherwise data["wireModule"] = "[borgo.wires.is_cut(WIRE_RESET_MODULE)?"FAULT":"NOMINAL"]" //DEBUG -- Camera(net) wire. FAULT if cut (or no cameranet camera), DISABLED if pulse-disabled, NOMINAL otherwise data["wireCamera"] = "[!borgo.builtInCamera || borgo.wires.is_cut(WIRE_CAMERA)?"FAULT":"[borgo.builtInCamera.can_use()?"NOMINAL":"DISABLED"]"]" @@ -110,7 +111,7 @@ if("alertPower") if(borgo.stat == CONSCIOUS) if(!borgo.cell || !borgo.cell.charge) - borgo.visible_message("The power warning light on [borgo] flashes urgently.", \ + borgo.visible_message(span_notice("The power warning light on [span_name("[borgo]")] flashes urgently."), \ "You announce you are operating in low power mode.") playsound(borgo, 'sound/machines/buzz-two.ogg', 50, FALSE) diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm index 92275b1e8b..d2d7590cb1 100644 --- a/code/modules/modular_computers/file_system/programs/secureye.dm +++ b/code/modules/modular_computers/file_system/programs/secureye.dm @@ -3,6 +3,7 @@ /datum/computer_file/program/secureye filename = "secureye" filedesc = "SecurEye" + category = PROGRAM_CATEGORY_MISC ui_header = "borg_mon.gif" program_icon_state = "generic" extended_desc = "This program allows access to standard security camera networks." diff --git a/code/modules/modular_computers/file_system/programs/signaler.dm b/code/modules/modular_computers/file_system/programs/signaler.dm new file mode 100644 index 0000000000..b7bbcacaa0 --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/signaler.dm @@ -0,0 +1,83 @@ +/datum/computer_file/program/signaler + filename = "signaler" + filedesc = "SignalCommander" + category = PROGRAM_CATEGORY_MISC + program_icon_state = "signal" + extended_desc = "A small built-in frequency app that sends out signaller signals with the appropriate hardware." + size = 2 + tgui_id = "NtosSignaler" + program_icon = "satellite-dish" + usage_flags = PROGRAM_TABLET | PROGRAM_LAPTOP + ///What is the saved signal frequency? + var/signal_frequency = FREQ_SIGNALER + /// What is the saved signal code? + var/signal_code = DEFAULT_SIGNALER_CODE + /// Radio connection datum used by signalers. + var/datum/radio_frequency/radio_connection + +/datum/computer_file/program/signaler/run_program(mob/living/user) + . = ..() + if (!.) + return + if(!computer?.get_modular_computer_part(MC_SIGNALER)) //Giving a clue to users why the program is spitting out zeros. + to_chat(user, span_warning("\The [computer] flashes an error: \"hardware\\signal_hardware\\startup.bin -- file not found\".")) + + +/datum/computer_file/program/signaler/ui_data(mob/user) + var/list/data = get_header_data() + var/obj/item/computer_hardware/radio_card/sensor = computer?.get_modular_computer_part(MC_SIGNALER) + if(sensor?.check_functionality()) + data["frequency"] = signal_frequency + data["code"] = signal_code + data["minFrequency"] = MIN_FREE_FREQ + data["maxFrequency"] = MAX_FREE_FREQ + return data + +/datum/computer_file/program/signaler/ui_act(action, list/params) + . = ..() + if(.) + return + var/obj/item/computer_hardware/radio_card/sensor = computer?.get_modular_computer_part(MC_SIGNALER) + if(!(sensor?.check_functionality())) + playsound(src, 'sound/machines/scanbuzz.ogg', 100, FALSE) + return + switch(action) + if("signal") + INVOKE_ASYNC(src, .proc/signal) + . = TRUE + if("freq") + signal_frequency = unformat_frequency(params["freq"]) + signal_frequency = sanitize_frequency(signal_frequency, TRUE) + set_frequency(signal_frequency) + . = TRUE + if("code") + signal_code = text2num(params["code"]) + signal_code = round(signal_code) + . = TRUE + if("reset") + if(params["reset"] == "freq") + signal_frequency = initial(signal_frequency) + else + signal_code = initial(signal_code) + . = TRUE + +/datum/computer_file/program/signaler/proc/signal() + if(!radio_connection) + return + + var/time = time2text(world.realtime,"hh:mm:ss") + var/turf/T = get_turf(src) + + var/logging_data + if(usr) + logging_data = "[time] : [usr.key] used [src] @ location ([T.x],[T.y],[T.z]) : [format_frequency(signal_frequency)]/[signal_code]" + GLOB.lastsignalers.Add(logging_data) + + var/datum/signal/signal = new(list("code" = signal_code), logging_data = logging_data) + radio_connection.post_signal(src, signal) + +/datum/computer_file/program/signaler/proc/set_frequency(new_frequency) + SSradio.remove_object(src, signal_frequency) + signal_frequency = new_frequency + radio_connection = SSradio.add_object(src, signal_frequency, RADIO_SIGNALER) + return diff --git a/code/modules/modular_computers/file_system/programs/sm_monitor.dm b/code/modules/modular_computers/file_system/programs/sm_monitor.dm index 6c9ce59c72..9038c71a88 100644 --- a/code/modules/modular_computers/file_system/programs/sm_monitor.dm +++ b/code/modules/modular_computers/file_system/programs/sm_monitor.dm @@ -1,6 +1,7 @@ /datum/computer_file/program/supermatter_monitor filename = "ntcims" filedesc = "NT CIMS" + category = PROGRAM_CATEGORY_ENGI ui_header = "smmon_0.gif" program_icon_state = "smmon_0" extended_desc = "Crystal Integrity Monitoring System, connects to specially calibrated supermatter sensors to provide information on the status of supermatter-based engines." @@ -12,7 +13,7 @@ alert_able = TRUE var/last_status = SUPERMATTER_INACTIVE var/list/supermatters - var/obj/machinery/power/supermatter_crystal/active // Currently selected supermatter crystal. + var/obj/machinery/power/supermatter_crystal/active // Currently selected supermatter crystal. /datum/computer_file/program/supermatter_monitor/Destroy() clear_signals() @@ -27,7 +28,7 @@ ui_header = "smmon_[last_status].gif" program_icon_state = "smmon_[last_status]" if(istype(computer)) - computer.update_icon() + computer.update_appearance() /datum/computer_file/program/supermatter_monitor/run_program(mob/living/user) . = ..(user) @@ -36,11 +37,15 @@ refresh() /datum/computer_file/program/supermatter_monitor/kill_program(forced = FALSE) + for(var/supermatter in supermatters) + clear_supermatter(supermatter) supermatters = null ..() // Refreshes list of active supermatter crystals /datum/computer_file/program/supermatter_monitor/proc/refresh() + for(var/supermatter in supermatters) + clear_supermatter(supermatter) supermatters = list() var/turf/T = get_turf(ui_host()) if(!T) @@ -50,9 +55,7 @@ if (!isturf(S.loc) || !(is_station_level(S.z) || is_mining_level(S.z) || S.z == T.z)) continue supermatters.Add(S) - - if(!(active in supermatters)) - active = null + RegisterSignal(S, COMSIG_PARENT_QDELETING, .proc/react_to_del) /datum/computer_file/program/supermatter_monitor/proc/get_status() . = SUPERMATTER_INACTIVE @@ -67,9 +70,9 @@ * the signal and exit. */ /datum/computer_file/program/supermatter_monitor/proc/set_signals() - // if(active) - // RegisterSignal(active, COMSIG_SUPERMATTER_DELAM_ALARM, .proc/send_alert, override = TRUE) - // RegisterSignal(active, COMSIG_SUPERMATTER_DELAM_START_ALARM, .proc/send_start_alert, override = TRUE) + if(active) + RegisterSignal(active, COMSIG_SUPERMATTER_DELAM_ALARM, .proc/send_alert, override = TRUE) + RegisterSignal(active, COMSIG_SUPERMATTER_DELAM_START_ALARM, .proc/send_start_alert, override = TRUE) /** * Removes the signal listener for Supermatter delaminations from the selected supermatter. @@ -77,9 +80,9 @@ * Pretty much does what it says. */ /datum/computer_file/program/supermatter_monitor/proc/clear_signals() - // if(active) - // UnregisterSignal(active, COMSIG_SUPERMATTER_DELAM_ALARM) - // UnregisterSignal(active, COMSIG_SUPERMATTER_DELAM_START_ALARM) + if(active) + UnregisterSignal(active, COMSIG_SUPERMATTER_DELAM_ALARM) + UnregisterSignal(active, COMSIG_SUPERMATTER_DELAM_START_ALARM) /** * Sends an SM delam alert to the computer. @@ -90,6 +93,7 @@ * the supermatter probably don't need constant beeping to distract them. */ /datum/computer_file/program/supermatter_monitor/proc/send_alert() + SIGNAL_HANDLER if(!computer.get_ntnet_status()) return if(computer.active_program != src) @@ -106,12 +110,13 @@ * minimized or closed to avoid double-notifications. */ /datum/computer_file/program/supermatter_monitor/proc/send_start_alert() + SIGNAL_HANDLER if(!computer.get_ntnet_status()) return if(computer.active_program == src) computer.alert_call(src, "Crystal delamination in progress!") -/datum/computer_file/program/supermatter_monitor/ui_data() +/datum/computer_file/program/supermatter_monitor/ui_data(mob/user) var/list/data = get_header_data() if(istype(active)) @@ -125,30 +130,9 @@ active = null return - data["active"] = TRUE - data["SM_integrity"] = active.get_integrity() - data["SM_power"] = active.power - data["SM_ambienttemp"] = air.return_temperature() - data["SM_ambientpressure"] = air.return_pressure() - //data["SM_EPR"] = round((air.total_moles / air.group_multiplier) / 23.1, 0.01) - var/list/gasdata = list() + data += active.ui_data() + data["singlecrystal"] = FALSE - - if(air.total_moles()) - for(var/gasid in air.get_gases()) - var/amount = air.get_moles(gasid) - if(amount) - gasdata.Add(list(list( - "name"= GLOB.gas_data.names[gasid], - "amount" = round(100*amount/air.total_moles(),0.01)))) - - else - for(var/gasid in air.get_gases()) - gasdata.Add(list(list( - "name"= GLOB.gas_data.names[gasid], - "amount" = 0))) - - data["gases"] = gasdata else var/list/SMS = list() for(var/obj/machinery/power/supermatter_crystal/S in supermatters) @@ -185,3 +169,13 @@ active = S set_signals() return TRUE + +/datum/computer_file/program/supermatter_monitor/proc/react_to_del(datum/source) + SIGNAL_HANDLER + clear_supermatter(source) + +/datum/computer_file/program/supermatter_monitor/proc/clear_supermatter(matter) + supermatters -= matter + if(matter == active) + active = null + UnregisterSignal(matter, COMSIG_PARENT_QDELETING) diff --git a/code/modules/modular_computers/hardware/_hardware.dm b/code/modules/modular_computers/hardware/_hardware.dm index 0ccb9f6b96..b1a44c4cc5 100644 --- a/code/modules/modular_computers/hardware/_hardware.dm +++ b/code/modules/modular_computers/hardware/_hardware.dm @@ -4,22 +4,34 @@ icon = 'icons/obj/module.dmi' icon_state = "std_mod" - w_class = WEIGHT_CLASS_TINY // w_class limits which devices can contain this component. + w_class = WEIGHT_CLASS_TINY // w_class limits which devices can contain this component. // 1: PDAs/Tablets, 2: Laptops, 3-4: Consoles only var/obj/item/modular_computer/holder = null // Computer that holds this hardware, if any. - var/power_usage = 0 // If the hardware uses extra power, change this. - var/enabled = TRUE // If the hardware is turned off set this to 0. - var/critical = FALSE // Prevent disabling for important component, like the CPU. - var/can_install = TRUE // Prevents direct installation of removable media. - var/expansion_hw = FALSE // Hardware that fits into expansion bays. - var/removable = TRUE // Whether the hardware is removable or not. - var/damage = 0 // Current damage level - var/max_damage = 100 // Maximal damage level. - var/damage_malfunction = 20 // "Malfunction" threshold. When damage exceeds this value the hardware piece will semi-randomly fail and do !!FUN!! things - var/damage_failure = 50 // "Failure" threshold. When damage exceeds this value the hardware piece will not work at all. - var/malfunction_probability = 10// Chance of malfunction when the component is damaged + // If the hardware uses extra power, change this. + var/power_usage = 0 + // If the hardware is turned off set this to 0. + var/enabled = TRUE + // Prevent disabling for important component, like the CPU. + var/critical = FALSE + // Prevents direct installation of removable media. + var/can_install = TRUE + // Hardware that fits into expansion bays. + var/expansion_hw = FALSE + // Whether the hardware is removable or not. + var/removable = TRUE + // Current damage level + var/damage = 0 +// Maximal damage level. + var/max_damage = 100 + // "Malfunction" threshold. When damage exceeds this value the hardware piece will semi-randomly fail and do !!FUN!! things + var/damage_malfunction = 20 + // "Failure" threshold. When damage exceeds this value the hardware piece will not work at all. + var/damage_failure = 50 + // Chance of malfunction when the component is damaged + var/malfunction_probability = 10 + // What define is used to qualify this piece of hardware? Important for upgraded versions of the same hardware. var/device_type /obj/item/computer_hardware/New(obj/L) @@ -38,10 +50,10 @@ if(istype(I, /obj/item/stack/cable_coil)) var/obj/item/stack/S = I if(obj_integrity == max_integrity) - to_chat(user, "\The [src] doesn't seem to require repairs.") + to_chat(user, span_warning("\The [src] doesn't seem to require repairs.")) return 1 if(S.use(1)) - to_chat(user, "You patch up \the [src] with a bit of \the [I].") + to_chat(user, span_notice("You patch up \the [src] with a bit of \the [I].")) obj_integrity = min(obj_integrity + 10, max_integrity) return 1 @@ -78,11 +90,11 @@ /obj/item/computer_hardware/examine(mob/user) . = ..() if(damage > damage_failure) - . += "It seems to be severely damaged!" + . += span_danger("It seems to be severely damaged!") else if(damage > damage_malfunction) - . += "It seems to be damaged!" + . += span_warning("It seems to be damaged!") else if(damage) - . += "It seems to be slightly damaged." + . += span_notice("It seems to be slightly damaged.") // Component-side compatibility check. /obj/item/computer_hardware/proc/can_install(obj/item/modular_computer/M, mob/living/user = null) @@ -93,8 +105,9 @@ return // Called when component is removed from PC. -/obj/item/computer_hardware/proc/on_remove(obj/item/modular_computer/M, mob/living/user = null) - try_eject(forced = TRUE) +/obj/item/computer_hardware/proc/on_remove(obj/item/modular_computer/M, mob/living/user) + if(M.physical || !QDELETED(M)) + try_eject(forced = TRUE) // Called when someone tries to insert something in it - paper in printer, card in card reader, etc. /obj/item/computer_hardware/proc/try_insert(obj/item/I, mob/living/user = null) diff --git a/code/modules/modular_computers/hardware/ai_slot.dm b/code/modules/modular_computers/hardware/ai_slot.dm index 5d42747308..8740b59b35 100644 --- a/code/modules/modular_computers/hardware/ai_slot.dm +++ b/code/modules/modular_computers/hardware/ai_slot.dm @@ -7,13 +7,14 @@ device_type = MC_AI expansion_hw = TRUE - var/obj/item/aicard/stored_card = null + var/obj/item/aicard/stored_card var/locked = FALSE -/obj/item/computer_hardware/ai_slot/handle_atom_del(atom/A) - if(A == stored_card) - try_eject(forced = TRUE) - . = ..() +///What happens when the intellicard is removed (or deleted) from the module, through try_eject() or not. +/obj/item/computer_hardware/ai_slot/Exited(atom/movable/gone, direction) + if(stored_card == gone) + stored_card = null + return ..() /obj/item/computer_hardware/ai_slot/examine(mob/user) . = ..() @@ -28,34 +29,33 @@ return FALSE if(stored_card) - to_chat(user, "You try to insert \the [I] into \the [src], but the slot is occupied.") + to_chat(user, span_warning("You try to insert \the [I] into \the [src], but the slot is occupied.")) return FALSE if(user && !user.transferItemToLoc(I, src)) return FALSE stored_card = I - to_chat(user, "You insert \the [I] into \the [src].") + to_chat(user, span_notice("You insert \the [I] into \the [src].")) return TRUE /obj/item/computer_hardware/ai_slot/try_eject(mob/living/user = null, forced = FALSE) if(!stored_card) - to_chat(user, "There is no card in \the [src].") + to_chat(user, span_warning("There is no card in \the [src].")) return FALSE if(locked && !forced) - to_chat(user, "Safeties prevent you from removing the card until reconstruction is complete...") + to_chat(user, span_warning("Safeties prevent you from removing the card until reconstruction is complete...")) return FALSE if(stored_card) - to_chat(user, "You remove [stored_card] from [src].") + to_chat(user, span_notice("You remove [stored_card] from [src].")) locked = FALSE - if(user) + if(Adjacent(user)) user.put_in_hands(stored_card) else stored_card.forceMove(drop_location()) - stored_card = null return TRUE return FALSE @@ -64,6 +64,6 @@ if(..()) return if(I.tool_behaviour == TOOL_SCREWDRIVER) - to_chat(user, "You press down on the manual eject button with \the [I].") + to_chat(user, span_notice("You press down on the manual eject button with \the [I].")) try_eject(user, TRUE) return diff --git a/code/modules/modular_computers/hardware/battery_module.dm b/code/modules/modular_computers/hardware/battery_module.dm index 0668248315..27d3546ca2 100644 --- a/code/modules/modular_computers/hardware/battery_module.dm +++ b/code/modules/modular_computers/hardware/battery_module.dm @@ -4,25 +4,28 @@ icon_state = "cell_con" critical = 1 malfunction_probability = 1 - var/obj/item/stock_parts/cell/battery = null + var/obj/item/stock_parts/cell/battery device_type = MC_CELL /obj/item/computer_hardware/battery/get_cell() return battery -/obj/item/computer_hardware/battery/New(loc, battery_type = null) +/obj/item/computer_hardware/battery/Initialize(mapload, battery_type) + . = ..() if(battery_type) battery = new battery_type(src) - ..() /obj/item/computer_hardware/battery/Destroy() - . = ..() - QDEL_NULL(battery) + battery = null + return ..() -/obj/item/computer_hardware/battery/handle_atom_del(atom/A) - if(A == battery) - try_eject(forced = TRUE) - . = ..() +///What happens when the battery is removed (or deleted) from the module, through try_eject() or not. +/obj/item/computer_hardware/battery/Exited(atom/movable/gone, direction) + if(battery == gone) + battery = null + if(holder?.enabled && !holder.use_power()) + holder.shutdown_computer() + return ..() /obj/item/computer_hardware/battery/try_insert(obj/item/I, mob/living/user = null) if(!holder) @@ -32,46 +35,33 @@ return FALSE if(battery) - to_chat(user, "You try to connect \the [I] to \the [src], but its connectors are occupied.") + to_chat(user, span_warning("You try to connect \the [I] to \the [src], but its connectors are occupied.")) return FALSE if(I.w_class > holder.max_hardware_size) - to_chat(user, "This power cell is too large for \the [holder]!") + to_chat(user, span_warning("This power cell is too large for \the [holder]!")) return FALSE if(user && !user.transferItemToLoc(I, src)) return FALSE battery = I - to_chat(user, "You connect \the [I] to \the [src].") + to_chat(user, span_notice("You connect \the [I] to \the [src].")) return TRUE - -/obj/item/computer_hardware/battery/try_eject(mob/living/user = null, forced = FALSE) +/obj/item/computer_hardware/battery/try_eject(mob/living/user, forced = FALSE) if(!battery) - to_chat(user, "There is no power cell connected to \the [src].") + to_chat(user, span_warning("There is no power cell connected to \the [src].")) return FALSE else if(user) user.put_in_hands(battery) + to_chat(user, span_notice("You detach \the [battery] from \the [src].")) else battery.forceMove(drop_location()) - to_chat(user, "You detach \the [battery] from \the [src].") - battery = null - - if(holder) - if(holder.enabled && !holder.use_power()) - holder.shutdown_computer() - return TRUE - - - - - - /obj/item/stock_parts/cell/computer name = "standard battery" desc = "A standard power cell, commonly seen in high-end portable microcomputers or low-end laptops." @@ -80,7 +70,6 @@ w_class = WEIGHT_CLASS_TINY maxcharge = 750 - /obj/item/stock_parts/cell/computer/advanced name = "advanced battery" desc = "An advanced power cell, often used in most laptops. It is too large to be fitted into smaller devices." diff --git a/code/modules/modular_computers/hardware/card_slot.dm b/code/modules/modular_computers/hardware/card_slot.dm index 9139eee0b0..13f1b3bbc9 100644 --- a/code/modules/modular_computers/hardware/card_slot.dm +++ b/code/modules/modular_computers/hardware/card_slot.dm @@ -1,17 +1,31 @@ /obj/item/computer_hardware/card_slot - name = "primary RFID card module" // \improper breaks the find_hardware_by_name proc + name = "primary RFID card module" // \improper breaks the find_hardware_by_name proc desc = "A module allowing this computer to read or write data on ID cards. Necessary for some programs to run properly." power_usage = 10 //W icon_state = "card_mini" w_class = WEIGHT_CLASS_TINY device_type = MC_CARD - var/obj/item/card/id/stored_card = null + var/obj/item/card/id/stored_card -/obj/item/computer_hardware/card_slot/handle_atom_del(atom/A) - if(A == stored_card) - try_eject(null, TRUE) - . = ..() +///What happens when the ID card is removed (or deleted) from the module, through try_eject() or not. +/obj/item/computer_hardware/card_slot/Exited(atom/movable/gone, direction) + if(stored_card == gone) + stored_card = null + if(holder) + if(holder.active_program) + holder.active_program.event_idremoved(0) + for(var/p in holder.idle_threads) + var/datum/computer_file/program/computer_program = p + computer_program.event_idremoved(1) + + holder.update_slot_icon() + + if(ishuman(holder.loc)) + var/mob/living/carbon/human/human_wearer = holder.loc + if(human_wearer.wear_id == holder) + human_wearer.sec_hud_set_ID() + return ..() /obj/item/computer_hardware/card_slot/Destroy() try_eject(forced = TRUE) @@ -47,6 +61,11 @@ if(stored_card) return FALSE + + // item instead of player is checked so telekinesis will still work if the item itself is close + if(!in_range(src, I)) + return FALSE + if(user) if(!user.transferItemToLoc(I, src)) return FALSE @@ -54,38 +73,32 @@ I.forceMove(src) stored_card = I - to_chat(user, "You insert \the [I] into \the [expansion_hw ? "secondary":"primary"] [src].") + to_chat(user, span_notice("You insert \the [I] into \the [expansion_hw ? "secondary":"primary"] [src].")) playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, FALSE) - if(ishuman(user)) - var/mob/living/carbon/human/H = user - H.sec_hud_set_ID() + + var/holder_loc = holder.loc + if(ishuman(holder_loc)) + var/mob/living/carbon/human/human_wearer = holder_loc + if(human_wearer.wear_id == holder) + human_wearer.sec_hud_set_ID() + holder.update_slot_icon() return TRUE /obj/item/computer_hardware/card_slot/try_eject(mob/living/user = null, forced = FALSE) if(!stored_card) - to_chat(user, "There are no cards in \the [src].") + to_chat(user, span_warning("There are no cards in \the [src].")) return FALSE - if(user) + if(user && !issilicon(user) && in_range(src, user)) user.put_in_hands(stored_card) else stored_card.forceMove(drop_location()) - stored_card = null - if(holder) - if(holder.active_program) - holder.active_program.event_idremoved(0) - - for(var/p in holder.idle_threads) - var/datum/computer_file/program/computer_program = p - computer_program.event_idremoved(1) - if(ishuman(user)) - var/mob/living/carbon/human/human_user = user - human_user.sec_hud_set_ID() - to_chat(user, "You remove the card from \the [src].") + to_chat(user, span_notice("You remove the card from \the [src].")) playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, FALSE) + return TRUE /obj/item/computer_hardware/card_slot/attackby(obj/item/I, mob/living/user) @@ -93,11 +106,11 @@ return if(I.tool_behaviour == TOOL_SCREWDRIVER) if(stored_card) - to_chat(user, "You press down on the manual eject button with \the [I].") + to_chat(user, span_notice("You press down on the manual eject button with \the [I].")) try_eject(user) return swap_slot() - to_chat(user, "You adjust the connecter to fit into [expansion_hw ? "an expansion bay" : "the primary ID bay"].") + to_chat(user, span_notice("You adjust the connecter to fit into [expansion_hw ? "an expansion bay" : "the primary ID bay"].")) /** *Swaps the card_slot hardware between using the dedicated card slot bay on a computer, and using an expansion bay. diff --git a/code/modules/modular_computers/hardware/hard_drive.dm b/code/modules/modular_computers/hardware/hard_drive.dm index 8debb00c19..29614cc7b0 100644 --- a/code/modules/modular_computers/hardware/hard_drive.dm +++ b/code/modules/modular_computers/hardware/hard_drive.dm @@ -8,19 +8,19 @@ device_type = MC_HDD var/max_capacity = 128 var/used_capacity = 0 - var/list/stored_files = list() // List of stored files on this drive. DO NOT MODIFY DIRECTLY! + var/list/stored_files = list() // List of stored files on this drive. DO NOT MODIFY DIRECTLY! /obj/item/computer_hardware/hard_drive/on_remove(obj/item/modular_computer/MC, mob/user) MC.shutdown_computer() /obj/item/computer_hardware/hard_drive/proc/install_default_programs() - store_file(new/datum/computer_file/program/computerconfig(src)) // Computer configuration utility, allows hardware control and displays more info than status bar - store_file(new/datum/computer_file/program/ntnetdownload(src)) // NTNet Downloader Utility, allows users to download more software from NTNet repository - store_file(new/datum/computer_file/program/filemanager(src)) // File manager, allows text editor functions and basic file manipulation. + store_file(new/datum/computer_file/program/computerconfig(src)) // Computer configuration utility, allows hardware control and displays more info than status bar + store_file(new/datum/computer_file/program/ntnetdownload(src)) // NTNet Downloader Utility, allows users to download more software from NTNet repository + store_file(new/datum/computer_file/program/filemanager(src)) // File manager, allows text editor functions and basic file manipulation. /obj/item/computer_hardware/hard_drive/examine(user) . = ..() - . += "It has [max_capacity] GQ of storage capacity." + . += span_notice("It has [max_capacity] GQ of storage capacity.") /obj/item/computer_hardware/hard_drive/diagnostics(mob/user) ..() @@ -117,7 +117,7 @@ return null /obj/item/computer_hardware/hard_drive/Destroy() - stored_files = null + QDEL_LIST(stored_files) return ..() /obj/item/computer_hardware/hard_drive/Initialize() @@ -129,7 +129,7 @@ name = "advanced hard disk drive" desc = "A hybrid HDD, for use in higher grade computers where balance between power efficiency and capacity is desired." max_capacity = 256 - power_usage = 50 // Hybrid, medium capacity and medium power storage + power_usage = 50 // Hybrid, medium capacity and medium power storage icon_state = "harddisk_mini" w_class = WEIGHT_CLASS_SMALL @@ -137,7 +137,7 @@ name = "super hard disk drive" desc = "A high capacity HDD, for use in cluster storage solutions where capacity is more important than power efficiency." max_capacity = 512 - power_usage = 100 // High-capacity but uses lots of power, shortening battery life. Best used with APC link. + power_usage = 100 // High-capacity but uses lots of power, shortening battery life. Best used with APC link. icon_state = "harddisk_mini" w_class = WEIGHT_CLASS_SMALL @@ -161,8 +161,8 @@ // For borg integrated tablets. No downloader. /obj/item/computer_hardware/hard_drive/small/integrated/install_default_programs() - store_file(new /datum/computer_file/program/computerconfig(src)) // Computer configuration utility, allows hardware control and displays more info than status bar - store_file(new /datum/computer_file/program/filemanager(src)) // File manager, allows text editor functions and basic file manipulation. + store_file(new /datum/computer_file/program/computerconfig(src)) // Computer configuration utility, allows hardware control and displays more info than status bar + store_file(new /datum/computer_file/program/filemanager(src)) // File manager, allows text editor functions and basic file manipulation. store_file(new /datum/computer_file/program/robotact(src)) diff --git a/code/modules/modular_computers/hardware/network_card.dm b/code/modules/modular_computers/hardware/network_card.dm index 625ead6ed7..074e158dbf 100644 --- a/code/modules/modular_computers/hardware/network_card.dm +++ b/code/modules/modular_computers/hardware/network_card.dm @@ -3,8 +3,9 @@ desc = "A basic wireless network card for usage with standard NTNet frequencies." power_usage = 50 icon_state = "radio_mini" - var/identification_id = null // Identification ID. Technically MAC address of this device. Can't be changed by user. - var/identification_string = "" // Identification string, technically nickname seen in the network. Can be set by user. + // network_id = NETWORK_CARDS // Network we are on + var/hardware_id = null // Identification ID. Technically MAC address of this device. Can't be changed by user. + var/identification_string = "" // Identification string, technically nickname seen in the network. Can be set by user. var/long_range = 0 var/ethernet = 0 // Hard-wired, therefore always on, ignores NTNet wireless checks. malfunction_probability = 1 @@ -13,7 +14,7 @@ /obj/item/computer_hardware/network_card/diagnostics(mob/user) ..() - to_chat(user, "NIX Unique ID: [identification_id]") + to_chat(user, "NIX Unique ID: [hardware_id]") to_chat(user, "NIX User Tag: [identification_string]") to_chat(user, "Supported protocols:") to_chat(user, "511.m SFS (Subspace) - Standard Frequency Spread") @@ -24,11 +25,11 @@ /obj/item/computer_hardware/network_card/New(l) ..() - identification_id = ntnet_card_uid++ + hardware_id = ntnet_card_uid++ // Returns a string identifier of this network card /obj/item/computer_hardware/network_card/proc/get_network_tag() - return "[identification_string] (NID [identification_id])" + return "[identification_string] (NID [hardware_id])" // 0 - No signal, 1 - Low signal, 2 - High signal. 3 - Wired Connection /obj/item/computer_hardware/network_card/proc/get_signal(specific_action = 0) diff --git a/code/modules/modular_computers/hardware/printer.dm b/code/modules/modular_computers/hardware/printer.dm index 3bd5946435..99756e3dd8 100644 --- a/code/modules/modular_computers/hardware/printer.dm +++ b/code/modules/modular_computers/hardware/printer.dm @@ -11,11 +11,11 @@ /obj/item/computer_hardware/printer/diagnostics(mob/living/user) ..() - to_chat(user, "Paper level: [stored_paper]/[max_paper].") + to_chat(user, span_notice("Paper level: [stored_paper]/[max_paper].")) /obj/item/computer_hardware/printer/examine(mob/user) . = ..() - . += "Paper level: [stored_paper]/[max_paper]." + . += span_notice("Paper level: [stored_paper]/[max_paper].") /obj/item/computer_hardware/printer/proc/print_text(text_to_print, paper_title = "") @@ -33,7 +33,7 @@ P.info = text_to_print if(paper_title) P.name = paper_title - P.update_icon() + P.update_appearance() stored_paper-- P = null return TRUE @@ -41,12 +41,12 @@ /obj/item/computer_hardware/printer/try_insert(obj/item/I, mob/living/user = null) if(istype(I, /obj/item/paper)) if(stored_paper >= max_paper) - to_chat(user, "You try to add \the [I] into [src], but its paper bin is full!") + to_chat(user, span_warning("You try to add \the [I] into [src], but its paper bin is full!")) return FALSE if(user && !user.temporarilyRemoveItemFromInventory(I)) return FALSE - to_chat(user, "You insert \the [I] into [src]'s paper recycler.") + to_chat(user, span_notice("You insert \the [I] into [src]'s paper recycler.")) qdel(I) stored_paper++ return TRUE diff --git a/code/modules/modular_computers/hardware/recharger.dm b/code/modules/modular_computers/hardware/recharger.dm index ecfbf4c6b2..e8c479db47 100644 --- a/code/modules/modular_computers/hardware/recharger.dm +++ b/code/modules/modular_computers/hardware/recharger.dm @@ -55,7 +55,7 @@ /obj/item/computer_hardware/recharger/wired/can_install(obj/item/modular_computer/M, mob/living/user = null) if(ismachinery(M.physical) && M.physical.anchored) return ..() - to_chat(user, "\The [src] is incompatible with portable computers!") + to_chat(user, span_warning("\The [src] is incompatible with portable computers!")) return FALSE /obj/item/computer_hardware/recharger/wired/use_power(amount, charging=0) @@ -96,3 +96,4 @@ /obj/item/computer_hardware/recharger/lambda/use_power(amount, charging=0) return 1 + diff --git a/code/modules/modular_computers/hardware/sensor_package.dm b/code/modules/modular_computers/hardware/sensor_package.dm index c0363bc809..0579b752fb 100644 --- a/code/modules/modular_computers/hardware/sensor_package.dm +++ b/code/modules/modular_computers/hardware/sensor_package.dm @@ -6,3 +6,12 @@ w_class = WEIGHT_CLASS_TINY device_type = MC_SENSORS expansion_hw = TRUE + +/obj/item/computer_hardware/radio_card + name = "integrated radio card" + desc = "An integrated signaling assembly for computers to send an outgoing frequency signal. Required by certain programs." + icon_state = "signal_card" + w_class = WEIGHT_CLASS_TINY + device_type = MC_SIGNALER + expansion_hw = TRUE + power_usage = 10 diff --git a/code/modules/modular_computers/laptop_vendor.dm b/code/modules/modular_computers/laptop_vendor.dm index 0811983088..4a268c2911 100644 --- a/code/modules/modular_computers/laptop_vendor.dm +++ b/code/modules/modular_computers/laptop_vendor.dm @@ -13,19 +13,19 @@ var/obj/item/modular_computer/tablet/fabricated_tablet = null // Utility vars - var/state = 0 // 0: Select device type, 1: Select loadout, 2: Payment, 3: Thankyou screen - var/devtype = 0 // 0: None(unselected), 1: Laptop, 2: Tablet - var/total_price = 0 // Price of currently vended device. + var/state = 0 // 0: Select device type, 1: Select loadout, 2: Payment, 3: Thankyou screen + var/devtype = 0 // 0: None(unselected), 1: Laptop, 2: Tablet + var/total_price = 0 // Price of currently vended device. var/credits = 0 // Device loadout - var/dev_cpu = 1 // 1: Default, 2: Upgraded - var/dev_battery = 1 // 1: Default, 2: Upgraded, 3: Advanced - var/dev_disk = 1 // 1: Default, 2: Upgraded, 3: Advanced - var/dev_netcard = 0 // 0: None, 1: Basic, 2: Long-Range - var/dev_apc_recharger = 0 // 0: None, 1: Standard (LAPTOP ONLY) - var/dev_printer = 0 // 0: None, 1: Standard - var/dev_card = 0 // 0: None, 1: Standard + var/dev_cpu = 1 // 1: Default, 2: Upgraded + var/dev_battery = 1 // 1: Default, 2: Upgraded, 3: Advanced + var/dev_disk = 1 // 1: Default, 2: Upgraded, 3: Advanced + var/dev_netcard = 0 // 0: None, 1: Basic, 2: Long-Range + var/dev_apc_recharger = 0 // 0: None, 1: Standard (LAPTOP ONLY) + var/dev_printer = 0 // 0: None, 1: Standard + var/dev_card = 0 // 0: None, 1: Standard // Removes all traces of old order and allows you to begin configuration from scratch. /obj/machinery/lapvend/proc/reset_order() @@ -48,7 +48,7 @@ // Recalculates the price and optionally even fabricates the device. /obj/machinery/lapvend/proc/fabricate_and_recalc_price(fabricate = FALSE) total_price = 0 - if(devtype == 1) // Laptop, generally cheaper to make it accessible for most station roles + if(devtype == 1) // Laptop, generally cheaper to make it accessible for most station roles var/obj/item/computer_hardware/battery/battery_module = null if(fabricate) fabricated_laptop = new /obj/item/modular_computer/laptop/buildable(src) @@ -111,7 +111,7 @@ fabricated_laptop.install_component(new /obj/item/computer_hardware/card_slot/secondary) return total_price - else if(devtype == 2) // Tablet, more expensive, not everyone could probably afford this. + else if(devtype == 2) // Tablet, more expensive, not everyone could probably afford this. var/obj/item/computer_hardware/battery/battery_module = null if(fabricate) fabricated_tablet = new(src) @@ -241,13 +241,13 @@ if(!user.temporarilyRemoveItemFromInventory(c)) return credits += c.value - visible_message("[user] inserts [c.value] cr into [src].") + visible_message(span_info("[span_name("[user]")] inserts [c.value] cr into [src].")) qdel(c) return else if(istype(I, /obj/item/holochip)) var/obj/item/holochip/HC = I credits += HC.credits - visible_message("[user] inserts a [HC.credits] cr holocredit chip into [src].") + visible_message(span_info("[user] inserts a [HC.credits] cr holocredit chip into [src].")) qdel(HC) return else if(istype(I, /obj/item/card/id)) diff --git a/code/modules/paperwork/clipboard.dm b/code/modules/paperwork/clipboard.dm index 13de7898c1..c957a927ec 100644 --- a/code/modules/paperwork/clipboard.dm +++ b/code/modules/paperwork/clipboard.dm @@ -1,127 +1,201 @@ +/** + * Clipboard + */ /obj/item/clipboard name = "clipboard" icon = 'icons/obj/bureaucracy.dmi' icon_state = "clipboard" item_state = "clipboard" - // inhand_icon_state = "clipboard" - // worn_icon_state = "clipboard" throwforce = 0 w_class = WEIGHT_CLASS_SMALL throw_speed = 3 throw_range = 7 - var/obj/item/pen/haspen //The stored pen. - var/obj/item/paper/toppaper //The topmost piece of paper. slot_flags = ITEM_SLOT_BELT resistance_flags = FLAMMABLE + /// The stored pen + var/obj/item/pen/pen + /// Is the pen integrated? + var/integrated_pen = FALSE + /** + * Weakref of the topmost piece of paper + * + * This is used for the paper displayed on the clipboard's icon + * and it is the one attacked, when attacking the clipboard. + * (As you can't organise contents directly in BYOND) + */ + var/datum/weakref/toppaper_ref /obj/item/clipboard/suicide_act(mob/living/carbon/user) - user.visible_message("[user] begins putting [user.p_their()] head into the clip of \the [src]! It looks like [user.p_theyre()] trying to commit suicide!") - return BRUTELOSS//the clipboard's clip is very strong. industrial duty. can kill a man easily. + user.visible_message(span_suicide("[user] begins putting [user.p_their()] head into the clip of \the [src]! It looks like [user.p_theyre()] trying to commit suicide!")) + return BRUTELOSS //The clipboard's clip is very strong. Industrial duty. Can kill a man easily. /obj/item/clipboard/Initialize() - update_icon() + update_appearance() . = ..() /obj/item/clipboard/Destroy() - QDEL_NULL(haspen) - QDEL_NULL(toppaper) //let movable/Destroy handle the rest + QDEL_NULL(pen) return ..() +/obj/item/clipboard/examine() + . = ..() + if(!integrated_pen && pen) + . += span_notice("Alt-click to remove [pen].") + var/obj/item/paper/toppaper = toppaper_ref?.resolve() + if(toppaper) + . += span_notice("Right-click to remove [toppaper].") + +/// Take out the topmost paper +/obj/item/clipboard/proc/remove_paper(obj/item/paper/paper, mob/user) + if(!istype(paper)) + return + paper.forceMove(user.loc) + user.put_in_hands(paper) + to_chat(user, span_notice("You remove [paper] from [src].")) + var/obj/item/paper/toppaper = toppaper_ref?.resolve() + if(paper == toppaper) + UnregisterSignal(toppaper, COMSIG_ATOM_UPDATED_ICON) + toppaper_ref = null + var/obj/item/paper/newtop = locate(/obj/item/paper) in src + if(newtop && (newtop != paper)) + toppaper_ref = WEAKREF(newtop) + else + toppaper_ref = null + update_icon() + +/obj/item/clipboard/proc/remove_pen(mob/user) + pen.forceMove(user.loc) + user.put_in_hands(pen) + to_chat(user, span_notice("You remove [pen] from [src].")) + pen = null + update_icon() + +/obj/item/clipboard/AltClick(mob/user) + ..() + if(pen) + if(integrated_pen) + to_chat(user, span_warning("You can't seem to find a way to remove [src]'s [pen].")) + else + remove_pen(user) + /obj/item/clipboard/update_overlays() . = ..() + var/obj/item/paper/toppaper = toppaper_ref?.resolve() if(toppaper) . += toppaper.icon_state . += toppaper.overlays - if(haspen) + if(pen) . += "clipboard_pen" . += "clipboard_over" -/obj/item/clipboard/attackby(obj/item/W, mob/user, params) - if(istype(W, /obj/item/paper)) - if(!user.transferItemToLoc(W, src)) +/obj/item/clipboard/attack_hand(mob/user, act_intent) + if(act_intent != INTENT_HELP) + var/obj/item/paper/toppaper = toppaper_ref?.resolve() + remove_paper(toppaper, user) + return TRUE + . = ..() + +/obj/item/clipboard/attackby(obj/item/weapon, mob/user, params) + var/obj/item/paper/toppaper = toppaper_ref?.resolve() + if(istype(weapon, /obj/item/paper)) + //Add paper into the clipboard + if(!user.transferItemToLoc(weapon, src)) return - toppaper = W - to_chat(user, "You clip the paper onto \the [src].") - update_icon() + if(toppaper) + UnregisterSignal(toppaper, COMSIG_ATOM_UPDATED_ICON) + RegisterSignal(weapon, COMSIG_ATOM_UPDATED_ICON, .proc/on_top_paper_change) + toppaper_ref = WEAKREF(weapon) + to_chat(user, span_notice("You clip [weapon] onto [src].")) + else if(istype(weapon, /obj/item/pen) && !pen) + //Add a pen into the clipboard, attack (write) if there is already one + if(!usr.transferItemToLoc(weapon, src)) + return + pen = weapon + to_chat(usr, span_notice("You slot [weapon] into [src].")) else if(toppaper) toppaper.attackby(user.get_active_held_item(), user) - update_icon() - + update_appearance() /obj/item/clipboard/attack_self(mob/user) - var/dat = "Clipboard" - if(haspen) - dat += "Remove Pen

" - else - dat += "Add Pen

" - - //The topmost paper. You can't organise contents directly in byond, so this is what we're stuck with. -Pete - if(toppaper) - var/obj/item/paper/P = toppaper - dat += "Write Remove - [P.name]

" - - for(P in src) - if(P == toppaper) - continue - dat += "Write Remove Move to top - [P.name]
" - user << browse(dat, "window=clipboard") - onclose(user, "clipboard") add_fingerprint(usr) + ui_interact(user) + return +/obj/item/clipboard/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Clipboard") + ui.open() -/obj/item/clipboard/Topic(href, href_list) - ..() - if(usr.stat != CONSCIOUS || usr.restrained()) //HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED)) +/obj/item/clipboard/ui_data(mob/user) + // prepare data for TGUI + var/list/data = list() + data["pen"] = "[pen]" + data["integrated_pen"] = integrated_pen + + var/obj/item/paper/toppaper = toppaper_ref?.resolve() + data["top_paper"] = "[toppaper]" + data["top_paper_ref"] = "[REF(toppaper)]" + + data["paper"] = list() + data["paper_ref"] = list() + for(var/obj/item/paper/paper in src) + if(paper == toppaper) + continue + data["paper"] += "[paper]" + data["paper_ref"] += "[REF(paper)]" + + return data + +/obj/item/clipboard/ui_act(action, params) + . = ..() + if(.) return - if(usr.contents.Find(src)) + if(usr.stat != CONSCIOUS || usr.restrained()) + return - if(href_list["pen"]) - if(haspen) - haspen.forceMove(usr.loc) - usr.put_in_hands(haspen) - haspen = null + switch(action) + // Take the pen out + if("remove_pen") + if(pen) + if(!integrated_pen) + remove_pen(usr) + else + to_chat(usr, span_warning("You can't seem to find a way to remove [src]'s [pen].")) + . = TRUE + // Take paper out + if("remove_paper") + var/obj/item/paper/paper = locate(params["ref"]) in src + if(istype(paper)) + remove_paper(paper, usr) + . = TRUE + // Look at (or edit) the paper + if("edit_paper") + var/obj/item/paper/paper = locate(params["ref"]) in src + if(istype(paper)) + paper.ui_interact(usr) + update_icon() + . = TRUE + // Move paper to the top + if("move_top_paper") + var/obj/item/paper/paper = locate(params["ref"]) in src + if(istype(paper)) + toppaper_ref = WEAKREF(paper) + to_chat(usr, span_notice("You move [paper] to the top.")) + update_icon() + . = TRUE + // Rename the paper (it's a verb) + if("rename_paper") + var/obj/item/paper/paper = locate(params["ref"]) in src + if(istype(paper)) + paper.rename() + update_icon() + . = TRUE - if(href_list["addpen"]) - if(!haspen) - var/obj/item/held = usr.get_active_held_item() - if(istype(held, /obj/item/pen)) - var/obj/item/pen/W = held - if(!usr.transferItemToLoc(W, src)) - return - haspen = W - to_chat(usr, "You slot [W] into [src].") - - if(href_list["write"]) - var/obj/item/P = locate(href_list["write"]) in src - if(istype(P)) - if(usr.get_active_held_item()) - P.attackby(usr.get_active_held_item(), usr) - - if(href_list["remove"]) - var/obj/item/P = locate(href_list["remove"]) in src - if(istype(P)) - P.forceMove(usr.loc) - usr.put_in_hands(P) - if(P == toppaper) - toppaper = null - var/obj/item/paper/newtop = locate(/obj/item/paper) in src - if(newtop && (newtop != P)) - toppaper = newtop - else - toppaper = null - - if(href_list["read"]) - var/obj/item/paper/P = locate(href_list["read"]) in src - if(istype(P)) - usr.examinate(P) - - if(href_list["top"]) - var/obj/item/P = locate(href_list["top"]) in src - if(istype(P)) - toppaper = P - to_chat(usr, "You move [P.name] to the top.") - - //Update everything - attack_self(usr) - update_icon() +/** + * This is a simple proc to handle calling update_icon() upon receiving the top paper's `COMSIG_ATOM_UPDATE_APPEARANCE`. + */ +/obj/item/clipboard/proc/on_top_paper_change() + SIGNAL_HANDLER + update_appearance() diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm index 7a98287809..366d1a3f68 100644 --- a/code/modules/paperwork/paper.dm +++ b/code/modules/paperwork/paper.dm @@ -5,7 +5,7 @@ * lipstick wiping is in code/game/objects/items/weapons/cosmetics.dm! */ #define MAX_PAPER_LENGTH 5000 -#define MAX_PAPER_STAMPS 30 // Too low? +#define MAX_PAPER_STAMPS 30 // Too low? #define MAX_PAPER_STAMPS_OVERLAYS 4 #define MODE_READING 0 #define MODE_WRITING 1 @@ -22,6 +22,8 @@ icon = 'icons/obj/bureaucracy.dmi' icon_state = "paper" item_state = "paper" + // worn_icon_state = "paper" + // custom_fire_overlay = "paper_onfire_overlay" throwforce = 0 w_class = WEIGHT_CLASS_TINY throw_range = 1 @@ -41,8 +43,8 @@ var/show_written_words = TRUE /// The (text for the) stamps on the paper. - var/list/stamps /// Positioning for the stamp in tgui - var/list/stamped /// Overlay info + var/list/stamps /// Positioning for the stamp in tgui + var/list/stamped /// Overlay info var/contact_poison // Reagent ID to transfer on contact var/contact_poison_volume = 0 @@ -61,20 +63,23 @@ /** * This proc copies this sheet of paper to a new - * sheet, Makes it nice and easy for carbon and - * the copyer machine + * sheet. Used by carbon papers and the photocopier machine. */ -/obj/item/paper/proc/copy() - var/obj/item/paper/N = new(arglist(args)) - N.info = info - N.color = color - N.update_icon_state() - N.stamps = stamps - N.stamped = stamped.Copy() - N.form_fields = form_fields.Copy() - N.field_counter = field_counter - copy_overlays(N, TRUE) - return N +/obj/item/paper/proc/copy(paper_type = /obj/item/paper, atom/location = loc, colored = TRUE) + var/obj/item/paper/new_paper = new paper_type (location) + if(colored) + new_paper.color = color + new_paper.info = info + else //This basically just breaks the existing color tag, which we need to do because the innermost tag takes priority. + var/static/greyscale_info = regex("You cut yourself on the paper! Ahhhh! Ahhhhh!") + to_chat(H, span_warning("You cut yourself on the paper! Ahhhh! Ahhhhh!")) H.damageoverlaytemp = 9001 H.update_damage_hud() return var/n_name = stripped_input(usr, "What would you like to label the paper?", "Paper Labelling", null, MAX_NAME_LEN) - if((loc == usr && usr.stat == CONSCIOUS)) + if(((loc == usr || istype(loc, /obj/item/clipboard)) && usr.stat == CONSCIOUS)) name = "paper[(n_name ? text("- '[n_name]'") : null)]" add_fingerprint(usr) /obj/item/paper/suicide_act(mob/user) - user.visible_message("[user] scratches a grid on [user.p_their()] wrist with the paper! It looks like [user.p_theyre()] trying to commit sudoku...") + user.visible_message(span_suicide("[user] scratches a grid on [user.p_their()] wrist with the paper! It looks like [user.p_theyre()] trying to commit sudoku...")) return (BRUTELOSS) /obj/item/paper/proc/clearpaper() @@ -139,18 +145,18 @@ /obj/item/paper/examine(mob/user) . = ..() if(!in_range(user, src) && !isobserver(user)) - . += "You're too far away to read it!" + . += span_warning("You're too far away to read it!") return if(user.can_read(src)) ui_interact(user) return - . += "You cannot read it!" + . += span_warning("You cannot read it!") /obj/item/paper/ui_status(mob/user,/datum/ui_state/state) // Are we on fire? Hard ot read if so if(resistance_flags & ON_FIRE) return UI_CLOSE - if(!in_range(user,src)) + if(!in_range(user, src) && !isobserver(user)) return UI_CLOSE if(user.incapacitated(TRUE, TRUE) || (isobserver(user) && !IsAdminGhost(user))) return UI_UPDATE @@ -158,7 +164,7 @@ // .. or if you cannot read if(!user.can_read(src)) return UI_CLOSE - if(in_contents_of(/obj/machinery/door/airlock)) + if(in_contents_of(/obj/machinery/door/airlock) || in_contents_of(/obj/item/clipboard)) return UI_INTERACTIVE return ..() @@ -176,8 +182,8 @@ return . = TRUE if(!bypass_clumsy && HAS_TRAIT(user, TRAIT_CLUMSY) && prob(10) && Adjacent(user)) - user.visible_message("[user] accidentally ignites [user.p_them()]self!", \ - "You miss [src] and accidentally light yourself on fire!") + user.visible_message(span_warning("[user] accidentally ignites [user.p_them()]self!"), \ + span_userdanger("You miss [src] and accidentally light yourself on fire!")) if(user.is_holding(I)) //checking if they're holding it in case TK is involved user.dropItemToGround(I) user.adjust_fire_stacks(1) @@ -195,19 +201,23 @@ SStgui.close_uis(src) return - if(istype(P, /obj/item/pen) || istype(P, /obj/item/toy/crayon)) + // Enable picking paper up by clicking on it with the clipboard or folder + if(istype(P, /obj/item/clipboard) || istype(P, /obj/item/folder)) + P.attackby(src, user) + return + else if(istype(P, /obj/item/pen) || istype(P, /obj/item/toy/crayon)) if(length(info) >= MAX_PAPER_LENGTH) // Sheet must have less than 1000 charaters - to_chat(user, "This sheet of paper is full!") + to_chat(user, span_warning("This sheet of paper is full!")) return ui_interact(user) return else if(istype(P, /obj/item/stamp)) - to_chat(user, "You ready your stamp over the paper! ") + to_chat(user, span_notice("You ready your stamp over the paper! ")) ui_interact(user) return /// Normaly you just stamp, you don't need to read the thing else // cut paper? the sky is the limit! - ui_interact(user) // The other ui will be created with just read mode outside of this + ui_interact(user) // The other ui will be created with just read mode outside of this return ..() @@ -234,8 +244,8 @@ . = list() .["text"] = info .["max_length"] = MAX_PAPER_LENGTH - .["paper_color"] = !color || color == "white" ? "#FFFFFF" : color // color might not be set - .["paper_state"] = icon_state /// TODO: show the sheet will bloodied or crinkling? + .["paper_color"] = !color || color == "white" ? "#FFFFFF" : color // color might not be set + .["paper_state"] = icon_state /// TODO: show the sheet will bloodied or crinkling? .["stamps"] = stamps @@ -244,27 +254,34 @@ var/list/data = list() data["edit_usr"] = "[user]" - var/obj/O = user.get_active_held_item() - if(istype(O, /obj/item/toy/crayon)) - var/obj/item/toy/crayon/PEN = O + var/obj/holding = user.get_active_held_item() + // Use a clipboard's pen, if applicable + if(istype(loc, /obj/item/clipboard)) + var/obj/item/clipboard/clipboard = loc + // This is just so you can still use a stamp if you're holding one. Otherwise, it'll + // use the clipboard's pen, if applicable. + if(!istype(holding, /obj/item/stamp) && clipboard.pen) + holding = clipboard.pen + if(istype(holding, /obj/item/toy/crayon)) + var/obj/item/toy/crayon/PEN = holding data["pen_font"] = CRAYON_FONT data["pen_color"] = PEN.paint_color data["edit_mode"] = MODE_WRITING data["is_crayon"] = TRUE data["stamp_class"] = "FAKE" data["stamp_icon_state"] = "FAKE" - else if(istype(O, /obj/item/pen)) - var/obj/item/pen/PEN = O + else if(istype(holding, /obj/item/pen)) + var/obj/item/pen/PEN = holding data["pen_font"] = PEN.font data["pen_color"] = PEN.colour data["edit_mode"] = MODE_WRITING data["is_crayon"] = FALSE data["stamp_class"] = "FAKE" data["stamp_icon_state"] = "FAKE" - else if(istype(O, /obj/item/stamp)) + else if(istype(holding, /obj/item/stamp)) var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/simple/paper) - data["stamp_icon_state"] = O.icon_state - data["stamp_class"] = sheet.icon_class_name(O.icon_state) + data["stamp_icon_state"] = holding.icon_state + data["stamp_class"] = sheet.icon_class_name(holding.icon_state) data["edit_mode"] = MODE_STAMPING data["pen_font"] = "FAKE" data["pen_color"] = "FAKE" @@ -276,6 +293,10 @@ data["is_crayon"] = FALSE data["stamp_icon_state"] = "FAKE" data["stamp_class"] = "FAKE" + if(istype(loc, /obj/structure/noticeboard)) + var/obj/structure/noticeboard/noticeboard = loc + if(!noticeboard.allowed(user)) + data["edit_mode"] = MODE_READING data["field_counter"] = field_counter data["form_fields"] = form_fields @@ -289,14 +310,14 @@ if("stamp") var/stamp_x = text2num(params["x"]) var/stamp_y = text2num(params["y"]) - var/stamp_r = text2num(params["r"]) // rotation in degrees + var/stamp_r = text2num(params["r"]) // rotation in degrees var/stamp_icon_state = params["stamp_icon_state"] var/stamp_class = params["stamp_class"] if (isnull(stamps)) stamps = list() if(stamps.len < MAX_PAPER_STAMPS) // I hate byond when dealing with freaking lists - stamps[++stamps.len] = list(stamp_class, stamp_x, stamp_y, stamp_r) /// WHHHHY + stamps[++stamps.len] = list(stamp_class, stamp_x, stamp_y, stamp_r) /// WHHHHY /// This does the overlay stuff if (isnull(stamped)) @@ -307,10 +328,11 @@ stampoverlay.pixel_y = rand(-3, 2) add_overlay(stampoverlay) LAZYADD(stamped, stamp_icon_state) + update_icon() update_static_data(usr,ui) var/obj/O = ui.user.get_active_held_item() - ui.user.visible_message("[ui.user] stamps [src] with \the [O.name]!", "You stamp [src] with \the [O.name]!") + ui.user.visible_message(span_notice("[ui.user] stamps [src] with \the [O.name]!"), span_notice("You stamp [src] with \the [O.name]!")) else to_chat(usr, pick("You try to stamp but you miss!", "There is no where else you can stamp!")) . = TRUE @@ -336,9 +358,14 @@ update_static_data(usr,ui) - update_icon() + update_appearance() . = TRUE +/obj/item/paper/ui_host(mob/user) + if(istype(loc, /obj/structure/noticeboard)) + return loc + return ..() + /** * Construction paper */ @@ -361,9 +388,6 @@ slot_flags = null show_written_words = FALSE -/obj/item/paper/crumpled/update_icon_state() - return - /obj/item/paper/crumpled/bloody icon_state = "scrap_bloodied" diff --git a/code/modules/power/port_gen.dm b/code/modules/power/port_gen.dm index b5ed659e47..a57711a539 100644 --- a/code/modules/power/port_gen.dm +++ b/code/modules/power/port_gen.dm @@ -19,7 +19,7 @@ /obj/machinery/power/port_gen/Initialize() . = ..() - soundloop = new(list(src), active) + soundloop = new(src, active) /obj/machinery/power/port_gen/Destroy() QDEL_NULL(soundloop) diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm index 08d2ff5c84..5f6e110a9c 100644 --- a/code/modules/power/singularity/narsie.dm +++ b/code/modules/power/singularity/narsie.dm @@ -62,7 +62,7 @@ for(var/datum/mind/cult_mind in SSticker.mode.cult) if(isliving(cult_mind.current)) var/mob/living/L = cult_mind.current - L.narsie_act() + INVOKE_ASYNC(L, /atom.proc/narsie_act) for(var/mob/living/player in GLOB.player_list) if(player.stat != DEAD && player.loc && is_station_level(player.loc.z) && !iscultist(player) && !isanimal(player)) souls_needed[player] = TRUE diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm index a1fbf5d7ad..201200a95d 100644 --- a/code/modules/power/supermatter/supermatter.dm +++ b/code/modules/power/supermatter/supermatter.dm @@ -210,7 +210,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) AddElement(/datum/element/bsa_blocker) RegisterSignal(src, COMSIG_ATOM_BSA_BEAM, .proc/call_explode) - soundloop = new(list(src), TRUE) + soundloop = new(src, TRUE) /obj/machinery/power/supermatter_crystal/Destroy() investigate_log("has been destroyed.", INVESTIGATE_SUPERMATTER) @@ -220,6 +220,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) QDEL_NULL(countdown) if(is_main_engine && GLOB.main_supermatter_engine == src) GLOB.main_supermatter_engine = null + QDEL_NULL(soundloop) return ..() /obj/machinery/power/supermatter_crystal/examine(mob/user) @@ -229,6 +230,57 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) if (!istype(C.glasses, /obj/item/clothing/glasses/meson) && (get_dist(user, src) < HALLUCINATION_RANGE(power))) . += "You get headaches just from looking at it." +// SupermatterMonitor UI for ghosts only. Inherited attack_ghost will call this. +/obj/machinery/power/supermatter_crystal/ui_interact(mob/user, datum/tgui/ui) + if(!isobserver(user)) + return FALSE + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "SupermatterMonitor") + ui.open() + +/obj/machinery/power/supermatter_crystal/ui_data(mob/user) + var/list/data = list() + + var/turf/local_turf = get_turf(src) + + var/datum/gas_mixture/air = local_turf.return_air() + + // singlecrystal set to true eliminates the back sign on the gases breakdown. + data["singlecrystal"] = TRUE + data["active"] = TRUE + data["SM_integrity"] = get_integrity() + data["SM_power"] = power + data["SM_ambienttemp"] = air.return_temperature() + data["SM_ambientpressure"] = air.return_pressure() + data["SM_bad_moles_amount"] = MOLE_PENALTY_THRESHOLD / gasefficency + data["SM_moles"] = 0 + data["SM_uid"] = uid + var/area/active_supermatter_area = get_area(src) + data["SM_area_name"] = active_supermatter_area.name + + var/list/gasdata = list() + + if(air.total_moles()) + data["SM_moles"] = air.total_moles() + for(var/id in air.get_gases()) + var/gas_level = air.get_moles(id)/air.total_moles() + if(gas_level > 0) + gasdata.Add(list(list( + "name"= "[GLOB.gas_data.names[id]]", + "amount" = round(gas_level*100, 0.01)))) + + else + for(var/id in air.get_gases()) + gasdata.Add(list(list( + "name"= "[GLOB.gas_data.names[id]]", + "amount" = 0))) + + data["gases"] = gasdata + + return data + /obj/machinery/power/supermatter_crystal/proc/get_status() var/turf/T = get_turf(src) if(!T) @@ -628,6 +680,8 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) //Tells the engi team to get their butt in gear if(damage > warning_point) // while the core is still damaged and it's still worth noting its status + if(damage_archived < warning_point) //If damage_archive is under the warning point, this is the very first cycle that we've reached said point. + SEND_SIGNAL(src, COMSIG_SUPERMATTER_DELAM_START_ALARM) if((REALTIMEOFDAY - lastwarning) / 10 >= WARNING_DELAY) alarm() @@ -635,6 +689,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) if(damage > emergency_point) // it's bad, LETS YELL radio.talk_into(src, "[emergency_alert] Integrity: [get_integrity()]%", common_channel, list(SPAN_YELL)) + SEND_SIGNAL(src, COMSIG_SUPERMATTER_DELAM_ALARM) lastwarning = REALTIMEOFDAY if(!has_reached_emergency) investigate_log("has reached the emergency point for the first time.", INVESTIGATE_SUPERMATTER) @@ -642,6 +697,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) has_reached_emergency = TRUE else if(damage >= damage_archived) // The damage is still going up radio.talk_into(src, "[warning_alert] Integrity: [get_integrity()]%", engineering_channel) + SEND_SIGNAL(src, COMSIG_SUPERMATTER_DELAM_ALARM) lastwarning = REALTIMEOFDAY - (WARNING_DELAY * 5) else // Phew, we're safe diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm index 37a60a0469..1ee780b7b9 100644 --- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm @@ -1050,6 +1050,7 @@ name = "Mannitol" description = "Efficiently restores brain damage." color = "#DCDCFF" + taste_description = "sweetness" pH = 10.4 chemical_flags = REAGENT_ALL_PROCESS diff --git a/code/modules/surgery/tools.dm b/code/modules/surgery/tools.dm index 5c7b71b779..1d2286214d 100644 --- a/code/modules/surgery/tools.dm +++ b/code/modules/surgery/tools.dm @@ -295,7 +295,7 @@ lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi' righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi' hitsound = 'sound/weapons/circsawhit.ogg' - throwhitsound = 'sound/weapons/pierce.ogg' + mob_throw_hit_sound = 'sound/weapons/pierce.ogg' item_flags = SURGICAL_TOOL flags_1 = CONDUCT_1 force = 15 @@ -327,7 +327,7 @@ icon = 'icons/obj/surgery.dmi' icon_state = "saw" hitsound = 'sound/weapons/circsawhit.ogg' - throwhitsound = 'sound/weapons/pierce.ogg' + mob_throw_hit_sound = 'sound/weapons/pierce.ogg' flags_1 = CONDUCT_1 force = 10 w_class = WEIGHT_CLASS_SMALL diff --git a/code/modules/tgchat/to_chat.dm b/code/modules/tgchat/to_chat.dm index 3030ec7fe9..c0e27a2ba3 100644 --- a/code/modules/tgchat/to_chat.dm +++ b/code/modules/tgchat/to_chat.dm @@ -7,18 +7,28 @@ * Circumvents the message queue and sends the message * to the recipient (target) as soon as possible. */ -/proc/to_chat_immediate(target, html, - type = null, - text = null, - avoid_highlighting = FALSE, - // FIXME: These flags are now pointless and have no effect - handle_whitespace = TRUE, - trailing_newline = TRUE, - confidential = FALSE) - if(!target || (!html && !text)) +/proc/to_chat_immediate( + target, + html, + type = null, + text = null, + avoid_highlighting = FALSE, + // FIXME: These flags are now pointless and have no effect + handle_whitespace = TRUE, + trailing_newline = TRUE, + confidential = FALSE +) + // Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "") + html = "[html]" + text = "[text]" + + if(!target) return + if(!html && !text) + CRASH("Empty or null string in to_chat proc call.") if(target == world) target = GLOB.clients + // Build a message var/message = list() if(type) message["type"] = type @@ -53,21 +63,32 @@ * html = "You have found [object]") * ``` */ -/proc/to_chat(target, html, - type = null, - text = null, - avoid_highlighting = FALSE, - // FIXME: These flags are now pointless and have no effect - handle_whitespace = TRUE, - trailing_newline = TRUE, - confidential = FALSE) - if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized) - to_chat_immediate(target, html, type, text) +/proc/to_chat( + target, + html, + type = null, + text = null, + avoid_highlighting = FALSE, + // FIXME: These flags are now pointless and have no effect + handle_whitespace = TRUE, + trailing_newline = TRUE, + confidential = FALSE +) + if(isnull(Master) || Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.initialized) + to_chat_immediate(target, html, type, text, avoid_highlighting) return - if(!target || (!html && !text)) + + // Useful where the integer 0 is the entire message. Use case is enabling to_chat(target, some_boolean) while preventing to_chat(target, "") + html = "[html]" + text = "[text]" + + if(!target) return + if(!html && !text) + CRASH("Empty or null string in to_chat proc call.") if(target == world) target = GLOB.clients + // Build a message var/message = list() if(type) message["type"] = type diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index d858c2d6bf..69888bd641 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -75,6 +75,7 @@ */ /datum/proc/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(src, COMSIG_UI_ACT, usr, action) // If UI is not interactive or usr calling Topic is not the UI user, bail. if(!ui || ui.status != UI_INTERACTIVE) return TRUE diff --git a/code/modules/tgui/status_composers.dm b/code/modules/tgui/status_composers.dm new file mode 100644 index 0000000000..5a8ef843d7 --- /dev/null +++ b/code/modules/tgui/status_composers.dm @@ -0,0 +1,111 @@ +/// The sane defaults for a UI such as a computer or a machine. +/proc/default_ui_state(mob/user, atom/source) + return min( + ui_status_user_is_abled(user, source), + ui_status_user_has_free_hands(user, source), + ui_status_user_is_advanced_tool_user(user), + ui_status_only_living(user), + max( + ui_status_user_is_adjacent(user, source), + ui_status_silicon_has_access(user, source), + ) + ) + +/// Returns a UI status such that users adjacent to source will be able to interact, +/// far away users will be able to see, and anyone farther won't see anything. +/// Dead users will receive updates no matter what, though you likely want to add +/// a [`ui_status_only_living`] check for finer observer interactions. +/proc/ui_status_user_is_adjacent(mob/user, atom/source) + if (isliving(user)) + var/mob/living/living_user = user + return living_user.shared_living_ui_distance(source) + else + return UI_UPDATE + +/// Returns a UI status such that the dead will be able to watch, but not interact. +/proc/ui_status_only_living(mob/user, source) + if (isliving(user)) + return UI_INTERACTIVE + + if(isobserver(user)) + // If they turn on ghost AI control, admins can always interact. + if(IsAdminGhost(user)) + return UI_INTERACTIVE + + // Regular ghosts can always at least view if in range. + var/datum/client_interface/client = GET_CLIENT(user) + if(client) + var/clientviewlist = getviewsize(client.view) + if(get_dist(source, user) < max(clientviewlist[1], clientviewlist[2])) + return UI_UPDATE + + return UI_CLOSE + +/// Returns a UI status such that users with debilitating conditions, such as +/// being dead or not having power for silicons, will not be able to interact. +/// Being dead will disable UI, being incapacitated will continue updating it, +/// and anything else will make it interactive. +/proc/ui_status_user_is_abled(mob/user, atom/source) + return user.shared_ui_interaction(source) + +/// Returns a UI status such that those without blocked hands will be able to interact, +/// but everyone else can only watch. +/proc/ui_status_user_has_free_hands(mob/user, atom/source) + return (!user.restrained(TRUE) && user.get_num_arms() && user.get_empty_held_indexes()) ? UI_UPDATE : UI_INTERACTIVE +/// Returns a UI status such that advanced tool users will be able to interact, +/// but everyone else can only watch. +/proc/ui_status_user_is_advanced_tool_user(mob/user) + return user.IsAdvancedToolUser() ? UI_INTERACTIVE : UI_UPDATE + +/// Returns a UI status such that silicons will be able to interact with whatever +/// they would have access to if this was a machine. For example, AIs can +/// interact if there's cameras with wireless control is enabled. +/proc/ui_status_silicon_has_access(mob/user, atom/source) + if (!issilicon(user)) + return UI_CLOSE + var/mob/living/silicon/silicon_user = user + return silicon_user.get_ui_access(source) + +/// Returns a UI status representing this silicon's capability to access +/// the given source. Called by `ui_status_silicon_has_access`. +/mob/living/silicon/proc/get_ui_access(atom/source) + return UI_CLOSE + +/mob/living/silicon/robot/get_ui_access(atom/source) + // Robots can interact with anything they can see. + var/list/clientviewlist = getviewsize(client.view) + if(get_dist(src, source) <= min(clientviewlist[1],clientviewlist[2])) + return UI_INTERACTIVE + return UI_DISABLED // Otherwise they can keep the UI open. + +/mob/living/silicon/ai/get_ui_access(atom/source) + // The AI can interact with anything it can see nearby, or with cameras while wireless control is enabled. + if(!control_disabled && can_see(source)) + return UI_INTERACTIVE + return UI_CLOSE + +/mob/living/silicon/pai/get_ui_access(atom/source) + // pAIs can only use themselves and the owner's radio. + if((source == src || source == radio) && !stat) + return UI_INTERACTIVE + else + return UI_CLOSE + +/// Returns UI_INTERACTIVE if the user is conscious and lying down. +/// Returns UI_UPDATE otherwise. +/proc/ui_status_user_is_conscious_and_lying_down(mob/user) + if (!isliving(user)) + return UI_UPDATE + + var/mob/living/living_user = user + return (living_user.lying && living_user.stat == CONSCIOUS) \ + ? UI_INTERACTIVE \ + : UI_UPDATE + +/// Return UI_INTERACTIVE if the user is strictly adjacent to the target atom, whether they can see it or not. +/// Return UI_CLOSE otherwise. +/proc/ui_status_user_strictly_adjacent(mob/user, atom/target) + if(get_dist(target, user) > 1) + return UI_CLOSE + + return UI_INTERACTIVE diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index c739d69d0b..943e0070a5 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -92,7 +92,6 @@ window.initialize( fancy = user.client.prefs.tgui_fancy, inline_assets = list( - get_asset_datum(/datum/asset/simple/tgui_common), get_asset_datum(/datum/asset/simple/tgui), )) else @@ -300,7 +299,6 @@ process_status() if(src_object.ui_act(act_type, payload, src, state)) SStgui.update_uis(src_object) - usr?.client?.last_activity = world.time return FALSE switch(type) if("ready") diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm index 89973a925d..e5b3602e86 100644 --- a/code/modules/tgui_panel/external.dm +++ b/code/modules/tgui_panel/external.dm @@ -16,8 +16,8 @@ nuke_chat() - // Failed to fix - action = alert(src, "Did that work?", "", "Yes", "No, switch to old ui") + // Failed to fix, using tgalert as fallback + action = tgalert(src, "Did that work?", "", "Yes", "No, switch to old ui") if (action == "No, switch to old ui") winset(src, "output", "on-show=&is-disabled=0&is-visible=1") winset(src, "browseroutput", "is-disabled=1;is-visible=0") diff --git a/code/modules/tgui_panel/telemetry.dm b/code/modules/tgui_panel/telemetry.dm index e1abfb1e12..98ba1f14b8 100644 --- a/code/modules/tgui_panel/telemetry.dm +++ b/code/modules/tgui_panel/telemetry.dm @@ -78,3 +78,4 @@ var/msg = "[key_name(client)] has a banned account in connection history! (Matched: [found["ckey"]], [found["address"]], [found["computer_id"]])" message_admins(msg) log_admin_private(msg) + log_suspicious_login(msg, access_log_mirror = FALSE) diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index 895333daee..7e2c8ac61f 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -43,7 +43,6 @@ initialized_at = world.time // Perform a clean initialization window.initialize(inline_assets = list( - get_asset_datum(/datum/asset/simple/tgui_common), get_asset_datum(/datum/asset/simple/tgui_panel), )) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) diff --git a/code/modules/tooltip/tooltip.dm b/code/modules/tooltip/tooltip.dm index 725eca89db..82ce046540 100644 --- a/code/modules/tooltip/tooltip.dm +++ b/code/modules/tooltip/tooltip.dm @@ -119,42 +119,34 @@ Notes: //Includes sanity checks. /proc/closeToolTip(mob/user) if(istype(user)) - if(user.client) - var/client/client = user.client - if(client.tooltips) - client.tooltips.hide() - deltimer(client.tip_timer) //delete any in-progress timer if the mouse is moved off the item before it finishes - client.tip_timer = null + if(user.client && user.client.tooltips) + user.client.tooltips.hide() + deltimer(user.client.tip_timer) //delete any in-progress timer if the mouse is moved off the item before it finishes + user.client.tip_timer = null /** - * # `get_tooltip_data()` - * * If set, will return a list for the tooltip (that will also be put together in a `Join()`) * However, if returning `null`, the tooltip will not be shown as #14942 changed it. * * Though no tooltips will be created for atoms that have `tooltips = FALSE` */ /atom/movable/proc/get_tooltip_data() - return + return list() /atom/movable/MouseEntered(location, control, params) . = ..() if(tooltips) - if(!QDELETED(src) && usr?.client.prefs.enable_tips) + if((get(src, /mob) == usr && !QDELETED(src)) && usr?.client.prefs.enable_tips) var/list/tooltip_data = get_tooltip_data() if(length(tooltip_data)) var/examine_data = tooltip_data.Join("
") - var/timedelay = max(usr.client.prefs.tip_delay * 0.01, 0.01) // I heard multiplying is faster, also runtimes from very low/negative numbers - usr.client.tip_timer = addtimer(CALLBACK(GLOBAL_PROC, .proc/openToolTip, usr, src, params, name, examine_data), timedelay, TIMER_STOPPABLE) + var/timedelay = usr.client.prefs.tip_delay/100 + usr.client.tip_timer = addtimer(CALLBACK(GLOBAL_PROC, .proc/openToolTip, usr, src, params, name, examine_data), timedelay, TIMER_STOPPABLE)//timer takes delay in deciseconds, but the pref is in milliseconds. dividing by 100 converts it. + +/obj/item/MouseDrop(atom/over, src_location, over_location, src_control, over_control, params) + . = ..() + closeToolTip(usr) /atom/movable/MouseExited(location, control, params) . = ..() closeToolTip(usr) - -/client/MouseDown(object, location, control, params) - closeToolTip(usr) - . = ..() - -/client - /// Timers are now handled by clients, not by doing a mess on the item and multiple people overwriting a single timer on the object, have fun. - var/tip_timer = null diff --git a/code/modules/uplink/uplink_devices.dm b/code/modules/uplink/uplink_devices.dm index 9c09a7334a..9660718bb6 100644 --- a/code/modules/uplink/uplink_devices.dm +++ b/code/modules/uplink/uplink_devices.dm @@ -19,25 +19,46 @@ throw_range = 7 w_class = WEIGHT_CLASS_SMALL + /// The uplink flag for this type. + /// See [`code/__DEFINES/uplink.dm`] + var/uplink_flag = UPLINK_TRAITORS + /obj/item/uplink/Initialize(mapload, owner, tc_amount = 20) . = ..() - AddComponent(/datum/component/uplink, owner, FALSE, TRUE, null, tc_amount) + AddComponent(/datum/component/uplink, owner, FALSE, TRUE, uplink_flag, tc_amount) -/obj/item/uplink/nuclear/Initialize() +/obj/item/uplink/debug + name = "debug uplink" + +/obj/item/uplink/debug/Initialize(mapload, owner, tc_amount = 9000) . = ..() var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) - hidden_uplink.set_gamemode(/datum/game_mode/nuclear) + hidden_uplink.name = "debug uplink" + hidden_uplink.debug = TRUE + +/obj/item/uplink/nuclear + uplink_flag = UPLINK_NUKE_OPS + +/obj/item/uplink/nuclear/debug + name = "debug nuclear uplink" + uplink_flag = UPLINK_NUKE_OPS + +/obj/item/uplink/nuclear/debug/Initialize(mapload, owner, tc_amount = 9000) + . = ..() + var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) + hidden_uplink.name = "debug nuclear uplink" + hidden_uplink.debug = TRUE + +/obj/item/uplink/nuclear_restricted + uplink_flag = UPLINK_NUKE_OPS /obj/item/uplink/nuclear_restricted/Initialize() . = ..() var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) hidden_uplink.allow_restricted = FALSE - hidden_uplink.set_gamemode(/datum/game_mode/nuclear) -/obj/item/uplink/clownop/Initialize() - . = ..() - var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) - hidden_uplink.set_gamemode(/datum/game_mode/nuclear/clown_ops) +/obj/item/uplink/clownop + uplink_flag = UPLINK_CLOWN_OPS /obj/item/uplink/old name = "dusty radio" @@ -51,28 +72,9 @@ // Multitool uplink /obj/item/multitool/uplink/Initialize(mapload, owner, tc_amount = 20) . = ..() - AddComponent(/datum/component/uplink, owner, FALSE, TRUE, null, tc_amount) + AddComponent(/datum/component/uplink, owner, FALSE, TRUE, UPLINK_TRAITORS, tc_amount) // Pen uplink /obj/item/pen/uplink/Initialize(mapload, owner, tc_amount = 20) . = ..() - AddComponent(/datum/component/uplink, owner, TRUE, FALSE, null, tc_amount) - -/obj/item/uplink/debug - name = "debug uplink" - -/obj/item/uplink/debug/Initialize(mapload, owner, tc_amount = 9000) - . = ..() - var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) - hidden_uplink.name = "debug uplink" - hidden_uplink.debug = TRUE - -/obj/item/uplink/nuclear/debug - name = "debug nuclear uplink" - -/obj/item/uplink/nuclear/debug/Initialize(mapload, owner, tc_amount = 9000) - . = ..() - var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) - hidden_uplink.set_gamemode(/datum/game_mode/nuclear) - hidden_uplink.name = "debug nuclear uplink" - hidden_uplink.debug = TRUE + AddComponent(/datum/component/uplink, owner, TRUE, FALSE, UPLINK_TRAITORS, tc_amount) diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm index cc676777db..e33b675921 100644 --- a/code/modules/uplink/uplink_items.dm +++ b/code/modules/uplink/uplink_items.dm @@ -1,54 +1,76 @@ -/proc/get_uplink_items(datum/game_mode/gamemode, allow_sales = TRUE, allow_restricted = TRUE, other_filter = list()) - var/list/filtered_uplink_items = GLOB.uplink_categories.Copy() // list of uplink categories without associated values. +GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item)) + +/proc/get_uplink_items(uplink_flag, allow_sales = TRUE, allow_restricted = TRUE) + var/list/filtered_uplink_items = list() var/list/sale_items = list() for(var/path in GLOB.uplink_items) var/datum/uplink_item/I = new path - if(I.include_modes.len) - if(!gamemode && SSticker.mode && !(SSticker.mode.type in I.include_modes)) - continue - if(gamemode && !(gamemode in I.include_modes)) - continue - if(I.exclude_modes.len) - if(!gamemode && SSticker.mode && (SSticker.mode.type in I.exclude_modes)) - continue - if(gamemode && (gamemode in I.exclude_modes)) - continue + if(!I.item) + continue + if (!(I.purchasable_from & uplink_flag)) + continue if(I.player_minimum && I.player_minimum > GLOB.joined_player_list.len) continue if (I.restricted && !allow_restricted) continue - if (I.type in other_filter) - continue - LAZYSET(filtered_uplink_items[I.category], I.name, I) + if(!filtered_uplink_items[I.category]) + filtered_uplink_items[I.category] = list() + filtered_uplink_items[I.category][I.name] = I if(I.limited_stock < 0 && !I.cant_discount && I.item && I.cost > 1) sale_items += I if(allow_sales) - for(var/i in 1 to 3) - var/datum/uplink_item/I = pick_n_take(sale_items) - var/datum/uplink_item/A = new I.type - var/discount = A.get_discount() - var/list/disclaimer = list("Void where prohibited.", "Not recommended for children.", "Contains small parts.", "Check local laws for legality in region.", "Do not taunt.", "Not responsible for direct, indirect, incidental or consequential damages resulting from any defect, error or failure to perform.", "Keep away from fire or flames.", "Product is provided \"as is\" without any implied or expressed warranties.", "As seen on TV.", "For recreational use only.", "Use only as directed.", "16% sales tax will be charged for orders originating within Space Nebraska.") - A.limited_stock = 1 - I.refundable = FALSE //THIS MAN USES ONE WEIRD TRICK TO GAIN FREE TC, CODERS HATES HIM! - A.refundable = FALSE - if(A.cost >= 20) //Tough love for nuke ops - discount *= 0.5 - A.cost = max(round(A.cost * discount),1) - A.category = "Discounted Gear" - A.name += " ([round(((initial(A.cost)-A.cost)/initial(A.cost))*100)]% off!)" - A.desc += " Normally costs [initial(A.cost)] TC. All sales final. [pick(disclaimer)]" - A.item = I.item + var/datum/team/nuclear/nuclear_team + // if (uplink_flag & UPLINK_NUKE_OPS) // uplink code kind of needs a redesign + // nuclear_team = locate() in GLOB.antagonist_teams // the team discounts could be in a GLOB with this design but it would make sense for them to be team specific... + if (!nuclear_team) + create_uplink_sales(3, "Discounted Gear", 1, sale_items, filtered_uplink_items) + else + if (!nuclear_team.team_discounts) + // create 5 unlimited stock discounts + create_uplink_sales(5, "Discounted Team Gear", -1, sale_items, filtered_uplink_items) + // Create 10 limited stock discounts + create_uplink_sales(10, "Limited Stock Team Gear", 1, sale_items, filtered_uplink_items) + nuclear_team.team_discounts = list("Discounted Team Gear" = filtered_uplink_items["Discounted Team Gear"], "Limited Stock Team Gear" = filtered_uplink_items["Limited Stock Team Gear"]) + else + for(var/cat in nuclear_team.team_discounts) + for(var/item in nuclear_team.team_discounts[cat]) + var/datum/uplink_item/D = nuclear_team.team_discounts[cat][item] + var/datum/uplink_item/O = filtered_uplink_items[initial(D.category)][initial(D.name)] + O.refundable = FALSE - LAZYSET(filtered_uplink_items[A.category], A.name, A) + filtered_uplink_items["Discounted Team Gear"] = nuclear_team.team_discounts["Discounted Team Gear"] + filtered_uplink_items["Limited Stock Team Gear"] = nuclear_team.team_discounts["Limited Stock Team Gear"] - for(var/category in filtered_uplink_items) - if(!filtered_uplink_items[category]) //empty categories with no associated uplink item. Remove. - filtered_uplink_items -= category return filtered_uplink_items +/proc/create_uplink_sales(num, category_name, limited_stock, sale_items, uplink_items) + if (num <= 0) + return + + if(!uplink_items[category_name]) + uplink_items[category_name] = list() + + for (var/i in 1 to num) + var/datum/uplink_item/I = pick_n_take(sale_items) + var/datum/uplink_item/A = new I.type + var/discount = A.get_discount() + var/list/disclaimer = list("Void where prohibited.", "Not recommended for children.", "Contains small parts.", "Check local laws for legality in region.", "Do not taunt.", "Not responsible for direct, indirect, incidental or consequential damages resulting from any defect, error or failure to perform.", "Keep away from fire or flames.", "Product is provided \"as is\" without any implied or expressed warranties.", "As seen on TV.", "For recreational use only.", "Use only as directed.", "16% sales tax will be charged for orders originating within Space Nebraska.") + A.limited_stock = limited_stock + I.refundable = FALSE //THIS MAN USES ONE WEIRD TRICK TO GAIN FREE TC, CODERS HATES HIM! + A.refundable = FALSE + if(A.cost >= 20) //Tough love for nuke ops + discount *= 0.5 + A.category = category_name + A.cost = max(round(A.cost * discount),1) + A.name += " ([round(((initial(A.cost)-A.cost)/initial(A.cost))*100)]% off!)" + A.desc += " Normally costs [initial(A.cost)] TC. All sales final. [pick(disclaimer)]" + A.item = I.item + + uplink_items[category_name][A.name] = A + /** * Uplink Items @@ -67,12 +89,14 @@ var/surplus = 100 // Chance of being included in the surplus crate. var/cant_discount = FALSE var/limited_stock = -1 //Setting this above zero limits how many times this item can be bought by the same traitor in a round, -1 is unlimited - var/list/include_modes = list() // Game modes to allow this item in. - var/list/exclude_modes = list() // Game modes to disallow this item from. + /// A bitfield to represent what uplinks can purchase this item. + /// See [`code/__DEFINES/uplink.dm`]. + var/purchasable_from = ALL var/list/restricted_roles = list() //If this uplink item is only available to certain roles. Roles are dependent on the frequency chip or stored ID. var/player_minimum //The minimum crew size needed for this item to be added to uplinks. var/purchase_log_vis = TRUE // Visible in the purchase log? var/restricted = FALSE // Adds restrictions for VR/Events + var/list/restricted_species //Limits items to a specific species. Hopefully. var/illegal_tech = TRUE // Can this item be deconstructed to unlock certain techweb research nodes? /datum/uplink_item/proc/get_discount() @@ -80,6 +104,7 @@ /datum/uplink_item/proc/purchase(mob/user, datum/component/uplink/U) var/atom/A = spawn_item(item, user, U) + // log_uplink("[key_name(user)] purchased [src] for [cost] telecrystals from [U.parent]'s uplink") if(purchase_log_vis && U.purchase_log) U.purchase_log.LogPurchase(A, src, cost) @@ -94,70 +119,66 @@ if(ishuman(user) && istype(A, /obj/item)) var/mob/living/carbon/human/H = user if(H.put_in_hands(A)) - to_chat(H, "[A] materializes into your hands!") + to_chat(H, span_boldnotice("[A] materializes into your hands!")) return A - to_chat(user, "[A] materializes onto the floor.") + to_chat(user, span_boldnotice("[A] materializes onto the floor!")) return A -/* - Uplink Categories: - Due to how the typesof() in-built byond proc works, it should be kept in mind - the order categories are displayed in the uplink UI is same to the order they are loaded in the code. - I trust no extra filter is needed as long as they are all contained within the following lines. - When adding new uplink categories, please keep them separate from their sub paths here and without set item. - Failure to comply may result in the new categories being listed at the bottom of the UI. -*/ +//Discounts (dynamically filled above) +/datum/uplink_item/discounts + category = "Discounts" +// cit specific. idk /datum/uplink_item/holiday category = "Holiday" -/datum/uplink_item/bundles_TC - category = "Telecrystals and Bundles" +//All bundles and telecrystals +/datum/uplink_item/bundles_tc + category = "Bundles" surplus = 0 cant_discount = TRUE +// Dangerous Items /datum/uplink_item/dangerous category = "Conspicuous Weapons" -/datum/uplink_item/stealthy_weapons - category = "Stealthy Weapons" - -/datum/uplink_item/ammo - category = "Ammunition" - surplus = 40 - -/datum/uplink_item/explosives - category = "Explosives" - +//Support and Mechs /datum/uplink_item/support category = "Support and Exosuits" surplus = 0 - include_modes = list(/datum/game_mode/nuclear) - -/datum/uplink_item/suits - category = "Clothing" - surplus = 40 + purchasable_from = UPLINK_NUKE_OPS +// Stealth Items /datum/uplink_item/stealthy_tools category = "Stealth Gadgets" +//Space Suits and Hardsuits +/datum/uplink_item/suits + category = "Space Suits" + surplus = 40 + +// Devices and Tools /datum/uplink_item/device_tools category = "Misc. Gadgets" +// Implants /datum/uplink_item/implants category = "Implants" surplus = 50 +//Race-specific items +/datum/uplink_item/race_restricted + category = "Species-Restricted" + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) + surplus = 0 + +// Role-specific items /datum/uplink_item/role_restricted category = "Role-Restricted" - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) surplus = 0 - cant_discount = TRUE +// Pointless /datum/uplink_item/badass category = "(Pointless) Badassery" surplus = 0 - -//Discounts (dynamically filled above) -/datum/uplink_item/discounts - category = "Discounted Gear" diff --git a/code/modules/uplink/uplink_items/uplink_ammo.dm b/code/modules/uplink/uplink_items/uplink_ammo.dm index a7f3f5321d..a1c96d1082 100644 --- a/code/modules/uplink/uplink_items/uplink_ammo.dm +++ b/code/modules/uplink/uplink_items/uplink_ammo.dm @@ -12,7 +12,7 @@ desc = "Contains 10 additional .45-70 GOVT rounds. Caliber is exceedingly rare, and thus, comes at a premium." item = /obj/item/ammo_box/g4570 cost = 5 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/pistol name = "10mm Handgun Magazine" @@ -20,7 +20,7 @@ are dirt cheap but are half as effective as .357 rounds." item = /obj/item/ammo_box/magazine/m10mm cost = 1 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/ammo/pistol/box name = "Ammo Box - 10mm" @@ -34,7 +34,7 @@ These rounds are less effective at injuring the target but penetrate protective gear." item = /obj/item/ammo_box/magazine/m10mm/ap cost = 2 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/ammo/pistolap/box name = "Ammo Box - 10mm Armour Piercing" @@ -48,7 +48,7 @@ These rounds are more damaging but ineffective against armour." item = /obj/item/ammo_box/magazine/m10mm/hp cost = 3 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/ammo/pistolhp/box name = "Ammo Box - 10mm Hollow Point" @@ -62,7 +62,7 @@ Loaded with incendiary rounds which inflict little damage, but ignite the target." item = /obj/item/ammo_box/magazine/m10mm/fire cost = 2 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/ammo/pistolfire/box name = "Ammo Box - 10mm Incendiary" @@ -85,7 +85,7 @@ /datum/uplink_item/ammo/shotgun cost = 2 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/shotgun/bag name = "12g Ammo Duffel Bag" @@ -136,7 +136,7 @@ For when you really need a lot of things dead." item = /obj/item/ammo_box/a357 cost = 3 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/ammo/revolver/ap name = ".357 Armor Piercing Speed Loader" @@ -150,25 +150,25 @@ Your teammates will ask you to not shoot these down small hallways." item = /obj/item/ammo_casing/a40mm cost = 2 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/smg/bag name = ".45 Ammo Duffel Bag" desc = "A duffel bag filled with enough .45 ammo to supply an entire team, at a discounted price." item = /obj/item/storage/backpack/duffelbag/syndie/ammo/smg cost = 20 //instead of 27 TC - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/smg name = ".45 SMG Magazine" desc = "An additional 24-round .45 magazine suitable for use with the C-20r submachine gun." item = /obj/item/ammo_box/magazine/smgm45 cost = 3 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/sniper cost = 4 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/sniper/basic name = ".50 Magazine" @@ -194,7 +194,7 @@ These bullets pack less punch than 7.12x82mm rounds, but they still offer more power than .45 ammo." item = /obj/item/ammo_box/magazine/m556 cost = 4 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/machinegun/match name = "7.12x82mm (Match) Box Magazine" @@ -206,7 +206,7 @@ /datum/uplink_item/ammo/machinegun cost = 6 surplus = 0 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/machinegun/basic name = "1.95x129mm Box Magazine" @@ -234,7 +234,7 @@ item = /obj/item/ammo_box/magazine/mm195x129/incen /datum/uplink_item/ammo/rocket - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/rocket/basic name = "84mm HE Rocket" @@ -254,7 +254,7 @@ desc = "An additional 15-round 9mm magazine, compatible with the Stechkin APS pistol, found in the Spetsnaz Pyro bundle." item = /obj/item/ammo_box/magazine/pistolm9mm cost = 2 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/pistolaps name = "Ammo Box - 9mm" @@ -268,7 +268,7 @@ Loaded with armor piercing flechettes that very nearly ignore armor, but are not very effective against flesh." item = /obj/item/ammo_box/magazine/flechette cost = 2 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/flechettes name = "Serrated Flechette Magazine" @@ -277,7 +277,7 @@ These flechettes are highly likely to sever arteries, and even limbs." item = /obj/item/ammo_box/magazine/flechette/s cost = 2 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/toydarts name = "Box of Riot Darts" @@ -293,32 +293,32 @@ and broca systems, making it impossible for them to move or speak for some time." item = /obj/item/storage/box/syndie_kit/bioterror cost = 6 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/ammo/bolt_action name = "Surplus Rifle Clip" desc = "A stripper clip used to quickly load bolt action rifles. Contains 5 rounds." item = /obj/item/ammo_box/a762 cost = 1 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/bolt_action_bulk name = "Surplus Rifle Clip Box" desc = "An ammo box we found in a warehouse, holding 7 clips of 5 rounds for bolt-action rifles. Yes, the cheap ones." item = /obj/item/storage/toolbox/ammo cost = 4 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/dark_gygax/bag name = "Dark Gygax Ammo Bag" desc = "A duffel bag containing ammo for three full reloads of the incendiary carbine and flash bang launcher that are equipped on a standard Dark Gygax exosuit." item = /obj/item/storage/backpack/duffelbag/syndie/ammo/dark_gygax cost = 4 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/ammo/mauler/bag name = "Mauler Ammo Bag" desc = "A duffel bag containing ammo for three full reloads of the LMG, scattershot carbine, and SRM-8 missile laucher that are equipped on a standard Mauler exosuit." item = /obj/item/storage/backpack/duffelbag/syndie/ammo/mauler cost = 6 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS diff --git a/code/modules/uplink/uplink_items/uplink_badass.dm b/code/modules/uplink/uplink_items/uplink_badass.dm index ec0ebf66d1..23f7063c7e 100644 --- a/code/modules/uplink/uplink_items/uplink_badass.dm +++ b/code/modules/uplink/uplink_items/uplink_badass.dm @@ -14,9 +14,9 @@ item = /obj/item/storage/box/syndie_kit/chameleon/broken /datum/uplink_item/badass/costumes - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) cost = 4 cant_discount = TRUE + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/badass/costumes/centcom_official name = "CentCom Official Costume" @@ -77,14 +77,14 @@ cost = 4 limited_stock = 1 cant_discount = TRUE - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/badass/gaming_cardpack name = "TCG Card Operatives Bundle" desc = "A bundle full of goodies required to work as a TCG Card Operative. A warm pajama, a mug of cocoa, a plushie and a two packs full of rare 2560 Core Set cards!" item = /obj/item/storage/box/syndie_kit/sleepytime/cardpack cost = 20 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/badass/cardpack name = "TCG Nuclear Cardpack" diff --git a/code/modules/uplink/uplink_items/uplink_bundles.dm b/code/modules/uplink/uplink_items/uplink_bundles.dm index cf33c893ad..a7ff04abbc 100644 --- a/code/modules/uplink/uplink_items/uplink_bundles.dm +++ b/code/modules/uplink/uplink_items/uplink_bundles.dm @@ -7,30 +7,30 @@ When adding new entries to the file, please keep them sorted by category. */ -/datum/uplink_item/bundles_TC/chemical +/datum/uplink_item/bundles_tc/chemical name = "Bioterror bundle" desc = "For the madman: Contains a handheld Bioterror chem sprayer, a Bioterror foam grenade, a box of lethal chemicals, a dart pistol, \ box of syringes, Donksoft assault rifle, and some riot darts. Remember: Seal suit and equip internals before use." item = /obj/item/storage/backpack/duffelbag/syndie/med/bioterrorbundle cost = 30 // normally 42 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/bulldog +/datum/uplink_item/bundles_tc/bulldog name = "Bulldog bundle" desc = "Lean and mean: Optimized for people that want to get up close and personal. Contains the popular \ Bulldog shotgun, a 12g buckshot drum, a 12g taser slug drum and a pair of Thermal imaging goggles." item = /obj/item/storage/backpack/duffelbag/syndie/bulldogbundle cost = 13 // normally 16 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/c20r +/datum/uplink_item/bundles_tc/c20r name = "C-20r bundle" desc = "Old Faithful: The classic C-20r, bundled with two magazines, and a (surplus) suppressor at discount price." item = /obj/item/storage/backpack/duffelbag/syndie/c20rbundle cost = 14 // normally 16 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/contract_kit +/datum/uplink_item/bundles_tc/contract_kit name = "Contract Kit" desc = "The Syndicate have offered you the chance to become a contractor, take on kidnapping contracts for TC and cash payouts. Upon purchase, \ you'll be granted your own contract uplink embedded within the supplied tablet computer. Additionally, you'll be granted \ @@ -39,10 +39,10 @@ item = /obj/item/storage/box/syndie_kit/contract_kit cost = 20 player_minimum = 25 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) restricted = TRUE -/datum/uplink_item/bundles_TC/northstar_bundle +/datum/uplink_item/bundles_tc/northstar_bundle name = "Northstar Bundle" desc = "An item usually reserved for the Gorlex Marauders and their operatives, now available for recreational use. \ These armbands let the user punch people very fast and with the lethality of a legendary martial artist. \ @@ -50,16 +50,16 @@ Combines with all martial arts, but the user will be unable to bring themselves to use guns, nor remove the armbands." item = /obj/item/storage/box/syndie_kit/northstar cost = 20 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) -/datum/uplink_item/bundles_TC/scarp_bundle +/datum/uplink_item/bundles_tc/scarp_bundle name = "Sleeping Carp Bundle" desc = "Become one with your inner carp! Your ancient fish masters leave behind their legacy, and bestow to you their teachings, sacred uniform, and staff. \ Please be aware that you will not be able to use dishonerable ranged weapons." item = /obj/item/storage/box/syndie_kit/scarp cost = 20 player_minimum = 20 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/suits/infiltrator_bundle name = "Insidious Infiltration Gear Case" @@ -69,86 +69,85 @@ item = /obj/item/storage/toolbox/infiltrator cost = 5 limited_stock = 1 //you only get one so you don't end up with too many gun cases - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) -/datum/uplink_item/bundles_TC/cybernetics_bundle +/datum/uplink_item/bundles_tc/cybernetics_bundle name = "Cybernetic Implants Bundle" desc = "A random selection of cybernetic implants. Guaranteed 5 high quality implants. Comes with an autosurgeon." item = /obj/item/storage/box/cyber_implants cost = 40 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/medical +/datum/uplink_item/bundles_tc/medical name = "Medical bundle" desc = "The support specialist: Aid your fellow operatives with this medical bundle. Contains a tactical medkit, \ a Donksoft LMG, a box of riot darts and a pair of magboots to rescue your friends in no-gravity environments." item = /obj/item/storage/backpack/duffelbag/syndie/med/medicalbundle cost = 15 // normally 20 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/modular +/datum/uplink_item/bundles_tc/modular name = "Modular Pistol Kit" desc = "A heavy briefcase containing one modular pistol (chambered in 10mm), one supressor, and spare ammunition, including a box of soporific ammo. \ Includes a suit jacket that is padded with a robust liner." item = /obj/item/storage/briefcase/modularbundle cost = 12 -/datum/uplink_item/bundles_TC/shredder +/datum/uplink_item/bundles_tc/shredder name = "Shredder bundle" desc = "A truly horrific weapon designed simply to maim its victim, the CX Shredder is banned by several intergalactic treaties. \ You'll get two of them with this. And spare ammo to boot. And we'll throw in an extra elite hardsuit and chest rig to hold them all!" item = /obj/item/storage/backpack/duffelbag/syndie/shredderbundle cost = 30 // normally 41 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/sniper +/datum/uplink_item/bundles_tc/sniper name = "Sniper bundle" desc = "Elegant and refined: Contains a collapsed sniper rifle in an expensive carrying case, \ two soporific knockout magazines, a free surplus supressor, and a sharp-looking tactical turtleneck suit. \ We'll throw in a free red tie if you order NOW." item = /obj/item/storage/briefcase/sniperbundle cost = 20 // normally 26 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/firestarter +/datum/uplink_item/bundles_tc/firestarter name = "Spetsnaz Pyro bundle" desc = "For systematic suppression of carbon lifeforms in close quarters: Contains a lethal New Russian backpack spray, Elite hardsuit, \ Stechkin APS pistol, two magazines, a minibomb and a stimulant syringe. \ Order NOW and comrade Boris will throw in an extra tracksuit." item = /obj/item/storage/backpack/duffelbag/syndie/firestarter cost = 30 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS -/datum/uplink_item/bundles_TC/bundle +/datum/uplink_item/bundles_tc/bundle name = "Syndicate Bundle" desc = "Syndicate Bundles are specialized groups of items that arrive in a plain box. \ These items are collectively worth more than 20 telecrystals, but you do not know which specialization \ you will receive. May contain discontinued and/or exotic items." item = /obj/item/storage/box/syndicate cost = 20 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~(UPLINK_NUKE_OPS) cant_discount = TRUE -/datum/uplink_item/bundles_TC/surplus +/datum/uplink_item/bundles_tc/surplus name = "Syndicate Surplus Crate" desc = "A dusty crate from the back of the Syndicate warehouse. Rumored to contain a valuable assortment of items, \ but you never know. Contents are sorted to always be worth 50 TC." item = /obj/structure/closet/crate cost = 20 - player_minimum = 20 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) - cant_discount = TRUE + player_minimum = 25 + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) var/starting_crate_value = 50 -/datum/uplink_item/bundles_TC/surplus/super +/datum/uplink_item/bundles_tc/surplus/super name = "Super Surplus Crate" desc = "A dusty SUPER-SIZED from the back of the Syndicate warehouse. Rumored to contain a valuable assortment of items, \ but you never know. Contents are sorted to always be worth 125 TC." cost = 40 - player_minimum = 30 + player_minimum = 40 starting_crate_value = 125 -/datum/uplink_item/bundles_TC/surplus/purchase(mob/user, datum/component/uplink/U) +/datum/uplink_item/bundles_tc/surplus/purchase(mob/user, datum/component/uplink/U) var/list/uplink_items = get_uplink_items(SSticker && SSticker.mode? SSticker.mode : null, FALSE) var/crate_value = starting_crate_value @@ -170,7 +169,7 @@ U.purchase_log.LogPurchase(goods, I, 0) return C -/datum/uplink_item/bundles_TC/reroll +/datum/uplink_item/bundles_tc/reroll name = "Renegotiate Contract" desc = "Selecting this will inform your employers that you wish for new objectives. Can only be done twice." item = /obj/effect/gibspawner/generic @@ -179,22 +178,21 @@ restricted = TRUE limited_stock = 2 -/datum/uplink_item/bundles_TC/reroll/purchase(mob/user, datum/component/uplink/U) +/datum/uplink_item/bundles_tc/reroll/purchase(mob/user, datum/component/uplink/U) var/datum/antagonist/traitor/T = user?.mind?.has_antag_datum(/datum/antagonist/traitor) if(istype(T)) T.set_traitor_kind(get_random_traitor_kind(blacklist = list(/datum/traitor_class/human/freeform, /datum/traitor_class/human/hijack, /datum/traitor_class/human/martyr))) else to_chat(user,"Invalid user for contract renegotiation.") -/datum/uplink_item/bundles_TC/random +/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 cost = 0 cant_discount = TRUE - exclude_modes = list(/datum/game_mode/traitor/internal_affairs) -/datum/uplink_item/bundles_TC/random/purchase(mob/user, datum/component/uplink/U) +/datum/uplink_item/bundles_tc/random/purchase(mob/user, datum/component/uplink/U) var/list/uplink_items = U.uplink_items var/list/possible_items = list() for(var/category in uplink_items) @@ -202,7 +200,7 @@ var/datum/uplink_item/I = uplink_items[category][item] if(src == I || !I.item) continue - if(istype(I, /datum/uplink_item/bundles_TC/reroll)) //oops! + if(istype(I, /datum/uplink_item/bundles_tc/reroll)) //oops! continue if(U.telecrystals < I.cost) continue @@ -215,7 +213,7 @@ SSblackbox.record_feedback("tally", "traitor_random_uplink_items_gotten", 1, initial(I.name)) U.MakePurchase(user, I) -/datum/uplink_item/bundles_TC/telecrystal +/datum/uplink_item/bundles_tc/telecrystal name = "1 Raw Telecrystal" desc = "A telecrystal in its rawest and purest form; can be utilized on active uplinks to increase their telecrystal count." item = /obj/item/stack/telecrystal @@ -226,13 +224,13 @@ // it's just used to buy more items (including itself!) purchase_log_vis = FALSE -/datum/uplink_item/bundles_TC/telecrystal/five +/datum/uplink_item/bundles_tc/telecrystal/five name = "5 Raw Telecrystals" desc = "Five telecrystals in their rawest and purest form; can be utilized on active uplinks to increase their telecrystal count." item = /obj/item/stack/telecrystal/five cost = 5 -/datum/uplink_item/bundles_TC/telecrystal/twenty +/datum/uplink_item/bundles_tc/telecrystal/twenty name = "20 Raw Telecrystals" desc = "Twenty telecrystals in their rawest and purest form; can be utilized on active uplinks to increase their telecrystal count." item = /obj/item/stack/telecrystal/twenty diff --git a/code/modules/uplink/uplink_items/uplink_clothing.dm b/code/modules/uplink/uplink_items/uplink_clothing.dm index 064d1bb429..4f9f8a7b67 100644 --- a/code/modules/uplink/uplink_items/uplink_clothing.dm +++ b/code/modules/uplink/uplink_items/uplink_clothing.dm @@ -12,35 +12,35 @@ desc = "A slightly armored conspicious jumpsuit that has no suit sensors attached to them, if someone sees you in this hope they think its a fake." item = /obj/item/clothing/under/syndicate cost = 1 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) //They already get these + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/suits/turtlenck_skirt name = "Tactical Skirtleneck" desc = "A slightly armored conspicious jumpsuit that has no suit sensors attached to them, if someone sees you in this hope they think its a fake." item = /obj/item/clothing/under/syndicate/skirt cost = 1 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) //They already get these + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/suits/padding name = "Soft Padding" desc = "An inconspicious soft padding meant to be worn underneath jumpsuits, will cushion the user from melee harm." item = /obj/item/clothing/accessory/padding cost = 2 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/suits/kevlar name = "Kevlar Padding" desc = "An inconspicious kevlar padding meant to be worn underneath jumpsuits, will cushion the wearer from ballistic harm." item = /obj/item/clothing/accessory/kevlar cost = 2 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/suits/plastic name = "Ablative Padding" desc = "An inconspicious ablative padding meant to be worn underneath jumpsuits, will cushion the wearer from energy lasers harm." item = /obj/item/clothing/accessory/plastics cost = 2 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/suits/space_suit name = "Syndicate Space Suit" @@ -59,7 +59,7 @@ Nanotrasen crew who spot these suits are known to panic." item = /obj/item/clothing/suit/space/hardsuit/syndi cost = 8 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) //you can't buy it in nuke, because the elite hardsuit costs the same while being better + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) //you can't buy it in nuke, because the elite hardsuit costs the same while being better /datum/uplink_item/suits/hardsuit/elite name = "Elite Syndicate Hardsuit" @@ -67,8 +67,7 @@ provides the user with superior armor and mobility compared to the standard Syndicate hardsuit." item = /obj/item/clothing/suit/space/hardsuit/syndi/elite cost = 8 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) - exclude_modes = list() + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/suits/hardsuit/shielded name = "Shielded Syndicate Hardsuit" @@ -76,8 +75,7 @@ The shields can handle up to three impacts within a short duration and will rapidly recharge while not under fire." item = /obj/item/clothing/suit/space/hardsuit/shielded/syndi cost = 30 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) - exclude_modes = list() + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/suits/thiefgloves name = "Thieving Gloves" @@ -90,13 +88,13 @@ desc = "Through bluespace magic stolen from an organisation that hoards technology, these boots simply allow you to slip through the atoms that make up anything, but only while walking, for safety reasons. As well as this, they unfortunately cause minor breath loss as the majority of atoms in your lungs are sucked out into any solid object you walk through." item = /obj/item/clothing/shoes/wallwalkers cost = 6 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/device_tools/guerrillagloves name = "Guerrilla Gloves" desc = "A pair of highly robust combat gripper gloves that excels at performing takedowns at close range, with an added lining of insulation. Careful not to hit a wall!" item = /obj/item/clothing/gloves/tackler/combat/insulated - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS cost = 2 /datum/uplink_item/device_tools/syndicate_eyepatch @@ -110,4 +108,4 @@ desc = "A pair of highly reinforced armwraps allowing the user to parry almost anything. Fully reflects projectiles, no downsides to failing, but is very hard to parry melee with." cost = 6 item = /obj/item/clothing/gloves/fingerless/ablative - exclude_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS diff --git a/code/modules/uplink/uplink_items/uplink_dangerous.dm b/code/modules/uplink/uplink_items/uplink_dangerous.dm index 0032f83d09..dac426db2b 100644 --- a/code/modules/uplink/uplink_items/uplink_dangerous.dm +++ b/code/modules/uplink/uplink_items/uplink_dangerous.dm @@ -13,7 +13,7 @@ with suppressors." item = /obj/item/storage/box/syndie_kit/pistol cost = 7 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/revolver name = "Syndicate Revolver Kit" @@ -22,7 +22,7 @@ cost = 13 player_minimum = 15 surplus = 50 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/rawketlawnchair name = "84mm Rocket Propelled Grenade Launcher" @@ -31,7 +31,7 @@ item = /obj/item/gun/ballistic/rocketlauncher cost = 8 surplus = 30 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/antitank name = "Anti Tank Pistol" @@ -42,7 +42,7 @@ item = /obj/item/gun/ballistic/automatic/pistol/antitank/syndicate cost = 14 surplus = 25 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/pie_cannon name = "Banana Cream Pie Cannon" @@ -50,7 +50,7 @@ cost = 10 item = /obj/item/pneumatic_cannon/pie/selfcharge surplus = 0 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/bananashield name = "Bananium Energy Shield" @@ -60,7 +60,7 @@ item = /obj/item/shield/energy/bananium cost = 16 surplus = 0 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/clownsword name = "Bananium Energy Sword" @@ -69,7 +69,7 @@ item = /obj/item/melee/transforming/energy/sword/bananium cost = 3 surplus = 0 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/bioterror name = "Biohazardous Chemical Sprayer" @@ -79,7 +79,7 @@ item = /obj/item/reagent_containers/spray/chemsprayer/bioterror cost = 20 surplus = 0 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/throwingweapons name = "Box of Throwing Weapons" @@ -95,7 +95,7 @@ item = /obj/item/gun/ballistic/automatic/shotgun/bulldog cost = 8 surplus = 40 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/smg name = "C-20r Submachine Gun" @@ -104,7 +104,7 @@ item = /obj/item/gun/ballistic/automatic/c20r cost = 10 surplus = 40 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/doublesword name = "Double-Bladed Energy Sword" @@ -113,7 +113,7 @@ item = /obj/item/dualsaber player_minimum = 25 cost = 16 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/doublesword/get_discount() return pick(4;0.8,2;0.65,1;0.5) @@ -125,7 +125,7 @@ item = /obj/item/dualsaber/hypereutactic player_minimum = 25 cost = 16 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/hyperblade/get_discount() return pick(4;0.8,2;0.65,1;0.5) @@ -136,7 +136,7 @@ pocketed when inactive. Activating it produces a loud, distinctive noise." item = /obj/item/melee/transforming/energy/sword/saber cost = 8 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/shield name = "Energy Shield" @@ -145,7 +145,7 @@ item = /obj/item/shield/energy cost = 16 surplus = 20 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/rapier name = "Rapier" @@ -154,7 +154,7 @@ However, due to the size of the blade and obvious nature of the sheath, the weapon stands out as being obviously nefarious." item = /obj/item/storage/belt/sabre/rapier cost = 8 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/flamethrower name = "Flamethrower" @@ -163,7 +163,7 @@ item = /obj/item/flamethrower/full/tank cost = 4 surplus = 40 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/flechettegun name = "Flechette Launcher" @@ -173,7 +173,7 @@ item = /obj/item/gun/ballistic/automatic/flechette cost = 12 surplus = 30 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/rapid name = "Bands of the North Star" @@ -182,7 +182,7 @@ Combines with all martial arts, but the user will be unable to bring themselves to use guns, nor remove the armbands." item = /obj/item/clothing/gloves/fingerless/pugilist/rapid cost = 30 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/guardian name = "Holoparasites" @@ -194,7 +194,7 @@ refundable = TRUE cant_discount = TRUE surplus = 0 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) player_minimum = 25 restricted = TRUE refund_path = /obj/item/guardiancreator/tech/choose/traitor @@ -208,7 +208,7 @@ refundable = TRUE surplus = 50 refund_path = /obj/item/guardiancreator/tech/choose/nukie - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/machinegun name = "L6 Squad Automatic Weapon" @@ -217,7 +217,7 @@ item = /obj/item/gun/ballistic/automatic/l6_saw cost = 18 surplus = 0 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/carbine name = "M-90gl Carbine" @@ -226,7 +226,7 @@ item = /obj/item/gun/ballistic/automatic/m90 cost = 18 surplus = 50 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/maulergauntlets name = "Mauler Gauntlets" @@ -245,7 +245,6 @@ deal extra damage and hit targets further. Use a screwdriver to take out any attached tanks." item = /obj/item/melee/powerfist cost = 8 - exclude_modes = list(/datum/game_mode/traitor/internal_affairs) /datum/uplink_item/dangerous/sniper name = "Sniper Rifle" @@ -253,14 +252,14 @@ item = /obj/item/gun/ballistic/automatic/sniper_rifle/syndicate cost = 16 surplus = 25 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/bolt_action name = "Surplus Rifle" desc = "A horribly outdated bolt action weapon. You've got to be desperate to use this." item = /obj/item/gun/ballistic/shotgun/boltaction cost = 2 - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/dangerous/foamsmg name = "Toy Submachine Gun" @@ -268,7 +267,7 @@ item = /obj/item/gun/ballistic/automatic/c20r/toy cost = 5 surplus = 0 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/foammachinegun name = "Toy Machine Gun" @@ -277,7 +276,7 @@ item = /obj/item/gun/ballistic/automatic/l6_saw/toy cost = 10 surplus = 0 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/dangerous/foampistol name = "Toy Pistol with Riot Darts" @@ -293,4 +292,4 @@ Allows you to cut from a far distance!" item = /obj/item/gun/magic/staff/motivation cost = 10 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS diff --git a/code/modules/uplink/uplink_items/uplink_devices.dm b/code/modules/uplink/uplink_items/uplink_devices.dm index d7cd759c36..104773ee1b 100644 --- a/code/modules/uplink/uplink_items/uplink_devices.dm +++ b/code/modules/uplink/uplink_items/uplink_devices.dm @@ -46,7 +46,7 @@ item = /obj/item/assault_pod cost = 30 surplus = 0 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS restricted = TRUE /datum/uplink_item/device_tools/binary @@ -66,7 +66,7 @@ 'Advanced Magboots' slow you down in simulated-gravity environments much like the standard issue variety." item = /obj/item/clothing/shoes/magboots/syndie cost = 2 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/device_tools/compressionkit name = "Bluespace Compression Kit" @@ -98,7 +98,7 @@ desc = "A robust seven-slot set of webbing that is capable of holding all manner of tactical equipment." item = /obj/item/storage/belt/military cost = 1 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/device_tools/ammo_pouch name = "Ammo Pouch" @@ -136,7 +136,7 @@ desc = "A cheap bottle of one use syndicate brand super glue. \ Use on any item to make it undroppable. \ Be careful not to glue an item you're already holding!" - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) item = /obj/item/syndie_glue cost = 2 @@ -165,7 +165,7 @@ operatives in the fight, even while under fire. Don't cross the streams!" item = /obj/item/gun/medbeam cost = 15 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/device_tools/nutcracker name = "Nutcracker" @@ -223,7 +223,7 @@ and other supplies helpful for a field medic." item = /obj/item/storage/firstaid/tactical/nukeop cost = 4 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/device_tools/surgerybag name = "Syndicate Surgery Duffel Bag" @@ -271,7 +271,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 = 2 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS restricted = TRUE /* for now diff --git a/code/modules/uplink/uplink_items/uplink_explosives.dm b/code/modules/uplink/uplink_items/uplink_explosives.dm index f44966fb3b..11b892a208 100644 --- a/code/modules/uplink/uplink_items/uplink_explosives.dm +++ b/code/modules/uplink/uplink_items/uplink_explosives.dm @@ -15,7 +15,7 @@ item = /obj/item/grenade/chem_grenade/bioterrorfoam cost = 5 surplus = 35 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/explosives/bombanana name = "Bombanana" @@ -24,7 +24,7 @@ item = /obj/item/reagent_containers/food/snacks/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 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/explosives/buzzkill name = "Buzzkill Grenade Box" @@ -33,7 +33,7 @@ item = /obj/item/storage/box/syndie_kit/bee_grenades cost = 15 surplus = 35 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/explosives/c4 name = "Composition C-4" @@ -58,7 +58,6 @@ item = /obj/item/storage/backpack/duffelbag/syndie/x4 cost = 4 // cant_discount = TRUE - exclude_modes = list(/datum/game_mode/traitor/internal_affairs) /datum/uplink_item/explosives/clown_bomb_clownops name = "Clown Bomb" @@ -70,7 +69,7 @@ item = /obj/item/sbeacondrop/clownbomb cost = 15 surplus = 0 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/explosives/detomatix name = "Detomatix PDA Cartridge" @@ -97,14 +96,14 @@ item = /obj/item/storage/box/syndie_kit/tuberculosisgrenade cost = 8 surplus = 35 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS restricted = TRUE /datum/uplink_item/explosives/grenadier name = "Grenadier's belt" desc = "A belt containing 26 lethally dangerous and destructive grenades. Comes with an extra multitool and screwdriver." item = /obj/item/storage/belt/grenade/full - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS cost = 22 surplus = 0 @@ -125,7 +124,6 @@ be defused, and some crew may attempt to do so." item = /obj/item/sbeacondrop/bomb cost = 11 - exclude_modes = list(/datum/game_mode/traitor/internal_affairs) /datum/uplink_item/explosives/syndicate_detonator name = "Syndicate Detonator" @@ -135,7 +133,7 @@ the blast radius before using the detonator." item = /obj/item/syndicatedetonator cost = 3 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/explosives/syndicate_minibomb name = "Syndicate Minibomb" @@ -143,7 +141,7 @@ in addition to dealing high amounts of damage to nearby personnel." item = /obj/item/grenade/syndieminibomb cost = 6 - exclude_modes = list(/datum/game_mode/nuclear/clown_ops, /datum/game_mode/traitor/internal_affairs) + purchasable_from = ~UPLINK_CLOWN_OPS /datum/uplink_item/explosives/tearstache name = "Teachstache Grenade" @@ -152,7 +150,7 @@ item = /obj/item/grenade/chem_grenade/teargas/moustache cost = 3 surplus = 0 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/explosives/viscerators name = "Viscerator Delivery Grenade" @@ -161,4 +159,4 @@ item = /obj/item/grenade/spawnergrenade/manhacks cost = 5 surplus = 35 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS diff --git a/code/modules/uplink/uplink_items/uplink_implants.dm b/code/modules/uplink/uplink_items/uplink_implants.dm index 19dab96ccb..e3fe151143 100644 --- a/code/modules/uplink/uplink_items/uplink_implants.dm +++ b/code/modules/uplink/uplink_items/uplink_implants.dm @@ -20,7 +20,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/anti_stun cost = 12 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/implants/deathrattle name = "Box of Deathrattle Implants" @@ -31,7 +31,7 @@ item = /obj/item/storage/box/syndie_kit/imp_deathrattle cost = 4 surplus = 0 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/implants/freedom name = "Freedom Implant" @@ -45,7 +45,7 @@ desc = "An implant injected into the body and later activated at the user's will. Allows the user to teleport to where they were 10 seconds ago. Has a 10 second cooldown." item = /obj/item/storage/box/syndie_kit/imp_warp cost = 6 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/implants/hijack name = "Hijack Implant" @@ -70,7 +70,7 @@ This will permanently destroy your body, however." item = /obj/item/storage/box/syndie_kit/imp_microbomb cost = 2 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/implants/macrobomb name = "Macrobomb Implant" @@ -78,7 +78,7 @@ Upon death, releases a massive explosion that will wipe out everything nearby." item = /obj/item/storage/box/syndie_kit/imp_macrobomb cost = 20 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS restricted = TRUE /datum/uplink_item/implants/reviver @@ -86,7 +86,7 @@ desc = "This implant will attempt to revive and heal you if you lose consciousness. Comes with an autosurgeon." item = /obj/item/autosurgeon/reviver cost = 8 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/implants/stealthimplant name = "Stealth Implant" @@ -107,7 +107,7 @@ desc = "These cybernetic eyes will give you thermal vision. Comes with a free autosurgeon." item = /obj/item/autosurgeon/thermal_eyes cost = 8 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS /datum/uplink_item/implants/uplink name = "Uplink Implant" @@ -125,4 +125,4 @@ item = /obj/item/autosurgeon/xray_eyes cost = 10 surplus = 0 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS diff --git a/code/modules/uplink/uplink_items/uplink_roles.dm b/code/modules/uplink/uplink_items/uplink_roles.dm index 72e0111c41..d741013ad2 100644 --- a/code/modules/uplink/uplink_items/uplink_roles.dm +++ b/code/modules/uplink/uplink_items/uplink_roles.dm @@ -30,7 +30,6 @@ item = /obj/item/gun/blastcannon cost = 14 //High cost because of the potential for extreme damage in the hands of a skilled gas masked scientist. restricted_roles = list("Research Director", "Scientist") - exclude_modes = list(/datum/game_mode/traitor/internal_affairs) /datum/uplink_item/role_restricted/alientech name = "Alien Research Disk" @@ -99,7 +98,6 @@ player_minimum = 20 refundable = TRUE restricted_roles = list("Chaplain") - exclude_modes = list(/datum/game_mode/traitor/internal_affairs) /datum/uplink_item/role_restricted/arcane_tome name = "Arcane Tome" @@ -109,7 +107,6 @@ player_minimum = 20 refundable = TRUE restricted_roles = list("Chaplain") - exclude_modes = list(/datum/game_mode/traitor/internal_affairs) /datum/uplink_item/role_restricted/explosive_hot_potato name = "Exploding Hot Potato" diff --git a/code/modules/uplink/uplink_items/uplink_stealth.dm b/code/modules/uplink/uplink_items/uplink_stealth.dm index f401514542..8b79b15c80 100644 --- a/code/modules/uplink/uplink_items/uplink_stealth.dm +++ b/code/modules/uplink/uplink_items/uplink_stealth.dm @@ -19,14 +19,14 @@ to learn the abilities of krav maga to the wearer." item = /obj/item/clothing/gloves/krav_maga/combatglovesplus cost = 5 - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS surplus = 0 /datum/uplink_item/stealthy_weapons/cqc name = "CQC Manual" desc = "A manual that teaches a single user tactical Close-Quarters Combat before self-destructing." item = /obj/item/book/granter/martial/cqc - include_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS cost = 13 surplus = 0 @@ -61,7 +61,7 @@ name = "Antique Derringer" desc = "An easy to conceal, yet extremely deadly handgun, capable of firing .45-70 Govt rounds. Comes in a unique pack of cigarettes with additional munitions." item = /obj/item/storage/fancy/cigarettes/derringer/midworld - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS cost = 10 surplus = 2 @@ -79,7 +79,7 @@ cost = 17 player_minimum = 20 surplus = 0 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_weapons/martialartstwo name = "Rising Bass Scroll" @@ -89,7 +89,7 @@ cost = 18 player_minimum = 20 surplus = 0 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_weapons/martialartsthree name = "Krav Maga Scroll" @@ -99,7 +99,6 @@ cost = 16 player_minimum = 25 surplus = 0 - include_modes = list(/datum/game_mode/traitor/internal_affairs) /datum/uplink_item/stealthy_weapons/crossbow name = "Miniature Energy Crossbow" @@ -112,7 +111,7 @@ item = /obj/item/gun/energy/kinetic_accelerator/crossbow cost = 12 surplus = 50 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_weapons/traitor_chem_bottle name = "Poison Kit" @@ -130,7 +129,7 @@ cost = 25 player_minimum = 25 cant_discount = TRUE - exclude_modes = list(/datum/game_mode/nuclear) + purchasable_from = ~UPLINK_NUKE_OPS /datum/uplink_item/stealthy_weapons/sleepy_pen name = "Sleepy Pen" @@ -140,14 +139,14 @@ falls asleep, they will be able to move and act." item = /obj/item/pen/sleepy cost = 4 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_weapons/taeclowndo_shoes name = "Tae-clown-do Shoes" desc = "A pair of shoes for the most elite agents of the honkmotherland. They grant the mastery of taeclowndo with some honk-fu moves as long as they're worn." cost = 12 item = /obj/item/clothing/shoes/clown_shoes/taeclowndo - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/stealthy_weapons/suppressor name = "Suppressor" diff --git a/code/modules/uplink/uplink_items/uplink_stealthdevices.dm b/code/modules/uplink/uplink_items/uplink_stealthdevices.dm index 28d02cf79b..bd699e1bde 100644 --- a/code/modules/uplink/uplink_items/uplink_stealthdevices.dm +++ b/code/modules/uplink/uplink_items/uplink_stealthdevices.dm @@ -32,7 +32,7 @@ Due to budget cuts, the shoes don't provide protection against slipping." item = /obj/item/storage/box/syndie_kit/chameleon cost = 2 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_tools/chameleon_proj name = "Chameleon Projector" @@ -56,7 +56,7 @@ item = /obj/item/clothing/shoes/clown_shoes/banana_shoes/combat cost = 6 surplus = 0 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/stealthy_tools/emplight name = "EMP Flashlight" @@ -74,7 +74,7 @@ cost = 1 surplus = 0 restricted = TRUE - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_tools/failsafe/spawn_item(spawn_path, mob/user, datum/component/uplink/U) if(!U) @@ -92,7 +92,7 @@ item = /obj/item/reagent_containers/syringe/mulligan cost = 3 surplus = 30 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_tools/syndigaloshes name = "No-Slip Chameleon Shoes" @@ -100,13 +100,12 @@ They do not work on heavily lubricated surfaces." item = /obj/item/clothing/shoes/chameleon/noslip cost = 2 - exclude_modes = list(/datum/game_mode/nuclear, /datum/game_mode/nuclear/clown_ops) + purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS) /datum/uplink_item/stealthy_tools/syndigaloshes/nuke item = /obj/item/clothing/shoes/chameleon/noslip cost = 4 - exclude_modes = list() - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS /datum/uplink_item/stealthy_tools/jammer name = "Radio Jammer" diff --git a/code/modules/uplink/uplink_items/uplink_support.dm b/code/modules/uplink/uplink_items/uplink_support.dm index fe1415adc8..efdfe1b06a 100644 --- a/code/modules/uplink/uplink_items/uplink_support.dm +++ b/code/modules/uplink/uplink_items/uplink_support.dm @@ -12,7 +12,7 @@ desc = "Call in an additional clown to share the fun, equipped with full starting gear, but no telecrystals." item = /obj/item/antag_spawner/nuke_ops/clown cost = 20 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS restricted = TRUE /datum/uplink_item/support/reinforcement @@ -22,7 +22,7 @@ item = /obj/item/antag_spawner/nuke_ops cost = 25 refundable = TRUE - include_modes = list(/datum/game_mode/nuclear) + purchasable_from = UPLINK_NUKE_OPS restricted = TRUE /datum/uplink_item/support/reinforcement/assault_borg @@ -66,7 +66,7 @@ desc = "A clown combat mech equipped with bombanana peel and tearstache grenade launchers, as well as the ubiquitous HoNkER BlAsT 5000." item = /obj/mecha/combat/honker/dark/loaded cost = 80 - include_modes = list(/datum/game_mode/nuclear/clown_ops) + purchasable_from = UPLINK_CLOWN_OPS /datum/uplink_item/support/mauler name = "Mauler Exosuit" diff --git a/code/modules/uplink/uplink_purchase_log.dm b/code/modules/uplink/uplink_purchase_log.dm index 293191b170..c9ac13fa13 100644 --- a/code/modules/uplink/uplink_purchase_log.dm +++ b/code/modules/uplink/uplink_purchase_log.dm @@ -1,8 +1,8 @@ -GLOBAL_LIST(uplink_purchase_logs_by_key) //assoc key = /datum/uplink_purchase_log +GLOBAL_LIST(uplink_purchase_logs_by_key) //assoc key = /datum/uplink_purchase_log /datum/uplink_purchase_log var/owner - var/list/purchase_log //assoc path-of-item = /datum/uplink_purchase_entry + var/list/purchase_log //assoc path-of-item = /datum/uplink_purchase_entry var/total_spent = 0 /datum/uplink_purchase_log/New(_owner, datum/component/uplink/_parent) diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm index 75cf6594e6..16caef5b4e 100644 --- a/code/modules/vending/_vending.dm +++ b/code/modules/vending/_vending.dm @@ -12,16 +12,14 @@ products = list() contraband = list() premium = list() - -IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY CANISTER CHARGES in vending_items.dm */ #define MAX_VENDING_INPUT_AMOUNT 30 /** - * # vending record datum - * - * A datum that represents a product that is vendable - */ + * # vending record datum + * + * A datum that represents a product that is vendable + */ /datum/data/vending_product name = "generic" ///Typepath of the product that is created when this record "sells" @@ -34,12 +32,16 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C var/custom_price ///Does the item have a custom premium price override var/custom_premium_price + ///Whether spessmen with an ID with an age below AGE_MINOR (20 by default) can buy this item + var/age_restricted = FALSE // @unimplimented + ///Whether the product can be recolored by the GAGS system + var/colorable // @unimplimented /** - * # vending machines - * - * Captalism in the year 2525, everything in a vending machine, even love - */ + * # vending machines + * + * Captalism in the year 2525, everything in a vending machine, even love + */ /obj/machinery/vending name = "\improper Vendomat" desc = "A generic vending machine." @@ -55,36 +57,43 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C armor = list("melee" = 20, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 50, "acid" = 70) circuit = /obj/item/circuitboard/machine/vendor payment_department = ACCOUNT_SRV - light_power = 0.3 + light_power = 0.5 light_range = MINIMUM_USEFUL_LIGHT_RANGE /// Is the machine active (No sales pitches if off)! - var/active = TRUE + var/active = 1 ///Are we ready to vend?? Is it time?? var/vend_ready = TRUE ///Next world time to send a purchase message var/purchase_message_cooldown - ///Last mob to shop with us + ///The ref of the last mob to shop with us + var/last_shopper + var/tilted = FALSE + var/tiltable = TRUE + var/squish_damage = 75 + var/forcecrit = 0 + var/num_shards = 7 + var/list/pinned_mobs = list() /** * List of products this machine sells * - * form should be list(/type/path = amount, /type/path2 = amount2) + * form should be list(/type/path = amount, /type/path2 = amount2) */ - var/list/products = list() + var/list/products = list() /** * List of products this machine sells when you hack it * - * form should be list(/type/path = amount, /type/path2 = amount2) + * form should be list(/type/path = amount, /type/path2 = amount2) */ - var/list/contraband = list() + var/list/contraband = list() /** * List of premium products this machine sells * - * form should be list(/type/path, /type/path2) as there is only ever one in stock + * form should be list(/type/path, /type/path2) as there is only ever one in stock */ - var/list/premium = list() + var/list/premium = list() ///String of slogans separated by semicolons, optional var/product_slogans = "" @@ -97,20 +106,12 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C var/list/slogan_list = list() ///Small ad messages in the vending screen - random chance of popping up whenever you open it var/list/small_ads = list() - var/dish_quants = list() //used by the snack machine's custom compartment to count dishes. ///Message sent post vend (Thank you for shopping!) var/vend_reply ///Last world tick we sent a vent reply - var/last_reply + var/last_reply = 0 ///Last world tick we sent a slogan message out - var/last_slogan - var/last_shopper - var/tilted = FALSE - var/tiltable = TRUE - var/squish_damage = 75 - var/forcecrit = 0 - var/num_shards = 7 - var/list/pinned_mobs = list() + var/last_slogan = 0 ///How many ticks until we can send another var/slogan_delay = 6000 ///Icon when vending an item to the user @@ -120,32 +121,37 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C ///World ticks the machine is electified for var/seconds_electrified = MACHINE_NOT_ELECTRIFIED ///When this is TRUE, we fire items at customers! We're broken! - var/shoot_inventory - ///How likely this is to happen (prob 100) - var/shoot_inventory_chance = 2 + var/shoot_inventory = 0 + ///How likely this is to happen (prob 100) per second + var/shoot_inventory_chance = 1 //Stop spouting those godawful pitches! - var/shut_up + var/shut_up = 0 ///can we access the hidden inventory? - var/extended_inventory + var/extended_inventory = 0 ///Are we checking the users ID - var/scan_id = TRUE + var/scan_id = 1 + ///Coins that we accept? var/obj/item/coin/coin + ///Bills we accept? + var/obj/item/stack/spacecash/bill ///Default price of items if not overridden var/default_price = 25 ///Default price of premium items if not overridden var/extra_price = 50 + ///Whether our age check is currently functional + var/age_restrictions = TRUE ///cost multiplier per department or access var/list/cost_multiplier_per_dept = list() - /** + /** * Is this item on station or not * * if it doesn't originate from off-station during mapload, everything is free */ var/onstation = TRUE //if it doesn't originate from off-station during mapload, everything is free - ///A variable to change on a per instance basis on the map that allows the instance to force cost and ID requirements + ///A variable to change on a per instance basis on the map that allows the instance to force cost and ID requirements var/onstation_override = FALSE //change this on the object on the map to override the onstation check. DO NOT APPLY THIS GLOBALLY. - ///ID's that can load this vending machine wtih refills + ///ID's that can load this vending machine wtih refills var/list/canload_access_list @@ -162,15 +168,19 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C ///Name of lighting mask for the vending machine var/light_mask + /// used for narcing on underages + var/obj/item/radio/Radio + + /** - * Initialize the vending machine - * - * Builds the vending machine inventory, sets up slogans and other such misc work - * - * This also sets the onstation var to: - * * FALSE - if the machine was maploaded on a zlevel that doesn't pass the is_station_level check - * * TRUE - all other cases - */ + * Initialize the vending machine + * + * Builds the vending machine inventory, sets up slogans and other such misc work + * + * This also sets the onstation var to: + * * FALSE - if the machine was maploaded on a zlevel that doesn't pass the is_station_level check + * * TRUE - all other cases + */ /obj/machinery/vending/Initialize(mapload) var/build_inv = FALSE if(!refill_canister) @@ -189,18 +199,25 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C // so if slogantime is 10 minutes, it will say it at somewhere between 10 and 20 minutes after the machine is crated. last_slogan = world.time + rand(0, slogan_delay) power_change() + if(onstation_override) //overrides the checks if true. onstation = TRUE return - if(mapload && !is_station_level(z)) //check if it was initially created off station during mapload. - onstation = FALSE - if(circuit) - circuit.onstation = onstation //sync up the circuit so the pricing schema is carried over if it's reconstructed. + if(mapload) //check if it was initially created off station during mapload. + if(!is_station_level(z)) + onstation = FALSE + if(circuit) + circuit.onstation = onstation //sync up the circuit so the pricing schema is carried over if it's reconstructed. else if(circuit && (circuit.onstation != onstation)) //check if they're not the same to minimize the amount of edited values. onstation = circuit.onstation //if it was constructed outside mapload, sync the vendor up with the circuit's var so you can't bypass price requirements by moving / reconstructing it off station. + Radio = new /obj/item/radio(src) + Radio.listening = 0 /obj/machinery/vending/Destroy() QDEL_NULL(wires) + QDEL_NULL(coin) + QDEL_NULL(bill) + QDEL_NULL(Radio) return ..() /obj/machinery/vending/can_speak() @@ -227,16 +244,20 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C else ..() +/obj/machinery/vending/update_appearance(updates=ALL) + . = ..() + if(stat & BROKEN) + set_light(0) + return + set_light(powered() ? MINIMUM_USEFUL_LIGHT_RANGE : 0) + + /obj/machinery/vending/update_icon_state() if(stat & BROKEN) icon_state = "[initial(icon_state)]-broken" - set_light(0) - else if(powered()) - icon_state = initial(icon_state) - set_light(1.4) - else - icon_state = "[initial(icon_state)]-off" - set_light(0) + return ..() + icon_state = "[initial(icon_state)][powered() ? null : "-off"]" + return ..() /obj/machinery/vending/update_overlays() @@ -276,14 +297,14 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C GLOBAL_LIST_EMPTY(vending_products) /** - * Build the inventory of the vending machine from it's product and record lists - * - * This builds up a full set of /datum/data/vending_products from the product list of the vending machine type - * Arguments: - * * productlist - the list of products that need to be converted - * * recordlist - the list containing /datum/data/vending_product datums - * * startempty - should we set vending_product record amount from the product list (so it's prefilled at roundstart) - */ + * Build the inventory of the vending machine from it's product and record lists + * + * This builds up a full set of /datum/data/vending_products from the product list of the vending machine type + * Arguments: + * * productlist - the list of products that need to be converted + * * recordlist - the list containing /datum/data/vending_product datums + * * startempty - should we set vending_product record amount from the product list (so it's prefilled at roundstart) + */ /obj/machinery/vending/proc/build_inventory(list/productlist, list/recordlist, start_empty = FALSE) for(var/typepath in productlist) var/amount = productlist[typepath] @@ -298,17 +319,21 @@ GLOBAL_LIST_EMPTY(vending_products) if(!start_empty) R.amount = amount R.max_amount = amount + ///Prices of vending machines are all increased uniformly. R.custom_price = initial(temp.custom_price) R.custom_premium_price = initial(temp.custom_premium_price) + // R.age_restricted = initial(temp.age_restricted) + // R.colorable = !!(initial(temp.greyscale_config) && initial(temp.greyscale_colors) && (initial(temp.flags_1) & IS_PLAYER_COLORABLE_1)) recordlist += R + /** - * Refill a vending machine from a refill canister - * - * This takes the products from the refill canister and then fills the products,contraband and premium product categories - * - * Arguments: - * * canister - the vending canister we are refilling from - */ + * Refill a vending machine from a refill canister + * + * This takes the products from the refill canister and then fills the products,contraband and premium product categories + * + * Arguments: + * * canister - the vending canister we are refilling from + */ /obj/machinery/vending/proc/restock(obj/item/vending_refill/canister) if (!canister.products) canister.products = products.Copy() @@ -321,12 +346,12 @@ GLOBAL_LIST_EMPTY(vending_products) . += refill_inventory(canister.contraband, hidden_records) . += refill_inventory(canister.premium, coin_records) /** - * Refill our inventory from the passed in product list into the record list - * - * Arguments: - * * productlist - list of types -> amount - * * recordlist - existing record datums - */ + * Refill our inventory from the passed in product list into the record list + * + * Arguments: + * * productlist - list of types -> amount + * * recordlist - existing record datums + */ /obj/machinery/vending/proc/refill_inventory(list/productlist, list/recordlist) . = 0 for(var/R in recordlist) @@ -337,10 +362,10 @@ GLOBAL_LIST_EMPTY(vending_products) record.amount += diff . += diff /** - * Set up a refill canister that matches this machines products - * - * This is used when the machine is deconstructed, so the items aren't "lost" - */ + * Set up a refill canister that matches this machines products + * + * This is used when the machine is deconstructed, so the items aren't "lost" + */ /obj/machinery/vending/proc/update_canister() if (!component_parts) return @@ -354,8 +379,8 @@ GLOBAL_LIST_EMPTY(vending_products) R.premium = unbuild_inventory(coin_records) /** - * Given a record list, go through and and return a list of type -> amount - */ + * Given a record list, go through and and return a list of type -> amount + */ /obj/machinery/vending/proc/unbuild_inventory(list/recordlist) . = list() for(var/R in recordlist) @@ -385,32 +410,32 @@ GLOBAL_LIST_EMPTY(vending_products) add_overlay("[initial(icon_state)]-panel") updateUsrDialog() else - to_chat(user, "You must first secure [src].") + to_chat(user, span_warning("You must first secure [src].")) return TRUE -/obj/machinery/vending/attackby(obj/item/I, mob/user, params) +/obj/machinery/vending/attackby(obj/item/I, mob/living/user, params) if(panel_open && is_wire_tool(I)) wires.interact(user) return + if(refill_canister && istype(I, refill_canister)) if (!panel_open) - to_chat(user, "You should probably unscrew the service panel first!") + to_chat(user, span_warning("You should probably unscrew the service panel first!")) else if (stat & (BROKEN|NOPOWER)) - to_chat(user, "[src] does not respond.") + to_chat(user, span_notice("[src] does not respond.")) else //if the panel is open we attempt to refill the machine var/obj/item/vending_refill/canister = I if(canister.get_part_rating() == 0) - to_chat(user, "[canister] is empty!") + to_chat(user, span_warning("[canister] is empty!")) else // instantiate canister if needed var/transferred = restock(canister) if(transferred) - to_chat(user, "You loaded [transferred] items in [src].") + to_chat(user, span_notice("You loaded [transferred] items in [src].")) else - to_chat(user, "There's nothing to restock!") + to_chat(user, span_warning("There's nothing to restock!")) return - if(compartmentLoadAccessCheck(user) && user.a_intent != INTENT_HARM) if(canLoadItem(I)) loadingAttempt(I,user) @@ -422,7 +447,7 @@ GLOBAL_LIST_EMPTY(vending_products) var/denied_items = 0 for(var/obj/item/the_item in T.contents) if(contents.len >= MAX_VENDING_INPUT_AMOUNT) // no more than 30 item can fit inside, legacy from snack vending although not sure why it exists - to_chat(user, "[src]'s compartment is full.") + to_chat(user, span_warning("[src]'s compartment is full.")) break if(canLoadItem(the_item) && loadingAttempt(the_item,user)) SEND_SIGNAL(T, COMSIG_TRY_STORAGE_TAKE, the_item, src, TRUE) @@ -430,9 +455,9 @@ GLOBAL_LIST_EMPTY(vending_products) else denied_items++ if(denied_items) - to_chat(user, "[src] refuses some items!") + to_chat(user, span_warning("[src] refuses some items!")) if(loaded) - to_chat(user, "You insert [loaded] dishes into [src]'s compartment.") + to_chat(user, span_notice("You insert [loaded] dishes into [src]'s compartment.")) updateUsrDialog() else . = ..() @@ -450,7 +475,7 @@ GLOBAL_LIST_EMPTY(vending_products) tilt(user, crit=TRUE) /obj/machinery/vending/proc/freebie(mob/fatty, freebies) - visible_message("[src] yields [freebies > 1 ? "several free goodies" : "a free goody"]!") + visible_message(span_notice("[src] yields [freebies > 1 ? "several free goodies" : "a free goody"]!")) for(var/i in 1 to freebies) playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3) @@ -466,8 +491,9 @@ GLOBAL_LIST_EMPTY(vending_products) new dump_path(get_turf(src)) break -/obj/machinery/vending/proc/tilt(mob/fatty, crit=FALSE) - visible_message("[src] tips over!") +///Tilts ontop of the atom supplied, if crit is true some extra shit can happen. Returns TRUE if it dealt damage to something. +/obj/machinery/vending/proc/tilt(atom/fatty, crit=FALSE) + visible_message(span_danger("[src] tips over!")) tilted = TRUE layer = ABOVE_MOB_LAYER @@ -478,16 +504,21 @@ GLOBAL_LIST_EMPTY(vending_products) if(forcecrit) crit_case = forcecrit + . = FALSE + if(in_range(fatty, src)) for(var/mob/living/L in get_turf(fatty)) + var/was_alive = (L.stat != DEAD) var/mob/living/carbon/C = L + // SEND_SIGNAL(L, COMSIG_ON_VENDOR_CRUSH) + if(istype(C)) var/crit_rebate = 0 // lessen the normal damage we deal for some of the crits - if(crit_case < 5) // the head asplode case has its own description - C.visible_message("[C] is crushed by [src]!", \ - "You are crushed by [src]!") + if(crit_case < 5) // the body/head asplode case has its own description + C.visible_message(span_danger("[C] is crushed by [src]!"), \ + span_userdanger("You are crushed by [src]!")) switch(crit_case) // only carbons can have the fun crits if(1) // shatter their legs and bleed 'em @@ -500,13 +531,13 @@ GLOBAL_LIST_EMPTY(vending_products) if(r) r.receive_damage(brute=200, updating_health=TRUE) if(l || r) - C.visible_message("[C]'s legs shatter with a sickening crunch!", \ - "Your legs shatter with a sickening crunch!") + C.visible_message(span_danger("[C]'s legs shatter with a sickening crunch!"), \ + span_userdanger("Your legs shatter with a sickening crunch!")) if(2) // pin them beneath the machine until someone untilts it forceMove(get_turf(C)) buckle_mob(C, force=TRUE) - C.visible_message("[C] is pinned underneath [src]!", \ - "You are pinned down by [src]!") + C.visible_message(span_danger("[C] is pinned underneath [src]!"), \ + span_userdanger("You are pinned down by [src]!")) if(3) // glass candy crit_rebate = 50 for(var/i = 0, i < num_shards, i++) @@ -518,7 +549,7 @@ GLOBAL_LIST_EMPTY(vending_products) shard.updateEmbedding() if(4) // paralyze this binch // the new paraplegic gets like 4 lines of losing their legs so skip them - visible_message("[C]'s spinal cord is obliterated with a sickening crunch!", ignored_mobs = list(C)) + visible_message(span_danger("[C]'s spinal cord is obliterated with a sickening crunch!"), ignored_mobs = list(C)) C.gain_trauma(/datum/brain_trauma/severe/paralysis/spinesnapped) if(5) // limb squish! for(var/i in C.bodyparts) @@ -528,13 +559,13 @@ GLOBAL_LIST_EMPTY(vending_products) squish_part.force_wound_upwards(type_wound) else squish_part.receive_damage(brute=30) - C.visible_message("[C]'s body is maimed underneath the mass of [src]!", \ - "Your body is maimed underneath the mass of [src]!") + C.visible_message(span_danger("[C]'s body is maimed underneath the mass of [src]!"), \ + span_userdanger("Your body is maimed underneath the mass of [src]!")) if(6) // skull squish! var/obj/item/bodypart/head/O = C.get_bodypart(BODY_ZONE_HEAD) if(O) - C.visible_message("[O] explodes in a shower of gore beneath [src]!", \ - "Oh f-") + C.visible_message(span_danger("[O] explodes in a shower of gore beneath [src]!"), \ + span_userdanger("Oh f-")) O.dismember() O.drop_organs() qdel(O) @@ -545,19 +576,19 @@ GLOBAL_LIST_EMPTY(vending_products) else C.take_bodypart_damage((squish_damage - crit_rebate)*0.5, wound_bonus = 5) // otherwise, deal it to 2 random limbs (or the same one) which will likely shatter something C.take_bodypart_damage((squish_damage - crit_rebate)*0.5, wound_bonus = 5) - C.AddElement(/datum/element/squish, 18 SECONDS) + C.AddElement(/datum/element/squish, 80 SECONDS) else - L.visible_message("[L] is crushed by [src]!", \ - "You are crushed by [src]!") + L.visible_message(span_danger("[L] is crushed by [src]!"), \ + span_userdanger("You are crushed by [src]!")) L.apply_damage(squish_damage, forced=TRUE) if(crit_case) L.apply_damage(squish_damage, forced=TRUE) - - if(L.stat == DEAD && L.client) + if(was_alive && L.stat == DEAD && L.client) L.client.give_award(/datum/award/achievement/misc/vendor_squish, L) // good job losing a fight with an inanimate object idiot L.Paralyze(60) L.emote("scream") + . = TRUE playsound(L, 'sound/effects/blobattack.ogg', 40, TRUE) playsound(L, 'sound/effects/splat.ogg', 50, TRUE) @@ -566,14 +597,15 @@ GLOBAL_LIST_EMPTY(vending_products) transform = M if(get_turf(fatty) != get_turf(src)) - throw_at(get_turf(fatty), 1, 1, spin=FALSE) + throw_at(get_turf(fatty), 1, 1, spin=FALSE) //, quickstart=FALSE) /obj/machinery/vending/proc/untilt(mob/user) - user.visible_message("[user] rights [src].", \ - "You right [src].") + if(user) + user.visible_message(span_notice("[user] rights [src]."), \ + span_notice("You right [src].")) unbuckle_all_mobs(TRUE) - anchored = FALSE //so you can push it back into position + tilted = FALSE layer = initial(layer) @@ -589,21 +621,20 @@ GLOBAL_LIST_EMPTY(vending_products) vending_machine_input[format_text(I.name)]++ else vending_machine_input[format_text(I.name)] = 1 - to_chat(user, "You insert [I] into [src]'s input compartment.") + to_chat(user, span_notice("You insert [I] into [src]'s input compartment.")) loaded_items++ - /obj/machinery/vending/unbuckle_mob(mob/living/buckled_mob, force=FALSE) if(!force) return . = ..() /** - * Is the passed in user allowed to load this vending machines compartments - * - * Arguments: - * * user - mob that is doing the loading of the vending machine - */ + * Is the passed in user allowed to load this vending machines compartments + * + * Arguments: + * * user - mob that is doing the loading of the vending machine + */ /obj/machinery/vending/proc/compartmentLoadAccessCheck(mob/user) if(!canload_access_list) return TRUE @@ -621,7 +652,7 @@ GLOBAL_LIST_EMPTY(vending_products) if(do_you_have_access) return TRUE else - to_chat(user, "[src]'s input compartment blinks red: Access denied.") + to_chat(user, span_warning("[src]'s input compartment blinks red: Access denied.")) return FALSE /obj/machinery/vending/exchange_parts(mob/user, obj/item/storage/part_replacer/W) @@ -642,7 +673,7 @@ GLOBAL_LIST_EMPTY(vending_products) else display_parts(user) if(moved) - to_chat(user, "[moved] items restocked.") + to_chat(user, span_notice("[moved] items restocked.")) W.play_rped_sound() return TRUE @@ -651,22 +682,22 @@ GLOBAL_LIST_EMPTY(vending_products) . = ..() /obj/machinery/vending/emag_act(mob/user) - . = ..() if(obj_flags & EMAGGED) return obj_flags |= EMAGGED - to_chat(user, "You short out the product lock on [src].") - return TRUE + to_chat(user, span_notice("You short out the product lock on [src].")) /obj/machinery/vending/_try_interact(mob/user) if(seconds_electrified && !(stat & NOPOWER)) if(shock(user, 100)) return + if(tilted && !user.buckled && !isAI(user)) - to_chat(user, "You begin righting [src].") + to_chat(user, span_notice("You begin righting [src].")) if(do_after(user, 50, target=src)) untilt(user) return + return ..() /obj/machinery/vending/ui_assets(mob/user) @@ -736,7 +767,12 @@ GLOBAL_LIST_EMPTY(vending_products) .["user"]["department"] = "No Department" .["stock"] = list() for (var/datum/data/vending_product/R in product_records + coin_records + hidden_records) - .["stock"][R.name] = R.amount + var/list/product_data = list( + name = R.name, + amount = R.amount, + colorable = FALSE, // R.colorable, + ) + .["stock"][R.name] = product_data .["extended_inventory"] = extended_inventory /obj/machinery/vending/ui_act(action, params) @@ -745,92 +781,156 @@ GLOBAL_LIST_EMPTY(vending_products) return switch(action) if("vend") - . = TRUE - if(!vend_ready) - return - if(panel_open) - to_chat(usr, "The vending machine cannot dispense products while its service panel is open!") - return - vend_ready = FALSE //One thing at a time!! - var/datum/data/vending_product/R = locate(params["ref"]) - var/list/record_to_check = product_records + coin_records - if(extended_inventory) - record_to_check = product_records + coin_records + hidden_records - if(!R || !istype(R) || !R.product_path) - vend_ready = TRUE - return - var/price_to_use = default_price - if(R.custom_price) - price_to_use = R.custom_price - if(R in hidden_records) - if(!extended_inventory) - vend_ready = TRUE - return - else if (!(R in record_to_check)) - vend_ready = TRUE - message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!") - return - if (R.amount <= 0) - say("Sold out of [R.name].") - flick(icon_deny,src) - vend_ready = TRUE - return - if(onstation && ishuman(usr)) - var/mob/living/carbon/human/H = usr - var/obj/item/card/id/C = H.get_idcard(TRUE) + . = vend(params) + // if("select_colors") + // . = select_colors(params) - if(!C) - say("No card found.") - flick(icon_deny,src) - vend_ready = TRUE - return - else if (!C.registered_account) - say("No account found.") - flick(icon_deny,src) - vend_ready = TRUE - return - // else if(age_restrictions && R.age_restricted && (!C.registered_age || C.registered_age < AGE_MINOR)) - // say("You are not of legal age to purchase [R.name].") - // if(!(usr in GLOB.narcd_underages)) - // Radio.set_frequency(FREQ_SECURITY) - // Radio.talk_into(src, "SECURITY ALERT: Underaged crewmember [H] recorded attempting to purchase [R.name] in [get_area(src)]. Please watch for substance abuse.", FREQ_SECURITY) - // GLOB.narcd_underages += H - // flick(icon_deny,src) - // vend_ready = TRUE - // return - var/datum/bank_account/account = C.registered_account - if(account.account_job && account.account_job.paycheck_department == payment_department) - price_to_use = 0 - if(coin_records.Find(R) || hidden_records.Find(R)) - price_to_use = R.custom_premium_price ? R.custom_premium_price : extra_price - if(price_to_use && !account.adjust_money(-price_to_use)) - say("You do not possess the funds to purchase [R.name].") - flick(icon_deny,src) - vend_ready = TRUE - return - var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department) - if(D) - D.adjust_money(price_to_use) - SSblackbox.record_feedback("amount", "vending_spent", price_to_use) - //log_econ("[price_to_use] credits were inserted into [src] by [D.account_holder] to buy [R].") - if(last_shopper != usr || purchase_message_cooldown < world.time) - say("Thank you for shopping with [src]!") - purchase_message_cooldown = world.time + 5 SECONDS - last_shopper = usr - use_power(5) - if(icon_vend) //Show the vending animation if needed - flick(icon_vend,src) - playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3) - var/obj/item/vended = new R.product_path(get_turf(src)) - R.amount-- - if(usr.CanReach(src) && usr.put_in_hands(vended)) - to_chat(usr, "You take [R.name] out of the slot.") - else - to_chat(usr, "[capitalize(R.name)] falls onto the floor!") - SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]")) +/obj/machinery/vending/proc/can_vend(user, silent=FALSE) + . = FALSE + if(!vend_ready) + return + if(panel_open) + to_chat(user, span_warning("The vending machine cannot dispense products while its service panel is open!")) + return + return TRUE + +// /obj/machinery/vending/proc/select_colors(list/params) +// . = TRUE +// if(!can_vend(usr)) +// return +// var/datum/data/vending_product/product = locate(params["ref"]) +// var/atom/fake_atom = product.product_path + +// var/list/allowed_configs = list() +// var/config = initial(fake_atom.greyscale_config) +// if(!config) +// return +// allowed_configs += "[config]" +// if(ispath(fake_atom, /obj/item)) +// var/obj/item/item = fake_atom +// if(initial(item.greyscale_config_worn)) +// allowed_configs += "[initial(item.greyscale_config_worn)]" +// if(initial(item.greyscale_config_inhand_left)) +// allowed_configs += "[initial(item.greyscale_config_inhand_left)]" +// if(initial(item.greyscale_config_inhand_right)) +// allowed_configs += "[initial(item.greyscale_config_inhand_right)]" + +// var/datum/greyscale_modify_menu/menu = new( +// src, usr, allowed_configs, CALLBACK(src, .proc/vend_greyscale, params), +// starting_icon_state=initial(fake_atom.icon_state), +// starting_config=initial(fake_atom.greyscale_config), +// starting_colors=initial(fake_atom.greyscale_colors) +// ) +// menu.ui_interact(usr) + +// /obj/machinery/vending/proc/vend_greyscale(list/params, datum/greyscale_modify_menu/menu) +// if(usr != menu.user) +// return +// if(!menu.target.can_interact(usr)) +// return +// vend(params, menu.split_colors) + +/obj/machinery/vending/proc/vend(list/params, list/greyscale_colors) + . = TRUE + if(!can_vend(usr)) + return + vend_ready = FALSE //One thing at a time!! + var/datum/data/vending_product/R = locate(params["ref"]) + var/list/record_to_check = product_records + coin_records + if(extended_inventory) + record_to_check = product_records + coin_records + hidden_records + if(!R || !istype(R) || !R.product_path) + vend_ready = TRUE + return + var/price_to_use = default_price + if(R.custom_price) + price_to_use = R.custom_price + if(R in hidden_records) + if(!extended_inventory) vend_ready = TRUE + return + else if (!(R in record_to_check)) + vend_ready = TRUE + message_admins("Vending machine exploit attempted by [ADMIN_LOOKUPFLW(usr)]!") + return + if (R.amount <= 0) + say("Sold out of [R.name].") + flick(icon_deny,src) + vend_ready = TRUE + return + if(onstation) + var/obj/item/card/id/C + if(isliving(usr)) + var/mob/living/L = usr + C = L.get_idcard(TRUE) + if(!C) + say("No card found.") + flick(icon_deny,src) + vend_ready = TRUE + return + else if (!C.registered_account) + say("No account found.") + flick(icon_deny,src) + vend_ready = TRUE + return + else if(!C.registered_account.account_job) + say("Departmental accounts have been blacklisted from personal expenses due to embezzlement.") + flick(icon_deny, src) + vend_ready = TRUE + return + // else if(age_restrictions && R.age_restricted && (!C.registered_age || C.registered_age < AGE_MINOR)) + // say("You are not of legal age to purchase [R.name].") + // if(!(usr in GLOB.narcd_underages)) + // Radio.set_frequency(FREQ_SECURITY) + // Radio.talk_into(src, "SECURITY ALERT: Underaged crewmember [usr] recorded attempting to purchase [R.name] in [get_area(src)]. Please watch for substance abuse.", FREQ_SECURITY) + // GLOB.narcd_underages += usr + // flick(icon_deny,src) + // vend_ready = TRUE + // return + var/datum/bank_account/account = C.registered_account -/obj/machinery/vending/process() + var/discounts = FALSE + try // too lazy, and i do NOT want to use for() to check, as & is faster + discounts = !!(cost_multiplier_per_dept.len > 0 && (cost_multiplier_per_dept & account.account_job.access) > 0) + catch + // L + discounts = FALSE + + if(account.account_job && account.account_job.paycheck_department == payment_department || discounts) + price_to_use = 0 // it's free shut up + if(coin_records.Find(R) || hidden_records.Find(R)) + price_to_use = R.custom_premium_price ? R.custom_premium_price : extra_price + if(price_to_use && !account.adjust_money(-price_to_use)) + say("You do not possess the funds to purchase [R.name].") + flick(icon_deny,src) + vend_ready = TRUE + return + var/datum/bank_account/D = SSeconomy.get_dep_account(payment_department) + if(D) + D.adjust_money(price_to_use) + SSblackbox.record_feedback("amount", "vending_spent", price_to_use) + // log_econ("[price_to_use] credits were inserted into [src] by [D.account_holder] to buy [R].") + if(last_shopper != REF(usr) || purchase_message_cooldown < world.time) + say("Thank you for shopping with [src]!") + purchase_message_cooldown = world.time + 5 SECONDS + //This is not the best practice, but it's safe enough here since the chances of two people using a machine with the same ref in 5 seconds is fuck low + last_shopper = REF(usr) + use_power(5) + if(icon_vend) //Show the vending animation if needed + flick(icon_vend,src) + playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3) + var/obj/item/vended_item = new R.product_path(get_turf(src)) + // if(greyscale_colors) + // vended_item.set_greyscale(colors=greyscale_colors) + R.amount-- + if(usr.CanReach(src) && usr.put_in_hands(vended_item)) + to_chat(usr, span_notice("You take [R.name] out of the slot.")) + else + to_chat(usr, span_warning("[capitalize(R.name)] falls onto the floor!")) + SSblackbox.record_feedback("nested tally", "vending_machine_usage", 1, list("[type]", "[R.product_path]")) + vend_ready = TRUE + +/obj/machinery/vending/process(delta_time) if(stat & (BROKEN|NOPOWER)) return PROCESS_KILL if(!active) @@ -840,21 +940,21 @@ GLOBAL_LIST_EMPTY(vending_products) seconds_electrified-- //Pitch to the people! Really sell it! - if(last_slogan + slogan_delay <= world.time && slogan_list.len > 0 && !shut_up && prob(5)) + if(last_slogan + slogan_delay <= world.time && slogan_list.len > 0 && !shut_up && DT_PROB(2.5, delta_time)) var/slogan = pick(slogan_list) speak(slogan) last_slogan = world.time - if(shoot_inventory && prob(shoot_inventory_chance)) + if(shoot_inventory && DT_PROB(shoot_inventory_chance, delta_time)) throw_item() /** - * Speak the given message verbally - * - * Checks if the machine is powered and the message exists - * - * Arguments: - * * message - the message to speak - */ + * Speak the given message verbally + * + * Checks if the machine is powered and the message exists + * + * Arguments: + * * message - the message to speak + */ /obj/machinery/vending/proc/speak(message) if(stat & (BROKEN|NOPOWER)) return @@ -863,25 +963,6 @@ GLOBAL_LIST_EMPTY(vending_products) say(message) -/** - * Gets the best discount from a given ID card, comparing its access and paycheck depart with cost_multiplier_per_dept. - * It only applies to the regular selection, not premium or contraband. And a bank account is still required. - * But it can also be used to charge more to certain departments or accesses. :) - * - * Arguments: - * * list/dept_access_list - the list to compare - */ -/obj/machinery/vending/proc/get_best_discount(obj/item/card/id/C) - var/list/discounts = NUMLIST2TEXTLIST(C.GetAccess()) - if(C.registered_account?.account_job) - discounts += C.registered_account.account_job.paycheck_department - discounts &= cost_multiplier_per_dept - if(!length(discounts)) - return 1 - . = INFINITY - for(var/k in discounts) - . = min(cost_multiplier_per_dept[k], .) - /obj/machinery/vending/power_change() . = ..() if(powered()) @@ -889,22 +970,24 @@ GLOBAL_LIST_EMPTY(vending_products) //Somebody cut an important wire and now we're following a new definition of "pitch." /** - * Throw an item from our internal inventory out in front of us - * - * This is called when we are hacked, it selects a random product from the records that has an amount > 0 - * This item is then created and tossed out in front of us with a visible message - */ + * Throw an item from our internal inventory out in front of us + * + * This is called when we are hacked, it selects a random product from the records that has an amount > 0 + * This item is then created and tossed out in front of us with a visible message + */ /obj/machinery/vending/proc/throw_item() var/obj/throw_item = null - var/mob/living/target = locate() in view(7, src) + var/mob/living/target = locate() in view(7,src) if(!target) return FALSE + for(var/datum/data/vending_product/R in shuffle(product_records)) if(R.amount <= 0) //Try to use a record that actually has something to dump. continue var/dump_path = R.product_path if(!dump_path) continue + R.amount-- throw_item = new dump_path(loc) break @@ -914,30 +997,30 @@ GLOBAL_LIST_EMPTY(vending_products) pre_throw(throw_item) throw_item.throw_at(target, 16, 3) - visible_message("[src] launches [throw_item] at [target]!") + visible_message(span_danger("[src] launches [throw_item] at [target]!")) return TRUE /** - * A callback called before an item is tossed out - * - * Override this if you need to do any special case handling - * - * Arguments: - * * I - obj/item being thrown - */ + * A callback called before an item is tossed out + * + * Override this if you need to do any special case handling + * + * Arguments: + * * I - obj/item being thrown + */ /obj/machinery/vending/proc/pre_throw(obj/item/I) return /** - * Shock the passed in user - * - * This checks we have power and that the passed in prob is passed, then generates some sparks - * and calls electrocute_mob on the user - * - * Arguments: - * * user - the user to shock - * * prb - probability the shock happens - */ + * Shock the passed in user + * + * This checks we have power and that the passed in prob is passed, then generates some sparks + * and calls electrocute_mob on the user + * + * Arguments: + * * user - the user to shock + * * prb - probability the shock happens + */ /obj/machinery/vending/proc/shock(mob/living/user, prb) - if(!istype(user) || stat & (BROKEN|NOPOWER)) // unpowered, no shock + if(!istype(user) || stat & (BROKEN|NOPOWER)) // unpowered, no shock return FALSE if(!prob(prb)) return FALSE @@ -948,24 +1031,39 @@ GLOBAL_LIST_EMPTY(vending_products) else return FALSE /** - * Are we able to load the item passed in - * - * Arguments: - * * I - the item being loaded - * * user - the user doing the loading - */ + * Are we able to load the item passed in + * + * Arguments: + * * I - the item being loaded + * * user - the user doing the loading + */ /obj/machinery/vending/proc/canLoadItem(obj/item/I, mob/user) return FALSE -/obj/machinery/vending/onTransitZ() - return +/obj/machinery/vending/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) + . = ..() + var/mob/living/L = AM + if(tilted || !istype(L) || !prob(20 * (throwingdatum.speed - L.throw_speed))) // hulk throw = +20%, neckgrab throw = +20% + return + + tilt(L) + +/obj/machinery/vending/attack_tk_grab(mob/user) + to_chat(user, span_warning("[src] seems to resist your mental grasp!")) + +///Crush the mob that the vending machine got thrown at +/obj/machinery/vending/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) + if(isliving(hit_atom)) + tilt(fatty=hit_atom) + return ..() /obj/machinery/vending/custom name = "Custom Vendor" - icon_state = "robotics" - icon_deny = "robotics-deny" + icon_state = "custom" + icon_deny = "custom-deny" max_integrity = 400 payment_department = NO_FREEBIES + light_mask = "custom-light-mask" refill_canister = /obj/item/vending_refill/custom /// where the money is sent var/datum/bank_account/private_a @@ -976,21 +1074,23 @@ GLOBAL_LIST_EMPTY(vending_products) /obj/machinery/vending/custom/compartmentLoadAccessCheck(mob/user) . = FALSE - var/mob/living/carbon/human/H - var/obj/item/card/id/C - if(ishuman(user)) - H = user - C = H.get_idcard(FALSE) - if(C?.registered_account && C.registered_account == private_a) - return TRUE + if(!isliving(user)) + return FALSE + var/mob/living/L = user + var/obj/item/card/id/C = L.get_idcard(FALSE) + if(C?.registered_account && C.registered_account == private_a) + return TRUE /obj/machinery/vending/custom/canLoadItem(obj/item/I, mob/user) . = FALSE + if(I.flags_1 & HOLOGRAM_1) + say("This vendor cannot accept nonexistent items.") + return if(loaded_items >= max_loaded_items) say("There are too many items in stock.") return if(istype(I, /obj/item/stack)) - say("Loose items may cause problems, try use it inside wrapping paper.") + say("Loose items may cause problems, try to use it inside wrapping paper.") return if(I.custom_price) return TRUE @@ -1004,19 +1104,21 @@ GLOBAL_LIST_EMPTY(vending_products) var/base64 var/price = 0 for(var/obj/T in contents) - if(T.name == O) + if(format_text(T.name) == O) price = T.custom_price if(!base64) if(base64_cache[T.type]) base64 = base64_cache[T.type] else - base64 = icon2base64(icon(T.icon, T.icon_state, SOUTH, 1)) + base64 = icon2base64(getFlatIcon(T, no_anim=TRUE)) base64_cache[T.type] = base64 break var/list/data = list( name = O, price = price, - img = base64 + img = base64, + amount = vending_machine_input[O], + colorable = FALSE ) .["vending_machine_input"] += list(data) @@ -1032,64 +1134,63 @@ GLOBAL_LIST_EMPTY(vending_products) var/N = params["item"] var/obj/S vend_ready = FALSE - if(ishuman(usr)) - var/mob/living/carbon/human/H = usr - var/obj/item/card/id/C = H.get_idcard(TRUE) - - if(!C) - say("No card found.") - flick(icon_deny,src) + var/obj/item/card/id/C + if(isliving(usr)) + var/mob/living/L = usr + C = L.get_idcard(TRUE) + if(!C) + say("No card found.") + flick(icon_deny,src) + vend_ready = TRUE + return + else if (!C.registered_account) + say("No account found.") + flick(icon_deny,src) + vend_ready = TRUE + return + var/datum/bank_account/account = C.registered_account + for(var/obj/O in contents) + if(format_text(O.name) == N) + S = O + break + if(S) + if(compartmentLoadAccessCheck(usr)) + vending_machine_input[N] = max(vending_machine_input[N] - 1, 0) + S.forceMove(drop_location()) + loaded_items-- + use_power(5) vend_ready = TRUE + updateUsrDialog() return - else if (!C.registered_account) - say("No account found.") - flick(icon_deny,src) + if(account.has_money(S.custom_price)) + account.adjust_money(-S.custom_price) + var/datum/bank_account/owner = private_a + if(owner) + owner.adjust_money(S.custom_price) + SSblackbox.record_feedback("amount", "vending_spent", S.custom_price) + // log_econ("[S.custom_price] credits were spent on [src] buying a [S] by [owner.account_holder], owned by [private_a.account_holder].") + vending_machine_input[N] = max(vending_machine_input[N] - 1, 0) + S.forceMove(drop_location()) + loaded_items-- + use_power(5) + if(last_shopper != REF(usr) || purchase_message_cooldown < world.time) + say("Thank you for buying local and purchasing [S]!") + purchase_message_cooldown = world.time + 5 SECONDS + last_shopper = REF(usr) vend_ready = TRUE + updateUsrDialog() return - var/datum/bank_account/account = C.registered_account - for(var/obj/O in contents) - if(O.name == N) - S = O - break - if(S) - if(compartmentLoadAccessCheck(usr)) - vending_machine_input[N] = max(vending_machine_input[N] - 1, 0) - S.forceMove(drop_location()) - loaded_items-- - use_power(5) - vend_ready = TRUE - updateUsrDialog() - return - if(account.has_money(S.custom_price)) - account.adjust_money(-S.custom_price) - var/datum/bank_account/owner = private_a - if(owner) - owner.adjust_money(S.custom_price) - vending_machine_input[N] = max(vending_machine_input[N] - 1, 0) - S.forceMove(drop_location()) - loaded_items-- - use_power(5) - if(last_shopper != usr || purchase_message_cooldown < world.time) - say("Thank you for buying local and purchasing [S]!") - purchase_message_cooldown = world.time + 5 SECONDS - last_shopper = usr - vend_ready = TRUE - updateUsrDialog() - return - else - say("You do not possess the funds to purchase this.") + else + say("You do not possess the funds to purchase this.") vend_ready = TRUE /obj/machinery/vending/custom/attackby(obj/item/I, mob/user, params) - if(!private_a) - var/mob/living/carbon/human/H - var/obj/item/card/id/C - if(ishuman(user)) - H = user - C = H.get_idcard(TRUE) - if(C?.registered_account) - private_a = C.registered_account - say("[src] has been linked to [C].") + if(!private_a && isliving(user)) + var/mob/living/L = user + var/obj/item/card/id/C = L.get_idcard(TRUE) + if(C?.registered_account) + private_a = C.registered_account + say("\The [src] has been linked to [C].") if(compartmentLoadAccessCheck(user)) if(istype(I, /obj/item/pen)) @@ -1099,15 +1200,6 @@ GLOBAL_LIST_EMPTY(vending_products) last_slogan = world.time + rand(0, slogan_delay) return - if(canLoadItem(I)) - loadingAttempt(I,user) - updateUsrDialog() - return - - if(panel_open && is_wire_tool(I)) - wires.interact(user) - return - return ..() /obj/machinery/vending/custom/crowbar_act(mob/living/user, obj/item/I) @@ -1119,7 +1211,7 @@ GLOBAL_LIST_EMPTY(vending_products) if(T) for(var/obj/item/I in contents) I.forceMove(T) - explosion(T, -1, 0, 3) + explosion(src, devastation_range = -1, light_impact_range = 3) return ..() /obj/machinery/vending/custom/unbreakable @@ -1142,7 +1234,7 @@ GLOBAL_LIST_EMPTY(vending_products) /obj/item/price_tagger/attack_self(mob/user) price = max(1, round(input(user,"set price","price") as num|null, 1)) - to_chat(user, " The [src] will now give things a [price] cr tag.") + to_chat(user, span_notice(" The [src] will now give things a [price] cr tag.")) /obj/item/price_tagger/afterattack(atom/target, mob/user, proximity) . = ..() @@ -1151,4 +1243,23 @@ GLOBAL_LIST_EMPTY(vending_products) if(isitem(target)) var/obj/item/I = target I.custom_price = price - to_chat(user, "You set the price of [I] to [price] cr.") + to_chat(user, span_notice("You set the price of [I] to [price] cr.")) + +/obj/machinery/vending/custom/greed //name and like decided by the spawn + icon_state = "greed" + icon_deny = "greed-deny" + light_mask = "greed-light-mask" + custom_materials = list(/datum/material/gold = MINERAL_MATERIAL_AMOUNT * 5) + +/obj/machinery/vending/custom/greed/Initialize(mapload) + . = ..() + //starts in a state where you can move it + panel_open = TRUE + set_anchored(FALSE) + add_overlay("[initial(icon_state)]-panel") + //and references the deity + name = "[GLOB.deity]'s Consecrated Vendor" + desc = "A vending machine created by [GLOB.deity]." + slogan_list = list("[GLOB.deity] says: It's your divine right to buy!") + add_filter("vending_outline", 9, list("type" = "outline", "color" = COLOR_VERY_SOFT_YELLOW)) + add_filter("vending_rays", 10, list("type" = "rays", "size" = 35, "color" = COLOR_VIVID_YELLOW)) diff --git a/code/modules/vending/assist.dm b/code/modules/vending/assist.dm index 29d1e760d4..9ab384111b 100644 --- a/code/modules/vending/assist.dm +++ b/code/modules/vending/assist.dm @@ -1,4 +1,9 @@ /obj/machinery/vending/assist + name = "\improper Part-Mart" + desc = "All the finest of miscellaneous electronics one could ever need! Not responsible for any injuries caused by reckless misuse of parts." + // icon_state = "parts" + // icon_deny = "parts-deny" + products = list(/obj/item/assembly/prox_sensor = 7, /obj/item/assembly/igniter = 6, /obj/item/assembly/playback = 4, @@ -15,13 +20,13 @@ premium = list(/obj/item/stock_parts/cell/upgraded/plus = 2, /obj/item/flashlight/lantern = 2, /obj/item/beacon = 2) - product_ads = "Only the finest!;Have some tools.;The most robust equipment.;The finest gear in space!" - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) refill_canister = /obj/item/vending_refill/assist - resistance_flags = FIRE_PROOF + product_ads = "Only the finest!;Have some tools.;The most robust equipment.;The finest gear in space!" default_price = PRICE_REALLY_CHEAP extra_price = PRICE_ALMOST_CHEAP payment_department = NO_FREEBIES + // light_mask = "parts-light-mask" /obj/item/vending_refill/assist - icon_state = "refill_engi" + machine_name = "Part-Mart" + icon_state = "refill_parts" diff --git a/code/modules/vending/autodrobe.dm b/code/modules/vending/autodrobe.dm index 264d262a1d..f1fbb95ff2 100644 --- a/code/modules/vending/autodrobe.dm +++ b/code/modules/vending/autodrobe.dm @@ -3,32 +3,39 @@ desc = "A vending machine for costumes." icon_state = "theater" icon_deny = "theater-deny" + req_access = list(ACCESS_THEATRE) product_slogans = "Dress for success!;Suited and booted!;It's show time!;Why leave style up to fate? Use AutoDrobe!" vend_reply = "Thank you for using AutoDrobe!" products = list(/obj/item/clothing/suit/chickensuit = 1, - /obj/item/clothing/head/chicken = 1, - /obj/item/clothing/under/costume/gladiator = 1, - /obj/item/clothing/head/helmet/gladiator = 1, - /obj/item/clothing/under/rank/captain/suit = 1, - /obj/item/clothing/head/flatcap = 1, - /obj/item/clothing/suit/toggle/labcoat/mad = 1, - /obj/item/clothing/shoes/jackboots = 1, - /obj/item/clothing/under/costume/schoolgirl = 1, - /obj/item/clothing/under/costume/schoolgirl/red = 1, - /obj/item/clothing/under/costume/schoolgirl/green = 1, - /obj/item/clothing/under/costume/schoolgirl/orange = 1, - /obj/item/clothing/head/kitty = 1, - /obj/item/clothing/under/dress/skirt = 1, - /obj/item/clothing/head/beret = 1, - /obj/item/clothing/accessory/waistcoat = 1, - /obj/item/clothing/under/suit/black = 1, - /obj/item/clothing/head/that = 1, - /obj/item/clothing/under/costume/kilt = 1, - /obj/item/clothing/head/beret = 1, - /obj/item/clothing/accessory/waistcoat = 1, - /obj/item/clothing/glasses/monocle =1, - /obj/item/clothing/head/bowler = 1, - /obj/item/cane = 1, + /obj/item/clothing/head/chicken = 1, + /obj/item/clothing/under/rank/civilian/clown/blue = 1, + /obj/item/clothing/under/rank/civilian/clown/green = 1, + /obj/item/clothing/under/rank/civilian/clown/yellow = 1, + /obj/item/clothing/under/rank/civilian/clown/orange = 1, + /obj/item/clothing/under/rank/civilian/clown/purple = 1, + /obj/item/clothing/under/costume/gladiator = 1, + /obj/item/clothing/head/helmet/gladiator = 1, + /obj/item/clothing/under/rank/captain/suit = 1, + /obj/item/clothing/under/rank/captain/suit/skirt = 1, + /obj/item/clothing/head/flatcap = 1, + /obj/item/clothing/suit/toggle/labcoat/mad = 1, + /obj/item/clothing/shoes/jackboots = 1, + /obj/item/clothing/under/costume/schoolgirl = 1, + /obj/item/clothing/under/costume/schoolgirl/red = 1, + /obj/item/clothing/under/costume/schoolgirl/green = 1, + /obj/item/clothing/under/costume/schoolgirl/orange = 1, + /obj/item/clothing/head/kitty = 1, + /obj/item/clothing/under/dress/skirt = 1, + /obj/item/clothing/head/beret = 1, + /obj/item/clothing/accessory/waistcoat = 1, + /obj/item/clothing/under/suit/black = 1, + /obj/item/clothing/head/that = 1, + /obj/item/clothing/under/costume/kilt = 1, + /obj/item/clothing/head/beret = 3, + /obj/item/clothing/accessory/waistcoat = 1, + /obj/item/clothing/glasses/monocle =1, + /obj/item/clothing/head/bowler = 1, + /obj/item/cane = 1, /obj/item/clothing/under/rank/civilian/victorian_redsleeves = 1, /obj/item/clothing/under/rank/civilian/victorian_redvest = 1, /obj/item/clothing/under/rank/civilian/victorian_vest = 1, @@ -39,110 +46,111 @@ /obj/item/clothing/under/rank/civilian/victorianblackdress = 1, /obj/item/clothing/suit/vickyblack =1, /obj/item/clothing/under/rank/civilian/dutch = 2, - /obj/item/clothing/under/suit/sl = 1, - /obj/item/clothing/mask/fakemoustache = 1, - /obj/item/clothing/suit/bio_suit/plaguedoctorsuit = 1, - /obj/item/clothing/head/plaguedoctorhat = 1, - /obj/item/clothing/mask/gas/plaguedoctor = 1, - /obj/item/clothing/suit/toggle/owlwings = 1, - /obj/item/clothing/under/costume/owl = 1, - /obj/item/clothing/mask/gas/owl_mask = 1, - /obj/item/clothing/suit/toggle/owlwings/griffinwings = 1, - /obj/item/clothing/under/costume/griffin = 1, - /obj/item/clothing/shoes/griffin = 1, - /obj/item/clothing/head/griffin = 1, - /obj/item/clothing/suit/apron = 1, - /obj/item/clothing/under/suit/waiter = 1, - /obj/item/clothing/suit/jacket/miljacket = 1, - /obj/item/clothing/under/costume/pirate = 1, - /obj/item/clothing/suit/pirate = 1, - /obj/item/clothing/head/pirate = 1, - /obj/item/clothing/head/bandana = 1, - /obj/item/clothing/head/bandana = 1, - /obj/item/clothing/under/costume/soviet = 1, - /obj/item/clothing/head/ushanka = 1, - /obj/item/clothing/suit/imperium_monk = 1, - /obj/item/clothing/mask/gas/cyborg = 1, - /obj/item/clothing/suit/chaplain/holidaypriest = 1, - /obj/item/clothing/head/wizard/marisa/fake = 1, - /obj/item/clothing/suit/wizrobe/marisa/fake = 1, - /obj/item/clothing/under/dress/sundress = 1, - /obj/item/clothing/head/witchwig = 1, - /obj/item/staff/broom = 1, - /obj/item/clothing/suit/wizrobe/fake = 1, - /obj/item/clothing/head/wizard/fake = 1, - /obj/item/staff = 3, - /obj/item/clothing/under/rank/civilian/mime/skirt = 1, - /obj/item/clothing/under/rank/captain/suit/skirt = 1, + /obj/item/clothing/under/suit/sl = 1, + /obj/item/clothing/mask/fakemoustache = 1, + /obj/item/clothing/suit/bio_suit/plaguedoctorsuit = 1, + /obj/item/clothing/head/plaguedoctorhat = 1, + /obj/item/clothing/mask/gas/plaguedoctor = 1, + /obj/item/clothing/suit/toggle/owlwings = 1, + /obj/item/clothing/under/costume/owl = 1, + /obj/item/clothing/mask/gas/owl_mask = 1, + /obj/item/clothing/suit/toggle/owlwings/griffinwings = 1, + /obj/item/clothing/under/costume/griffin = 1, + /obj/item/clothing/shoes/griffin = 1, + /obj/item/clothing/head/griffin = 1, + /obj/item/clothing/suit/apron = 1, + /obj/item/clothing/under/suit/waiter = 1, + /obj/item/clothing/suit/jacket/miljacket = 1, + /obj/item/clothing/under/costume/pirate = 1, + /obj/item/clothing/suit/pirate = 1, + /obj/item/clothing/head/pirate = 1, + /obj/item/clothing/head/bandana = 1, + /obj/item/clothing/head/bandana = 1, + /obj/item/clothing/under/costume/soviet = 1, + /obj/item/clothing/head/ushanka = 1, + /obj/item/clothing/suit/imperium_monk = 1, + /obj/item/clothing/mask/gas/cyborg = 1, + /obj/item/clothing/suit/chaplain/holidaypriest = 1, + /obj/item/clothing/head/wizard/marisa/fake = 1, + /obj/item/clothing/suit/wizrobe/marisa/fake = 1, + /obj/item/clothing/under/dress/sundress = 1, + /obj/item/clothing/head/witchwig = 1, + /obj/item/staff/broom = 1, + /obj/item/clothing/suit/wizrobe/fake = 1, + /obj/item/clothing/head/wizard/fake = 1, + /obj/item/staff = 3, /obj/item/clothing/mask/gas/clown_hat/sexy = 1, - /obj/item/clothing/under/rank/civilian/clown/sexy = 1, + /obj/item/clothing/under/rank/civilian/clown/sexy = 1, /obj/item/clothing/mask/gas/mime/sexy = 1, - /obj/item/clothing/under/rank/civilian/mime/sexy = 1, - /obj/item/clothing/mask/rat/bat = 1, - /obj/item/clothing/mask/rat/bee = 1, - /obj/item/clothing/mask/rat/bear = 1, - /obj/item/clothing/mask/rat/raven = 1, - /obj/item/clothing/mask/rat/jackal = 1, - /obj/item/clothing/mask/rat/fox = 1, - /obj/item/clothing/mask/frog = 1, - /obj/item/clothing/mask/rat/tribal = 1, - /obj/item/clothing/mask/rat = 1, - /obj/item/clothing/suit/apron/overalls = 1, - /obj/item/clothing/head/rabbitears =1, - /obj/item/clothing/head/sombrero = 1, - /obj/item/clothing/head/sombrero/green = 1, - /obj/item/clothing/suit/poncho = 1, - /obj/item/clothing/suit/poncho/green = 1, - /obj/item/clothing/suit/poncho/red = 1, - /obj/item/clothing/head/maid = 1, - /obj/item/clothing/under/costume/maid = 1, - /obj/item/clothing/under/rank/civilian/janitor/maid = 1, + /obj/item/clothing/under/rank/civilian/mime/sexy = 1, + /obj/item/clothing/under/rank/civilian/mime/skirt = 1, + /obj/item/clothing/mask/rat/bat = 1, + /obj/item/clothing/mask/rat/bee = 1, + /obj/item/clothing/mask/rat/bear = 1, + /obj/item/clothing/mask/rat/raven = 1, + /obj/item/clothing/mask/rat/jackal = 1, + /obj/item/clothing/mask/rat/fox = 1, + /obj/item/clothing/mask/frog = 1, + /obj/item/clothing/mask/rat/tribal = 1, + /obj/item/clothing/mask/rat = 1, + /obj/item/clothing/suit/apron/overalls = 1, + /obj/item/clothing/head/rabbitears =1, + /obj/item/clothing/head/sombrero = 1, + /obj/item/clothing/head/sombrero/green = 1, + /obj/item/clothing/suit/poncho = 1, + /obj/item/clothing/suit/poncho/green = 1, + /obj/item/clothing/suit/poncho/red = 1, + /obj/item/clothing/head/maid = 1, + /obj/item/clothing/under/costume/maid = 1, + /obj/item/clothing/under/rank/civilian/janitor/maid = 1, /obj/item/clothing/gloves/evening = 1, - /obj/item/clothing/glasses/cold=1, - /obj/item/clothing/glasses/heat=1, - /obj/item/clothing/suit/whitedress = 1, - /obj/item/clothing/under/rank/civilian/clown/jester = 1, - /obj/item/clothing/head/jester = 1, - /obj/item/clothing/under/costume/villain = 1, - /obj/item/clothing/shoes/singery = 1, - /obj/item/clothing/under/costume/singer/yellow = 1, - /obj/item/clothing/shoes/singerb = 1, - /obj/item/clothing/under/costume/singer/blue = 1, - /obj/item/clothing/suit/hooded/carp_costume = 1, - /obj/item/clothing/suit/hooded/ian_costume = 1, - /obj/item/clothing/suit/hooded/bee_costume = 1, - /obj/item/clothing/suit/snowman = 1, - /obj/item/clothing/head/snowman = 1, - /obj/item/clothing/mask/joy = 1, - /obj/item/clothing/head/cueball = 1, - /obj/item/clothing/under/suit/white_on_white = 1, - /obj/item/clothing/under/costume/sailor = 1, - /obj/item/clothing/ears/headphones = 2, - /obj/item/clothing/head/wig/random = 3, - /obj/item/clothing/suit/ran = 2, - /obj/item/clothing/head/ran = 2, - /obj/item/clothing/mask/gas/timidcostume = 3, - /obj/item/clothing/suit/hooded/wintercoat/timidcostume = 3, - /obj/item/clothing/shoes/timidcostume = 3, - /obj/item/clothing/mask/gas/timidcostume/man = 3, - /obj/item/clothing/suit/hooded/wintercoat/timidcostume/man = 3, - /obj/item/clothing/shoes/timidcostume/man = 3, - ) + /obj/item/clothing/glasses/cold=1, + /obj/item/clothing/glasses/heat=1, + /obj/item/clothing/suit/whitedress = 1, + /obj/item/clothing/under/rank/civilian/clown/jester = 1, + /obj/item/clothing/head/jester = 1, + /obj/item/clothing/under/costume/villain = 1, + /obj/item/clothing/shoes/singery = 1, + /obj/item/clothing/under/costume/singer/yellow = 1, + /obj/item/clothing/shoes/singerb = 1, + /obj/item/clothing/under/costume/singer/blue = 1, + /obj/item/clothing/suit/hooded/carp_costume = 1, + /obj/item/clothing/suit/hooded/ian_costume = 1, + /obj/item/clothing/suit/hooded/bee_costume = 1, + /obj/item/clothing/suit/snowman = 1, + /obj/item/clothing/head/snowman = 1, + /obj/item/clothing/mask/joy = 1, + /obj/item/clothing/head/cueball = 1, + /obj/item/clothing/under/suit/white_on_white = 1, + /obj/item/clothing/under/costume/sailor = 1, + /obj/item/clothing/ears/headphones = 2, + /obj/item/clothing/head/wig/random = 3, + /obj/item/clothing/suit/ran = 2, + /obj/item/clothing/head/ran = 2, + /obj/item/clothing/mask/gas/timidcostume = 3, + /obj/item/clothing/suit/hooded/wintercoat/timidcostume = 3, + /obj/item/clothing/shoes/timidcostume = 3, + /obj/item/clothing/mask/gas/timidcostume/man = 3, + /obj/item/clothing/suit/hooded/wintercoat/timidcostume/man = 3, + /obj/item/clothing/shoes/timidcostume/man = 3, + ) contraband = list(/obj/item/clothing/suit/judgerobe = 1, - /obj/item/clothing/head/powdered_wig = 1, - /obj/item/gun/magic/wand = 2, - /obj/item/clothing/glasses/sunglasses/garb = 2, - /obj/item/clothing/glasses/sunglasses/blindfold = 1, - /obj/item/clothing/mask/muzzle = 2, - /obj/item/clothing/under/syndicate/camo/cosmetic = 3) + /obj/item/clothing/head/powdered_wig = 1, + /obj/item/gun/magic/wand = 2, + /obj/item/clothing/glasses/sunglasses/garb = 2, + /obj/item/clothing/glasses/sunglasses/blindfold = 1, + /obj/item/clothing/mask/muzzle = 2, + /obj/item/clothing/under/syndicate/camo/cosmetic = 3 + ) premium = list(/obj/item/clothing/suit/pirate/captain = 2, - /obj/item/clothing/head/pirate/captain = 2, - /obj/item/clothing/head/helmet/roman/fake = 1, - /obj/item/clothing/head/helmet/roman/legionnaire/fake = 1, - /obj/item/clothing/under/costume/roman = 1, - /obj/item/clothing/shoes/roman = 1, - /obj/item/shield/riot/roman/fake = 1, - /obj/item/skub = 1, + /obj/item/clothing/head/pirate/captain = 2, + /obj/item/clothing/under/rank/civilian/clown/rainbow = 1, + /obj/item/clothing/head/helmet/roman/fake = 1, + /obj/item/clothing/head/helmet/roman/legionnaire/fake = 1, + /obj/item/clothing/under/costume/roman = 1, + /obj/item/clothing/shoes/roman = 1, + /obj/item/shield/riot/roman/fake = 1, + /obj/item/skub = 1, /obj/item/clothing/under/costume/lobster = 1, /obj/item/clothing/head/lobsterhat = 1, /obj/item/clothing/head/drfreezehat = 1, @@ -157,11 +165,11 @@ /obj/item/clothing/head/christmashat = 3, /obj/item/clothing/head/christmashatg = 3, /obj/item/clothing/under/costume/drfreeze = 1) - refill_canister = /obj/item/vending_refill/autodrobe default_price = PRICE_ALMOST_CHEAP extra_price = PRICE_ALMOST_EXPENSIVE payment_department = ACCOUNT_SRV + light_mask="theater-light-mask" /obj/machinery/vending/autodrobe/Initialize() . = ..() @@ -170,6 +178,10 @@ /obj/machinery/vending/autodrobe/canLoadItem(obj/item/I,mob/user) return (I.type in products) +/obj/machinery/vending/autodrobe/all_access + desc = "A vending machine for costumes. This model appears to have no access restrictions." + req_access = null + /obj/item/vending_refill/autodrobe machine_name = "AutoDrobe" icon_state = "refill_costume" diff --git a/code/modules/vending/boozeomat.dm b/code/modules/vending/boozeomat.dm index 87f2a0940b..df57dadd2e 100644 --- a/code/modules/vending/boozeomat.dm +++ b/code/modules/vending/boozeomat.dm @@ -41,13 +41,15 @@ premium = list(/obj/item/reagent_containers/glass/bottle/ethanol = 4, /obj/item/reagent_containers/food/drinks/bottle/champagne = 5, /obj/item/reagent_containers/food/drinks/bottle/trappist = 5) + product_slogans = "I hope nobody asks me for a bloody cup o' tea...;Alcohol is humanity's friend. Would you abandon a friend?;Quite delighted to serve you!;Is nobody thirsty on this station?" product_ads = "Drink up!;Booze is good for you!;Alcohol is humanity's best friend.;Quite delighted to serve you!;Care for a nice, cold beer?;Nothing cures you like booze!;Have a sip!;Have a drink!;Have a beer!;Beer is good for you!;Only the finest alcohol!;Best quality booze since 2053!;Award-winning wine!;Maximum alcohol!;Man loves beer.;A toast for progress!" + refill_canister = /obj/item/vending_refill/boozeomat default_price = PRICE_ALMOST_CHEAP extra_price = PRICE_EXPENSIVE payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) + light_mask = "boozeomat-light-mask" /obj/machinery/vending/boozeomat/pubby_maint //abandoned bar on Pubbystation products = list(/obj/item/reagent_containers/food/drinks/bottle/whiskey = 1, @@ -67,12 +69,14 @@ /obj/item/reagent_containers/food/drinks/drinkingglass = 6, /obj/item/reagent_containers/food/drinks/ice = 1, /obj/item/reagent_containers/food/drinks/drinkingglass/shotglass = 4); + payment_department = ACCOUNT_SEC -/obj/machinery/vending/boozeomat/pubby_captain/Initialize() - . = ..() - cost_multiplier_per_dept = list("[ACCESS_CAPTAIN]" = 0) - +/obj/machinery/vending/boozeomat/all_access + desc = "A technological marvel, supposedly able to mix just the mixture you'd like to drink the moment you ask for one. This model appears to have no access restrictions." + req_access = null /obj/machinery/vending/boozeomat/syndicate_access + req_access = list(ACCESS_SYNDICATE) + age_restrictions = FALSE payment_department = NO_FREEBIES /obj/machinery/vending/boozeomat/syndicate_access/Initialize() diff --git a/code/modules/vending/cartridge.dm b/code/modules/vending/cartridge.dm index 69635007c9..db8db77ad4 100644 --- a/code/modules/vending/cartridge.dm +++ b/code/modules/vending/cartridge.dm @@ -11,19 +11,18 @@ /obj/item/cartridge/janitor = 10, /obj/item/cartridge/signal/toxins = 10, /obj/item/cartridge/roboticist = 10, - /obj/item/pda/heads = 10) - premium = list(/obj/item/cartridge/captain = 2, - /obj/item/cartridge/quartermaster = 2) + /obj/item/pda/heads = 10, + /obj/item/cartridge/captain = 3, + /obj/item/cartridge/quartermaster = 10) armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) refill_canister = /obj/item/vending_refill/cart resistance_flags = FIRE_PROOF default_price = PRICE_ALMOST_EXPENSIVE extra_price = PRICE_ALMOST_ONE_GRAND payment_department = ACCOUNT_SRV - -/obj/machinery/vending/cart/Initialize() - . = ..() - cost_multiplier_per_dept = list("[ACCESS_CHANGE_IDS]" = 0) + light_mask="cart-light-mask" /obj/item/vending_refill/cart - icon_state = "refill_pda" + machine_name = "PTech" + icon_state = "refill_smoke" + diff --git a/code/modules/vending/cigarette.dm b/code/modules/vending/cigarette.dm index fa4247575f..269e0aaa7b 100644 --- a/code/modules/vending/cigarette.dm +++ b/code/modules/vending/cigarette.dm @@ -17,13 +17,15 @@ /obj/item/storage/fancy/cigarettes/cigpack_shadyjims = 1, /obj/item/clothing/mask/cigarette/dart = 3) premium = list(/obj/item/storage/fancy/cigarettes/cigpack_robustgold = 3, - /obj/item/storage/fancy/cigarettes/cigars = 1, - /obj/item/storage/fancy/cigarettes/cigars/havana = 1, - /obj/item/storage/fancy/cigarettes/cigars/cohiba = 1) + /obj/item/lighter = 3, + /obj/item/storage/fancy/cigarettes/cigars = 1, + /obj/item/storage/fancy/cigarettes/cigars/havana = 1, + /obj/item/storage/fancy/cigarettes/cigars/cohiba = 1) refill_canister = /obj/item/vending_refill/cigarette default_price = PRICE_ALMOST_CHEAP extra_price = PRICE_ABOVE_NORMAL payment_department = ACCOUNT_SRV + light_mask = "cigs-light-mask" /obj/machinery/vending/cigarette/syndicate products = list(/obj/item/storage/fancy/cigarettes/cigpack_syndicate = 7, @@ -34,7 +36,6 @@ /obj/item/storage/box/matches = 10, /obj/item/lighter/greyscale = 4, /obj/item/storage/fancy/rollingpapers = 5) - payment_department = NO_FREEBIES /obj/machinery/vending/cigarette/syndicate/Initialize() . = ..() diff --git a/code/modules/vending/clothesmate.dm b/code/modules/vending/clothesmate.dm index 05166bf2c3..b2ac560849 100644 --- a/code/modules/vending/clothesmate.dm +++ b/code/modules/vending/clothesmate.dm @@ -1,10 +1,9 @@ -//DON'T FORGET TO CHANGE THE REFILL SIZE IF YOU CHANGE THE MACHINE'S CONTENTS! /obj/machinery/vending/clothing name = "ClothesMate" //renamed to make the slogan rhyme desc = "A vending machine for clothing." icon_state = "clothes" icon_deny = "clothes-deny" - product_slogans = "Dress for success!;Prepare to look swagalicious!;Look at all this free swag!;Why leave style up to fate? Use the ClothesMate!" + product_slogans = "Dress for success!;Prepare to look swagalicious!;Look at all this swag!;Why leave style up to fate? Use the ClothesMate!" vend_reply = "Thank you for using the ClothesMate!" products = list(/obj/item/clothing/head/that = 4, /obj/item/clothing/head/fedora = 3, @@ -232,9 +231,11 @@ /obj/item/clothing/suit/jacket/bluehoodie = 4, /obj/item/clothing/suit/toggle/jacket/whitehoodie = 4) refill_canister = /obj/item/vending_refill/clothing - default_price = PRICE_CHEAP + default_price = PRICE_CHEAP //Default of extra_price = PRICE_BELOW_NORMAL payment_department = NO_FREEBIES + light_mask = "wardrobe-light-mask" + light_color = LIGHT_COLOR_ELECTRIC_GREEN /obj/machinery/vending/clothing/canLoadItem(obj/item/I,mob/user) return (I.type in products) diff --git a/code/modules/vending/coffee.dm b/code/modules/vending/coffee.dm index fd555526c6..cdc794f61b 100644 --- a/code/modules/vending/coffee.dm +++ b/code/modules/vending/coffee.dm @@ -1,7 +1,7 @@ /obj/machinery/vending/coffee name = "\improper Solar's Best Hot Drinks" desc = "A vending machine which dispenses hot drinks." - product_ads = "Just what you need!;Have a drink!;Drink up!;It's good for you!;Would you like a hot joe?;I'd kill for some coffee!;The best beans in the galaxy.;Only the finest brew for you.;Mmmm. Nothing like a coffee.;I like coffee, don't you?;Coffee helps you work!;Try some tea.;We hope you like the best!;Try our new chocolate!;Admin conspiracies" + product_ads = "Have a drink!;Drink up!;It's good for you!;Would you like a hot joe?;I'd kill for some coffee!;The best beans in the galaxy.;Only the finest brew for you.;Mmmm. Nothing like a coffee.;I like coffee, don't you?;Coffee helps you work!;Try some tea.;We hope you like the best!;Try our new chocolate!;Admin conspiracies" icon_state = "coffee" icon_vend = "coffee-vend" products = list(/obj/item/reagent_containers/food/drinks/coffee = 25, @@ -15,11 +15,12 @@ /obj/item/reagent_containers/food/drinks/bottle/cream = 2, /obj/item/reagent_containers/food/condiment/sugar = 1, /obj/item/reagent_containers/food/drinks/mug/tea/forest = 3,) - refill_canister = /obj/item/vending_refill/coffee default_price = PRICE_REALLY_CHEAP extra_price = PRICE_PRETTY_CHEAP payment_department = ACCOUNT_SRV + light_mask = "coffee-light-mask" + light_color = COLOR_DARK_MODERATE_ORANGE /obj/item/vending_refill/coffee machine_name = "Solar's Best Hot Drinks" diff --git a/code/modules/vending/cola.dm b/code/modules/vending/cola.dm index bb5b8ef288..6bc5b9cb93 100644 --- a/code/modules/vending/cola.dm +++ b/code/modules/vending/cola.dm @@ -6,7 +6,7 @@ product_slogans = "Robust Softdrinks: More robust than a toolbox to the head!" product_ads = "Refreshing!;Hope you're thirsty!;Over 1 million drinks sold!;Thirsty? Why not cola?;Please, have a drink!;Drink up!;The best drinks in space." products = list(/obj/item/reagent_containers/food/drinks/soda_cans/cola = 10, - /obj/item/reagent_containers/food/drinks/soda_cans/space_mountain_wind = 10, + /obj/item/reagent_containers/food/drinks/soda_cans/space_mountain_wind = 10, /obj/item/reagent_containers/food/drinks/soda_cans/dr_gibb = 10, /obj/item/reagent_containers/food/drinks/soda_cans/starkist = 10, /obj/item/reagent_containers/food/drinks/soda_cans/space_up = 10, @@ -18,14 +18,15 @@ /obj/item/reagent_containers/food/drinks/soda_cans/shamblers = 6, /obj/item/reagent_containers/glass/beaker/waterbottle/wataur = 2) premium = list(/obj/item/reagent_containers/food/drinks/drinkingglass/filled/nuka_cola = 1, - /obj/item/reagent_containers/food/drinks/soda_cans/air = 1, - /obj/item/reagent_containers/food/drinks/soda_cans/grey_bull = 1, - /obj/item/reagent_containers/food/drinks/soda_cans/monkey_energy = 1) + /obj/item/reagent_containers/food/drinks/soda_cans/air = 1, + /obj/item/reagent_containers/food/drinks/soda_cans/monkey_energy = 1, + /obj/item/reagent_containers/food/drinks/soda_cans/grey_bull = 1) refill_canister = /obj/item/vending_refill/cola default_price = PRICE_CHEAP_AS_FREE extra_price = PRICE_ABOVE_NORMAL payment_department = ACCOUNT_SRV + /obj/item/vending_refill/cola machine_name = "Robust Softdrinks" icon_state = "refill_cola" @@ -43,43 +44,56 @@ /obj/machinery/vending/cola/blue icon_state = "Cola_Machine" + light_mask = "cola-light-mask" + light_color = COLOR_MODERATE_BLUE /obj/machinery/vending/cola/black icon_state = "cola_black" + light_mask = "cola-light-mask" /obj/machinery/vending/cola/red icon_state = "red_cola" name = "\improper Space Cola Vendor" desc = "It vends cola, in space." product_slogans = "Cola in space!" + light_mask = "red_cola-light-mask" + light_color = COLOR_DARK_RED /obj/machinery/vending/cola/space_up icon_state = "space_up" name = "\improper Space-up! Vendor" desc = "Indulge in an explosion of flavor." product_slogans = "Space-up! Like a hull breach in your mouth." + light_mask = "space_up-light-mask" + light_color = COLOR_DARK_MODERATE_LIME_GREEN /obj/machinery/vending/cola/starkist icon_state = "starkist" name = "\improper Star-kist Vendor" desc = "The taste of a star in liquid form." product_slogans = "Drink the stars! Star-kist!" + light_mask = "starkist-light-mask" + light_color = COLOR_LIGHT_ORANGE /obj/machinery/vending/cola/sodie icon_state = "soda" + light_mask = "soda-light-mask" + light_color = COLOR_WHITE /obj/machinery/vending/cola/pwr_game icon_state = "pwr_game" name = "\improper Pwr Game Vendor" desc = "You want it, we got it. Brought to you in partnership with Vlad's Salads." product_slogans = "The POWER that gamers crave! PWR GAME!" + light_mask = "pwr_game-light-mask" + light_color = COLOR_STRONG_VIOLET /obj/machinery/vending/cola/shamblers name = "\improper Shambler's Vendor" desc = "~Shake me up some of that Shambler's Juice!~" icon_state = "shamblers_juice" products = list(/obj/item/reagent_containers/food/drinks/soda_cans/cola = 10, - /obj/item/reagent_containers/food/drinks/soda_cans/space_mountain_wind = 10, + /obj/item/reagent_containers/food/drinks/soda_cans/space_mountain_wind = 10, /obj/item/reagent_containers/food/drinks/soda_cans/dr_gibb = 10, /obj/item/reagent_containers/food/drinks/soda_cans/starkist = 10, /obj/item/reagent_containers/food/drinks/soda_cans/space_up = 10, @@ -89,7 +103,8 @@ /obj/item/reagent_containers/food/drinks/soda_cans/shamblers = 10) product_slogans = "~Shake me up some of that Shambler's Juice!~" product_ads = "Refreshing!;Jyrbv dv lg jfdv fw kyrk Jyrdscvi'j Alztv!;Over 1 trillion souls drank!;Thirsty? Nyp efk uizeb kyv uribevjj?;Kyv Jyrdscvi uizebj kyv ezxyk!;Drink up!;Krjkp." - + light_mask = "shamblers-light-mask" + light_color = COLOR_MOSTLY_PURE_PINK /obj/machinery/vending/cola/buzz_fuzz name = "\improper Buzz Fuzz Vendor" desc = "~A hive of Flavour!~" diff --git a/code/modules/vending/engineering.dm b/code/modules/vending/engineering.dm index ef4edd6b67..fc88e6404c 100644 --- a/code/modules/vending/engineering.dm +++ b/code/modules/vending/engineering.dm @@ -5,9 +5,9 @@ icon_state = "engi" icon_deny = "engi-deny" products = list(/obj/item/clothing/under/rank/engineering/chief_engineer = 4, - /obj/item/clothing/under/rank/engineering/engineer = 4, - /obj/item/clothing/shoes/sneakers/orange = 4, - /obj/item/clothing/head/hardhat = 4, + /obj/item/clothing/under/rank/engineering/engineer = 4, + /obj/item/clothing/shoes/sneakers/orange = 4, + /obj/item/clothing/head/hardhat = 4, /obj/item/storage/belt/utility = 4, /obj/item/clothing/glasses/meson/engine = 4, /obj/item/clothing/gloves/color/yellow = 4, @@ -26,9 +26,13 @@ /obj/item/stock_parts/micro_laser = 5, /obj/item/stock_parts/matter_bin = 5, /obj/item/stock_parts/manipulator = 5) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) + refill_canister = /obj/item/vending_refill/engineering resistance_flags = FIRE_PROOF default_price = PRICE_NORMAL extra_price = PRICE_NORMAL payment_department = ACCOUNT_ENG - cost_multiplier_per_dept = list(ACCOUNT_ENG = 0) + light_mask = "engi-light-mask" + +/obj/item/vending_refill/engineering + machine_name = "Robco Tool Maker" + icon_state = "refill_engi" diff --git a/code/modules/vending/engivend.dm b/code/modules/vending/engivend.dm index 965ebddd15..559e56eec7 100644 --- a/code/modules/vending/engivend.dm +++ b/code/modules/vending/engivend.dm @@ -27,13 +27,13 @@ /obj/item/rcd_ammo/large = 5, /obj/item/storage/bag/material = 3 ) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) refill_canister = /obj/item/vending_refill/engivend resistance_flags = FIRE_PROOF default_price = PRICE_ALMOST_EXPENSIVE extra_price = PRICE_ABOVE_EXPENSIVE payment_department = ACCOUNT_ENG - cost_multiplier_per_dept = list(ACCOUNT_ENG = 0) + light_mask = "engivend-light-mask" /obj/item/vending_refill/engivend + machine_name = "Engi-Vend" icon_state = "refill_engi" diff --git a/code/modules/vending/games.dm b/code/modules/vending/games.dm index 7664a191c8..e6a6eb01e9 100644 --- a/code/modules/vending/games.dm +++ b/code/modules/vending/games.dm @@ -4,15 +4,17 @@ product_ads = "Escape to a fantasy world!;Fuel your gambling addiction!;Ruin your friendships!;Roll for initiative!;Elves and dwarves!;Paranoid computers!;Totally not satanic!;Fun times forever!" icon_state = "games" products = list(/obj/item/toy/cards/deck = 5, - /obj/item/storage/dice = 10, - /obj/item/toy/cards/deck/cas = 3, - /obj/item/toy/cards/deck/cas/black = 3, + /obj/item/storage/dice = 10, + /obj/item/toy/cards/deck/cas = 3, + /obj/item/toy/cards/deck/cas/black = 3, /obj/item/toy/cards/deck/unum = 3, + /obj/item/camera = 3, /obj/item/cardpack/series_one = 10, - /obj/item/dyespray=3, /obj/item/tcgcard_binder = 5, /obj/item/canvas = 3, - /obj/item/toy/crayon/spraycan = 3) + /obj/item/toy/crayon/spraycan = 3, + /obj/item/dyespray=3, + ) contraband = list(/obj/item/dice/fudge = 9) premium = list(/obj/item/melee/skateboard/pro = 3, /obj/item/melee/skateboard/hoverboard = 1) @@ -20,7 +22,7 @@ default_price = PRICE_CHEAP extra_price = PRICE_ALMOST_EXPENSIVE payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) + light_mask = "games-light-mask" /obj/item/vending_refill/games machine_name = "\improper Good Clean Fun" diff --git a/code/modules/vending/liberation.dm b/code/modules/vending/liberation.dm index 81afab3a70..36bde6dd6b 100644 --- a/code/modules/vending/liberation.dm +++ b/code/modules/vending/liberation.dm @@ -2,15 +2,15 @@ name = "\improper Liberation Station" desc = "An overwhelming amount of ancient patriotism washes over you just by looking at the machine." icon_state = "liberationstation" - product_slogans = "Liberation Station: Your one-stop shop for all things second ammendment!;Be a patriot today, pick up a gun!;Quality weapons for cheap prices!;Better dead than red!" - product_ads = "Float like an astronaut, sting like a bullet!;Express your second ammendment today!;Guns don't kill people, but you can!;Who needs responsibilities when you have guns?" + product_slogans = "Liberation Station: Your one-stop shop for all things second amendment!;Be a patriot today, pick up a gun!;Quality weapons for cheap prices!;Better dead than red!" + product_ads = "Float like an astronaut, sting like a bullet!;Express your second amendment today!;Guns don't kill people, but you can!;Who needs responsibilities when you have guns?" vend_reply = "Remember the name: Liberation Station!" products = list(/obj/item/reagent_containers/food/snacks/burger/plain = 5, //O say can you see, by the dawn's early light /obj/item/reagent_containers/food/snacks/burger/baseball = 3, //What so proudly we hailed at the twilight's last gleaming /obj/item/reagent_containers/food/snacks/fries = 5, //Whose broad stripes and bright stars through the perilous fight /obj/item/reagent_containers/food/drinks/beer/light = 10, //O'er the ramparts we watched, were so gallantly streaming? /obj/item/gun/ballistic/automatic/pistol/deagle/gold = 2, - /obj/item/gun/ballistic/automatic/pistol/deagle/camo = 2, + /obj/item/gun/ballistic/automatic/pistol/deagle/camo = 2, /obj/item/gun/ballistic/automatic/pistol/m1911 = 2, /obj/item/gun/ballistic/automatic/proto/unrestricted = 2, /obj/item/gun/ballistic/shotgun/automatic/combat = 2, @@ -18,16 +18,16 @@ /obj/item/gun/ballistic/shotgun = 2, /obj/item/gun/ballistic/automatic/ar = 2) premium = list(/obj/item/ammo_box/magazine/smgm9mm = 2, - /obj/item/ammo_box/magazine/m50 = 4, - /obj/item/ammo_box/magazine/m45 = 2, - /obj/item/ammo_box/magazine/m75 = 2, + /obj/item/ammo_box/magazine/m50 = 4, + /obj/item/ammo_box/magazine/m45 = 2, + /obj/item/ammo_box/magazine/m75 = 2, /obj/item/reagent_containers/food/snacks/cheesyfries = 5, /obj/item/reagent_containers/food/snacks/burger/baconburger = 5) //Premium burgers for the premium section contraband = list(/obj/item/clothing/under/misc/patriotsuit = 3, - /obj/item/bedsheet/patriot = 5, + /obj/item/bedsheet/patriot = 5, /obj/item/reagent_containers/food/snacks/burger/superbite = 3) //U S A - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) resistance_flags = FIRE_PROOF default_price = PRICE_ABOVE_NORMAL extra_price = PRICE_ABOVE_EXPENSIVE payment_department = ACCOUNT_SEC + light_mask = "liberation-light-mask" diff --git a/code/modules/vending/liberation_toy.dm b/code/modules/vending/liberation_toy.dm index 9093d55b0d..eea1150abf 100644 --- a/code/modules/vending/liberation_toy.dm +++ b/code/modules/vending/liberation_toy.dm @@ -16,19 +16,16 @@ /obj/item/clothing/suit/syndicatefake = 5, /obj/item/clothing/head/syndicatefake = 5) //OPS IN DORMS oh wait it's just an assistant contraband = list(/obj/item/gun/ballistic/shotgun/toy/crossbow = 10, //Congrats, you unlocked the +18 setting! - /obj/item/gun/ballistic/automatic/c20r/toy/unrestricted/riot = 10, - /obj/item/gun/ballistic/automatic/l6_saw/toy/unrestricted/riot = 10, - /obj/item/ammo_box/foambox/riot = 20, - /obj/item/toy/katana = 10, - /obj/item/dualsaber/toy = 5, - /obj/item/toy/cards/deck/syndicate = 10) //Gambling and it hurts, making it a +18 item + /obj/item/gun/ballistic/automatic/c20r/toy/unrestricted/riot = 10, + /obj/item/gun/ballistic/automatic/l6_saw/toy/unrestricted/riot = 10, + /obj/item/ammo_box/foambox/riot = 20, + /obj/item/toy/katana = 10, + /obj/item/dualsaber/toy = 5, + /obj/item/toy/cards/deck/syndicate = 10) //Gambling and it hurts, making it a +18 item armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) resistance_flags = FIRE_PROOF refill_canister = /obj/item/vending_refill/donksoft default_price = PRICE_ABOVE_NORMAL extra_price = PRICE_EXPENSIVE - payment_department = NO_FREEBIES - -/obj/machinery/vending/toyliberationstation/Initialize() - . = ..() - cost_multiplier_per_dept = list("[ACCESS_SYNDICATE]" = 0) + payment_department = ACCOUNT_SRV + light_mask = "donksoft-light-mask" diff --git a/code/modules/vending/magivend.dm b/code/modules/vending/magivend.dm index 9dcd77e4ab..104f7b3c02 100644 --- a/code/modules/vending/magivend.dm +++ b/code/modules/vending/magivend.dm @@ -4,18 +4,19 @@ icon_state = "MagiVend" product_slogans = "Sling spells the proper way with MagiVend!;Be your own Houdini! Use MagiVend!" vend_reply = "Have an enchanted evening!" - product_ads = "EI NATH;Destroy the station!;Admin conspiracies since forever!;Space-time bending hardware!;Now-magic proofing venders!" + product_ads = "FJKLFJSD;AJKFLBJAKL;1234 LOONIES LOL!;>MFW;Kill them fuckers!;GET DAT FUKKEN DISK;HONK!;EI NATH;Destroy the station!;Admin conspiracies since forever!;Space-time bending hardware!" products = list(/obj/item/clothing/head/wizard = 1, - /obj/item/clothing/suit/wizrobe = 1, - /obj/item/clothing/head/wizard/red = 1, - /obj/item/clothing/suit/wizrobe/red = 1, - /obj/item/clothing/head/wizard/yellow = 1, - /obj/item/clothing/suit/wizrobe/yellow = 1, - /obj/item/clothing/shoes/sandal/magic = 1, - /obj/item/staff = 2) - contraband = list(/obj/item/reagent_containers/glass/bottle/wizarditis = 1) //No one can get to the machine to hack it anyways; for the lulz - Microwave + /obj/item/clothing/suit/wizrobe = 1, + /obj/item/clothing/head/wizard/red = 1, + /obj/item/clothing/suit/wizrobe/red = 1, + /obj/item/clothing/head/wizard/yellow = 1, + /obj/item/clothing/suit/wizrobe/yellow = 1, + /obj/item/clothing/shoes/sandal/magic = 1, + /obj/item/staff = 2) + contraband = list(/obj/item/reagent_containers/glass/bottle/wizarditis = 1) //No one can get to the machine to hack it anyways; for the lulz - Microwave armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50, "magic" = 100) resistance_flags = FIRE_PROOF - default_price = PRICE_EXPENSIVE + default_price = 0 //Just in case, since it's primary use is storage. extra_price = PRICE_ABOVE_EXPENSIVE payment_department = ACCOUNT_SRV + light_mask = "magivend-light-mask" diff --git a/code/modules/vending/medical.dm b/code/modules/vending/medical.dm index a24233b17c..0158547515 100644 --- a/code/modules/vending/medical.dm +++ b/code/modules/vending/medical.dm @@ -4,6 +4,7 @@ icon_state = "med" icon_deny = "med-deny" product_ads = "Go save some lives!;The best stuff for your medbay.;Only the finest tools.;Natural chemicals!;This stuff saves lives.;Don't you want some?;Ping!" + req_access = list(ACCESS_MEDICAL) products = list(/obj/item/reagent_containers/syringe = 12, /obj/item/reagent_containers/dropper = 3, /obj/item/healthanalyzer = 4, @@ -50,13 +51,11 @@ /obj/item/storage/briefcase/medical = 2, /obj/item/plunger/reinforced = 2) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) - resistance_flags = FIRE_PROOF refill_canister = /obj/item/vending_refill/medical default_price = PRICE_ALMOST_CHEAP extra_price = PRICE_ABOVE_NORMAL payment_department = ACCOUNT_MED - cost_multiplier_per_dept = list(ACCOUNT_MED = 0) + light_mask = "med-light-mask" /obj/item/vending_refill/medical machine_name = "NanoMed Plus" @@ -64,8 +63,9 @@ /obj/machinery/vending/medical/syndicate_access name = "\improper SyndiMed Plus" - payment_department = NO_FREEBIES + req_access = list(ACCESS_SYNDICATE) -/obj/machinery/vending/medical/syndicate_access/Initialize() + +/obj/machinery/vending/medical/syndicate_access/Initialize(mapload) . = ..() cost_multiplier_per_dept = list("[ACCESS_SYNDICATE]" = 0) diff --git a/code/modules/vending/medical_wall.dm b/code/modules/vending/medical_wall.dm index 2d4c30080d..fdc062b2cb 100644 --- a/code/modules/vending/medical_wall.dm +++ b/code/modules/vending/medical_wall.dm @@ -18,14 +18,28 @@ contraband = list(/obj/item/reagent_containers/pill/tox = 2, /obj/item/reagent_containers/pill/morphine = 2) premium = list(/obj/item/reagent_containers/medspray/synthflesh = 2) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) - resistance_flags = FIRE_PROOF refill_canister = /obj/item/vending_refill/wallmed default_price = PRICE_FREE extra_price = PRICE_NORMAL payment_department = ACCOUNT_MED - cost_multiplier_per_dept = list(ACCOUNT_MED = 0) tiltable = FALSE + light_mask = "wallmed-light-mask" + +/obj/machinery/vending/wallmed/directional/north + dir = SOUTH + pixel_y = 32 + +/obj/machinery/vending/wallmed/directional/south + dir = NORTH + pixel_y = -32 + +/obj/machinery/vending/wallmed/directional/east + dir = WEST + pixel_x = 32 + +/obj/machinery/vending/wallmed/directional/west + dir = EAST + pixel_x = -32 /obj/item/vending_refill/wallmed machine_name = "NanoMed" diff --git a/code/modules/vending/megaseed.dm b/code/modules/vending/megaseed.dm index 45199298ca..790bf551b7 100644 --- a/code/modules/vending/megaseed.dm +++ b/code/modules/vending/megaseed.dm @@ -4,6 +4,7 @@ product_slogans = "THIS'S WHERE TH' SEEDS LIVE! GIT YOU SOME!;Hands down the best seed selection on the station!;Also certain mushroom varieties available, more for experts! Get certified today!" product_ads = "We like plants!;Grow some crops!;Grow, baby, growww!;Aw h'yeah son!" icon_state = "seeds" + light_mask = "seeds-light-mask" products = list(/obj/item/seeds/aloe = 3, /obj/item/seeds/ambrosia = 3, /obj/item/seeds/apple = 3, @@ -20,6 +21,7 @@ /obj/item/seeds/cotton = 3, /obj/item/seeds/corn = 3, /obj/item/seeds/eggplant = 3, + /obj/item/seeds/garlic = 3, /obj/item/seeds/grape = 3, /obj/item/seeds/grass = 3, /obj/item/seeds/lemon = 3, @@ -56,13 +58,11 @@ /obj/item/seeds/starthistle = 2, /obj/item/seeds/random = 2) premium = list(/obj/item/reagent_containers/spray/waterflower = 1) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) refill_canister = /obj/item/vending_refill/hydroseeds - resistance_flags = FIRE_PROOF default_price = PRICE_ALMOST_CHEAP extra_price = PRICE_NORMAL payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) /obj/item/vending_refill/hydroseeds - icon_state = "refill_hydro" + machine_name = "MegaSeed Servitor" + icon_state = "refill_plant" diff --git a/code/modules/vending/nutrimax.dm b/code/modules/vending/nutrimax.dm index 40ca06b78b..38bab5c599 100644 --- a/code/modules/vending/nutrimax.dm +++ b/code/modules/vending/nutrimax.dm @@ -5,6 +5,7 @@ product_ads = "We like plants!;Don't you want some?;The greenest thumbs ever.;We like big plants.;Soft soil..." icon_state = "nutri" icon_deny = "nutri-deny" + light_mask = "nutri-light-mask" products = list(/obj/item/reagent_containers/glass/bottle/nutrient/ez = 30, /obj/item/reagent_containers/glass/bottle/nutrient/l4z = 20, /obj/item/reagent_containers/glass/bottle/nutrient/rh = 10, @@ -15,14 +16,12 @@ /obj/item/shovel/spade = 3, /obj/item/plant_analyzer = 4) contraband = list(/obj/item/reagent_containers/glass/bottle/ammonia = 10, - /obj/item/reagent_containers/glass/bottle/diethylamine = 5) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) + /obj/item/reagent_containers/glass/bottle/diethylamine = 5) refill_canister = /obj/item/vending_refill/hydronutrients - resistance_flags = FIRE_PROOF default_price = PRICE_REALLY_CHEAP extra_price = PRICE_CHEAP payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) /obj/item/vending_refill/hydronutrients - icon_state = "refill_hydro" + machine_name = "NutriMax" + icon_state = "refill_plant" diff --git a/code/modules/vending/plasmaresearch.dm b/code/modules/vending/plasmaresearch.dm index bb16570ab3..2bd022eeb2 100644 --- a/code/modules/vending/plasmaresearch.dm +++ b/code/modules/vending/plasmaresearch.dm @@ -1,10 +1,10 @@ //This one's from bay12 /obj/machinery/vending/plasmaresearch - name = "\improper Toximate 3000" + name = "\improper Bombuddy 3000" desc = "All the fine parts you need in one vending machine!" products = list(/obj/item/clothing/under/rank/rnd/scientist = 6, - /obj/item/clothing/suit/bio_suit = 6, - /obj/item/clothing/head/bio_hood = 6, + /obj/item/clothing/suit/bio_suit = 6, + /obj/item/clothing/head/bio_hood = 6, /obj/item/transfer_valve = 6, /obj/item/assembly/timer = 6, /obj/item/assembly/signaler = 6, @@ -14,4 +14,3 @@ default_price = PRICE_EXPENSIVE extra_price = PRICE_REALLY_EXPENSIVE payment_department = ACCOUNT_SCI - cost_multiplier_per_dept = list(ACCOUNT_SCI = 0) diff --git a/code/modules/vending/robotics.dm b/code/modules/vending/robotics.dm index 2d77b2fc51..714f05a60d 100644 --- a/code/modules/vending/robotics.dm +++ b/code/modules/vending/robotics.dm @@ -4,6 +4,8 @@ desc = "All the tools you need to create your own robot army." icon_state = "robotics" icon_deny = "robotics-deny" + light_mask = "robotics-light-mask" + req_access = list(ACCESS_ROBOTICS) products = list(/obj/item/clothing/suit/toggle/labcoat = 4, /obj/item/clothing/under/rank/rnd/roboticist = 4, /obj/item/stack/cable_coil = 4, @@ -14,13 +16,16 @@ /obj/item/healthanalyzer = 3, /obj/item/scalpel = 2, /obj/item/circular_saw = 2, + /obj/item/bonesetter = 2, /obj/item/tank/internals/anesthetic = 2, /obj/item/clothing/mask/breath/medical = 5, /obj/item/screwdriver = 5, - /obj/item/crowbar = 6, + /obj/item/crowbar = 5, /obj/item/stack/medical/nanogel = 5) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) - resistance_flags = FIRE_PROOF + refill_canister = /obj/item/vending_refill/robotics default_price = PRICE_EXPENSIVE payment_department = ACCOUNT_SCI - cost_multiplier_per_dept = list(ACCOUNT_SCI = 0) + +/obj/item/vending_refill/robotics + machine_name = "Robotech Deluxe" + icon_state = "refill_engi" diff --git a/code/modules/vending/security.dm b/code/modules/vending/security.dm index c3540777ab..52fbd7dac4 100644 --- a/code/modules/vending/security.dm +++ b/code/modules/vending/security.dm @@ -1,9 +1,11 @@ /obj/machinery/vending/security name = "\improper SecTech" desc = "A security equipment vendor." - product_ads = "Crack capitalist skulls!;Beat some heads in!;Don't forget - harm is good!;Your weapons are right here.;Handcuffs!;Freeze, scumbag!;Don't tase me bro!;Tase them, bro.;Why not have a donut?" + product_ads = "Crack communist skulls!;Beat some heads in!;Don't forget - harm is good!;Your weapons are right here.;Handcuffs!;Freeze, scumbag!;Don't tase me bro!;Tase them, bro.;Why not have a donut?" icon_state = "sec" icon_deny = "sec-deny" + light_mask = "sec-light-mask" + req_access = list(ACCESS_SECURITY) products = list(/obj/item/restraints/handcuffs = 8, /obj/item/restraints/handcuffs/cable/zipties = 10, /obj/item/grenade/flashbang = 4, @@ -17,20 +19,17 @@ /obj/item/storage/fancy/donut_box = 2, /obj/item/storage/belt/sabre/secbelt = 1) premium = list(/obj/item/coin/antagtoken = 1, - /obj/item/clothing/head/helmet/blueshirt = 1, - /obj/item/clothing/suit/armor/vest/blueshirt = 1, + /obj/item/clothing/head/helmet/blueshirt = 1, + /obj/item/clothing/suit/armor/vest/blueshirt = 1, /obj/item/clothing/under/rank/security/officer/blueshirt = 1, - /obj/item/clothing/gloves/tackler = 5, - /obj/item/grenade/stingbang = 1, + /obj/item/clothing/gloves/tackler = 5, + /obj/item/grenade/stingbang = 1, /obj/item/ssword_kit = 1, /obj/item/storage/bag/ammo = 3) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) - resistance_flags = FIRE_PROOF refill_canister = /obj/item/vending_refill/security default_price = PRICE_ALMOST_EXPENSIVE extra_price = PRICE_REALLY_EXPENSIVE payment_department = ACCOUNT_SEC - cost_multiplier_per_dept = list(ACCOUNT_SEC = 0) /obj/machinery/vending/security/pre_throw(obj/item/I) if(istype(I, /obj/item/grenade)) @@ -42,4 +41,4 @@ F.update_brightness() /obj/item/vending_refill/security - icon_state = "refill_games" + icon_state = "refill_sec" diff --git a/code/modules/vending/snack.dm b/code/modules/vending/snack.dm index ff8fd46676..edd4802dc2 100644 --- a/code/modules/vending/snack.dm +++ b/code/modules/vending/snack.dm @@ -4,6 +4,7 @@ product_slogans = "Try our new nougat bar!;Twice the calories for half the price!" product_ads = "The healthiest!;Award-winning chocolate bars!;Mmm! So good!;Oh my god it's so juicy!;Have a snack.;Snacks are good for you!;Have some more Getmore!;Best quality snacks straight from mars.;We love chocolate!;Try our new jerky!" icon_state = "snack" + light_mask = "snack-light-mask" products = list(/obj/item/reagent_containers/food/snacks/candy = 5, /obj/item/reagent_containers/food/snacks/chocolatebar = 5, /obj/item/reagent_containers/food/drinks/dry_ramen = 5, @@ -25,13 +26,11 @@ /obj/item/reagent_containers/food/snacks/chococoin = 1, /obj/item/storage/box/marshmallow = 1, /obj/item/storage/box/donkpockets = 2) - refill_canister = /obj/item/vending_refill/snack canload_access_list = list(ACCESS_KITCHEN) default_price = PRICE_REALLY_CHEAP extra_price = PRICE_ALMOST_CHEAP payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) input_display_header = "Chef's Food Selection" /obj/item/vending_refill/snack diff --git a/code/modules/vending/sovietsoda.dm b/code/modules/vending/sovietsoda.dm index 88aba484e1..f28dbd7116 100644 --- a/code/modules/vending/sovietsoda.dm +++ b/code/modules/vending/sovietsoda.dm @@ -2,11 +2,17 @@ name = "\improper BODA" desc = "Old sweet water vending machine." icon_state = "sovietsoda" + light_mask = "soviet-light-mask" product_ads = "For Tsar and Country.;Have you fulfilled your nutrition quota today?;Very nice!;We are simple people, for this is all we eat.;If there is a person, there is a problem. If there is no person, then there is no problem." products = list(/obj/item/reagent_containers/food/drinks/drinkingglass/filled/soda = 30) contraband = list(/obj/item/reagent_containers/food/drinks/drinkingglass/filled/cola = 20) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) resistance_flags = FIRE_PROOF - default_price = PRICE_FREE - extra_price = PRICE_FREE + refill_canister = /obj/item/vending_refill/sovietsoda + default_price = 1 + extra_price = 2 //One credit for every state of FREEDOM payment_department = NO_FREEBIES + light_color = COLOR_PALE_ORANGE + +/obj/item/vending_refill/sovietsoda + machine_name = "BODA" + icon_state = "refill_cola" diff --git a/code/modules/vending/sustenance.dm b/code/modules/vending/sustenance.dm index 2dcc1e99f7..9370cf0c24 100644 --- a/code/modules/vending/sustenance.dm +++ b/code/modules/vending/sustenance.dm @@ -2,21 +2,24 @@ name = "\improper Sustenance Vendor" desc = "A vending machine which vends food, as required by section 47-C of the NT's Prisoner Ethical Treatment Agreement." product_slogans = "Enjoy your meal.;Enough calories to support strenuous labor." - product_ads = "Sufficiently healthy.;Efficiently produced tofu!;Mmm! So good!;Have a meal.;You need food to live!;Have some more candy corn!;Try our new ice cups!" + product_ads = "Sufficiently healthy.;Efficiently produced tofu!;Mmm! So good!;Have a meal.;You need food to live!;Even prisoners deserve their daily bread!;Have some more candy corn!;Try our new ice cups!" + light_mask = "snack-light-mask" icon_state = "sustenance" products = list(/obj/item/reagent_containers/food/snacks/tofu = 24, /obj/item/reagent_containers/food/drinks/ice/sustanance = 12, - /obj/item/reagent_containers/food/snacks/candy_corn = 6) + /obj/item/reagent_containers/food/snacks/candy_corn = 6 + ) contraband = list(/obj/item/kitchen/knife = 6, /obj/item/reagent_containers/food/drinks/coffee = 12, /obj/item/tank/internals/emergency_oxygen = 6, - /obj/item/clothing/mask/breath = 6) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) + /obj/item/clothing/mask/breath = 6 + ) + refill_canister = /obj/item/vending_refill/sustenance - resistance_flags = FIRE_PROOF default_price = PRICE_FREE extra_price = PRICE_FREE payment_department = NO_FREEBIES /obj/item/vending_refill/sustenance - icon_state = "refill_cook" + machine_name = "Sustenance Vendor" + icon_state = "refill_snack" diff --git a/code/modules/vending/toys.dm b/code/modules/vending/toys.dm index c5095ebff8..683f2d9f23 100644 --- a/code/modules/vending/toys.dm +++ b/code/modules/vending/toys.dm @@ -5,6 +5,7 @@ product_slogans = "Get your cool toys today!;Trigger a valid hunter today!;Quality toy weapons for cheap prices!;Give them to HoPs for all access!;Give them to HoS to get permabrigged!" product_ads = "Feel robust with your toys!;Express your inner child today!;Toy weapons don't kill people, but valid hunters do!;Who needs responsibilities when you have toy weapons?;Make your next murder FUN!" vend_reply = "Come back for more!" + light_mask = "donksoft-light-mask" circuit = /obj/item/circuitboard/machine/vending/donksofttoyvendor products = list( /obj/item/gun/ballistic/automatic/toy/unrestricted = 10, @@ -22,8 +23,6 @@ /obj/item/gun/ballistic/automatic/l6_saw/toy/unrestricted = 10, /obj/item/toy/katana = 10, /obj/item/dualsaber/toy = 5) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) - resistance_flags = FIRE_PROOF refill_canister = /obj/item/vending_refill/donksoft default_price = PRICE_ABOVE_NORMAL extra_price = PRICE_EXPENSIVE diff --git a/code/modules/vending/wardrobes.dm b/code/modules/vending/wardrobes.dm index eebb07938b..e1a5b69d72 100644 --- a/code/modules/vending/wardrobes.dm +++ b/code/modules/vending/wardrobes.dm @@ -6,6 +6,7 @@ extra_price = PRICE_EXPENSIVE payment_department = NO_FREEBIES input_display_header = "Returned Clothing" + light_mask = "wardrobe-light-mask" /obj/machinery/vending/wardrobe/canLoadItem(obj/item/I,mob/user) return (I.type in products) @@ -35,51 +36,11 @@ /obj/item/clothing/head/beret/sec/navyofficer = 5) refill_canister = /obj/item/vending_refill/wardrobe/sec_wardrobe payment_department = ACCOUNT_SEC - cost_multiplier_per_dept = list(ACCOUNT_SEC = 0) - default_price = PRICE_ABOVE_NORMAL - extra_price = PRICE_EXPENSIVE + light_color = COLOR_MOSTLY_PURE_RED /obj/item/vending_refill/wardrobe/sec_wardrobe machine_name = "SecDrobe" - -/obj/machinery/vending/wardrobe/det_wardrobe - name = "\improper DetDrobe" - desc = "A machine for all your detective needs, as long as you need clothes." - icon_state = "detdrobe" - product_ads = "Apply your brilliant deductive methods in style!" - vend_reply = "Thank you for using the DetDrobe!" - products = list(/obj/item/clothing/under/rank/security/detective = 2, - /obj/item/clothing/under/rank/security/detective/skirt = 2, - /obj/item/clothing/under/rank/security/detective/brown = 2, - /obj/item/clothing/under/rank/security/detective/brown/brown2 = 2, - /obj/item/clothing/under/rank/security/officer/blueshirt/seccorp/detcorp = 2, - /obj/item/clothing/under/rank/security/officer/util = 2, - /obj/item/clothing/shoes/sneakers/brown = 2, - /obj/item/clothing/suit/det_suit = 2, - /obj/item/clothing/head/fedora/det_hat = 2, - /obj/item/clothing/under/rank/security/detective/grey = 2, - /obj/item/clothing/under/rank/security/detective/grey/skirt = 2, - /obj/item/clothing/accessory/waistcoat = 2, - /obj/item/clothing/shoes/laceup = 2, - /obj/item/clothing/suit/det_suit/grey = 1, - /obj/item/clothing/suit/det_suit/forensicsred = 1, - /obj/item/clothing/suit/det_suit/forensicsred/long = 1, - /obj/item/clothing/suit/det_suit/forensicsblue = 1, - /obj/item/clothing/suit/det_suit/forensicsblue/long = 1, - /obj/item/clothing/head/fedora = 2, - /obj/item/clothing/gloves/color/black = 2, - /obj/item/clothing/gloves/color/latex = 2, - /obj/item/reagent_containers/food/drinks/flask/det = 2, - /obj/item/storage/fancy/cigarettes = 5) - premium = list(/obj/item/clothing/head/flatcap = 1) - refill_canister = /obj/item/vending_refill/wardrobe/det_wardrobe - extra_price = 350 - payment_department = ACCOUNT_SEC - -/obj/item/vending_refill/wardrobe/det_wardrobe - machine_name = "DetDrobe" - /obj/machinery/vending/wardrobe/medi_wardrobe name = "\improper MediDrobe" desc = "A vending machine rumoured to be capable of dispensing clothing for medical personnel." @@ -118,7 +79,6 @@ /obj/item/clothing/suit/toggle/labcoat/emt/highvis = 5) refill_canister = /obj/item/vending_refill/wardrobe/medi_wardrobe payment_department = ACCOUNT_MED - cost_multiplier_per_dept = list(ACCOUNT_MED = 0) /obj/item/vending_refill/wardrobe/medi_wardrobe machine_name = "MediDrobe" @@ -149,7 +109,7 @@ /obj/item/clothing/head/hardhat/weldhat = 3) refill_canister = /obj/item/vending_refill/wardrobe/engi_wardrobe payment_department = ACCOUNT_ENG - cost_multiplier_per_dept = list(ACCOUNT_ENG = 0) + light_color = COLOR_VIVID_YELLOW /obj/item/vending_refill/wardrobe/engi_wardrobe machine_name = "EngiDrobe" @@ -172,7 +132,7 @@ /obj/item/clothing/shoes/sneakers/black = 5) refill_canister = /obj/item/vending_refill/wardrobe/atmos_wardrobe payment_department = ACCOUNT_ENG - cost_multiplier_per_dept = list(ACCOUNT_ENG = 0) + light_color = COLOR_VIVID_YELLOW /obj/item/vending_refill/wardrobe/atmos_wardrobe machine_name = "AtmosDrobe" @@ -195,7 +155,6 @@ /obj/item/radio/headset/headset_cargo = 3) refill_canister = /obj/item/vending_refill/wardrobe/cargo_wardrobe payment_department = ACCOUNT_CAR - cost_multiplier_per_dept = list(ACCOUNT_CAR = 0) /obj/item/vending_refill/wardrobe/cargo_wardrobe machine_name = "CargoDrobe" @@ -223,17 +182,16 @@ /obj/item/clothing/under/misc/mechsuitblue = 1) contraband = list(/obj/item/clothing/suit/hooded/techpriest = 2) refill_canister = /obj/item/vending_refill/wardrobe/robo_wardrobe + extra_price = PRICE_EXPENSIVE * 1.2 payment_department = ACCOUNT_SCI - cost_multiplier_per_dept = list(ACCOUNT_SCI = 0) - /obj/item/vending_refill/wardrobe/robo_wardrobe machine_name = "RoboDrobe" /obj/machinery/vending/wardrobe/science_wardrobe name = "SciDrobe" - desc = "A simple vending machine suitable to dispense well tailored science clothing. Endorsed by Cubans." + desc = "A simple vending machine suitable to dispense well tailored science clothing. Endorsed by Space Cubans." icon_state = "scidrobe" - product_ads = "Longing for the smell of flesh plasma? Buy your science clothing now!;Made with 10% Auxetics, so you don't have to worry losing your arm!" + product_ads = "Longing for the smell of plasma burnt flesh? Buy your science clothing now!;Made with 10% Auxetics, so you don't have to worry about losing your arm!" vend_reply = "Thank you for using the SciDrobe!" products = list(/obj/item/clothing/accessory/pocketprotector = 5, /obj/item/clothing/head/beret/sci = 3, @@ -251,8 +209,6 @@ /obj/item/clothing/mask/gas = 5) refill_canister = /obj/item/vending_refill/wardrobe/science_wardrobe payment_department = ACCOUNT_SCI - cost_multiplier_per_dept = list(ACCOUNT_SCI = 0) - /obj/item/vending_refill/wardrobe/science_wardrobe machine_name = "SciDrobe" @@ -274,7 +230,7 @@ /obj/item/clothing/mask/bandana = 4) refill_canister = /obj/item/vending_refill/wardrobe/hydro_wardrobe payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) + light_color = LIGHT_COLOR_ELECTRIC_GREEN /obj/item/vending_refill/wardrobe/hydro_wardrobe machine_name = "HyDrobe" @@ -299,9 +255,7 @@ /obj/item/clothing/glasses/regular/jamjar = 1, /obj/item/storage/bag/books = 1) refill_canister = /obj/item/vending_refill/wardrobe/curator_wardrobe - payment_department = ACCOUNT_CIV - cost_multiplier_per_dept = list(ACCOUNT_CIV = 0) - + payment_department = ACCOUNT_SRV /obj/item/vending_refill/wardrobe/curator_wardrobe machine_name = "CuraDrobe" @@ -331,8 +285,6 @@ /obj/item/storage/belt/bandolier = 1) refill_canister = /obj/item/vending_refill/wardrobe/bar_wardrobe payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) - /obj/item/vending_refill/wardrobe/bar_wardrobe machine_name = "BarDrobe" @@ -340,8 +292,8 @@ name = "ChefDrobe" desc = "This vending machine might not dispense meat, but it certainly dispenses chef related clothing." icon_state = "chefdrobe" - product_ads = "Our clothes are guaranteed to protect you from food splatters!;Now stocking recipe books!" - vend_reply = "Thank you for using the ChefDrobe!;Just like your grandmother's old recipes!" + product_ads = "Our clothes are guaranteed to protect you from food splatters!" + vend_reply = "Thank you for using the ChefDrobe!" products = list(/obj/item/clothing/under/suit/waiter = 3, /obj/item/radio/headset/headset_srv = 4, /obj/item/clothing/accessory/waistcoat = 3, @@ -358,8 +310,6 @@ /obj/item/book/granter/crafting_recipe/coldcooking = 2) refill_canister = /obj/item/vending_refill/wardrobe/chef_wardrobe payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) - /obj/item/vending_refill/wardrobe/chef_wardrobe machine_name = "ChefDrobe" @@ -390,8 +340,10 @@ /obj/item/screwdriver = 2, /obj/item/stack/cable_coil/random = 4) refill_canister = /obj/item/vending_refill/wardrobe/jani_wardrobe + default_price = PRICE_CHEAP + extra_price = PRICE_EXPENSIVE * 0.8 payment_department = ACCOUNT_SRV - cost_multiplier_per_dept = list(ACCOUNT_SRV = 0) + light_color = COLOR_STRONG_MAGENTA /obj/item/vending_refill/wardrobe/jani_wardrobe machine_name = "JaniDrobe" @@ -423,18 +375,16 @@ /obj/item/clothing/shoes/laceup = 3, /obj/item/clothing/accessory/lawyers_badge = 3) refill_canister = /obj/item/vending_refill/wardrobe/law_wardrobe - payment_department = ACCOUNT_CIV - cost_multiplier_per_dept = list(ACCOUNT_CIV = 0) - + payment_department = ACCOUNT_SRV /obj/item/vending_refill/wardrobe/law_wardrobe machine_name = "LawDrobe" /obj/machinery/vending/wardrobe/chap_wardrobe - name = "ChapDrobe" - desc = "This most blessed and holy machine vends clothing only suitable for chaplains to gaze upon." + name = "DeusVend" + desc = "God wills your purchase." icon_state = "chapdrobe" product_ads = "Are you being bothered by cultists or pesky revenants? Then come and dress like the holy man!;Clothes for men of the cloth!" - vend_reply = "Thank you for using the ChapDrobe!" + vend_reply = "Thank you for using the DeusVend!" products = list(/obj/item/choice_beacon/holy = 1, /obj/item/storage/backpack/cultpack = 2, /obj/item/clothing/accessory/pocketprotector/cosmetology = 2, @@ -451,11 +401,9 @@ premium = list(/obj/item/toy/plush/plushvar = 1, /obj/item/toy/plush/narplush = 1) refill_canister = /obj/item/vending_refill/wardrobe/chap_wardrobe - payment_department = ACCOUNT_CIV - cost_multiplier_per_dept = list(ACCOUNT_CIV = 0) - + payment_department = ACCOUNT_SRV /obj/item/vending_refill/wardrobe/chap_wardrobe - machine_name = "ChapDrobe" + machine_name = "DeusVend" /obj/machinery/vending/wardrobe/chem_wardrobe name = "ChemDrobe" @@ -475,8 +423,6 @@ /obj/item/fermichem/pHbooklet = 3) refill_canister = /obj/item/vending_refill/wardrobe/chem_wardrobe payment_department = ACCOUNT_MED - cost_multiplier_per_dept = list(ACCOUNT_MED = 0) - /obj/item/vending_refill/wardrobe/chem_wardrobe machine_name = "ChemDrobe" @@ -494,9 +440,7 @@ /obj/item/storage/backpack/genetics = 3, /obj/item/storage/backpack/satchel/gen = 3) refill_canister = /obj/item/vending_refill/wardrobe/gene_wardrobe - payment_department = ACCOUNT_MED - cost_multiplier_per_dept = list(ACCOUNT_MED = 0) - + payment_department = ACCOUNT_SCI /obj/item/vending_refill/wardrobe/gene_wardrobe machine_name = "GeneDrobe" @@ -517,11 +461,46 @@ /obj/item/storage/backpack/satchel/vir = 3) refill_canister = /obj/item/vending_refill/wardrobe/viro_wardrobe payment_department = ACCOUNT_MED - cost_multiplier_per_dept = list(ACCOUNT_MED = 0) - /obj/item/vending_refill/wardrobe/viro_wardrobe machine_name = "ViroDrobe" +/obj/machinery/vending/wardrobe/det_wardrobe + name = "\improper DetDrobe" + desc = "A machine for all your detective needs, as long as you need clothes." + icon_state = "detdrobe" + product_ads = "Apply your brilliant deductive methods in style!" + vend_reply = "Thank you for using the DetDrobe!" + products = list(/obj/item/clothing/under/rank/security/detective = 2, + /obj/item/clothing/under/rank/security/detective/skirt = 2, + /obj/item/clothing/under/rank/security/detective/brown = 2, + /obj/item/clothing/under/rank/security/detective/brown/brown2 = 2, + /obj/item/clothing/under/rank/security/officer/blueshirt/seccorp/detcorp = 2, + /obj/item/clothing/under/rank/security/officer/util = 2, + /obj/item/clothing/shoes/sneakers/brown = 2, + /obj/item/clothing/suit/det_suit = 2, + /obj/item/clothing/head/fedora/det_hat = 2, + /obj/item/clothing/under/rank/security/detective/grey = 2, + /obj/item/clothing/under/rank/security/detective/grey/skirt = 2, + /obj/item/clothing/accessory/waistcoat = 2, + /obj/item/clothing/shoes/laceup = 2, + /obj/item/clothing/suit/det_suit/grey = 1, + /obj/item/clothing/suit/det_suit/forensicsred = 1, + /obj/item/clothing/suit/det_suit/forensicsred/long = 1, + /obj/item/clothing/suit/det_suit/forensicsblue = 1, + /obj/item/clothing/suit/det_suit/forensicsblue/long = 1, + /obj/item/clothing/head/fedora = 2, + /obj/item/clothing/gloves/color/black = 2, + /obj/item/clothing/gloves/color/latex = 2, + /obj/item/reagent_containers/food/drinks/flask/det = 2, + /obj/item/storage/fancy/cigarettes = 5) + premium = list(/obj/item/clothing/head/flatcap = 1) + refill_canister = /obj/item/vending_refill/wardrobe/det_wardrobe + extra_price = PRICE_EXPENSIVE * 1.75 + payment_department = ACCOUNT_SEC + +/obj/item/vending_refill/wardrobe/det_wardrobe + machine_name = "DetDrobe" + /obj/machinery/vending/wardrobe/cap_wardrobe name = "Captain's Wardrobe" desc = "The latest and greatest in Nanotrasen fashion for your great leader." @@ -552,10 +531,6 @@ default_price = PRICE_ALMOST_EXPENSIVE extra_price = PRICE_ABOVE_EXPENSIVE -/obj/machinery/vending/wardrobe/cap_wardrobe/Initialize() - . = ..() - cost_multiplier_per_dept = list("[ACCESS_CAPTAIN]" = 0) - /obj/item/vending_refill/wardrobe/cap_wardrobe machine_name = "Captain's Wardrobe" icon_state = "refill_caps" diff --git a/code/modules/vending/youtool.dm b/code/modules/vending/youtool.dm index 2119197aed..4b8a8c27e5 100644 --- a/code/modules/vending/youtool.dm +++ b/code/modules/vending/youtool.dm @@ -3,6 +3,7 @@ desc = "Tools for tools." icon_state = "tool" icon_deny = "tool-deny" + light_mask = "tool-light-mask" products = list(/obj/item/stack/cable_coil/random = 15, /obj/item/crowbar = 10, /obj/item/weldingtool = 6, @@ -19,13 +20,11 @@ /obj/item/multitool = 2) premium = list(/obj/item/clothing/gloves/color/yellow = 2, /obj/item/weldingtool/hugetank = 2) - armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 70) - refill_canister = /obj/item/vending_refill/tool - resistance_flags = FIRE_PROOF + refill_canister = /obj/item/vending_refill/youtool default_price = PRICE_REALLY_CHEAP extra_price = PRICE_EXPENSIVE payment_department = ACCOUNT_ENG - cost_multiplier_per_dept = list(ACCOUNT_ENG = 0) -/obj/item/vending_refill/tool +/obj/item/vending_refill/youtool + machine_name = "YouTool" icon_state = "refill_engi" diff --git a/code/modules/wiremod/components/abstract/compare.dm b/code/modules/wiremod/components/abstract/compare.dm new file mode 100644 index 0000000000..ef6ba171b7 --- /dev/null +++ b/code/modules/wiremod/components/abstract/compare.dm @@ -0,0 +1,58 @@ +/** + * # Compare Component + * + * Abstract component to build conditional components + */ +/obj/item/circuit_component/compare + display_name = "Compare" + + /// The amount of input ports to have + var/input_port_amount = 4 + + /// The trigger for the true/false signals + var/datum/port/input/compare + + /// Signals sent on compare + var/datum/port/output/true + var/datum/port/output/false + + /// The result from the output + var/datum/port/output/result + + var/list/datum/port/input/compare_ports = list() + +/obj/item/circuit_component/compare/Initialize() + . = ..() + for(var/port_id in 1 to input_port_amount) + var/letter = ascii2text(text2ascii("A") + (port_id-1)) + compare_ports += add_input_port(letter, PORT_TYPE_ANY) + + load_custom_ports() + compare = add_input_port("Compare", PORT_TYPE_SIGNAL) + + true = add_output_port("True", PORT_TYPE_SIGNAL) + false = add_output_port("False", PORT_TYPE_SIGNAL) + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/** + * Used by derivatives to load their own ports in for custom use. + */ +/obj/item/circuit_component/compare/proc/load_custom_ports() + return + +/obj/item/circuit_component/compare/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/logic_result = do_comparisons(compare_ports) + if(COMPONENT_TRIGGERED_BY(compare, port)) + if(logic_result) + true.set_output(COMPONENT_SIGNAL) + else + false.set_output(COMPONENT_SIGNAL) + result.set_output(logic_result) + +/// Do the comparisons and return a result +/obj/item/circuit_component/compare/proc/do_comparisons(list/ports) + return FALSE diff --git a/code/modules/wiremod/components/abstract/module.dm b/code/modules/wiremod/components/abstract/module.dm new file mode 100644 index 0000000000..a3c04b2a11 --- /dev/null +++ b/code/modules/wiremod/components/abstract/module.dm @@ -0,0 +1,306 @@ +/** + * # Module Component + * + * A component that has an input, output + */ +/obj/item/circuit_component/module + display_name = "Module" + desc = "A component that has other components within it, acting like a function. Use it in your hand to control the amount of input and output ports it has, as well as being able to access the integrated circuit contained inside." + + var/obj/item/integrated_circuit/module/internal_circuit + + var/obj/item/circuit_component/module_input/input_component + var/obj/item/circuit_component/module_output/output_component + + /// Linked ports that follow a `first_port = second_port` keyed structure. + var/list/linked_ports = list() + + var/port_limit = 10 + +/obj/item/integrated_circuit/module + var/obj/item/circuit_component/module/attached_module + +/obj/item/integrated_circuit/module/ui_host(mob/user) + . = ..() + if(. == src) + return attached_module + +/obj/item/integrated_circuit/module/set_display_name(new_name) + . = ..() + attached_module.display_name = new_name + attached_module.name = "module ([new_name])" + +/obj/item/integrated_circuit/module/load_component(type) + if(!attached_module) + return ..() + + if(ispath(type, /obj/item/circuit_component/module_input)) + return attached_module.input_component + + if(ispath(type, /obj/item/circuit_component/module_output)) + return attached_module.output_component + + return ..() + +/obj/item/integrated_circuit/module/Destroy() + attached_module = null + return ..() + +/obj/item/circuit_component/module_input + display_name = "Input" + desc = "A component that receives data from the module it is attached to" + + removable = FALSE + + /// The currently attached module + var/obj/item/circuit_component/module/attached_module + +/obj/item/circuit_component/module_input/Destroy() + attached_module = null + return ..() + +/obj/item/circuit_component/module_output + display_name = "Output" + desc = "A component that outputs data to the module it is attached to." + + removable = FALSE + + /// The currently attached module + var/obj/item/circuit_component/module/attached_module + +/obj/item/circuit_component/module_output/input_received(datum/port/input/port) + . = ..() + if(!port) + return + // We don't check the parent here because frankly, we don't care. We only sync our input with the module's output + var/datum/port/output/port_to_update = attached_module.linked_ports[port] + if(!port_to_update) + CRASH("[port.type] doesn't have a linked port in [type]!") + + port_to_update.set_output(port.value) + +/obj/item/circuit_component/module/input_received(datum/port/input/port) + . = ..() + if(!port) + return + var/datum/port/output/port_to_update = linked_ports[port] + if(!port_to_update) + CRASH("[port.type] doesn't have a linked port in [type]!") + + port_to_update.set_output(port.value) + +/obj/item/circuit_component/module_output/Destroy() + attached_module = null + return ..() + +/obj/item/circuit_component/module/Initialize() + . = ..() + internal_circuit = new(src) + internal_circuit.attached_module = src + + input_component = new(internal_circuit) + input_component.attached_module = src + internal_circuit.add_component(input_component) + input_component.rel_x = 0 + input_component.rel_y = 200 + + output_component = new(internal_circuit) + output_component.attached_module = src + internal_circuit.add_component(output_component) + output_component.rel_x = 400 + output_component.rel_y = 200 + +/obj/item/circuit_component/module/save_data_to_list(list/component_data) + . = ..() + component_data["integrated_circuit"] = internal_circuit.convert_to_json() + + var/list/input_data = list() + for(var/datum/port/input/input_port as anything in input_ports) + input_data += list(list( + "name" = input_port.name, + "type" = input_port.datatype, + )) + + var/list/output_data = list() + for(var/datum/port/output/output_port as anything in output_ports) + output_data += list(list( + "name" = output_port.name, + "type" = output_port.datatype, + )) + + component_data["input_ports"] = input_data + component_data["output_ports"] = output_data + +/obj/item/circuit_component/module/load_data_from_list(list/component_data) + . = ..() + + var/list/input_ports = component_data["input_ports"] + for(var/list/port_data as anything in input_ports) + add_and_link_input_port(port_data["name"], port_data["type"]) + + var/list/output_ports = component_data["output_ports"] + for(var/list/port_data as anything in output_ports) + add_and_link_output_port(port_data["name"], port_data["type"]) + + if(component_data["integrated_circuit"]) + internal_circuit.load_circuit_data(component_data["integrated_circuit"]) + +/obj/item/circuit_component/module/proc/add_and_link_input_port(name, type) + var/datum/port/new_port = add_input_port(name, type) + linked_ports[new_port] = input_component.add_output_port(name, type) + +/obj/item/circuit_component/module/proc/add_and_link_output_port(name, type) + var/datum/port/new_port = output_component.add_input_port(name, type) + linked_ports[new_port] = add_output_port(name, type) + +/obj/item/circuit_component/module/add_to(obj/item/integrated_circuit/added_to) + . = ..() + RegisterSignal(added_to, COMSIG_CIRCUIT_SET_CELL, .proc/handle_set_cell) + RegisterSignal(added_to, COMSIG_CIRCUIT_SET_ON, .proc/handle_set_on) + RegisterSignal(added_to, COMSIG_CIRCUIT_SET_SHELL, .proc/handle_set_shell) + internal_circuit.set_cell(added_to.cell) + internal_circuit.set_shell(added_to.shell) + internal_circuit.set_on(added_to.on) + + +/obj/item/circuit_component/module/removed_from(obj/item/integrated_circuit/removed_from) + internal_circuit.set_cell(null) + internal_circuit.set_on(FALSE) + internal_circuit.remove_current_shell() + UnregisterSignal(removed_from, list( + COMSIG_CIRCUIT_SET_CELL, + COMSIG_CIRCUIT_SET_ON, + COMSIG_CIRCUIT_SET_SHELL, + )) + return ..() + +/obj/item/circuit_component/module/proc/handle_set_cell(datum/source, obj/item/stock_parts/cell/cell) + SIGNAL_HANDLER + internal_circuit.set_cell(cell) + +/obj/item/circuit_component/module/proc/handle_set_on(datum/source, new_value) + SIGNAL_HANDLER + internal_circuit.set_on(new_value) + +/obj/item/circuit_component/module/proc/handle_set_shell(datum/source, atom/movable/new_shell) + SIGNAL_HANDLER + internal_circuit.set_shell(new_shell) + +/obj/item/circuit_component/module/Destroy() + QDEL_NULL(input_component) + QDEL_NULL(output_component) + QDEL_NULL(internal_circuit) + linked_ports = null + return ..() + +/obj/item/circuit_component/module/ui_data(mob/user) + . = list() + .["input_ports"] = list() + for(var/datum/port/input/input_port as anything in input_ports) + .["input_ports"] += list(list( + "name" = input_port.name, + "type" = input_port.datatype, + )) + + .["output_ports"] = list() + for(var/datum/port/output/output_port as anything in output_ports) + .["output_ports"] += list(list( + "name" = output_port.name, + "type" = output_port.datatype, + )) + +/obj/item/circuit_component/module/ui_static_data(mob/user) + . = list() + .["global_port_types"] = GLOB.wiremod_basic_types + +/obj/item/circuit_component/module/attackby(obj/item/I, mob/living/user, params) + if(istype(I, /obj/item/circuit_component)) + internal_circuit.attackby(I, user, params) + return + return ..() + +#define WITHIN_RANGE(id, table) (id >= 1 && id <= length(table)) + +/obj/item/circuit_component/module/ui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("open_internal_circuit") + internal_circuit.interact(usr) + . = TRUE + if("add_input_port") + if(length(input_ports) > port_limit) + return + add_and_link_input_port("Input Port", PORT_TYPE_ANY) + . = TRUE + if("remove_input_port") + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(port_id, input_ports)) + return + var/datum/port/removed_port = input_ports[port_id] + linked_ports -= removed_port + remove_input_port(removed_port) + input_component.remove_output_port(input_component.output_ports[port_id]) + . = TRUE + if("add_output_port") + if(length(output_ports) > port_limit) + return + add_and_link_output_port("Output Port", PORT_TYPE_ANY) + . = TRUE + if("remove_output_port") + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(port_id, output_ports)) + return + + var/datum/port/removed_port = output_component.input_ports[port_id] + linked_ports -= removed_port + remove_output_port(output_ports[port_id]) + output_component.remove_input_port(removed_port) + . = TRUE + if("set_port_name", "set_port_type") + var/port_id = text2num(params["port_id"]) + var/is_input = params["is_input"] + + var/list/ports_to_use + var/list/internal_ports_to_use + if(is_input) + ports_to_use = input_ports + internal_ports_to_use = input_component.output_ports + else + ports_to_use = output_ports + internal_ports_to_use = output_component.input_ports + + if(!WITHIN_RANGE(port_id, ports_to_use)) + return + + var/datum/port/component_port = ports_to_use[port_id] + var/datum/port/internal_component_port = internal_ports_to_use[port_id] + + if(action == "set_port_type") + var/type = params["port_type"] + if(!(type in GLOB.wiremod_basic_types)) + return + component_port.set_datatype(type) + internal_component_port.set_datatype(type) + else + var/port_name = params["port_name"] + if(!port_name) + return + port_name = strip_html(port_name, PORT_MAX_NAME_LENGTH) + component_port.name = port_name + internal_component_port.name = port_name + . = TRUE + + if(.) + SStgui.update_uis(internal_circuit) + +#undef WITHIN_RANGE + +/obj/item/circuit_component/module/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "CircuitModule", name) + ui.open() + ui.set_autoupdate(FALSE) diff --git a/code/modules/wiremod/components/action/light.dm b/code/modules/wiremod/components/action/light.dm new file mode 100644 index 0000000000..14427eae0e --- /dev/null +++ b/code/modules/wiremod/components/action/light.dm @@ -0,0 +1,68 @@ +/** + * # Light Component + * + * Emits a light of a specific brightness and colour. Requires a shell. + */ +/obj/item/circuit_component/light + display_name = "Light" + desc = "A component that emits a light of a specific brightness and colour. Requires a shell." + + /// The colours of the light + var/datum/port/input/red + var/datum/port/input/green + var/datum/port/input/blue + + /// The brightness + var/datum/port/input/brightness + + /// Whether the light is on or not + var/datum/port/input/on + + var/max_power = 5 + var/min_lightness = 0.4 + var/shell_light_color + +/obj/item/circuit_component/light/get_ui_notices() + . = ..() + . += create_ui_notice("Maximum Brightness: [max_power]", "orange", "lightbulb") + +/obj/item/circuit_component/light/Initialize() + . = ..() + red = add_input_port("Red", PORT_TYPE_NUMBER) + green = add_input_port("Green", PORT_TYPE_NUMBER) + blue = add_input_port("Blue", PORT_TYPE_NUMBER) + brightness = add_input_port("Brightness", PORT_TYPE_NUMBER) + + on = add_input_port("On", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/light/register_shell(atom/movable/shell) + . = ..() + TRIGGER_CIRCUIT_COMPONENT(src, null) + +/obj/item/circuit_component/light/unregister_shell(atom/movable/shell) + shell.set_light_on(FALSE) + return ..() + +/obj/item/circuit_component/light/input_received(datum/port/input/port) + . = ..() + brightness.set_value(clamp(brightness.value || 0, 0, max_power)) + red.set_value(clamp(red.value, 0, 255)) + blue.set_value(clamp(blue.value, 0, 255)) + green.set_value(clamp(green.value, 0, 255)) + var/list/hsl = rgb2hsl(red.value || 0, green.value || 0, blue.value || 0) + var/list/light_col = hsl2rgb(hsl[1], hsl[2], max(min_lightness, hsl[3])) + shell_light_color = rgb(light_col[1], light_col[2], light_col[3]) + if(.) + return + + if(parent.shell) + set_atom_light(parent.shell) + +/obj/item/circuit_component/light/proc/set_atom_light(atom/movable/target_atom) + // Clamp anyways just for safety + var/bright_val = min(max(brightness.value || 0, 0), max_power) + + target_atom.set_light_power(bright_val) + target_atom.set_light_range(bright_val) + target_atom.set_light_color(shell_light_color) + target_atom.set_light_on(!!on.value) diff --git a/code/modules/wiremod/components/action/mmi.dm b/code/modules/wiremod/components/action/mmi.dm new file mode 100644 index 0000000000..9c242059a6 --- /dev/null +++ b/code/modules/wiremod/components/action/mmi.dm @@ -0,0 +1,175 @@ +/** + * # Man-Machine Interface Component + * + * Allows an MMI to be inserted into a shell, allowing it to be linked up. Requires a shell. + */ +/obj/item/circuit_component/mmi + display_name = "Man-Machine Interface" + desc = "A component that allows MMI to enter shells to send output signals." + + /// The message to send to the MMI in the shell. + var/datum/port/input/message + /// Sends the current MMI a message + var/datum/port/input/send + /// Ejects the current MMI + var/datum/port/input/eject + + /// Called when the MMI tries moving north + var/datum/port/output/north + /// Called when the MMI tries moving east + var/datum/port/output/east + /// Called when the MMI tries moving south + var/datum/port/output/south + /// Called when the MMI tries moving west + var/datum/port/output/west + + /// Returns what the MMI last clicked on. + var/datum/port/output/clicked_atom + /// Called when the MMI clicks. + var/datum/port/output/attack + /// Called when the MMI right clicks. + var/datum/port/output/secondary_attack + + /// The current MMI card + var/obj/item/mmi/brain + + /// Maximum length of the message that can be sent to the MMI + var/max_length = 300 + +/obj/item/circuit_component/mmi/Initialize() + . = ..() + message = add_input_port("Message", PORT_TYPE_STRING) + send = add_input_port("Send Message", PORT_TYPE_SIGNAL) + eject = add_input_port("Eject", PORT_TYPE_SIGNAL) + + north = add_output_port("North", PORT_TYPE_SIGNAL) + east = add_output_port("East", PORT_TYPE_SIGNAL) + south = add_output_port("South", PORT_TYPE_SIGNAL) + west = add_output_port("West", PORT_TYPE_SIGNAL) + + attack = add_output_port("Attack", PORT_TYPE_SIGNAL) + secondary_attack = add_output_port("Secondary Attack", PORT_TYPE_SIGNAL) + clicked_atom = add_output_port("Target Entity", PORT_TYPE_ATOM) + +/obj/item/circuit_component/mmi/Destroy() + remove_current_brain() + return ..() + +/obj/item/circuit_component/mmi/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!brain) + return + + if(COMPONENT_TRIGGERED_BY(eject, port)) + remove_current_brain() + if(COMPONENT_TRIGGERED_BY(send, port)) + if(!message.value) + return + + var/msg_str = copytext(html_encode(message.value), 1, max_length) + + var/mob/living/target = brain.brainmob + if(!target) + return + + to_chat(target, "[span_bold("You hear a message in your ear: ")][msg_str]") + + +/obj/item/circuit_component/mmi/register_shell(atom/movable/shell) + . = ..() + RegisterSignal(shell, COMSIG_PARENT_ATTACKBY, .proc/handle_attack_by) + +/obj/item/circuit_component/mmi/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_PARENT_ATTACKBY) + remove_current_brain() + return ..() + +/obj/item/circuit_component/mmi/proc/handle_attack_by(atom/movable/shell, obj/item/item, mob/living/attacker) + SIGNAL_HANDLER + if(istype(item, /obj/item/mmi)) + var/obj/item/mmi/target_mmi = item + if(!target_mmi.brainmob) + return + add_mmi(item) + return COMPONENT_NO_AFTERATTACK + +/obj/item/circuit_component/mmi/proc/add_mmi(obj/item/mmi/to_add) + remove_current_brain() + + to_add.forceMove(src) + if(to_add.brainmob) + update_mmi_mob(to_add, null, to_add.brainmob) + brain = to_add + RegisterSignal(to_add, COMSIG_PARENT_QDELETING, .proc/remove_current_brain) + RegisterSignal(to_add, COMSIG_MOVABLE_MOVED, .proc/mmi_moved) + +/obj/item/circuit_component/mmi/proc/mmi_moved(atom/movable/mmi) + SIGNAL_HANDLER + + if(mmi.loc != src) + remove_current_brain() + +/obj/item/circuit_component/mmi/proc/remove_current_brain() + SIGNAL_HANDLER + if(!brain) + return + + if(brain.brainmob) + update_mmi_mob(brain, brain.brainmob) + UnregisterSignal(brain, list( + COMSIG_PARENT_QDELETING, + COMSIG_MOVABLE_MOVED + )) + if(brain.loc == src) + brain.forceMove(drop_location()) + brain = null + +/obj/item/circuit_component/mmi/proc/update_mmi_mob(datum/source, mob/living/old_mmi, mob/living/new_mmi) + SIGNAL_HANDLER + if(old_mmi) + old_mmi.remote_control = null + UnregisterSignal(old_mmi, COMSIG_MOB_CLICKON) + if(new_mmi) + new_mmi.remote_control = src + RegisterSignal(new_mmi, COMSIG_MOB_CLICKON, .proc/handle_mmi_attack) + +/obj/item/circuit_component/mmi/relaymove(mob/living/user, direct) + if(user != brain.brainmob) + return ..() + + if(direct & NORTH) + north.set_output(COMPONENT_SIGNAL) + if(direct & WEST) + west.set_output(COMPONENT_SIGNAL) + if(direct & EAST) + east.set_output(COMPONENT_SIGNAL) + if(direct & SOUTH) + south.set_output(COMPONENT_SIGNAL) + + return TRUE + +/obj/item/circuit_component/mmi/proc/handle_mmi_attack(mob/living/source, atom/target, list/mods) + SIGNAL_HANDLER + var/list/modifiers = params2list(mods) + if(modifiers[RIGHT_CLICK]) + clicked_atom.set_output(target) + secondary_attack.set_output(COMPONENT_SIGNAL) + . = COMSIG_MOB_CANCEL_CLICKON + else if(modifiers[LEFT_CLICK] && !modifiers[SHIFT_CLICK] && !modifiers[ALT_CLICK] && !modifiers[CTRL_CLICK]) + clicked_atom.set_output(target) + attack.set_output(COMPONENT_SIGNAL) + . = COMSIG_MOB_CANCEL_CLICKON + +/obj/item/circuit_component/mmi/add_to(obj/item/integrated_circuit/add_to) + . = ..() + if(HAS_TRAIT(add_to, TRAIT_COMPONENT_MMI)) + return FALSE + ADD_TRAIT(add_to, TRAIT_COMPONENT_MMI, src) + +/obj/item/circuit_component/mmi/removed_from(obj/item/integrated_circuit/removed_from) + REMOVE_TRAIT(removed_from, TRAIT_COMPONENT_MMI, src) + remove_current_brain() + return ..() diff --git a/code/modules/wiremod/components/action/pathfind.dm b/code/modules/wiremod/components/action/pathfind.dm new file mode 100644 index 0000000000..10856e0de8 --- /dev/null +++ b/code/modules/wiremod/components/action/pathfind.dm @@ -0,0 +1,113 @@ +/** + * # Pathfinding component + * + * Calcualtes a path, returns a list of entities. Each entity is the next step in the path. Can be used with the direction component to move. + */ +/obj/item/circuit_component/pathfind + display_name = "Pathfinder" + desc = "When triggered, the next step to the target's location as an entity. This can be used with the direction component and the drone shell to make it move on its own. The Id Card input port is for considering ID access when pathing, it does not give the shell actual access." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/input_X + var/datum/port/input/input_Y + var/datum/port/input/id_card + + var/datum/port/output/output + var/datum/port/output/finished + var/datum/port/output/failed + var/datum/port/output/reason_failed + + var/list/path + var/turf/old_dest + var/turf/next_turf + + // Cooldown to limit how frequently we can path to the same location. + var/same_path_cooldown = 5 SECONDS + var/different_path_cooldown = 30 SECONDS + + var/max_range = 60 + +/obj/item/circuit_component/pathfind/get_ui_notices() + . = ..() + // Not necessary to show the same path cooldown, since it doesn't change much for the player + . += create_ui_notice("Pathfinding Cooldown: [DisplayTimeText(different_path_cooldown)]", "orange", "stopwatch") + . += create_ui_notice("Maximum Range: [max_range] tiles", "orange", "info") + +/obj/item/circuit_component/pathfind/Initialize() + . = ..() + input_X = add_input_port("Target X", PORT_TYPE_NUMBER, FALSE) + input_Y = add_input_port("Target Y", PORT_TYPE_NUMBER, FALSE) + id_card = add_input_port("ID Card", PORT_TYPE_ATOM, FALSE) + + output = add_output_port("Next step", PORT_TYPE_ATOM) + finished = add_output_port("Arrived to destination", PORT_TYPE_SIGNAL) + failed = add_output_port("Failed", PORT_TYPE_SIGNAL) + reason_failed = add_output_port("Fail reason", PORT_TYPE_STRING) + +/obj/item/circuit_component/pathfind/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/target_X = input_X.value + if(isnull(target_X)) + return + + var/target_Y = input_Y.value + if(isnull(target_Y)) + return + + var/atom/path_id = id_card.value + if(path_id && !istype(path_id, /obj/item/card/id)) + path_id = null + failed.set_output(COMPONENT_SIGNAL) + reason_failed.set_output("Object marked is not an ID! Using no ID instead.") + + // Get both the current turf and the destination's turf + var/turf/current_turf = get_turf(src) + var/turf/destination = locate(target_X, target_Y, current_turf?.z) + + // We're already here! No need to do anything. + if(current_turf == destination) + finished.set_output(COMPONENT_SIGNAL) + old_dest = null + TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME) + next_turf = null + return + + // If we're going to the same place and the cooldown hasn't subsided, we're probably on the same path as before + if (destination == old_dest && TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME)) + + // Check if the current turf is the same as the current turf we're supposed to be in. If so, then we set the next step as the next turf on the list + if(current_turf == next_turf) + popleft(path) + next_turf = get_turf(path[1]) + output.set_output(next_turf) + + // Restart the cooldown since we don't need a new path ( TIMER_COOLDOWN_START might restart the timer by itself and i dont need to call TIMER_COOLDOWN_END, but better safe than sorry ) + TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME, same_path_cooldown) + + + else // Either we're not going to the same place or the cooldown is over. Either way, we need a new path + + if(destination != old_dest && TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_PATHFIND_DIF)) + failed.set_output(COMPONENT_SIGNAL) + reason_failed.set_output("Cooldown still active!") + return + + TIMER_COOLDOWN_END(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME) + + old_dest = destination + path = get_path_to(src, destination, max_range, id=path_id) + if(length(path) == 0 || !path)// Check if we can even path there + next_turf = null + failed.set_output(COMPONENT_SIGNAL) + reason_failed.set_output("Can't go there!") + return + else + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_PATHFIND_DIF, different_path_cooldown) + next_turf = get_turf(path[1]) + output.set_output(next_turf) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_PATHFIND_SAME, same_path_cooldown) + diff --git a/code/modules/wiremod/components/action/pull.dm b/code/modules/wiremod/components/action/pull.dm new file mode 100644 index 0000000000..ecaa6bd3aa --- /dev/null +++ b/code/modules/wiremod/components/action/pull.dm @@ -0,0 +1,31 @@ +/** + * # Pull Component + * + * Tells the shell to start pulling on a designated atom. Only works on movable shells. + */ +/obj/item/circuit_component/pull + display_name = "Start Pulling" + desc = "A component that can force the shell to pull entities. Only works for drone shells." + + /// Frequency input + var/datum/port/input/target + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/pull/Initialize() + . = ..() + target = add_input_port("Target", PORT_TYPE_ATOM) + +/obj/item/circuit_component/pull/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/atom/target_atom = target.value + if(!target_atom) + return + + var/mob/shell = parent.shell + if(!istype(shell) || get_dist(shell, target_atom) > 1 || shell.z != target_atom.z) + return + + shell.start_pulling(target_atom) diff --git a/code/modules/wiremod/components/action/radio.dm b/code/modules/wiremod/components/action/radio.dm new file mode 100644 index 0000000000..540b79f7cf --- /dev/null +++ b/code/modules/wiremod/components/action/radio.dm @@ -0,0 +1,74 @@ +#define COMP_RADIO_PUBLIC "public" +#define COMP_RADIO_PRIVATE "private" + +/** + * # Radio Component + * + * Listens out for signals on the designated frequencies and sends signals on designated frequencies + */ +/obj/item/circuit_component/radio + display_name = "Radio" + desc = "A component that can listen and send frequencies. If set to private, the component will only receive signals from other components attached to circuitboards with the same owner id." + + /// The publicity options. Controls whether it's public or private. + var/datum/port/input/option/public_options + + /// Frequency input + var/datum/port/input/freq + /// Signal input + var/datum/port/input/code + + /// Current frequency value + var/current_freq = DEFAULT_SIGNALER_CODE + + var/datum/radio_frequency/radio_connection + +/obj/item/circuit_component/radio/populate_options() + var/static/component_options = list( + COMP_RADIO_PUBLIC, + COMP_RADIO_PRIVATE, + ) + public_options = add_option_port("Encryption Options", component_options) + +/obj/item/circuit_component/radio/Initialize() + . = ..() + freq = add_input_port("Frequency", PORT_TYPE_NUMBER, default = FREQ_SIGNALER) + code = add_input_port("Code", PORT_TYPE_NUMBER, default = DEFAULT_SIGNALER_CODE) + TRIGGER_CIRCUIT_COMPONENT(src, null) + // These are cleaned up on the parent + trigger_input = add_input_port("Send", PORT_TYPE_SIGNAL) + trigger_output = add_output_port("Received", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/radio/Destroy() + SSradio.remove_object(src, current_freq) + return ..() + +/obj/item/circuit_component/radio/input_received(datum/port/input/port) + . = ..() + freq.set_value(sanitize_frequency(freq.value, TRUE)) + if(.) + return + var/frequency = freq.value + + SSradio.remove_object(src, current_freq) + radio_connection = SSradio.add_object(src, frequency, RADIO_SIGNALER) + current_freq = frequency + + if(COMPONENT_TRIGGERED_BY(trigger_input, port)) + var/datum/signal/signal = new(list("code" = round(code.value) || 0, "key" = parent?.owner_id)) + radio_connection.post_signal(src, signal) + +/obj/item/circuit_component/radio/receive_signal(datum/signal/signal) + . = FALSE + if(!signal) + return + if(signal.data["code"] != round(code.value || 0)) + return + + if(public_options.value == COMP_RADIO_PRIVATE && parent?.owner_id != signal.data["key"]) + return + + trigger_output.set_output(COMPONENT_SIGNAL) + +#undef COMP_RADIO_PUBLIC +#undef COMP_RADIO_PRIVATE diff --git a/code/modules/wiremod/components/action/soundemitter.dm b/code/modules/wiremod/components/action/soundemitter.dm new file mode 100644 index 0000000000..6d18978c53 --- /dev/null +++ b/code/modules/wiremod/components/action/soundemitter.dm @@ -0,0 +1,66 @@ +/** + * # Sound Emitter Component + * + * A component that emits a sound when it receives an input. + */ +/obj/item/circuit_component/soundemitter + display_name = "Sound Emitter" + desc = "A component that emits a sound when it receives an input. The frequency is a multiplier which determines the speed at which the sound is played" + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// Sound to play + var/datum/port/input/option/sound_file + + /// Volume of the sound when played + var/datum/port/input/volume + + /// Frequency of the sound when played + var/datum/port/input/frequency + + /// The cooldown for this component of how often it can play sounds. + var/sound_cooldown = 2 SECONDS + + var/list/options_map + +/obj/item/circuit_component/soundemitter/get_ui_notices() + . = ..() + . += create_ui_notice("Sound Cooldown: [DisplayTimeText(sound_cooldown)]", "orange", "stopwatch") + + +/obj/item/circuit_component/soundemitter/Initialize() + . = ..() + volume = add_input_port("Volume", PORT_TYPE_NUMBER, default = 35) + frequency = add_input_port("Frequency", PORT_TYPE_NUMBER, default = 0) + +/obj/item/circuit_component/soundemitter/populate_options() + var/static/component_options = list( + "Buzz" = 'sound/machines/buzz-sigh.ogg', + "Buzz Twice" = 'sound/machines/buzz-two.ogg', + "Chime" = 'sound/machines/chime.ogg', + "Honk" = 'sound/items/bikehorn.ogg', + "Ping" = 'sound/machines/ping.ogg', + "Sad Trombone" = 'sound/misc/sadtrombone.ogg', + "Warn" = 'sound/machines/warning-buzzer.ogg', + "Slow Clap" = 'sound/machines/slowclap.ogg', + ) + sound_file = add_option_port("Sound Option", component_options) + options_map = component_options + + +/obj/item/circuit_component/soundemitter/input_received(datum/port/input/port) + . = ..() + volume.set_value(clamp(volume.value, 0, 100)) + frequency.set_value(clamp(frequency.value, -100, 100)) + if(.) + return + + if(TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_SOUNDEMITTER)) + return + + var/sound_to_play = options_map[sound_file.value] + if(!sound_to_play) + return + + playsound(src, sound_to_play, volume.value, frequency != 0, frequency = frequency.value) + + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_SOUNDEMITTER, sound_cooldown) diff --git a/code/modules/wiremod/components/action/speech.dm b/code/modules/wiremod/components/action/speech.dm new file mode 100644 index 0000000000..6732a2058a --- /dev/null +++ b/code/modules/wiremod/components/action/speech.dm @@ -0,0 +1,40 @@ +/** + * # Speech Component + * + * Sends a message. Requires a shell. + */ +/obj/item/circuit_component/speech + display_name = "Speech" + desc = "A component that sends a message. Requires a shell." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The message to send + var/datum/port/input/message + + /// The cooldown for this component of how often it can send speech messages. + var/speech_cooldown = 1 SECONDS + +/obj/item/circuit_component/speech/get_ui_notices() + . = ..() + . += create_ui_notice("Speech Cooldown: [DisplayTimeText(speech_cooldown)]", "orange", "stopwatch") + +/obj/item/circuit_component/speech/Initialize() + . = ..() + message = add_input_port("Message", PORT_TYPE_STRING, FALSE) + +/obj/item/circuit_component/speech/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_SPEECH)) + return + + if(message.value) + var/atom/movable/shell = parent.shell + // Prevents appear as the individual component if there is a shell. + if(shell) + shell.say(message.value) + else + say(message.value) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_SPEECH, speech_cooldown) diff --git a/code/modules/wiremod/components/admin/getvar.dm b/code/modules/wiremod/components/admin/getvar.dm new file mode 100644 index 0000000000..cef8341f38 --- /dev/null +++ b/code/modules/wiremod/components/admin/getvar.dm @@ -0,0 +1,42 @@ +/** + * # Get Variable Component + * + * A component that gets a variable on an object + */ +/obj/item/circuit_component/get_variable + display_name = "Get Variable" + desc = "A component that gets a variable on an object." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// Entity to get variable of + var/datum/port/input/entity + + /// Variable name + var/datum/port/input/variable_name + + /// Variable value + var/datum/port/output/output_value + + +/obj/item/circuit_component/get_variable/Initialize() + . = ..() + entity = add_input_port("Target", PORT_TYPE_ATOM) + variable_name = add_input_port("Variable Name", PORT_TYPE_STRING) + + output_value = add_output_port("Output Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/get_variable/input_received(datum/port/input/port) + . = ..() + if(.) + return + var/atom/object = entity.value + var/var_name = variable_name.value + if(!var_name || !object) + output_value.set_output(null) + return + + if(!object.can_vv_get(var_name) || !(var_name in object.vars)) + output_value.set_output(null) + return + + output_value.set_output(object.vars[var_name]) diff --git a/code/modules/wiremod/components/admin/proccall.dm b/code/modules/wiremod/components/admin/proccall.dm new file mode 100644 index 0000000000..1349c81636 --- /dev/null +++ b/code/modules/wiremod/components/admin/proccall.dm @@ -0,0 +1,72 @@ +#define COMP_PROC_GLOBAL "Global" +#define COMP_PROC_OBJECT "Object" + + +/** + * # Proc Call Component + * + * A component that calls a proc on an object and outputs the return value + */ +/obj/item/circuit_component/proccall + display_name = "Proc Call" + desc = "A component that calls a proc on an object." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + var/datum/port/input/option/proccall_options + + /// Entity to proccall on + var/datum/port/input/entity + + /// Proc to call + var/datum/port/input/proc_name + + /// Arguments + var/datum/port/input/arguments + + /// Returns the output from the proccall + var/datum/port/output/output_value + +/obj/item/circuit_component/proccall/populate_options() + var/static/list/component_options = list( + COMP_PROC_OBJECT, + COMP_PROC_GLOBAL, + ) + + proccall_options = add_option_port("Proccall Options", component_options) + +/obj/item/circuit_component/proccall/Initialize() + . = ..() + entity = add_input_port("Target", PORT_TYPE_ATOM) + proc_name = add_input_port("Proc Name", PORT_TYPE_STRING) + arguments = add_input_port("Arguments", PORT_TYPE_LIST) + + output_value = add_output_port("Output Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/proccall/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/called_on + if(proccall_options.value == COMP_PROC_OBJECT) + called_on = entity.value + else + called_on = GLOBAL_PROC + + if(!called_on) + return + + var/to_invoke = proc_name.value + var/params = arguments.value || list() + + if(!to_invoke) + return + + GLOB.AdminProcCaller = "CHAT_[parent.display_name]" //_ won't show up in ckeys so it'll never match with a real admin + var/result = WrapAdminProcCall(called_on, to_invoke, params) + GLOB.AdminProcCaller = null + + output_value.set_output(result) + +#undef COMP_PROC_GLOBAL +#undef COMP_PROC_OBJECT diff --git a/code/modules/wiremod/components/admin/sdql.dm b/code/modules/wiremod/components/admin/sdql.dm new file mode 100644 index 0000000000..871c3f247e --- /dev/null +++ b/code/modules/wiremod/components/admin/sdql.dm @@ -0,0 +1,36 @@ +/** + * # SDQL Component + * + * A component that performs an sdql operation + */ +/obj/item/circuit_component/sdql_operation + display_name = "SDQL Operation" + desc = "A component that performs an SDQL operation when invoked." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// SDQL Operation to invoke + var/datum/port/input/sdql_operation + + var/datum/port/output/results + + +/obj/item/circuit_component/sdql_operation/Initialize() + . = ..() + sdql_operation = add_input_port("SDQL String", PORT_TYPE_STRING) + results = add_output_port("Result", PORT_TYPE_LIST) + +/obj/item/circuit_component/sdql_operation/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/operation = sdql_operation.value + + if(GLOB.AdminProcCaller || !operation) + return TRUE + + GLOB.AdminProcCaller = "CHAT_[parent.display_name]" //_ won't show up in ckeys so it'll never match with a real admin + var/list/result = world.SDQL2_query(operation, parent.get_creator_admin(), parent.get_creator()) + GLOB.AdminProcCaller = null + + results.set_output(result) diff --git a/code/modules/wiremod/components/admin/setvar.dm b/code/modules/wiremod/components/admin/setvar.dm new file mode 100644 index 0000000000..0c66450cc9 --- /dev/null +++ b/code/modules/wiremod/components/admin/setvar.dm @@ -0,0 +1,36 @@ +/** + * # Set Variable Component + * + * A component that sets a variable on an object + */ +/obj/item/circuit_component/set_variable + display_name = "Set Variable" + desc = "A component that sets a variable on an object." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// Entity to set variable of + var/datum/port/input/entity + + /// Variable name + var/datum/port/input/variable_name + + /// New value to set the variable name to. + var/datum/port/input/new_value + + +/obj/item/circuit_component/set_variable/Initialize() + . = ..() + entity = add_input_port("Target", PORT_TYPE_ATOM) + variable_name = add_input_port("Variable Name", PORT_TYPE_STRING) + new_value = add_input_port("New Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/set_variable/input_received(datum/port/input/port) + . = ..() + if(.) + return + var/atom/object = entity.value + var/var_name = variable_name.value + if(!var_name || !object) + return + + object.vv_edit_var(var_name, new_value.value) diff --git a/code/modules/wiremod/components/admin/spawn.dm b/code/modules/wiremod/components/admin/spawn.dm new file mode 100644 index 0000000000..2d3697598b --- /dev/null +++ b/code/modules/wiremod/components/admin/spawn.dm @@ -0,0 +1,47 @@ +/** + * # Spawn Atom Component + * + * Spawns an atom. + */ +/obj/item/circuit_component/spawn_atom + display_name = "Spawn Atom" + desc = "Spawns an atom at a desired location" + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// The input path to convert into a typepath + var/datum/port/input/input_path + + /// The turf to spawn them at + var/datum/port/input/spawn_at + + /// Parameters to pass to the atom being spawned + var/datum/port/input/parameters + + /// The result from the output + var/datum/port/output/spawned_atom + +/obj/item/circuit_component/spawn_atom/Initialize() + . = ..() + input_path = add_input_port("Type", PORT_TYPE_ANY) + spawn_at = add_input_port("Spawn At", PORT_TYPE_ATOM) + parameters = add_input_port("Parameters", PORT_TYPE_LIST) + + spawned_atom = add_output_port("Spawned Atom", PORT_TYPE_ATOM) + +/obj/item/circuit_component/spawn_atom/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/typepath = input_path.value + + if(!ispath(typepath, /atom)) + return + + var/list/params = parameters.value + if(!params) + params = list() + + params.Insert(1, spawn_at.value) + + spawned_atom.set_output(new typepath(arglist(params))) diff --git a/code/modules/wiremod/components/admin/to_type.dm b/code/modules/wiremod/components/admin/to_type.dm new file mode 100644 index 0000000000..e4a34223bb --- /dev/null +++ b/code/modules/wiremod/components/admin/to_type.dm @@ -0,0 +1,28 @@ +/** + * # To Type Component + * + * Converts a string into a typepath. Useful for adding components. + */ +/obj/item/circuit_component/to_type + display_name = "String To Type" + desc = "Converts a string into a typepath. Useful for adding components." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL|CIRCUIT_FLAG_ADMIN + + /// The input path to convert into a typepath + var/datum/port/input/input_path + + /// The type output + var/datum/port/output/type_output + +/obj/item/circuit_component/to_type/Initialize() + . = ..() + input_path = add_input_port("Type", PORT_TYPE_STRING) + type_output = add_output_port("Typepath", PORT_TYPE_ANY) + +/obj/item/circuit_component/to_type/input_received(datum/port/input/port) + . = ..() + if(.) + return + + type_output.set_output(text2path(input_path.value)) + diff --git a/code/modules/wiremod/components/atom/direction.dm b/code/modules/wiremod/components/atom/direction.dm new file mode 100644 index 0000000000..140720a2d3 --- /dev/null +++ b/code/modules/wiremod/components/atom/direction.dm @@ -0,0 +1,66 @@ +/** + * # Direction Component + * + * Return the direction of a mob relative to the component + */ +/obj/item/circuit_component/direction + display_name = "Get Direction" + desc = "A component that returns the direction of itself and an entity." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + // Directions outputs + var/datum/port/output/north + var/datum/port/output/south + var/datum/port/output/east + var/datum/port/output/west + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// Maximum range for a valid direction to be returned + var/max_range = 7 + +/obj/item/circuit_component/direction/get_ui_notices() + . = ..() + . += create_ui_notice("Maximum Range: [max_range] tiles", "orange", "info") + +/obj/item/circuit_component/direction/Initialize() + . = ..() + input_port = add_input_port("Organism", PORT_TYPE_ATOM) + + output = add_output_port("Direction", PORT_TYPE_STRING) + + north = add_output_port("North", PORT_TYPE_SIGNAL) + east = add_output_port("East", PORT_TYPE_SIGNAL) + south = add_output_port("South", PORT_TYPE_SIGNAL) + west = add_output_port("West", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/direction/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/atom/object = input_port.value + if(!object) + return + var/turf/location = get_turf(src) + + if(object.z != location.z || get_dist(location, object) > max_range) + output.set_output(null) + return + + var/direction = get_dir(location, get_turf(object)) + output.set_output(dir2text(direction)) + + if(direction & NORTH) + north.set_output(COMPONENT_SIGNAL) + if(direction & SOUTH) + south.set_output(COMPONENT_SIGNAL) + if(direction & EAST) + east.set_output(COMPONENT_SIGNAL) + if(direction & WEST) + west.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/components/atom/gps.dm b/code/modules/wiremod/components/atom/gps.dm new file mode 100644 index 0000000000..cdf61385a0 --- /dev/null +++ b/code/modules/wiremod/components/atom/gps.dm @@ -0,0 +1,34 @@ +/** + * # GPS Component + * + * Return the location of this + */ +/obj/item/circuit_component/gps + display_name = "Internal GPS" + desc = "A component that returns the xyz co-ordinates of itself." + + /// The result from the output + var/datum/port/output/x_pos + var/datum/port/output/y_pos + var/datum/port/output/z_pos + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/gps/Initialize() + . = ..() + + x_pos = add_output_port("X", PORT_TYPE_NUMBER) + y_pos = add_output_port("Y", PORT_TYPE_NUMBER) + z_pos = add_output_port("Z", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/gps/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/turf/location = get_turf(src) + + x_pos.set_output(location?.x) + y_pos.set_output(location?.y) + z_pos.set_output(location?.z) + diff --git a/code/modules/wiremod/components/atom/health.dm b/code/modules/wiremod/components/atom/health.dm new file mode 100644 index 0000000000..7849180f9a --- /dev/null +++ b/code/modules/wiremod/components/atom/health.dm @@ -0,0 +1,62 @@ +/** + * # Get Health Component + * + * Return the health of a mob + */ +/obj/item/circuit_component/health + display_name = "Get Health" + desc = "A component that returns the health of an organism." + + /// The input port + var/datum/port/input/input_port + + /// Brute damage + var/datum/port/output/brute + /// Burn damage + var/datum/port/output/burn + /// Toxin damage + var/datum/port/output/toxin + /// Oxyloss damage + var/datum/port/output/oxy + /// Health + var/datum/port/output/health + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/max_range = 5 + +/obj/item/circuit_component/health/get_ui_notices() + . = ..() + . += create_ui_notice("Maximum Range: [max_range] tiles", "orange", "info") + +/obj/item/circuit_component/health/Initialize() + . = ..() + input_port = add_input_port("Organism", PORT_TYPE_ATOM) + + brute = add_output_port("Brute Damage", PORT_TYPE_NUMBER) + burn = add_output_port("Burn Damage", PORT_TYPE_NUMBER) + toxin = add_output_port("Toxin Damage", PORT_TYPE_NUMBER) + oxy = add_output_port("Suffocation Damage", PORT_TYPE_NUMBER) + health = add_output_port("Health", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/health/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/mob/living/organism = input_port.value + var/turf/current_turf = get_turf(src) + if(!istype(organism) || get_dist(current_turf, organism) > max_range || current_turf.z != organism.z) + brute.set_output(null) + burn.set_output(null) + toxin.set_output(null) + oxy.set_output(null) + health.set_output(null) + return + + brute.set_output(organism.getBruteLoss()) + burn.set_output(organism.getFireLoss()) + toxin.set_output(organism.getToxLoss()) + oxy.set_output(organism.getOxyLoss()) + health.set_output(organism.health) + diff --git a/code/modules/wiremod/components/atom/hear.dm b/code/modules/wiremod/components/atom/hear.dm new file mode 100644 index 0000000000..23fbba1e0a --- /dev/null +++ b/code/modules/wiremod/components/atom/hear.dm @@ -0,0 +1,35 @@ +/** + * # Hear Component + * + * Listens for messages. Requires a shell. + */ +/obj/item/circuit_component/hear + display_name = "Voice Activator" + desc = "A component that listens for messages. Requires a shell." + + /// The message heard + var/datum/port/output/message_port + /// The language heard + var/datum/port/output/language_port + /// The speaker + var/datum/port/output/speaker_port + /// The trigger sent when this event occurs + var/datum/port/output/trigger_port + +/obj/item/circuit_component/hear/Initialize() + . = ..() + message_port = add_output_port("Message", PORT_TYPE_STRING) + language_port = add_output_port("Language", PORT_TYPE_STRING) + speaker_port = add_output_port("Speaker", PORT_TYPE_ATOM) + trigger_port = add_output_port("Triggered", PORT_TYPE_SIGNAL) + become_hearing_sensitive(ROUNDSTART_TRAIT) + +/obj/item/circuit_component/hear/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods) + if(speaker == parent?.shell) + return + + message_port.set_output(raw_message) + if(message_language) + language_port.set_output(initial(message_language.name)) + speaker_port.set_output(speaker) + trigger_port.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/components/atom/self.dm b/code/modules/wiremod/components/atom/self.dm new file mode 100644 index 0000000000..7389a19f69 --- /dev/null +++ b/code/modules/wiremod/components/atom/self.dm @@ -0,0 +1,21 @@ +/** + * # Self Component + * + * Return the current shell. + */ +/obj/item/circuit_component/self + display_name = "Self" + desc = "A component that returns the current shell." + + /// The shell this component is attached to. + var/datum/port/output/output + +/obj/item/circuit_component/self/Initialize() + . = ..() + output = add_output_port("Self", PORT_TYPE_ATOM) + +/obj/item/circuit_component/self/register_shell(atom/movable/shell) + output.set_output(shell) + +/obj/item/circuit_component/self/unregister_shell(atom/movable/shell) + output.set_output(null) diff --git a/code/modules/wiremod/components/atom/species.dm b/code/modules/wiremod/components/atom/species.dm new file mode 100644 index 0000000000..9404a37697 --- /dev/null +++ b/code/modules/wiremod/components/atom/species.dm @@ -0,0 +1,35 @@ +/** + * # Get Species Component + * + * Return the species of a mob + */ +/obj/item/circuit_component/species + display_name = "Get Species" + desc = "A component that returns the species of its input." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/species/Initialize() + . = ..() + input_port = add_input_port("Organism", PORT_TYPE_ATOM) + + output = add_output_port("Species", PORT_TYPE_STRING) + +/obj/item/circuit_component/species/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/mob/living/carbon/human/human = input_port.value + if(!istype(human) || !human.has_dna()) + output.set_output(null) + return + + output.set_output(human.dna.species.name) + diff --git a/code/modules/wiremod/components/hud/bar_overlay.dm b/code/modules/wiremod/components/hud/bar_overlay.dm new file mode 100644 index 0000000000..f7ecfb1e1f --- /dev/null +++ b/code/modules/wiremod/components/hud/bar_overlay.dm @@ -0,0 +1,62 @@ +#define COMP_BAR_OVERLAY_VERTICAL "Vertical" +#define COMP_BAR_OVERLAY_HORIZONTAL "Horizontal" + +/** + * # Bar Overlay Component + * + * Basically an advanced verion of object overlay component that shows a horizontal/vertical bar. + * Requires a BCI shell. + */ + +/obj/item/circuit_component/object_overlay/bar + display_name = "Bar Overlay" + desc = "Requires a BCI shell. A component that shows a bar overlay ontop of an object from a range of 0 to 100." + + var/datum/port/input/option/bar_overlay_options + var/datum/port/input/bar_number + + var/overlay_limit = 10 + +/obj/item/circuit_component/object_overlay/bar/Initialize() + . = ..() + bar_number = add_input_port("Number", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/object_overlay/bar/populate_options() + var/static/component_options_bar = list( + COMP_BAR_OVERLAY_VERTICAL = "barvert", + COMP_BAR_OVERLAY_HORIZONTAL = "barhoriz" + ) + bar_overlay_options = add_option_port("Bar Overlay Options", component_options_bar) + options_map = component_options_bar + +/obj/item/circuit_component/object_overlay/bar/show_to_owner(atom/target_atom, mob/living/owner) + if(LAZYLEN(active_overlays) >= overlay_limit) + return + + var/current_option = bar_overlay_options.value + + if(active_overlays[target_atom]) + QDEL_NULL(active_overlays[target_atom]) + + var/number_clear = clamp(bar_number.value, 0, 100) + if(current_option == COMP_BAR_OVERLAY_HORIZONTAL) + number_clear = round(number_clear / 6.25) * 6.25 + else if(current_option == COMP_BAR_OVERLAY_VERTICAL) + number_clear = round(number_clear / 10) * 10 + var/image/cool_overlay = image(icon = 'icons/hud/screen_bci.dmi', loc = target_atom, icon_state = "[options_map[current_option]][number_clear]", layer = RIPPLE_LAYER) + + if(image_pixel_x.value) + cool_overlay.pixel_x = image_pixel_x.value + + if(image_pixel_y.value) + cool_overlay.pixel_y = image_pixel_y.value + + active_overlays[target_atom] = WEAKREF(target_atom.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "bar_overlay_[REF(src)]", + cool_overlay, + owner, + )) + +#undef COMP_BAR_OVERLAY_VERTICAL +#undef COMP_BAR_OVERLAY_HORIZONTAL diff --git a/code/modules/wiremod/components/hud/counter_overlay.dm b/code/modules/wiremod/components/hud/counter_overlay.dm new file mode 100644 index 0000000000..ecce816e74 --- /dev/null +++ b/code/modules/wiremod/components/hud/counter_overlay.dm @@ -0,0 +1,103 @@ +/** + * # Counter Overlay Component + * + * Shows an counter overlay. + * Requires a BCI shell. + */ + +/obj/item/circuit_component/counter_overlay + display_name = "Counter Overlay" + desc = "A component that shows an three digit counter. Requires a BCI shell." + + required_shells = list(/obj/item/organ/cyberimp/bci) + + var/datum/port/input/counter_number + + var/datum/port/input/image_pixel_x + var/datum/port/input/image_pixel_y + + var/datum/port/input/signal_update + + var/obj/item/organ/cyberimp/bci/bci + var/list/numbers = list() + var/counter_appearance + +/obj/item/circuit_component/counter_overlay/Initialize() + . = ..() + counter_number = add_input_port("Displayed Number", PORT_TYPE_NUMBER) + + signal_update = add_input_port("Update Overlay", PORT_TYPE_SIGNAL) + + image_pixel_x = add_input_port("X-Axis Shift", PORT_TYPE_NUMBER) + image_pixel_y = add_input_port("Y-Axis Shift", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/counter_overlay/register_shell(atom/movable/shell) + if(istype(shell, /obj/item/organ/cyberimp/bci)) + bci = shell + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/counter_overlay/unregister_shell(atom/movable/shell) + bci = null + QDEL_NULL(counter_appearance) + for(var/number in numbers) + QDEL_NULL(number) + UnregisterSignal(shell, COMSIG_ORGAN_REMOVED) + +/obj/item/circuit_component/counter_overlay/input_received(datum/port/input/port) + . = ..() + + if(. || !bci) + return + + var/mob/living/owner = bci.owner + + if(!owner || !istype(owner) || !owner.client) + return + + for(var/number in numbers) + QDEL_NULL(number) + numbers = list() + + QDEL_NULL(counter_appearance) + var/image/counter = image(icon = 'icons/hud/screen_bci.dmi', icon_state = "hud_numbers", loc = owner) + if(image_pixel_x.value) + counter.pixel_x = image_pixel_x.value + if(image_pixel_y.value) + counter.pixel_y = image_pixel_y.value + + counter_appearance = WEAKREF(owner.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "counter_overlay_[REF(src)]", + counter, + owner, + )) + + var/cleared_number = clamp(round(counter_number.value), 0, 999) + + for(var/i = 1 to 3) + var/cur_num = round(cleared_number / (10 ** (3 - i))) % 10 + var/image/number = image(icon = 'icons/hud/screen_bci.dmi', icon_state = "hud_number_[cur_num]", loc = owner) + + if(image_pixel_x.value) + number.pixel_x = image_pixel_x.value + (i - 1) * 9 + if(image_pixel_y.value) + number.pixel_y = image_pixel_y.value + + numbers.Add(WEAKREF(owner.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "counter_overlay_[REF(src)]_[i]", + number, + owner, + ))) + +/obj/item/circuit_component/counter_overlay/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + QDEL_NULL(counter_appearance) + for(var/number in numbers) + QDEL_NULL(number) + +/obj/item/circuit_component/counter_overlay/Destroy() + QDEL_NULL(counter_appearance) + for(var/number in numbers) + QDEL_NULL(number) + return ..() diff --git a/code/modules/wiremod/components/hud/object_overlay.dm b/code/modules/wiremod/components/hud/object_overlay.dm new file mode 100644 index 0000000000..2cab4a57fa --- /dev/null +++ b/code/modules/wiremod/components/hud/object_overlay.dm @@ -0,0 +1,122 @@ +/** + * # Object Overlay Component + * + * Shows an overlay ontop of an object. Toggleable. + * Requires a BCI shell. + */ + +#define OBJECT_OVERLAY_LIMIT 10 + +/obj/item/circuit_component/object_overlay + display_name = "Object Overlay" + desc = "Requires a BCI shell. A component that shows an overlay on top of an object." + + required_shells = list(/obj/item/organ/cyberimp/bci) + + var/datum/port/input/option/object_overlay_options + + /// Target atom + var/datum/port/input/target + + var/datum/port/input/image_pixel_x + var/datum/port/input/image_pixel_y + + /// On/Off signals + var/datum/port/input/signal_on + var/datum/port/input/signal_off + + var/obj/item/organ/cyberimp/bci/bci + var/list/active_overlays = list() + var/list/options_map + +/obj/item/circuit_component/object_overlay/Initialize() + . = ..() + target = add_input_port("Target", PORT_TYPE_ATOM) + + signal_on = add_input_port("Create Overlay", PORT_TYPE_SIGNAL) + signal_off = add_input_port("Remove Overlay", PORT_TYPE_SIGNAL) + + image_pixel_x = add_input_port("X-Axis Shift", PORT_TYPE_NUMBER) + image_pixel_y = add_input_port("Y-Axis Shift", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/object_overlay/Destroy() + for(var/active_overlay in active_overlays) + QDEL_NULL(active_overlay) + return ..() + +/obj/item/circuit_component/object_overlay/populate_options() + var/static/component_options = list( + "Corners (Blue)" = "hud_corners", + "Corners (Red)" = "hud_corners_red", + "Circle (Blue)" = "hud_circle", + "Circle (Red)" = "hud_circle_red", + "Small Corners (Blue)" = "hud_corners_small", + "Small Corners (Red)" = "hud_corners_small_red", + "Triangle (Blue)" = "hud_triangle", + "Triangle (Red)" = "hud_triangle_red", + "HUD mark (Blue)" = "hud_mark", + "HUD mark (Red)" = "hud_mark_red" + ) + object_overlay_options = add_option_port("Object", component_options) + options_map = component_options + +/obj/item/circuit_component/object_overlay/register_shell(atom/movable/shell) + if(istype(shell, /obj/item/organ/cyberimp/bci)) + bci = shell + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/object_overlay/unregister_shell(atom/movable/shell) + bci = null + UnregisterSignal(shell, COMSIG_ORGAN_REMOVED) + +/obj/item/circuit_component/object_overlay/input_received(datum/port/input/port) + . = ..() + + if(. || !bci) + return + + var/mob/living/owner = bci.owner + var/atom/target_atom = target.value + + if(!owner || !istype(owner) || !owner.client || !target_atom) + return + + if(COMPONENT_TRIGGERED_BY(signal_on, port)) + show_to_owner(target_atom, owner) + + if(COMPONENT_TRIGGERED_BY(signal_off, port) && (target_atom in active_overlays)) + QDEL_NULL(active_overlays[target_atom]) + active_overlays.Remove(target_atom) + +/obj/item/circuit_component/object_overlay/proc/show_to_owner(atom/target_atom, mob/living/owner) + if(LAZYLEN(active_overlays) >= OBJECT_OVERLAY_LIMIT) + return + + if(active_overlays[target_atom]) + QDEL_NULL(active_overlays[target_atom]) + + var/image/cool_overlay = image(icon = 'icons/hud/screen_bci.dmi', loc = target_atom, icon_state = options_map[object_overlay_options.value], layer = RIPPLE_LAYER) + + if(image_pixel_x.value) + cool_overlay.pixel_x = image_pixel_x.value + + if(image_pixel_y.value) + cool_overlay.pixel_y = image_pixel_y.value + + var/alt_appearance = WEAKREF(target_atom.add_alt_appearance( + /datum/atom_hud/alternate_appearance/basic/one_person, + "object_overlay_[REF(src)]", + cool_overlay, + owner, + )) + + active_overlays[target_atom] = alt_appearance + +/obj/item/circuit_component/object_overlay/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + for(var/atom/target_atom in active_overlays) + QDEL_NULL(active_overlays[target_atom]) + active_overlays.Remove(target_atom) + +#undef OBJECT_OVERLAY_LIMIT diff --git a/code/modules/wiremod/components/hud/target_intercept.dm b/code/modules/wiremod/components/hud/target_intercept.dm new file mode 100644 index 0000000000..2a19264b00 --- /dev/null +++ b/code/modules/wiremod/components/hud/target_intercept.dm @@ -0,0 +1,64 @@ +/** + * # Target Intercept Component + * + * When activated intercepts next click and outputs clicked atom. + * Requires a BCI shell. + */ + +/obj/item/circuit_component/target_intercept + display_name = "Target Intercept" + desc = "Requires a BCI shell. When activated, this component will allow user to target an object using their brain and will output the reference to said object." + + required_shells = list(/obj/item/organ/cyberimp/bci) + + var/datum/port/output/clicked_atom + + var/obj/item/organ/cyberimp/bci/bci + var/intercept_cooldown = 1 SECONDS + +/obj/item/circuit_component/target_intercept/Initialize() + . = ..() + trigger_input = add_input_port("Activate", PORT_TYPE_SIGNAL) + trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL) + clicked_atom = add_output_port("Targeted Object", PORT_TYPE_ATOM) + +/obj/item/circuit_component/target_intercept/register_shell(atom/movable/shell) + if(istype(shell, /obj/item/organ/cyberimp/bci)) + bci = shell + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/target_intercept/unregister_shell(atom/movable/shell) + bci = null + UnregisterSignal(shell, COMSIG_ORGAN_REMOVED) + +/obj/item/circuit_component/target_intercept/input_received(datum/port/input/port) + . = ..() + + if(. || !bci) + return + + var/mob/living/owner = bci.owner + if(!owner || !istype(owner) || !owner.client) + return + + if(TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_TARGET_INTERCEPT)) + return + + to_chat(owner, "Left-click to trigger target interceptor!") + owner.client.click_intercept = src + +/obj/item/circuit_component/target_intercept/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + if(owner.client && owner.client.click_intercept == src) + owner.client.click_intercept = null + +/obj/item/circuit_component/target_intercept/proc/InterceptClickOn(mob/user, params, atom/object) + user.client.click_intercept = null + clicked_atom.set_output(object) + trigger_output.set_output(COMPONENT_SIGNAL) + TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_TARGET_INTERCEPT, intercept_cooldown) + +/obj/item/circuit_component/target_intercept/get_ui_notices() + . = ..() + . += create_ui_notice("Target Interception Cooldown: [DisplayTimeText(intercept_cooldown)]", "orange", "stopwatch") diff --git a/code/modules/wiremod/components/list/concat.dm b/code/modules/wiremod/components/list/concat.dm new file mode 100644 index 0000000000..32e16758d3 --- /dev/null +++ b/code/modules/wiremod/components/list/concat.dm @@ -0,0 +1,48 @@ +/** + * # Concat List Component + * + * Concatenates a list with a separator + */ +/obj/item/circuit_component/concat_list + display_name = "Concatenate List" + desc = "A component that joins up a list with a separator into a single string." + + /// The input port + var/datum/port/input/list_port + + /// The seperator + var/datum/port/input/separator + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/concat_list/Initialize() + . = ..() + list_port = add_input_port("List", PORT_TYPE_LIST) + separator = add_input_port("Seperator", PORT_TYPE_STRING) + + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/concat_list/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/seperator = separator.value + if(!seperator) + return + + var/list/list_input = list_port.value + if(!list_input) + return + + var/list/text_list = list() + for(var/entry in list_input) + if(isatom(entry)) + text_list += PORT_TYPE_ATOM + else + text_list += "[entry]" + + output.set_output(text_list.Join(seperator)) + diff --git a/code/modules/wiremod/components/list/get_column.dm b/code/modules/wiremod/components/list/get_column.dm new file mode 100644 index 0000000000..07ff7ef11b --- /dev/null +++ b/code/modules/wiremod/components/list/get_column.dm @@ -0,0 +1,42 @@ +/** + * # Get Column Component + * + * Gets the column of a table and returns it as a regular list. + */ +/obj/item/circuit_component/get_column + display_name = "Get Column" + desc = "Gets the column of a table and returns it as a regular list." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The list to perform the filter on + var/datum/port/input/received_table + + /// The name of the column to check + var/datum/port/input/column_name + + /// The filtered list + var/datum/port/output/output_list + +/obj/item/circuit_component/get_column/Initialize() + . = ..() + received_table = add_input_port("Input", PORT_TYPE_TABLE) + column_name = add_input_port("Column Name", PORT_TYPE_STRING) + output_list = add_output_port("Output", PORT_TYPE_LIST) + +/obj/item/circuit_component/get_column/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/input_list = received_table.value + if(!islist(input_list) || isnum(column_name.value)) + return + + var/list/new_list = list() + for(var/list/entry in input_list) + var/anything = entry[column_name.value] + if(islist(anything)) + continue + new_list += anything + + output_list.set_output(new_list) diff --git a/code/modules/wiremod/components/list/index.dm b/code/modules/wiremod/components/list/index.dm new file mode 100644 index 0000000000..d51a60e2bf --- /dev/null +++ b/code/modules/wiremod/components/list/index.dm @@ -0,0 +1,42 @@ +/** + * # Index Component + * + * Return the index of a list + */ +/obj/item/circuit_component/index + display_name = "Index List" + desc = "A component that returns the value of a list at a given index." + + /// The input port + var/datum/port/input/list_port + var/datum/port/input/index_port + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/index/Initialize() + . = ..() + index_port = add_input_port("Index", PORT_TYPE_ANY) + list_port = add_input_port("List", PORT_TYPE_LIST) + + output = add_output_port("Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/index/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/index = index_port.value + var/list/list_input = list_port.value + + if(!islist(list_input) || !index) + output.set_output(null) + return + + if(isnum(index) && (index < 1 || index > length(list_input))) + output.set_output(null) + return + + output.set_output(list_input[index]) + diff --git a/code/modules/wiremod/components/list/index_table.dm b/code/modules/wiremod/components/list/index_table.dm new file mode 100644 index 0000000000..376bc46465 --- /dev/null +++ b/code/modules/wiremod/components/list/index_table.dm @@ -0,0 +1,42 @@ +/** + * # Index Table Component + * + * Gets the row of a table using the index inputted. Will return no value if the index is invalid or a proper table is not returned. + */ +/obj/item/circuit_component/index_table + display_name = "Index Table" + desc = "Gets the row of a table using the index inputted. Will return no value if the index is invalid or a proper table is not returned." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The list to perform the filter on + var/datum/port/input/received_table + + /// The target index + var/datum/port/input/target_index + + /// The filtered list + var/datum/port/output/output_list + +/obj/item/circuit_component/index_table/Initialize() + . = ..() + received_table = add_input_port("Input", PORT_TYPE_TABLE) + target_index = add_input_port("Index", PORT_TYPE_NUMBER) + + output_list = add_output_port("Output", PORT_TYPE_LIST) + +/obj/item/circuit_component/index_table/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/target_list = received_table.value + if(!islist(target_list) || !length(target_list)) + output_list.set_output(null) + return + + var/index = target_index.value + if(index < 1 || index > length(target_list)) + output_list.set_output(null) + return + + output_list.set_output(target_list[index]) diff --git a/code/modules/wiremod/components/list/list_literal.dm b/code/modules/wiremod/components/list/list_literal.dm new file mode 100644 index 0000000000..e9bd741606 --- /dev/null +++ b/code/modules/wiremod/components/list/list_literal.dm @@ -0,0 +1,86 @@ +/** + * # List Literal Component + * + * Return a list literal. + */ +/obj/item/circuit_component/list_literal + display_name = "List Literal" + desc = "A component that returns the value of a list at a given index. Attack in hand to increase list size, right click to decrease list size." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The inputs used to create the list + var/list/datum/port/input/entry_ports = list() + /// The result from the output + var/datum/port/output/list_output + + var/length = 0 + + var/default_list_size = 2 + + var/min_size = 1 + var/max_size = 20 + +/obj/item/circuit_component/list_literal/save_data_to_list(list/component_data) + . = ..() + component_data["length"] = length + +/obj/item/circuit_component/list_literal/load_data_from_list(list/component_data) + set_list_size(component_data["length"]) + + return ..() + +/obj/item/circuit_component/list_literal/proc/set_list_size(new_size) + if(new_size <= 0) + for(var/datum/port/input/port in entry_ports) + remove_input_port(port) + entry_ports = list() + length = 0 + return + + while(length > new_size) + var/index = length(entry_ports) + var/entry_port = entry_ports[index] + entry_ports -= entry_port + remove_input_port(entry_port) + length-- + + while(length < new_size) + length++ + var/index = length(input_ports) + if(trigger_input) + index -= 1 + entry_ports += add_input_port("Index [index+1]", PORT_TYPE_ANY, index = index+1) + +/obj/item/circuit_component/list_literal/Initialize() + . = ..() + set_list_size(default_list_size) + list_output = add_output_port("Value", PORT_TYPE_LIST) + +/obj/item/circuit_component/list_literal/Destroy() + list_output = null + return ..() + +// Increases list length +/obj/item/circuit_component/list_literal/attack_self(mob/user, list/modifiers) + . = ..() + set_list_size(min(length + 1, max_size)) + balloon_alert(user, "new size is now [length]") + +// Decreases list length +/obj/item/circuit_component/list_literal/attack_self_secondary(mob/user, list/modifiers) + . = ..() + set_list_size(max(length - 1, min_size)) + balloon_alert(user, "new size is now [length]") + +/obj/item/circuit_component/list_literal/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/new_literal = list() + for(var/datum/port/input/entry_port as anything in entry_ports) + // Prevents lists from merging together + new_literal += list(entry_port.value) + + list_output.set_output(new_literal) + diff --git a/code/modules/wiremod/components/list/select.dm b/code/modules/wiremod/components/list/select.dm new file mode 100644 index 0000000000..8eba84d034 --- /dev/null +++ b/code/modules/wiremod/components/list/select.dm @@ -0,0 +1,93 @@ +/** + * # Select Component + * + * Selects a list from a list of lists by a specific column. Used only by USBs for communications to and from computers with lists of varying sizes. + */ +/obj/item/circuit_component/select + display_name = "Select Query" + desc = "A component used with USB cables that can perform select queries on a list based on the column name selected. The values are then compared with the comparison input." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/option/comparison_options + + /// The list to perform the filter on + var/datum/port/input/received_table + + /// The name of the column to check + var/datum/port/input/column_name + + /// The input to compare with + var/datum/port/input/comparison_input + + /// The filtered list + var/datum/port/output/filtered_table + + var/current_type = PORT_TYPE_ANY + +/obj/item/circuit_component/select/populate_options() + var/static/component_options = list( + COMP_COMPARISON_EQUAL, + COMP_COMPARISON_NOT_EQUAL, + COMP_COMPARISON_GREATER_THAN, + COMP_COMPARISON_LESS_THAN, + COMP_COMPARISON_GREATER_THAN_OR_EQUAL, + COMP_COMPARISON_LESS_THAN_OR_EQUAL, + ) + comparison_options = add_option_port("Comparison Options", component_options) + +/obj/item/circuit_component/select/Initialize() + . = ..() + received_table = add_input_port("Input", PORT_TYPE_TABLE) + column_name = add_input_port("Column Name", PORT_TYPE_STRING) + comparison_input = add_input_port("Comparison Input", PORT_TYPE_ANY) + + filtered_table = add_output_port("Output", PORT_TYPE_TABLE) + +/obj/item/circuit_component/select/input_received(datum/port/input/port) + . = ..() + var/current_option = comparison_options.value + + switch(current_option) + if(COMP_COMPARISON_EQUAL, COMP_COMPARISON_NOT_EQUAL) + if(current_type != PORT_TYPE_ANY) + current_type = PORT_TYPE_ANY + comparison_input.set_datatype(PORT_TYPE_ANY) + else + if(current_type != PORT_TYPE_NUMBER) + current_type = PORT_TYPE_NUMBER + comparison_input.set_datatype(PORT_TYPE_NUMBER) + + if(.) + return + + var/list/input_list = received_table.value + if(!islist(input_list) || isnum(column_name.value)) + return + + var/comparison_value = comparison_input.value + var/list/new_list = list() + for(var/list/entry in input_list) + var/anything = entry[column_name.value] + if(islist(anything)) + continue + if(current_option != COMP_COMPARISON_EQUAL && current_option != COMP_COMPARISON_NOT_EQUAL && !isnum(anything)) + continue + var/add_to_list = FALSE + switch(current_option) + if(COMP_COMPARISON_EQUAL) + add_to_list = anything == comparison_value + if(COMP_COMPARISON_NOT_EQUAL) + add_to_list = anything != comparison_value + if(COMP_COMPARISON_GREATER_THAN) + add_to_list = anything > comparison_value + if(COMP_COMPARISON_GREATER_THAN_OR_EQUAL) + add_to_list = anything >= comparison_value + if(COMP_COMPARISON_LESS_THAN) + add_to_list = anything < comparison_value + if(COMP_COMPARISON_LESS_THAN_OR_EQUAL) + add_to_list = anything <= comparison_value + + if(add_to_list) + new_list += list(entry) + + filtered_table.set_output(new_list) diff --git a/code/modules/wiremod/components/list/split.dm b/code/modules/wiremod/components/list/split.dm new file mode 100644 index 0000000000..27af366fac --- /dev/null +++ b/code/modules/wiremod/components/list/split.dm @@ -0,0 +1,42 @@ +/** + * # Split component + * + * Splits a string + */ +/obj/item/circuit_component/split + display_name = "Split" + desc = "Splits a string by the separator, turning it into a list" + + /// The input port + var/datum/port/input/input_port + + /// The seperator + var/datum/port/input/separator + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/split/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_STRING) + separator = add_input_port("Seperator", PORT_TYPE_STRING) + output = add_output_port("Output", PORT_TYPE_LIST) + +/obj/item/circuit_component/split/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/separator_value = separator.value + if(isnull(separator_value)) + return + + var/value = input_port.value + if(isnull(value)) + return + + var/list/result = splittext(value,separator_value) + + output.set_output(result) diff --git a/code/modules/wiremod/components/math/arithmetic.dm b/code/modules/wiremod/components/math/arithmetic.dm new file mode 100644 index 0000000000..77c8b93b25 --- /dev/null +++ b/code/modules/wiremod/components/math/arithmetic.dm @@ -0,0 +1,88 @@ +#define COMP_ARITHMETIC_ADD "Add" +#define COMP_ARITHMETIC_SUBTRACT "Subtract" +#define COMP_ARITHMETIC_MULTIPLY "Multiply" +#define COMP_ARITHMETIC_DIVIDE "Divide" +#define COMP_ARITHMETIC_MIN "Minimum" +#define COMP_ARITHMETIC_MAX "Maximum" + +/** + * # Arithmetic Component + * + * General arithmetic unit with add/sub/mult/divide capabilities + * This one only works with numbers. + */ +/obj/item/circuit_component/arithmetic + display_name = "Arithmetic" + desc = "General arithmetic component with arithmetic capabilities." + + /// The amount of input ports to have + var/input_port_amount = 4 + + var/datum/port/input/option/arithmetic_option + + /// The result from the output + var/datum/port/output/output + + var/list/arithmetic_ports + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/arithmetic/populate_options() + var/static/component_options = list( + COMP_ARITHMETIC_ADD, + COMP_ARITHMETIC_SUBTRACT, + COMP_ARITHMETIC_MULTIPLY, + COMP_ARITHMETIC_DIVIDE, + COMP_ARITHMETIC_MIN, + COMP_ARITHMETIC_MAX, + ) + arithmetic_option = add_option_port("Arithmetic Option", component_options) + +/obj/item/circuit_component/arithmetic/Initialize() + . = ..() + arithmetic_ports = list() + for(var/port_id in 1 to input_port_amount) + var/letter = ascii2text(text2ascii("A") + (port_id-1)) + arithmetic_ports += add_input_port(letter, PORT_TYPE_NUMBER) + + output = add_output_port("Output", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/arithmetic/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/list/ports = arithmetic_ports.Copy() + var/datum/port/input/first_port = popleft(ports) + var/result = first_port.value + + for(var/datum/port/input/input_port as anything in ports) + var/value = input_port.value + if(isnull(value)) + continue + + switch(arithmetic_option.value) + if(COMP_ARITHMETIC_ADD) + result += value + if(COMP_ARITHMETIC_SUBTRACT) + result -= value + if(COMP_ARITHMETIC_MULTIPLY) + result *= value + if(COMP_ARITHMETIC_DIVIDE) + // Protect from div by zero errors. + if(value == 0) + result = null + break + result /= value + if(COMP_ARITHMETIC_MAX) + result = max(result, value) + if(COMP_ARITHMETIC_MIN) + result = min(result, value) + + output.set_output(result) + +#undef COMP_ARITHMETIC_ADD +#undef COMP_ARITHMETIC_SUBTRACT +#undef COMP_ARITHMETIC_MULTIPLY +#undef COMP_ARITHMETIC_DIVIDE +#undef COMP_ARITHMETIC_MIN +#undef COMP_ARITHMETIC_MAX diff --git a/code/modules/wiremod/components/math/comparison.dm b/code/modules/wiremod/components/math/comparison.dm new file mode 100644 index 0000000000..0415b04568 --- /dev/null +++ b/code/modules/wiremod/components/math/comparison.dm @@ -0,0 +1,62 @@ +/** + * # Comparison Component + * + * Compares two objects + */ +/obj/item/circuit_component/compare/comparison + display_name = "Comparison" + desc = "A component that compares two objects." + + var/datum/port/input/option/comparison_option + + input_port_amount = 2 + var/current_type = PORT_TYPE_ANY + +/obj/item/circuit_component/compare/comparison/populate_options() + var/static/component_options = list( + COMP_COMPARISON_EQUAL, + COMP_COMPARISON_NOT_EQUAL, + COMP_COMPARISON_GREATER_THAN, + COMP_COMPARISON_LESS_THAN, + COMP_COMPARISON_GREATER_THAN_OR_EQUAL, + COMP_COMPARISON_LESS_THAN_OR_EQUAL, + ) + comparison_option = add_option_port("Comparison Option", component_options) + +/obj/item/circuit_component/compare/comparison/input_received(datum/port/input/port) + switch(comparison_option.value) + if(COMP_COMPARISON_EQUAL, COMP_COMPARISON_NOT_EQUAL) + if(current_type != PORT_TYPE_ANY) + current_type = PORT_TYPE_ANY + compare_ports[1].set_datatype(PORT_TYPE_ANY) + compare_ports[2].set_datatype(PORT_TYPE_ANY) + else + if(current_type != PORT_TYPE_NUMBER) + current_type = PORT_TYPE_NUMBER + compare_ports[1].set_datatype(PORT_TYPE_NUMBER) + compare_ports[2].set_datatype(PORT_TYPE_NUMBER) + return ..() + + +/obj/item/circuit_component/compare/comparison/do_comparisons(list/ports) + if(length(ports) < input_port_amount) + return FALSE + + // Comparison component only compares the first two ports + var/input1 = compare_ports[1].value + var/input2 = compare_ports[2].value + var/current_option = comparison_option.value + + switch(current_option) + if(COMP_COMPARISON_EQUAL) + return input1 == input2 + if(COMP_COMPARISON_NOT_EQUAL) + return input1 != input2 + if(COMP_COMPARISON_GREATER_THAN) + return input1 > input2 + if(COMP_COMPARISON_GREATER_THAN_OR_EQUAL) + return input1 >= input2 + if(COMP_COMPARISON_LESS_THAN) + return input1 < input2 + if(COMP_COMPARISON_LESS_THAN_OR_EQUAL) + return input1 <= input2 diff --git a/code/modules/wiremod/components/math/length.dm b/code/modules/wiremod/components/math/length.dm new file mode 100644 index 0000000000..d6702c9b5d --- /dev/null +++ b/code/modules/wiremod/components/math/length.dm @@ -0,0 +1,29 @@ +/** + * # Length Component + * + * Return the length of an input + */ +/obj/item/circuit_component/length + display_name = "Length" + desc = "A component that returns the length of its input." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/length/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + + output = add_output_port("Length", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/length/input_received(datum/port/input/port) + . = ..() + if(.) + return + + output.set_output(length(input_port.value)) + diff --git a/code/modules/wiremod/components/math/logic.dm b/code/modules/wiremod/components/math/logic.dm new file mode 100644 index 0000000000..9f632a9242 --- /dev/null +++ b/code/modules/wiremod/components/math/logic.dm @@ -0,0 +1,57 @@ +#define COMP_LOGIC_AND "AND" +#define COMP_LOGIC_OR "OR" +#define COMP_LOGIC_XOR "XOR" + +/** + * # Logic Component + * + * General logic unit with AND OR capabilities + */ +/obj/item/circuit_component/compare/logic + display_name = "Logic" + desc = "A component with 'and' and 'or' capabilities." + + var/datum/port/input/option/logic_options + +/obj/item/circuit_component/compare/logic/populate_options() + var/static/component_options = list( + COMP_LOGIC_AND, + COMP_LOGIC_OR, + COMP_LOGIC_XOR, + ) + logic_options = add_option_port("Logic Options", component_options) + +/obj/item/circuit_component/compare/logic/do_comparisons(list/ports) + . = FALSE + var/current_option = logic_options.value + + // Used by XOR + var/total_ports = 0 + var/total_true_ports = 0 + for(var/datum/port/input/port as anything in ports) + if(isnull(port.value) && length(port.connected_ports) == 0) + continue + + total_ports += 1 + switch(current_option) + if(COMP_LOGIC_AND) + if(!port.value) + return FALSE + . = TRUE + if(COMP_LOGIC_OR) + if(port.value) + return TRUE + if(COMP_LOGIC_XOR) + if(port.value) + . = TRUE + total_true_ports += 1 + + if(current_option == COMP_LOGIC_XOR) + if(total_ports == total_true_ports) + return FALSE + if(.) + return TRUE + +#undef COMP_LOGIC_AND +#undef COMP_LOGIC_OR +#undef COMP_LOGIC_XOR diff --git a/code/modules/wiremod/components/math/not.dm b/code/modules/wiremod/components/math/not.dm new file mode 100644 index 0000000000..6a55024b54 --- /dev/null +++ b/code/modules/wiremod/components/math/not.dm @@ -0,0 +1,29 @@ +/** + * # Logic Component + * + * General logic unit with AND OR capabilities + */ +/obj/item/circuit_component/not + display_name = "Not" + desc = "A component that inverts its input." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/result + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/not/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/not/input_received(datum/port/input/port) + . = ..() + if(.) + return + + result.set_output(!input_port.value) + diff --git a/code/modules/wiremod/components/math/random.dm b/code/modules/wiremod/components/math/random.dm new file mode 100644 index 0000000000..548376ed8d --- /dev/null +++ b/code/modules/wiremod/components/math/random.dm @@ -0,0 +1,40 @@ +/** + * # Random Component + * + * Generates a random number between specific values + */ +/obj/item/circuit_component/random + display_name = "Random" + desc = "A component that returns random values." + + /// The minimum value that the random number can be + var/datum/port/input/minimum + /// The maximum value that the random number can be + var/datum/port/input/maximum + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The result from the output + var/datum/port/output/output + +/obj/item/circuit_component/random/Initialize() + . = ..() + minimum = add_input_port("Minimum", PORT_TYPE_NUMBER, FALSE) + maximum = add_input_port("Maximum", PORT_TYPE_NUMBER, FALSE) + + output = add_output_port("Output", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/random/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/min_val = minimum.value || 0 + var/max_val = maximum.value || 0 + + if(max_val < min_val) + output.set_output(0) + return + + output.set_output(rand(min_val, max_val)) + diff --git a/code/modules/wiremod/components/ntnet/ntnet_receive.dm b/code/modules/wiremod/components/ntnet/ntnet_receive.dm new file mode 100644 index 0000000000..0bcb1c5ed0 --- /dev/null +++ b/code/modules/wiremod/components/ntnet/ntnet_receive.dm @@ -0,0 +1,62 @@ +/** + * # NTNet Receiver Component + * + * Receives data through NTNet. + */ + +/obj/item/circuit_component/ntnet_receive + display_name = "NTNet Receiver" + desc = "Receives data packages through NTNet. If Encryption Key is set then only signals with the same Encryption Key will be received." + + circuit_flags = CIRCUIT_FLAG_OUTPUT_SIGNAL //trigger_output + + network_id = __NETWORK_CIRCUITS + + var/datum/port/input/push_hid + var/datum/port/output/hid + var/datum/port/output/data_package + var/datum/port/output/secondary_package + var/datum/port/input/enc_key + var/datum/port/input/option/data_type_options + var/datum/port/input/option/secondary_data_type_options + +/obj/item/circuit_component/ntnet_receive/Initialize() + . = ..() + data_package = add_output_port("Data Package", PORT_TYPE_ANY) + secondary_package = add_output_port("Secondary Package", PORT_TYPE_ANY) + enc_key = add_input_port("Encryption Key", PORT_TYPE_STRING) + RegisterSignal(src, COMSIG_COMPONENT_NTNET_RECEIVE, .proc/ntnet_receive) + +/obj/item/circuit_component/ntnet_receive/populate_options() + var/static/component_options = list( + PORT_TYPE_ANY, + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + ) + data_type_options = add_option_port("Data Type", component_options) + secondary_data_type_options = add_option_port("Secondary Data Type", component_options) + +/obj/item/circuit_component/ntnet_receive/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(COMPONENT_TRIGGERED_BY(data_type_options, port)) + data_package.set_datatype(data_type_options.value) + + if(COMPONENT_TRIGGERED_BY(secondary_data_type_options, port)) + secondary_package.set_datatype(secondary_data_type_options.value) + + return TRUE + +/obj/item/circuit_component/ntnet_receive/proc/ntnet_receive(datum/source, datum/netdata/data) + SIGNAL_HANDLER + + if(data.data["enc_key"] != enc_key.value) + return + + data_package.set_output(data.data["data"]) + secondary_package.set_output(data.data["data_secondary"]) + trigger_output.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/components/ntnet/ntnet_send.dm b/code/modules/wiremod/components/ntnet/ntnet_send.dm new file mode 100644 index 0000000000..33d9370fae --- /dev/null +++ b/code/modules/wiremod/components/ntnet/ntnet_send.dm @@ -0,0 +1,29 @@ +/** + * # NTNet Transmitter Component + * + * Sends a data package through NTNet + */ + +/obj/item/circuit_component/ntnet_send + display_name = "NTNet Transmitter" + desc = "Sends a data package through NTNet. If Encryption Key is set then transmitted data will be only picked up by receivers with the same Encryption Key." + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL + + network_id = __NETWORK_CIRCUITS + + var/datum/port/input/data_package + var/datum/port/input/secondary_package + var/datum/port/input/enc_key + +/obj/item/circuit_component/ntnet_send/Initialize() + . = ..() + data_package = add_input_port("Data Package", PORT_TYPE_ANY) + secondary_package = add_input_port("Secondary Package", PORT_TYPE_ANY) + enc_key = add_input_port("Encryption Key", PORT_TYPE_STRING) + +/obj/item/circuit_component/ntnet_send/input_received(datum/port/input/port) + . = ..() + if(.) + return + ntnet_send(list("data" = data_package.value, "data_secondary" = secondary_package.value, "enc_key" = enc_key.value)) diff --git a/code/modules/wiremod/components/sensors/pressuresensor.dm b/code/modules/wiremod/components/sensors/pressuresensor.dm new file mode 100644 index 0000000000..25f39b9c8e --- /dev/null +++ b/code/modules/wiremod/components/sensors/pressuresensor.dm @@ -0,0 +1,36 @@ +/** + * # Pressure Sensor + * + * Returns the pressure of the tile + */ +/obj/item/circuit_component/pressuresensor + display_name = "Pressure Sensor" + desc = "Outputs the current pressure of the tile" + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The result from the output + var/datum/port/output/result + +/obj/item/circuit_component/pressuresensor/Initialize() + . = ..() + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/pressuresensor/input_received(datum/port/input/port) + . = ..() + if(.) + return + //Get current turf + var/turf/location = get_turf(src) + if(!location) + result.set_output(null) + return + //Get environment info + var/datum/gas_mixture/environment = location.return_air() + var/total_moles = environment.total_moles() + var/pressure = environment.return_pressure() + if(total_moles) + //If there's atmos, return pressure + result.set_output(round(pressure,1)) + else + result.set_output(0) diff --git a/code/modules/wiremod/components/sensors/tempsensor.dm b/code/modules/wiremod/components/sensors/tempsensor.dm new file mode 100644 index 0000000000..bb2410ce77 --- /dev/null +++ b/code/modules/wiremod/components/sensors/tempsensor.dm @@ -0,0 +1,36 @@ +/** + * # Temperature Sensor + * + * Returns the temperature of the tile + */ +/obj/item/circuit_component/tempsensor + display_name = "Temperature Sensor" + desc = "Outputs the current temperature of the tile" + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The result from the output + var/datum/port/output/result + +/obj/item/circuit_component/tempsensor/Initialize() + . = ..() + result = add_output_port("Result", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/tempsensor/input_received(datum/port/input/port) + . = ..() + if(.) + return + //Get current turf + var/turf/location = get_turf(src) + if(!location) + result.set_output(null) + return + //Get environment info + var/datum/gas_mixture/environment = location.return_air() + var/total_moles = environment.total_moles() + if(total_moles) + //If there's atmos, return temperature + result.set_output(round(environment.temperature,1)) + else + result.set_output(0) + diff --git a/code/modules/wiremod/components/string/concat.dm b/code/modules/wiremod/components/string/concat.dm new file mode 100644 index 0000000000..55dd5530a1 --- /dev/null +++ b/code/modules/wiremod/components/string/concat.dm @@ -0,0 +1,42 @@ +/** + * # Concatenate Component + * + * General string concatenation component. Puts strings together. + */ +/obj/item/circuit_component/concat + display_name = "Concatenate" + desc = "A component that combines strings." + + /// The amount of input ports to have + var/input_port_amount = 4 + + /// The result from the output + var/datum/port/output/output + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/concat/Initialize() + . = ..() + for(var/port_id in 1 to input_port_amount) + var/letter = ascii2text(text2ascii("A") + (port_id-1)) + add_input_port(letter, PORT_TYPE_STRING) + + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/concat/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/result = "" + var/list/ports = input_ports.Copy() + ports -= trigger_input + + for(var/datum/port/input/input_port as anything in ports) + var/value = input_port.value + if(isnull(value)) + continue + + result += "[value]" + + output.set_output(result) + diff --git a/code/modules/wiremod/components/string/contains.dm b/code/modules/wiremod/components/string/contains.dm new file mode 100644 index 0000000000..61b5e0ca22 --- /dev/null +++ b/code/modules/wiremod/components/string/contains.dm @@ -0,0 +1,35 @@ +/** + * # String Contains Component + * + * Checks if a string contains a word/letter + */ +/obj/item/circuit_component/compare/contains + display_name = "String Contains" + desc = "Checks if a string contains a word/letter" + + input_port_amount = 0 + + var/datum/port/input/needle + var/datum/port/input/haystack + +/obj/item/circuit_component/compare/contains/load_custom_ports() + needle = add_input_port("Needle", PORT_TYPE_STRING) + haystack = add_input_port("Haystack", PORT_TYPE_STRING) + +/obj/item/circuit_component/compare/contains/Destroy() + needle = null + haystack = null + return ..() + + +/obj/item/circuit_component/compare/contains/do_comparisons(list/ports) + if(length(ports) < input_port_amount) + return + + var/to_find = needle.value + var/to_search = haystack.value + + if(!to_find || !to_search) + return + + return findtext(to_search, to_find) diff --git a/code/modules/wiremod/components/string/textcase.dm b/code/modules/wiremod/components/string/textcase.dm new file mode 100644 index 0000000000..a021074433 --- /dev/null +++ b/code/modules/wiremod/components/string/textcase.dm @@ -0,0 +1,54 @@ +#define COMP_TEXT_LOWER "To Lower" +#define COMP_TEXT_UPPER "To Upper" + +/** + * # Text Component + * + * Either makes the text upper case or lower case. + */ +/obj/item/circuit_component/textcase + display_name = "Text Case" + desc = "A component that makes its input uppercase or lowercase." + + var/datum/port/input/option/textcase_options + + /// The input port + var/datum/port/input/input_port + + /// The result of the text operation + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/textcase/populate_options() + var/static/component_options = list( + COMP_TEXT_LOWER, + COMP_TEXT_UPPER, + ) + textcase_options = add_option_port("Textcase Options", component_options) + +/obj/item/circuit_component/textcase/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_STRING) + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/textcase/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/value = input_port.value + if(isnull(value)) + return + + var/result + switch(textcase_options.value) + if(COMP_TEXT_LOWER) + result = lowertext(value) + if(COMP_TEXT_UPPER) + result = uppertext(value) + + output.set_output(result) + +#undef COMP_TEXT_LOWER +#undef COMP_TEXT_UPPER diff --git a/code/modules/wiremod/components/string/tonumber.dm b/code/modules/wiremod/components/string/tonumber.dm new file mode 100644 index 0000000000..6012ab339d --- /dev/null +++ b/code/modules/wiremod/components/string/tonumber.dm @@ -0,0 +1,29 @@ +/** + * #To Number Component + * + * Converts a string into a Number + */ +/obj/item/circuit_component/tonumber + display_name = "To Number" + desc = "A component that converts its input (a string) to a number. If there's text in the input, it'll only consider it if it starts with a number. It will take that number and ignore the rest." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + +/obj/item/circuit_component/tonumber/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_STRING) + + output = add_output_port("Output", PORT_TYPE_NUMBER) + +/obj/item/circuit_component/tonumber/input_received(datum/port/input/port) + . = ..() + if(.) + return + + output.set_output(text2num(input_port.value)) diff --git a/code/modules/wiremod/components/string/tostring.dm b/code/modules/wiremod/components/string/tostring.dm new file mode 100644 index 0000000000..bc044f157a --- /dev/null +++ b/code/modules/wiremod/components/string/tostring.dm @@ -0,0 +1,40 @@ +/** + * # To String Component + * + * Converts any value into a string + */ +/obj/item/circuit_component/tostring + display_name = "To String" + desc = "A component that converts its input to text." + + /// The input port + var/datum/port/input/input_port + + /// The result from the output + var/datum/port/output/output + + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/max_range = 5 + +/obj/item/circuit_component/tostring/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + + output = add_output_port("Output", PORT_TYPE_STRING) + +/obj/item/circuit_component/tostring/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/value = input_port.value + if(isatom(value)) + var/turf/location = get_turf(src) + var/atom/object = value + if(object.z != location.z || get_dist(location, object) > max_range) + output.set_output(PORT_TYPE_ATOM) + return + + output.set_output("[value]") + diff --git a/code/modules/wiremod/components/utility/clock.dm b/code/modules/wiremod/components/utility/clock.dm new file mode 100644 index 0000000000..4c570b0ec5 --- /dev/null +++ b/code/modules/wiremod/components/utility/clock.dm @@ -0,0 +1,57 @@ +/** + * # Clock Component + * + * Fires every tick of the circuit timer SS + */ +/obj/item/circuit_component/clock + display_name = "Clock" + desc = "A component that repeatedly fires." + + /// Whether the clock is on or not + var/datum/port/input/on + + /// The signal from this clock component + var/datum/port/output/signal + +/obj/item/circuit_component/clock/get_ui_notices() + . = ..() + . += create_ui_notice("Clock Interval: [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock") + +/obj/item/circuit_component/clock/Initialize() + . = ..() + on = add_input_port("On", PORT_TYPE_NUMBER) + + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/clock/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(on.value) + start_process() + else + stop_process() + +/obj/item/circuit_component/clock/Destroy() + stop_process() + return ..() + +/obj/item/circuit_component/clock/process(delta_time) + signal.set_output(COMPONENT_SIGNAL) + +/** + * Adds the component to the SSclock_component process list + * + * Starts ticking to send signals between periods of time + */ +/obj/item/circuit_component/clock/proc/start_process() + START_PROCESSING(SSclock_component, src) + +/** + * Removes the component to the SSclock_component process list + * + * Signals stop getting sent. + */ +/obj/item/circuit_component/clock/proc/stop_process() + STOP_PROCESSING(SSclock_component, src) diff --git a/code/modules/wiremod/components/utility/delay.dm b/code/modules/wiremod/components/utility/delay.dm new file mode 100644 index 0000000000..37fa5a79f6 --- /dev/null +++ b/code/modules/wiremod/components/utility/delay.dm @@ -0,0 +1,43 @@ +/// The minimum delay value that the delay component can have. +#define COMP_DELAY_MIN_VALUE 0.1 + +/** + * # Delay Component + * + * Delays a signal by a specified duration. + */ +/obj/item/circuit_component/delay + display_name = "Delay" + desc = "A component that delays a signal by a specified duration." + + /// Amount to delay by + var/datum/port/input/delay_amount + /// Input signal to fire the delay + var/datum/port/input/trigger + + /// The output of the signal + var/datum/port/output/output + +/obj/item/circuit_component/delay/Initialize() + . = ..() + delay_amount = add_input_port("Delay", PORT_TYPE_NUMBER, FALSE) + trigger = add_input_port("Trigger", PORT_TYPE_SIGNAL) + + output = add_output_port("Result", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/delay/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!COMPONENT_TRIGGERED_BY(trigger, port)) + return + + var/delay = delay_amount.value + if(delay > COMP_DELAY_MIN_VALUE) + // Convert delay into deciseconds + addtimer(CALLBACK(output, /datum/port/output.proc/set_output, trigger.value), delay*10) + else + output.set_output(trigger.value) + +#undef COMP_DELAY_MIN_VALUE diff --git a/code/modules/wiremod/components/utility/getter.dm b/code/modules/wiremod/components/utility/getter.dm new file mode 100644 index 0000000000..7bca3083da --- /dev/null +++ b/code/modules/wiremod/components/utility/getter.dm @@ -0,0 +1,70 @@ +/** + * # Getter Component + * + * Gets the current value from a variable. + */ +/obj/item/circuit_component/getter + display_name = "Variable Getter" + desc = "A component that gets a variable globally on the circuit." + + /// Variable name + var/datum/port/input/option/variable_name + + /// The value of the variable + var/datum/port/output/value + + var/datum/circuit_variable/current_variable + +/obj/item/circuit_component/getter/populate_options() + variable_name = add_option_port("Variable", null) + +/obj/item/circuit_component/getter/add_to(obj/item/integrated_circuit/added_to) + . = ..() + variable_name.possible_options = added_to.circuit_variables + +/obj/item/circuit_component/getter/removed_from(obj/item/integrated_circuit/removed_from) + variable_name.possible_options = null + return ..() + +/obj/item/circuit_component/getter/Initialize() + . = ..() + value = add_output_port("Value", PORT_TYPE_ANY) + +/obj/item/circuit_component/getter/input_received(datum/port/input/port) + . = ..() + // We don't care much about the parent's return value. We only care if the parent exists + // since this should never really fail. + if(!parent) + return + + var/variable_string = variable_name.value + if(!variable_string) + remove_current_variable() + value.set_output(null) + return + + var/datum/circuit_variable/variable = parent.circuit_variables[variable_string] + if(!variable) + remove_current_variable() + value.set_output(null) + return + + set_current_variable(variable) + value.set_output(variable.value) + +/obj/item/circuit_component/getter/proc/remove_current_variable() + SIGNAL_HANDLER + if(current_variable) + current_variable.remove_listener(src) + UnregisterSignal(current_variable, COMSIG_PARENT_QDELETING) + current_variable = null + +/obj/item/circuit_component/getter/proc/set_current_variable(datum/circuit_variable/variable) + if(variable == current_variable) + return + + remove_current_variable() + current_variable = variable + current_variable.add_listener(src) + RegisterSignal(current_variable, COMSIG_PARENT_QDELETING, .proc/remove_current_variable) + value.set_datatype(variable.datatype) diff --git a/code/modules/wiremod/components/utility/router.dm b/code/modules/wiremod/components/utility/router.dm new file mode 100644 index 0000000000..4086392f5b --- /dev/null +++ b/code/modules/wiremod/components/utility/router.dm @@ -0,0 +1,82 @@ +/** + * # Router Component + * + * Writes one of multiple inputs to one of multiple outputs. + */ +/obj/item/circuit_component/router + display_name = "Router" + desc = "Copies the input chosen by \"Input Selector\" to the output chosen by \"Output Selector\"." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/option/router_options + + /// Which ports to connect. + var/datum/port/input/input_selector + var/datum/port/input/output_selector + + /// How many ports to have. + var/input_port_amount = 4 + var/output_port_amount = 4 + + /// Current type of the ports + var/current_type + + /// The ports to route. + var/list/datum/port/input/ins + var/list/datum/port/output/outs + +/obj/item/circuit_component/router/populate_options() + var/static/component_options = list( + PORT_TYPE_ANY, + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + ) + router_options = add_option_port("Router Options", component_options) + +/obj/item/circuit_component/router/Initialize() + . = ..() + current_type = router_options.value + if(input_port_amount > 1) + input_selector = add_input_port("Input Selector", PORT_TYPE_NUMBER, default = 1) + if(output_port_amount > 1) + output_selector = add_input_port("Output Selector", PORT_TYPE_NUMBER, default = 1) + ins = list() + for(var/port_id in 1 to input_port_amount) + ins += add_input_port(input_port_amount > 1 ? "Input [port_id]" : "Input", current_type) + outs = list() + for(var/port_id in 1 to output_port_amount) + outs += add_output_port(output_port_amount > 1 ? "Output [port_id]" : "Output", current_type) + +/obj/item/circuit_component/router/Destroy() + input_selector = null + output_selector = null + ins.Cut() + ins = null + outs.Cut() + outs = null + return ..() + + +// If I is in range, L[I]. If I is out of range, wrap around. +#define WRAPACCESS(L, I) L[(((I||1)-1)%length(L)+length(L))%length(L)+1] +/obj/item/circuit_component/router/input_received(datum/port/input/port) + . = ..() + var/current_option = router_options.value + if(current_type != current_option) + current_type = current_option + for(var/datum/port/input/input as anything in ins) + input.set_datatype(current_type) + for(var/datum/port/output/output as anything in outs) + output.set_datatype(current_type) + if(.) + return + var/datum/port/input/input = WRAPACCESS(ins, input_selector ? input_selector.value : 1) + var/datum/port/output/output = WRAPACCESS(outs, output_selector ? output_selector.value : 1) + output.set_output(input.value) + +/obj/item/circuit_component/router/multiplexer + display_name = "Multiplexer" + desc = "Copies the input chosen by \"Input Selector\" to the output." + output_port_amount = 1 diff --git a/code/modules/wiremod/components/utility/setter.dm b/code/modules/wiremod/components/utility/setter.dm new file mode 100644 index 0000000000..614e700798 --- /dev/null +++ b/code/modules/wiremod/components/utility/setter.dm @@ -0,0 +1,58 @@ +/** + * # Setter Component + * + * Stores the current input when triggered into a variable. + */ +/obj/item/circuit_component/setter + display_name = "Variable Setter" + desc = "A component that sets a variable globally on the circuit." + + circuit_flags = CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// Variable name + var/datum/port/input/option/variable_name + + /// The input to store + var/datum/port/input/input_port + /// The trigger to store the current value of the input + var/datum/port/input/trigger + + var/current_type + +/obj/item/circuit_component/setter/populate_options() + variable_name = add_option_port("Variable", null) + +/obj/item/circuit_component/setter/add_to(obj/item/integrated_circuit/added_to) + . = ..() + variable_name.possible_options = added_to.circuit_variables + +/obj/item/circuit_component/setter/removed_from(obj/item/integrated_circuit/removed_from) + variable_name.possible_options = null + return ..() + +/obj/item/circuit_component/setter/Initialize() + . = ..() + input_port = add_input_port("Input", PORT_TYPE_ANY) + trigger = add_input_port("Store", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/setter/input_received(datum/port/input/port) + . = ..() + var/variable_string = variable_name.value + if(!variable_string) + return + + var/datum/circuit_variable/variable = parent.circuit_variables[variable_string] + if(!variable) + return + + if(variable.datatype != current_type) + current_type = variable.datatype + input_port.set_datatype(current_type) + + if(.) + return + + if(!COMPONENT_TRIGGERED_BY(trigger, port)) + return TRUE + + variable.set_value(input_port.value) diff --git a/code/modules/wiremod/components/utility/typecast.dm b/code/modules/wiremod/components/utility/typecast.dm new file mode 100644 index 0000000000..ee204c332e --- /dev/null +++ b/code/modules/wiremod/components/utility/typecast.dm @@ -0,0 +1,60 @@ +/** + * # Typecast Component + * + * A component that casts a value to a type if it matches or outputs null. + */ +/obj/item/circuit_component/typecast + display_name = "Typecast" + desc = "A component that casts a value to a type if it matches or outputs null." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/input/option/typecast_options + + var/datum/port/input/input_value + + var/datum/port/output/output_value + + var/current_type + +/obj/item/circuit_component/typecast/Initialize() + . = ..() + current_type = typecast_options.value + input_value = add_input_port("Input", PORT_TYPE_ANY) + output_value = add_output_port("Output", current_type) + +/obj/item/circuit_component/typecast/populate_options() + var/static/list/component_options = list( + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + ) + typecast_options = add_option_port("Typecast Options", component_options) + +/obj/item/circuit_component/typecast/input_received(datum/port/input/port) + . = ..() + var/current_option = typecast_options.value + if(current_type != current_option) + current_type = current_option + output_value.set_datatype(current_type) + + if(.) + return + + var/value = input_value.value + var/value_to_set = null + switch(current_option) + if(PORT_TYPE_STRING) + if(istext(value)) + value_to_set = value + if(PORT_TYPE_NUMBER) + if(isnum(value)) + value_to_set = value + if(PORT_TYPE_LIST) + if(islist(value)) + value_to_set = value + if(PORT_TYPE_ATOM) + if(isatom(value)) + value_to_set = value + + output_value.set_output(value_to_set) diff --git a/code/modules/wiremod/components/utility/typecheck.dm b/code/modules/wiremod/components/utility/typecheck.dm new file mode 100644 index 0000000000..8caa009fd8 --- /dev/null +++ b/code/modules/wiremod/components/utility/typecheck.dm @@ -0,0 +1,51 @@ +#define COMP_TYPECHECK_MOB "organism" +#define COMP_TYPECHECK_HUMAN "humanoid" + +/** + * # Typecheck Component + * + * Checks the type of a value + */ +/obj/item/circuit_component/compare/typecheck + display_name = "Typecheck" + desc = "A component that checks the type of its input." + + input_port_amount = 1 + var/datum/port/input/option/typecheck_options + +/obj/item/circuit_component/compare/typecheck/populate_options() + var/static/component_options = list( + PORT_TYPE_STRING, + PORT_TYPE_NUMBER, + PORT_TYPE_LIST, + PORT_TYPE_ATOM, + COMP_TYPECHECK_MOB, + COMP_TYPECHECK_HUMAN, + ) + typecheck_options = add_option_port("Typecheck Options", component_options) + +/obj/item/circuit_component/compare/typecheck/do_comparisons(list/ports) + if(!length(ports)) + return + . = FALSE + + // We're only comparing the first port/value. There shouldn't be any more. + var/datum/port/input/input_port = ports[1] + var/input_val = input_port.value + switch(typecheck_options.value) + if(PORT_TYPE_STRING) + return istext(input_val) + if(PORT_TYPE_NUMBER) + return isnum(input_val) + if(PORT_TYPE_LIST) + return islist(input_val) + if(PORT_TYPE_ATOM) + return isatom(input_val) + if(COMP_TYPECHECK_MOB) + return ismob(input_val) + if(COMP_TYPECHECK_HUMAN) + return ishuman(input_val) + + +#undef COMP_TYPECHECK_MOB +#undef COMP_TYPECHECK_HUMAN diff --git a/code/modules/wiremod/core/admin_panel.dm b/code/modules/wiremod/core/admin_panel.dm new file mode 100644 index 0000000000..33af02f514 --- /dev/null +++ b/code/modules/wiremod/core/admin_panel.dm @@ -0,0 +1,75 @@ +/// An admin verb to view all circuits, plus useful information +/datum/admins/proc/view_all_circuits() + set category = "Admin.Game" + set name = "View All Circuits" + + var/static/datum/circuit_admin_panel/circuit_admin_panel = new + circuit_admin_panel.ui_interact(usr) + +/datum/circuit_admin_panel + +/datum/circuit_admin_panel/ui_static_data(mob/user) + var/list/data = list() + data["circuits"] = list() + + for (var/obj/item/integrated_circuit/circuit as anything in GLOB.integrated_circuits) + var/datum/mind/inserter = circuit.inserter_mind?.resolve() + + data["circuits"] += list(list( + "ref" = REF(circuit), + "name" = "[circuit.name] in [loc_name(circuit)]", + "creator" = circuit.get_creator(), + "has_inserter" = !isnull(inserter), + )) + + return data + +/datum/circuit_admin_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if (.) + return . + + if (!istext(params["circuit"])) + return FALSE + + var/obj/item/integrated_circuit/circuit = locate(params["circuit"]) + if (!istype(circuit)) + to_chat(usr, span_warning("That circuit no longer exists.")) + return FALSE + + switch (action) + if ("duplicate_circuit") + if (alert(usr, "This will spawn the new circuit at where you are, are you sure?", "Confirm", "Yes", "No") != "Yes") + return FALSE + + var/list/errors = list() + + var/obj/item/integrated_circuit/loaded/new_circuit = new(usr.drop_location()) + new_circuit.load_circuit_data(circuit.convert_to_json(), errors) + + if (length(errors)) + to_chat(usr, span_warning("Somehow, duplicating the circuit failed:")) + for (var/error in errors) + to_chat(usr, span_warning(error)) + if ("follow_circuit") + usr.client?.admin_follow(circuit) + if ("save_circuit") + circuit.attempt_save_to(usr.client) + if ("vv_circuit") + usr.client?.debug_variables(circuit) + if ("open_circuit") + circuit.ui_interact(usr) + if ("open_player_panel") + var/datum/mind/inserter = circuit.inserter_mind?.resolve() + usr.client?.holder?.show_player_panel(inserter?.current) + + return TRUE + +/datum/circuit_admin_panel/ui_state(mob/user) + return GLOB.admin_state + +/datum/circuit_admin_panel/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "CircuitAdminPanel") + ui.open() diff --git a/code/modules/wiremod/core/component.dm b/code/modules/wiremod/core/component.dm new file mode 100644 index 0000000000..0939debc3c --- /dev/null +++ b/code/modules/wiremod/core/component.dm @@ -0,0 +1,270 @@ +/** + * # Integrated Circuit Component + * + * A component that performs a function when given an input + * + * Can be attached to an integrated circuitboard, where it can then + * be connected between other components to provide an output or to receive + * an input. This is the base type of all components + */ +/obj/item/circuit_component + name = COMPONENT_DEFAULT_NAME + icon = 'icons/obj/module.dmi' + icon_state = "component" + inhand_icon_state = "electronic" + + /// The name of the component shown on the UI + var/display_name = "Generic" + + /// The integrated_circuit that this component is attached to. + var/obj/item/integrated_circuit/parent + + /// A list that contains the outpurt ports on this component + /// Used to connect between the ports + var/list/datum/port/output/output_ports = list() + + /// A list that contains the components the input ports on this component + /// Used to connect between the ports + var/list/datum/port/input/input_ports = list() + + /// Generic trigger input for triggering this component + var/datum/port/input/trigger_input + var/datum/port/output/trigger_output + + /// The flags of the circuit to control basic generalised behaviour. + var/circuit_flags = NONE + + /// Used to determine the x position of the component within the UI + var/rel_x = 0 + /// Used to determine the y position of the component within the UI + var/rel_y = 0 + + /// The power usage whenever this component receives an input + var/power_usage_per_input = 1 + + // Whether the component is removable or not. Only affects user UI + var/removable = TRUE + + // Defines which shells support this component. Only used as an informational guide, does not restrict placing these components in circuits. + var/required_shells = null + +/// Called when the option ports should be set up +/obj/item/circuit_component/proc/populate_options() + return + +/// Extension of add_input_port. Simplifies the code to make an option port to reduce boilerplate +/obj/item/circuit_component/proc/add_option_port(name, list/list_to_use) + return add_input_port(name, PORT_TYPE_OPTION, port_type = /datum/port/input/option, extra_args = list("possible_options" = list_to_use)) + +/obj/item/circuit_component/Initialize() + . = ..() + if(name == COMPONENT_DEFAULT_NAME) + name = "[lowertext(display_name)] [COMPONENT_DEFAULT_NAME]" + populate_options() + + return INITIALIZE_HINT_LATELOAD + +/obj/item/circuit_component/LateInitialize() + . = ..() + if(circuit_flags & CIRCUIT_FLAG_INPUT_SIGNAL) + trigger_input = add_input_port("Trigger", PORT_TYPE_SIGNAL) + if(circuit_flags & CIRCUIT_FLAG_OUTPUT_SIGNAL) + trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/Destroy() + if(parent) + // Prevents a Destroy() recursion + var/obj/item/integrated_circuit/old_parent = parent + parent = null + old_parent.remove_component(src) + + trigger_input = null + trigger_output = null + + QDEL_LIST(output_ports) + QDEL_LIST(input_ports) + return ..() + +/** + * Called when a shell is registered from the component/the component is added to a circuit. + * + * Register all signals here on the shell. + * Arguments: + * * shell - Shell being registered + */ +/obj/item/circuit_component/proc/register_shell(atom/movable/shell) + return + +/** + * Called when a shell is unregistered from the component/the component is removed from a circuit. + * + * Unregister all signals here on the shell. + * Arguments: + * * shell - Shell being unregistered + */ +/obj/item/circuit_component/proc/unregister_shell(atom/movable/shell) + return + +/** + * Disconnects a component from other components + * + * Disconnects both the input and output ports of the component + */ +/obj/item/circuit_component/proc/disconnect() + for(var/datum/port/output/port_to_disconnect as anything in output_ports) + port_to_disconnect.disconnect_all() + + for(var/datum/port/input/port_to_disconnect as anything in input_ports) + port_to_disconnect.disconnect_all() + +/** + * Adds an input port and returns it + * + * Arguments: + * * name - The name of the input port + * * type - The datatype it handles + * * trigger - Whether this input port triggers an update on the component when updated. + */ +/obj/item/circuit_component/proc/add_input_port(name, type, trigger = TRUE, default = null, index = null, port_type = /datum/port/input, extra_args = null) + var/list/arguments = list(src) + arguments += args + if(extra_args) + arguments += extra_args + var/datum/port/input/input_port = new port_type(arglist(arguments)) + if(index) + input_ports.Insert(index, input_port) + else + input_ports += input_port + if(parent) + SStgui.update_uis(parent) + return input_port + +/** + * Removes an input port and deletes it. This will not cleanup any references made by derivatives of the circuit component + * + * Arguments: + * * input_port - The input port to remove. + */ +/obj/item/circuit_component/proc/remove_input_port(datum/port/input/input_port) + input_ports -= input_port + qdel(input_port) + if(parent) + SStgui.update_uis(parent) + +/** + * Adds an output port and returns it + * + * Arguments: + * * name - The name of the output port + * * type - The datatype it handles. + */ +/obj/item/circuit_component/proc/add_output_port(name, type) + var/list/arguments = list(src) + arguments += args + var/datum/port/output/output_port = new(arglist(arguments)) + output_ports += output_port + return output_port + +/** + * Removes an output port and deletes it. This will not cleanup any references made by derivatives of the circuit component + * + * Arguments: + * * output_port - The output port to remove. + */ +/obj/item/circuit_component/proc/remove_output_port(datum/port/output/output_port) + output_ports -= output_port + qdel(output_port) + if(parent) + SStgui.update_uis(parent) + +/** + * Called whenever an input is received from one of the ports. + * + * Return value indicates that the circuit should not do anything. Also prevents an output signal. + * Arguments: + * * port - Can be null. The port that sent the input + */ +/obj/item/circuit_component/proc/input_received(datum/port/input/port) + SHOULD_CALL_PARENT(TRUE) + if(!parent?.on) + return TRUE + + if(!parent.admin_only) + if(circuit_flags & CIRCUIT_FLAG_ADMIN) + message_admins("[display_name] tried to execute on [parent.get_creator_admin()] that has admin_only set to 0") + return TRUE + + var/obj/item/stock_parts/cell/cell = parent.get_cell() + if(!cell?.use(power_usage_per_input)) + return TRUE + + if((circuit_flags & CIRCUIT_FLAG_INPUT_SIGNAL) && !COMPONENT_TRIGGERED_BY(trigger_input, port)) + return TRUE + +/// Called when this component is about to be added to an integrated_circuit. +/obj/item/circuit_component/proc/add_to(obj/item/integrated_circuit/added_to) + return TRUE + +/// Called when this component is removed from an integrated_circuit. +/obj/item/circuit_component/proc/removed_from(obj/item/integrated_circuit/removed_from) + return + +/** + * Gets the UI notices to be displayed on the CircuitInfo panel. + * + * Returns a list of buttons in the following format + * list( + * "icon" = ICON(string) + * "content" = CONTENT(string) + * "color" = COLOR(string, not a hex) + * ) + */ +/obj/item/circuit_component/proc/get_ui_notices() + . = list() + + if(!removable) + . += create_ui_notice("Unremovable", "red", "lock") + + if(length(required_shells)) + . += create_ui_notice("Supported Shells:", "green", "notes-medical") + for(var/atom/movable/shell as anything in required_shells) + . += create_ui_notice(initial(shell.name), "green", "plus-square") + + if(length(input_ports)) + . += create_ui_notice("Power Usage Per Input: [power_usage_per_input]", "orange", "bolt") + +/** + * Creates a UI notice entry to be used in get_ui_notices() + * + * Returns a list that can then be added to the return list in get_ui_notices() + */ +/obj/item/circuit_component/proc/create_ui_notice(content, color, icon) + SHOULD_BE_PURE(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + return list(list( + "icon" = icon, + "content" = content, + "color" = color, + )) + +/** + * Creates a table UI notice entry to be used in get_ui_notices() + * + * Returns a list that can then be added to the return list in get_ui_notices() + * Used by components to list their available columns. Recommended to use at the end of get_ui_notices() + */ +/obj/item/circuit_component/proc/create_table_notices(list/entries) + SHOULD_BE_PURE(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + . = list() + . += create_ui_notice("Available Columns:", "grey", "question-circle") + + + for(var/entry in entries) + . += create_ui_notice("Column Name: '[entry]'", "grey", "columns") + +/obj/item/circuit_component/proc/register_usb_parent(atom/movable/parent) + return + +/obj/item/circuit_component/proc/unregister_usb_parent(atom/movable/parent) + return diff --git a/code/modules/wiremod/core/component_printer.dm b/code/modules/wiremod/core/component_printer.dm new file mode 100644 index 0000000000..7400aafbf2 --- /dev/null +++ b/code/modules/wiremod/core/component_printer.dm @@ -0,0 +1,394 @@ +/// Component printer, creates components for integrated circuits. +/obj/machinery/component_printer + name = "component printer" + desc = "Produces components for the creation of integrated circuits." + icon = 'icons/obj/wiremod_fab.dmi' + icon_state = "fab-idle" + circuit = /obj/item/circuitboard/machine/component_printer + + /// The internal material bus + var/datum/component/remote_materials/materials + + density = TRUE + + /// The techweb the printer will get researched designs from + var/datum/techweb/techweb + +/obj/machinery/component_printer/Initialize(mapload) + . = ..() + + techweb = SSresearch.science_tech + + materials = AddComponent( \ + /datum/component/remote_materials, \ + "component_printer", \ + mapload, \ + mat_container_flags = BREAKDOWN_FLAGS_LATHE, \ + ) + +/obj/machinery/component_printer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ComponentPrinter", name) + ui.open() + +/obj/machinery/component_printer/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/sheetmaterials) + ) + +/obj/machinery/component_printer/ui_act(action, list/params) + . = ..() + if (.) + return + + switch (action) + if ("print") + var/design_id = params["designId"] + if (!techweb.researched_designs[design_id]) + return TRUE + + var/datum/design/design = SSresearch.techweb_design_by_id(design_id) + if (!(design.build_type & COMPONENT_PRINTER)) + return TRUE + + if (materials.on_hold()) + say("Mineral access is on hold, please contact the quartermaster.") + return TRUE + + if (!materials.mat_container?.has_materials(design.materials)) + say("Not enough materials.") + return TRUE + + balloon_alert_to_viewers("printed [design.name]") + materials.mat_container?.use_materials(design.materials) + materials.silo_log(src, "printed", -1, design.name, design.materials) + var/atom/printed_design = new design.build_path(drop_location()) + printed_design.pixel_x = printed_design.base_pixel_x + rand(-5, 5) + printed_design.pixel_y = printed_design.base_pixel_y + rand(-5, 5) + if ("remove_mat") + var/datum/material/material = locate(params["ref"]) + var/amount = text2num(params["amount"]) + + if (!amount) + return TRUE + + // SAFETY: eject_sheets checks for valid mats + materials.eject_sheets(material, amount) + + return TRUE + +/obj/machinery/component_printer/ui_data(mob/user) + var/list/data = list() + data["materials"] = materials.mat_container.ui_data() + return data + +/obj/machinery/component_printer/ui_static_data(mob/user) + var/list/data = list() + + var/list/designs = list() + + // for (var/datum/design/component/component_design_type as anything in subtypesof(/datum/design/component)) + for (var/researched_design_id in techweb.researched_designs) + var/datum/design/design = SSresearch.techweb_design_by_id(researched_design_id) + if (!(design.build_type & COMPONENT_PRINTER)) + continue + + designs[researched_design_id] = list( + "name" = design.name, + "description" = design.desc, + "materials" = get_material_cost_data(design.materials), + "categories" = design.category, + ) + + data["designs"] = designs + + return data + +/obj/machinery/component_printer/crowbar_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_crowbar(tool) + +/obj/machinery/component_printer/screwdriver_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_screwdriver(user, "fab-o", "fab-idle", tool) + +/obj/machinery/component_printer/proc/get_material_cost_data(list/materials) + var/list/data = list() + + for (var/datum/material/material_type as anything in materials) + data[initial(material_type.name)] = materials[material_type] + + return data + +/obj/item/circuitboard/machine/component_printer + name = "\improper Component Printer (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_SCIENCE + build_path = /obj/machinery/component_printer + req_components = list( + /obj/item/stock_parts/matter_bin = 2, + /obj/item/stock_parts/manipulator = 2, + /obj/item/reagent_containers/glass/beaker = 2, + ) + +/obj/machinery/debug_component_printer + name = "debug component printer" + desc = "Produces components for the creation of integrated circuits." + icon = 'icons/obj/wiremod_fab.dmi' + icon_state = "fab-idle" + + /// All of the possible circuit designs stored by this debug printer + var/list/all_circuit_designs + + density = TRUE + +/obj/machinery/debug_component_printer/Initialize() + . = ..() + all_circuit_designs = list() + + for(var/id in SSresearch.techweb_designs) + var/datum/design/design = SSresearch.techweb_design_by_id(id) + if((design.build_type & COMPONENT_PRINTER) && design.build_path) + all_circuit_designs[design.build_path] = list( + "name" = design.name, + "description" = design.desc, + "materials" = design.materials, + "categories" = design.category + ) + + for(var/obj/item/circuit_component/component as anything in subtypesof(/obj/item/circuit_component)) + var/categories = list("Inaccessible") + if(initial(component.circuit_flags) & CIRCUIT_FLAG_ADMIN) + categories = list("Admin") + if(!(component in all_circuit_designs)) + all_circuit_designs[component] = list( + "name" = initial(component.display_name), + "description" = initial(component.desc), + "materials" = list(), + "categories" = categories, + ) + +/obj/machinery/debug_component_printer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ComponentPrinter", name) + ui.open() + +/obj/machinery/debug_component_printer/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/sheetmaterials) + ) + +/obj/machinery/debug_component_printer/ui_act(action, list/params) + . = ..() + if (.) + return + + switch (action) + if ("print") + var/build_path = text2path(params["designId"]) + if (!build_path) + return TRUE + + var/list/design = all_circuit_designs[build_path] + if(!design) + return TRUE + + balloon_alert_to_viewers("printed [design["name"]]") + var/atom/printed_design = new build_path(drop_location()) + printed_design.pixel_x = printed_design.base_pixel_x + rand(-5, 5) + printed_design.pixel_y = printed_design.base_pixel_y + rand(-5, 5) + + return TRUE + +/obj/machinery/debug_component_printer/ui_static_data(mob/user) + var/list/data = list() + + data["materials"] = list() + data["designs"] = all_circuit_designs + + return data + +/// Module duplicator, allows you to save and recreate module components. +/obj/machinery/module_duplicator + name = "module duplicator" + desc = "Allows you to duplicate module components so that you don't have to recreate them. Scan a module component over this machine to add it as an entry." + icon = 'icons/obj/wiremod_fab.dmi' + icon_state = "module-fab-idle" + circuit = /obj/item/circuitboard/machine/module_duplicator + + /// The internal material bus + var/datum/component/remote_materials/materials + + density = TRUE + + var/list/scanned_designs = list() + + var/cost_per_component = 1000 + +/obj/machinery/module_duplicator/Initialize(mapload) + . = ..() + + materials = AddComponent( \ + /datum/component/remote_materials, \ + "module_duplicator", \ + mapload, \ + mat_container_flags = BREAKDOWN_FLAGS_LATHE, \ + ) + +/obj/machinery/module_duplicator/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ComponentPrinter", name) + ui.open() + +/obj/machinery/module_duplicator/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/spritesheet/sheetmaterials) + ) + +/obj/machinery/module_duplicator/ui_act(action, list/params) + . = ..() + if (.) + return + + switch (action) + if ("print") + var/design_id = text2num(params["designId"]) + + if (design_id < 1 || design_id > length(scanned_designs)) + return TRUE + + var/list/design = scanned_designs[design_id] + + if (materials.on_hold()) + say("Mineral access is on hold, please contact the quartermaster.") + return TRUE + + if (!materials.mat_container?.has_materials(design["materials"])) + say("Not enough materials.") + return TRUE + + balloon_alert_to_viewers("printed [design["name"]]") + materials.mat_container?.use_materials(design["materials"]) + materials.silo_log(src, "printed", -1, design["name"], design["materials"]) + print_module(design) + if ("remove_mat") + var/datum/material/material = locate(params["ref"]) + var/amount = text2num(params["amount"]) + + if (!amount) + return TRUE + + // SAFETY: eject_sheets checks for valid mats + materials.eject_sheets(material, amount) + + return TRUE + +/obj/machinery/module_duplicator/proc/print_module(list/design) + flick("module-fab-print", src) + addtimer(CALLBACK(src, .proc/finish_module_print, design), 1.6 SECONDS) + +/obj/machinery/module_duplicator/proc/finish_module_print(list/design) + var/obj/item/circuit_component/module/module = new(drop_location()) + module.load_data_from_list(design["dupe_data"]) + module.pixel_x = module.base_pixel_x + rand(-5, 5) + module.pixel_y = module.base_pixel_y + rand(-5, 5) + +/obj/machinery/module_duplicator/attackby(obj/item/weapon, mob/user, params) + if(!istype(weapon, /obj/item/circuit_component/module)) + return ..() + + var/obj/item/circuit_component/module/module = weapon + if(module.circuit_flags & CIRCUIT_FLAG_UNDUPEABLE) + balloon_alert(user, "module cannot be saved!") + return ..() + + if(module.display_name == initial(module.display_name)) + balloon_alert(user, "module needs a name!") + return ..() + + for(var/list/component_data as anything in scanned_designs) + if(component_data["name"] == module.display_name) + balloon_alert(user, "module name already exists!") + return ..() + + var/total_cost = 0 + for(var/obj/item/circuit_component/component as anything in module.internal_circuit.attached_components) + if(component.circuit_flags & CIRCUIT_FLAG_UNDUPEABLE) + balloon_alert(user, "module contains prohibited components!") + return ..() + + total_cost += cost_per_component + + var/list/data = list() + + data["dupe_data"] = list() + module.save_data_to_list(data["dupe_data"]) + + data["name"] = module.display_name + data["desc"] = "A module that has been loaded in by [user]." + data["materials"] = list(/datum/material/glass = total_cost) + + flick("module-fab-scan", src) + addtimer(CALLBACK(src, .proc/finish_module_scan, user, data), 1.4 SECONDS) + +/obj/machinery/module_duplicator/proc/finish_module_scan(mob/user, data) + scanned_designs += list(data) + + balloon_alert(user, "module has been saved.") + playsound(src, 'sound/machines/ping.ogg', 50) + +/obj/machinery/module_duplicator/ui_data(mob/user) + var/list/data = list() + data["materials"] = materials.mat_container.ui_data() + return data + +/obj/machinery/module_duplicator/ui_static_data(mob/user) + var/list/data = list() + + var/list/designs = list() + + var/index = 1 + for (var/list/design as anything in scanned_designs) + designs["[index]"] = list( + "name" = design["name"], + "description" = design["desc"], + "materials" = get_material_cost_data(design["materials"]), + "categories" = list("Circuitry"), + ) + index++ + + data["designs"] = designs + + return data + +/obj/machinery/module_duplicator/crowbar_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_crowbar(tool) + +/obj/machinery/module_duplicator/screwdriver_act(mob/living/user, obj/item/tool) + if(..()) + return TRUE + return default_deconstruction_screwdriver(user, "module-fab-o", "module-fab-idle", tool) + +/obj/machinery/module_duplicator/proc/get_material_cost_data(list/materials) + var/list/data = list() + + for (var/datum/material/material_type as anything in materials) + data[initial(material_type.name)] = materials[material_type] + + return data + +/obj/item/circuitboard/machine/module_duplicator + name = "\improper Module Duplicator (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_SCIENCE + build_path = /obj/machinery/module_duplicator + req_components = list( + /obj/item/stock_parts/matter_bin = 2, + /obj/item/stock_parts/manipulator = 2, + /obj/item/reagent_containers/glass/beaker = 2, + ) diff --git a/code/modules/wiremod/core/datatypes.dm b/code/modules/wiremod/core/datatypes.dm new file mode 100644 index 0000000000..b81ad0876b --- /dev/null +++ b/code/modules/wiremod/core/datatypes.dm @@ -0,0 +1,91 @@ +// An assoc list of all the possible datatypes. +GLOBAL_LIST_INIT(circuit_datatypes, generate_circuit_datatypes()) + +/proc/generate_circuit_datatypes() + var/list/datatypes_by_key = list() + for(var/datum/circuit_datatype/type as anything in subtypesof(/datum/circuit_datatype)) + if(!initial(type.datatype)) + continue + datatypes_by_key[initial(type.datatype)] = new type() + return datatypes_by_key + +/** + * A circuit datatype. Used to determine the datatype of a port and also handle any additional behaviour. + */ +/datum/circuit_datatype + /// The key. Used to identify the datatype. Should be a define. + var/datatype + + /// The color of the port in the UI. Doesn't work with hex colours. + var/color = "blue" + + /// The flags of the circuit datatype + var/datatype_flags = 0 + +/** + * Returns the value to be set for the port + * + * Used for implicit conversions between outputs and inputs (e.g. number -> string) + * and applying/removing signals on inputs + */ +/datum/circuit_datatype/proc/convert_value(datum/port/port, value_to_convert) + return value_to_convert + +/** + * Determines if a datatype is compatible with another port of a different type. + * Note: This is ALWAYS called on the input port, never on the output port. + * Inputs need to care about what types they're receiving, output ports don't have to care. + * + * Arguments: + * * datatype_to_check - The datatype to check + */ +/datum/circuit_datatype/proc/can_receive_from_datatype(datatype_to_check) + return datatype == datatype_to_check // This is already done by default on the input port. + +/** + * Called when the datatype is given to a port. + * + * Arguments: + * * gained_port - The gained port. + */ +/datum/circuit_datatype/proc/on_gain(datum/port/gained_port) + return + +/** + * Called when the datatype is removed from a port. + * + * Arguments: + * * lost_port - The removed port. + */ +/datum/circuit_datatype/proc/on_loss(datum/port/lost_port) + return + +/** + * Determines if a port is compatible with this datatype. + * This WILL throw a runtime if it returns false. This is for sanity checking and it should not return false + * unless under extraordinary circumstances or people fail to write proper code. + * + * Arguments: + * * port - The port to check if it is compatible. + */ +/datum/circuit_datatype/proc/is_compatible(datum/port/port) + return TRUE + +/** + * The data to send to the UI attached to the port. Received by the type in FUNDAMENTAL_PORT_TYPES + * + * Arguments: + * * port - The port sending the data. + */ +/datum/circuit_datatype/proc/datatype_ui_data(datum/port/port) + return + +/** + * When an input is manually set by a player. This is where extra sanitizing can happen. Will still call convert_value() + * + * Arguments: + * * port - The port sending the data. + * * + */ +/datum/circuit_datatype/proc/handle_manual_input(datum/port/input/port, mob/user, user_input) + return user_input diff --git a/code/modules/wiremod/core/duplicator.dm b/code/modules/wiremod/core/duplicator.dm new file mode 100644 index 0000000000..fd215255ba --- /dev/null +++ b/code/modules/wiremod/core/duplicator.dm @@ -0,0 +1,227 @@ +#define LOG_ERROR(list, error) if(list) { list.Add(error) } + +// Determines if a port can have a predefined input value if it is of this type. +GLOBAL_LIST_INIT(circuit_dupe_whitelisted_types, list( + PORT_TYPE_NUMBER, + PORT_TYPE_STRING, + PORT_TYPE_LIST, + PORT_TYPE_ANY, + PORT_TYPE_OPTION, +)) + +/// Loads a circuit based on json data at a location. Can also load usb connections, such as arrest consoles. +/obj/item/integrated_circuit/proc/load_circuit_data(json_data, list/errors) + var/list/general_data = json_decode(json_data) + + if(!general_data) + LOG_ERROR(errors, "Invalid json format!") + return + + if(general_data["display_name"]) + set_display_name(general_data["display_name"]) + + var/list/variable_data = general_data["variables"] + for(var/list/variable as anything in variable_data) + var/variable_name = variable["name"] + circuit_variables[variable_name] = new /datum/circuit_variable(variable_name, variable["datatype"]) + + admin_only = general_data["admin_only"] + + var/list/circuit_data = general_data["components"] + var/list/identifiers_to_circuit = list() + for(var/identifier in circuit_data) + var/list/component_data = circuit_data[identifier] + var/type = text2path(component_data["type"]) + if(!ispath(type, /obj/item/circuit_component)) + LOG_ERROR(errors, "Invalid path for circuit component, expected [/obj/item/circuit_component], got [type]") + continue + var/obj/item/circuit_component/component = load_component(type) + identifiers_to_circuit[identifier] = component + component.load_data_from_list(component_data) + + var/list/input_ports_data = component_data["input_ports_stored_data"] + for(var/port_name in input_ports_data) + var/datum/port/input/port + var/list/port_data = input_ports_data[port_name] + for(var/datum/port/input/port_to_check as anything in component.input_ports) + if(port_to_check.name == port_name) + port = port_to_check + break + + port.set_input(port_data["stored_data"]) + + var/list/external_objects = general_data["external_objects"] + for(var/identifier in external_objects) + var/list/object_data = external_objects[identifier] + var/type = text2path(object_data["type"]) + if(!ispath(type)) + LOG_ERROR(errors, "Invalid path for external object, expected a path, got [type]") + continue + var/atom/movable/object = new type(drop_location()) + var/list/connected_components = list() + for(var/component_id in object_data["connected_components"]) + var/obj/item/circuit_component/component = identifiers_to_circuit[component_id] + if(!component) + continue + connected_components += component + SEND_SIGNAL(object, COMSIG_MOVABLE_CIRCUIT_LOADED, src, connected_components) + + for(var/identifier in identifiers_to_circuit) + var/obj/item/circuit_component/component = identifiers_to_circuit[identifier] + var/list/component_data = circuit_data[identifier] + + var/list/connections = component_data["connections"] + for(var/port_name in connections) + var/datum/port/input/port + var/list/connection_data = connections[port_name] + for(var/datum/port/input/port_to_check as anything in component.input_ports) + if(port_to_check.name == port_name) + port = port_to_check + break + + if(!port) + LOG_ERROR(errors, "Port [port_name] not found for [component.type].") + continue + + if(connection_data["stored_data"]) + if(!(port.datatype in GLOB.circuit_dupe_whitelisted_types)) + continue + port.set_input(connection_data["stored_data"]) + continue + + // The || list(connected_data) is for backwards compatibility with when inputs could only be connected to up to one output. + for(var/list/output_data in (connection_data["connected_ports"] || list(connection_data))) + var/obj/item/circuit_component/connected_component = identifiers_to_circuit[output_data["component_id"]] + if(!connected_component) + LOG_ERROR(errors, "No connected component found for [component.type] for port [connection_data["port_name"]]. (connected component identifier: [connection_data["component_id"]])") + continue + + var/datum/port/output/output_port + var/output_port_name = output_data["port_name"] + for(var/datum/port/output/port_to_check as anything in connected_component.output_ports) + if(port_to_check.name == output_port_name) + output_port = port_to_check + break + + if(!output_port) + LOG_ERROR(errors, "No output port found for [component.type] for port [output_port_name] on component [connected_component.type]") + continue + + port.connect(output_port) + +#undef LOG_ERROR + +/// Converts a circuit into json. +/obj/item/integrated_circuit/proc/convert_to_json() + var/list/circuit_to_identifiers = list() + var/list/identifiers = list() + var/list/external_objects = list() // Objects that are connected to a component. These objects will be linked to the components. + for(var/obj/item/circuit_component/component as anything in attached_components) + var/identifier = "[component.type][length(identifiers)]" + identifiers += identifier + circuit_to_identifiers[component] = identifier + var/list/objects = list() + SEND_SIGNAL(component, COMSIG_CIRCUIT_COMPONENT_SAVE, objects) + + for(var/atom/movable/object as anything in objects) + if(object in external_objects) + external_objects[object] += identifier + continue + external_objects[object] = list(identifier) + + var/list/circuit_data = list() + for(var/obj/item/circuit_component/component as anything in circuit_to_identifiers) + var/identifier = circuit_to_identifiers[component] + var/list/component_data = list() + + component_data["type"] = component.type + + var/list/connections = list() + var/list/input_ports_stored_data = list() + for(var/datum/port/input/input as anything in component.input_ports) + var/list/connection_data = list() + if(!length(input.connected_ports)) + if(isnull(input.value) || !(input.datatype in GLOB.circuit_dupe_whitelisted_types)) + continue + connection_data["stored_data"] = input.value + input_ports_stored_data[input.name] = connection_data + continue + connection_data["connected_ports"] = list() + for(var/datum/port/output/output as anything in input.connected_ports) + connection_data["connected_ports"] += list(list( + "component_id" = circuit_to_identifiers[output.connected_component], + "port_name" = output.name, + )) + connections[input.name] = connection_data + component_data["connections"] = connections + component_data["input_ports_stored_data"] = input_ports_stored_data + + component.save_data_to_list(component_data) + circuit_data[identifier] = component_data + + var/external_objects_key = list() + for(var/atom/movable/object as anything in external_objects) + var/list/new_data = list() + new_data["type"] = object.type + new_data["connected_components"] = external_objects[object] + external_objects_key["[object.type][length(external_objects_key)]"] = new_data + + var/list/general_data = list() + general_data["components"] = circuit_data + general_data["external_objects"] = external_objects_key + general_data["display_name"] = display_name + general_data["admin_only"] = admin_only + + var/list/variables = list() + for(var/variable_identifier in circuit_variables) + var/list/new_data = list() + var/datum/circuit_variable/variable = circuit_variables[variable_identifier] + new_data["name"] = variable.name + new_data["datatype"] = variable.datatype + variables += list(new_data) + general_data["variables"] = variables + + return json_encode(general_data) + +/obj/item/integrated_circuit/proc/load_component(type) + var/obj/item/circuit_component/component = new type(src) + add_component(component) + return component + +/// Saves data to a list. Shouldn't be used unless you are quite literally saving the data of a component to a list. Input value is the list to save the data to +/obj/item/circuit_component/proc/save_data_to_list(list/component_data) + component_data["rel_x"] = rel_x + component_data["rel_y"] = rel_y + +/// Loads data from a list +/obj/item/circuit_component/proc/load_data_from_list(list/component_data) + rel_x = component_data["rel_x"] + rel_y = component_data["rel_y"] + +/client/proc/load_circuit() + set name = "Load Circuit" + set category = "Admin.Fun" + + if(!check_rights(R_VAREDIT)) + return + + var/list/errors = list() + + var/option = alert(usr, "Load by file or direct input?", "Load by file or string", "File", "Direct Input") + var/txt + switch(option) + if("File") + txt = file2text(tgui_input_num(usr, "Input File") as file|null) + if("Direct Input") + txt = tgui_input_num(usr, "Input JSON", "Input JSON") as text|null + + if(!txt) + return + + var/obj/item/integrated_circuit/loaded/circuit = new(mob.drop_location()) + circuit.load_circuit_data(txt, errors) + + if(length(errors)) + to_chat(src, span_warning("The following errors were found whilst compiling the circuit data:")) + for(var/error in errors) + to_chat(src, span_warning(error)) diff --git a/code/modules/wiremod/core/integrated_circuit.dm b/code/modules/wiremod/core/integrated_circuit.dm new file mode 100644 index 0000000000..f69caa7542 --- /dev/null +++ b/code/modules/wiremod/core/integrated_circuit.dm @@ -0,0 +1,600 @@ +/// A list of all integrated circuits +GLOBAL_LIST_EMPTY_TYPED(integrated_circuits, /obj/item/integrated_circuit) + +/** + * # Integrated Circuitboard + * + * A circuitboard that holds components that work together + * + * Has a limited amount of power. + */ +/obj/item/integrated_circuit + name = "integrated circuit" + desc = "By inserting components and a cell into this, wiring them up, and putting them into a shell, anyone can pretend to be a programmer." + icon = 'icons/obj/module.dmi' + icon_state = "integrated_circuit" + inhand_icon_state = "electronic" + + /// The name that appears on the shell. + var/display_name = "" + + /// The max length of the name. + var/label_max_length = 24 + + /// The power of the integrated circuit + var/obj/item/stock_parts/cell/cell + + /// The shell that this circuitboard is attached to. Used by components. + var/atom/movable/shell + + /// The attached components + var/list/obj/item/circuit_component/attached_components = list() + + /// Whether the integrated circuit is on or not. Handled by the shell. + var/on = FALSE + + /// Whether the integrated circuit is locked or not. Handled by the shell. + var/locked = FALSE + + /// Whether the integrated circuit is admin only. Disables power usage and allows admin circuits to be attached, at the cost of making it inaccessible to regular users. + var/admin_only = FALSE + + /// The ID that is authorized to unlock/lock the shell so that the circuit can/cannot be removed. + var/datum/weakref/owner_id + + /// The current examined component. Used in IntegratedCircuit UI + var/datum/weakref/examined_component + + /// Set by the shell. Holds the reference to the owner who inserted the component into the shell. + var/datum/weakref/inserter_mind + + /// Variables stored on this integrated circuit. with a `variable_name = value` structure + var/list/datum/circuit_variable/circuit_variables = list() + + /// The maximum amount of setters and getters a circuit can have + var/max_setters_and_getters = 30 + + /// The current setter and getter count the circuit has. + var/setter_and_getter_count = 0 + + /// X position of the examined_component + var/examined_rel_x = 0 + + /// Y position of the examined component + var/examined_rel_y = 0 + + /// The X position of the screen. Used for adding components + var/screen_x = 0 + + /// The Y position of the screen. Used for adding components. + var/screen_y = 0 + +/obj/item/integrated_circuit/Initialize() + . = ..() + + GLOB.integrated_circuits += src + + RegisterSignal(src, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, .proc/on_atom_usb_cable_try_attach) + +/obj/item/integrated_circuit/loaded/Initialize() + . = ..() + set_cell(new /obj/item/stock_parts/cell/high(src)) + +/obj/item/integrated_circuit/Destroy() + for(var/obj/item/circuit_component/to_delete in attached_components) + remove_component(to_delete) + qdel(to_delete) + QDEL_LIST(circuit_variables) + attached_components.Cut() + shell = null + examined_component = null + owner_id = null + QDEL_NULL(cell) + GLOB.integrated_circuits -= src + return ..() + +/obj/item/integrated_circuit/examine(mob/user) + . = ..() + if(cell) + . += span_notice("The charge meter reads [cell ? round(cell.percent(), 1) : 0]%.") + else + . += span_notice("There is no power cell installed.") + +/obj/item/integrated_circuit/proc/set_cell(obj/item/stock_parts/cell_to_set) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_CELL, cell_to_set) + cell = cell_to_set + +/obj/item/integrated_circuit/attackby(obj/item/I, mob/living/user, params) + . = ..() + if(istype(I, /obj/item/circuit_component)) + add_component_manually(I, user) + return + + if(istype(I, /obj/item/stock_parts/cell)) + if(cell) + balloon_alert(user, "there already is a cell inside!") + return + if(!user.transferItemToLoc(I, src)) + return + set_cell(I) + I.add_fingerprint(user) + user.visible_message(span_notice("[user] inserts a power cell into [src]."), span_notice("You insert the power cell into [src].")) + return + + if(istype(I, /obj/item/card/id)) + balloon_alert(user, "owner id set for [I]") + owner_id = WEAKREF(I) + return + + if(I.tool_behaviour == TOOL_SCREWDRIVER) + if(!cell) + return + I.play_tool_sound(src) + user.visible_message(span_notice("[user] unscrews the power cell from [src]."), span_notice("You unscrew the power cell from [src].")) + cell.forceMove(drop_location()) + set_cell(null) + return + +/** + * Registers an movable atom as a shell + * + * No functionality is done here. This is so that input components + * can properly register any signals on the shell. + * Arguments: + * * new_shell - The new shell to register. + */ +/obj/item/integrated_circuit/proc/set_shell(atom/movable/new_shell) + remove_current_shell() + set_on(TRUE) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_SHELL, new_shell) + shell = new_shell + RegisterSignal(shell, COMSIG_PARENT_QDELETING, .proc/remove_current_shell) + for(var/obj/item/circuit_component/attached_component as anything in attached_components) + attached_component.register_shell(shell) + // Their input ports may be updated with user values, but the outputs haven't updated + // because on is FALSE + TRIGGER_CIRCUIT_COMPONENT(attached_component, null) + +/** + * Unregisters the current shell attached to this circuit. + */ +/obj/item/integrated_circuit/proc/remove_current_shell() + SIGNAL_HANDLER + if(!shell) + return + shell.name = initial(shell.name) + for(var/obj/item/circuit_component/attached_component as anything in attached_components) + attached_component.unregister_shell(shell) + UnregisterSignal(shell, COMSIG_PARENT_QDELETING) + shell = null + set_on(FALSE) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SHELL_REMOVED) + +/obj/item/integrated_circuit/proc/set_on(new_value) + SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_ON, new_value) + on = new_value + +/** + * Adds a component to the circuitboard + * + * Once the component is added, the ports can be attached to other components + */ +/obj/item/integrated_circuit/proc/add_component(obj/item/circuit_component/to_add, mob/living/user) + if(to_add.parent) + return + + if(SEND_SIGNAL(src, COMSIG_CIRCUIT_ADD_COMPONENT, to_add, user) & COMPONENT_CANCEL_ADD_COMPONENT) + return + + if(!to_add.add_to(src)) + return + + var/success = FALSE + if(user) + success = user.transferItemToLoc(to_add, src) + else + success = to_add.forceMove(src) + + if(!success) + return + + to_add.rel_x = rand(COMPONENT_MIN_RANDOM_POS, COMPONENT_MAX_RANDOM_POS) - screen_x + to_add.rel_y = rand(COMPONENT_MIN_RANDOM_POS, COMPONENT_MAX_RANDOM_POS) - screen_y + to_add.parent = src + attached_components += to_add + RegisterSignal(to_add, COMSIG_MOVABLE_MOVED, .proc/component_move_handler) + SStgui.update_uis(src) + + if(shell) + to_add.register_shell(shell) + return TRUE + +/** + * Adds a component to the circuitboard through a manual action. + */ +/obj/item/integrated_circuit/proc/add_component_manually(obj/item/circuit_component/to_add, mob/living/user) + if (SEND_SIGNAL(src, COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY, to_add, user) & COMPONENT_CANCEL_ADD_COMPONENT) + return + + return add_component(to_add, user) + +/obj/item/integrated_circuit/proc/component_move_handler(obj/item/circuit_component/source) + SIGNAL_HANDLER + if(source.loc != src) + remove_component(source) + +/** + * Removes a component to the circuitboard + * + * This removes all connects between the ports + */ +/obj/item/integrated_circuit/proc/remove_component(obj/item/circuit_component/to_remove) + if(shell) + to_remove.unregister_shell(shell) + + UnregisterSignal(to_remove, COMSIG_MOVABLE_MOVED) + attached_components -= to_remove + to_remove.disconnect() + to_remove.parent = null + SEND_SIGNAL(to_remove, COMSIG_CIRCUIT_COMPONENT_REMOVED, src) + SStgui.update_uis(src) + to_remove.removed_from(src) + +/obj/item/integrated_circuit/get_cell() + return cell + +/obj/item/integrated_circuit/ui_assets(mob/user) + return list( + get_asset_datum(/datum/asset/simple/circuit_assets) + ) + +/obj/item/integrated_circuit/ui_static_data(mob/user) + . = list() + .["global_basic_types"] = GLOB.wiremod_basic_types + .["screen_x"] = screen_x + .["screen_y"] = screen_y + +/obj/item/integrated_circuit/ui_data(mob/user) + . = list() + .["components"] = list() + for(var/obj/item/circuit_component/component as anything in attached_components) + if (component.circuit_flags & CIRCUIT_FLAG_HIDDEN) + .["components"] += null + continue + + var/list/component_data = list() + component_data["input_ports"] = list() + for(var/datum/port/input/port as anything in component.input_ports) + var/current_data = port.value + if(isatom(current_data)) // Prevent passing the name of the atom. + current_data = null + var/list/connected_to = list() + for(var/datum/port/output/output as anything in port.connected_ports) + connected_to += REF(output) + component_data["input_ports"] += list(list( + "name" = port.name, + "type" = port.datatype, + "ref" = REF(port), // The ref is the identifier to work out what it is connected to + "connected_to" = connected_to, + "color" = port.color, + "current_data" = current_data, + "datatype_data" = port.datatype_ui_data(user), + )) + component_data["output_ports"] = list() + for(var/datum/port/output/port as anything in component.output_ports) + component_data["output_ports"] += list(list( + "name" = port.name, + "type" = port.datatype, + "ref" = REF(port), + "color" = port.color, + )) + + component_data["name"] = component.display_name + component_data["x"] = component.rel_x + component_data["y"] = component.rel_y + component_data["removable"] = component.removable + .["components"] += list(component_data) + + .["variables"] = list() + for(var/variable_name in circuit_variables) + var/datum/circuit_variable/variable = circuit_variables[variable_name] + var/list/variable_data = list() + variable_data["name"] = variable.name + variable_data["datatype"] = variable.datatype + variable_data["color"] = variable.color + .["variables"] += list(variable_data) + + + .["display_name"] = display_name + + var/obj/item/circuit_component/examined + if(examined_component) + examined = examined_component.resolve() + + .["examined_name"] = examined?.display_name + .["examined_desc"] = examined?.desc + .["examined_notices"] = examined?.get_ui_notices() + .["examined_rel_x"] = examined_rel_x + .["examined_rel_y"] = examined_rel_y + + .["is_admin"] = check_rights_for(user.client, R_VAREDIT) + +/obj/item/integrated_circuit/ui_host(mob/user) + if(shell) + return shell + return ..() + +/obj/item/integrated_circuit/can_interact(mob/user) + if(locked) + return FALSE + return ..() + +/obj/item/integrated_circuit/ui_status(mob/user) + . = ..() + + if (isobserver(user)) + . = max(., UI_UPDATE) + + // Extra protection because ui_state will not close the UI if they already have the ui open, + // as ui_state is only set during + if(admin_only) + if(!check_rights_for(user.client, R_VAREDIT)) + return UI_CLOSE + else + return UI_INTERACTIVE + +/obj/item/integrated_circuit/ui_state(mob/user) + if(!shell) + return GLOB.hands_state + return GLOB.physical_obscured_state + +/obj/item/integrated_circuit/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "IntegratedCircuit", name) + ui.open() + ui.set_autoupdate(FALSE) + +#define WITHIN_RANGE(id, table) (id >= 1 && id <= length(table)) + +/obj/item/integrated_circuit/ui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("add_connection") + var/input_component_id = text2num(params["input_component_id"]) + var/output_component_id = text2num(params["output_component_id"]) + var/input_port_id = text2num(params["input_port_id"]) + var/output_port_id = text2num(params["output_port_id"]) + if(!WITHIN_RANGE(input_component_id, attached_components) || !WITHIN_RANGE(output_component_id, attached_components)) + return + var/obj/item/circuit_component/input_component = attached_components[input_component_id] + var/obj/item/circuit_component/output_component = attached_components[output_component_id] + + if(!WITHIN_RANGE(input_port_id, input_component.input_ports) || !WITHIN_RANGE(output_port_id, output_component.output_ports)) + return + var/datum/port/input/input_port = input_component.input_ports[input_port_id] + var/datum/port/output/output_port = output_component.output_ports[output_port_id] + + if(!input_port.can_receive_from_datatype(output_port.datatype)) + return + input_port.connect(output_port) + . = TRUE + if("remove_connection") + var/component_id = text2num(params["component_id"]) + var/is_input = params["is_input"] + var/port_id = text2num(params["port_id"]) + + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + + var/list/port_table + if(is_input) + port_table = component.input_ports + else + port_table = component.output_ports + + if(!WITHIN_RANGE(port_id, port_table)) + return + + var/datum/port/port = port_table[port_id] + port.disconnect_all() + . = TRUE + if("detach_component") + var/component_id = text2num(params["component_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + if(!component.removable) + return + component.disconnect() + remove_component(component) + if(component.loc == src) + usr.put_in_hands(component) + . = TRUE + if("set_component_coordinates") + var/component_id = text2num(params["component_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + component.rel_x = min(max(-COMPONENT_MAX_POS, text2num(params["rel_x"])), COMPONENT_MAX_POS) + component.rel_y = min(max(-COMPONENT_MAX_POS, text2num(params["rel_y"])), COMPONENT_MAX_POS) + . = TRUE + if("set_component_input") + var/component_id = text2num(params["component_id"]) + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + if(!WITHIN_RANGE(port_id, component.input_ports)) + return + var/datum/port/input/port = component.input_ports[port_id] + + if(params["set_null"]) + port.set_input(null) + return TRUE + + if(params["marked_atom"]) + if(port.datatype != PORT_TYPE_ATOM && port.datatype != PORT_TYPE_ANY) + return + var/obj/item/multitool/circuit/marker = usr.get_active_held_item() + if(!istype(marker)) + var/client/user = usr.client + if(!check_rights_for(user, R_VAREDIT)) + return TRUE + var/atom/marked_atom = user.holder.marked_datum + if(!marked_atom) + return TRUE + port.set_input(marked_atom) + balloon_alert(usr, "updated [port.name]'s value to marked object.") + return TRUE + if(!marker.marked_atom) + port.set_input(null) + marker.say("Cleared port ('[port.name]')'s value.") + return TRUE + marker.say("Updated port ('[port.name]')'s value to the marked entity.") + port.set_input(marker.marked_atom) + return TRUE + + var/user_input = port.handle_manual_input(usr, params["input"]) + if(isnull(user_input)) + return TRUE + port.set_input(user_input) + . = TRUE + if("get_component_value") + var/component_id = text2num(params["component_id"]) + var/port_id = text2num(params["port_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + var/obj/item/circuit_component/component = attached_components[component_id] + if(!WITHIN_RANGE(port_id, component.output_ports)) + return + + var/datum/port/output/port = component.output_ports[port_id] + var/value = port.value + if(isatom(value)) + value = PORT_TYPE_ATOM + else if(isnull(value)) + value = "null" + var/string_form = copytext("[value]", 1, PORT_MAX_STRING_DISPLAY) + if(length(string_form) >= PORT_MAX_STRING_DISPLAY-1) + string_form += "..." + balloon_alert(usr, "[port.name] value: [string_form]") + . = TRUE + if("set_display_name") + var/new_name = params["display_name"] + + if(new_name) + set_display_name(strip_html(params["display_name"], label_max_length)) + else + set_display_name("") + + if(shell) + if(display_name != "") + shell.name = "[initial(shell.name)] ([display_name])" + else + shell.name = initial(shell.name) + + . = TRUE + if("set_examined_component") + var/component_id = text2num(params["component_id"]) + if(!WITHIN_RANGE(component_id, attached_components)) + return + examined_component = WEAKREF(attached_components[component_id]) + examined_rel_x = text2num(params["x"]) + examined_rel_y = text2num(params["y"]) + . = TRUE + if("remove_examined_component") + examined_component = null + . = TRUE + if("save_circuit") + return attempt_save_to(usr.client) + if("add_variable") + var/variable_identifier = trim(copytext(params["variable_name"], 1, PORT_MAX_NAME_LENGTH)) + if(variable_identifier in circuit_variables) + return TRUE + if(variable_identifier == "") + return TRUE + var/variable_datatype = params["variable_datatype"] + if(!(variable_datatype in GLOB.wiremod_basic_types)) + return + + circuit_variables[variable_identifier] = new /datum/circuit_variable(variable_identifier, variable_datatype) + . = TRUE + if("remove_variable") + var/variable_identifier = params["variable_name"] + if(!(variable_identifier in circuit_variables)) + return + var/datum/circuit_variable/variable = circuit_variables[variable_identifier] + if(!variable) + return + circuit_variables -= variable_identifier + qdel(variable) + . = TRUE + if("add_setter_or_getter") + if(setter_and_getter_count >= max_setters_and_getters) + balloon_alert(usr, "setter and getter count at maximum capacity") + return + var/designated_type = /obj/item/circuit_component/getter + if(params["is_setter"]) + designated_type = /obj/item/circuit_component/setter + var/obj/item/circuit_component/component = new designated_type(src) + if(!add_component(component, usr)) + qdel(component) + return + RegisterSignal(component, COMSIG_CIRCUIT_COMPONENT_REMOVED, .proc/clear_setter_or_getter) + setter_and_getter_count++ + if("move_screen") + screen_x = text2num(params["screen_x"]) + screen_y = text2num(params["screen_y"]) + +/obj/item/integrated_circuit/proc/clear_setter_or_getter(datum/source) + SIGNAL_HANDLER + // This'll also be called in the Destroy() override of /obj/item/circuit_component + if(!QDELING(source)) + qdel(source) + setter_and_getter_count-- + +/obj/item/integrated_circuit/proc/on_atom_usb_cable_try_attach(datum/source, obj/item/usb_cable/usb_cable, mob/user) + SIGNAL_HANDLER + usb_cable.balloon_alert(user, "circuit needs to be in a compatible shell") + return COMSIG_CANCEL_USB_CABLE_ATTACK + +#undef WITHIN_RANGE + +/// Sets the display name that appears on the shell. +/obj/item/integrated_circuit/proc/set_display_name(new_name) + display_name = new_name + +/** + * Returns the creator of the integrated circuit. Used in admin messages and other related things. + */ +/obj/item/integrated_circuit/proc/get_creator_admin() + return get_creator(include_link = TRUE) + +/** + * Returns the creator of the integrated circuit. Used in admin logs and other related things. + */ +/obj/item/integrated_circuit/proc/get_creator(include_link = FALSE) + var/datum/mind/inserter + if(inserter_mind) + inserter = inserter_mind.resolve() + + var/obj/item/card/id/id_card + if(owner_id) + id_card = owner_id.resolve() + + return "[src] (Shell: [shell || "*null*"], Inserter: [key_name(inserter, include_link)], Owner ID: [id_card?.name || "*null*"])" + +/// Attempts to save a circuit to a given client +/obj/item/integrated_circuit/proc/attempt_save_to(client/saver) + if(!check_rights_for(saver, R_VAREDIT)) + return FALSE + var/temp_file = file("data/CircuitDownloadTempFile") + fdel(temp_file) + WRITE_FILE(temp_file, convert_to_json()) + DIRECT_OUTPUT(saver, ftp(temp_file, "[display_name || "circuit"].json")) + return TRUE diff --git a/code/modules/wiremod/core/marker.dm b/code/modules/wiremod/core/marker.dm new file mode 100644 index 0000000000..82d34be37a --- /dev/null +++ b/code/modules/wiremod/core/marker.dm @@ -0,0 +1,60 @@ +/obj/item/multitool/circuit + name = "circuit multitool" + desc = "A circuit multitool. Used to mark entities which can then be uploaded to components by pressing the upload button on a port. \ + Acts as a normal multitool otherwise. Use in hand to clear marked entity so that you can mark another entity." + icon_state = "multitool_circuit" + + /// The marked atom of this multitool + var/atom/marked_atom + +/obj/item/multitool/circuit/Destroy() + marked_atom = null + return ..() + +/obj/item/multitool/circuit/examine(mob/user) + . = ..() + . += span_notice("It has [marked_atom? "a" : "no"] marked entity registered.") + +/obj/item/multitool/circuit/attack_self(mob/user, modifiers) + . = ..() + if(.) + return + if(!marked_atom) + return + + say("Cleared marked targets.") + clear_marked_atom() + return TRUE + +/obj/item/multitool/circuit/melee_attack_chain(mob/user, atom/target, params) + var/is_right_clicking = LAZYACCESS(params2list(params), RIGHT_CLICK) + + if(marked_atom || !user.Adjacent(target) || is_right_clicking) + return ..() + + say("Marked [target].") + marked_atom = target + RegisterSignal(marked_atom, COMSIG_PARENT_QDELETING, .proc/cleanup_marked_atom) + update_icon() + flick("multitool_circuit_flick", src) + playsound(src.loc, 'sound/misc/compiler-stage2.ogg', 30, TRUE) + return TRUE + +/obj/item/multitool/circuit/update_overlays() + . = ..() + cut_overlays() + if(marked_atom) + . += "marked_overlay" + +/// Clears the current marked atom +/obj/item/multitool/circuit/proc/clear_marked_atom() + if(!marked_atom) + return + UnregisterSignal(marked_atom, COMSIG_PARENT_QDELETING) + marked_atom = null + update_icon() + +/obj/item/multitool/circuit/proc/cleanup_marked_atom(datum/source) + SIGNAL_HANDLER + if(source == marked_atom) + clear_marked_atom() diff --git a/code/modules/wiremod/core/port.dm b/code/modules/wiremod/core/port.dm new file mode 100644 index 0000000000..a58016743f --- /dev/null +++ b/code/modules/wiremod/core/port.dm @@ -0,0 +1,216 @@ +/** + * # Component Port + * + * A port used by a component. Connects to other ports. + */ +/datum/port + /// The component this port is attached to + var/obj/item/circuit_component/connected_component + + /// Name of the port. Used when displaying the port. + var/name + + /// The port type. Ports can only connect to each other if the type matches + var/datatype + + /// The value that's currently in the port. It's of the above type. + var/value + + /// The default port type. Stores the original datatype of the port set on Initialize. + var/datum/circuit_datatype/datatype_handler + + /// The port color. If unset, appears as blue. + var/color + +/datum/port/New(obj/item/circuit_component/to_connect, name, datatype) + if(!to_connect) + qdel(src) + return + . = ..() + connected_component = to_connect + src.name = name + set_datatype(datatype) + +/datum/port/Destroy(force) + disconnect_all() + connected_component = null + datatype_handler = null + return ..() + +/** + * Sets the port's value to value. + * Casts to the port's datatype (e.g. number -> string), and assumes this can be done. + */ +/datum/port/proc/set_value(value, force = FALSE) + if(src.value != value || force) + if(isatom(value)) + UnregisterSignal(value, COMSIG_PARENT_QDELETING) + src.value = datatype_handler.convert_value(src, value) + if(isatom(value)) + RegisterSignal(value, COMSIG_PARENT_QDELETING, .proc/null_value) + SEND_SIGNAL(src, COMSIG_PORT_SET_VALUE, value) + +/** + * Updates the value of the input and calls input_received on the connected component + */ +/datum/port/input/proc/set_input(value) + if(QDELETED(src)) //Pain + return + set_value(value) + if(trigger) + TRIGGER_CIRCUIT_COMPONENT(connected_component, src) + +/datum/port/output/proc/set_output(value) + set_value(value) + +/** + * Sets the datatype of the port. + * + * Arguments: + * * new_type - The type this port is to be set to. + */ +/datum/port/proc/set_datatype(type_to_set) + if(type_to_set == datatype) + return + + if(datatype_handler) + datatype_handler.on_loss(src) + datatype_handler = null + + var/datum/circuit_datatype/handler = GLOB.circuit_datatypes[type_to_set] + if(!handler || !handler.is_compatible(src)) + type_to_set = PORT_TYPE_ANY + handler = GLOB.circuit_datatypes[type_to_set] + // We can't leave this port without a type or else it'll just keep spewing out unnecessary and unneeded runtimes as well as leaving the circuit in a broken state. + stack_trace("[src] port attempted to be set to an incompatible datatype! (target datatype to set: [type_to_set])") + + datatype = type_to_set + datatype_handler = handler + color = datatype_handler.color + datatype_handler.on_gain(src) + src.value = datatype_handler.convert_value(src, value) + SEND_SIGNAL(src, COMSIG_PORT_SET_TYPE, type_to_set) + if(connected_component?.parent) + SStgui.update_uis(connected_component.parent) + +/datum/port/input/set_datatype(new_type) + for(var/datum/port/output/output as anything in connected_ports) + check_type(output) + ..() + +/** + * Returns the data from the datatype + */ +/datum/port/proc/datatype_ui_data() + return datatype_handler.datatype_ui_data(src) + +/** + * # Output Port + * + * An output port that many input ports can connect to + * + * Sends a signal whenever the output value is changed + */ +/datum/port/output + +/** + * Disconnects a port from all other ports. + * + * Called by [/obj/item/circuit_component] whenever it is disconnected from + * an integrated circuit + */ +/datum/port/proc/disconnect_all() + SEND_SIGNAL(src, COMSIG_PORT_DISCONNECT) + +/datum/port/input/disconnect_all() + ..() + for(var/datum/port/output/output as anything in connected_ports) + disconnect(output) + +/datum/port/input/proc/disconnect(datum/port/output/output) + connected_ports -= output + UnregisterSignal(output, COMSIG_PORT_SET_VALUE) + UnregisterSignal(output, COMSIG_PORT_SET_TYPE) + UnregisterSignal(output, COMSIG_PORT_DISCONNECT) + +/// Do our part in setting all source references anywhere to null. +/datum/port/proc/on_value_qdeleting(datum/source) + SIGNAL_HANDLER + if(value == source) + value = null + else + stack_trace("Impossible? [src] should only receive COMSIG_PARENT_QDELETING from an atom currently in the port, not [source].") + +/** + * # Input Port + * + * An input port remembers connected output ports. + * + * Registers the PORT_SET_VALUE signal on each connected port, + * and keeps its value equal to the last such signal received. + */ +/datum/port/input + /// Whether this port triggers an update whenever an output is received. + var/trigger = FALSE + + /// The ports this port is wired to. + var/list/datum/port/output/connected_ports + +/datum/port/input/New(obj/item/circuit_component/to_connect, name, datatype, trigger, default) + . = ..() + set_value(default) + src.trigger = trigger + src.connected_ports = list() + +/** + * Introduces two ports to one another. + */ +/datum/port/input/proc/connect(datum/port/output/output) + connected_ports |= output + RegisterSignal(output, COMSIG_PORT_SET_VALUE, .proc/receive_value) + RegisterSignal(output, COMSIG_PORT_SET_TYPE, .proc/check_type) + RegisterSignal(output, COMSIG_PORT_DISCONNECT, .proc/disconnect) + // For signals, we don't update the input to prevent sending a signal when connecting ports. + if(!(datatype_handler.datatype_flags & DATATYPE_FLAG_AVOID_VALUE_UPDATE)) + set_input(output.value) + +/** + * Determines if a datatype is compatible with another port of a different type. + * + * Arguments: + * * other_datatype - The datatype to check + */ +/datum/port/input/proc/can_receive_from_datatype(datatype_to_check) + return datatype_handler.can_receive_from_datatype(datatype_to_check) + +/** + * Determines if a datatype is compatible with another port of a different type. + * + * Arguments: + * * other_datatype - The datatype to check + */ +/datum/port/input/proc/handle_manual_input(mob/user, manual_input) + if(datatype_handler.datatype_flags & DATATYPE_FLAG_ALLOW_MANUAL_INPUT) + return datatype_handler.handle_manual_input(src, user, manual_input) + return null + +/** + * Mirror value updates from connected output ports after an input_receive_delay. + */ +/datum/port/input/proc/receive_value(datum/port/output/output, value) + SIGNAL_HANDLER + SScircuit_component.add_callback(CALLBACK(src, .proc/set_input, value)) + +/// Signal handler proc to null the input if an atom is deleted. An update is not sent because this was not set by anything. +/datum/port/proc/null_value(datum/source) + SIGNAL_HANDLER + if(value == source) + value = null + +/** + * Handle type updates from connected output ports, breaking uncastable connections. + */ +/datum/port/input/proc/check_type(datum/port/output/output) + SIGNAL_HANDLER + if(!can_receive_from_datatype(output.datatype)) + disconnect(output) diff --git a/code/modules/wiremod/core/usb_cable.dm b/code/modules/wiremod/core/usb_cable.dm new file mode 100644 index 0000000000..df0dec936c --- /dev/null +++ b/code/modules/wiremod/core/usb_cable.dm @@ -0,0 +1,123 @@ +/// A cable that can connect integrated circuits to anything with a USB port, such as computers and machines. +/obj/item/usb_cable + name = "usb cable" + desc = "A cable that can connect integrated circuits to anything with a USB port, such as computers and machines." + icon = 'icons/obj/wiremod.dmi' + icon_state = "usb_cable" + inhand_icon_state = "coil" + base_icon_state = "coil" + w_class = WEIGHT_CLASS_TINY + custom_materials = list(/datum/material/iron = 75) + + /// The currently connected circuit + var/obj/item/integrated_circuit/attached_circuit + +/obj/item/usb_cable/Destroy() + attached_circuit = null + STOP_PROCESSING(SSobj, src) + return ..() + +/obj/item/usb_cable/Initialize() + . = ..() + RegisterSignal(src, COMSIG_MOVABLE_MOVED, .proc/on_moved) + +/obj/item/usb_cable/examine(mob/user) + . = ..() + + if (!isnull(attached_circuit)) + . += span_notice("It is attached to [attached_circuit.shell || attached_circuit].") + +// Look, I'm not happy about this either, but moving an object doesn't call Moved if it's inside something else. +// There's good reason for this, but there's no element or similar yet to track it as far as I know. +// SSobj runs infrequently, this is only ran while there's an attached circuit, its performance cost is negligible. +/obj/item/usb_cable/process(delta_time) + if (!check_in_range()) + return PROCESS_KILL + +/obj/item/usb_cable/pre_attack(atom/target, mob/living/user, params) + . = ..() + if (.) + return + + if (prob(1)) + balloon_alert(user, "wrong way, god damnit") + return TRUE + + var/signal_result = SEND_SIGNAL(target, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, src, user) + + var/last_attached_circuit = attached_circuit + if (signal_result & COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT) + if (isnull(attached_circuit)) + CRASH("Producers of COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT must set attached_circuit") + balloon_alert(user, "connected to circuit\nconnect to a port") + + playsound(src, 'sound/machines/pda_button1.ogg', 20, TRUE) + + if (last_attached_circuit != attached_circuit) + if (!isnull(last_attached_circuit)) + unregister_circuit_signals(last_attached_circuit) + register_circuit_signals() + + START_PROCESSING(SSobj, src) + + return TRUE + + if (signal_result & COMSIG_USB_CABLE_ATTACHED) + // Short messages are better to read + var/connection_description = "port" + if (istype(target, /obj/machinery/computer)) + connection_description = "computer" + else if (ismachinery(target)) + connection_description = "machine" + + balloon_alert(user, "connected to [connection_description]") + playsound(src, 'sound/items/screwdriver2.ogg', 20, TRUE) + + return TRUE + + if (signal_result & COMSIG_CANCEL_USB_CABLE_ATTACK) + return TRUE + + return FALSE + +/obj/item/usb_cable/suicide_act(mob/user) + user.visible_message(span_suicide("[user] is wrapping [src] around [user.p_their()] neck! It looks like [user.p_theyre()] trying to commit suicide!")) + return OXYLOSS + +/obj/item/usb_cable/proc/register_circuit_signals() + RegisterSignal(attached_circuit, COMSIG_MOVABLE_MOVED, .proc/on_moved) + RegisterSignal(attached_circuit, COMSIG_PARENT_QDELETING, .proc/on_circuit_qdeling) + RegisterSignal(attached_circuit.shell, COMSIG_MOVABLE_MOVED, .proc/on_moved) + +/obj/item/usb_cable/proc/unregister_circuit_signals(obj/item/integrated_circuit/old_circuit) + UnregisterSignal(attached_circuit, list( + COMSIG_MOVABLE_MOVED, + COMSIG_PARENT_QDELETING, + )) + + UnregisterSignal(attached_circuit.shell, COMSIG_MOVABLE_MOVED) + +/obj/item/usb_cable/proc/on_moved() + SIGNAL_HANDLER + + check_in_range() + +/obj/item/usb_cable/proc/check_in_range() + if (isnull(attached_circuit)) + STOP_PROCESSING(SSobj, src) + return FALSE + + if (!IN_GIVEN_RANGE(attached_circuit, src, USB_CABLE_MAX_RANGE)) + balloon_alert_to_viewers("detached, too far away") + unregister_circuit_signals(attached_circuit) + attached_circuit = null + STOP_PROCESSING(SSobj, src) + return FALSE + + return TRUE + +/obj/item/usb_cable/proc/on_circuit_qdeling() + SIGNAL_HANDLER + + attached_circuit = null + STOP_PROCESSING(SSobj, src) diff --git a/code/modules/wiremod/core/variable.dm b/code/modules/wiremod/core/variable.dm new file mode 100644 index 0000000000..62d5239ade --- /dev/null +++ b/code/modules/wiremod/core/variable.dm @@ -0,0 +1,49 @@ +/** + * A circuit variable that holds the name, the datatype and the colour of the variable (taken from the datatype). + * + * Used in integrated circuits for setter and getter circuit components. + */ +/datum/circuit_variable + /// The display name of the circuit variable + var/name + + /// The datatype of the circuit variable. Used by the setter and getter circuit components + var/datatype + + /// The colour that appears in the UI. The value is set to the datatype's matching colour + var/color + + /// The current value held by the variable. + var/value + + /// The components that are currently listening. Triggers them when the value is updated. + var/list/obj/item/circuit_component/listeners + +/datum/circuit_variable/New(name, datatype) + . = ..() + src.name = name + src.datatype = datatype + + var/datum/circuit_datatype/circuit_datatype = GLOB.circuit_datatypes[datatype] + + src.listeners = list() + src.color = circuit_datatype.color + + +/datum/circuit_variable/Destroy(force, ...) + listeners = null + return ..() + +/// Sets the value of the circuit component and triggers the appropriate listeners +/datum/circuit_variable/proc/set_value(new_value) + value = new_value + for(var/obj/item/circuit_component/component as anything in listeners) + TRIGGER_CIRCUIT_COMPONENT(component, null) + +/// Adds a listener to receive inputs when the variable has a value that is set. +/datum/circuit_variable/proc/add_listener(obj/item/circuit_component/to_add) + listeners += to_add + +/// Removes a listener to receive inputs when the variable has a value that is set. Listener will usually clean themselves up +/datum/circuit_variable/proc/remove_listener(obj/item/circuit_component/to_remove) + listeners -= to_remove diff --git a/code/modules/wiremod/datatypes/any.dm b/code/modules/wiremod/datatypes/any.dm new file mode 100644 index 0000000000..a8a2f8457f --- /dev/null +++ b/code/modules/wiremod/datatypes/any.dm @@ -0,0 +1,10 @@ +/datum/circuit_datatype/any + datatype = PORT_TYPE_ANY + color = "blue" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/any/can_receive_from_datatype(datatype_to_check) + return TRUE + +/datum/circuit_datatype/any/handle_manual_input(datum/port/input/port, mob/user, user_input) + return text2num(user_input) || user_input diff --git a/code/modules/wiremod/datatypes/basic.dm b/code/modules/wiremod/datatypes/basic.dm new file mode 100644 index 0000000000..d112f2b9d9 --- /dev/null +++ b/code/modules/wiremod/datatypes/basic.dm @@ -0,0 +1,9 @@ +// This file is for types that do not have any special conversion behaviour + +/datum/circuit_datatype/list_type + datatype = PORT_TYPE_LIST + color = "white" + +/datum/circuit_datatype/table + datatype = PORT_TYPE_TABLE + color = "grey" diff --git a/code/modules/wiremod/datatypes/entity.dm b/code/modules/wiremod/datatypes/entity.dm new file mode 100644 index 0000000000..44e00b9c6c --- /dev/null +++ b/code/modules/wiremod/datatypes/entity.dm @@ -0,0 +1,10 @@ +/datum/circuit_datatype/entity + datatype = PORT_TYPE_ATOM + color = "purple" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/entity/convert_value(datum/port/port, value_to_convert) + var/atom/object = value_to_convert + if(QDELETED(object)) + return null + return object diff --git a/code/modules/wiremod/datatypes/number.dm b/code/modules/wiremod/datatypes/number.dm new file mode 100644 index 0000000000..7013340249 --- /dev/null +++ b/code/modules/wiremod/datatypes/number.dm @@ -0,0 +1,14 @@ +/datum/circuit_datatype/number + datatype = PORT_TYPE_NUMBER + color = "green" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/number/can_receive_from_datatype(datatype_to_check) + . = ..() + if(.) + return + + return datatype_to_check == PORT_TYPE_NUMBER + +/datum/circuit_datatype/number/handle_manual_input(datum/port/input/port, mob/user, user_input) + return text2num(user_input) diff --git a/code/modules/wiremod/datatypes/option.dm b/code/modules/wiremod/datatypes/option.dm new file mode 100644 index 0000000000..c6de346c9d --- /dev/null +++ b/code/modules/wiremod/datatypes/option.dm @@ -0,0 +1,33 @@ +/datum/port/input/option + var/list/possible_options + +/datum/port/input/option/New(obj/item/circuit_component/to_connect, name, datatype, trigger, default, possible_options) + . = ..() + src.possible_options = possible_options + set_value(default, force = TRUE) + +/datum/circuit_datatype/option + datatype = PORT_TYPE_OPTION + color = "violet" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/option/is_compatible(datum/port/gained_port) + return istype(gained_port, /datum/port/input/option) + +/datum/circuit_datatype/option/can_receive_from_datatype(datatype_to_check) + . = ..() + if(.) + return + + return datatype_to_check == PORT_TYPE_STRING + +/datum/circuit_datatype/option/convert_value(datum/port/input/option/port, value_to_convert) + if(!port.possible_options) + return null + + if(value_to_convert in port.possible_options) + return value_to_convert + return port.possible_options[1] + +/datum/circuit_datatype/option/datatype_ui_data(datum/port/input/option/port) + return port.possible_options diff --git a/code/modules/wiremod/datatypes/signal.dm b/code/modules/wiremod/datatypes/signal.dm new file mode 100644 index 0000000000..2226f509fc --- /dev/null +++ b/code/modules/wiremod/datatypes/signal.dm @@ -0,0 +1,17 @@ +/datum/circuit_datatype/signal + datatype = PORT_TYPE_SIGNAL + color = "teal" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT|DATATYPE_FLAG_AVOID_VALUE_UPDATE + +/datum/circuit_datatype/signal/can_receive_from_datatype(datatype_to_check) + . = ..() + if(.) + return + + return datatype_to_check == PORT_TYPE_NUMBER + +/datum/circuit_datatype/signal/handle_manual_input(datum/port/input/port, mob/user, user_input) + var/atom/parent = port.connected_component + if(parent) + parent.balloon_alert(user, "triggered [port.name]") + return COMPONENT_SIGNAL diff --git a/code/modules/wiremod/datatypes/string.dm b/code/modules/wiremod/datatypes/string.dm new file mode 100644 index 0000000000..99c61b4a29 --- /dev/null +++ b/code/modules/wiremod/datatypes/string.dm @@ -0,0 +1,17 @@ +/datum/circuit_datatype/string + datatype = PORT_TYPE_STRING + color = "orange" + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/string/can_receive_from_datatype(datatype_to_check) + return TRUE + +/datum/circuit_datatype/string/convert_value(datum/port/port, value_to_convert) + if(isnull(value_to_convert)) + return + + // So that they can't easily get the name like this. + if(isatom(value_to_convert)) + return PORT_TYPE_ATOM + else + return copytext("[value_to_convert]", 1, PORT_MAX_STRING_LENGTH) diff --git a/code/modules/wiremod/preset/hello_world.dm b/code/modules/wiremod/preset/hello_world.dm new file mode 100644 index 0000000000..9aee88a300 --- /dev/null +++ b/code/modules/wiremod/preset/hello_world.dm @@ -0,0 +1,14 @@ +/** + * # Hello World preset + * + * Says "Hello World" when triggered. Needs to be wired up and connected first. + */ +/obj/item/integrated_circuit/loaded/hello_world + +/obj/item/integrated_circuit/loaded/hello_world/Initialize() + . = ..() + var/obj/item/circuit_component/speech/speech = new() + add_component(speech) + + speech.message.set_input("Hello World") + diff --git a/code/modules/wiremod/preset/speech_relay.dm b/code/modules/wiremod/preset/speech_relay.dm new file mode 100644 index 0000000000..5e5284bdeb --- /dev/null +++ b/code/modules/wiremod/preset/speech_relay.dm @@ -0,0 +1,20 @@ +/** + * # Speech Relay preset + * + * Acts like poly. Says whatever it hears. + */ +/obj/item/integrated_circuit/loaded/speech_relay + +/obj/item/integrated_circuit/loaded/speech_relay/Initialize() + . = ..() + var/obj/item/circuit_component/hear/hear = new() + add_component(hear) + hear.rel_x = 100 + hear.rel_y = 200 + + var/obj/item/circuit_component/speech/speech = new() + add_component(speech) + speech.rel_x = 400 + speech.rel_y = 200 + + speech.message.connect(hear.message_port) diff --git a/code/modules/wiremod/shell/airlock.dm b/code/modules/wiremod/shell/airlock.dm new file mode 100644 index 0000000000..b44bb40d02 --- /dev/null +++ b/code/modules/wiremod/shell/airlock.dm @@ -0,0 +1,133 @@ +/datum/wires/airlock/shell + holder_type = /obj/machinery/door/airlock/shell + proper_name = "Circuit Airlock" + +/datum/wires/airlock/shell/on_cut(wire, mend) + // Don't allow them to re-enable autoclose. + if(wire == WIRE_TIMING) + return + return ..() + +/obj/machinery/door/airlock/shell + name = "circuit airlock" + autoclose = FALSE + +/obj/machinery/door/airlock/shell/Initialize() + . = ..() + AddComponent( \ + /datum/component/shell, \ + unremovable_circuit_components = list(new /obj/item/circuit_component/airlock), \ + capacity = SHELL_CAPACITY_LARGE, \ + shell_flags = SHELL_FLAG_ALLOW_FAILURE_ACTION \ + ) + +/obj/machinery/door/airlock/shell/check_access(obj/item/I) + return FALSE + +/obj/machinery/door/airlock/shell/canAIControl(mob/user) + return FALSE + +/obj/machinery/door/airlock/shell/canAIHack(mob/user) + return FALSE + +/obj/machinery/door/airlock/shell/set_wires() + return new /datum/wires/airlock/shell(src) + +/obj/item/circuit_component/airlock + display_name = "Airlock" + desc = "The general interface with an airlock. Includes general statuses of the airlock" + + /// Called when attack_hand is called on the shell. + var/obj/machinery/door/airlock/attached_airlock + + /// Bolts the airlock (if possible) + var/datum/port/input/bolt + /// Unbolts the airlock (if possible) + var/datum/port/input/unbolt + /// Opens the airlock (if possible) + var/datum/port/input/open + /// Closes the airlock (if possible) + var/datum/port/input/close + + /// Contains whether the airlock is open or not + var/datum/port/output/is_open + /// Contains whether the airlock is bolted or not + var/datum/port/output/is_bolted + + /// Called when the airlock is opened. + var/datum/port/output/opened + /// Called when the airlock is closed + var/datum/port/output/closed + + /// Called when the airlock is bolted + var/datum/port/output/bolted + /// Called when the airlock is unbolted + var/datum/port/output/unbolted + +/obj/item/circuit_component/airlock/Initialize() + . = ..() + // Input Signals + bolt = add_input_port("Bolt", PORT_TYPE_SIGNAL) + unbolt = add_input_port("Unbolt", PORT_TYPE_SIGNAL) + open = add_input_port("Open", PORT_TYPE_SIGNAL) + close = add_input_port("Close", PORT_TYPE_SIGNAL) + // States + is_open = add_output_port("Is Open", PORT_TYPE_NUMBER) + is_bolted = add_output_port("Is Bolted", PORT_TYPE_NUMBER) + // Output Signals + opened = add_output_port("Opened", PORT_TYPE_SIGNAL) + closed = add_output_port("Closed", PORT_TYPE_SIGNAL) + bolted = add_output_port("Bolted", PORT_TYPE_SIGNAL) + unbolted = add_output_port("Unbolted", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/airlock/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/machinery/door/airlock)) + attached_airlock = shell + RegisterSignal(shell, COMSIG_AIRLOCK_SET_BOLT, .proc/on_airlock_set_bolted) + RegisterSignal(shell, COMSIG_AIRLOCK_OPEN, .proc/on_airlock_open) + RegisterSignal(shell, COMSIG_AIRLOCK_CLOSE, .proc/on_airlock_closed) + +/obj/item/circuit_component/airlock/unregister_shell(atom/movable/shell) + attached_airlock = null + UnregisterSignal(shell, list( + COMSIG_AIRLOCK_SET_BOLT, + COMSIG_AIRLOCK_OPEN, + COMSIG_AIRLOCK_CLOSE, + )) + return ..() + +/obj/item/circuit_component/airlock/proc/on_airlock_set_bolted(datum/source, should_bolt) + SIGNAL_HANDLER + is_bolted.set_output(should_bolt) + if(should_bolt) + bolted.set_output(COMPONENT_SIGNAL) + else + unbolted.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/airlock/proc/on_airlock_open(datum/source, force) + SIGNAL_HANDLER + is_open.set_output(TRUE) + opened.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/airlock/proc/on_airlock_closed(datum/source, forced) + SIGNAL_HANDLER + is_open.set_output(FALSE) + closed.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/airlock/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!attached_airlock) + return + + if(COMPONENT_TRIGGERED_BY(bolt, port)) + attached_airlock.bolt() + if(COMPONENT_TRIGGERED_BY(unbolt, port)) + attached_airlock.unbolt() + if(COMPONENT_TRIGGERED_BY(open, port) && attached_airlock.density) + INVOKE_ASYNC(attached_airlock, /obj/machinery/door/airlock.proc/open) + if(COMPONENT_TRIGGERED_BY(close, port) && !attached_airlock.density) + INVOKE_ASYNC(attached_airlock, /obj/machinery/door/airlock.proc/close) diff --git a/code/modules/wiremod/shell/bot.dm b/code/modules/wiremod/shell/bot.dm new file mode 100644 index 0000000000..f3c9fcae8d --- /dev/null +++ b/code/modules/wiremod/shell/bot.dm @@ -0,0 +1,55 @@ +/** + * # Bot + * + * Immobile (but not dense) shells that can interact with world. + */ +/obj/structure/bot + name = "bot" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_medium_box" + + density = FALSE + light_system = MOVABLE_LIGHT + light_on = FALSE + +/obj/structure/bot/Initialize() + . = ..() + AddComponent( \ + /datum/component/shell, \ + unremovable_circuit_components = list(new /obj/item/circuit_component/bot), \ + capacity = SHELL_CAPACITY_LARGE, \ + shell_flags = SHELL_FLAG_USB_PORT, \ + ) + +/obj/item/circuit_component/bot + display_name = "Bot" + desc = "Triggers when someone interacts with the bot." + + /// Called when attack_hand is called on the shell. + var/datum/port/output/signal + /// The user who used the bot + var/datum/port/output/entity + +/obj/item/circuit_component/bot/Initialize() + . = ..() + entity = add_output_port("User", PORT_TYPE_ATOM) + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/bot/Destroy() + signal = null + entity = null + return ..() + +/obj/item/circuit_component/bot/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand) + +/obj/item/circuit_component/bot/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_ATOM_ATTACK_HAND) + +/obj/item/circuit_component/bot/proc/on_attack_hand(atom/source, mob/user) + SIGNAL_HANDLER + source.balloon_alert(user, "pushed button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + signal.set_output(COMPONENT_SIGNAL) + diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm new file mode 100644 index 0000000000..d288edbc2d --- /dev/null +++ b/code/modules/wiremod/shell/brain_computer_interface.dm @@ -0,0 +1,553 @@ +/obj/item/organ/cyberimp/bci + name = "brain-computer interface" + desc = "An implant that can be placed in a user's head to control circuits using their brain." + icon = 'icons/obj/wiremod.dmi' + icon_state = "bci" + zone = BODY_ZONE_HEAD + w_class = WEIGHT_CLASS_TINY + +/obj/item/organ/cyberimp/bci/Initialize() + . = ..() + + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/bci_core, + new /obj/item/circuit_component/bci_action(null, "One"), + new /obj/item/circuit_component/bci_action(null, "Two"), + new /obj/item/circuit_component/bci_action(null, "Three"), + ), SHELL_CAPACITY_SMALL) + +/obj/item/organ/cyberimp/bci/Insert(mob/living/carbon/reciever, special, drop_if_replaced) + . = ..() + + // Organs are put in nullspace, but this breaks circuit interactions + forceMove(reciever) + +/obj/item/organ/cyberimp/bci/say(message, bubble_type, list/spans, sanitize, datum/language/language, ignore_spam, forced) + if (owner) + // Otherwise say_dead will be called. + // It's intentional that a circuit for a dead person does not speak from the shell. + if (owner.stat == DEAD) + return + + owner.say(message, forced = "circuit speech") + else + return ..() + +/obj/item/circuit_component/bci_action + display_name = "BCI Action" + desc = "Represents an action the user can take when implanted with the brain-computer interface." + required_shells = list(/obj/item/organ/cyberimp/bci) + + /// The icon of the button + var/datum/port/input/option/icon_options + + /// The name to use for the button + var/datum/port/input/button_name + + /// Called when the user presses the button + var/datum/port/output/signal + + /// A reference to the action button itself + var/datum/action/innate/bci_action/bci_action + +/obj/item/circuit_component/bci_action/Initialize(mapload, default_icon) + . = ..() + + if (!isnull(default_icon)) + icon_options.set_input(default_icon) + + button_name = add_input_port("Name", PORT_TYPE_STRING) + + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/bci_action/Destroy() + QDEL_NULL(bci_action) + return ..() + +/obj/item/circuit_component/bci_action/populate_options() + var/static/action_options = list( + "Blank", + + "One", + "Two", + "Three", + "Four", + "Five", + + "Blood", + "Bomb", + "Brain", + "Brain Damage", + "Cross", + "Electricity", + "Exclamation", + "Heart", + "Id", + "Info", + "Injection", + "Magnetism", + "Minus", + "Network", + "Plus", + "Power", + "Question", + "Radioactive", + "Reaction", + "Repair", + "Say", + "Scan", + "Shield", + "Skull", + "Sleep", + "Wireless", + ) + + icon_options = add_option_port("Icon", action_options) + +/obj/item/circuit_component/bci_action/register_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci_action = new(src) + update_action() + + bci.actions += list(bci_action) + +/obj/item/circuit_component/bci_action/unregister_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci.actions -= bci_action + QDEL_NULL(bci_action) + +/obj/item/circuit_component/bci_action/input_received(datum/port/input/port) + . = ..() + + if (.) + return + + if (!isnull(bci_action)) + update_action() + +/obj/item/circuit_component/bci_action/proc/update_action() + bci_action.name = button_name.value + bci_action.button_icon_state = "bci_[replacetextEx(lowertext(icon_options.value), " ", "_")]" + +/datum/action/innate/bci_action + name = "Action" + icon_icon = 'icons/mob/actions/actions_items.dmi' + check_flags = AB_CHECK_CONSCIOUS + button_icon_state = "bci_power" + + var/obj/item/circuit_component/bci_action/circuit_component + +/datum/action/innate/bci_action/New(obj/item/circuit_component/bci_action/circuit_component) + ..() + + src.circuit_component = circuit_component + +/datum/action/innate/bci_action/Destroy() + circuit_component.bci_action = null + circuit_component = null + + return ..() + +/datum/action/innate/bci_action/Activate() + circuit_component.signal.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/bci_core + display_name = "BCI Core" + desc = "Controls the core operations of the BCI." + + /// A reference to the action button to look at charge/get info + var/datum/action/innate/bci_charge_action/charge_action + + var/datum/port/input/message + var/datum/port/input/send_message_signal + + var/datum/port/output/user_port + + var/datum/weakref/user + +/obj/item/circuit_component/bci_core/Initialize() + . = ..() + + message = add_input_port("Message", PORT_TYPE_STRING) + send_message_signal = add_input_port("Send Message", PORT_TYPE_SIGNAL) + + user_port = add_output_port("User", PORT_TYPE_ATOM) + +/obj/item/circuit_component/bci_core/Destroy() + QDEL_NULL(charge_action) + return ..() + +/obj/item/circuit_component/bci_core/register_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + charge_action = new(src) + bci.actions += list(charge_action) + + RegisterSignal(shell, COMSIG_ORGAN_IMPLANTED, .proc/on_organ_implanted) + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/bci_core/unregister_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci.actions -= charge_action + QDEL_NULL(charge_action) + + UnregisterSignal(shell, list( + COMSIG_ORGAN_IMPLANTED, + COMSIG_ORGAN_REMOVED, + )) + +/obj/item/circuit_component/bci_core/input_received(datum/port/input/port) + . = ..() + if (.) + return . + + if (COMPONENT_TRIGGERED_BY(send_message_signal, port)) + var/sent_message = trim(message.value) + if (!sent_message) + return + + var/mob/living/carbon/resolved_owner = user?.resolve() + if (isnull(resolved_owner)) + return + + if (resolved_owner.stat == DEAD) + return + + to_chat(resolved_owner, "You hear a strange, robotic voice in your head... \"[span_robot("[html_encode(sent_message)]")]\"") + +/obj/item/circuit_component/bci_core/proc/on_organ_implanted(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + user_port.set_output(owner) + user = WEAKREF(owner) + + RegisterSignal(owner, COMSIG_PARENT_EXAMINE, .proc/on_examine) + RegisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, .proc/on_borg_charge) + RegisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT, .proc/on_electrocute) + +/obj/item/circuit_component/bci_core/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + user_port.set_output(null) + user = null + + UnregisterSignal(owner, list( + COMSIG_PARENT_EXAMINE, + COMSIG_PROCESS_BORGCHARGER_OCCUPANT, + COMSIG_LIVING_ELECTROCUTE_ACT, + )) + +/obj/item/circuit_component/bci_core/proc/on_borg_charge(datum/source, amount) + SIGNAL_HANDLER + + if (isnull(parent.cell)) + return + + parent.cell.give(amount) + +/obj/item/circuit_component/bci_core/proc/on_electrocute(datum/source, shock_damage, siemens_coefficient, flags) + SIGNAL_HANDLER + + if (isnull(parent.cell)) + return + + if (flags & SHOCK_ILLUSION) + return + + parent.cell.give(shock_damage * 2) + to_chat(source, span_notice("You absorb some of the shock into your [parent.name]!")) + +/obj/item/circuit_component/bci_core/proc/on_examine(datum/source, mob/mob, list/examine_text) + SIGNAL_HANDLER + + if (isobserver(mob)) + examine_text += span_notice("[source.p_they(capitalized = TRUE)] [source.p_have()] \a [parent] implanted in [source.p_them()].") + +/obj/item/circuit_component/bci_core/Topic(href, list/href_list) + ..() + + if (!isobserver(usr)) + return + + if (href_list["open_bci"]) + parent.attack_ghost(usr) + +/datum/action/innate/bci_charge_action + name = "Check BCI Charge" + check_flags = NONE + icon_icon = 'icons/obj/power.dmi' + button_icon_state = "cell" + + var/obj/item/circuit_component/bci_core/circuit_component + +/datum/action/innate/bci_charge_action/New(obj/item/circuit_component/bci_core/circuit_component) + ..() + + src.circuit_component = circuit_component + + button.maptext_x = 2 + button.maptext_y = 0 + update_maptext() + + START_PROCESSING(SSobj, src) + +/datum/action/innate/bci_charge_action/Destroy() + circuit_component.charge_action = null + circuit_component = null + + STOP_PROCESSING(SSobj, src) + + return ..() + +/datum/action/innate/bci_charge_action/Trigger() + var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell + + if (isnull(cell)) + to_chat(owner, span_boldwarning("[circuit_component.parent] has no power cell.")) + else + to_chat(owner, span_info("[circuit_component.parent]'s [cell.name] has [cell.percent()]% charge left.")) + to_chat(owner, span_info("You can recharge it by using a cyborg recharging station.")) + +/datum/action/innate/bci_charge_action/process(delta_time) + update_maptext() + +/datum/action/innate/bci_charge_action/proc/update_maptext() + var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell + button.maptext = cell ? MAPTEXT("[cell.percent()]%") : "" + +/obj/machinery/bci_implanter + name = "brain-computer interface manipulation chamber" + desc = "A machine that, when given a brain-computer interface, will implant it into an occupant. Otherwise, will remove any brain-computer interfaces they already have." + circuit = /obj/item/circuitboard/machine/bci_implanter + icon = 'icons/obj/machines/bci_implanter.dmi' + icon_state = "bci_implanter" + base_icon_state = "bci_implanter" + layer = ABOVE_WINDOW_LAYER + use_power = IDLE_POWER_USE + anchored = TRUE + density = TRUE + obj_flags = NO_BUILD // Becomes undense when the door is open + idle_power_usage = 50 + active_power_usage = 300 + + var/busy = FALSE + var/busy_icon_state + var/locked = FALSE + + var/datum/weakref/bci_to_implant + + COOLDOWN_DECLARE(message_cooldown) + +/obj/machinery/bci_implanter/Initialize() + . = ..() + occupant_typecache = typecacheof(/mob/living/carbon) + +/obj/machinery/bci_implanter/Destroy() + QDEL_NULL(bci_to_implant) + return ..() + +/obj/machinery/bci_implanter/examine(mob/user) + . = ..() + + if (isnull(bci_to_implant?.resolve())) + . += span_notice("There is no BCI inserted.") + else + . += span_notice("Right-click to remove current BCI.") + +/obj/machinery/bci_implanter/proc/set_busy(status, working_icon) + busy = status + busy_icon_state = working_icon + update_appearance() + +/obj/machinery/bci_implanter/update_icon_state() + if (occupant) + icon_state = busy ? busy_icon_state : "[base_icon_state]_occupied" + return ..() + icon_state = "[base_icon_state][state_open ? "_open" : null]" + return ..() + +/obj/machinery/bci_implanter/update_overlays() + var/list/overlays = ..() + + if ((stat & MAINT) || panel_open) + overlays += "maint" + return overlays + + if (stat & (NOPOWER|BROKEN)) + return overlays + + if (busy || locked) + overlays += "red" + if (locked) + overlays += "bolted" + return overlays + + overlays += "green" + + return overlays + +/obj/machinery/bci_implanter/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if (. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return . + + if(!user.Adjacent(src)) + return + + if (locked) + balloon_alert(user, "it's locked!") + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/obj/item/organ/cyberimp/bci/bci_to_implant_resolved = bci_to_implant?.resolve() + if (isnull(bci_to_implant_resolved)) + balloon_alert(user, "no bci inserted!") + else + user.put_in_hands(bci_to_implant_resolved) + balloon_alert(user, "ejected bci") + + bci_to_implant = null + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/bci_implanter/attackby(obj/item/weapon, mob/user, params) + var/obj/item/organ/cyberimp/bci/new_bci = weapon + if (istype(new_bci)) + if (!(locate(/obj/item/integrated_circuit) in new_bci)) + balloon_alert(user, "bci has no circuit!") + return + + var/obj/item/organ/cyberimp/bci/previous_bci_to_implant = bci_to_implant?.resolve() + + bci_to_implant = WEAKREF(weapon) + weapon.forceMove(src) + + if (isnull(previous_bci_to_implant)) + balloon_alert(user, "inserted bci") + else + balloon_alert(user, "swapped bci") + user.put_in_hands(previous_bci_to_implant) + + return + + return ..() + +/obj/machinery/bci_implanter/attackby_secondary(obj/item/weapon, mob/user, params) + if (!occupant && default_deconstruction_screwdriver(user, icon_state, icon_state, weapon)) + update_appearance() + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if (default_pry_open(weapon)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if (default_deconstruction_crowbar(weapon)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/bci_implanter/proc/start_process() + if (stat & (NOPOWER|BROKEN)) + return + if ((stat & MAINT) || panel_open) + return + if (!occupant || busy) + return + + var/locked_state = locked + locked = TRUE + + set_busy(TRUE, "[initial(icon_state)]_raising") + addtimer(CALLBACK(src, .proc/set_busy, TRUE, "[initial(icon_state)]_active"), 1 SECONDS) + addtimer(CALLBACK(src, .proc/set_busy, TRUE, "[initial(icon_state)]_falling"), 2 SECONDS) + addtimer(CALLBACK(src, .proc/complete_process, locked_state), 3 SECONDS) + +/obj/machinery/bci_implanter/proc/complete_process(locked_state) + locked = locked_state + set_busy(FALSE) + + var/mob/living/carbon/carbon_occupant = occupant + if (!istype(carbon_occupant)) + return + + playsound(loc, 'sound/machines/ping.ogg', 30, FALSE) + + var/obj/item/organ/cyberimp/bci/bci_organ = carbon_occupant.getorgan(/obj/item/organ/cyberimp/bci) + var/obj/item/organ/cyberimp/bci/bci_to_implant_resolved = bci_to_implant?.resolve() + + if (bci_organ) + bci_organ.Remove(carbon_occupant) + + if (isnull(bci_to_implant_resolved)) + say("Occupant's previous brain-computer interface has been transferred to internal storage unit.") + bci_organ.forceMove(src) + bci_to_implant = WEAKREF(bci_organ) + else + say("Occupant's previous brain-computer interface has been ejected.") + bci_organ.forceMove(drop_location()) + else if (!isnull(bci_to_implant_resolved)) + say("Occupant has been injected with [bci_to_implant_resolved].") + bci_to_implant_resolved.Insert(carbon_occupant) + bci_to_implant = null + +/obj/machinery/bci_implanter/open_machine() + if(state_open) + return FALSE + + ..() + + return TRUE + +/obj/machinery/bci_implanter/close_machine(mob/living/carbon/user) + if(!state_open) + return FALSE + + ..() + + var/mob/living/carbon/carbon_occupant = occupant + if (istype(occupant)) + var/obj/item/organ/cyberimp/bci/existing_bci_organ = carbon_occupant.getorgan(/obj/item/organ/cyberimp/bci) + if (isnull(existing_bci_organ) && isnull(bci_to_implant?.resolve())) + say("No brain-computer interface inserted, and occupant does not have one. Insert a BCI to implant one.") + playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE) + return FALSE + + addtimer(CALLBACK(src, .proc/start_process), 1 SECONDS) + return TRUE + +/obj/machinery/bci_implanter/relaymove(mob/living/user, direction) + var/message + + if (locked) + message = "it won't budge!" + else if (user.stat != CONSCIOUS) + message = "you don't have the energy!" + + if (!isnull(message)) + if (COOLDOWN_FINISHED(src, message_cooldown)) + COOLDOWN_START(src, message_cooldown, 5 SECONDS) + balloon_alert(user, message) + + return + + open_machine() + +/obj/machinery/bci_implanter/interact(mob/user) + if (state_open) + close_machine(null, user) + return + else if (locked) + balloon_alert(user, "it's locked!") + return + + open_machine() + +/obj/item/circuitboard/machine/bci_implanter + name = "Brain-Computer Interface Manipulation Chamber (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_SCIENCE + build_path = /obj/machinery/bci_implanter + req_components = list( + /obj/item/stock_parts/micro_laser = 2, + /obj/item/stock_parts/manipulator = 1, + ) diff --git a/code/modules/wiremod/shell/compact_remote.dm b/code/modules/wiremod/shell/compact_remote.dm new file mode 100644 index 0000000000..d5fe33e285 --- /dev/null +++ b/code/modules/wiremod/shell/compact_remote.dm @@ -0,0 +1,51 @@ +/** + * # Compact Remote + * + * A handheld device with one big button. + */ +/obj/item/compact_remote + name = "compact remote" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_small_simple" + inhand_icon_state = "electronic" + worn_icon_state = "electronic" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/obj/item/compact_remote/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/compact_remote() + ), SHELL_CAPACITY_SMALL) + +/obj/item/circuit_component/compact_remote + display_name = "Compact Remote" + desc = "Used to receive inputs from the compact remote shell. Use the shell in hand to trigger the output signal." + + /// Called when attack_self is called on the shell. + var/datum/port/output/signal + /// The user who used the bot + var/datum/port/output/entity + +/obj/item/circuit_component/compact_remote/Initialize() + . = ..() + entity = add_output_port("User", PORT_TYPE_ATOM) + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/compact_remote/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ITEM_ATTACK_SELF, .proc/send_trigger) + +/obj/item/circuit_component/compact_remote/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_ITEM_ATTACK_SELF) + +/** + * Called when the shell item is used in hand. + */ +/obj/item/circuit_component/compact_remote/proc/send_trigger(atom/source, mob/user) + SIGNAL_HANDLER + source.balloon_alert(user, "clicked primary button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + signal.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/shell/controller.dm b/code/modules/wiremod/shell/controller.dm new file mode 100644 index 0000000000..d3af0ca88e --- /dev/null +++ b/code/modules/wiremod/shell/controller.dm @@ -0,0 +1,89 @@ +/** + * # Compact Remote + * + * A handheld device with several buttons. + * In game, this translates to having different signals for normal usage, alt-clicking, and ctrl-clicking when in your hand. + */ +/obj/item/controller + name = "controller" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_small_calc" + inhand_icon_state = "electronic" + worn_icon_state = "electronic" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/obj/item/controller/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/controller() + ), SHELL_CAPACITY_MEDIUM) + +/obj/item/circuit_component/controller + display_name = "Controller" + desc = "Used to receive inputs from the controller shell. Use the shell in hand to trigger the output signal. Alt-click for the alternate signal. Right click for the extra signal." + + /// The three separate buttons that are called in attack_hand on the shell. + var/datum/port/output/signal + var/datum/port/output/alt + var/datum/port/output/right + + /// The entity output + var/datum/port/output/entity + +/obj/item/circuit_component/controller/Initialize() + . = ..() + entity = add_output_port("User", PORT_TYPE_ATOM) + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + alt = add_output_port("Alternate Signal", PORT_TYPE_SIGNAL) + right = add_output_port("Extra Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/controller/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ITEM_ATTACK_SELF, .proc/send_trigger) + RegisterSignal(shell, COMSIG_CLICK_ALT, .proc/send_alternate_signal) + RegisterSignal(shell, COMSIG_ITEM_ATTACK_SELF_SECONDARY, .proc/send_right_signal) + +/obj/item/circuit_component/controller/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, list( + COMSIG_ITEM_ATTACK_SELF, + COMSIG_ITEM_ATTACK_SELF_SECONDARY, + COMSIG_CLICK_ALT, + )) + +/** + * Called when the shell item is used in hand + */ +/obj/item/circuit_component/controller/proc/send_trigger(atom/source, mob/user) + SIGNAL_HANDLER + if(!user.Adjacent(source)) + return + source.balloon_alert(user, "clicked primary button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + signal.set_output(COMPONENT_SIGNAL) + +/** + * Called when the shell item is alt-clicked + */ +/obj/item/circuit_component/controller/proc/send_alternate_signal(atom/source, mob/user) + SIGNAL_HANDLER + if(!user.Adjacent(source)) + return + source.balloon_alert(user, "clicked alternate button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + alt.set_output(COMPONENT_SIGNAL) + +/** + * Called when the shell item is right-clicked in active hand + */ +/obj/item/circuit_component/controller/proc/send_right_signal(atom/source, mob/user) + SIGNAL_HANDLER + if(!user.Adjacent(source)) + return + source.balloon_alert(user, "clicked extra button") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + entity.set_output(user) + right.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/shell/drone.dm b/code/modules/wiremod/shell/drone.dm new file mode 100644 index 0000000000..d091f20b65 --- /dev/null +++ b/code/modules/wiremod/shell/drone.dm @@ -0,0 +1,82 @@ +/** + * # Drone + * + * A movable mob that can be fed inputs on which direction to travel. + */ +/mob/living/circuit_drone + name = "drone" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_medium_med" + living_flags = 0 + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/mob/living/circuit_drone/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/bot_circuit() + ), SHELL_CAPACITY_LARGE) + +/mob/living/circuit_drone/updatehealth() + . = ..() + if(health < 0) + gib(no_brain = TRUE, no_organs = TRUE, no_bodyparts = TRUE) + +/mob/living/circuit_drone/spawn_gibs() + new /obj/effect/gibspawner/robot(drop_location(), src, get_static_viruses()) + +/obj/item/circuit_component/bot_circuit + display_name = "Drone" + desc = "Used to send movement output signals to the drone shell." + + /// The inputs to allow for the drone to move + var/datum/port/input/north + var/datum/port/input/east + var/datum/port/input/south + var/datum/port/input/west + + // Done like this so that travelling diagonally is more simple + COOLDOWN_DECLARE(north_delay) + COOLDOWN_DECLARE(east_delay) + COOLDOWN_DECLARE(south_delay) + COOLDOWN_DECLARE(west_delay) + + /// Delay between each movement + var/move_delay = 0.2 SECONDS + +/obj/item/circuit_component/bot_circuit/Initialize() + . = ..() + north = add_input_port("Move North", PORT_TYPE_SIGNAL) + east = add_input_port("Move East", PORT_TYPE_SIGNAL) + south = add_input_port("Move South", PORT_TYPE_SIGNAL) + west = add_input_port("Move West", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/bot_circuit/input_received(datum/port/input/port) + . = ..() + if(.) + return + + var/mob/living/shell = parent.shell + if(!istype(shell) || shell.stat) + return + + var/direction + + if(COMPONENT_TRIGGERED_BY(north, port) && COOLDOWN_FINISHED(src, north_delay)) + direction = NORTH + COOLDOWN_START(src, north_delay, move_delay) + else if(COMPONENT_TRIGGERED_BY(east, port) && COOLDOWN_FINISHED(src, east_delay)) + direction = EAST + COOLDOWN_START(src, east_delay, move_delay) + else if(COMPONENT_TRIGGERED_BY(south, port) && COOLDOWN_FINISHED(src, south_delay)) + direction = SOUTH + COOLDOWN_START(src, south_delay, move_delay) + else if(COMPONENT_TRIGGERED_BY(west, port) && COOLDOWN_FINISHED(src, west_delay)) + direction = WEST + COOLDOWN_START(src, west_delay, move_delay) + + if(!direction) + return + + if(shell.Process_Spacemove(direction)) + shell.Move(get_step(shell, direction), direction) diff --git a/code/modules/wiremod/shell/moneybot.dm b/code/modules/wiremod/shell/moneybot.dm new file mode 100644 index 0000000000..fe3cd30733 --- /dev/null +++ b/code/modules/wiremod/shell/moneybot.dm @@ -0,0 +1,140 @@ +/** + * # Money Bot + * + * Immobile (but not dense) shell that can receive and dispense money. + */ +/obj/structure/money_bot + name = "money bot" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_large" + + density = FALSE + light_system = MOVABLE_LIGHT + light_on = FALSE + + var/stored_money = 0 + +/obj/structure/money_bot/deconstruct(disassembled) + new /obj/item/holochip(drop_location(), stored_money) + return ..() + +/obj/structure/money_bot/proc/add_money(to_add) + stored_money += to_add + SEND_SIGNAL(src, COMSIG_MONEYBOT_ADD_MONEY, to_add) + +/obj/structure/money_bot/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/money_bot(), + new /obj/item/circuit_component/money_dispenser() + ), SHELL_CAPACITY_LARGE) + +/obj/structure/money_bot/wrench_act(mob/living/user, obj/item/tool) + set_anchored(!anchored) + tool.play_tool_sound(src) + balloon_alert(user, "You [anchored?"secure":"unsecure"] [src].") + return TRUE + + +/obj/item/circuit_component/money_dispenser + display_name = "Money Dispenser" + desc = "Used to dispense money from the money bot. Money is taken from the internal storage of money." + circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL + + /// The amount of money to dispense + var/datum/port/input/dispense_amount + + /// Outputs a signal when it fails to output any money. + var/datum/port/output/on_fail + + var/obj/structure/money_bot/attached_bot + +/obj/item/circuit_component/money_dispenser/Initialize() + . = ..() + dispense_amount = add_input_port("Amount", PORT_TYPE_NUMBER) + on_fail = add_output_port("On Failed", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/money_dispenser/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/structure/money_bot)) + attached_bot = shell + +/obj/item/circuit_component/money_dispenser/unregister_shell(atom/movable/shell) + attached_bot = null + return ..() + +/obj/item/circuit_component/money_dispenser/input_received(datum/port/input/port) + . = ..() + if(.) + return + + if(!attached_bot) + return + + var/to_dispense = clamp(dispense_amount.value, 0, attached_bot.stored_money) + if(!to_dispense) + on_fail.set_output(COMPONENT_SIGNAL) + return + + attached_bot.add_money(-to_dispense) + new /obj/item/holochip(drop_location(), to_dispense) + +/obj/item/circuit_component/money_bot + display_name = "Money Bot" + var/obj/structure/money_bot/attached_bot + desc = "Used to receive input signals when money is inserted into the money bot shell and also keep track of the total money in the shell." + + /// Total money in the shell + var/datum/port/output/total_money + /// Amount of the last money inputted into the shell + var/datum/port/output/money_input + /// Trigger for when money is inputted into the shell + var/datum/port/output/money_trigger + /// The person who input the money + var/datum/port/output/entity + +/obj/item/circuit_component/money_bot/Initialize() + . = ..() + total_money = add_output_port("Total Money", PORT_TYPE_NUMBER) + money_input = add_output_port("Last Input Money", PORT_TYPE_NUMBER) + entity = add_output_port("User", PORT_TYPE_ATOM) + money_trigger = add_output_port("Money Input", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/money_bot/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/structure/money_bot)) + attached_bot = shell + total_money.set_output(attached_bot.stored_money) + RegisterSignal(shell, COMSIG_PARENT_ATTACKBY, .proc/handle_money_insert) + RegisterSignal(shell, COMSIG_MONEYBOT_ADD_MONEY, .proc/handle_money_update) + +/obj/item/circuit_component/money_bot/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, list( + COMSIG_PARENT_ATTACKBY, + COMSIG_MONEYBOT_ADD_MONEY, + )) + total_money.set_output(null) + attached_bot = null + return ..() + +/obj/item/circuit_component/money_bot/proc/handle_money_insert(atom/source, obj/item/item, mob/living/attacker) + SIGNAL_HANDLER + if(!attached_bot || !iscash(item)) + return + + var/amount_to_insert = item.get_item_credit_value() + if(!amount_to_insert) + balloon_alert(attacker, "this has no value!") + return + + attached_bot.add_money(amount_to_insert) + balloon_alert(attacker, "inserted [amount_to_insert] credits.") + money_input.set_output(amount_to_insert) + entity.set_output(attacker) + money_trigger.set_output(COMPONENT_SIGNAL) + qdel(item) + +/obj/item/circuit_component/money_bot/proc/handle_money_update(atom/source) + SIGNAL_HANDLER + if(attached_bot) + total_money.set_output(attached_bot.stored_money) diff --git a/code/modules/wiremod/shell/scanner.dm b/code/modules/wiremod/shell/scanner.dm new file mode 100644 index 0000000000..fd46910914 --- /dev/null +++ b/code/modules/wiremod/shell/scanner.dm @@ -0,0 +1,62 @@ +/** + * # Scanner + * + * A handheld device that lets you flash it over people. + */ +/obj/item/wiremod_scanner + name = "scanner" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_small" + inhand_icon_state = "electronic" + worn_icon_state = "electronic" + lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' + righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' + light_system = MOVABLE_LIGHT_DIRECTIONAL + light_on = FALSE + +/obj/item/wiremod_scanner/Initialize() + . = ..() + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/wiremod_scanner() + ), SHELL_CAPACITY_SMALL) + +/obj/item/circuit_component/wiremod_scanner + display_name = "Scanner" + desc = "Used to receive scanned entities from the scanner." + + /// Called when afterattack is called on the shell. + var/datum/port/output/signal + + /// The attacker + var/datum/port/output/attacker + + /// The entity being attacked + var/datum/port/output/attacking + + + +/obj/item/circuit_component/wiremod_scanner/Initialize() + . = ..() + attacker = add_output_port("Scanner", PORT_TYPE_ATOM) + attacking = add_output_port("Scanned Entity", PORT_TYPE_ATOM) + signal = add_output_port("Scanned", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/wiremod_scanner/register_shell(atom/movable/shell) + RegisterSignal(shell, COMSIG_ITEM_AFTERATTACK, .proc/handle_afterattack) + +/obj/item/circuit_component/wiremod_scanner/unregister_shell(atom/movable/shell) + UnregisterSignal(shell, COMSIG_ITEM_AFTERATTACK) + +/** + * Called when the shell item attacks something + */ +/obj/item/circuit_component/wiremod_scanner/proc/handle_afterattack(atom/source, atom/target, mob/user, proximity_flag) + SIGNAL_HANDLER + if(!proximity_flag) + return + source.balloon_alert(user, "scanned object") + playsound(source, get_sfx("terminal_type"), 25, FALSE) + attacker.set_output(user) + attacking.set_output(target) + signal.set_output(COMPONENT_SIGNAL) + diff --git a/code/modules/wiremod/shell/scanner_gate.dm b/code/modules/wiremod/shell/scanner_gate.dm new file mode 100644 index 0000000000..312c690bd8 --- /dev/null +++ b/code/modules/wiremod/shell/scanner_gate.dm @@ -0,0 +1,66 @@ +/obj/structure/scanner_gate_shell + name = "circuit scanner gate" + desc = "A gate able to perform mid-depth scans on any organisms who pass under it." + icon = 'icons/obj/machines/scangate.dmi' + icon_state = "scangate_black" + var/scanline_timer + +/obj/structure/scanner_gate_shell/Initialize() + . = ..() + set_scanline("passive") + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = .proc/on_entered, + ) + AddElement(/datum/element/connect_loc, loc_connections) + + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/scanner_gate() + ), SHELL_CAPACITY_LARGE, SHELL_FLAG_REQUIRE_ANCHOR) + +/obj/structure/scanner_gate_shell/wrench_act(mob/living/user, obj/item/tool) + set_anchored(!anchored) + tool.play_tool_sound(src) + balloon_alert(user, "You [anchored?"secure":"unsecure"] [src].") + return TRUE + +/obj/structure/scanner_gate_shell/proc/on_entered(datum/source, atom/movable/AM) + SIGNAL_HANDLER + set_scanline("scanning", 10) + SEND_SIGNAL(src, COMSIG_SCANGATE_SHELL_PASS, AM) + +/obj/structure/scanner_gate_shell/proc/set_scanline(type, duration) + cut_overlays() + deltimer(scanline_timer) + add_overlay(type) + if(duration) + scanline_timer = addtimer(CALLBACK(src, .proc/set_scanline, "passive"), duration, TIMER_STOPPABLE) + +/obj/item/circuit_component/scanner_gate + display_name = "Scanner Gate" + desc = "A gate able to perform mid-depth scans on any object that pass through it." + + circuit_flags = CIRCUIT_FLAG_OUTPUT_SIGNAL + + var/datum/port/output/scanned + + var/obj/structure/scanner_gate_shell/attached_gate + +/obj/item/circuit_component/scanner_gate/Initialize() + . = ..() + scanned = add_output_port("Scanned Object", PORT_TYPE_ATOM) + +/obj/item/circuit_component/scanner_gate/register_shell(atom/movable/shell) + . = ..() + if(istype(shell, /obj/structure/scanner_gate_shell)) + attached_gate = shell + RegisterSignal(attached_gate, COMSIG_SCANGATE_SHELL_PASS, .proc/on_trigger) + +/obj/item/circuit_component/scanner_gate/unregister_shell(atom/movable/shell) + UnregisterSignal(attached_gate, COMSIG_SCANGATE_SHELL_PASS) + attached_gate = null + return ..() + +/obj/item/circuit_component/scanner_gate/proc/on_trigger(datum/source, atom/movable/passed) + SIGNAL_HANDLER + scanned.set_output(passed) + trigger_output.set_output(COMPONENT_SIGNAL) diff --git a/code/modules/wiremod/shell/server.dm b/code/modules/wiremod/shell/server.dm new file mode 100644 index 0000000000..5e5a44f897 --- /dev/null +++ b/code/modules/wiremod/shell/server.dm @@ -0,0 +1,24 @@ +/** + * # Server + * + * Immobile (but not dense) shells that can interact with + * world. + */ +/obj/structure/server + name = "server" + icon = 'icons/obj/wiremod.dmi' + icon_state = "setup_stationary" + + density = TRUE + light_system = MOVABLE_LIGHT + light_on = FALSE + +/obj/structure/server/Initialize() + . = ..() + AddComponent(/datum/component/shell, null, SHELL_CAPACITY_VERY_LARGE, SHELL_FLAG_REQUIRE_ANCHOR|SHELL_FLAG_USB_PORT) + +/obj/structure/server/wrench_act(mob/living/user, obj/item/tool) + set_anchored(!anchored) + tool.play_tool_sound(src) + balloon_alert(user, "You [anchored?"secure":"unsecure"] [src].") + return TRUE diff --git a/code/modules/wiremod/shell/shell_items.dm b/code/modules/wiremod/shell/shell_items.dm new file mode 100644 index 0000000000..4e6673e7c8 --- /dev/null +++ b/code/modules/wiremod/shell/shell_items.dm @@ -0,0 +1,65 @@ +/** + * # Shell Item + * + * Printed out by protolathes. Screwdriver to complete the shell. + */ +/obj/item/shell + name = "assembly" + desc = "A shell assembly that can be completed by screwdrivering it." + icon = 'icons/obj/wiremod.dmi' + var/shell_to_spawn + var/screw_delay = 3 SECONDS + +/obj/item/shell/screwdriver_act(mob/living/user, obj/item/tool) + user.visible_message(span_notice("[user] begins finishing [src]."), span_notice("You begin finishing [src].")) + tool.play_tool_sound(src) + if(!do_after(user, screw_delay, src)) + return + user.visible_message(span_notice("[user] finishes [src]."), span_notice("You finish [src].")) + + var/turf/drop_loc = drop_location() + + qdel(src) + if(drop_loc) + new shell_to_spawn(drop_loc) + + return TRUE + +/obj/item/shell/bot + name = "bot assembly" + icon_state = "setup_medium_box-open" + shell_to_spawn = /obj/structure/bot + +/obj/item/shell/money_bot + name = "money bot assembly" + icon_state = "setup_large-open" + shell_to_spawn = /obj/structure/money_bot + +/obj/item/shell/drone + name = "drone assembly" + icon_state = "setup_medium_med-open" + shell_to_spawn = /mob/living/circuit_drone + +/obj/item/shell/server + name = "server assembly" + icon_state = "setup_stationary-open" + shell_to_spawn = /obj/structure/server + screw_delay = 10 SECONDS + +/obj/item/shell/airlock + name = "circuit airlock assembly" + icon = 'icons/obj/doors/airlocks/station/public.dmi' + icon_state = "construction" + shell_to_spawn = /obj/machinery/door/airlock/shell + screw_delay = 10 SECONDS + +/obj/item/shell/bci + name = "brain-computer interface assembly" + icon_state = "bci-open" + shell_to_spawn = /obj/item/organ/cyberimp/bci + +/obj/item/shell/scanner_gate + name = "scanner gate assembly" + icon = 'icons/obj/machines/scangate.dmi' + icon_state = "scangate_black_open" + shell_to_spawn = /obj/structure/scanner_gate_shell diff --git a/config/entries/logging.txt b/config/entries/logging.txt index 1690dbfacf..31692afccb 100644 --- a/config/entries/logging.txt +++ b/config/entries/logging.txt @@ -13,6 +13,9 @@ LOG_SAY ## log client access (logon/logoff) LOG_ACCESS +## Enables log entries for logins that failed due to suspicious circumstances (banned player, CID randomiser, spoofed BYOND version, etc.) to a dedicated file. +LOG_SUSPICIOUS_LOGIN + ## log game actions (start of round, results, etc.) LOG_GAME diff --git a/html/changelog.html b/html/changelog.html index 6d9e435f40..36b584a67e 100644 --- a/html/changelog.html +++ b/html/changelog.html @@ -50,6 +50,14 @@ -->
+

25 January 2022

+

Putnam3145 updated:

+
    +
  • Artificial zeolite now has a taste
  • +
  • "Zeolites" has been renamed to "zeolite" ("a beaker of zeolites" is like "a beaker of woods")
  • +
  • Various taste descriptors have had their end punctuation removed
  • +
+

23 January 2022

DeltaFire15 updated: