diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index ab0fbc9d781..bb899492072 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -352,6 +352,7 @@ #define COMSIG_PROJECTILE_FIRE "projectile_fire" ///sent to targets during the process_hit proc of projectiles #define COMSIG_PROJECTILE_PREHIT "com_proj_prehit" + #define PROJECTILE_INTERRUPT_HIT (1<<0) ///from the base of /obj/projectile/Range(): () #define COMSIG_PROJECTILE_RANGE "projectile_range" ///from the base of /obj/projectile/on_range(): () diff --git a/code/datums/components/bullet_intercepting.dm b/code/datums/components/bullet_intercepting.dm new file mode 100644 index 00000000000..151f748e8aa --- /dev/null +++ b/code/datums/components/bullet_intercepting.dm @@ -0,0 +1,67 @@ +/** + * Component which allows an equipped item to occasionally absorb a projectile. + */ +/datum/component/bullet_intercepting + /// Chance to intercept a projectile + var/block_chance + /// Type of bullet to intercept + var/block_type + /// Slots in which effect can be active + var/active_slots + /// Person currently wearing us + var/mob/wearer + /// Callback called when we catch a projectile + var/datum/callback/on_intercepted + +/datum/component/bullet_intercepting/Initialize(block_chance = 2, block_type = BULLET, active_slots, datum/callback/on_intercepted) + . = ..() + if (!isitem(parent)) + return COMPONENT_INCOMPATIBLE + src.block_chance = block_chance + src.block_type = block_type + src.active_slots = active_slots + src.on_intercepted = on_intercepted + + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_parent_equipped)) + RegisterSignal(parent, COMSIG_ITEM_PRE_UNEQUIP, PROC_REF(on_unequipped)) + +/datum/component/bullet_intercepting/Destroy(force, silent) + QDEL_NULL(on_intercepted) + return ..() + +/// Called when item changes slots, check if we're in a valid location to take bullets +/datum/component/bullet_intercepting/proc/on_parent_equipped(obj/item/clothing/source, mob/equipper, slot) + SIGNAL_HANDLER + if (wearer) + if (!(active_slots & slot)) + on_unequipped() + return + + if (!(active_slots & slot)) + return + RegisterSignal(equipper, COMSIG_PROJECTILE_PREHIT, PROC_REF(on_wearer_shot)) + RegisterSignal(equipper, COMSIG_QDELETING, PROC_REF(on_wearer_deleted)) + wearer = equipper + +/// Called when item is unequipped, stop tracking bullets +/datum/component/bullet_intercepting/proc/on_unequipped() + SIGNAL_HANDLER + if (!wearer) + return + UnregisterSignal(wearer, list(COMSIG_PROJECTILE_PREHIT, COMSIG_QDELETING)) + wearer = null + +/// Called when wearer is shot, check if we're going to block the hit +/datum/component/bullet_intercepting/proc/on_wearer_shot(mob/living/victim, list/signal_args, obj/projectile/bullet) + SIGNAL_HANDLER + if (victim != wearer || victim.stat == DEAD || bullet.armor_flag != block_type ) + return + if (!prob(block_chance)) + return + on_intercepted?.Invoke(victim, bullet) + return PROJECTILE_INTERRUPT_HIT + +/// Called when wearer is deleted, stop tracking them +/datum/component/bullet_intercepting/proc/on_wearer_deleted() + SIGNAL_HANDLER + wearer = null diff --git a/code/game/objects/items/cigs_lighters.dm b/code/game/objects/items/cigs_lighters.dm index 264847e3cb6..e19824d09d4 100644 --- a/code/game/objects/items/cigs_lighters.dm +++ b/code/game/objects/items/cigs_lighters.dm @@ -740,8 +740,23 @@ CIGARETTE PACKETS ARE IN FANCY.DM . = ..() if(!overlay_state) overlay_state = pick(overlay_list) + AddComponent(\ + /datum/component/bullet_intercepting,\ + block_chance = 0.5,\ + active_slots = ITEM_SLOT_SUITSTORE,\ + on_intercepted = CALLBACK(src, PROC_REF(on_intercepted_bullet)),\ + ) update_appearance() +/// Destroy the lighter when it's shot by a bullet +/obj/item/lighter/proc/on_intercepted_bullet(mob/living/victim, obj/projectile/bullet) + victim.visible_message(span_warning("\The [bullet] shatters on [victim]'s lighter!")) + playsound(victim, get_sfx(SFX_RICOCHET), 100, TRUE) + new /obj/effect/decal/cleanable/oil(get_turf(src)) + do_sparks(1, TRUE, src) + victim.dropItemToGround(src, force = TRUE, silent = TRUE) + qdel(src) + /obj/item/lighter/cyborg_unequip(mob/user) if(!lit) return diff --git a/code/modules/antagonists/fugitive/hunters/hunter_outfits.dm b/code/modules/antagonists/fugitive/hunters/hunter_outfits.dm index 52de7ee46c0..395be958e5e 100644 --- a/code/modules/antagonists/fugitive/hunters/hunter_outfits.dm +++ b/code/modules/antagonists/fugitive/hunters/hunter_outfits.dm @@ -100,7 +100,7 @@ name = "Bounty Hunter - Armored" uniform = /obj/item/clothing/under/rank/prisoner back = /obj/item/storage/backpack - head = /obj/item/clothing/head/cowboy + head = /obj/item/clothing/head/cowboy/bounty suit = /obj/item/clothing/suit/space/hunter belt = /obj/item/gun/ballistic/automatic/pistol/fire_mag gloves = /obj/item/clothing/gloves/tackler/combat diff --git a/code/modules/clothing/head/hat.dm b/code/modules/clothing/head/hat.dm index 373787abe03..a17d3fb3c83 100644 --- a/code/modules/clothing/head/hat.dm +++ b/code/modules/clothing/head/hat.dm @@ -99,6 +99,24 @@ inhand_icon_state = null armor_type = /datum/armor/head_cowboy resistance_flags = FIRE_PROOF | ACID_PROOF + /// Chance that the hat will catch a bullet for you + var/deflect_chance = 2 + +/obj/item/clothing/head/cowboy/Initialize(mapload) + . = ..() + AddComponent(\ + /datum/component/bullet_intercepting,\ + block_chance = deflect_chance,\ + active_slots = ITEM_SLOT_HEAD,\ + on_intercepted = CALLBACK(src, PROC_REF(on_intercepted_bullet)),\ + ) + +/// When we catch a bullet, fling away +/obj/item/clothing/head/cowboy/proc/on_intercepted_bullet(mob/living/victim, obj/projectile/bullet) + victim.visible_message(span_warning("\The [bullet] sends [victim]'s hat flying!")) + victim.dropItemToGround(src, force = TRUE, silent = TRUE) + throw_at(get_edge_target_turf(loc, pick(GLOB.alldirs)), range = 3, speed = 3) + playsound(victim, get_sfx(SFX_RICOCHET), 100, TRUE) /datum/armor/head_cowboy melee = 5 @@ -106,6 +124,10 @@ laser = 5 energy = 15 +/// Bounty hunter's hat, very likely to intercept bullets +/obj/item/clothing/head/cowboy/bounty + deflect_chance = 50 + /obj/item/clothing/head/cowboy/black name = "desperado hat" desc = "People with ropes around their necks don't always hang." diff --git a/code/modules/library/bibles.dm b/code/modules/library/bibles.dm index 5a22b95db17..ecf85954fef 100644 --- a/code/modules/library/bibles.dm +++ b/code/modules/library/bibles.dm @@ -77,12 +77,39 @@ GLOBAL_LIST_INIT(bibleitemstates, list( unique = TRUE /// Deity this bible is related to var/deity_name = "Space Jesus" + /// Component which catches bullets for us + var/datum/component/bullet_catcher /obj/item/book/bible/Initialize(mapload) . = ..() AddComponent(/datum/component/anti_magic, MAGIC_RESISTANCE_HOLY) + bullet_catcher = AddComponent(\ + /datum/component/bullet_intercepting,\ + active_slots = ITEM_SLOT_SUITSTORE,\ + on_intercepted = CALLBACK(src, PROC_REF(on_intercepted_bullet)),\ + ) carve_out() +/obj/item/book/bible/Destroy(force) + QDEL_NULL(bullet_catcher) + return ..() + +/// Destroy the bible when it's shot by a bullet +/obj/item/book/bible/proc/on_intercepted_bullet(mob/living/victim, obj/projectile/bullet) + victim.add_mood_event("blessing", /datum/mood_event/blessing) + playsound(victim, 'sound/magic/magic_block_holy.ogg', 50, TRUE) + victim.visible_message(span_warning("\The [src] takes \the [bullet] in [victim]'s place!")) + var/obj/structure/fluff/paper/stack/pages = new(get_turf(src)) + pages.dir = pick(GLOB.alldirs) + name = "punctured bible" + desc = "A memento of good luck, or perhaps divine intervention?" + icon_state = "shot" + if (!GLOB.bible_icon_state) + GLOB.bible_icon_state = "shot" // New symbol of your religion if you hadn't picked one + atom_storage?.remove_all(get_turf(src)) + QDEL_NULL(atom_storage) + QDEL_NULL(bullet_catcher) + /obj/item/book/bible/examine(mob/user) . = ..() if(deity_name) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index cb38fb684c2..7f53613a672 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -503,7 +503,9 @@ return process_hit(T, select_target(T, target, bumped), bumped, hit_something) // try to hit something else // at this point we are going to hit the thing // in which case send signal to it - SEND_SIGNAL(target, COMSIG_PROJECTILE_PREHIT, args) + if (SEND_SIGNAL(target, COMSIG_PROJECTILE_PREHIT, args, src) & PROJECTILE_INTERRUPT_HIT) + qdel(src) + return BULLET_ACT_BLOCK if(mode == PROJECTILE_PIERCE_HIT) ++pierces hit_something = TRUE diff --git a/icons/obj/storage/book.dmi b/icons/obj/storage/book.dmi index aff3dc7ddd3..72cf214bd5b 100644 Binary files a/icons/obj/storage/book.dmi and b/icons/obj/storage/book.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 37451783ac3..078437c6e22 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -961,6 +961,7 @@ #include "code\datums\components\blood_walk.dm" #include "code\datums\components\bloodysoles.dm" #include "code\datums\components\boomerang.dm" +#include "code\datums\components\bullet_intercepting.dm" #include "code\datums\components\bumpattack.dm" #include "code\datums\components\burning.dm" #include "code\datums\components\butchering.dm"