From 727b299d33debf74a47ee693c0ee7d212b3c4b86 Mon Sep 17 00:00:00 2001
From: Luc <89928798+lewcc@users.noreply.github.com>
Date: Tue, 21 Jun 2022 13:26:27 -0700
Subject: [PATCH] Rapid Syringe Gun rework (#17816)
* First commit reworking most of RSG
* Fixes, add alternate versions
* Update in uplink, remove from R&D
* Add custom pre-loaded version to uplink
* Fixes message, linter error
* Change initialization of half rapidsyringe
* Some review updates
* Add some sound effects
* Use internal reagents instead of a literal bluespace beaker
* Update rapid syringe description
* Drop RSG to 8 TC
* Update code/modules/projectiles/guns/syringe_gun.dm
Co-authored-by: Charlie <69320440+hal9000PR@users.noreply.github.com>
* More review changes, improve messaging
* Update code/modules/projectiles/guns/syringe_gun.dm
Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com>
* Address reviews
* Remove extraneous check
* Message and documentation reviews
* Syringening
* More review
* attempt to bump CI
* spacing
* last review concerns
Co-authored-by: Charlie <69320440+hal9000PR@users.noreply.github.com>
Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com>
---
code/datums/uplink_item.dm | 9 +-
code/modules/projectiles/guns/syringe_gun.dm | 314 +++++++++++++++++-
.../reagents/reagent_containers/syringes.dm | 2 +-
.../research/designs/weapon_designs.dm | 10 -
4 files changed, 310 insertions(+), 25 deletions(-)
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."