diff --git a/code/__HELPERS/pronouns.dm b/code/__HELPERS/pronouns.dm
index 12095e171e7..55a26a9e1b9 100644
--- a/code/__HELPERS/pronouns.dm
+++ b/code/__HELPERS/pronouns.dm
@@ -45,6 +45,13 @@
if(.)
. = "e[.]"
+// Functionally the \a macro, for the cases where you put a bicon between "some [bicon] pop corn"
+/datum/proc/p_a(temp_gender)
+ var/backslash_a = "\a [src]"
+ backslash_a = splittext_char(backslash_a, " ")
+ if(length(backslash_a) >= 2) // ["some", "pop", "corn"], but we dont want "\a ["Thing"]" which is just ["Thing"]
+ . = backslash_a[1]
+
//like clients, which do have gender.
/client/p_they(capitalized, temp_gender)
if(!temp_gender)
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 33db8019b97..646d2ded722 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -579,6 +579,42 @@ Returns 1 if the chain up to the area contains the given typepath
return starting_turf
+// returns turf relative to A for a given clockwise angle at set range
+// result is bounded to map size
+/proc/get_angle_target_turf(atom/A, angle, range)
+ if(!istype(A))
+ return null
+ var/x = A.x
+ var/y = A.y
+
+ x += range * sin(angle)
+ y += range * cos(angle)
+
+ //Restricts to map boundaries while keeping the final angle the same
+ var/dx = A.x - x
+ var/dy = A.y - y
+ var/ratio
+ if(dy == 0) //prevents divide-by-zero errors
+ ratio = INFINITY
+ else
+ ratio = dx / dy
+
+ if(x < 1)
+ y += (1 - x) / ratio
+ x = 1
+ else if(x > world.maxx)
+ y += (world.maxx - x) / ratio
+ x = world.maxx
+
+ if(y < 1)
+ x += (1 - y) * ratio
+ y = 1
+ else if(y > world.maxy)
+ x += (world.maxy - y) * ratio
+ y = world.maxy
+
+ return locate(round(x, 1), round(y, 1), A.z)
+
// returns turf relative to A offset in dx and dy tiles
// bound to map limits
/proc/get_offset_target_turf(atom/A, dx, dy)
diff --git a/code/game/objects/explosion.dm b/code/game/objects/explosion.dm
index 8fc02767591..f549f5f8cd3 100644
--- a/code/game/objects/explosion.dm
+++ b/code/game/objects/explosion.dm
@@ -276,6 +276,44 @@
T.color = null
T.maptext = ""
+/**
+ * Creates an explosion of shrapnel at a turf.
+ * - /turf/epicenter - where the explosion occurs
+ * - shrapnel_number - the amount of shrapnel to create
+ * - /obj/item/projectile/shrapnel_type - the type of shrapnel bullets to shoot
+ * - chance_to_hit_same_turf - the probability to hit someone on the same turf, doubled for someone lying down
+ */
+/proc/create_shrapnel(turf/epicenter, shrapnel_number = 10, obj/item/projectile/shrapnel_type = /obj/item/projectile/bullet/shrapnel, chance_to_hit_same_turf = 50)
+ epicenter = get_turf(epicenter)
+ if(!epicenter || !shrapnel_number || !shrapnel_type)
+ return
+ shrapnel_number = min(shrapnel_number, 200) // calm down badmins, no crashing the server
+
+ var/angle_increment = 360 / shrapnel_number
+ var/mob/living/mob_standing_on_turf
+ var/mob/living/mob_lying_on_turf
+
+ for(var/mob/living/M in epicenter) //find a mob at the epicenter. Non-prone mobs take priority
+ if(!IS_HORIZONTAL(M) && !mob_standing_on_turf)
+ mob_standing_on_turf = M
+ else if(!mob_lying_on_turf)
+ mob_lying_on_turf = M
+
+ for(var/i in 1 to shrapnel_number)
+ var/obj/item/projectile/Shrapnel = new shrapnel_type(epicenter)
+
+ // You can't just stand over a shrapnel explosion to avoid it
+ if(mob_standing_on_turf && prob(chance_to_hit_same_turf))
+ Shrapnel.Bump(mob_standing_on_turf, TRUE)
+ continue
+ // If you dive on it, you're even more likely to get hit
+ if(mob_lying_on_turf && prob(2 * chance_to_hit_same_turf))
+ Shrapnel.Bump(mob_lying_on_turf, TRUE)
+ continue
+
+ var/angle = i * angle_increment + rand(-angle_increment / 2, angle_increment / 2)
+ Shrapnel.fire(angle)
+
#undef CREAK_DELAY
#undef DEVASTATION_PROB
#undef HEAVY_IMPACT_PROB
diff --git a/code/game/objects/items/weapons/grenades/frag.dm b/code/game/objects/items/weapons/grenades/frag.dm
index f264619c335..e93c923df9c 100644
--- a/code/game/objects/items/weapons/grenades/frag.dm
+++ b/code/game/objects/items/weapons/grenades/frag.dm
@@ -1,51 +1,74 @@
+#define DEFAULT_SHRAPNEL_RANGE 5
+
/obj/item/grenade/frag
name = "frag grenade"
desc = "Fire in the hole."
icon_state = "frag"
item_state = "grenade"
origin_tech = "materials=3;magnets=4"
- var/range = 5
- var/max_shrapnel = 4
- var/embed_prob = 100 //reduced by armor
- var/embedded_type = /obj/item/embedded/shrapnel
+ var/shrapnel_contained = 20
+ var/embedded_type = /obj/item/projectile/bullet/shrapnel
/obj/item/grenade/frag/prime()
update_mob()
- var/turf/epicenter = get_turf(src)
- for(var/mob/living/carbon/human/H in epicenter)
- if(IS_HORIZONTAL(H)) //grenade is jumped on but get real fucked up
- embed_shrapnel(H, max_shrapnel)
- range = 1
- explosion(loc, 0, 1, range, breach = FALSE)
- for(var/turf/T in view(range, loc))
- for(var/mob/living/carbon/human/H in T)
- var/shrapnel_amount = max_shrapnel - T.Distance(epicenter)
- if(shrapnel_amount > 0)
- embed_shrapnel(H, shrapnel_amount)
+ explosion(loc, 0, 1, DEFAULT_SHRAPNEL_RANGE, breach = FALSE)
+ create_shrapnel(loc, shrapnel_contained, shrapnel_type = embedded_type)
qdel(src)
-/obj/item/grenade/frag/proc/embed_shrapnel(mob/living/carbon/human/H, amount)
- for(var/i = 0, i < amount, i++)
- if(prob(embed_prob - ARMOUR_VALUE_TO_PERCENTAGE(H.getarmor(null, BOMB))))
- var/obj/item/embedded/S = new embedded_type(src)
- H.hitby(S, skipcatch = 1)
- S.throwforce = 1
- S.throw_speed = 1
- S.sharp = FALSE
- else
- to_chat(H, "Shrapnel bounces off your armor!")
-
-/obj/item/embedded/shrapnel
+/**
+ * Shrapnel that flies through the air and hits you
+ */
+/obj/item/projectile/bullet/shrapnel
name = "shrapnel"
+ icon_state = "magspear"
+ gender = PLURAL
+ range = DEFAULT_SHRAPNEL_RANGE
+ damage = 1 // 1 damage, to trigger stuff that reacts to damage. Rest of the damage is done through the physical shrapnel
+ var/embed_prob = 100 //reduced by armor
+ var/embedded_type = /obj/item/shrapnel
+
+/obj/item/projectile/bullet/shrapnel/on_hit(atom/target, blocked)
+ . = ..()
+ var/obj/item/new_possible_embed = new embedded_type(get_turf(src)) // drop it on the floor if we hit somethig non-living
+ if(!.)
+ return
+ if(!ishuman(target))
+ return
+
+ var/mob/living/carbon/human/H = target
+ if(!prob(embed_prob - ARMOUR_VALUE_TO_PERCENTAGE(H.getarmor(null, BOMB))))
+ to_chat(H, "Shrapnel bounces off your armor!")
+ return
+ H.try_embed_object(new_possible_embed)
+
+/obj/item/projectile/bullet/shrapnel/on_range()
+ var/obj/item/we_missed = new embedded_type(get_turf(src)) // we missed, lets toss the shrapnel
+ var/range = gaussian(4, 2)
+ if(range > 0)
+ var/atom/i_wasnt_aiming_for_the_truck = get_angle_target_turf(get_turf(src), Angle, range)
+ we_missed.throw_at(i_wasnt_aiming_for_the_truck, 16, 3)
+ return ..()
+
+/**
+ * Shrapnel projectiles turn into this after trying to embed
+ */
+/obj/item/shrapnel
+ name = "shrapnel"
+ desc = "Metal shards at high velocity, a classic method of blowing your enemies up."
icon = 'icons/obj/shards.dmi'
- throwforce = 10
- throw_speed = EMBED_THROWSPEED_THRESHOLD
+ icon_state = "shrapnel1"
+ force = 8 // its a sharp piece of metal, but still not very effective
+ gender = PLURAL
embed_chance = 100
embedded_fall_chance = 0
w_class = WEIGHT_CLASS_SMALL
sharp = TRUE
hitsound = 'sound/weapons/pierce.ogg'
-/obj/item/embedded/shrapnel/New()
- ..()
+/obj/item/shrapnel/Initialize(mapload)
+ . = ..()
icon_state = pick("shrapnel1", "shrapnel2", "shrapnel3")
+ pixel_x = rand(-8, 8)
+ pixel_y = rand(-8, 8)
+
+#undef DEFAULT_SHRAPNEL_RANGE
diff --git a/code/game/objects/items/weapons/tanks/tanks.dm b/code/game/objects/items/weapons/tanks/tanks.dm
index 6e11a28b0a6..1e5cdf84f3a 100644
--- a/code/game/objects/items/weapons/tanks/tanks.dm
+++ b/code/game/objects/items/weapons/tanks/tanks.dm
@@ -82,7 +82,7 @@
if(!in_range(src, user))
if(icon == src)
- . += "It's \a [bicon(icon)][src]! If you want any more information you'll need to get closer."
+ . += "It's [p_a()] [bicon(icon)] [name]! If you want any more information you'll need to get closer."
return
var/celsius_temperature = air_contents.temperature - T0C
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index e028d9c57cd..fe394afcaef 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -596,17 +596,23 @@ emp_act
else if(I)
if(((throwingdatum ? throwingdatum.speed : I.throw_speed) >= EMBED_THROWSPEED_THRESHOLD) || I.embedded_ignore_throwspeed_threshold)
- if(can_embed(I))
- if(prob(I.embed_chance) && !HAS_TRAIT(src, TRAIT_PIERCEIMMUNE))
- var/obj/item/organ/external/L = pick(bodyparts)
- L.add_embedded_object(I)
- I.add_mob_blood(src)//it embedded itself in you, of course it's bloody!
- L.receive_damage(I.w_class*I.embedded_impact_pain_multiplier)
- visible_message("[I] embeds itself in [src]'s [L.name]!","[I] embeds itself in your [L.name]!")
- hitpush = FALSE
- skipcatch = TRUE //can't catch the now embedded item
+ if(try_embed_object(I))
+ hitpush = FALSE
+ skipcatch = TRUE //can't catch the now embedded item
return ..()
+/mob/living/carbon/human/proc/try_embed_object(obj/item/I)
+ if(!can_embed(I))
+ return FALSE
+ if(!prob(I.embed_chance) || HAS_TRAIT(src, TRAIT_PIERCEIMMUNE))
+ return FALSE
+ var/obj/item/organ/external/L = pick(bodyparts)
+ L.add_embedded_object(I)
+ I.add_mob_blood(src)//it embedded itself in you, of course it's bloody!
+ L.receive_damage(I.w_class * I.embedded_impact_pain_multiplier)
+ visible_message("[I] embeds itself in [src]'s [L.name]!","[I] embeds itself in your [L.name]!")
+ return TRUE
+
/mob/living/carbon/human/proc/bloody_hands(mob/living/source, amount = 2)
if(gloves)
diff --git a/code/modules/mob/living/carbon/human/human_examine.dm b/code/modules/mob/living/carbon/human/human_examine.dm
index b8ecb090471..ff929ce611b 100644
--- a/code/modules/mob/living/carbon/human/human_examine.dm
+++ b/code/modules/mob/living/carbon/human/human_examine.dm
@@ -140,7 +140,8 @@
msg += "[p_their(TRUE)] [ignore_limb_branding(E.limb_name)] has an open incision!\n"
for(var/obj/item/I in E.embedded_objects)
- msg += "[p_they(TRUE)] [p_have()] \a [bicon(I)] [I] embedded in [p_their()] [E.name]!\n"
+ // we cant just use \a here, as we want it to appear before the bicon
+ msg += "[p_they(TRUE)] [p_have()] [I.p_a()] [bicon(I)] [I.name] embedded in [p_their()] [E.name]!\n"
//Handles the text strings being added to the actual description.
//If they have something that covers the limb, and it is not missing, put flavortext. If it is covered but bleeding, add other flavortext.