mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-05 22:43:46 +00:00
About The Pull Request
It annoyed me that we have a perfectly good frag grenade item, and a perfectly good shrapnel component, but no crossover episode between the two. This remedies that, and does a lot, lot more.
dreamseeker_2020-03-30_05-01-13.png
dreamseeker_2020-03-30_05-01-26.png
Big points:
Adds new component: pellet_cloud, which can be used by ammo casings, guns, and landmines to spray shrapnel and display aggregate hit messages ("You're hit by 6 buckshot pellets!" vs "You're hit by the buckshot pellet in the X" x6). All gun ammo that shoot multiple pellets now use this component on firing.
Adds stingbangs, premium less-lethal grenades that shoot off lots of stinger pellets, to cargo. Frag grenades are also reworked to have smaller booms, but shoot off lots of shrapnel shards. You can jump on top of these grenades to absorb a portion of the shrapnel to save those around you! There's an achievement for dying this way, called "Look Out, Sir!"
Projectiles can now embed items/shrapnel. Adds .38 DumDum ammo to cargo that does less damage and has negative armor pen, but can embed in people. This is the only ammo that currently embeds.
Bullets can now ricochet off walls, structures, and machinery (harder surfaces are more likely to ricochet). Only standard .38 and Match Grade .38/.357/L6 ammo can ricochet, with Match Grade being much better at ricocheting. You can buy Match Grade .38 from cargo and Match Grade L6 ammo from the nuke uplink, while Match .357 is admin only.
Armor now protects you from harmful embeds, taking the better of the bullet/bomb armor on the affected limb. Armor penetration can modify this of course, and many blunt embeds like stingbangs and DumDum bullets are significantly worse if you have even 1 armor.
Other misc fixes/changes
Refactored the embed element a bunch and fixed it creating new elements for every instance rather than expected bespoke behavior. There are new /obj/item helpers for modifying and adding embedding.
Fixes #49989: Spears can no longer embed in turfs cause their sprite is annoying to me, it's generally harder for most things to embed in turfs
Fixes #49741: New carbon helpers for removing embedded objects
Fixes #46416: Handles embedded objects getting qdel'd or moved while embedded
Renamed the old shrapnel component for RPG loot to MIRV to avoid confusion
Repathed frag grenades from under minibombs to under base grenades, and added explosion vars to base grenades
Why It's Good For The Game
Fixes a bunch of janky design with embeds, adds lots of new avenues for projectile and grenade variety, ricochets and collateral damage are fun!
Changelog
🆑 Ryll/Shaps
add: Adds stingbangs to cargo (and one in the sec vendor premium), premium less-lethal grenades that shoot off a giant swarm of stingball pellets to help incapacitate swarms of people in tight quarters. You can jump on top of a live one to be a hero and absorb a bunch of shrapnel, same with frag grenades. There's even an achievement for dying to a grenade you jumped on!
add: Projectiles can now embed in people! Or at least grenade shrapnel and the new .38 DumDum ammo, now available in cargo, can. DumDum rounds excel against unarmored targets, but are pricey and do poorly against armored targets.
add: Bullets can now ricochet! Or at least, standard .38 and the new .38/L6 Match Grade ammo can. Match Grade ammo is finely tuned to ricochet easier and seek targets off bounces better, and can be purchased from cargo (for the .38) or nuke ops uplink (for the L6), but standard .38 ammo has a chance to ricochet as well.
tweak: Frag grenades now have smaller explosions but shoot off a bunch of devastating shrapnel, excellent for soft targets!
tweak: Shotguns and other multi-pellet guns now print aggregate messages, so you'll get one "You've been hit by 6 buckshot pellets!" rather than 6 "You've been hit by the buckshot pellet in the X!" messages. Bye bye lag!
balance: Armor can now protect against embedding weapons, taking the best of either the bullet or bomb armor for the limb in question away from the embed chance. Some weapons are better at piercing armor than others!
/🆑
237 lines
12 KiB
Plaintext
237 lines
12 KiB
Plaintext
/*
|
|
* This component is used when you want to create a bunch of shrapnel or projectiles (say, shrapnel from a fragmentation grenade, or buckshot from a shotgun) from a central point,
|
|
* without necessarily printing a separate message for every single impact. This component should be instantiated right when you need it (like the moment of firing), then activated
|
|
* by signal.
|
|
*
|
|
* Pellet cloud currently works on two classes of sources: directed (ammo casings), and circular (grenades, landmines).
|
|
* -Directed: This means you're shooting multiple pellets, like buckshot. If an ammo casing is defined as having multiple pellets, it will automatically create a pellet cloud
|
|
* and call COMSIG_PELLET_CLOUD_INIT (see [/obj/item/ammo_casing/proc/fire_casing]). Thus, the only projectiles fired will be the ones fired here.
|
|
* The magnitude var controls how many pellets are created.
|
|
* -Circular: This results in a big spray of shrapnel flying all around the detonation point when the grenade fires COMSIG_GRENADE_PRIME or landmine triggers COMSIG_MINE_TRIGGERED.
|
|
* The magnitude var controls how big the detonation radius is (the bigger the magnitude, the more shrapnel is created). Grenades can be covered with bodies to reduce shrapnel output.
|
|
*
|
|
* Once all of the fired projectiles either hit a target or disappear due to ranging out/whatever else, we resolve the list of all the things we hit and print aggregate messages so we get
|
|
* one "You're hit by 6 buckshot pellets" vs 6x "You're hit by the buckshot blah blah" messages.
|
|
*
|
|
* Note that this is how all guns handle shooting ammo casings with multiple pellets, in case such a thing comes up.
|
|
*/
|
|
|
|
/datum/component/pellet_cloud
|
|
var/projectile_type /// What's the projectile path of the shrapnel we're shooting?
|
|
|
|
var/num_pellets /// How many shrapnel projectiles are we responsible for tracking? May be reduced for grenades if someone dives on top of it. Defined by ammo casing for casings, derived from magnitude otherwise
|
|
var/radius = 4 /// For grenades/landmines, how big is the radius of turfs we're targeting? Note this does not effect the projectiles range, only how many we generate
|
|
|
|
var/list/pellets = list() /// The list of pellets we're responsible for tracking, once these are all accounted for, we finalize.
|
|
var/list/targets_hit = list() /// An associated list with the atom hit as the key and how many pellets they've eaten for the value, for printing aggregate messages
|
|
var/list/bodies /// For grenades, any /mob/living's the grenade is moved onto, see [/datum/component/pellet_cloud/proc/handle_martyrs()]
|
|
var/list/purple_hearts /// For grenades, tracking people who die covering a grenade for achievement purposes, see [/datum/component/pellet_cloud/proc/handle_martyrs()]
|
|
|
|
var/terminated /// how many pellets ranged out without hitting anything
|
|
var/hits /// how many pellets impacted something
|
|
|
|
var/mob/living/shooter /// for if we're an ammo casing being fired
|
|
|
|
/datum/component/pellet_cloud/Initialize(projectile_type=/obj/item/shrapnel, magnitude=5)
|
|
if(!isammocasing(parent) && !isgrenade(parent) && !islandmine(parent))
|
|
return COMPONENT_INCOMPATIBLE
|
|
|
|
if(magnitude < 1)
|
|
stack_trace("Invalid magnitude [magnitude] < 1 on pellet_cloud, parent: [parent]")
|
|
magnitude = 1
|
|
|
|
src.projectile_type = projectile_type
|
|
|
|
if(isammocasing(parent))
|
|
num_pellets = magnitude
|
|
else if(isgrenade(parent) || islandmine(parent))
|
|
radius = magnitude
|
|
|
|
/datum/component/pellet_cloud/Destroy(force, silent)
|
|
purple_hearts = null
|
|
pellets = null
|
|
targets_hit = null
|
|
bodies = null
|
|
return ..()
|
|
|
|
/datum/component/pellet_cloud/RegisterWithParent()
|
|
if(isammocasing(parent))
|
|
RegisterSignal(parent, COMSIG_PELLET_CLOUD_INIT, .proc/create_casing_pellets)
|
|
else if(isgrenade(parent))
|
|
RegisterSignal(parent, COMSIG_GRENADE_ARMED, .proc/grenade_armed)
|
|
RegisterSignal(parent, COMSIG_GRENADE_PRIME, .proc/create_blast_pellets)
|
|
else if(islandmine(parent))
|
|
RegisterSignal(parent, COMSIG_MINE_TRIGGERED, .proc/create_blast_pellets)
|
|
|
|
/datum/component/pellet_cloud/UnregisterFromParent()
|
|
UnregisterSignal(parent, list(COMSIG_PELLET_CLOUD_INIT, COMSIG_GRENADE_PRIME, COMSIG_GRENADE_ARMED, COMSIG_MOVABLE_MOVED, COMSIG_MOVABLE_UNCROSSED, COMSIG_MINE_TRIGGERED, COMSIG_ITEM_DROPPED))
|
|
|
|
/**
|
|
* create_casing_pellets() is for directed pellet clouds for ammo casings that have multiple pellets (buckshot and scatter lasers for instance)
|
|
*
|
|
* Honestly this is mostly just a rehash of [/obj/item/ammo_casing/proc/fire_casing()] for pellet counts > 1, except this lets us tamper with the pellets and hook onto them for tracking purposes.
|
|
* The arguments really don't matter, this proc is triggered by COMSIG_PELLET_CLOUD_INIT which is only for this really, it's just a big mess of the state vars we need for doing the stuff over here.
|
|
*/
|
|
/datum/component/pellet_cloud/proc/create_casing_pellets(obj/item/ammo_casing/A, atom/target, mob/living/user, fired_from, randomspread, spread, zone_override, params, distro)
|
|
shooter = user
|
|
var/targloc = get_turf(target)
|
|
if(!zone_override)
|
|
zone_override = shooter.zone_selected
|
|
|
|
for(var/i in 1 to num_pellets)
|
|
A.ready_proj(target, user, SUPPRESSED_VERY, zone_override, fired_from)
|
|
if(distro)
|
|
if(randomspread)
|
|
spread = round((rand() - 0.5) * distro)
|
|
else //Smart spread
|
|
spread = round((i / num_pellets - 0.5) * distro)
|
|
|
|
RegisterSignal(A.BB, COMSIG_PROJECTILE_SELF_ON_HIT, .proc/pellet_hit)
|
|
RegisterSignal(A.BB, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PARENT_QDELETING), .proc/pellet_range)
|
|
pellets += A.BB
|
|
if(!A.throw_proj(target, targloc, shooter, params, spread))
|
|
return
|
|
if(i != num_pellets)
|
|
A.newshot()
|
|
|
|
/**
|
|
* create_blast_pellets() is for when we have a central point we want to shred the surroundings of with a ring of shrapnel, namely frag grenades and landmines.
|
|
*
|
|
* Note that grenades have extra handling for someone throwing themselves/being thrown on top of it, while landmines do not (obviously, it's a landmine!). See [/datum/component/pellet_cloud/proc/handle_martyrs()]
|
|
*/
|
|
/datum/component/pellet_cloud/proc/create_blast_pellets()
|
|
var/atom/A = parent
|
|
var/total_pellets_absorbed = 0
|
|
|
|
if(isgrenade(parent)) // handle_martyrs can reduce the radius and thus the number of pellets we produce if someone dives on top of a frag grenade
|
|
total_pellets_absorbed = handle_martyrs() // note that we can modify radius in this proc
|
|
|
|
if(radius < 1)
|
|
return
|
|
|
|
var/list/all_the_turfs_were_gonna_lacerate = RANGE_TURFS(radius, A) - RANGE_TURFS(radius-1, A)
|
|
num_pellets = all_the_turfs_were_gonna_lacerate.len + total_pellets_absorbed
|
|
|
|
for(var/T in all_the_turfs_were_gonna_lacerate)
|
|
var/turf/shootat_turf = T
|
|
pew(shootat_turf)
|
|
|
|
/**
|
|
* handle_martyrs() is used for grenades that shoot shrapnel to check if anyone threw themselves/were thrown on top of the grenade, thus absorbing a good chunk of the shrapnel
|
|
*
|
|
* Between the time the grenade is armed and the actual detonation, we set var/list/bodies to the list of mobs currently on the new tile, as if the grenade landed on top of them, tracking if any of them move off the tile and removing them from the "under" list
|
|
* Once the grenade detonates, handle_martyrs() is called and gets all the new mobs on the tile, and add the ones not in var/list/bodies to var/list/martyrs
|
|
* We then iterate through the martyrs and reduce the shrapnel magnitude for each mob on top of it, shredding each of them with some of the shrapnel they helped absorb. This can snuff out all of the shrapnel if there's enough bodies
|
|
*
|
|
* Note we track anyone who's alive and client'd when they get shredded in var/list/purple_hearts, for achievement checking later
|
|
*/
|
|
/datum/component/pellet_cloud/proc/handle_martyrs()
|
|
var/list/martyrs = list()
|
|
for(var/mob/living/body in get_turf(parent))
|
|
if(!(body in bodies))
|
|
martyrs += body // promoted from a corpse to a hero
|
|
|
|
var/magnitude_absorbed
|
|
var/total_pellets_absorbed
|
|
|
|
for(var/M in martyrs)
|
|
var/mob/living/martyr = M
|
|
if(radius > 4)
|
|
martyr.visible_message("<b><span class='danger'>[martyr] heroically covers \the [parent] with [martyr.p_their()] body, absorbing a load of the shrapnel!</span></b>", "<span class='userdanger'>You heroically cover \the [parent] with your body, absorbing a load of the shrapnel!</span>")
|
|
magnitude_absorbed += round(radius * 0.5)
|
|
else if(radius >= 2)
|
|
martyr.visible_message("<b><span class='danger'>[martyr] heroically covers \the [parent] with [martyr.p_their()] body, absorbing some of the shrapnel!</span></b>", "<span class='userdanger'>You heroically cover \the [parent] with your body, absorbing some of the shrapnel!</span>")
|
|
magnitude_absorbed += 2
|
|
else
|
|
martyr.visible_message("<b><span class='danger'>[martyr] heroically covers \the [parent] with [martyr.p_their()] body, snuffing out the shrapnel!</span></b>", "<span class='userdanger'>You heroically cover \the [parent] with your body, snuffing out the shrapnel!</span>")
|
|
magnitude_absorbed = radius
|
|
|
|
var/pellets_absorbed = (radius ** 2) - ((radius - magnitude_absorbed - 1) ** 2)
|
|
radius -= magnitude_absorbed
|
|
total_pellets_absorbed += round(pellets_absorbed/2)
|
|
|
|
if(martyr.stat != DEAD && martyr.client)
|
|
LAZYADD(purple_hearts, martyr)
|
|
|
|
for(var/i in 1 to round(pellets_absorbed/2))
|
|
pew(martyr)
|
|
|
|
if(radius < 1)
|
|
break
|
|
|
|
return total_pellets_absorbed
|
|
|
|
///One of our pellets hit something, record what it was and check if we're done (terminated == num_pellets)
|
|
/datum/component/pellet_cloud/proc/pellet_hit(obj/projectile/P, atom/movable/firer, atom/target, Angle)
|
|
pellets -= P
|
|
terminated++
|
|
hits++
|
|
targets_hit[target]++
|
|
UnregisterSignal(P, list(COMSIG_PARENT_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
|
|
if(terminated == num_pellets)
|
|
finalize()
|
|
|
|
///One of our pellets disappeared due to hitting their max range (or just somehow got qdel'd), remove it from our list and check if we're done (terminated == num_pellets)
|
|
/datum/component/pellet_cloud/proc/pellet_range(obj/projectile/P)
|
|
pellets -= P
|
|
terminated++
|
|
UnregisterSignal(P, list(COMSIG_PARENT_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
|
|
if(terminated == num_pellets)
|
|
finalize()
|
|
|
|
/// Minor convenience function for creating each shrapnel piece with circle explosions, mostly stolen from the MIRV component
|
|
/datum/component/pellet_cloud/proc/pew(atom/target, spread=0)
|
|
var/obj/projectile/P = new projectile_type(get_turf(parent))
|
|
|
|
//Shooting Code:
|
|
P.spread = spread
|
|
P.original = target
|
|
P.fired_from = parent
|
|
P.firer = parent // don't hit ourself that would be really annoying
|
|
P.permutated += parent // don't hit the target we hit already with the flak
|
|
P.suppressed = SUPPRESSED_VERY // set the projectiles to make no message so we can do our own aggregate message
|
|
P.preparePixelProjectile(target, parent)
|
|
RegisterSignal(P, COMSIG_PROJECTILE_SELF_ON_HIT, .proc/pellet_hit)
|
|
RegisterSignal(P, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PARENT_QDELETING), .proc/pellet_range)
|
|
pellets += P
|
|
P.fire()
|
|
|
|
///All of our pellets are accounted for, time to go target by target and tell them how many things they got hit by.
|
|
/datum/component/pellet_cloud/proc/finalize()
|
|
var/obj/projectile/P = projectile_type
|
|
var/proj_name = initial(P.name)
|
|
|
|
for(var/atom/target in targets_hit)
|
|
var/num_hits = targets_hit[target]
|
|
if(num_hits > 1)
|
|
target.visible_message("<span class='danger'>[target] is hit by [num_hits] [proj_name]s!</span>", null, null, COMBAT_MESSAGE_RANGE, target)
|
|
to_chat(target, "<span class='userdanger'>You're hit by [num_hits] [proj_name]s!</span>")
|
|
else
|
|
target.visible_message("<span class='danger'>[target] is hit by a [proj_name]!</span>", null, null, COMBAT_MESSAGE_RANGE, target)
|
|
to_chat(target, "<span class='userdanger'>You're hit by [num_hits] [proj_name]s!</span>")
|
|
|
|
for(var/M in purple_hearts)
|
|
var/mob/living/martyr = M
|
|
if(martyr.stat == DEAD && martyr.client)
|
|
martyr.client.give_award(/datum/award/achievement/misc/lookoutsir, martyr)
|
|
|
|
qdel(parent)
|
|
qdel(src)
|
|
|
|
/// Look alive, we're armed! Now we start watching to see if anyone's covering us
|
|
/datum/component/pellet_cloud/proc/grenade_armed()
|
|
LAZYINITLIST(bodies)
|
|
RegisterSignal(parent, list(COMSIG_ITEM_DROPPED, COMSIG_MOVABLE_MOVED), .proc/grenade_moved)
|
|
RegisterSignal(parent, COMSIG_MOVABLE_UNCROSSED, .proc/grenade_uncrossed)
|
|
|
|
/// Our grenade has moved, reset var/list/bodies so we're "on top" of any mobs currently on the tile
|
|
/datum/component/pellet_cloud/proc/grenade_moved()
|
|
LAZYCLEARLIST(bodies)
|
|
for(var/mob/living/L in get_turf(parent))
|
|
bodies += L
|
|
|
|
/// Someone who was originally "under" the grenade has moved off the tile and is now eligible for being a martyr and "covering" it
|
|
/datum/component/pellet_cloud/proc/grenade_uncrossed(datum/source, atom/movable/AM)
|
|
bodies -= AM
|
|
|