Files
Bubberstation/code/modules/projectiles/ammunition/_ammunition.dm
Hatterhat 8e350f1f86 Splits ammo box multiload into a bitflag, adds a icon for .223, adds a desc for toy magazines (#92409)
## About The Pull Request

Ammo box `multiload` var was changed into a bitflag covering
`AMMO_BOX_MULTILOAD_IN`, and `AMMO_BOX_MULTILOAD_OUT`, respectively. By
default, ammo boxes have both multiloading in and out, while magazines
only have multiloading in, which means you can't use magazines as
speedloaders, but you can still feed ammo into magazines quickly if you
had, say, an ammo box for that magazine's caliber. Or... something.

---

Also adds an icon state for .223 casings, which is just an edit of the
old 7.62 casings to be shorter.
<img width="403" height="89" alt="image"
src="https://github.com/user-attachments/assets/b3206b94-c2b3-4b90-8162-b4c620bbe4cf"
/>

---

Also adds a description for toy gun magazines.

## Why It's Good For The Game

Splitting ammo box multiload into "multiload in" and "multiload out"
means you can't use an ammo box as a speedloader, which I thought was
silly. Emergent gameplay, sure, but also silly.

.223 having a unique icon instead of just looking like any other pistol
casing is nice (read: pistol-caliber casings from a rifle round annoyed
me).

Toy gun magazine thing was for funsies at this point.

## Changelog

🆑
code: Ammo boxes' multiload variable is now a bitflag that determines if
a magazine can be multiloaded into or out of. Ammo boxes can multiload
in or out, while magazines can only multiload in.
image: .223 ammo now has a distinct casing icon.
fix: Foam force magazines now have a proper description.
/🆑

---------

Co-authored-by: Hatterhat <Hatterhat@users.noreply.github.com>
2025-08-12 04:03:31 +10: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] [box.casing_phrasing]\s. [box] now contains [box.stored_ammo.len] [box.casing_phrasing]\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.