Makes smoke and foam attempt to fill the available space. (#65281)

Have you ever noticed that the chemical smoke and chemical foam reactions are a lot less effective in confined spaces? This is because they currently attempt to spread to all tiles within n steps of their origin. If they can't expand onto a tile they get blocked and the expanding cloud/flood misses out on all the tiles that would be in range, but that can't be reached.

Obviously smoke and foam getting blocked by walls and the like makes intuitive sense, but it seemed a bit nonsensical that walls would basically delete a significant chunk of an expanding, amoebic mass. The solution I came up with is making smoke and foam expand until they cover a certain area, with a shared tracker for the target size and total size of the flood. The flood will simply expand as normal until it covers the desired target area. Blocked expansions just don't count and will be made up for with expansion elsewhere.

Attendant to these changes are a whole bunch of minor code improvement to smoke, foam, and one for wizard spells because I was already in the area and :pain:.

There have been some minor balance changes to the chemical smoke and foam reactions:

I converted them over to passing the desired area of the resulting smoke cloud/foam flood. The old equation for the resulting area was along the lines of 2sqrt(x)(sqrt(x) + 1) + 1 given reaction volume x and given unobstructed expansion. I've made them just pass around 2x instead. This is actually less than they used to try for, but now they're guaranteed to reach that unless the flood is fully contained. Not entirely certain if buff or nerf. Probably buff on the station.
Also, foam dilution is now based on covered area instead of target expansion range. Since this scales faster than it used to foam has been effectively nerfed at high volumes. To compensate for this I removed the jank 6/7 effect multiplier and increased the base reagent scaling a bit. Again, not certain if buff or nerf.
This commit is contained in:
TemporalOroboros
2022-05-07 13:10:37 -07:00
committed by GitHub
parent 63b0fad930
commit 068a3be859
75 changed files with 1483 additions and 981 deletions
@@ -57,8 +57,8 @@
/datum/effect_system/explosion/smoke
/datum/effect_system/explosion/smoke/proc/create_smoke()
var/datum/effect_system/smoke_spread/S = new
S.set_up(2, location)
var/datum/effect_system/fluid_spread/smoke/S = new
S.set_up(2, location = location)
S.start()
/datum/effect_system/explosion/smoke/start()
@@ -1,386 +0,0 @@
// Foam
// Similar to smoke, but slower and mobs absorb its reagent through their exposed skin.
#define ALUMINUM_FOAM 1
#define IRON_FOAM 2
#define RESIN_FOAM 3
/obj/effect/particle_effect/foam
name = "foam"
icon_state = "foam"
opacity = FALSE
anchored = TRUE
density = FALSE
layer = EDGED_TURF_LAYER
plane = GAME_PLANE_UPPER
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
var/amount = 3
animate_movement = NO_STEPS
var/metal = 0
var/lifetime = 40
var/reagent_divisor = 7
var/static/list/blacklisted_turfs = typecacheof(list(
/turf/open/space/transit,
/turf/open/chasm,
/turf/open/lava,
))
var/slippery_foam = TRUE
/obj/effect/particle_effect/foam/firefighting
name = "firefighting foam"
lifetime = 20 //doesn't last as long as normal foam
amount = 0 //no spread
slippery_foam = FALSE
var/absorbed_plasma = 0
/obj/effect/particle_effect/foam/firefighting/Initialize(mapload)
. = ..()
RemoveElement(/datum/element/atmos_sensitive)
/obj/effect/particle_effect/foam/firefighting/process()
..()
var/turf/open/T = get_turf(src)
var/obj/effect/hotspot/hotspot = (locate(/obj/effect/hotspot) in T)
if(hotspot && istype(T) && T.air)
qdel(hotspot)
var/datum/gas_mixture/G = T.air
if(G.gases[/datum/gas/plasma])
var/plas_amt = min(30,G.gases[/datum/gas/plasma][MOLES]) //Absorb some plasma
G.gases[/datum/gas/plasma][MOLES] -= plas_amt
absorbed_plasma += plas_amt
if(G.temperature > T20C)
G.temperature = max(G.temperature/2,T20C)
G.garbage_collect()
T.air_update_turf(FALSE, FALSE)
/obj/effect/particle_effect/foam/firefighting/kill_foam()
STOP_PROCESSING(SSfastprocess, src)
if(absorbed_plasma)
var/obj/effect/decal/cleanable/plasma/P = (locate(/obj/effect/decal/cleanable/plasma) in get_turf(src))
if(!P)
P = new(loc)
P.reagents.add_reagent(/datum/reagent/stable_plasma, absorbed_plasma)
flick("[icon_state]-disolve", src)
QDEL_IN(src, 5)
/obj/effect/particle_effect/foam/firefighting/foam_mob(mob/living/L)
if(!istype(L))
return
L.adjust_wet_stacks(2)
/obj/effect/particle_effect/foam/metal
name = "aluminium foam"
metal = ALUMINUM_FOAM
icon_state = "mfoam"
slippery_foam = FALSE
/obj/effect/particle_effect/foam/metal/smart
name = "smart foam"
/obj/effect/particle_effect/foam/metal/iron
name = "iron foam"
metal = IRON_FOAM
/obj/effect/particle_effect/foam/metal/resin
name = "resin foam"
metal = RESIN_FOAM
/obj/effect/particle_effect/foam/long_life
lifetime = 150
/obj/effect/particle_effect/foam/Initialize(mapload)
. = ..()
create_reagents(1000) //limited by the size of the reagent holder anyway.
START_PROCESSING(SSfastprocess, src)
playsound(src, 'sound/effects/bubbles2.ogg', 80, TRUE, -3)
AddElement(/datum/element/atmos_sensitive, mapload)
/obj/effect/particle_effect/foam/ComponentInitialize()
. = ..()
if(slippery_foam)
AddComponent(/datum/component/slippery, 100)
/obj/effect/particle_effect/foam/Destroy()
STOP_PROCESSING(SSfastprocess, src)
return ..()
/obj/effect/particle_effect/foam/proc/kill_foam()
STOP_PROCESSING(SSfastprocess, src)
switch(metal)
if(ALUMINUM_FOAM)
new /obj/structure/foamedmetal(get_turf(src))
if(IRON_FOAM)
new /obj/structure/foamedmetal/iron(get_turf(src))
if(RESIN_FOAM)
new /obj/structure/foamedmetal/resin(get_turf(src))
flick("[icon_state]-disolve", src)
QDEL_IN(src, 5)
/obj/effect/particle_effect/foam/smart/kill_foam() //Smart foam adheres to area borders for walls
STOP_PROCESSING(SSfastprocess, src)
if(metal)
var/turf/T = get_turf(src)
if(isspaceturf(T)) //Block up any exposed space
T.PlaceOnTop(/turf/open/floor/plating/foam, flags = CHANGETURF_INHERIT_AIR)
for(var/direction in GLOB.cardinals)
var/turf/cardinal_turf = get_step(T, direction)
if(get_area(cardinal_turf) != get_area(T)) //We're at an area boundary, so let's block off this turf!
new/obj/structure/foamedmetal(T)
break
flick("[icon_state]-disolve", src)
QDEL_IN(src, 5)
/obj/effect/particle_effect/foam/process()
lifetime--
if(lifetime < 1)
kill_foam()
return
var/fraction = 1/initial(reagent_divisor)
for(var/obj/O in range(0,src))
if(O.type == src.type)
continue
if(isturf(O.loc))
var/turf/T = O.loc
if(T.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(O, TRAIT_T_RAY_VISIBLE))
continue
if(lifetime % reagent_divisor)
reagents.expose(O, VAPOR, fraction)
var/hit = 0
for(var/mob/living/L in range(0,src))
hit += foam_mob(L)
if(hit)
lifetime++ //this is so the decrease from mobs hit and the natural decrease don't cumulate.
var/T = get_turf(src)
if(lifetime % reagent_divisor)
reagents.expose(T, VAPOR, fraction)
if(--amount < 0)
return
spread_foam()
/obj/effect/particle_effect/foam/proc/foam_mob(mob/living/L)
if(lifetime<1)
return FALSE
if(!istype(L))
return FALSE
var/fraction = 1/initial(reagent_divisor)
if(lifetime % reagent_divisor)
reagents.expose(L, VAPOR, fraction)
lifetime--
return TRUE
/obj/effect/particle_effect/foam/proc/spread_foam()
var/turf/t_loc = get_turf(src)
//This should just be atmos adjacent turfs, come on guys
for(var/turf/T in t_loc.reachableAdjacentTurfs())
var/obj/effect/particle_effect/foam/foundfoam = locate() in T //Don't spread foam where there's already foam!
if(foundfoam)
continue
if(is_type_in_typecache(T, blacklisted_turfs))
continue
for(var/mob/living/L in T)
foam_mob(L)
var/obj/effect/particle_effect/foam/F = new src.type(T)
F.amount = amount
reagents.copy_to(F, (reagents.total_volume))
F.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
F.metal = metal
/obj/effect/particle_effect/foam/should_atmos_process(datum/gas_mixture/air, exposed_temperature)
return exposed_temperature > 475
/obj/effect/particle_effect/foam/atmos_expose(datum/gas_mixture/air, exposed_temperature)
if(prob(max(0, exposed_temperature - 475))) //foam dissolves when heated
kill_foam()
///////////////////////////////////////////////
//FOAM EFFECT DATUM
/datum/effect_system/foam_spread
/// the size of the foam spread.
var/amount = 10
/// Stupid hack alertm exists to hold chems for us
var/atom/movable/chem_holder/chemholder
effect_type = /obj/effect/particle_effect/foam
var/metal = 0
/datum/effect_system/foam_spread/metal
effect_type = /obj/effect/particle_effect/foam/metal
/datum/effect_system/foam_spread/metal/smart
effect_type = /obj/effect/particle_effect/foam/smart
/datum/effect_system/foam_spread/long
effect_type = /obj/effect/particle_effect/foam/long_life
/datum/effect_system/foam_spread/New()
..()
chemholder = new()
chemholder.create_reagents(1000, NO_REACT)
/datum/effect_system/foam_spread/Destroy()
QDEL_NULL(chemholder)
return ..()
/datum/effect_system/foam_spread/set_up(amt=5, loca, datum/reagents/carry = null, metaltype = 0)
if(isturf(loca))
location = loca
else
location = get_turf(loca)
amount = round(sqrt(amt / 2), 1)
carry.copy_to(chemholder, carry.total_volume)
if(metaltype)
metal = metaltype
/datum/effect_system/foam_spread/start()
var/obj/effect/particle_effect/foam/F = new effect_type(location)
var/foamcolor = mix_color_from_reagents(chemholder.reagents.reagent_list)
// To prevent insane reagent multiplication with 1u foam
// I am capping amount of reagent foam recieves by limiting how low it can go
// Any radius of foam less than 3 makes foam recieve same amount of reagents as foam of radius 3
// Maximum multiplication of reagents is about 166% (3 times as low as before, it was about 500% with 1u foam)
//
// amount is radius of the foam
// 10u foam has radius of 3
// 5u foam has radius of 2
// 1u foam has radius of 1
var/effective_amount = chemholder.reagents.total_volume / max(amount, 3)
chemholder.reagents.copy_to(F, effective_amount)
F.add_atom_colour(foamcolor, FIXED_COLOUR_PRIORITY)
F.amount = amount
F.metal = metal
//////////////////////////////////////////////////////////
// FOAM STRUCTURE. Formed by metal foams. Dense and opaque, but easy to break
/obj/structure/foamedmetal
icon = 'icons/effects/effects.dmi'
icon_state = "metalfoam"
density = TRUE
opacity = TRUE // changed in New()
anchored = TRUE
layer = EDGED_TURF_LAYER
plane = GAME_PLANE_UPPER
resistance_flags = FIRE_PROOF | ACID_PROOF
name = "foamed metal"
desc = "A lightweight foamed metal wall that can be used as base to construct a wall."
gender = PLURAL
max_integrity = 20
can_atmos_pass = ATMOS_PASS_DENSITY
obj_flags = CAN_BE_HIT | BLOCK_Z_IN_DOWN | BLOCK_Z_IN_UP
///Var used to prevent spamming of the construction sound
var/next_beep = 0
/obj/structure/foamedmetal/Initialize(mapload)
. = ..()
air_update_turf(TRUE, TRUE)
/obj/structure/foamedmetal/Destroy()
air_update_turf(TRUE, FALSE)
. = ..()
/obj/structure/foamedmetal/Move()
var/turf/T = loc
. = ..()
move_update_air(T)
/obj/structure/foamedmetal/attack_paw(mob/user, list/modifiers)
return attack_hand(user, modifiers)
/obj/structure/foamedmetal/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
/obj/structure/foamedmetal/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
user.changeNext_move(CLICK_CD_MELEE)
user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
to_chat(user, span_warning("You hit [src] but bounce off it!"))
playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
/obj/structure/foamedmetal/attackby(obj/item/W, mob/user, params)
///A speed modifier for how fast the wall is build
var/platingmodifier = 1
if(HAS_TRAIT(user, TRAIT_QUICK_BUILD))
platingmodifier = 0.7
if(next_beep <= world.time)
next_beep = world.time + 1 SECONDS
playsound(src, 'sound/machines/clockcult/integration_cog_install.ogg', 50, TRUE)
add_fingerprint(user)
if(!istype(W, /obj/item/stack/sheet))
return ..()
var/obj/item/stack/sheet/sheet_for_plating = W
if(istype(sheet_for_plating, /obj/item/stack/sheet/iron))
if(sheet_for_plating.get_amount() < 2)
to_chat(user, span_warning("You need two sheets of iron to finish a wall on [src]!"))
return
to_chat(user, span_notice("You start adding plating to the foam structure..."))
if (do_after(user, 40*platingmodifier, target = src))
if(!sheet_for_plating.use(2))
return
to_chat(user, span_notice("You add the plating."))
var/turf/T = get_turf(src)
T.PlaceOnTop(/turf/closed/wall/metal_foam_base)
transfer_fingerprints_to(T)
qdel(src)
return
add_hiddenprint(user)
/obj/structure/foamedmetal/iron
max_integrity = 50
icon_state = "ironfoam"
//Atmos Backpack Resin, transparent, prevents atmos and filters the air
/obj/structure/foamedmetal/resin
name = "\improper ATMOS Resin"
desc = "A lightweight, transparent resin used to suffocate fires, scrub the air of toxins, and restore the air to a safe temperature. It can be used as base to construct a wall."
opacity = FALSE
icon_state = "atmos_resin"
alpha = 120
max_integrity = 10
pass_flags_self = PASSGLASS
/obj/structure/foamedmetal/resin/Initialize(mapload)
. = ..()
if(isopenturf(loc))
var/turf/open/O = loc
O.ClearWet()
if(O.air)
var/datum/gas_mixture/G = O.air
G.temperature = 293.15
for(var/obj/effect/hotspot/H in O)
qdel(H)
var/list/G_gases = G.gases
for(var/I in G_gases)
if(I == /datum/gas/oxygen || I == /datum/gas/nitrogen)
continue
G_gases[I][MOLES] = 0
G.garbage_collect()
for(var/obj/machinery/atmospherics/components/unary/U in O)
if(!U.welded)
U.welded = TRUE
U.update_appearance()
U.visible_message(span_danger("[U] sealed shut!"))
for(var/mob/living/L in O)
L.extinguish_mob()
for(var/obj/item/Item in O)
Item.extinguish()
#undef ALUMINUM_FOAM
#undef IRON_FOAM
#undef RESIN_FOAM
@@ -1,381 +0,0 @@
/////////////////////////////////////////////
//// SMOKE SYSTEMS
/////////////////////////////////////////////
/obj/effect/particle_effect/smoke
name = "smoke"
icon = 'icons/effects/96x96.dmi'
icon_state = "smoke"
pixel_x = -32
pixel_y = -32
opacity = FALSE
plane = ABOVE_GAME_PLANE
layer = FLY_LAYER
anchored = TRUE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
animate_movement = FALSE
var/amount = 4
var/lifetime = 5
var/opaque = 1 //whether the smoke can block the view when in enough amount
/obj/effect/particle_effect/smoke/proc/fade_out(frames = 16)
if(alpha == 0) //Handle already transparent case
return
if(frames == 0)
frames = 1 //We will just assume that by 0 frames, the coder meant "during one frame".
var/step = alpha / frames
for(var/i in 1 to frames)
alpha -= step
if(alpha < 160)
set_opacity(0) //if we were blocking view, we aren't now because we're fading out
stoplag()
/obj/effect/particle_effect/smoke/Initialize(mapload)
. = ..()
create_reagents(1000)
START_PROCESSING(SSobj, src)
/obj/effect/particle_effect/smoke/Destroy()
STOP_PROCESSING(SSobj, src)
return ..()
/obj/effect/particle_effect/smoke/proc/kill_smoke()
STOP_PROCESSING(SSobj, src)
INVOKE_ASYNC(src, .proc/fade_out)
QDEL_IN(src, 10)
/obj/effect/particle_effect/smoke/process()
lifetime--
if(lifetime < 1)
kill_smoke()
return FALSE
for(var/mob/living/L in range(0,src))
smoke_mob(L)
return TRUE
/obj/effect/particle_effect/smoke/proc/smoke_mob(mob/living/carbon/C)
if(!istype(C))
return FALSE
if(lifetime<1)
return FALSE
if(C.internal != null || C.has_smoke_protection())
return FALSE
if(C.smoke_delay)
return FALSE
C.smoke_delay++
addtimer(CALLBACK(src, .proc/remove_smoke_delay, C), 10)
return TRUE
/obj/effect/particle_effect/smoke/proc/remove_smoke_delay(mob/living/carbon/C)
if(C)
C.smoke_delay = 0
/obj/effect/particle_effect/smoke/proc/spread_smoke()
var/turf/t_loc = get_turf(src)
if(!t_loc)
return
var/list/newsmokes = list()
for(var/turf/T in t_loc.get_atmos_adjacent_turfs())
var/obj/effect/particle_effect/smoke/foundsmoke = locate() in T //Don't spread smoke where there's already smoke!
if(foundsmoke)
continue
for(var/mob/living/L in T)
smoke_mob(L)
var/obj/effect/particle_effect/smoke/S = new type(T)
reagents.copy_to(S, reagents.total_volume)
S.setDir(pick(GLOB.cardinals))
S.amount = amount-1
S.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
S.lifetime = lifetime
if(S.amount>0)
if(opaque)
S.set_opacity(TRUE)
newsmokes.Add(S)
//the smoke spreads rapidly but not instantly
for(var/obj/effect/particle_effect/smoke/SM in newsmokes)
addtimer(CALLBACK(SM, /obj/effect/particle_effect/smoke.proc/spread_smoke), 1)
/datum/effect_system/smoke_spread
var/amount = 10
effect_type = /obj/effect/particle_effect/smoke
/datum/effect_system/smoke_spread/set_up(radius = 5, loca)
if(isturf(loca))
location = loca
else
location = get_turf(loca)
amount = radius
/datum/effect_system/smoke_spread/start()
if(holder)
location = get_turf(holder)
var/obj/effect/particle_effect/smoke/S = new effect_type(location)
S.amount = amount
if(S.amount)
S.spread_smoke()
/////////////////////////////////////////////
// Bad smoke
/////////////////////////////////////////////
/obj/effect/particle_effect/smoke/bad
lifetime = 8
/obj/effect/particle_effect/smoke/bad/Initialize(mapload)
. = ..()
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
AddElement(/datum/element/connect_loc, loc_connections)
/obj/effect/particle_effect/smoke/bad/smoke_mob(mob/living/carbon/M)
. = ..()
if(.)
M.drop_all_held_items()
M.adjustOxyLoss(1)
M.emote("cough")
return TRUE
/obj/effect/particle_effect/smoke/bad/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
if(istype(arrived, /obj/projectile/beam))
var/obj/projectile/beam/beam = arrived
beam.damage *= 0.5
/datum/effect_system/smoke_spread/bad
effect_type = /obj/effect/particle_effect/smoke/bad
/////////////////////////////////////////////
// Nanofrost smoke
/////////////////////////////////////////////
/obj/effect/particle_effect/smoke/freezing
name = "nanofrost smoke"
color = "#B2FFFF"
opaque = FALSE
/datum/effect_system/smoke_spread/freezing
effect_type = /obj/effect/particle_effect/smoke/freezing
var/blast = 0
var/temperature = 2
var/weldvents = TRUE
var/distcheck = TRUE
/datum/effect_system/smoke_spread/freezing/proc/Chilled(atom/A)
if(isopenturf(A))
var/turf/open/T = A
if(T.air)
var/datum/gas_mixture/G = T.air
if(!distcheck || get_dist(T, location) < blast) // Otherwise we'll get silliness like people using Nanofrost to kill people through walls with cold air
G.temperature = temperature
T.air_update_turf(FALSE, FALSE)
for(var/obj/effect/hotspot/H in T)
qdel(H)
var/list/G_gases = G.gases
if(G_gases[/datum/gas/plasma])
G.assert_gas(/datum/gas/nitrogen)
G_gases[/datum/gas/nitrogen][MOLES] += (G_gases[/datum/gas/plasma][MOLES])
G_gases[/datum/gas/plasma][MOLES] = 0
G.garbage_collect()
if (weldvents)
for(var/obj/machinery/atmospherics/components/unary/U in T)
if(!isnull(U.welded) && !U.welded) //must be an unwelded vent pump or vent scrubber.
U.welded = TRUE
U.update_appearance()
U.visible_message(span_danger("[U] is frozen shut!"))
for(var/mob/living/L in T)
L.extinguish_mob()
for(var/obj/item/Item in T)
Item.extinguish()
/datum/effect_system/smoke_spread/freezing/set_up(radius = 5, loca, blast_radius = 0)
..()
blast = blast_radius
/datum/effect_system/smoke_spread/freezing/start()
if(blast)
for(var/turf/T in RANGE_TURFS(blast, location))
Chilled(T)
..()
/datum/effect_system/smoke_spread/freezing/decon
temperature = 293.15
distcheck = FALSE
weldvents = FALSE
/////////////////////////////////////////////
// Sleep smoke
/////////////////////////////////////////////
/obj/effect/particle_effect/smoke/sleeping
color = "#9C3636"
lifetime = 10
/obj/effect/particle_effect/smoke/sleeping/smoke_mob(mob/living/carbon/M)
if(..())
M.Sleeping(200)
M.emote("cough")
return 1
/datum/effect_system/smoke_spread/sleeping
effect_type = /obj/effect/particle_effect/smoke/sleeping
/////////////////////////////////////////////
// Chem smoke
/////////////////////////////////////////////
/obj/effect/particle_effect/smoke/chem
lifetime = 10
/obj/effect/particle_effect/smoke/chem/process()
. = ..()
if(.)
var/turf/T = get_turf(src)
var/fraction = 1/initial(lifetime)
for(var/atom/movable/AM in T)
if(AM.type == src.type)
continue
if(T.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(AM, TRAIT_T_RAY_VISIBLE))
continue
reagents.expose(AM, TOUCH, fraction)
reagents.expose(T, TOUCH, fraction)
return TRUE
/obj/effect/particle_effect/smoke/chem/smoke_mob(mob/living/carbon/M)
if(lifetime<1)
return FALSE
if(!istype(M))
return FALSE
var/mob/living/carbon/C = M
if(C.internal != null || C.has_smoke_protection())
return FALSE
var/fraction = 1/initial(lifetime)
reagents.copy_to(C, fraction*reagents.total_volume)
reagents.expose(M, INGEST, fraction)
return TRUE
/datum/effect_system/smoke_spread/chem
/// Evil evil hack so we have something to "hold" our reagents
var/atom/movable/chem_holder/chemholder
effect_type = /obj/effect/particle_effect/smoke/chem
/datum/effect_system/smoke_spread/chem/New()
..()
chemholder = new()
chemholder.create_reagents(1000, NO_REACT)
/datum/effect_system/smoke_spread/chem/Destroy()
QDEL_NULL(chemholder)
return ..()
/datum/effect_system/smoke_spread/chem/set_up(datum/reagents/carry = null, radius = 1, loca, silent = FALSE)
if(isturf(loca))
location = loca
else
location = get_turf(loca)
amount = radius
carry.copy_to(chemholder, carry.total_volume)
if(!silent)
var/contained = ""
for(var/reagent in carry.reagent_list)
contained += " [reagent] "
if(contained)
contained = "\[[contained]\]"
var/where = "[AREACOORD(location)]"
if(carry.my_atom?.fingerprintslast) //Some reagents don't have a my_atom in some cases
var/mob/M = get_mob_by_key(carry.my_atom.fingerprintslast)
var/more = ""
if(M)
more = "[ADMIN_LOOKUPFLW(M)] "
if(!istype(carry.my_atom, /obj/machinery/plumbing))
message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. Key: [more ? more : carry.my_atom.fingerprintslast].")
log_game("A chemical smoke reaction has taken place in ([where])[contained]. Last touched by [carry.my_atom.fingerprintslast].")
else
if(!istype(carry.my_atom, /obj/machinery/plumbing))
message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. No associated key.")
log_game("A chemical smoke reaction has taken place in ([where])[contained]. No associated key.")
/datum/effect_system/smoke_spread/chem/start()
var/mixcolor = mix_color_from_reagents(chemholder.reagents.reagent_list)
if(holder)
location = get_turf(holder)
var/obj/effect/particle_effect/smoke/chem/S = new effect_type(location)
if(chemholder.reagents.total_volume > 1) // can't split 1 very well
chemholder.reagents.copy_to(S, chemholder.reagents.total_volume)
if(mixcolor)
S.add_atom_colour(mixcolor, FIXED_COLOUR_PRIORITY) // give the smoke color, if it has any to begin with
S.amount = amount
if(S.amount)
S.spread_smoke() //calling process right now so the smoke immediately attacks mobs.
/////////////////////////////////////////////
// Transparent smoke
/////////////////////////////////////////////
//Same as the base type, but the smoke produced is not opaque
/datum/effect_system/smoke_spread/transparent
effect_type = /obj/effect/particle_effect/smoke/transparent
/obj/effect/particle_effect/smoke/transparent
opaque = FALSE
/proc/do_smoke(range=0, location=null, smoke_type=/obj/effect/particle_effect/smoke)
var/datum/effect_system/smoke_spread/smoke = new
smoke.effect_type = smoke_type
smoke.set_up(range, location)
smoke.start()
/////////////////////////////////////////////
// Bad Smoke (But Green (and Black))
/////////////////////////////////////////////
/obj/effect/particle_effect/smoke/bad/green
name = "green smoke"
color = "#00FF00"
opaque = FALSE
/datum/effect_system/smoke_spread/bad/green
effect_type = /obj/effect/particle_effect/smoke/bad/green
/obj/effect/particle_effect/smoke/bad/black
name = "black smoke"
color = "#383838"
opaque = FALSE
/datum/effect_system/smoke_spread/bad/black
effect_type = /obj/effect/particle_effect/smoke/bad/black
/////////////////////////////////////////////
// Quick smoke
/////////////////////////////////////////////
/obj/effect/particle_effect/smoke/quick
lifetime = 1
opaque = FALSE
/datum/effect_system/smoke_spread/quick
effect_type = /obj/effect/particle_effect/smoke/quick
/obj/effect/particle_effect/smoke/chem/quick
lifetime = 2 //under lifetime 1, this kills itself the first time it processes, not working. i hate smoke code
opaque = FALSE
alpha = 100
/datum/effect_system/smoke_spread/chem/quick
effect_type = /obj/effect/particle_effect/smoke/chem/quick
@@ -0,0 +1,126 @@
/////////////////////////////////////////////
//// SMOKE SYSTEMS
/////////////////////////////////////////////
/**
* A group of fluid objects.
*/
/datum/fluid_group
/// The set of fluid objects currently in this group.
var/list/nodes
/// The number of fluid object that this group wants to have contained.
var/target_size
/// The total number of fluid objects that have ever been in this group.
var/total_size = 0
/datum/fluid_group/New(target_size = 0)
. = ..()
src.nodes = list()
src.target_size = target_size
/datum/fluid_group/Destroy(force)
QDEL_LAZYLIST(nodes)
return ..()
/**
* Adds a fluid node to this fluid group.
*
* Is a noop if the node is already in the group.
* Removes the node from any other fluid groups it is in.
* Syncs the group of the node with the group it is being added to (this one).
* Increments the total size of the fluid group.
*
* Arguments:
* - [node][/obj/effect/particle_effect/fluid]: The fluid node that is going to be added to this group.
*
* Returns:
* - [TRUE]: If the node to be added is in this group by the end of the proc.
* - [FALSE]: Otherwise.
*/
/datum/fluid_group/proc/add_node(obj/effect/particle_effect/fluid/node)
if(!istype(node))
CRASH("Attempted to add non-fluid node [isnull(node) ? "NULL" : node] to a fluid group.")
if(QDELING(node))
CRASH("Attempted to add qdeling node to a fluid group")
if(node.group)
if(node.group == src)
return TRUE
if(!node.group.remove_node(node))
return FALSE
nodes += node
node.group = src
total_size++
return TRUE
/**
* Removes a fluid node from this fluid group.
*
* Is a noop if the node is not in this group.
* Nulls the nodes fluid group ref to sync it with its new state.
* DOES NOT decrement the total size of the fluid group.
*
* Arguments:
* - [node][/obj/effect/particle_effect/fluid]: The fluid node that is going to be removed from this group.
*
* Returns:
* - [TRUE]: If the node to be removed is not in the group by the end of the proc.
*/
/datum/fluid_group/proc/remove_node(obj/effect/particle_effect/fluid/node)
if(node.group != src)
return TRUE
nodes -= node
node.group = null
return TRUE // Note: does not decrement total size since we don't want the group to expand again when it begins to dissipate or it will never stop.
/**
* A particle effect that belongs to a fluid group.
*/
/obj/effect/particle_effect/fluid
name = "fluid"
/// The fluid group that this particle effect belongs to.
var/datum/fluid_group/group
/// What SSfluid bucket this particle effect is currently in.
var/tmp/effect_bucket
/// The index of the fluid spread bucket this is being spread in.
var/tmp/spread_bucket
/obj/effect/particle_effect/fluid/Initialize(mapload, datum/fluid_group/group, obj/effect/particle_effect/fluid/source)
. = ..()
if(!group)
group = source?.group || new
group.add_node(src)
/obj/effect/particle_effect/fluid/Destroy()
group.remove_node(src)
return ..()
/**
* Attempts to spread this fluid node to wherever it can spread.
*
* Exact results vary by subtype implementation.
*/
/obj/effect/particle_effect/fluid/proc/spread()
CRASH("The base fluid spread proc is not implemented and should not be called. You called it.")
/**
* A factory which produces fluid groups.
*/
/datum/effect_system/fluid_spread
effect_type = /obj/effect/particle_effect/fluid
/// The amount of smoke to produce.
var/amount = 10
/datum/effect_system/fluid_spread/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location, ...)
src.location = get_turf(location)
src.amount = amount
/datum/effect_system/fluid_spread/start()
var/location = holder ? get_turf(holder) : src.location
var/obj/effect/particle_effect/fluid/flood = new effect_type(location, new /datum/fluid_group(amount))
flood.spread()
@@ -0,0 +1,429 @@
/// The minimum foam range required to start diluting the reagents past the minimum dilution rate.
#define MINIMUM_FOAM_DILUTION_RANGE 3
/// The minumum foam-area based divisor used to decrease foam exposure volume.
#define MINIMUM_FOAM_DILUTION DIAMOND_AREA(MINIMUM_FOAM_DILUTION_RANGE)
/// The effective scaling of the reagents in the foam. (Total delivered at or below [MINIMUM_FOAM_DILUTION])
#define FOAM_REAGENT_SCALE 3.2
/**
* ## Foam
*
* Similar to smoke, but slower and mobs absorb its reagent through their exposed skin.
*/
/obj/effect/particle_effect/fluid/foam
name = "foam"
icon_state = "foam"
opacity = FALSE
anchored = TRUE
density = FALSE
layer = EDGED_TURF_LAYER
plane = GAME_PLANE_UPPER
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
animate_movement = NO_STEPS
/// The types of turfs that this foam cannot spread to.
var/static/list/blacklisted_turfs = typecacheof(list(
/turf/open/space/transit,
/turf/open/chasm,
/turf/open/lava,
))
/// The typepath for what this foam leaves behind when it dissipates.
var/atom/movable/result_type = null
/// Whether or not this foam can produce a remnant movable if something of the same type is already on its turf.
var/allow_duplicate_results = TRUE
/// The amount of time this foam stick around for before it dissipates.
var/lifetime = 8 SECONDS
/// Whether or not this foam should be slippery.
var/slippery_foam = TRUE
/obj/effect/particle_effect/fluid/foam/Initialize(mapload)
. = ..()
create_reagents(1000)
playsound(src, 'sound/effects/bubbles2.ogg', 80, TRUE, -3)
AddElement(/datum/element/atmos_sensitive, mapload)
SSfoam.start_processing(src)
/obj/effect/particle_effect/fluid/foam/ComponentInitialize()
. = ..()
if(slippery_foam)
AddComponent(/datum/component/slippery, 100)
/obj/effect/particle_effect/fluid/foam/Destroy()
SSfoam.stop_processing(src)
if (spread_bucket)
SSfoam.cancel_spread(src)
return ..()
/**
* Makes the foam dissipate and create whatever remnants it must.
*/
/obj/effect/particle_effect/fluid/foam/proc/kill_foam()
SSfoam.stop_processing(src)
if (spread_bucket)
SSfoam.cancel_spread(src)
make_result()
flick("[icon_state]-disolve", src)
QDEL_IN(src, 0.5 SECONDS)
/**
* Makes the foam leave behind something when it dissipates.
*
* Returns the thing the foam leaves behind for further modification by subtypes.
*/
/obj/effect/particle_effect/fluid/foam/proc/make_result()
if(isnull(result_type))
return null
var/atom/location = loc
return (!allow_duplicate_results && (locate(result_type) in location)) || (new result_type(location))
/obj/effect/particle_effect/fluid/foam/process(delta_time)
var/ds_delta_time = delta_time SECONDS
lifetime -= ds_delta_time
if(lifetime <= 0)
kill_foam()
return
var/fraction = (ds_delta_time * MINIMUM_FOAM_DILUTION) / (initial(lifetime) * max(MINIMUM_FOAM_DILUTION, group.total_size))
var/turf/location = loc
for(var/obj/object in location)
if(object == src)
continue
if(location.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(object, TRAIT_T_RAY_VISIBLE))
continue
reagents.expose(object, VAPOR, fraction)
var/hit = 0
for(var/mob/living/foamer in location)
hit += foam_mob(foamer, delta_time)
if(hit)
lifetime += ds_delta_time //this is so the decrease from mobs hit and the natural decrease don't cumulate.
reagents.expose(location, VAPOR, fraction)
/**
* Applies the effect of this foam to a mob.
*
* Arguments:
* - [foaming][/mob/living]: The mob that this foam is acting on.
* - delta_time: The amount of time that this foam is acting on them over.
*
* Returns:
* - [TRUE]: If the foam was successfully applied to the mob. Used to scale how quickly foam dissipates according to the number of mobs it is applied to.
* - [FALSE]: Otherwise.
*/
/obj/effect/particle_effect/fluid/foam/proc/foam_mob(mob/living/foaming, delta_time)
if(lifetime <= 0)
return FALSE
if(!istype(foaming))
return FALSE
delta_time = min(delta_time SECONDS, lifetime)
var/fraction = (delta_time * MINIMUM_FOAM_DILUTION) / (initial(lifetime) * max(MINIMUM_FOAM_DILUTION, group.total_size))
reagents.expose(foaming, VAPOR, fraction)
lifetime -= delta_time
return TRUE
/obj/effect/particle_effect/fluid/foam/spread(delta_time = 0.2 SECONDS)
if(group.total_size > group.target_size)
return
var/turf/location = get_turf(src)
if(!istype(location))
return FALSE
for(var/turf/spread_turf as anything in location.reachableAdjacentTurfs())
var/obj/effect/particle_effect/fluid/foam/foundfoam = locate() in spread_turf //Don't spread foam where there's already foam!
if(foundfoam)
continue
if(is_type_in_typecache(spread_turf, blacklisted_turfs))
continue
for(var/mob/living/foaming in spread_turf)
foam_mob(foaming, delta_time)
var/obj/effect/particle_effect/fluid/foam/spread_foam = new type(spread_turf, group, src)
reagents.copy_to(spread_foam, (reagents.total_volume))
spread_foam.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
spread_foam.result_type = result_type
SSfoam.queue_spread(spread_foam)
/obj/effect/particle_effect/fluid/foam/should_atmos_process(datum/gas_mixture/air, exposed_temperature)
return exposed_temperature > 475
/obj/effect/particle_effect/fluid/foam/atmos_expose(datum/gas_mixture/air, exposed_temperature)
if(prob(max(0, exposed_temperature - 475))) //foam dissolves when heated
kill_foam()
/// A factory for foam fluid floods.
/datum/effect_system/fluid_spread/foam
effect_type = /obj/effect/particle_effect/fluid/foam
/// A container for all of the chemicals we distribute through the foam.
var/datum/reagents/chemholder
/// The amount that
var/reagent_scale = FOAM_REAGENT_SCALE
/// What type of thing the foam should leave behind when it dissipates.
var/atom/movable/result_type = null
/datum/effect_system/fluid_spread/foam/New()
..()
chemholder = new(1000, NO_REACT)
/datum/effect_system/fluid_spread/foam/Destroy()
QDEL_NULL(chemholder)
return ..()
/datum/effect_system/fluid_spread/foam/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location = null, datum/reagents/carry = null, result_type = null)
. = ..()
carry?.copy_to(chemholder, carry.total_volume)
if(!isnull(result_type))
src.result_type = result_type
/datum/effect_system/fluid_spread/foam/start()
var/obj/effect/particle_effect/fluid/foam/foam = new effect_type(location, new /datum/fluid_group(amount))
var/foamcolor = mix_color_from_reagents(chemholder.reagent_list)
if(reagent_scale > 1) // Make room in case we were created by a particularly stuffed payload.
foam.reagents.maximum_volume *= reagent_scale
chemholder.copy_to(foam, chemholder.total_volume, reagent_scale) // Foam has an amplifying effect on the reagents it is supplied with. This is balanced by the reagents being diluted as the area the foam covers increases.
foam.add_atom_colour(foamcolor, FIXED_COLOUR_PRIORITY)
if(!isnull(result_type))
foam.result_type = result_type
SSfoam.queue_spread(foam)
// Long lasting foam
/// A foam variant which lasts for an extended amount of time.
/obj/effect/particle_effect/fluid/foam/long_life
lifetime = 30 SECONDS
/// A factory which produces foam with an extended lifespan.
/datum/effect_system/fluid_spread/foam/long
effect_type = /obj/effect/particle_effect/fluid/foam/long_life
reagent_scale = FOAM_REAGENT_SCALE * (30 / 8)
// Firefighting foam
/// A variant of foam which absorbs plasma in the air if there is a fire.
/obj/effect/particle_effect/fluid/foam/firefighting
name = "firefighting foam"
lifetime = 20 //doesn't last as long as normal foam
result_type = /obj/effect/decal/cleanable/plasma
allow_duplicate_results = FALSE
slippery_foam = FALSE
/// The amount of plasma gas this foam has absorbed. To be deposited when the foam dissipates.
var/absorbed_plasma = 0
/obj/effect/particle_effect/fluid/foam/firefighting/Initialize(mapload)
. = ..()
RemoveElement(/datum/element/atmos_sensitive)
/obj/effect/particle_effect/fluid/foam/firefighting/process()
..()
var/turf/open/location = loc
if(!istype(location))
return
var/obj/effect/hotspot/hotspot = locate() in location
if(!(hotspot && location.air))
return
QDEL_NULL(hotspot)
var/datum/gas_mixture/air = location.air
var/list/gases = air.gases
if (gases[/datum/gas/plasma])
var/scrub_amt = min(30, gases[/datum/gas/plasma][MOLES]) //Absorb some plasma
gases[/datum/gas/plasma][MOLES] -= scrub_amt
absorbed_plasma += scrub_amt
if (air.temperature > T20C)
air.temperature = max(air.temperature / 2, T20C)
air.garbage_collect()
location.air_update_turf(FALSE, FALSE)
/obj/effect/particle_effect/fluid/foam/firefighting/make_result()
var/atom/movable/deposit = ..()
if(istype(deposit) && deposit.reagents && absorbed_plasma > 0)
deposit.reagents.add_reagent(/datum/reagent/stable_plasma, absorbed_plasma)
absorbed_plasma = 0
return deposit
/obj/effect/particle_effect/fluid/foam/firefighting/foam_mob(mob/living/foaming, delta_time)
if(!istype(foaming))
return
foaming.adjust_wet_stacks(2)
// Metal foam
/// A foam variant which
/obj/effect/particle_effect/fluid/foam/metal
name = "aluminium foam"
result_type = /obj/structure/foamedmetal
icon_state = "mfoam"
slippery_foam = FALSE
/// A factory which produces aluminium metal foam.
/datum/effect_system/fluid_spread/foam/metal
effect_type = /obj/effect/particle_effect/fluid/foam/metal
/// FOAM STRUCTURE. Formed by metal foams. Dense and opaque, but easy to break
/obj/structure/foamedmetal
icon = 'icons/effects/effects.dmi'
icon_state = "metalfoam"
density = TRUE
opacity = TRUE // changed in New()
anchored = TRUE
layer = EDGED_TURF_LAYER
plane = GAME_PLANE_UPPER
resistance_flags = FIRE_PROOF | ACID_PROOF
name = "foamed metal"
desc = "A lightweight foamed metal wall that can be used as base to construct a wall."
gender = PLURAL
max_integrity = 20
can_atmos_pass = ATMOS_PASS_DENSITY
obj_flags = CAN_BE_HIT | BLOCK_Z_IN_DOWN | BLOCK_Z_IN_UP
///Var used to prevent spamming of the construction sound
var/next_beep = 0
/obj/structure/foamedmetal/Initialize(mapload)
. = ..()
air_update_turf(TRUE, TRUE)
/obj/structure/foamedmetal/Destroy()
air_update_turf(TRUE, FALSE)
. = ..()
/obj/structure/foamedmetal/Move()
var/turf/T = loc
. = ..()
move_update_air(T)
/obj/structure/foamedmetal/attack_paw(mob/user, list/modifiers)
return attack_hand(user, modifiers)
/obj/structure/foamedmetal/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
/obj/structure/foamedmetal/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
user.changeNext_move(CLICK_CD_MELEE)
user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
to_chat(user, span_warning("You hit [src] but bounce off it!"))
playsound(src.loc, 'sound/weapons/tap.ogg', 100, TRUE)
/obj/structure/foamedmetal/attackby(obj/item/W, mob/user, params)
///A speed modifier for how fast the wall is build
var/platingmodifier = 1
if(HAS_TRAIT(user, TRAIT_QUICK_BUILD))
platingmodifier = 0.7
if(next_beep <= world.time)
next_beep = world.time + 1 SECONDS
playsound(src, 'sound/machines/clockcult/integration_cog_install.ogg', 50, TRUE)
add_fingerprint(user)
if(!istype(W, /obj/item/stack/sheet))
return ..()
var/obj/item/stack/sheet/sheet_for_plating = W
if(istype(sheet_for_plating, /obj/item/stack/sheet/iron))
if(sheet_for_plating.get_amount() < 2)
to_chat(user, span_warning("You need two sheets of iron to finish a wall on [src]!"))
return
to_chat(user, span_notice("You start adding plating to the foam structure..."))
if (do_after(user, 40 * platingmodifier, target = src))
if(!sheet_for_plating.use(2))
return
to_chat(user, span_notice("You add the plating."))
var/turf/T = get_turf(src)
T.PlaceOnTop(/turf/closed/wall/metal_foam_base)
transfer_fingerprints_to(T)
qdel(src)
return
add_hiddenprint(user)
/// A metal foam variant which produces slightly sturdier walls.
/obj/effect/particle_effect/fluid/foam/metal/iron
name = "iron foam"
result_type = /obj/structure/foamedmetal/iron
/// A factory which produces iron metal foam.
/datum/effect_system/fluid_spread/foam/metal/iron
effect_type = /obj/effect/particle_effect/fluid/foam/metal/iron
/// A variant of metal foam walls with higher durability.
/obj/structure/foamedmetal/iron
max_integrity = 50
icon_state = "ironfoam"
/// A variant of metal foam which only produces walls at area boundaries.
/obj/effect/particle_effect/fluid/foam/metal/smart
name = "smart foam"
/// A factory which produces smart aluminium metal foam.
/datum/effect_system/fluid_spread/foam/metal/smart
effect_type = /obj/effect/particle_effect/fluid/foam/metal/smart
/obj/effect/particle_effect/fluid/foam/metal/smart/make_result() //Smart foam adheres to area borders for walls
var/turf/open/location = loc
if(isspaceturf(location))
location.PlaceOnTop(/turf/open/floor/plating/foam)
for(var/cardinal in GLOB.cardinals)
var/turf/cardinal_turf = get_step(location, cardinal)
if(get_area(cardinal_turf) != get_area(location))
return ..()
return null
/datum/effect_system/fluid_spread/foam/metal/resin
effect_type = /obj/effect/particle_effect/fluid/foam/metal/resin
/// A foam variant which produces atmos resin walls.
/obj/effect/particle_effect/fluid/foam/metal/resin
name = "resin foam"
result_type = /obj/structure/foamedmetal/resin
/// Atmos Backpack Resin, transparent, prevents atmos and filters the air
/obj/structure/foamedmetal/resin
name = "\improper ATMOS Resin"
desc = "A lightweight, transparent resin used to suffocate fires, scrub the air of toxins, and restore the air to a safe temperature. It can be used as base to construct a wall."
opacity = FALSE
icon_state = "atmos_resin"
alpha = 120
max_integrity = 10
pass_flags_self = PASSGLASS
/obj/structure/foamedmetal/resin/Initialize(mapload)
. = ..()
var/turf/open/location = loc
if(!istype(location))
return
location.ClearWet()
if(location.air)
var/datum/gas_mixture/air = location.air
air.temperature = 293.15
for(var/obj/effect/hotspot/fire in location)
qdel(fire)
var/list/gases = air.gases
for(var/gas_type in gases)
switch(gas_type)
if(/datum/gas/oxygen, /datum/gas/nitrogen)
continue
else
gases[gas_type][MOLES] = 0
air.garbage_collect()
for(var/obj/machinery/atmospherics/components/unary/comp in location)
if(!comp.welded)
comp.welded = TRUE
comp.update_appearance()
comp.visible_message(span_danger("[comp] sealed shut!"))
for(var/mob/living/potential_tinder in location)
potential_tinder.extinguish_mob()
for(var/obj/item/potential_tinder in location)
potential_tinder.extinguish()
@@ -0,0 +1,438 @@
/**
* A fluid which spreads through the air affecting every mob it engulfs.
*/
/obj/effect/particle_effect/fluid/smoke
name = "smoke"
icon = 'icons/effects/96x96.dmi'
icon_state = "smoke"
pixel_x = -32
pixel_y = -32
opacity = TRUE
plane = ABOVE_GAME_PLANE
layer = FLY_LAYER
anchored = TRUE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
animate_movement = FALSE
/// How long the smoke sticks around before it dissipates.
var/lifetime = 10 SECONDS
/obj/effect/particle_effect/fluid/smoke/Initialize(mapload, datum/fluid_group/group, ...)
. = ..()
create_reagents(1000)
setDir(pick(GLOB.cardinals))
SSsmoke.start_processing(src)
/obj/effect/particle_effect/fluid/smoke/Destroy()
SSsmoke.stop_processing(src)
if (spread_bucket)
SSsmoke.cancel_spread(src)
return ..()
/**
* Makes the smoke fade out and then deletes it.
*/
/obj/effect/particle_effect/fluid/smoke/proc/kill_smoke()
SSsmoke.stop_processing(src)
if (spread_bucket)
SSsmoke.cancel_spread(src)
INVOKE_ASYNC(src, .proc/fade_out)
QDEL_IN(src, 1 SECONDS)
/**
* Animates the smoke gradually fading out of visibility.
* Also makes the smoke turf transparent as it passes 160 alpha.
*
* Arguments:
* - frames = 0.8 [SECONDS]: The amount of time the smoke should fade out over.
*/
/obj/effect/particle_effect/fluid/smoke/proc/fade_out(frames = 0.8 SECONDS)
if(alpha == 0) //Handle already transparent case
if(opacity)
set_opacity(FALSE)
return
if(frames == 0)
set_opacity(FALSE)
alpha = 0
return
var/time_to_transparency = round(((alpha - 160) / alpha) * frames)
if(time_to_transparency >= 1)
addtimer(CALLBACK(src, /atom.proc/set_opacity, FALSE), time_to_transparency)
else
set_opacity(FALSE)
animate(src, time = frames, alpha = 0)
/obj/effect/particle_effect/fluid/smoke/spread(delta_time = 0.1 SECONDS)
if(group.total_size > group.target_size)
return
var/turf/t_loc = get_turf(src)
if(!t_loc)
return
for(var/turf/spread_turf in t_loc.get_atmos_adjacent_turfs())
if(group.total_size > group.target_size)
break
if(locate(type) in spread_turf)
continue // Don't spread smoke where there's already smoke!
for(var/mob/living/smoker in spread_turf)
smoke_mob(smoker, delta_time)
var/obj/effect/particle_effect/fluid/smoke/spread_smoke = new type(spread_turf, group)
reagents.copy_to(spread_smoke, reagents.total_volume)
spread_smoke.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
spread_smoke.lifetime = lifetime
// the smoke spreads rapidly, but not instantly
SSfoam.queue_spread(spread_smoke)
/obj/effect/particle_effect/fluid/smoke/process(delta_time)
lifetime -= delta_time SECONDS
if(lifetime <= 0)
kill_smoke()
return FALSE
for(var/mob/living/smoker in loc) // In case smoke somehow winds up in a locker or something this should still behave sanely.
smoke_mob(smoker, delta_time)
return TRUE
/**
* Handles the effects of this smoke on any mobs it comes into contact with.
*
* Arguments:
* - [smoker][/mob/living/carbon]: The mob that is being exposed to this smoke.
* - delta_time: A scaling factor for the effects this has. Primarily based off of tick rate to normalize effects to units of rate/sec.
*
* Returns whether the smoke effect was applied to the mob.
*/
/obj/effect/particle_effect/fluid/smoke/proc/smoke_mob(mob/living/carbon/smoker, delta_time)
if(!istype(smoker))
return FALSE
if(lifetime < 1)
return FALSE
if(smoker.internal != null || smoker.has_smoke_protection())
return FALSE
if(smoker.smoke_delay)
return FALSE
smoker.smoke_delay = TRUE
addtimer(VARSET_CALLBACK(smoker, smoke_delay, FALSE), 1 SECONDS)
return TRUE
/// A factory which produces clouds of smoke.
/datum/effect_system/fluid_spread/smoke
effect_type = /obj/effect/particle_effect/fluid/smoke
/////////////////////////////////////////////
// Transparent smoke
/////////////////////////////////////////////
/// Same as the base type, but the smoke produced is not opaque
/datum/effect_system/fluid_spread/smoke/transparent
effect_type = /obj/effect/particle_effect/fluid/smoke/transparent
/// Same as the base type, but is not opaque.
/obj/effect/particle_effect/fluid/smoke/transparent
opacity = FALSE
/**
* A helper proc used to spawn small puffs of smoke.
*
* Arguments:
* - range: The amount of smoke to produce as number of steps from origin covered.
* - amount: The amount of smoke to produce as the total desired coverage area. Autofilled from the range arg if not set.
* - location: Where to produce the smoke cloud.
* - smoke_type: The smoke typepath to spawn.
*/
/proc/do_smoke(range = 0, amount = DIAMOND_AREA(range), location = null, smoke_type = /obj/effect/particle_effect/fluid/smoke)
var/datum/effect_system/fluid_spread/smoke/smoke = new
smoke.effect_type = smoke_type
smoke.set_up(amount = amount, location = location)
smoke.start()
/////////////////////////////////////////////
// Quick smoke
/////////////////////////////////////////////
/// Smoke that dissipates as quickly as possible.
/obj/effect/particle_effect/fluid/smoke/quick
lifetime = 1 SECONDS
opacity = FALSE
/// A factory which produces smoke that dissipates as quickly as possible.
/datum/effect_system/fluid_spread/smoke/quick
effect_type = /obj/effect/particle_effect/fluid/smoke/quick
/////////////////////////////////////////////
// Bad smoke
/////////////////////////////////////////////
/// Smoke that makes you cough and reduces the power of lasers.
/obj/effect/particle_effect/fluid/smoke/bad
lifetime = 16 SECONDS
/obj/effect/particle_effect/fluid/smoke/bad/Initialize(mapload)
. = ..()
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
AddElement(/datum/element/connect_loc, loc_connections)
/obj/effect/particle_effect/fluid/smoke/bad/smoke_mob(mob/living/carbon/smoker)
. = ..()
if(!.)
return
smoker.drop_all_held_items()
smoker.adjustOxyLoss(1)
smoker.emote("cough")
/**
* Reduces the power of any beam projectile that passes through the smoke.
*
* Arguments:
* - [source][/datum]: The location that has just been entered. If [/datum/element/connect_loc] is working this is [src.loc][/atom/var/loc].
* - [arrived][/atom/movable]: The atom that has just entered the source location.
* - [old_loc][/atom]: The location the entering atom just was in.
* - [old_locs][/list/atom]: The set of locations the entering atom was just in.
*/
/obj/effect/particle_effect/fluid/smoke/bad/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
if(istype(arrived, /obj/projectile/beam))
var/obj/projectile/beam/beam = arrived
beam.damage *= 0.5
/// A factory which produces smoke that makes you cough.
/datum/effect_system/fluid_spread/smoke/bad
effect_type = /obj/effect/particle_effect/fluid/smoke/bad
/////////////////////////////////////////////
// Bad Smoke (But Green (and Black))
/////////////////////////////////////////////
/// Green smoke that makes you cough.
/obj/effect/particle_effect/fluid/smoke/bad/green
name = "green smoke"
color = "#00FF00"
opacity = FALSE
/// A factory which produces green smoke that makes you cough.
/datum/effect_system/fluid_spread/smoke/bad/green
effect_type = /obj/effect/particle_effect/fluid/smoke/bad/green
/// Black smoke that makes you cough. (Actually dark grey)
/obj/effect/particle_effect/fluid/smoke/bad/black
name = "black smoke"
color = "#383838"
opacity = FALSE
/// A factory which produces black smoke that makes you cough.
/datum/effect_system/fluid_spread/smoke/bad/black
effect_type = /obj/effect/particle_effect/fluid/smoke/bad/black
/////////////////////////////////////////////
// Nanofrost smoke
/////////////////////////////////////////////
/// Light blue, transparent smoke which is usually paired with a blast that chills every turf in the area.
/obj/effect/particle_effect/fluid/smoke/freezing
name = "nanofrost smoke"
color = "#B2FFFF"
opacity = FALSE
/// A factory which produces light blue, transparent smoke and a blast that chills every turf in the area.
/datum/effect_system/fluid_spread/smoke/freezing
effect_type = /obj/effect/particle_effect/fluid/smoke/freezing
/// The radius in which to chill every open turf.
var/blast = 0
/// The temperature to set the turfs air temperature to.
var/temperature = 2
/// Whether to weld every vent and air scrubber in the affected area shut.
var/weldvents = TRUE
/// Whether to make sure each affected turf is actually within range before cooling it.
var/distcheck = TRUE
/**
* Chills an open turf.
*
* Forces the air temperature to a specific value.
* Transmutes all of the plasma in the air into nitrogen.
* Extinguishes all fires and burning objects/mobs in the turf.
* May freeze all vents and vent scrubbers shut.
*
* Arguments:
* - [chilly][/turf/open]: The open turf to chill
*/
/datum/effect_system/fluid_spread/smoke/freezing/proc/Chilled(turf/open/chilly)
if(!istype(chilly))
return
if(chilly.air)
var/datum/gas_mixture/air = chilly.air
if(!distcheck || get_dist(location, chilly) < blast) // Otherwise we'll get silliness like people using Nanofrost to kill people through walls with cold air
air.temperature = temperature
var/list/gases = air.gases
if(gases[/datum/gas/plasma])
air.assert_gas(/datum/gas/nitrogen)
gases[/datum/gas/nitrogen][MOLES] += gases[/datum/gas/plasma][MOLES]
gases[/datum/gas/plasma][MOLES] = 0
air.garbage_collect()
for(var/obj/effect/hotspot/fire in chilly)
qdel(fire)
chilly.air_update_turf(FALSE, FALSE)
if(weldvents)
for(var/obj/machinery/atmospherics/components/unary/comp in chilly)
if(!isnull(comp.welded) && !comp.welded) //must be an unwelded vent pump or vent scrubber.
comp.welded = TRUE
comp.update_appearance()
comp.visible_message(span_danger("[comp] is frozen shut!"))
// Extinguishes everything in the turf
for(var/mob/living/potential_tinder in chilly)
potential_tinder.extinguish_mob()
for(var/obj/item/potential_tinder in chilly)
potential_tinder.extinguish()
/datum/effect_system/fluid_spread/smoke/freezing/set_up(range = 5, amount = DIAMOND_AREA(range), atom/location, blast_radius = 0)
. = ..()
blast = blast_radius
/datum/effect_system/fluid_spread/smoke/freezing/start()
if(blast)
for(var/turf/T in RANGE_TURFS(blast, location))
Chilled(T)
return ..()
/// A variant of the base freezing smoke formerly used by the vent decontamination event.
/datum/effect_system/fluid_spread/smoke/freezing/decon
temperature = 293.15
distcheck = FALSE
weldvents = FALSE
/////////////////////////////////////////////
// Sleep smoke
/////////////////////////////////////////////
/// Smoke which knocks you out if you breathe it in.
/obj/effect/particle_effect/fluid/smoke/sleeping
color = "#9C3636"
lifetime = 20 SECONDS
/obj/effect/particle_effect/fluid/smoke/sleeping/smoke_mob(mob/living/carbon/smoker, delta_time)
if(..())
smoker.Sleeping(20 SECONDS)
smoker.emote("cough")
return TRUE
/// A factory which produces sleeping smoke.
/datum/effect_system/fluid_spread/smoke/sleeping
effect_type = /obj/effect/particle_effect/fluid/smoke/sleeping
/////////////////////////////////////////////
// Chem smoke
/////////////////////////////////////////////
/**
* Smoke which contains reagents which it applies to everything it comes into contact with.
*/
/obj/effect/particle_effect/fluid/smoke/chem
lifetime = 20 SECONDS
/obj/effect/particle_effect/fluid/smoke/chem/process(delta_time)
. = ..()
if(!.)
return
var/turf/location = get_turf(src)
var/fraction = (delta_time SECONDS) / initial(lifetime)
for(var/atom/movable/thing as anything in location)
if(thing == src)
continue
if(location.underfloor_accessibility < UNDERFLOOR_INTERACTABLE && HAS_TRAIT(thing, TRAIT_T_RAY_VISIBLE))
continue
reagents.expose(thing, TOUCH, fraction)
reagents.expose(location, TOUCH, fraction)
return TRUE
/obj/effect/particle_effect/fluid/smoke/chem/smoke_mob(mob/living/carbon/smoker, delta_time)
if(lifetime < 1)
return FALSE
if(!istype(smoker))
return FALSE
if(smoker.internal != null || smoker.has_smoke_protection())
return FALSE
var/fraction = (delta_time SECONDS) / initial(lifetime)
reagents.copy_to(smoker, reagents.total_volume, fraction)
reagents.expose(smoker, INGEST, fraction)
return TRUE
/// A factory which produces clouds of chemical bearing smoke.
/datum/effect_system/fluid_spread/smoke/chem
/// Evil evil hack so we have something to "hold" our reagents
var/datum/reagents/chemholder
effect_type = /obj/effect/particle_effect/fluid/smoke/chem
/datum/effect_system/fluid_spread/smoke/chem/New()
..()
chemholder = new(1000, NO_REACT)
/datum/effect_system/fluid_spread/smoke/chem/Destroy()
QDEL_NULL(chemholder)
return ..()
/datum/effect_system/fluid_spread/smoke/chem/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location = null, datum/reagents/carry = null, silent = FALSE)
. = ..()
carry?.copy_to(chemholder, carry.total_volume)
if(silent)
return
var/list/contained_reagents = list()
for(var/datum/reagent/reagent as anything in chemholder.reagent_list)
contained_reagents += "[reagent.volume]u [reagent]"
var/where = "[AREACOORD(location)]"
var/contained = length(contained_reagents) ? "[contained_reagents.Join(", ", " \[", "\]")] @ [chemholder.chem_temp]K" : null
if(carry.my_atom?.fingerprintslast) //Some reagents don't have a my_atom in some cases
var/mob/M = get_mob_by_key(carry.my_atom.fingerprintslast)
var/more = ""
if(M)
more = "[ADMIN_LOOKUPFLW(M)] "
if(!istype(carry.my_atom, /obj/machinery/plumbing))
message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. Key: [more ? more : carry.my_atom.fingerprintslast].")
log_game("A chemical smoke reaction has taken place in ([where])[contained]. Last touched by [carry.my_atom.fingerprintslast].")
else
if(!istype(carry.my_atom, /obj/machinery/plumbing))
message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. No associated key.")
log_game("A chemical smoke reaction has taken place in ([where])[contained]. No associated key.")
/datum/effect_system/fluid_spread/smoke/chem/start()
var/start_loc = holder ? get_turf(holder) : src.location
var/mixcolor = mix_color_from_reagents(chemholder.reagent_list)
var/obj/effect/particle_effect/fluid/smoke/chem/smoke = new effect_type(start_loc, new /datum/fluid_group(amount))
chemholder.copy_to(smoke, chemholder.total_volume)
if(mixcolor)
smoke.add_atom_colour(mixcolor, FIXED_COLOUR_PRIORITY) // give the smoke color, if it has any to begin with
smoke.spread() // Making the smoke spread immediately.
/**
* A version of chemical smoke with a very short lifespan.
*/
/obj/effect/particle_effect/fluid/smoke/chem/quick
lifetime = 4 SECONDS
opacity = FALSE
alpha = 100
/datum/effect_system/fluid_spread/smoke/chem/quick
effect_type = /obj/effect/particle_effect/fluid/smoke/chem/quick