From 903fbf4cf8413efde3178c49636e87d2f927b457 Mon Sep 17 00:00:00 2001 From: SierraKomodo <11140088+SierraKomodo@users.noreply.github.com> Date: Tue, 31 Dec 2024 15:26:57 +0000 Subject: [PATCH] [MIRROR] Ammo Boxes --- baystation12.dme | 1 + .../objects/items/weapons/storage/ammobox.dm | 438 ++++++++++++++++++ .../objects/items/weapons/storage/boxes.dm | 2 +- code/modules/projectiles/ammunition.dm | 316 +++++++++++-- .../modules/projectiles/ammunition/bullets.dm | 9 + code/modules/psionics/equipment/null_ammo.dm | 1 + 6 files changed, 735 insertions(+), 32 deletions(-) create mode 100644 code/game/objects/items/weapons/storage/ammobox.dm diff --git a/baystation12.dme b/baystation12.dme index 859a8f518d7d8..7802ac328c8c6 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -1204,6 +1204,7 @@ #include "code\game\objects\items\weapons\material\urn.dm" #include "code\game\objects\items\weapons\melee\energy.dm" #include "code\game\objects\items\weapons\melee\misc.dm" +#include "code\game\objects\items\weapons\storage\ammobox.dm" #include "code\game\objects\items\weapons\storage\backpack.dm" #include "code\game\objects\items\weapons\storage\bags.dm" #include "code\game\objects\items\weapons\storage\belt.dm" diff --git a/code/game/objects/items/weapons/storage/ammobox.dm b/code/game/objects/items/weapons/storage/ammobox.dm new file mode 100644 index 0000000000000..c25e8270e737f --- /dev/null +++ b/code/game/objects/items/weapons/storage/ammobox.dm @@ -0,0 +1,438 @@ +/obj/item/ammobox + name = "ammo box" + icon = 'icons/obj/weapons/ammo_boxes.dmi' + icon_state = "ammo" + desc = "A sturdy metal box with several warning symbols on the front.
WARNING: Live ammunition. Misuse may result in serious injury or death." + + /// Path (Subtypes of `/obj/item/ammo_casing`). The ammo type this ammo box holds. Generally, you should not modify directly. See `set_ammo_type()` + var/obj/item/ammo_casing/ammo_type + + /// Boolean. Whether or not the box is carrying spent or unspent rounds. + var/ammo_spent = FALSE + + /// Positive Integer. The amount on ammunition currently in this box. Generally, you should not modify directly. See `insert_casing()` and `remove_casing()`. + var/ammo_count + + /// Positive Integer. The maximum amount of ammunition this box can hold. + var/ammo_max = 100 + + +/obj/item/ammobox/pistol + ammo_type = /obj/item/ammo_casing/pistol + ammo_count = 100 + + +/obj/item/ammobox/Initialize(mapload) + . = ..() + if (. == INITIALIZE_HINT_QDEL || QDELETED(src)) + return + + if (!ammo_count) + ammo_type = null + ammo_spent = null + + update_name() + + +/obj/item/ammobox/examine(mob/user, distance, is_adjacent) + . = ..() + + if (!ammo_count) + to_chat(user, SPAN_NOTICE("It is empty.")) + else + var/remaining_space = ammo_max - ammo_count + to_chat(user, SPAN_NOTICE("It is currently holding [ammo_count] [initial(ammo_type.name)]\s and has room for [remaining_space] more.")) + + +/obj/item/ammobox/attack_hand(mob/user) + if (user.get_inactive_hand() != src) + return ..() + if (!ammo_count) + USE_FEEDBACK_FAILURE("\The [src] is empty.") + return TRUE + if (!do_after(user, 0.25 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src)) + return TRUE + var/obj/item/ammo_casing/casing = remove_casing(user, get_turf(user)) + user.put_in_hands(casing) + user.visible_message( + SPAN_NOTICE("\The [user] removes \a [casing] from \a [src]."), + SPAN_NOTICE("You remove \a [casing.get_ammo_casing_name()] from \the [src]. [ammo_count ? "It now holds [ammo_count] more." : "It is not empty."]") + ) + return TRUE + + + +/obj/item/ammobox/attack_self(mob/living/user) + if (!ammo_count) + USE_FEEDBACK_FAILURE("\The [src] is empty.") + return + var/confirm = alert(user, "Dump \the [src]'s contents on the floor?" , name, "Yes", "No") + if (confirm != "Yes" || !user.use_sanity_check(src)) + return + var/turf/target = get_turf(user) + while (ammo_count) + var/obj/item/ammo_casing/ammo_casing = remove_casing(user, target) + ammo_casing.set_dir(pick(GLOB.alldirs)) + user.visible_message( + SPAN_NOTICE("\The [user] dumps \a [src] all over the floor."), + SPAN_NOTICE("You dump \the [src] all over the floor.") + ) + + +/obj/item/ammobox/use_tool(obj/item/tool, mob/living/user, list/click_params) + // Ammo Box - Transfer contents + if (istype(tool, /obj/item/ammobox)) + var/obj/item/ammobox/donor_box = tool + if (!donor_box.ammo_count) + USE_FEEDBACK_FAILURE("\The [donor_box] is empty.") + return TRUE + + if (ammo_count >= ammo_max) + USE_FEEDBACK_FAILURE("\The [src] is full.") + return TRUE + + if (ammo_count && (donor_box.ammo_type != ammo_type || donor_box.ammo_spent != ammo_spent)) + USE_FEEDBACK_FAILURE("\The [donor_box]'s contents can't be mixed with the rounds already in \the [src].") + return TRUE + + user.visible_message( + SPAN_NOTICE("\The [user] starts dumping \a [tool] into \a [src]."), + SPAN_NOTICE("You start dumping \the [tool] into \the [src].") + ) + + var/partial = FALSE // Alters the visible message to say "partially" if there was a casing that halted the loop + while (donor_box.ammo_count > 0 && ammo_count < ammo_max) + if (!do_after(user, 0.5 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src, tool)) + partial = TRUE + break + if (!insert_casing(new donor_box.ammo_type(src, donor_box.ammo_spent))) + partial = TRUE + break + donor_box.remove_casing() + + user.visible_message( + SPAN_NOTICE("\The [user] [partial ? "partially " : null]dumps \a [tool] into \a [src]."), + SPAN_NOTICE("You [partial ? "partially " : null]dump \the [tool] into \the [src]. The target box now holds [ammo_count] round\s. [partial ? "The donor box has [donor_box.ammo_count] round\s remaining." : null]") + ) + return TRUE + + + // Ammo Casing - Attempt to add to the box. + if (istype(tool, /obj/item/ammo_casing)) + if (!can_insert_casing(tool, user)) + return TRUE + if (!do_after(user, 0.5 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src, tool)) + return TRUE + if (!insert_casing(tool, user)) + return TRUE + var/obj/item/ammo_casing/ammo_casing = tool + user.visible_message( + SPAN_NOTICE("\The [user] adds \a [tool] to \a [src]."), + SPAN_NOTICE("You add \a [ammo_casing.get_ammo_casing_name()] to \the [src]. It now holds [ammo_count] round\s.") + ) + return TRUE + + + // Ammo Magazine - Attempt to feed the rounds to the box. + if (istype(tool, /obj/item/ammo_magazine)) + var/obj/item/ammo_magazine/magazine = tool + if (!length(magazine.stored_ammo)) + USE_FEEDBACK_FAILURE("\The [tool] is empty.") + return TRUE + + if (ammo_count >= ammo_max) + USE_FEEDBACK_FAILURE("\The [src] is full.") + return TRUE + + var/obj/item/ammo_casing/casing = magazine.stored_ammo[length(magazine.stored_ammo)] + if (!can_insert_casing(casing, user)) + return TRUE + + user.visible_message( + SPAN_NOTICE("\The [user] starts emptying \a [tool] into \a [src]."), + SPAN_NOTICE("You start emptying \the [tool] into \the [src].") + ) + + var/partial = FALSE // Alters the visible message to say "partially" if there was a casing that halted the loop + while (length(magazine.stored_ammo)) + casing = magazine.stored_ammo[length(magazine.stored_ammo)] + if (!can_insert_casing(casing, user)) + partial = TRUE + break + if (!do_after(user, 0.25 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src, tool)) + partial = TRUE + break + casing = magazine.remove_casing(user, src) + if (!casing) + partial = TRUE + break + insert_casing(casing) + + user.visible_message( + SPAN_NOTICE("\The [user] [partial ? "partially " : null]empties \a [tool] into \a [src]."), + SPAN_NOTICE("You [partial ? "partially " : null]empty \the [tool] into \the [src].") + ) + return TRUE + + + return ..() + + +/obj/item/ammobox/use_after(atom/target, mob/living/user, click_parameters) + // Magazine - Load magazine + if (istype(target, /obj/item/ammo_magazine)) + if (!ammo_count) + USE_FEEDBACK_FAILURE("\The [src] is empty.") + return TRUE + var/obj/item/ammo_magazine/magazine = target + if (magazine.caliber != initial(ammo_type.caliber)) + USE_FEEDBACK_FAILURE("\The [src]'s ammunition does not fit into \the [target].") + return TRUE + if (length(magazine.stored_ammo) >= magazine.max_ammo) + USE_FEEDBACK_FAILURE("\The [target] is full.") + return TRUE + + user.visible_message( + SPAN_NOTICE("\The [user] starts loading \a [target] from \a [src]."), + SPAN_NOTICE("You start loading \the [target] from \the [src].") + ) + var/partial = FALSE + var/count = 0 + while (ammo_count > 0 && length(magazine.stored_ammo) < magazine.max_ammo) + if (!do_after(user, 0.5 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(target, src)) + partial = TRUE + break + if (length(magazine.stored_ammo) >= magazine.max_ammo) + partial = TRUE + break + var/casing = remove_casing(user, magazine) + if (!casing) + partial = TRUE + break + count++ + magazine.load_casing(casing, user) + + if (!count) + user.visible_message( + SPAN_NOTICE("\The [user] fails to load any rounds from \a [src] to \a [target]."), + SPAN_WARNING("You fail to load any rounds from \the [src] to \the [target].") + ) + return TRUE + user.visible_message( + SPAN_NOTICE("\The [user] [partial ? "partially " : null]loads \a [target] with \a [src]."), + SPAN_NOTICE("You [partial ? "partially " : null]load \the [target] with [count] round\s from \the [src].
\The [src] now has [ammo_count] round\s remaining.
\The [target] now has [length(magazine.stored_ammo)] round\s loaded.") + ) + return TRUE + + + // Try to scoop bullets up + var/obj/item/ammo_casing/clicked + var/turf/target_turf + var/obj/item/ammo_casing/target_type + var/target_spent = FALSE + if (isturf(target)) + target_turf = target + else if (istype(target, /obj/item/ammo_casing) && isturf(target.loc)) + clicked = target + target_turf = target.loc + target_type = target.type + target_spent = !clicked.BB + else + return ..() + + if (ammo_count >= ammo_max) + USE_FEEDBACK_FAILURE("\The [src] is full.") + return TRUE + + if (ammo_count && target_type) + if (target_type != ammo_type || ammo_spent != !clicked.BB) + USE_FEEDBACK_FAILURE("The [clicked.get_ammo_casing_name()] can't be mixed with the rounds already in \the [src].") + return TRUE + + var/list/candidates = list() + for (var/obj/item/ammo_casing/ammo_casing in target_turf) + if (!ammo_count) + target_type = ammo_casing.type + target_spent = !ammo_casing.BB + else if (ammo_count && (ammo_casing.type != target_type || target_spent != !ammo_casing.BB)) + continue + candidates += ammo_casing + + if (!length(candidates)) + USE_FEEDBACK_FAILURE("There are no bullets \the [src] can hold here.") + return TRUE + + user.visible_message( + SPAN_NOTICE("\The [user] starts loading \a [src] with loose rounds."), + SPAN_NOTICE("You start loading \the [src] with loose rounds.") + ) + var/count = 0 + for (var/obj/item/ammo_casing/ammo_casing as anything in candidates) + if (ammo_casing.loc != target_turf) + continue + if (ammo_count && (ammo_casing.type != ammo_type || ammo_spent != ammo_casing.BB)) + continue + if (!do_after(user, 0.25 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src, ammo_casing, SANITY_CHECK_DEFAULT & ~SANITY_CHECK_TOOL_IN_HAND)) + break + if (!insert_casing(ammo_casing, user)) + break + count++ + + if (!count) + user.visible_message( + SPAN_NOTICE("\The [user] fails to load \a [src] with loose rounds."), + SPAN_WARNING("You fail to load \the [src] with loose rounds.") + ) + return TRUE + user.visible_message( + SPAN_NOTICE("\The [user] loads \a [src] with loose rounds."), + SPAN_NOTICE("You load \the [src] with loose rounds.") + ) + return TRUE + + +/obj/item/ammobox/get_mechanics_info() + return "

