Files
Bubberstation/code/modules/projectiles/ammunition/_ammunition.dm
necromanceranne 8f15d0ea2c Adds .38 Flare, which is a lethal laser bullet casing available for printing with Advanced Beam Weaponry. Adjust ammo material values. (#91726)
## About The Pull Request


![image](https://github.com/user-attachments/assets/1332d28c-8d5f-41b7-a614-b74027210145)

Adds the .38 Flare. It does 20 damage. This round highlights the target
for 2 minutes, and projectiles hitting the target always hit the limb
that the shooter was aiming at. Your shot has effectively perfect limb
accuracy, no matter how far the bullet needs to travel. This also
affects other projectiles hitting the target that aren't the .38 Flare.

To indicate whether or not a round in a magazine is either lead or
laser, I've borrowed the implementation of ammo overlays from the C-20r
toy magazines Now each bullet in the speedloaders for .38 can be a
distinct type visibly. Might be interesting if anyone wants to add
additional unique appearances for some of the other .38 rounds.

Reduces the overall cost of a lot of the ammunition in the sec and
autolathe, such as loose bullets. Also reduces the material quantity
within bullet casings.

## Why It's Good For The Game

> .38 Flare

By popular demand, I've come up with a new idea. And that idea...is an
anti-bullet deviation tool. Most players are probably not conscious of
bullet deviation. But if you're familiar with the mechanic, it's why
sometimes your shots may hit into an arm or a leg even though you were
aiming at the head.

The way this works is that over the course of a bullets flight, it
increases in inaccuracy and the pobability of drifting into a limb. From
my last estimate, shooting somewhere around 5 tiles away will usually
result in bullet drift.

While affected by the flare shot, that does not happen. You shoot at the
head, you hit the head.

To put it into perspective; you could consider bullet deviation a form
of damage loss (its a bit more complicated than this and there are
instances where it is positive), and this projectile eliminates that
problem for everyone shooting the target.

> Ammo cost

Some of these were pretty excessive for the cost, and a notable example
of this include the .357 loose casings, .310 Surplus loose casings, and
most shotgun shells. These should go down to roughly below 3/5th of a
sheet of metal when printed from a fully upgraded lathe.

Meanwhile, a single bullet casing was like a sheet of metal each, which
didn't seem right to me. So they're now by default one fifth of a sheet
of metal on recycle.

## Changelog
🆑
add: .38 Flare, a laser bullet! Available in both .38 speedloader and
BR-38 magazine once Advanced Beam Weaponry is researched.
add: .38 Flare highlights the target in an outline and makes sure your
bullets never accidentally hit any limb except the one you are aiming
at. Never accidentally hit someone in the arm when you were going for a
headshot.
balance: Reduces the printing costs of several ammunition types from the
autolathe and security lathe. Reduces the overall material contents of
said printed ammo/magazines. If you find a material dupe, let a coder
know.
/🆑

---------

Co-authored-by: projectkepler-RU <99981766+projectkepler-ru@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
2025-07-09 17:11:23 +00:00

168 lines
6.8 KiB
Plaintext

/obj/item/ammo_casing
name = "bullet casing"
desc = "A bullet casing."
icon = 'icons/obj/weapons/guns/ammo.dmi'
icon_state = "s-casing"
worn_icon_state = "bullet"
obj_flags = CONDUCTS_ELECTRICITY
slot_flags = ITEM_SLOT_BELT
throwforce = 0
w_class = WEIGHT_CLASS_TINY
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT)
override_notes = TRUE
///What sound should play when this ammo is fired
var/fire_sound = null
///Which kind of guns it can be loaded into
var/caliber = null
///The bullet type to create when New() is called
var/projectile_type = null
///the loaded projectile in this ammo casing
var/obj/projectile/loaded_projectile = null
///Pellets for spreadshot
var/pellets = 1
///Variance for inaccuracy fundamental to the casing
var/variance = 0
///Randomspread for automatics
var/randomspread = 0
///Delay for energy weapons
var/delay = 0
///Override this to make your gun have a faster fire rate, in tenths of a second. 4 is the default gun cooldown.
var/click_cooldown_override = 0
///the visual effect appearing when the ammo is fired.
var/firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect
///pacifism check for boolet, set to FALSE if bullet is non-lethal
var/harmful = TRUE
/// How much force is applied when fired in zero-G
var/newtonian_force = 1
///If set to true or false, this ammunition can or cannot misfire, regardless the gun can_misfire setting
var/can_misfire = null
///This is how much misfire probability is added to the gun when it fires this casing.
var/misfire_increment = 0
///If set, this casing will damage any gun it's fired from by the specified amount
var/integrity_damage = 0
/// Set when this casing is fired. Only used for checking if it should burn a user's hand when caught from an ejection port.
var/shot_timestamp = 0
/obj/item/ammo_casing/spent
name = "spent bullet casing"
loaded_projectile = null
/obj/item/ammo_casing/Initialize(mapload)
. = ..()
if(projectile_type)
loaded_projectile = new projectile_type(src)
pixel_x = base_pixel_x + rand(-10, 10)
pixel_y = base_pixel_y + rand(-10, 10)
setDir(pick(GLOB.alldirs))
update_appearance()
/obj/item/ammo_casing/Destroy()
var/turf/T = get_turf(src)
if(T && !loaded_projectile && is_station_level(T.z))
SSblackbox.record_feedback("tally", "station_mess_destroyed", 1, name)
QDEL_NULL(loaded_projectile)
return ..()
/obj/item/ammo_casing/add_weapon_description()
AddElement(/datum/element/weapon_description, attached_proc = PROC_REF(add_notes_ammo))
/**
*
* Outputs type-specific weapon stats for ammunition based on the projectile loaded inside the casing.
* Distinguishes between critting and stam-critting in separate lines
*
*/
/obj/item/ammo_casing/proc/add_notes_ammo()
// Try to get a projectile to derive stats from
var/obj/projectile/exam_proj = projectile_type
var/initial_damage = initial(exam_proj.damage)
var/initial_stamina = initial(exam_proj.stamina)
// projectile damage multiplier for guns with snowflaked damage multipliers
var/proj_damage_mult = 1
if(!ispath(exam_proj) || pellets == 0)
return
// are we in an ammo box?
if(isammobox(loc))
var/obj/item/ammo_box/our_box = loc
// is our ammo box in a gun?
if(isgun(our_box.loc))
var/obj/item/gun/our_gun = our_box.loc
// grab the damage multiplier
proj_damage_mult = our_gun.projectile_damage_multiplier
// if not, are we just in a gun e.g. chambered
else if(isgun(loc))
var/obj/item/gun/our_gun = loc
// grab the damage multiplier.
proj_damage_mult = our_gun.projectile_damage_multiplier
var/list/readout = list()
if(proj_damage_mult <= 0 || (initial_damage <= 0 && initial_stamina <= 0))
return "Our legal team has determined the offensive nature of these [span_warning(caliber)] rounds to be esoteric."
// No dividing by 0
if(initial_damage)
readout += "Most monkeys our legal team subjected to these [span_warning(caliber)] rounds succumbed to their wounds after [span_warning("[HITS_TO_CRIT((initial(exam_proj.damage) * proj_damage_mult) * pellets)] shot\s")] at point-blank, taking [span_warning("[pellets] shot\s")] per round."
if(initial_stamina)
readout += "[!readout.len ? "Most monkeys" : "More fortunate monkeys"] collapsed from exhaustion after [span_warning("[HITS_TO_CRIT((initial(exam_proj.stamina) * proj_damage_mult) * pellets)] impact\s")] of these [span_warning("[caliber]")] rounds."
return readout.Join("\n") // Sending over a single string, rather than the whole list
/obj/item/ammo_casing/update_icon_state()
icon_state = "[initial(icon_state)][loaded_projectile ? "-live" : null]"
return ..()
/obj/item/ammo_casing/update_desc()
desc = "[initial(desc)][loaded_projectile ? null : " This one is spent."]"
return ..()
/*
* On accidental consumption, 'spend' the ammo, and add in some gunpowder
*/
/obj/item/ammo_casing/on_accidental_consumption(mob/living/carbon/victim, mob/living/carbon/user, obj/item/source_item, discover_after = TRUE)
if(loaded_projectile)
loaded_projectile = null
update_appearance()
victim.reagents?.add_reagent(/datum/reagent/gunpowder, 3)
source_item?.reagents?.add_reagent(/datum/reagent/gunpowder, source_item.reagents.total_volume*(2/3))
return ..()
//proc to magically refill a casing with a new projectile
/obj/item/ammo_casing/proc/newshot() //For energy weapons, syringe gun, shotgun shells and wands (!).
if(!loaded_projectile)
loaded_projectile = new projectile_type(src, src)
/obj/item/ammo_casing/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers)
if(istype(I, /obj/item/ammo_box))
var/obj/item/ammo_box/box = I
if(isturf(loc))
var/boolets = 0
for(var/obj/item/ammo_casing/bullet in loc)
if (box.stored_ammo.len >= box.max_ammo)
break
if (bullet.loaded_projectile)
if (box.give_round(bullet, 0))
boolets++
else
continue
if (boolets > 0)
box.update_appearance()
to_chat(user, span_notice("You collect [boolets] shell\s. [box] now contains [box.stored_ammo.len] shell\s."))
else
to_chat(user, span_warning("You fail to collect anything!"))
else
return ..()
/obj/item/ammo_casing/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
bounce_away(FALSE, NONE)
return ..()
/obj/item/ammo_casing/proc/bounce_away(still_warm = FALSE, bounce_delay = 3)
update_appearance()
SpinAnimation(10, 1)
var/turf/T = get_turf(src)
if(still_warm && T?.bullet_sizzle)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound), src, 'sound/items/tools/welder.ogg', 20, 1), bounce_delay) //If the turf is made of water and the shell casing is still hot, make a sizzling sound when it's ejected.
else if(T?.bullet_bounce_sound)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound), src, T.bullet_bounce_sound, 20, 1), bounce_delay) //Soft / non-solid turfs that shouldn't make a sound when a shell casing is ejected over them.