diff --git a/code/datums/uplink_item.dm b/code/datums/uplink_item.dm index 8427bef8baf..0a1fd85a665 100644 --- a/code/datums/uplink_item.dm +++ b/code/datums/uplink_item.dm @@ -929,7 +929,7 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item)) /datum/uplink_item/stealthy_weapons/dart_pistol name = "Dart Pistol Kit" - desc = "A miniaturized version of a normal syringe gun. It is very quiet when fired and can fit into any space a small item can. Comes with 3 syringes, a knockout poison, a silencing agent and a deadly neurotoxin." + desc = "A miniaturized version of a normal syringe gun. It is very quiet when fired and can fit into any space a small item can. Comes with 3 syringes: a knockout poison, a silencing agent and a deadly neurotoxin." reference = "DART" item = /obj/item/storage/box/syndie_kit/dart_gun cost = 4 @@ -938,11 +938,10 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item)) /datum/uplink_item/stealthy_weapons/RSG name = "Rapid Syringe Gun" - desc = "A rapid syringe gun able to hold six shot and fire them rapidly. Great together with the bioterror syringe" + desc = "A syndicate rapid syringe gun able to fill and fire syringes automatically from an internal reagent reservoir. Comes pre-loaded with 7 empty syringes, and has a maximum capacity of 14 syringes and 300u of reagents." reference = "RSG" - item = /obj/item/gun/syringe/rapidsyringe - cost = 4 - gamemodes = list(/datum/game_mode/nuclear) + item = /obj/item/gun/syringe/rapidsyringe/preloaded/half + cost = 8 /datum/uplink_item/stealthy_weapons/silencer name = "Universal Suppressor" diff --git a/code/modules/projectiles/guns/syringe_gun.dm b/code/modules/projectiles/guns/syringe_gun.dm index 467cd62468e..19e5bfc5852 100644 --- a/code/modules/projectiles/guns/syringe_gun.dm +++ b/code/modules/projectiles/guns/syringe_gun.dm @@ -14,7 +14,7 @@ var/list/syringes = list() var/max_syringes = 1 -/obj/item/gun/syringe/New() +/obj/item/gun/syringe/Initialize() ..() chambered = new /obj/item/ammo_casing/syringegun(src) @@ -49,18 +49,19 @@ return FALSE var/obj/item/reagent_containers/syringe/S - if(chambered.BB) // Remove the chambered syringe first + if(length(syringes)) + S = syringes[length(syringes)] + else if(chambered.BB) + // Remove the chambered syringe only if there's no syringe left S = new() chambered.BB.reagents.trans_to(S, chambered.BB.reagents.total_volume) qdel(chambered.BB) chambered.BB = null - else - S = syringes[length(syringes)] + process_chamber() user.put_in_hands(S) syringes.Remove(S) - process_chamber() - to_chat(user, "You unload [S] from \the [src]!") + to_chat(user, "You unload [S] from [src]!") return TRUE /obj/item/gun/syringe/attackby(obj/item/A, mob/user, params, show_msg = TRUE) @@ -69,7 +70,7 @@ if(in_clip < max_syringes) if(!user.unEquip(A)) return - to_chat(user, "You load [A] into \the [src]!") + to_chat(user, "You load [A] into [src]!") syringes.Add(A) A.loc = src process_chamber() // Chamber the syringe if none is already @@ -78,8 +79,7 @@ to_chat(user, "[src] cannot hold more syringes.") else return ..() - -/obj/item/gun/syringe/rapidsyringe +/obj/item/gun/syringe/rapidsyringe_old name = "rapid syringe gun" desc = "A modification of the syringe gun design, using a rotating cylinder to store up to six syringes." icon_state = "rapidsyringegun" @@ -95,3 +95,299 @@ force = 2 //Also very weak because it's smaller suppressed = 1 //Softer fire sound can_unsuppress = 0 //Permanently silenced + +// Not quite a syringe gun, but also not completely unlike one either. +// Uses an internal reservoir instead of separately filled syringes, and can unload them +// at a breakneck pace. +/obj/item/gun/syringe/rapidsyringe + name = "rapid syringe gun" + desc = "A syndicate rapid syringe gun based on an archived NanoTrasen prototype. Capable of storing and filling syringes from an internal reservoir. It has a large flap on the side that you can dump a box or bag of syringes into, and a port for filling it with liquid." + icon_state = "rapidsyringegun" + max_syringes = 14 // full two boxes worth + /// Maximum size of the internal reservoir + var/reservoir_volume = 300 + /// Possible amounts to fill each syringe with, toggleable by alt-clicking. + var/list/possible_transfer_amounts = list(5, 10, 15) + /// Index of possible_transfer_amounts that's currently active. + var/transfer_amount_selection = 1 + /// Amount of reagents to transfer out at once if no syringe is loaded. + var/reservoir_transfer_amount = 15 + /// Whether or not we've alerted the user that the reservoir is empty. + var/alarmed = TRUE // start out alarmed so a click just after buying doesn't give it away + container_type = AMOUNT_VISIBLE | TRANSPARENT // We'll handle transfers ourselves + +/obj/item/gun/syringe/rapidsyringe/Initialize(mapload) + . = ..() + create_reagents(reservoir_volume) + +/obj/item/gun/syringe/rapidsyringe/Destroy() + QDEL_LIST(syringes) + + if(chambered) + if(chambered.BB) + QDEL_NULL(chambered.BB) + qdel(chambered) + + return ..() + +/obj/item/gun/syringe/rapidsyringe/vv_edit_var(var_name, var_value) + . = ..() + switch(var_name) + if("reservoir_volume") + reagents.maximum_volume = var_value + +/obj/item/gun/syringe/rapidsyringe/build_reagent_description(mob/user) + . = list("There's a little window for the internal reservoir.") + . += ..() + +/obj/item/gun/syringe/rapidsyringe/proc/get_units_per_shot() + return possible_transfer_amounts[transfer_amount_selection] + +/obj/item/gun/syringe/rapidsyringe/examine(mob/user) + . = ..() + . += "A switch on the side is set to [get_units_per_shot()] unit\s per shot, alt-click to change it." + if(chambered?.BB) + . += "The chambered syringe contains [round(chambered.BB.reagents.total_volume)] units." + +/// If user is null in this proc, messages will just be ignored +/obj/item/gun/syringe/rapidsyringe/proc/insert_single_syringe(obj/item/reagent_containers/syringe/new_syringe, mob/user) + if(new_syringe.reagents.total_volume) + if(user) + to_chat(user, "[src] only accepts empty syringes.") + return FALSE + var/in_clip = length(syringes) + (chambered?.BB ? 1 : 0) + if(in_clip >= max_syringes) + if(user) + to_chat(user, "[src] is full!") + return FALSE + + if(user) + if(!user.unEquip(new_syringe)) + return + to_chat(user, "You load \the [new_syringe] into [src].") + playsound(src, 'sound/weapons/gun_interactions/bulletinsert.ogg', 50, 1) + syringes.Add(new_syringe) + new_syringe.forceMove(src) + + process_chamber() // Chamber the syringe if none is already + return TRUE + +/obj/item/gun/syringe/rapidsyringe/attackby(obj/item/A, mob/user, params, show_msg) + + if(istype(A, /obj/item/storage)) + // Boxes can be dumped in. + var/obj/item/storage/container = A + if(!length(container.contents)) + to_chat(user, "[A] is empty!") + return TRUE + if(length(syringes) + (chambered?.BB ? 1 : 0) == max_syringes) + to_chat(user, "[src] is full!") + return TRUE + + var/total_inserted = 0 + var/found_any_syringe = FALSE + for(var/obj/item/reagent_containers/syringe/S in container) + found_any_syringe = TRUE + if((length(syringes) + (chambered?.BB ? 1 : 0)) == max_syringes) + // full + break + + // only allow empty syringes. + // no user call here since it'd otherwise be spamming the user with chat messages. + if(insert_single_syringe(S)) + total_inserted++ + + if(total_inserted) + playsound(src, 'sound/weapons/gun_interactions/bulletinsert.ogg', 50, 1) + to_chat(user, "You load [total_inserted] empty syringes into [src].") + // Chamber a syringe. + process_chamber() + + else if(!found_any_syringe) + to_chat(user, "There are no empty syringes in [A]!") + return TRUE + + else if(istype(A, /obj/item/reagent_containers/syringe)) + insert_single_syringe(A, user) + return TRUE + + else if(istype(A, /obj/item/reagent_containers)) + // Loading with chemicals (but not from syringes) + var/obj/item/reagent_containers/incoming = A + if(!incoming.is_drainable()) + return + + if(!incoming.reagents.total_volume) + to_chat(user, "[incoming] is empty!") + return + + if(reagents.holder_full()) + to_chat(user, "[src]'s internal reservoir is full!") + return + + var/trans = incoming.reagents.trans_to(src, incoming.amount_per_transfer_from_this) + to_chat(user, "You transfer [round(trans)] unit\s of the solution to [src]'s internal reservoir.") + update_loaded_syringe() + + // Reset the reservoir alarm + alarmed = FALSE + + else + return ..() + +// Allow for emptying your deathmix, or for sec to find out what you were dumping into people +/obj/item/gun/syringe/rapidsyringe/afterattack(atom/target, mob/living/user, flag, params) + if(istype(target, /obj/item/reagent_containers)) + var/obj/item/reagent_containers/destination = target + + if(!destination.is_refillable()) + return + + if(!reagents.total_volume) + to_chat(user, "[src]'s internal reservoir is empty!") + return + + if(destination.reagents.holder_full()) + to_chat(user, "[destination] is full.") + return + + var/transfer_source = "[src]'s internal reservoir" + var/transfer_amount + + // Empty directly from the syringe, if it's loaded. + // Otherwise, we wouldn't be able to fully empty the gun's chems without firing it. + if(chambered?.BB && chambered.BB.reagents.total_volume) + transfer_amount = chambered.BB.reagents.trans_to(destination, get_units_per_shot()) + transfer_source = "the syringe chambered in [src]" + else + transfer_amount = reagents.trans_to(destination, reservoir_transfer_amount) + + to_chat(user, "You drain [transfer_amount] unit\s from [transfer_source] into [destination].") + // Refill the syringe + update_loaded_syringe() + return TRUE + else if(!reagents.total_volume && (!chambered?.BB || !chambered.BB.reagents.total_volume)) + // Play an alert when the reservoir has run out of reagents so people don't unknowingly dump empty syringes into their foes + // Running out of syringes is just handled by *click* + to_chat(user, "[src] [alarmed ? "beeps" : "whines"]: Internal chemical reservoir empty!") + if(!alarmed) + playsound(loc, 'sound/weapons/smg_empty_alarm.ogg', 25, 1, frequency = 60000) + alarmed = TRUE + // always send the to_chat so there's still feedback if the gun tries to fire + + return TRUE + else + return ..() + +// Switch the amount of reagents used per shot. +/obj/item/gun/syringe/rapidsyringe/AltClick(mob/living/user) + + transfer_amount_selection++ + if(transfer_amount_selection > length(possible_transfer_amounts)) + transfer_amount_selection = 1 + + playsound(src, 'sound/weapons/gun_interactions/selector.ogg', 25, 1) + to_chat(user, "[src] will now fill each syringe with up to [get_units_per_shot()] units.") + update_loaded_syringe() + +/// Update the chambered syringe's contents based on the reservoir contents. +/// Makes sure that what's contained in the syringe is representative of the mix as a whole. +/obj/item/gun/syringe/rapidsyringe/proc/update_loaded_syringe() + if(!chambered?.BB) + return + + // Refresh the mix in the syringe by pushing it into our internal beaker, and then pulling it out. + // The only time we /won't/ is when we dump out to a beaker. + if(chambered.BB.reagents.total_volume) + chambered.BB.reagents.trans_to(src, chambered.BB.reagents.total_volume) + + reagents.trans_to(chambered.BB, get_units_per_shot()) + +/obj/item/gun/syringe/rapidsyringe/process_chamber() + + if(!length(syringes) || chambered?.BB) + return + + var/obj/item/reagent_containers/syringe/S = syringes[1] + if(!S) + return + + chambered.BB = new S.projectile_type(src) + update_loaded_syringe() + chambered.BB.name = S.name + + syringes.Remove(S) + qdel(S) + +// Unload an empty syringe, making sure its existing contents get returned to the reservoir +/obj/item/gun/syringe/rapidsyringe/attack_self(mob/living/user) + if(!length(syringes) && !chambered.BB) + to_chat(user, "[src] is empty.") + return FALSE + + var/obj/item/reagent_containers/syringe/S + if(chambered.BB) // Remove the chambered syringe first + S = new() + // Dump it into the main reservoir beforehand + chambered.BB.reagents.trans_to(src, chambered.BB.reagents.total_volume) + qdel(chambered.BB) + chambered.BB = null + else + S = syringes[length(syringes)] + + user.put_in_hands(S) + syringes.Remove(S) + process_chamber() + playsound(src, "sound/weapons/gun_interactions/remove_bullet.ogg", 25, 1) + to_chat(user, "You unload [S] from [src].") + return TRUE + +/obj/item/gun/syringe/rapidsyringe/suicide_act(mob/user) + + if(!chambered?.BB) + visible_message("[user] puts [user.p_their()] mouth to [src]'s reagent port and swings [user.p_their()] head back, it looks like [user.p_theyre()] trying to commit suicide!") + if(!reagents.total_volume) + visible_message("...but there was nothing inside!") + return SHAME + else + reagents.trans_to(user, reagents.total_volume) + else + visible_message("[user] raises [src] to [user.p_their()] shoulder, it looks like [user.p_theyre()] trying to lose their medical license!") + handle_suicide(user, user) + return TOXLOSS + +/// Version that comes pre-loaded with a given amount of syringes. +/obj/item/gun/syringe/rapidsyringe/preloaded + /// The number of syringes to reload. If not set, defaults to max_syringes. + var/number_to_preload + +/obj/item/gun/syringe/rapidsyringe/preloaded/Initialize(mapload) + . = ..() + var/obj/item/reagent_containers/syringe/new_syringe + for(var/i in 0 to (number_to_preload == null ? max_syringes : number_to_preload)) + new_syringe = new() + insert_single_syringe(new_syringe) + +/// Version that comes loaded with half of the standard amount of syringes. Used in the uplink. +/obj/item/gun/syringe/rapidsyringe/preloaded/half + +/obj/item/gun/syringe/rapidsyringe/preloaded/half/Initialize(mapload) + number_to_preload = max_syringes / 2 + . = ..() + +/// For shenanigans. This is essentially an RSG that never needs to be refilled with syringes. +/obj/item/gun/syringe/rapidsyringe/preloaded/beaker_blaster + name = "beaker buster" + desc = "A syringe gun presumably cobbled together with the power of bluespace and powerful stimulants. Can shoot large amounts of reagents with an endless supply of syringes." + possible_transfer_amounts = list(1, 5, 10, 15, 20, 25, 30, 50) + reservoir_volume = 10000 // stupid big + max_syringes = 3 // doesn't really matter since they regen + +/obj/item/gun/syringe/rapidsyringe/preloaded/beaker_blaster/process_chamber() + . = ..() + // add a new syringe so it's technically infinite + insert_single_syringe(new /obj/item/reagent_containers/syringe) + +/obj/item/gun/syringe/rapidsyringe/preloaded/beaker_blaster/attack_self(mob/living/user) + // no printing infinite syringes. + return diff --git a/code/modules/reagents/reagent_containers/syringes.dm b/code/modules/reagents/reagent_containers/syringes.dm index b5e29bca1d8..bcfec88aeaa 100644 --- a/code/modules/reagents/reagent_containers/syringes.dm +++ b/code/modules/reagents/reagent_containers/syringes.dm @@ -3,7 +3,7 @@ #define SYRINGE_BROKEN 2 /obj/item/reagent_containers/syringe - name = "Syringe" + name = "syringe" desc = "A syringe." icon = 'icons/goonstation/objects/syringe.dmi' item_state = "syringe_0" diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm index 4ad9a03f193..72a27711bc8 100644 --- a/code/modules/research/designs/weapon_designs.dm +++ b/code/modules/research/designs/weapon_designs.dm @@ -175,16 +175,6 @@ materials = list(MAT_METAL = 6000, MAT_SILVER = 600, MAT_URANIUM = 2000) build_path = /obj/item/ammo_box/magazine/wt550m9/wttx -/datum/design/rapidsyringe - name = "Rapid Syringe Gun" - desc = "A gun that fires many syringes." - id = "rapidsyringe" - req_tech = list("combat" = 2, "biotech" = 3) - build_type = PROTOLATHE - materials = list(MAT_METAL = 5000, MAT_GLASS = 1000) - build_path = /obj/item/gun/syringe/rapidsyringe - category = list("Weapons") - /datum/design/stunshell name = "Stun Shell" desc = "A stunning shell for a shotgun."