mirror of
https://github.com/Aurorastation/Aurora.3.git
synced 2026-07-11 16:07:36 +01:00
23789d4113
This PR is intended to sort out our existing guns into something resembling organization. In addition, ancient decrepit guns are removed, and abstract paths are labeled as such. In addition, all guns are now standardized to refer to their model. Considering the huge number of weapons we have, a `pistol` is not descriptive enough. NOTE: At over 200 files touched, this is being split. Not everything was sorted, but this is already a monster to sort through. The rest can wait.
360 lines
12 KiB
Plaintext
360 lines
12 KiB
Plaintext
ABSTRACT_TYPE(/obj/item/gun/projectile)
|
|
name = "gun"
|
|
desc = "A gun that fires bullets."
|
|
origin_tech = list(TECH_COMBAT = 2, TECH_MATERIAL = 2)
|
|
w_class = WEIGHT_CLASS_NORMAL
|
|
matter = list(DEFAULT_WALL_MATERIAL = 1000)
|
|
recoil = 1
|
|
|
|
var/caliber = "357" //determines which casings will fit
|
|
var/handle_casings = EJECT_CASINGS //determines how spent casings should be handled
|
|
var/load_method = SINGLE_CASING|SPEEDLOADER //1 = Single shells, 2 = box or quick loader, 3 = magazine
|
|
var/obj/item/ammo_casing/chambered = null
|
|
|
|
//For SINGLE_CASING or SPEEDLOADER guns
|
|
var/max_shells = 0 //the number of casings that will fit inside
|
|
var/ammo_type = null //the type of ammo that the gun comes preloaded with
|
|
var/list/loaded = list() //stored ammo
|
|
|
|
//For MAGAZINE guns
|
|
var/magazine_type = null //the type of magazine that the gun comes preloaded with
|
|
var/obj/item/ammo_magazine/ammo_magazine = null //stored magazine
|
|
var/list/allowed_magazines //determines list of which magazines will fit in the gun
|
|
var/auto_eject = 0 //if the magazine should automatically eject itself when empty.
|
|
var/auto_eject_sound = null
|
|
|
|
var/jam_num = 0 //Whether this gun is jammed and how many self-uses until it's unjammed
|
|
var/unjam_cooldown = 0 //Gives the unjammer some time after spamming unjam to not eject their mag
|
|
var/jam_chance = 0 //Chance it jams on fire
|
|
|
|
///Pixel offset for the suppressor overlay on the x axis.
|
|
var/suppressor_x_offset
|
|
///Pixel offset for the suppressor overlay on the y axis.
|
|
var/suppressor_y_offset
|
|
|
|
//TODO generalize ammo icon states for guns
|
|
//var/magazine_states = 0
|
|
//var/list/icon_keys = list() //keys
|
|
//var/list/ammo_states = list() //values
|
|
|
|
/obj/item/gun/projectile/mechanics_hints(mob/user, distance, is_adjacent)
|
|
. += ..()
|
|
. += "This is a ballistic weapon. It fires [caliber] ammunition. To reload most guns, click the gun with an empty hand to remove any spent casings or magazines, and then insert new ones."
|
|
|
|
/obj/item/gun/projectile/feedback_hints(mob/user, distance, is_adjacent)
|
|
. += ..()
|
|
if(distance > 1)
|
|
return
|
|
if(jam_num)
|
|
. += SPAN_WARNING("It looks jammed.")
|
|
if(ammo_magazine)
|
|
. += "It has \a [ammo_magazine] loaded."
|
|
if(suppressed)
|
|
. += "It has a suppressor attached."
|
|
. += "Has [get_ammo()] round\s remaining."
|
|
|
|
/obj/item/gun/projectile/Initialize()
|
|
. = ..()
|
|
if(ispath(ammo_type) && (load_method & (SINGLE_CASING|SPEEDLOADER)))
|
|
for(var/i in 1 to max_shells)
|
|
loaded += new ammo_type(src)
|
|
if(ispath(magazine_type) && (load_method & MAGAZINE))
|
|
ammo_magazine = new magazine_type(src)
|
|
update_icon()
|
|
|
|
/obj/item/gun/projectile/Destroy()
|
|
chambered = null
|
|
QDEL_NULL(ammo_magazine)
|
|
QDEL_LIST(loaded)
|
|
. = ..()
|
|
|
|
/obj/item/gun/projectile/update_icon()
|
|
..()
|
|
if(suppressed)
|
|
var/mutable_appearance/MA = mutable_appearance('icons/obj/guns/attachments/suppressor.dmi', "suppressor")
|
|
if(suppressor_x_offset)
|
|
MA.pixel_x = suppressor_x_offset
|
|
if(suppressor_y_offset)
|
|
MA.pixel_y = suppressor_y_offset
|
|
underlays += MA
|
|
|
|
/obj/item/gun/projectile/consume_next_projectile()
|
|
if(jam_num)
|
|
return FALSE
|
|
//get the next casing
|
|
if(loaded.len)
|
|
chambered = loaded[1] //load next casing.
|
|
if(handle_casings != HOLD_CASINGS)
|
|
loaded -= chambered
|
|
else if(ammo_magazine && ammo_magazine.stored_ammo.len)
|
|
chambered = ammo_magazine.stored_ammo[1]
|
|
if(handle_casings != HOLD_CASINGS)
|
|
ammo_magazine.stored_ammo -= chambered
|
|
|
|
if(chambered)
|
|
return chambered.BB
|
|
return null
|
|
|
|
/obj/item/gun/projectile/handle_post_fire(mob/user)
|
|
..()
|
|
if(chambered)
|
|
chambered.expend()
|
|
process_chambered()
|
|
if(ammo_magazine && !length(ammo_magazine.stored_ammo) && ishuman(user))
|
|
var/mob/living/carbon/human/H = user
|
|
if(H.check_weapon_affinity(src))
|
|
unload_ammo(user, TRUE, TRUE)
|
|
update_maptext()
|
|
|
|
/obj/item/gun/projectile/handle_click_empty()
|
|
..()
|
|
process_chambered()
|
|
|
|
/obj/item/gun/projectile/special_check(var/mob/user)
|
|
if(!..())
|
|
return FALSE
|
|
if(!jam_num && jam_chance && get_ammo())
|
|
if(prob(jam_chance))
|
|
playsound(src.loc, 'sound/items/trayhit2.ogg', 50, TRUE)
|
|
to_chat(user, SPAN_DANGER("\The [src] jams!"))
|
|
balloon_alert(user, SPAN_RED("JAM"))
|
|
jam_num = rand(2, 5) // gotta attackself two to five times to unjam
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/obj/item/gun/projectile/proc/process_chambered()
|
|
if (!chambered) return
|
|
|
|
// Aurora forensics port, gunpowder residue.
|
|
if(chambered.leaves_residue)
|
|
var/mob/living/carbon/human/H = loc
|
|
if(istype(H))
|
|
if(!istype(H.gloves, /obj/item/clothing))
|
|
LAZYDISTINCTADD(H.gunshot_residue, chambered.caliber)
|
|
else
|
|
var/obj/item/clothing/G = H.gloves
|
|
LAZYDISTINCTADD(G.gunshot_residue, chambered.caliber)
|
|
|
|
switch(handle_casings)
|
|
if(DELETE_CASINGS)
|
|
qdel(chambered)
|
|
if(EJECT_CASINGS) //eject casing onto ground.
|
|
chambered.forceMove(get_turf(src))
|
|
chambered.throw_at(get_ranged_target_turf(get_turf(src),turn(loc.dir,270),1), rand(0,1), 5)
|
|
playsound(chambered, SFX_CASING_DROP, 50, FALSE)
|
|
if(CYCLE_CASINGS) //cycle the casing back to the end.
|
|
if(ammo_magazine)
|
|
ammo_magazine.stored_ammo += chambered
|
|
else
|
|
loaded += chambered
|
|
|
|
if(handle_casings != HOLD_CASINGS)
|
|
chambered = null
|
|
|
|
|
|
//Attempts to load A into src, depending on the type of thing being loaded and the load_method
|
|
//Maybe this should be broken up into separate procs for each load method?
|
|
/obj/item/gun/projectile/proc/load_ammo(var/obj/item/A, mob/user)
|
|
if(istype(A, /obj/item/ammo_magazine))
|
|
var/obj/item/ammo_magazine/AM = A
|
|
if(!(load_method & AM.mag_type) || caliber != AM.caliber || (allowed_magazines && !is_type_in_list(A, allowed_magazines)))
|
|
to_chat(user,SPAN_WARNING("[AM] won't load into [src]!"))
|
|
return
|
|
switch(AM.mag_type)
|
|
if(MAGAZINE)
|
|
if(ammo_magazine)
|
|
to_chat(user,SPAN_WARNING("[src] already has a magazine loaded.")) //already a magazine here
|
|
return
|
|
user.remove_from_mob(AM)
|
|
AM.forceMove(src)
|
|
ammo_magazine = AM
|
|
user.visible_message("[user] inserts [AM] into [src].", SPAN_NOTICE("You insert [AM] into [src]."))
|
|
playsound(src.loc, AM.insert_sound, 50, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
|
|
if(SPEEDLOADER)
|
|
if(loaded.len >= max_shells)
|
|
to_chat(user,SPAN_WARNING("[src] is full!"))
|
|
return
|
|
var/count = 0
|
|
for(var/obj/item/ammo_casing/C in AM.stored_ammo)
|
|
if(loaded.len >= max_shells)
|
|
break
|
|
if(C.caliber == caliber)
|
|
C.forceMove(src)
|
|
loaded += C
|
|
AM.stored_ammo -= C //should probably go inside an ammo_magazine proc, but I guess less proc calls this way...
|
|
count++
|
|
if(count)
|
|
user.visible_message("[user] reloads [src].", SPAN_NOTICE("You load [count] round\s into [src] using \the [AM]."))
|
|
playsound(src.loc, AM.insert_sound, 50, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
|
|
AM.update_icon()
|
|
else if(istype(A, /obj/item/ammo_casing))
|
|
var/obj/item/ammo_casing/C = A
|
|
if(!(load_method & SINGLE_CASING))
|
|
to_chat(user,SPAN_WARNING("[src] can not be loaded with single casings."))
|
|
return //incompatible
|
|
if(caliber != C.caliber)
|
|
to_chat(user,SPAN_WARNING("\The [C] does not fit."))
|
|
return //incompatible
|
|
if(loaded.len >= max_shells)
|
|
to_chat(user,SPAN_WARNING("[src] is full."))
|
|
return
|
|
|
|
user.remove_from_mob(C)
|
|
C.forceMove(src)
|
|
loaded.Insert(1, C) //add to the head of the list
|
|
user.visible_message("[user] inserts \a [C] into [src].", SPAN_NOTICE("You insert \a [C] into [src]."))
|
|
playsound(src.loc, C.reload_sound, 50, extrarange = SILENCED_SOUND_EXTRARANGE) //Casings, aka single bullets, are extremely quiet
|
|
update_maptext()
|
|
update_icon()
|
|
|
|
//attempts to unload src. If allow_dump is set to 0, the speedloader unloading method will be disabled
|
|
/obj/item/gun/projectile/proc/unload_ammo(mob/user, allow_dump = TRUE, drop_mag = FALSE)
|
|
if(ammo_magazine)
|
|
if(drop_mag)
|
|
ammo_magazine.forceMove(user.loc)
|
|
else
|
|
user.put_in_hands(ammo_magazine)
|
|
user.visible_message("[user] removes [ammo_magazine] from [src].", SPAN_NOTICE("You remove [ammo_magazine] from [src]."))
|
|
playsound(src.loc, ammo_magazine.eject_sound, 50, extrarange = SHORT_RANGE_SOUND_EXTRARANGE, falloff_exponent = (SOUND_FALLOFF_EXPONENT+2))
|
|
ammo_magazine.update_icon()
|
|
ammo_magazine = null
|
|
else if(loaded.len)
|
|
//presumably, if it can be speed-loaded, it can be speed-unloaded.
|
|
if(allow_dump && (load_method & SPEEDLOADER))
|
|
var/count = 0
|
|
var/turf/T = get_turf(user)
|
|
if(T)
|
|
for(var/obj/item/ammo_casing/C in loaded)
|
|
C.forceMove(T)
|
|
playsound(C, SFX_CASING_DROP, 50, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_exponent = (SOUND_FALLOFF_EXPONENT+2))
|
|
count++
|
|
loaded.Cut()
|
|
if(count)
|
|
user.visible_message("[user] unloads [src].", SPAN_NOTICE("You unload [count] round\s from [src]."))
|
|
else if(load_method & SINGLE_CASING)
|
|
var/obj/item/ammo_casing/C = loaded[loaded.len]
|
|
loaded.len--
|
|
user.put_in_hands(C)
|
|
user.visible_message("[user] removes \a [C] from [src].", SPAN_NOTICE("You remove \a [C] from [src]."))
|
|
else
|
|
to_chat(user, SPAN_WARNING("[src] is empty."))
|
|
update_maptext()
|
|
update_icon()
|
|
|
|
/obj/item/gun/projectile/attackby(obj/item/attacking_item, mob/user)
|
|
. = ..()
|
|
if(.)
|
|
return
|
|
load_ammo(attacking_item, user)
|
|
if(istype(attacking_item, /obj/item/suppressor))
|
|
var/obj/item/suppressor/S = attacking_item
|
|
if(!can_suppress)
|
|
balloon_alert(user, "doesn't fit!")
|
|
return
|
|
|
|
if(suppressed)
|
|
balloon_alert(user, "already has a suppressor!")
|
|
return
|
|
|
|
if(user.l_hand != S && user.r_hand != S)
|
|
balloon_alert(user, "not in hand!")
|
|
return
|
|
|
|
user.drop_from_inventory(suppressor, src)
|
|
balloon_alert(user, "[S.name] attached")
|
|
install_suppressor(S)
|
|
return
|
|
|
|
/obj/item/gun/projectile/toggle_firing_mode(mob/user)
|
|
if(jam_num)
|
|
playsound(src.loc, 'sound/weapons/click.ogg', 50, TRUE)
|
|
jam_num--
|
|
if(!jam_num)
|
|
visible_message(SPAN_DANGER("\The [user] unjams \the [src]!"))
|
|
balloon_alert(user, SPAN_GREEN("CLEAR"))
|
|
playsound(src.loc, 'sound/weapons/unjam.ogg', 100, TRUE)
|
|
unjam_cooldown = world.time
|
|
else
|
|
balloon_alert(user, SPAN_YELLOW("CLICK"))
|
|
else if(unjam_cooldown + 2 SECONDS > world.time)
|
|
return
|
|
else if(firemodes.len > 1)
|
|
..()
|
|
else
|
|
unload_ammo(user)
|
|
|
|
/obj/item/gun/projectile/attack_hand(mob/user)
|
|
if(user.get_inactive_hand() == src)
|
|
unload_ammo(user, allow_dump=0)
|
|
else
|
|
return ..()
|
|
|
|
/obj/item/gun/projectile/afterattack(atom/A, mob/living/user)
|
|
..()
|
|
if(auto_eject && ammo_magazine && ammo_magazine.stored_ammo && !ammo_magazine.stored_ammo.len)
|
|
ammo_magazine.forceMove(get_turf(src.loc))
|
|
user.visible_message(
|
|
"[ammo_magazine] falls out and clatters on the floor!",
|
|
SPAN_NOTICE("[ammo_magazine] falls out and clatters on the floor!")
|
|
)
|
|
playsound(user, ammo_magazine.eject_sound, 40, FALSE)
|
|
ammo_magazine.update_icon()
|
|
ammo_magazine = null
|
|
update_icon() //make sure to do this after unsetting ammo_magazine
|
|
|
|
/obj/item/gun/projectile/get_ammo()
|
|
var/bullets = 0
|
|
if(loaded)
|
|
bullets += loaded.len
|
|
if(ammo_magazine && ammo_magazine.stored_ammo)
|
|
bullets += ammo_magazine.stored_ammo.len
|
|
if(chambered)
|
|
bullets += 1
|
|
return bullets
|
|
|
|
/obj/item/gun/projectile/get_print_info()
|
|
. = ""
|
|
if(load_method & (SINGLE_CASING|SPEEDLOADER))
|
|
. += "Load Type: Single Casing or Speedloader<br>"
|
|
. += "Max Shots: [max_shells]<br>"
|
|
if(length(loaded))
|
|
var/obj/item/ammo_casing/casing = loaded[1]
|
|
var/obj/projectile/P = new casing.projectile_type
|
|
. += "<br><b>Projectile</b><br>"
|
|
. += P.get_print_info()
|
|
else
|
|
. += "No ammunition loaded.<br>"
|
|
else
|
|
. += "Load Type: Magazine<br>"
|
|
if(ammo_magazine)
|
|
var/obj/item/ammo_casing/casing = new ammo_magazine.ammo_type
|
|
var/obj/projectile/P = new casing.projectile_type
|
|
. += "<br><b>Projectile</b><br>"
|
|
. += P.get_print_info()
|
|
else
|
|
. += "No magazine inserted.<br>"
|
|
. += ..(FALSE)
|
|
|
|
///Installs a new suppressor, assumes that the suppressor is already in the contents of src
|
|
/obj/item/gun/projectile/proc/install_suppressor(obj/item/suppressor/S)
|
|
suppressed = TRUE
|
|
w_class += S.w_class //Add our weight class to the item's weight class
|
|
suppressor = S
|
|
update_icon()
|
|
|
|
/obj/item/gun/projectile/clear_suppressor()
|
|
if(!can_unsuppress)
|
|
return
|
|
if(istype(suppressor))
|
|
w_class -= suppressor.w_class
|
|
return ..()
|
|
|
|
/obj/item/gun/projectile/AltClick(mob/user)
|
|
if(use_check_and_message(user))
|
|
return
|
|
if(suppressed && can_unsuppress)
|
|
balloon_alert(user, "[suppressor.name] removed")
|
|
user.put_in_hands(suppressor)
|
|
clear_suppressor()
|
|
return ..()
|