Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MIRROR] Explosions Part I - Directional Explosions #2807

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading