Skip to content

Commit

Permalink
[MIRROR] Explosions Part I - Directional Explosions (#1895) (#2807)
Browse files Browse the repository at this point in the history
* Explosions Part I - Directional Explosions (#82429)

## About The Pull Request

Adds the ability for explosions to be directional. This is achieved by
adding an angle check to `prepare_explosion_turfs()` to drop any turfs
outside the cone of the explosion. If the arc covers a full 360 degrees,
as is the default, it will accept all the turfs without performing the
angle check.

Uses this functionality to rework both rocket launcher backblast and X4
explosions. Rocket launcher backblast has been changed from a shotgun of
indendiary bullets to a directional explosion of similar length. X4 now
uses a directional explosion to "ensure user safety".

Apparently the old method of moving the explosion one tile away didn't
even work, as it blew up `target` before trying to check its density for
the directional behaviour.

https://youtu.be/Mzdt7d7Le2Y

## Why It's Good For The Game

Directional explosions - Useful functionality for a range of potential
use cases, which can be implemented with minimal extra processing cost
(Worst case scenario being very large directional explosions)

Backblast - Looks way cooler than a bunch of projectiles, and should be
significantly more functional in high-lag situations where projectile
code tends to get fucky

X4 - More predictable for players wanting to use it as a breaching
charge, you can actually stand near the charge and not have to worry
about being hoist upon your own petard.

## Changelog
:cl:
add: Added support for directional explosions.
add: Rocket launcher backblast is now 271% more explosive, check your
six for friendlies!
add: X4 charges now explode in a cone away from the user when placed on
a sufficiently solid object.
fix: X4 charges will now behave correctly when placed on dense atoms
(note: don't try to read a variable from an atom you just blew up)
/:cl:

* Explosions Part I - Directional Explosions

---------

Co-authored-by: NovaBot <[email protected]>
Co-authored-by: Thunder12345 <[email protected]>
  • Loading branch information
3 people authored Apr 10, 2024
1 parent 6af63a0 commit 1e901e9
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 73 deletions.
6 changes: 6 additions & 0 deletions code/__DEFINES/explosions.dm
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
#define EXARG_KEY_SILENT STRINGIFY(silent)
/// Whether or not the explosion should produce smoke if it is large enough to warrant it.
#define EXARG_KEY_SMOKE STRINGIFY(smoke)
/// Whether or not to leave the epicenter turf unaffected
#define EXARG_KEY_PROTECT_EPICENTER STRINGIFY(protect_epicenter)
/// For directional explosions, the angle the explosion is pointing at.
#define EXARG_KEY_EXPLOSION_DIRECTION STRINGIFY(explosion_direction)
/// For directional explosions, the angle covered by the explosion, centred on EXPLOSION_DIRECTION.
#define EXARG_KEY_EXPLOSION_ARC STRINGIFY(explosion_arc)

// Explodable component deletion values
/// Makes the explodable component queue to reset its exploding status when it detonates.
Expand Down
73 changes: 59 additions & 14 deletions code/controllers/subsystem/explosions.dm
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ SUBSYSTEM_DEF(explosions)
* - flame_range: The range at which the explosion should produce hotspots.
* - silent: Whether to generate/execute sound effects.
* - smoke: Whether to generate a smoke cloud provided the explosion is powerful enough to warrant it.
* - protect_epicenter: Whether to leave the epicenter turf unaffected by the explosion
* - explosion_cause: [Optional] The atom that caused the explosion, when different to the origin. Used for logging.
* - explosion_direction: The angle in which the explosion is pointed (for directional explosions.)
* - explosion_arc: The angle of the arc covered by a directional explosion (if 360 the explosion is non-directional.)
*/
/proc/explosion(atom/origin, devastation_range = 0, heavy_impact_range = 0, light_impact_range = 0, flame_range = null, flash_range = null, adminlog = TRUE, ignorecap = FALSE, silent = FALSE, smoke = FALSE, atom/explosion_cause = null)
/proc/explosion(atom/origin, devastation_range = 0, heavy_impact_range = 0, light_impact_range = 0, flame_range = null, flash_range = null, adminlog = TRUE, ignorecap = FALSE, silent = FALSE, smoke = FALSE, protect_epicenter = FALSE, atom/explosion_cause = null, explosion_direction = 0, explosion_arc = 360)
. = SSexplosions.explode(arglist(args))


Expand All @@ -228,9 +231,12 @@ SUBSYSTEM_DEF(explosions)
* - flame_range: The range at which the explosion should produce hotspots.
* - silent: Whether to generate/execute sound effects.
* - smoke: Whether to generate a smoke cloud provided the explosion is powerful enough to warrant it.
* - protect_epicenter: Whether to leave the epicenter turf unaffected by the explosion
* - explosion_cause: [Optional] The atom that caused the explosion, when different to the origin. Used for logging.
* - explosion_direction: The angle in which the explosion is pointed (for directional explosions.)
* - explosion_arc: The angle of the arc covered by a directional explosion (if 360 the explosion is non-directional.)
*/
/datum/controller/subsystem/explosions/proc/explode(atom/origin, devastation_range = 0, heavy_impact_range = 0, light_impact_range = 0, flame_range = null, flash_range = null, adminlog = TRUE, ignorecap = FALSE, silent = FALSE, smoke = FALSE, atom/explosion_cause = null)
/datum/controller/subsystem/explosions/proc/explode(atom/origin, devastation_range = 0, heavy_impact_range = 0, light_impact_range = 0, flame_range = null, flash_range = null, adminlog = TRUE, ignorecap = FALSE, silent = FALSE, smoke = FALSE, protect_epicenter = FALSE, atom/explosion_cause = null, explosion_direction = 0, explosion_arc = 360)
var/list/arguments = list(
EXARG_KEY_ORIGIN = origin,
EXARG_KEY_DEV_RANGE = devastation_range,
Expand All @@ -242,7 +248,10 @@ SUBSYSTEM_DEF(explosions)
EXARG_KEY_IGNORE_CAP = ignorecap,
EXARG_KEY_SILENT = silent,
EXARG_KEY_SMOKE = smoke,
EXARG_KEY_PROTECT_EPICENTER = protect_epicenter,
EXARG_KEY_EXPLOSION_CAUSE = explosion_cause ? explosion_cause : origin,
EXARG_KEY_EXPLOSION_DIRECTION = explosion_direction,
EXARG_KEY_EXPLOSION_ARC = explosion_arc,
)
var/atom/location = isturf(origin) ? origin : origin.loc
if(SEND_SIGNAL(origin, COMSIG_ATOM_EXPLODE, arguments) & COMSIG_CANCEL_EXPLOSION)
Expand Down Expand Up @@ -270,7 +279,7 @@ SUBSYSTEM_DEF(explosions)
/**
* Handles the effects of an explosion originating from a given point.
*
* Primarily handles popagating the balstwave of the explosion to the relevant turfs.
* Primarily handles popagating the blastwave of the explosion to the relevant turfs.
* Also handles the fireball from the explosion.
* Also handles the smoke cloud from the explosion.
* Also handles sfx and screenshake.
Expand All @@ -286,9 +295,12 @@ SUBSYSTEM_DEF(explosions)
* - flame_range: The range at which the explosion should produce hotspots.
* - silent: Whether to generate/execute sound effects.
* - smoke: Whether to generate a smoke cloud provided the explosion is powerful enough to warrant it.
* - explosion_cause: The atom that caused the explosion. Used for logging.
* - protect_epicenter: Whether to leave the epicenter turf unaffected by the explosion
* - explosion_cause: [Optional] The atom that caused the explosion, when different to the origin. Used for logging.
* - explosion_direction: The angle in which the explosion is pointed (for directional explosions.)
* - explosion_arc: The angle of the arc covered by a directional explosion (if 360 the explosion is non-directional.)
*/
/datum/controller/subsystem/explosions/proc/propagate_blastwave(atom/epicenter, devastation_range, heavy_impact_range, light_impact_range, flame_range, flash_range, adminlog, ignorecap, silent, smoke, atom/explosion_cause)
/datum/controller/subsystem/explosions/proc/propagate_blastwave(atom/epicenter, devastation_range, heavy_impact_range, light_impact_range, flame_range, flash_range, adminlog, ignorecap, silent, smoke, protect_epicenter, atom/explosion_cause, explosion_direction, explosion_arc)
epicenter = get_turf(epicenter)
if(!epicenter)
return
Expand Down Expand Up @@ -387,7 +399,7 @@ SUBSYSTEM_DEF(explosions)
for(var/mob/living/L in viewers(flash_range, epicenter))
L.flash_act()

var/list/affected_turfs = prepare_explosion_turfs(max_range, epicenter)
var/list/affected_turfs = prepare_explosion_turfs(max_range, epicenter, protect_epicenter, explosion_direction, explosion_arc)

var/reactionary = CONFIG_GET(flag/reactionary_explosions)
// this list is setup in the form position -> block for that position
Expand Down Expand Up @@ -415,7 +427,6 @@ SUBSYSTEM_DEF(explosions)
block += our_block
cached_exp_block[explode] = our_block + explode.explosive_resistance


var/severity = EXPLODE_NONE
if(dist + (block * EXPLOSION_BLOCK_DEV) < devastation_range)
severity = EXPLODE_DEVASTATE
Expand Down Expand Up @@ -585,43 +596,77 @@ SUBSYSTEM_DEF(explosions)
/// Returns in a unique order, spiraling outwards
/// This is done to ensure our progressive cache of blast resistance is always valid
/// This is quite fast
/proc/prepare_explosion_turfs(range, turf/epicenter)
/proc/prepare_explosion_turfs(range, turf/epicenter, protect_epicenter, explosion_direction, explosion_arc)
var/list/outlist = list()
// Add in the center
outlist += epicenter
var/list/candidates = list()
// Add in the center if it's not protected
if(!protect_epicenter)
outlist += epicenter

var/our_x = epicenter.x
var/our_y = epicenter.y
var/our_z = epicenter.z

var/max_x = world.maxx
var/max_y = world.maxy

// Work out the angles to explode between
var/first_angle_limit = WRAP(explosion_direction - explosion_arc * 0.5, 0, 360)
var/second_angle_limit = WRAP(explosion_direction + explosion_arc * 0.5, 0, 360)

// Get everything in the right order
var/lower_angle_limit
var/upper_angle_limit
var/do_directional
var/reverse_angle

// Work out which case we're in
if(first_angle_limit == second_angle_limit) // CASE A: FULL CIRCLE
do_directional = FALSE
else if(first_angle_limit < second_angle_limit) // CASE B: When the arc does not cross 0 degrees
lower_angle_limit = first_angle_limit
upper_angle_limit = second_angle_limit
do_directional = TRUE
reverse_angle = FALSE
else if (first_angle_limit > second_angle_limit) // CASE C: When the arc crosses 0 degrees
lower_angle_limit = second_angle_limit
upper_angle_limit = first_angle_limit
do_directional = TRUE
reverse_angle = TRUE

for(var/i in 1 to range)
var/lowest_x = our_x - i
var/lowest_y = our_y - i
var/highest_x = our_x + i
var/highest_y = our_y + i
// top left to one before top right
if(highest_y <= max_y)
outlist += block(
candidates += block(
locate(max(lowest_x, 1), highest_y, our_z),
locate(min(highest_x - 1, max_x), highest_y, our_z))
// top right to one before bottom right
if(highest_x <= max_x)
outlist += block(
candidates += block(
locate(highest_x, min(highest_y, max_y), our_z),
locate(highest_x, max(lowest_y + 1, 1), our_z))
// bottom right to one before bottom left
if(lowest_y >= 1)
outlist += block(
candidates += block(
locate(min(highest_x, max_x), lowest_y, our_z),
locate(max(lowest_x + 1, 1), lowest_y, our_z))
// bottom left to one before top left
if(lowest_x >= 1)
outlist += block(
candidates += block(
locate(lowest_x, max(lowest_y, 1), our_z),
locate(lowest_x, min(highest_y - 1, max_y), our_z))

if(!do_directional)
outlist += candidates
else
for(var/turf/candidate as anything in candidates)
var/angle = get_angle(epicenter, candidate)
if(ISINRANGE(angle, lower_angle_limit, upper_angle_limit) ^ reverse_angle)
outlist += candidate
return outlist

/datum/controller/subsystem/explosions/fire(resumed = 0)
Expand Down
81 changes: 25 additions & 56 deletions code/datums/elements/backblast.dm
Original file line number Diff line number Diff line change
@@ -1,74 +1,43 @@
/**
* When attached to a gun and the gun is successfully fired, this element creates a "backblast" of fire and pain, like you'd find in a rocket launcher or recoilless rifle
* When attached to a gun and the gun is successfully fired, this element creates a "backblast", like you'd find in a rocket launcher or recoilless rifle
*
* The backblast is simulated by a number of fire plumes, or invisible incendiary rounds that will torch anything they come across for a short distance, as well as knocking
* back nearby items.
* The backblast is simulated by a directional explosion 180 degrees from the direction of the fired projectile.
*/
/datum/element/backblast
element_flags = ELEMENT_BESPOKE
argument_hash_start_idx = 2

/// How many "pellets" of backblast we're shooting backwards, spread between the angle defined in angle_spread
var/plumes
/// Assuming we don't just have 1 plume, this is the total angle we'll cover with the plumes, split down the middle directly behind the angle we fired at
var/angle_spread
/// How far each plume of fire will fly, assuming it doesn't hit a mob
var/range

/datum/element/backblast/Attach(datum/target, plumes = 4, angle_spread = 48, range = 6)
/// Devasatation range of the explosion
var/dev_range
/// HGeavy damage range of the explosion
var/heavy_range
/// Light damage range of the explosion
var/light_range
/// Flame range of the explosion
var/flame_range
/// What angle do we want the backblast to cover
var/blast_angle

/datum/element/backblast/Attach(datum/target, dev_range = 0, heavy_range = 0, light_range = 6, flame_range = 6, blast_angle = 60)
. = ..()
if(!isgun(target) || plumes < 1 || angle_spread < 1 || range < 1)
if(!isgun(target) || dev_range < 0 || heavy_range < 0 || light_range < 0 || flame_range < 0 || blast_angle < 1)
return ELEMENT_INCOMPATIBLE

src.plumes = plumes
src.angle_spread = angle_spread
src.range = range
src.dev_range = dev_range
src.heavy_range = heavy_range
src.light_range = light_range
src.flame_range = flame_range
src.blast_angle = blast_angle

if(plumes == 1)
RegisterSignal(target, COMSIG_GUN_FIRED, PROC_REF(gun_fired_simple))
else
RegisterSignal(target, COMSIG_GUN_FIRED, PROC_REF(gun_fired))
RegisterSignal(target, COMSIG_GUN_FIRED, PROC_REF(pew))

/datum/element/backblast/Detach(datum/source)
if(source)
UnregisterSignal(source, COMSIG_GUN_FIRED)
return ..()

/// For firing multiple plumes behind us, we evenly spread out our projectiles based on the [angle_spread][/datum/element/backblast/var/angle_spread] and [number of plumes][/datum/element/backblast/var/plumes]
/datum/element/backblast/proc/gun_fired(obj/item/gun/weapon, mob/living/user, atom/target, params, zone_override)
SIGNAL_HANDLER

if(!weapon.chambered || HAS_TRAIT(user, TRAIT_PACIFISM))
return

var/backwards_angle = get_angle(target, user)
var/starting_angle = SIMPLIFY_DEGREES(backwards_angle-(angle_spread * 0.5))
var/iter_offset = angle_spread / plumes // how much we increment the angle for each plume

for(var/i in 1 to plumes)
var/this_angle = SIMPLIFY_DEGREES(starting_angle + ((i - 1) * iter_offset))
var/turf/target_turf = get_turf_in_angle(this_angle, get_turf(user), 10)
INVOKE_ASYNC(src, PROC_REF(pew), target_turf, weapon, user)

/// If we're only firing one plume directly behind us, we don't need to bother with the loop or angles or anything
/datum/element/backblast/proc/gun_fired_simple(obj/item/gun/weapon, mob/living/user, atom/target, params, zone_override)
SIGNAL_HANDLER

if(!weapon.chambered || HAS_TRAIT(user, TRAIT_PACIFISM))
return

var/backwards_angle = get_angle(target, user)
var/turf/target_turf = get_turf_in_angle(backwards_angle, get_turf(user), 10)
INVOKE_ASYNC(src, PROC_REF(pew), target_turf, weapon, user)

/// For firing an actual backblast pellet
/datum/element/backblast/proc/pew(turf/target_turf, obj/item/gun/weapon, mob/living/user)
//Shooting Code:
var/obj/projectile/bullet/incendiary/fire/backblast/P = new (get_turf(user))
P.original = target_turf
P.range = range
P.fired_from = weapon
P.firer = user // don't hit ourself that would be really annoying
P.impacted = list(user = TRUE) // don't hit the target we hit already with the flak
P.preparePixelProjectile(target_turf, weapon)
P.fire()
/datum/element/backblast/proc/pew(obj/item/gun/weapon, mob/living/user, atom/target)
var/turf/origin = get_turf(weapon)
var/backblast_angle = get_angle(target, origin)
explosion(weapon, devastation_range = dev_range, heavy_impact_range = heavy_range, light_impact_range = light_range, flame_range = flame_range, adminlog = FALSE, protect_epicenter = TRUE, explosion_direction = backblast_angle, explosion_arc = blast_angle)
16 changes: 13 additions & 3 deletions code/game/objects/items/grenades/plastic.dm
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@
display_timer = FALSE
w_class = WEIGHT_CLASS_SMALL
gender = PLURAL
/// What the charge is stuck to
var/atom/target = null
/// C4 overlay to put on target
var/mutable_appearance/plastic_overlay
/// Do we do a directional explosion when target is a a dense atom?
var/directional = FALSE
/// When doing a directional explosion, what arc does the explosion cover
var/directional_arc = 120
/// For directional charges, which cardinal direction is the charge facing?
var/aim_dir = NORTH
/// List of explosion radii (DEV, HEAVY, LIGHT)
var/boom_sizes = list(0, 0, 3)
/// Do we apply the full force of a heavy ex_act() to mob targets
var/full_damage_on_mobs = FALSE
/// Minimum timer for c4 charges
var/minimum_timer = 10
Expand Down Expand Up @@ -68,18 +76,20 @@

. = ..()
var/turf/location
var/target_density
if(target)
if(!QDELETED(target))
location = get_turf(target)
target_density = target.density // We're about to blow target up, so need to save this value for later
target.cut_overlay(plastic_overlay, TRUE)
if(!ismob(target) || full_damage_on_mobs)
EX_ACT(target, EXPLODE_HEAVY, target)
else
location = get_turf(src)
if(location)
if(directional && target?.density)
var/turf/turf = get_step(location, aim_dir)
explosion(get_step(turf, aim_dir), devastation_range = boom_sizes[1], heavy_impact_range = boom_sizes[2], light_impact_range = boom_sizes[3], explosion_cause = src)
if(directional && target_density)
var/angle = dir2angle(aim_dir)
explosion(location, devastation_range = boom_sizes[1], heavy_impact_range = boom_sizes[2], light_impact_range = boom_sizes[3], explosion_cause = src, explosion_direction = angle, explosion_arc = directional_arc)
else
explosion(location, devastation_range = boom_sizes[1], heavy_impact_range = boom_sizes[2], light_impact_range = boom_sizes[3], explosion_cause = src)
qdel(src)
Expand Down

0 comments on commit 1e901e9

Please sign in to comment.