diff --git a/code/__DEFINES/explosions.dm b/code/__DEFINES/explosions.dm index a1645b659d1..dd9a74bbae3 100644 --- a/code/__DEFINES/explosions.dm +++ b/code/__DEFINES/explosions.dm @@ -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. diff --git a/code/controllers/subsystem/explosions.dm b/code/controllers/subsystem/explosions.dm index 74d1c324847..ec6a00badcb 100644 --- a/code/controllers/subsystem/explosions.dm +++ b/code/controllers/subsystem/explosions.dm @@ -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)) @@ -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, @@ -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) @@ -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. @@ -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 @@ -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 @@ -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 @@ -585,10 +596,12 @@ 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 @@ -596,6 +609,31 @@ SUBSYSTEM_DEF(explosions) 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 @@ -603,25 +641,32 @@ SUBSYSTEM_DEF(explosions) 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) diff --git a/code/datums/elements/backblast.dm b/code/datums/elements/backblast.dm index 169f961b3d3..8952afbfeaa 100644 --- a/code/datums/elements/backblast.dm +++ b/code/datums/elements/backblast.dm @@ -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) diff --git a/code/game/objects/items/grenades/plastic.dm b/code/game/objects/items/grenades/plastic.dm index 8e8aeb2617f..c1cf8c1010d 100644 --- a/code/game/objects/items/grenades/plastic.dm +++ b/code/game/objects/items/grenades/plastic.dm @@ -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 @@ -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)