Files
AlexisandMaia ae1cc71b34 Initial upstream fixes
Is this even working

Oooough

ok I'll crack at this

couple more fixes

Probably the worst of all my commits so far

Seriously, someone needs to check this.

It compiles

update atmos

stuff

update paths

snakes

linters 1

linters again

more CI

whoops

Update map_previews.dmi

Kills loose limbs

The component it used no longer exists.

podperson

CI Fixes(?)

Linters
2026-08-14 19:42:21 +02:00

891 lines
36 KiB
Plaintext

///Subtype for any kind of ballistic gun
///This has a shitload of vars on it, and I'm sorry for that, but it does make making new subtypes really easy
/obj/item/gun/ballistic
name = "projectile gun"
desc = "Now comes in flavors like GUN. Uses 10mm ammo, for some reason."
icon_state = "debug"
abstract_type = /obj/item/gun/ballistic
w_class = WEIGHT_CLASS_NORMAL
pickup_sound = 'sound/items/handling/gun/gun_pick_up.ogg'
drop_sound = 'sound/items/handling/gun/gun_drop.ogg'
sound_vary = TRUE
min_recoil = 0.1
///sound when inserting magazine
var/load_sound = 'sound/items/weapons/gun/general/magazine_insert_full.ogg'
///sound when inserting an empty magazine
var/load_empty_sound = 'sound/items/weapons/gun/general/magazine_insert_empty.ogg'
///volume of loading sound
var/load_sound_volume = 40
///whether loading sound should vary
var/load_sound_vary = TRUE
///sound of racking
var/rack_sound = 'sound/items/weapons/gun/general/bolt_rack.ogg'
///volume of racking
var/rack_sound_volume = 60
///whether racking sound should vary
var/rack_sound_vary = TRUE
///sound of when the bolt is locked back manually
var/lock_back_sound = 'sound/items/weapons/gun/general/slide_lock_1.ogg'
///volume of lock back
var/lock_back_sound_volume = 60
///whether lock back varies
var/lock_back_sound_vary = TRUE
///Sound of ejecting a magazine
var/eject_sound = 'sound/items/weapons/gun/general/magazine_remove_full.ogg'
///sound of ejecting an empty magazine
var/eject_empty_sound = 'sound/items/weapons/gun/general/magazine_remove_empty.ogg'
///volume of ejecting a magazine
var/eject_sound_volume = 40
///whether eject sound should vary
var/eject_sound_vary = TRUE
///sound of dropping the bolt or releasing a slide
var/bolt_drop_sound = 'sound/items/weapons/gun/general/bolt_drop.ogg'
///volume of bolt drop/slide release
var/bolt_drop_sound_volume = 60
///empty alarm sound (if enabled)
var/empty_alarm_sound = 'sound/items/weapons/gun/general/empty_alarm.ogg'
///empty alarm volume sound
var/empty_alarm_volume = 70
///whether empty alarm sound varies
var/empty_alarm_vary = TRUE
///Whether our gun clicks when it approaches an empty magazine/chamber
var/click_on_low_ammo = TRUE
/// What type (includes subtypes) of magazine will this gun accept being put into it
var/obj/item/ammo_box/magazine/accepted_magazine_type = /obj/item/ammo_box/magazine/m10mm
/// Whether the gun will spawn loaded with a magazine
var/spawnwithmagazine = TRUE
/// Change this if the gun should spawn with a different magazine type to what accepted_magazine_type defines. Will create errors if not a type or subtype of accepted magazine.
var/obj/item/ammo_box/magazine/spawn_magazine_type
///Whether the sprite has a visible magazine or not
var/mag_display = TRUE
///Whether the sprite has a visible ammo display or not
var/mag_display_ammo = FALSE
///Whether the sprite has a visible indicator for being empty or not.
var/empty_indicator = FALSE
///Whether the gun alarms when empty or not.
var/empty_alarm = FALSE
///Whether the gun supports multiple special mag types
var/special_mags = FALSE
/**
* The bolt type controls how the gun functions, and what iconstates you'll need to represent those functions.
* BOLT_TYPE_STANDARD - The Slide doesn't lock back. Clicking on it will only cycle the bolt. Only 1 sprite.
* BOLT_TYPE_OPEN - Same as standard, but it fires directly from the magazine - No need to rack. Doesn't hold the bullet when you drop the mag.
* BOLT_TYPE_LOCKING - This is most handguns and bolt action rifles. The bolt will lock back when it's empty. You need yourgun_bolt and yourgun_bolt_locked icon states.
* BOLT_TYPE_NO_BOLT - This is shotguns and revolvers. clicking will dump out all the bullets in the gun, spent or not.
* see combat.dm defines for bolt types: BOLT_TYPE_STANDARD; BOLT_TYPE_LOCKING; BOLT_TYPE_OPEN; BOLT_TYPE_NO_BOLT
**/
var/bolt_type = BOLT_TYPE_STANDARD
///Used for locking bolt and open bolt guns. Set a bit differently for the two but prevents firing when true for both.
var/bolt_locked = FALSE
var/show_bolt_icon = TRUE ///Hides the bolt icon.
///Whether the gun has to be racked each shot or not.
var/semi_auto = TRUE
///Actual magazine currently contained within the gun
var/obj/item/ammo_box/magazine/magazine
///whether the gun ejects the chambered casing
var/casing_ejector = TRUE
///Whether the gun has an internal magazine or a detatchable one. Overridden by BOLT_TYPE_NO_BOLT.
var/internal_magazine = FALSE
///Phrasing of the bolt in examine and notification messages; ex: bolt, slide, etc.
var/bolt_wording = "bolt"
///Phrasing of the magazine in examine and notification messages; ex: magazine, box, etx
var/magazine_wording = "magazine"
///Phrasing of the cartridge in examine and notification messages; ex: bullet, shell, dart, etc.
var/cartridge_wording = "bullet"
///length between individual racks
var/rack_delay = 5
///time of the most recent rack, used for cooldown purposes
var/recent_rack = 0
///Whether the gun can be tacloaded by slapping a fresh magazine directly on it
var/tac_reloads = TRUE //Snowflake mechanic no more.
///Whether we need to hold the gun in our off-hand to load it. FALSE means we can load it literally anywhere. Important for weapons like bows.
var/must_hold_to_load = FALSE
///Whether the gun can be sawn off by sawing tools
var/can_be_sawn_off = FALSE
var/suppressor_x_offset ///pixel offset for the suppressor overlay on the x axis.
var/suppressor_y_offset ///pixel offset for the suppressor overlay on the y axis.
/// Check if you are able to see if a weapon has a bullet loaded in or not.
var/hidden_chambered = FALSE
// Gun internal magazine modification and misfiring
///Can we modify our ammo type in this gun's internal magazine?
var/can_modify_ammo = FALSE
///our initial ammo type. Should match initial caliber, but a bit of redundency doesn't hurt.
var/initial_caliber
///our alternative ammo type.
var/alternative_caliber
///our initial fire sound. same reasons for initial caliber
var/initial_fire_sound
///our alternative fire sound, in case we want our gun to be louder or quieter or whatever
var/alternative_fire_sound
///If only our alternative ammuntion misfires and not our main ammunition, we set this to TRUE
var/alternative_ammo_misfires = FALSE
/// Misfire Variables ///
/// Whether our ammo misfires now or when it's set by the wrench_act. TRUE means it misfires.
var/can_misfire = FALSE
///How likely is our gun to misfire?
var/misfire_probability = 0
///How much does shooting the gun increment the misfire probability?
var/misfire_percentage_increment = 0
///What is the cap on our misfire probability? Do not set this to 100.
var/misfire_probability_cap = 25
/// Fire Selector Variables ///
/// Tracks the firemode of burst weapons. TRUE means it is in burst mode.
var/burst_fire_selection = FALSE
/// If it has an icon for a selector switch indicating current firemode.
var/selector_switch_icon = FALSE
/// Suppressor attached to the gun, if any
var/obj/item/suppressor/suppressor = null
/// Sound played when the burst mode is changed
var/burst_select_sound = SFX_FIRE_MODE_SWITCH
/obj/item/gun/ballistic/Initialize(mapload)
. = ..()
if(!spawn_magazine_type)
spawn_magazine_type = accepted_magazine_type
if (!spawnwithmagazine)
bolt_locked = TRUE
update_appearance()
return
if (!magazine)
magazine = new spawn_magazine_type(src)
if(!istype(magazine, accepted_magazine_type))
CRASH("[src] spawned with a magazine type that isn't allowed by its accepted_magazine_type!")
if(bolt_type == BOLT_TYPE_STANDARD || internal_magazine) //Internal magazines shouldn't get magazine + 1.
chamber_round()
else
chamber_round(replace_new_round = TRUE)
update_appearance()
RegisterSignal(src, COMSIG_ITEM_RECHARGED, PROC_REF(instant_reload))
/obj/item/gun/ballistic/Destroy()
QDEL_NULL(magazine)
QDEL_NULL(suppressor)
return ..()
/obj/item/gun/ballistic/Exited(atom/movable/gone, direction)
. = ..()
if(gone == suppressor)
clear_suppressor()
/obj/item/gun/ballistic/add_weapon_description()
AddElement(/datum/element/weapon_description, attached_proc = PROC_REF(add_notes_ballistic))
/obj/item/gun/ballistic/fire_sounds()
var/max_ammo = magazine?.max_ammo || initial(spawn_magazine_type.max_ammo)
var/current_ammo = get_ammo()
var/frequency_to_use = sin((90 / max_ammo) * current_ammo)
var/click_frequency_to_use = 1 - frequency_to_use * 0.75
var/play_click = round(sqrt(max_ammo * 2)) > current_ammo
if(suppressed)
playsound(src, suppressed_sound, suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
if(play_click && click_on_low_ammo)
playsound(src, 'sound/items/weapons/gun/general/ballistic_click.ogg', suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0, frequency = click_frequency_to_use)
else
playsound(src, fire_sound, fire_sound_volume, vary_fire_sound)
if(play_click && click_on_low_ammo)
playsound(src, 'sound/items/weapons/gun/general/ballistic_click.ogg', fire_sound_volume, vary_fire_sound, frequency = click_frequency_to_use)
/**
*
* Outputs type-specific weapon stats for ballistic weaponry based on its magazine and its caliber.
* It contains extra breaks for the sake of presentation
*
**/
/obj/item/gun/ballistic/proc/add_notes_ballistic()
if(magazine) // Make sure you have a magazine, to get the notes from
return "\n[magazine.add_notes_box()]"
else if(chambered) // if you don't have a magazine, is there something chambered?
return "\n[chambered.add_notes_ammo()]"
else // we have a very expensive mechanical paperweight.
return "\nThe lack of magazine and usable cartridge in chamber makes its usefulness questionable, at best."
/obj/item/gun/ballistic/vv_edit_var(vname, vval)
. = ..()
if(vname in list(NAMEOF(src, suppressor_x_offset), NAMEOF(src, suppressor_y_offset), NAMEOF(src, internal_magazine), NAMEOF(src, magazine), NAMEOF(src, chambered), NAMEOF(src, empty_indicator), NAMEOF(src, sawn_off), NAMEOF(src, bolt_locked), NAMEOF(src, bolt_type)))
update_appearance()
/obj/item/gun/ballistic/update_icon_state()
. = ..()
icon_state = "[base_icon_state || initial(icon_state)][sawn_off ? "_sawn" : ""]"
/obj/item/gun/ballistic/update_overlays()
. = ..()
if(selector_switch_icon)
if(burst_fire_selection)
. += "[initial(icon_state)]_burst"
else
. += "[initial(icon_state)]_semi"
if(show_bolt_icon)
if (bolt_type == BOLT_TYPE_LOCKING)
. += "[icon_state]_bolt[bolt_locked ? "_locked" : ""]"
if (bolt_type == BOLT_TYPE_OPEN && bolt_locked)
. += "[icon_state]_bolt"
if(suppressed && can_unsuppress) // if it can't be unsuppressed, we assume the suppressor is integrated into the gun itself and don't generate an overlay
var/mutable_appearance/MA = mutable_appearance(icon, "[icon_state]_suppressor")
if(suppressor_x_offset)
MA.pixel_w = suppressor_x_offset
if(suppressor_y_offset)
MA.pixel_z = suppressor_y_offset
. += MA
if(!chambered && empty_indicator) //this is duplicated in c20's update_overlayss due to a layering issue with the select fire icon.
. += "[icon_state]_empty"
if(gun_flags & TOY_FIREARM_OVERLAY)
. += "[icon_state]_toy"
if(!magazine || internal_magazine || !mag_display)
return
if(special_mags)
. += "[icon_state]_mag_[initial(magazine.icon_state)]"
if(mag_display_ammo && !magazine.ammo_count())
. += "[icon_state]_mag_empty"
return
. += "[icon_state]_mag"
if(!mag_display_ammo)
return
var/capacity_number
switch(get_ammo() / magazine.max_ammo)
if(1 to INFINITY) //cause we can have one in the chamber.
capacity_number = 100
if(0.8 to 1)
capacity_number = 80
if(0.6 to 0.8)
capacity_number = 60
if(0.4 to 0.6)
capacity_number = 40
if(0.2 to 0.4)
capacity_number = 20
if(capacity_number)
. += "[icon_state]_mag_[capacity_number]"
/obj/item/gun/ballistic/ui_action_click(mob/user, actiontype)
if(istype(actiontype, /datum/action/item_action/toggle_firemode))
burst_select()
else
..()
/obj/item/gun/ballistic/proc/burst_select()
var/mob/living/carbon/human/user = usr
burst_fire_selection = !burst_fire_selection
if(!burst_fire_selection)
burst_size = 1
fire_delay = 0 SECONDS
balloon_alert(user, "switched to semi-automatic")
else
burst_size = initial(burst_size)
fire_delay = initial(fire_delay)
balloon_alert(user, "switched to [burst_size]-round burst")
if(burst_select_sound)
playsound(user, burst_select_sound, 50, TRUE)
else
playsound(user, 'sound/items/weapons/empty.ogg', 100, TRUE)
update_appearance()
update_item_action_buttons()
// Didn't attempt to catch the casing.
#define CASING_CATCH_NO_ATTEMPT 0
// Tried to catch, failed because casing was hot and hands were unprotected.
#define CASING_CATCH_FAILED_SPICY 1
// Tried to catch, failed because clumsy.
#define CASING_CATCH_FAILED_CLUMSY 2
// Tried to catch, failed because hands full.
#define CASING_CATCH_FAILED_PLACEMENT 3
// Tried to catch, succeeded. Hands protected or casing was cold (not recently fired).
#define CASING_CATCH_SUCCESSFUL 4
// Tried to catch, succeeded. Casing was hot, hands were unprotected, hands burned.
#define CASING_CATCH_SUCCESSFUL_OUCH 5
// Offset added to an ejected casing's fire timestamp;
// if world.time is past the casing's fired timestamp plus this offset, casing is considered cold, and won't burn hands.
#define CASING_HOT_DELAY (5 SECONDS)
/obj/item/gun/ballistic/handle_chamber(empty_chamber = TRUE, from_firing = TRUE, chamber_next_round = TRUE)
if(!semi_auto && from_firing)
return
var/obj/item/ammo_casing/casing = chambered //Find chambered round
if(istype(casing)) //there's a chambered round
if(QDELING(casing))
stack_trace("Trying to move a qdeleted casing of type [casing.type]!")
chambered = null
else if(casing_ejector || !from_firing)
casing.forceMove(drop_location()) //Eject casing onto ground.
if(!QDELETED(casing))
SEND_SIGNAL(casing, COMSIG_CASING_EJECTED)
var/hitting_ground = TRUE
if(ishuman(loc))
var/mob/living/carbon/human/wielder = loc
var/caught_casing = can_catch_casing(casing, wielder, from_firing)
switch(caught_casing)
if(CASING_CATCH_NO_ATTEMPT)
hitting_ground = TRUE
if(CASING_CATCH_FAILED_SPICY)
hitting_ground = TRUE
wielder.visible_message(
span_warning("[wielder] reaches out for \the [casing] as it ejects from [src], and catches it... before fumbling it because it's a hot casing. Uncool!"),
span_warning("You reach out and catch \the [casing] as it ejects from [src]... before dropping it, because it's a hot casing! Ouch! Uncool!"),
span_notice("You hear someone reaching for something before a hiss of pain and the sound of something clattering."),
)
var/obj/item/bodypart/affecting = wielder.get_inactive_hand()
wielder.apply_damage(2, BURN, affecting, wound_bonus = CANT_WOUND)
if(CASING_CATCH_FAILED_CLUMSY)
hitting_ground = TRUE
wielder.visible_message(
span_warning("[wielder] reaches out for \the [casing] as it ejects from [src]... before fumbling it in an incredibly unlikely, comical manner! Uncool!"),
span_warning("You reach out and catch \the [casing] as it ejects from [src]... before fumbling it in an incredibly unlikely, comical manner! Uncool!"),
span_notice("You hear someone reaching for something, shortly followed by an embarassingly loud, comedic clattering."),
)
if(!(world.time >= casing.shot_timestamp + CASING_HOT_DELAY))
var/obj/item/bodypart/affecting = wielder.get_inactive_hand()
to_chat(wielder, span_warning("As if to add insult to injury, \the [casing] lands in the perfect way... to burn your [affecting.plaintext_zone]."))
wielder.apply_damage(5, BURN, affecting, wound_bonus = CANT_WOUND)
if(CASING_CATCH_FAILED_PLACEMENT)
hitting_ground = TRUE
wielder.visible_message(
span_warning("[wielder] reaches out for \the [casing] as it ejects from [src] and fumbles it due to [wielder.p_their()] full hands. Uncool!"),
span_warning("You try and reach out for \the [casing] as it ejects from [src], and fumble it because your hands are full. Uncool!"),
span_notice("You hear someone reaching for something, before a metallic clattering."),
)
if(CASING_CATCH_SUCCESSFUL)
hitting_ground = FALSE
casing.update_appearance()
to_chat(wielder, span_notice("You reach out and catch \the [casing] as it ejects from [src]. Awesome."))
if(CASING_CATCH_SUCCESSFUL_OUCH)
hitting_ground = FALSE
casing.update_appearance()
var/obj/item/bodypart/affecting = wielder.get_inactive_hand()
to_chat(wielder, span_notice("You reach out and catch \the [casing] as it ejects from [src]. Awesome. Your [affecting.plaintext_zone] hurts, though."))
wielder.apply_damage(4, BURN, affecting, wound_bonus = CANT_WOUND)
if(hitting_ground)
casing.bounce_away(TRUE)
else if(empty_chamber)
clear_chambered()
if (chamber_next_round && (magazine?.max_ammo > 1))
chamber_round()
/// Used to check if the mob `wielder` can catch an ejected casing.
/// Returns CASING_CATCH_NO_ATTEMPT if not trying, CASING_CATCH_FAILED if failed, CASING_CATCH_SUCCESSFUL if successful.
/obj/item/gun/ballistic/proc/can_catch_casing(obj/item/ammo_casing/casing, mob/living/carbon/human/wielder)
if(!wielder.throw_mode) // if they're not in throw mode, don't bother
return CASING_CATCH_NO_ATTEMPT
if(HAS_TRAIT(wielder, TRAIT_CLUMSY)) // feats of dexterity are beyond the jester
return CASING_CATCH_FAILED_CLUMSY
// following adapted from lightbulbs
var/protected_hands = FALSE
if(wielder.gloves)
var/obj/item/clothing/gloves/electrician_gloves = wielder.gloves
if(electrician_gloves.max_heat_protection_temperature && electrician_gloves.max_heat_protection_temperature > 360)
protected_hands = TRUE
// from left to right: are our hands protected from hot things via gloves? are we or our hands heat resistant? was this casing shot more than 5 seconds ago?
if(protected_hands || HAS_TRAIT(wielder, TRAIT_RESISTHEAT) || HAS_TRAIT(wielder, TRAIT_RESISTHEATHANDS) || world.time >= casing.shot_timestamp + CASING_HOT_DELAY)
if(wielder.put_in_hands(casing)) // try placement in hand,
return CASING_CATCH_SUCCESSFUL // success
return CASING_CATCH_FAILED_PLACEMENT // or not.
if(HAS_TRAIT(wielder, TRAIT_LIGHTBULB_REMOVER))
if(wielder.put_in_hands(casing)) // try placement in hand,
return CASING_CATCH_SUCCESSFUL_OUCH // success
return CASING_CATCH_FAILED_PLACEMENT // or not.
return CASING_CATCH_FAILED_SPICY
#undef CASING_CATCH_NO_ATTEMPT
#undef CASING_CATCH_FAILED_SPICY
#undef CASING_CATCH_FAILED_CLUMSY
#undef CASING_CATCH_FAILED_PLACEMENT
#undef CASING_CATCH_SUCCESSFUL
#undef CASING_CATCH_SUCCESSFUL_OUCH
#undef CASING_HOT_DELAY
///Used to chamber a new round and eject the old one
/obj/item/gun/ballistic/proc/chamber_round(spin_cylinder, replace_new_round)
if (chambered || !magazine)
return
if (magazine.ammo_count())
chambered = (bolt_type == BOLT_TYPE_OPEN && !bolt_locked) || bolt_type == BOLT_TYPE_NO_BOLT ? magazine.get_and_shuffle_round() : magazine.get_round()
if (bolt_type != BOLT_TYPE_OPEN && !(internal_magazine && bolt_type == BOLT_TYPE_NO_BOLT))
chambered.forceMove(src)
else
RegisterSignal(chambered, COMSIG_MOVABLE_MOVED, PROC_REF(clear_chambered))
if(replace_new_round)
magazine.give_round(new chambered.type)
/obj/item/gun/ballistic/proc/clear_chambered(datum/source)
SIGNAL_HANDLER
UnregisterSignal(chambered, COMSIG_MOVABLE_MOVED)
chambered = null
///updates a bunch of racking related stuff and also handles the sound effects and the like
/obj/item/gun/ballistic/proc/rack(mob/user = null)
if (bolt_type == BOLT_TYPE_NO_BOLT) //If there's no bolt, nothing to rack
return
if (bolt_type == BOLT_TYPE_OPEN)
if(!bolt_locked) //If it's an open bolt, racking again would do nothing
if (user)
balloon_alert(user, "[bolt_wording] already cocked!")
return
bolt_locked = FALSE
if (user)
balloon_alert(user, "[bolt_wording] racked")
process_chamber(!chambered, FALSE)
if (bolt_type == BOLT_TYPE_LOCKING && !chambered)
bolt_locked = TRUE
playsound(src, lock_back_sound, lock_back_sound_volume, lock_back_sound_vary)
else
playsound(src, rack_sound, rack_sound_volume, rack_sound_vary)
update_appearance()
///Drops the bolt from a locked position
/obj/item/gun/ballistic/proc/drop_bolt(mob/user = null)
playsound(src, bolt_drop_sound, bolt_drop_sound_volume, FALSE)
if (user)
balloon_alert(user, "[bolt_wording] dropped")
chamber_round()
bolt_locked = FALSE
update_appearance()
///Handles all the logic needed for magazine insertion
/obj/item/gun/ballistic/proc/insert_magazine(mob/user, obj/item/ammo_box/magazine/AM, display_message = TRUE)
if(!istype(AM, accepted_magazine_type))
balloon_alert(user, "[AM.name] doesn't fit!")
return FALSE
if(user.transferItemToLoc(AM, src))
magazine = AM
if (display_message)
balloon_alert(user, "[magazine_wording] loaded")
if (magazine.ammo_count())
playsound(src, load_sound, load_sound_volume, load_sound_vary)
else
playsound(src, load_empty_sound, load_sound_volume, load_sound_vary)
if (bolt_type == BOLT_TYPE_OPEN && !bolt_locked)
chamber_round()
update_appearance()
return TRUE
else
to_chat(user, span_warning("You cannot seem to get [src] out of your hands!"))
return FALSE
///Handles all the logic of magazine ejection, if tac_load is set that magazine will be tacloaded in the place of the old eject
/obj/item/gun/ballistic/proc/eject_magazine(mob/user, display_message = TRUE, obj/item/ammo_box/magazine/tac_load = null)
if(bolt_type == BOLT_TYPE_OPEN)
chambered = null
if (magazine.ammo_count())
playsound(src, eject_sound, eject_sound_volume, eject_sound_vary)
else
playsound(src, eject_empty_sound, eject_sound_volume, eject_sound_vary)
magazine.forceMove(drop_location())
var/obj/item/ammo_box/magazine/old_mag = magazine
if (tac_load)
if (insert_magazine(user, tac_load, FALSE))
balloon_alert(user, "[magazine_wording] swapped")
else
to_chat(user, span_warning("You dropped the old [magazine_wording], but the new one doesn't fit. How embarassing."))
magazine = null
else
magazine = null
user.put_in_hands(old_mag)
old_mag.update_appearance()
if (display_message)
balloon_alert(user, "[magazine_wording] unloaded")
update_appearance()
/obj/item/gun/ballistic/can_shoot()
return chambered?.loaded_projectile
/obj/item/gun/ballistic/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
. = ..()
if (.)
return
if (!internal_magazine && istype(tool, /obj/item/ammo_box/magazine))
if (!magazine)
insert_magazine(user, tool)
return ITEM_INTERACT_SUCCESS
if (tac_reloads)
eject_magazine(user, FALSE, tool)
return ITEM_INTERACT_SUCCESS
balloon_alert(user, "already loaded!")
return ITEM_INTERACT_FAILURE
if (isammocasing(tool) || istype(tool, /obj/item/ammo_box))
if (must_hold_to_load && !check_if_held(user))
return NONE
if (bolt_type == BOLT_TYPE_NO_BOLT || internal_magazine)
if (load_gun(tool, user))
return ITEM_INTERACT_SUCCESS
return ITEM_INTERACT_FAILURE
if(istype(tool, /obj/item/suppressor))
if(!can_suppress)
balloon_alert(user, "[tool.name] doesn't fit!")
return ITEM_INTERACT_FAILURE
if(!user.is_holding(src))
balloon_alert(user, "not in hand!")
return ITEM_INTERACT_FAILURE
if(suppressed)
balloon_alert(user, "already has a suppressor!")
return ITEM_INTERACT_FAILURE
if(!user.transferItemToLoc(tool, src))
balloon_alert(user, "cannot attach!")
return ITEM_INTERACT_FAILURE
balloon_alert(user, "[tool.name] attached")
install_suppressor(tool)
return ITEM_INTERACT_SUCCESS
if (can_be_sawn_off && sawoff(user, tool))
return ITEM_INTERACT_SUCCESS
/obj/item/gun/ballistic/proc/load_gun(obj/item/ammo, mob/living/user)
if (chambered && !chambered.loaded_projectile)
chambered.forceMove(drop_location())
if(length(magazine?.stored_ammo) && chambered != magazine.stored_ammo[1])
magazine.stored_ammo -= chambered
chambered = null
var/num_loaded = magazine?.try_load(user, ammo, silent = TRUE)
if (!num_loaded)
return FALSE
handle_box_reload(user, ammo, num_loaded) // BUBBER EDIT CHANGE - Original: balloon_alert(user, "[num_loaded] [cartridge_wording]\s loaded")
playsound(src, load_sound, load_sound_volume, load_sound_vary)
if (chambered == null && bolt_type == BOLT_TYPE_NO_BOLT)
chamber_round()
ammo.update_appearance()
update_appearance()
return TRUE
/obj/item/gun/ballistic/proc/check_if_held(mob/user)
if(src != user.get_inactive_held_item())
return FALSE
return TRUE
/obj/item/gun/ballistic/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0)
var/could_it_misfire = can_misfire || chambered.can_misfire
if(target != user && chambered.loaded_projectile && could_it_misfire && prob(misfire_probability) && blow_up(user))
to_chat(user, span_userdanger("[src] misfires!"))
return
if (sawn_off)
bonus_spread += SAWN_OFF_ACC_PENALTY
return ..()
/obj/item/gun/ballistic/shoot_live_shot(mob/living/user, pointblank = 0, atom/pbtarget = null, message = 1)
if(isnull(chambered))
return ..()
if(can_misfire)
misfire_probability += misfire_percentage_increment
misfire_probability = clamp(misfire_probability, 0, misfire_probability_cap)
if(chambered.can_misfire)
misfire_probability += chambered.misfire_increment
misfire_probability = clamp(misfire_probability, 0, misfire_probability_cap)
return ..()
///Installs a new suppressor, assumes that the suppressor is already in the contents of src
/obj/item/gun/ballistic/proc/install_suppressor(obj/item/suppressor/new_suppressor)
suppressor = new_suppressor
suppressed = suppressor.suppression
update_weight_class(w_class + suppressor.w_class) //so pistols do not fit in pockets when suppressed
can_muzzle_flash = FALSE
update_appearance()
/obj/item/gun/ballistic/clear_suppressor()
if(!can_unsuppress)
return
suppressed = SUPPRESSED_NONE
if(suppressor)
update_weight_class(w_class - suppressor.w_class)
suppressor = null
can_muzzle_flash = initial(can_muzzle_flash)
update_appearance()
/obj/item/gun/ballistic/click_alt(mob/user)
if(!suppressed || !can_unsuppress)
return CLICK_ACTION_BLOCKING
if(!user.is_holding(src))
return CLICK_ACTION_BLOCKING
balloon_alert(user, "[suppressor.name] removed")
user.put_in_hands(suppressor)
clear_suppressor()
return CLICK_ACTION_SUCCESS
///Prefire empty checks for the bolt drop
/obj/item/gun/ballistic/proc/prefire_empty_checks()
if (!chambered && !get_ammo())
if (bolt_type == BOLT_TYPE_OPEN && !bolt_locked)
bolt_locked = TRUE
playsound(src, bolt_drop_sound, bolt_drop_sound_volume)
update_appearance()
///postfire empty checks for bolt locking and sound alarms
/obj/item/gun/ballistic/proc/postfire_empty_checks(last_shot_succeeded)
if (!chambered && !get_ammo())
if (empty_alarm && last_shot_succeeded)
playsound(src, empty_alarm_sound, empty_alarm_volume, empty_alarm_vary)
update_appearance()
if (last_shot_succeeded && bolt_type == BOLT_TYPE_LOCKING && semi_auto)
bolt_locked = TRUE
update_appearance()
/obj/item/gun/ballistic/fire_gun(atom/target, mob/living/user, flag, params)
prefire_empty_checks()
. = ..() //The gun actually firing
postfire_empty_checks(.)
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/item/gun/ballistic/attack_hand(mob/user, list/modifiers)
if(!internal_magazine && loc == user && user.is_holding(src) && magazine)
eject_magazine(user)
return
return ..()
/obj/item/gun/ballistic/attack_self(mob/living/user)
if(!internal_magazine && magazine)
if(!magazine.ammo_count())
eject_magazine(user)
return
if(bolt_type == BOLT_TYPE_NO_BOLT)
unload_ammo(user)
return
if(bolt_type == BOLT_TYPE_LOCKING && bolt_locked)
drop_bolt(user)
return
if (recent_rack > world.time)
return
recent_rack = world.time + rack_delay
rack(user)
return
/obj/item/gun/ballistic/proc/unload_ammo(mob/living/user, forced = FALSE)
var/num_unloaded = 0
var/turf/drop_turf = get_turf(drop_location())
for(var/obj/item/ammo_casing/casing as anything in get_ammo_list(FALSE))
casing.forceMove(drop_location())
casing.bounce_away(FALSE, NONE)
num_unloaded++
if(drop_turf && is_station_level(drop_turf.z))
SSblackbox.record_feedback("tally", "station_mess_created", 1, casing.name)
if (!num_unloaded)
if (!forced)
balloon_alert(user, "it's empty!")
return
if (!forced)
balloon_alert(user, "[num_unloaded] [cartridge_wording]\s unloaded")
SEND_SIGNAL(src, COMSIG_UPDATE_AMMO_HUD) // BUBBER EDIT ADDITION - this is normally handled by eject_magazine() but internal magazines are a special case
playsound(user, eject_sound, eject_sound_volume, eject_sound_vary)
update_appearance()
/obj/item/gun/ballistic/examine(mob/user)
. = ..()
var/count_chambered = !(bolt_type == BOLT_TYPE_NO_BOLT || bolt_type == BOLT_TYPE_OPEN)
. += "It has <b>[get_ammo(count_chambered)]</b> round\s remaining."
if (!chambered && !hidden_chambered)
. += "It does not seem to have a round chambered."
if (bolt_locked)
. += "The [bolt_wording] is locked back and needs to be released before firing or de-fouling."
if (suppressor)
. += "It has a suppressor [can_unsuppress ? "attached that can be removed with <b>alt+click</b>." : "that is integral or can't otherwise be removed."]"
if(can_misfire)
. += span_danger("You get the feeling this might explode if you fire it...")
if(misfire_probability > 0)
. += span_danger("Given the state of the gun, there is a [misfire_probability]% chance it'll misfire.")
else if(misfire_probability > 0)
. += span_warning("You get a feeling this might explode if you fire it with the wrong ammunitions...")
. += span_warning("Given the state of the gun, there is a [EXAMINE_HINT("[misfire_probability]%")] chance it'll misfire.")
///Gets the number of bullets in the gun
/obj/item/gun/ballistic/proc/get_ammo(countchambered = TRUE)
var/bullets = 0 //No silly variable names on my watch.
if (chambered && countchambered)
bullets++
if (magazine)
bullets += magazine.ammo_count()
return bullets
///gets a list of every bullet in the gun
/obj/item/gun/ballistic/proc/get_ammo_list(countchambered = TRUE)
var/list/rounds = list()
if(chambered && countchambered)
rounds.Add(chambered)
if(magazine)
rounds.Add(magazine.ammo_list())
return rounds
#define BRAINS_BLOWN_THROW_RANGE 3
#define BRAINS_BLOWN_THROW_SPEED 1
/obj/item/gun/ballistic/suicide_act(mob/living/user)
var/obj/item/organ/brain/B = user.get_organ_slot(ORGAN_SLOT_BRAIN)
if (B && chambered && chambered.loaded_projectile && can_trigger_gun(user) && chambered.loaded_projectile.damage > 0)
user.visible_message(span_suicide("[user] is putting the barrel of [src] in [user.p_their()] mouth. It looks like [user.p_theyre()] trying to commit suicide!"))
sleep(2.5 SECONDS)
if(user.is_holding(src))
var/turf/T = get_turf(user)
process_fire(user, user, FALSE, null, BODY_ZONE_HEAD)
user.visible_message(span_suicide("[user] blows [user.p_their()] brain[user.p_s()] out with [src]!"))
var/turf/target = get_ranged_target_turf(user, REVERSE_DIR(user.dir), BRAINS_BLOWN_THROW_RANGE)
B.Remove(user)
B.forceMove(T)
var/datum/callback/gibspawner = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(spawn_atom_to_turf), /obj/effect/gibspawner/generic, B, 1, FALSE, user)
B.throw_at(target, BRAINS_BLOWN_THROW_RANGE, BRAINS_BLOWN_THROW_SPEED, callback=gibspawner)
return BRUTELOSS
else
user.visible_message(span_suicide("[user] panics and starts choking to death!"))
return OXYLOSS
else
user.visible_message(span_suicide("[user] is pretending to blow [user.p_their()] brain[user.p_s()] out with [src]! It looks like [user.p_theyre()] trying to commit suicide!</b>"))
playsound(src, dry_fire_sound, 30, TRUE)
return OXYLOSS
#undef BRAINS_BLOWN_THROW_SPEED
#undef BRAINS_BLOWN_THROW_RANGE
GLOBAL_LIST_INIT(gun_saw_types, typecacheof(list(
/obj/item/gun/energy/plasmacutter,
/obj/item/melee/energy,
/obj/item/dualsaber
)))
///Handles all the logic of sawing off guns,
/obj/item/gun/ballistic/proc/sawoff(mob/user, obj/item/saw, handle_modifications = TRUE)
if(!saw.get_sharpness() || (!is_type_in_typecache(saw, GLOB.gun_saw_types) && saw.tool_behaviour != TOOL_SAW)) //needs to be sharp. Otherwise turned off eswords can cut this.
return
if(sawn_off)
balloon_alert(user, "it's already shortened!")
return
if (SEND_SIGNAL(src, COMSIG_GUN_BEING_SAWNOFF, user) & COMPONENT_CANCEL_SAWING_OFF)
return
user.changeNext_move(CLICK_CD_MELEE)
user.visible_message(span_notice("[user] begins to shorten [src]."), span_notice("You begin to shorten [src]..."))
//if there's any live ammo inside the gun, makes it go off
if(blow_up(user))
user.visible_message(span_danger("[src] goes off!"), span_danger("[src] goes off in your face!"))
return
if(!do_after(user, 3 SECONDS, target = src))
return
if(sawn_off)
return
user.visible_message(span_notice("[user] shortens [src]!"), span_notice("You shorten [src]."))
sawn_off = TRUE
SEND_SIGNAL(src, COMSIG_GUN_SAWN_OFF)
if(!handle_modifications)
return TRUE
name = "sawn-off [src.name]"
desc = sawn_desc
update_weight_class(WEIGHT_CLASS_NORMAL)
//The file might not have a "gun" icon, let's prepare for this
lefthand_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi'
righthand_file = 'icons/mob/inhands/weapons/guns_righthand.dmi'
inhand_x_dimension = 32
inhand_y_dimension = 32
inhand_icon_state = "gun"
worn_icon_state = "gun"
slot_flags &= ~ITEM_SLOT_BACK //you can't sling it on your back
slot_flags |= ITEM_SLOT_BELT //but you can wear it on your belt (poorly concealed under a trenchcoat, ideally)
recoil = SAWN_OFF_RECOIL
update_appearance()
return TRUE
/obj/item/gun/ballistic/wrench_act(mob/living/user, obj/item/I)
if(!can_modify_ammo)
return
if(!user.is_holding(src))
balloon_alert(user, "hold to modify!")
return TRUE
if(get_ammo())
balloon_alert(user, "can't modify while loaded!")
return
if(!bolt_locked && bolt_type == BOLT_TYPE_LOCKING)
balloon_alert(user, "the bolt is in the way!")
return
balloon_alert(user, "tinkering...")
I.play_tool_sound(src)
if(!I.use_tool(src, user, 3 SECONDS))
return TRUE
if(magazine.caliber == initial_caliber)
magazine.caliber = alternative_caliber
if(alternative_ammo_misfires)
can_misfire = TRUE
fire_sound = alternative_fire_sound
to_chat(user, span_notice("You modify [src]. Now it will fire [alternative_caliber] rounds."))
else
magazine.caliber = initial_caliber
if(alternative_ammo_misfires)
can_misfire = FALSE
fire_sound = initial_fire_sound
to_chat(user, span_notice("You reset [src]. Now it will fire [initial_caliber] rounds."))
///used for sawing guns, causes the gun to fire without the input of the user
/obj/item/gun/ballistic/proc/blow_up(mob/user)
return chambered && process_fire(user, user, FALSE)
/obj/item/gun/ballistic/proc/instant_reload()
SIGNAL_HANDLER
if(magazine)
magazine.top_off()
else
if(!spawn_magazine_type)
return
magazine = new spawn_magazine_type(src)
chamber_round()
update_appearance()
/obj/item/gun/ballistic/toss_gun_hard(mob/living/carbon/thrower, mob/living/target)
. = ..()
if(!.)
return
switch(bolt_type)
if(BOLT_TYPE_NO_BOLT) //emptying the revolver cylinder
attack_self()
return
if(BOLT_TYPE_OPEN) //emptying the chamber of an automatic weapon, because rack() doesn't do this to it
handle_chamber(chamber_next_round = FALSE)
if(!internal_magazine && magazine) //if a magazine is attached to the weapon, we remove it and throw it aside
magazine.forceMove(drop_location())
magazine.throw_at(get_edge_target_turf(src, pick(GLOB.alldirs)), 1, 1)
magazine = null
update_icon() //updating the sprite of weapons without a magazine
if(!isnull(chambered)) //if there is a cartridge in the chamber, we remove it
rack()
/obj/item/suppressor
name = "suppressor"
desc = "A syndicate small-arms suppressor for maximum espionage."
icon = 'icons/obj/weapons/guns/ballistic.dmi'
icon_state = "suppressor"
w_class = WEIGHT_CLASS_TINY
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT, /datum/material/silver = HALF_SHEET_MATERIAL_AMOUNT)
/// How quiet should the gun be when we're installed?
var/suppression = SUPPRESSED_QUIET