A specialized box dedicated to holding loose ammunition. These boxes can only hold a single type of ammunition at a time.

" + + +/obj/item/ammobox/get_interactions_info() + . = ..() + .["Ammo Box"] = "Transfers the contents of the held box to the clicked box, if the ammo types match." + .["Bullet Casing"] = "Adds the bullet casing to the box, if the ammo types match." + .["Magazine"] = "Transfers the contents of the magazine to the box, if the ammo types match." + + +/** + * Sets the box's `ammo_type` to the given type, updating its name in the process. + * + * **Parameters**: + * - `new_ammo_type` (Path - Subtype of `obj/item/ammo_casing`). + * - `casing_spent` (Boolean). Only used if `new_ammo_type` is a path. Whether or not the casing is considered spent. + * + * Has no return value. + */ +/obj/item/ammobox/proc/set_ammo_type(obj/item/ammo_casing/new_ammo_type, casing_spent = FALSE) + if (isatom(new_ammo_type)) + if (!istype(new_ammo_type)) + return + casing_spent = !new_ammo_type.BB + new_ammo_type = new_ammo_type.type + + if (!ispath(new_ammo_type, /obj/item/ammo_casing)) + return + + if (ammo_type == new_ammo_type) + return + + ammo_type = new_ammo_type + ammo_spent = casing_spent + update_name() + + +/** + * Checks if the casing can be added to the box. + * + * Provides user feedback messages on failure. + * + * **Parameters**: + * - `ammo_casing` (Object or path. Subtypes of `/obj/item/ammo_casing`) - The casing to insert. Has to be the same type as `ammo_type`, unless `ammo_count` is `0`. + * - `user` - The mob attempting to insert the casing. Used for feedback messages. If not set, no feedback messages are sent. + * - `casing_spent` (Boolean). Only used if `ammo_casing` is a path. Whether or not the casing is considered spent. + * + * Returns boolean. `TRUE` if the casing was successfully inserted, `FALSE` otherwise. + */ +/obj/item/ammobox/proc/can_insert_casing(obj/item/ammo_casing/ammo_casing, mob/user, casing_spent = FALSE) + var/obj/item/ammo_casing/casing_type + var/casing_name = _get_ammo_casing_name(ammo_casing, casing_spent) + if (ispath(ammo_casing)) + casing_type = ammo_casing + else + casing_type = ammo_casing.type + casing_spent = !ammo_casing.BB + + if (!ispath(casing_type, /obj/item/ammo_casing)) + if (user) + USE_FEEDBACK_FAILURE("\The [src] isn't designed to hold \the [casing_name].") + return FALSE + + if (ammo_count && (ammo_type != casing_type || ammo_spent != casing_spent)) + if (user) + USE_FEEDBACK_FAILURE("\The [casing_name] can't be mixed with the rounds already in \the [src].") + return FALSE + + if (ammo_count >= ammo_max) + if (user) + USE_FEEDBACK_FAILURE("\The [src] is full.") + return FALSE + + return TRUE + + + +/** + * Adds `ammo_casing` to the ammo box's inventory. Checks `can_insert_casing()`. + * + * **Parameters**: + * - `ammo_casing` - The casing to insert. Has to be the same type as `ammo_type`, unless `ammo_count` is `0`. + * - `user` - The mob attempting to insert the casing. Used for feedback messages. If not set, no feedback messages are sent. + * - `skip_check` (Boolean, default `FALSE`) - If set, skips `can_insert_casing()` checks. Useful if you're already checking outside this proc, or simply want to force a casing. + * **Be warned this will also force update the ammoboxe's ammo type to the new casing.** + * + * Returns boolean. `TRUE` if the casing was successfully inserted, `FALSE` otherwise. + */ +/obj/item/ammobox/proc/insert_casing(obj/item/ammo_casing/ammo_casing, mob/user, skip_check = FALSE) + if (!skip_check && !can_insert_casing(ammo_casing, user)) + return FALSE + ammo_count++ + if (ammo_type != ammo_casing.type) + set_ammo_type(ammo_casing.type) + playsound(src, 'sound/weapons/guns/interaction/bullet_insert.ogg', 10, TRUE) + qdel(ammo_casing) + return TRUE + + +/** + * Removes a casing and places it in `target`. + * + * Provides user feedback messages on failure. + * + * **Parameters**: + * - `user` - The mob removing the casing. If not set, there will be no feedback messages. + * - `target` - The atom to place the casing in. If not set, the casing is not spawned and the round is simply removed. + * + * Returns the removed casing if one was created or `null`. + */ +/obj/item/ammobox/proc/remove_casing(mob/user, atom/target) + RETURN_TYPE(/obj/item/ammo_casing) + if (!ammo_count) + if (user) + USE_FEEDBACK_FAILURE("\The [src] is empty.") + return + + var/obj/item/ammo_casing/casing + ammo_count-- + if (target) + casing = new ammo_type(target, ammo_spent) + . = casing + + if (casing && isturf(target)) + playsound(src, pick(casing.fall_sounds), 10, TRUE) + else if (ismob(target)) + playsound(src, 'sound/weapons/guns/interaction/bullet_insert.ogg', 10, TRUE) + // Any other target type, i.e. magazine, already has its own sound effect + + if (!ammo_count) + ammo_type = null + ammo_spent = null + update_name() + + +/obj/item/ammobox/proc/update_name() + if (!ammo_count) + SetName("empty [initial(name)]") + return + SetName("[initial(name)] - [get_ammo_casing_name()]") + + +/obj/item/ammobox/proc/get_ammo_casing_name() + return _get_ammo_casing_name(ammo_type, ammo_spent) diff --git a/code/game/objects/items/weapons/storage/boxes.dm b/code/game/objects/items/weapons/storage/boxes.dm index 9d4cef4a36566..46448f451d3a1 100644 --- a/code/game/objects/items/weapons/storage/boxes.dm +++ b/code/game/objects/items/weapons/storage/boxes.dm @@ -171,7 +171,7 @@ /obj/item/storage/box/ammo - name = "ammo box" + name = "magazine box" icon = 'icons/obj/weapons/ammo_boxes.dmi' icon_state = "ammo" desc = "A sturdy metal box with several warning symbols on the front.
WARNING: Live ammunition. Misuse may result in serious injury or death." diff --git a/code/modules/projectiles/ammunition.dm b/code/modules/projectiles/ammunition.dm index bbdfe62890bd6..6e53b2c3102b6 100644 --- a/code/modules/projectiles/ammunition.dm +++ b/code/modules/projectiles/ammunition.dm @@ -1,3 +1,38 @@ +/** + * Determines the full descriptive name for an ammo casing. + * + * Global proc so it also functions with uninitialized type paths. + * + * **Parameters**: + * - `ammo_casing` (Object or path). + * - `spent` (Boolean, default `FALSE`). Only used if `ammo_casing` is a path. Whether the casing is considered spent or not. Otherwise, this is defined based on the presence of `ammo_casing.BB`. + * + * Returns string. + */ +/proc/_get_ammo_casing_name(obj/item/ammo_casing/ammo_casing, spent = FALSE) + if (!ammo_casing) + return + var/name + var/caliber + var/label + if (ispath(ammo_casing)) + name = initial(ammo_casing.name) + caliber = initial(ammo_casing.caliber) + label = initial(ammo_casing.label) + else + name = ammo_casing.name + caliber = ammo_casing.caliber + spent = !ammo_casing.BB + label = ammo_casing.label + + . = "[caliber] [name]" + if (spent) + . = "spent [.]" + if (label) + . = "[.] ([label])" + + + /obj/item/ammo_casing name = "bullet casing" desc = "A bullet casing." @@ -11,15 +46,19 @@ var/leaves_residue = TRUE var/caliber = "" //Which kind of guns it can be loaded into + /// String. Additional label used for `_get_ammo_casing_name()`. Should be things like 'practice', 'blank', 'AP', 'FMJ', etc. + var/label var/projectile_type //The bullet type to create when New() is called var/obj/item/projectile/BB = null //The loaded bullet - make it so that the projectiles are created only when needed? var/spent_icon = "pistolcasing-spent" var/fall_sounds = list('sound/weapons/guns/casingfall1.ogg','sound/weapons/guns/casingfall2.ogg','sound/weapons/guns/casingfall3.ogg') -/obj/item/ammo_casing/Initialize() - if(ispath(projectile_type)) +/obj/item/ammo_casing/Initialize(mapload, spawn_empty = FALSE) + if (ispath(projectile_type) && !spawn_empty) BB = new projectile_type(src) + if (spawn_empty) + update_icon() if(randpixel) pixel_x = rand(-randpixel, randpixel) pixel_y = rand(-randpixel, randpixel) @@ -97,6 +136,10 @@ to_chat(user, "This one is spent.") +/obj/item/ammo_casing/proc/get_ammo_casing_name() + return _get_ammo_casing_name(src) + + //An item that holds casings and can be used to put them inside guns /obj/item/ammo_magazine name = "magazine" @@ -149,50 +192,261 @@ update_icon() -/obj/item/ammo_magazine/use_tool(obj/item/W, mob/living/user, list/click_params) - if(istype(W, /obj/item/ammo_casing)) - var/obj/item/ammo_casing/C = W - if(C.caliber != caliber) - to_chat(user, SPAN_WARNING("\The [C] does not fit into \the [src].")) +/obj/item/ammo_magazine/use_tool(obj/item/tool, mob/living/user, list/click_params) + if (istype(tool, /obj/item/ammo_casing)) + if (!do_after(user, 0.25 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src, tool) || !load_casing(tool, user, TRUE)) return TRUE - if(length(stored_ammo) >= max_ammo) - to_chat(user, SPAN_WARNING("\The [src] is full!")) + user.visible_message( + SPAN_NOTICE("\The [user] loads \a [tool] into \a [src]."), + SPAN_NOTICE("You load \the [tool] into \the [src].") + ) + return TRUE + + + if (istype(tool, /obj/item/ammo_magazine)) + if (length(stored_ammo) >= max_ammo) + USE_FEEDBACK_FAILURE("\The [src] is full.") + return TRUE + var/obj/item/ammo_magazine/donor_magazine = tool + if (length(donor_magazine.stored_ammo) <= 0) + USE_FEEDBACK_FAILURE("\The [donor_magazine] is empty.") + return TRUE + if (donor_magazine.caliber != caliber) + USE_FEEDBACK_FAILURE("\The [donor_magazine]'s ammunition does not fit \the [src].") return TRUE - if(!user.unEquip(C, src)) - FEEDBACK_UNEQUIP_FAILURE(user, C) + + user.visible_message( + SPAN_NOTICE("\The [user] starts transferring bullets from \a [tool] to \a [src]."), + SPAN_NOTICE("You start transferring bullets from \the [tool] to \the [src].") + ) + var/partial = FALSE + var/count = 0 + while (length(donor_magazine.stored_ammo)) + if (!do_after(user, 0.5 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src, tool)) + partial = TRUE + break + if (length(donor_magazine.stored_ammo) <= 0) + USE_FEEDBACK_FAILURE("\The [donor_magazine] is empty.") + partial = TRUE + break + var/obj/item/ammo_casing/ammo_casing = donor_magazine.stored_ammo[length(donor_magazine.stored_ammo)] + if (!load_casing(ammo_casing)) + partial = TRUE + break + donor_magazine.stored_ammo -= ammo_casing + donor_magazine.update_icon() + count++ + + if (!count) + user.visible_message( + SPAN_NOTICE("\The [user] fails to transfer any bullets from \a [tool] to \a [src]."), + SPAN_NOTICE("Your fail to transfer any bullets from \the [tool] to \the [src].") + ) return TRUE - stored_ammo.Add(C) + user.visible_message( + SPAN_NOTICE("\The [user] [partial ? "partially " : null]transfers bullets from \a [tool] to \a [src]."), + SPAN_NOTICE("You [partial ? "partially " : null]transfer bullets from \the [tool] to \the [src].") + ) update_icon() + donor_magazine.update_icon() return TRUE + + return ..() -/obj/item/ammo_magazine/attack_self(mob/user) - if(!length(stored_ammo)) - to_chat(user, SPAN_NOTICE("[src] is already empty!")) - return - to_chat(user, SPAN_NOTICE("You empty [src].")) - for(var/obj/item/ammo_casing/C in stored_ammo) - C.forceMove(user.loc) - C.set_dir(pick(GLOB.alldirs)) +/obj/item/ammo_magazine/use_after(atom/target, mob/living/user, click_parameters) + // Try to scoop bullets up + var/turf/target_turf + if (isturf(target)) + target_turf = target + else if (istype(target, /obj/item/ammo_casing) && isturf(target.loc)) + target_turf = target.loc + if (!target_turf) + return ..() + + var/list/candidates = list() + for (var/obj/item/ammo_casing/ammo_casing in target_turf) + if (ammo_casing.caliber != caliber) + continue + candidates += ammo_casing + + if (!length(candidates)) + USE_FEEDBACK_FAILURE("There are no bullets \the [src] can hold here.") + return TRUE + if (length(stored_ammo) >= max_ammo) + USE_FEEDBACK_FAILURE("\The [src] is full.") + return TRUE + + user.visible_message( + SPAN_NOTICE("\The [user] starts loading \a [src] with loose bullets."), + SPAN_NOTICE("You start loading \the [src] with loose bullets.") + ) + var/count = 0 + for (var/obj/item/ammo_casing/ammo_casing as anything in candidates) + if (!do_after(user, 0.25 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src, ammo_casing, SANITY_CHECK_DEFAULT & ~SANITY_CHECK_TOOL_IN_HAND)) + break + if (!load_casing(ammo_casing, user)) + break + count++ + + if (!count) + user.visible_message( + SPAN_NOTICE("\The [user] fails to load \a [src] with loose bullets."), + SPAN_WARNING("You fail to load \the [src] with loose bullets.") + ) + return TRUE + user.visible_message( + SPAN_NOTICE("\The [user] loads \a [src] with loose bullets."), + SPAN_NOTICE("You load \the [src] with loose bullets.") + ) + return TRUE + + + +/** + * Checks if the casing can be loaded into this magazine. Provides user feedback messages on failure. + * + * Does not include unequip checks by default, as this is intended to be usable in cases those checks would be invalid. + * + * **Parameters**: + * - `ammo_casing` - The ammo casing to check. + * - `user` - The mob performing the action. If not set, feedback messages are skipped. + * + * Returns boolean. + */ +/obj/item/ammo_magazine/proc/can_load_casing(obj/item/ammo_casing/ammo_casing, mob/living/user) + if (!istype(ammo_casing) || ammo_casing.caliber != caliber) + if (user) + USE_FEEDBACK_FAILURE("\The [ammo_casing] does not fit in \the [src].") + return FALSE + + if (length(stored_ammo) >= max_ammo) + if (user) + USE_FEEDBACK_FAILURE("\The [src] is full.") + return FALSE + + return TRUE + + +/** + * Attempts to load the ammo casing into this magazine. Provides user feedback on failure. + * + * Checks `can_load_casing()`. + * +* **Parameters**: + * - `ammo_casing` - The ammo casing to load. + * - `user` - The mob performing the action. If not set, feedback messages are skipped. + * - `unequip_check` (Boolean, default `FALSE`) - If set, includes a `user.unEquip(ammo_casing, src)` check. + * + * Returns boolean. Indicates whether the casing was loaded or not. + */ +/obj/item/ammo_magazine/proc/load_casing(obj/item/ammo_casing/ammo_casing, mob/living/user, unequip_check = FALSE) + if (!can_load_casing(ammo_casing, user, unequip_check)) + return FALSE + + if (unequip_check && !user?.unEquip(ammo_casing, src)) + FEEDBACK_UNEQUIP_FAILURE(user, ammo_casing) + return FALSE + + playsound(src, 'sound/weapons/guns/interaction/shotgun_instert.ogg', 10, TRUE) + ammo_casing.forceMove(src) + stored_ammo += ammo_casing + update_icon() + return TRUE + + +/** + * Removes the last casing from the magazine. Provides user feedback on failure. + * + * Either `user` or `target` can be provided, the proc will function either way. + * + * **Parameters**: + * - `user` - The mob performing the action. + * - `target` (Default `user`, if set) - The target atom to move the casing to. If a mob, attempts to place it in hands. + * + * Returns the removed casing. + */ +/obj/item/ammo_magazine/proc/remove_casing(mob/living/user, atom/target = user) + if (!user && !target) + crash_with("`remove_casing()` requires either user or target, or both, to be provided. Neither were provided.") + return FALSE + + if (!length(stored_ammo)) + if (user) + USE_FEEDBACK_FAILURE("\The [src] is empty.") + return FALSE + + var/obj/item/ammo_casing/ammo_casing = stored_ammo[length(stored_ammo)] + stored_ammo -= ammo_casing + update_icon() + + if (ismob(target)) + var/mob/target_mob = target + target_mob.put_in_hands(ammo_casing) + else + ammo_casing.forceMove(target) + playsound(src, 'sound/weapons/guns/interaction/bullet_insert.ogg', 10, TRUE) + + return ammo_casing + + +/** + * Attempts to dump all casings into `target`. Provides user feedback on failure. + * + * **Parameters**: + * - `user` - The mob performing the action. + * - `target` (Default `get_turf(src)`) - The target atom to move the casings to. + * + * Returns a list of the removed casings, or `FALSE`. + */ +/obj/item/ammo_magazine/proc/dump_all_casings(mob/living/user, atom/target = get_turf(src)) + RETURN_TYPE(/list) + if (!length(stored_ammo)) + if (user) + USE_FEEDBACK_FAILURE("\The [src] is empty.") + return FALSE + + var/list/removed = list() + for (var/obj/item/ammo_casing/ammo_casing in stored_ammo) + playsound(src, pick(ammo_casing.fall_sounds), 10, TRUE) + ammo_casing.forceMove(target) + ammo_casing.set_dir(pick(GLOB.alldirs)) + removed += ammo_casing + stored_ammo.Cut() update_icon() + return removed + + +/obj/item/ammo_magazine/attack_self(mob/user) + if (!dump_all_casings(user, user.loc)) + return + user.visible_message( + SPAN_WARNING("\The [user] ejects \a [src]'s contents on the ground."), + SPAN_WARNING("You eject \the [src]'s contents on the ground.") + ) + /obj/item/ammo_magazine/attack_hand(mob/user) - if(user.get_inactive_hand() == src) - if(!length(stored_ammo)) - to_chat(user, SPAN_NOTICE("[src] is already empty!")) - else - var/obj/item/ammo_casing/C = stored_ammo[length(stored_ammo)] - stored_ammo-=C - user.put_in_hands(C) - user.visible_message("\The [user] removes \a [C] from [src].", SPAN_NOTICE("You remove \a [C] from [src].")) - update_icon() - else - ..() + if (user.get_inactive_hand() == src) + if (!length(stored_ammo)) + USE_FEEDBACK_FAILURE("\The [src] is empty.") + return TRUE + if (!do_after(user, 0.25 SECONDS, src, DO_PUBLIC_UNIQUE) || !user.use_sanity_check(src)) + return TRUE + var/atom/removed_casing = remove_casing(user) + if (!removed_casing) + return + user.visible_message( + SPAN_NOTICE("\The [user] removes \a [removed_casing] from \a [src]."), + SPAN_NOTICE("You remove \a [removed_casing] from \the [src].") + ) return + ..() + /obj/item/ammo_magazine/on_update_icon() ClearOverlays() diff --git a/code/modules/projectiles/ammunition/bullets.dm b/code/modules/projectiles/ammunition/bullets.dm index c1b0b982e35e7..1dd7b6cf25a90 100644 --- a/code/modules/projectiles/ammunition/bullets.dm +++ b/code/modules/projectiles/ammunition/bullets.dm @@ -8,11 +8,13 @@ /obj/item/ammo_casing/pistol/rubber desc = "A rubber pistol bullet casing." + label = "rubber" projectile_type = /obj/item/projectile/bullet/pistol/rubber icon_state = "pistolcasing_r" /obj/item/ammo_casing/pistol/practice desc = "A practice pistol bullet casing." + label = "practice" projectile_type = /obj/item/projectile/bullet/pistol/practice icon_state = "pistolcasing_p" @@ -25,11 +27,13 @@ /obj/item/ammo_casing/pistol/small/rubber desc = "A small pistol rubber bullet casing." + label = "rubber" projectile_type = /obj/item/projectile/bullet/pistol/rubber/holdout icon_state = "smallcasing_r" /obj/item/ammo_casing/pistol/small/practice desc = "A small pistol practice bullet casing." + label = "practice" projectile_type = /obj/item/projectile/bullet/pistol/practice icon_state = "smallcasing_p" @@ -85,6 +89,7 @@ desc = "A blank shell." icon_state = "blshell" spent_icon = "blshell-spent" + label = "blank" projectile_type = /obj/item/projectile/bullet/blank matter = list(MATERIAL_STEEL = 60) @@ -93,6 +98,7 @@ desc = "A practice shell." icon_state = "pshell" spent_icon = "pshell-spent" + label = "practice" projectile_type = /obj/item/projectile/bullet/shotgun/practice matter = list(MATERIAL_STEEL = 60) @@ -138,6 +144,7 @@ /obj/item/ammo_casing/rifle/practice desc = "A rifle practice bullet casing." + label = "practice" projectile_type = /obj/item/projectile/bullet/rifle/practice icon_state = "riflecasing_p" @@ -163,10 +170,12 @@ /obj/item/ammo_casing/rifle/military/light desc = "A low-power military rifle bullet casing." + label = "low-power" projectile_type = /obj/item/projectile/bullet/rifle/military /obj/item/ammo_casing/rifle/military/practice desc = "A military rifle practice bullet casing." + label = "practice" projectile_type = /obj/item/projectile/bullet/rifle/military/practice icon_state = "rifle_mil_p" diff --git a/code/modules/psionics/equipment/null_ammo.dm b/code/modules/psionics/equipment/null_ammo.dm index eb7fa46e428e7..162f85453c71c 100644 --- a/code/modules/psionics/equipment/null_ammo.dm +++ b/code/modules/psionics/equipment/null_ammo.dm @@ -8,6 +8,7 @@ /obj/item/ammo_casing/pistol/magnum/nullglass desc = "A revolver bullet casing with a nullglass coating." + label = "nullglass" projectile_type = /obj/item/projectile/bullet/nullglass /obj/item/ammo_casing/pistol/magnum/nullglass/disrupts_psionics()