diff --git a/code/__DEFINES/maps.dm b/code/__DEFINES/maps.dm
index a6dfe60eab5..dd4c1fadccb 100644
--- a/code/__DEFINES/maps.dm
+++ b/code/__DEFINES/maps.dm
@@ -88,6 +88,8 @@ Always compile, always use that verb, and always make sure that it works for wha
#define ZTRAIT_SNOWSTORM "Weather_Snowstorm"
#define ZTRAIT_ASHSTORM "Weather_Ashstorm"
#define ZTRAIT_VOIDSTORM "Weather_Voidstorm"
+#define ZTRAIT_RAINSTORM "Weather_Rainstorm"
+#define ZTRAIT_SANDSTORM "Weather_Sandstorm"
/// boolean - does this z prevent ghosts from observing it
#define ZTRAIT_SECRET "Secret"
diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm
index a96a950559a..c8f3ae2282c 100644
--- a/code/__DEFINES/maths.dm
+++ b/code/__DEFINES/maths.dm
@@ -36,6 +36,10 @@
#define ROUND_UP(x) ( -round(-(x)))
+/// Probabilistic rounding: Adds 1 to the integer part of x with a probability equal to the decimal part of x.
+/// ie. ROUND_PROB(40.25) returns 40 with 75% probability, and 41 with 25% probability.
+#define ROUND_PROB(x) ( floor(x) + (prob(fract(x) * 100)) )
+
/// Returns the number of digits in a number. Only works on whole numbers.
/// This is marginally faster than string interpolation -> length
#define DIGITS(x) (ROUND_UP(log(10, x)))
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index efc9bcef44e..df450ab43a2 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -244,6 +244,11 @@
#define SSMACHINES_MACHINES 7
#define SSMACHINES_MACHINES_LATE 8
+// Weather susbsytem tasks
+#define SSWEATHER_MOBS 1
+#define SSWEATHER_TURFS 2
+#define SSWEATHER_THUNDER 3
+
// Wardrobe subsystem tasks
#define SSWARDROBE_STOCK 1
#define SSWARDROBE_INSPECT 2
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index 0b7040ba421..7540444e170 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -700,6 +700,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_ASHSTORM_IMMUNE "ashstorm_immune"
#define TRAIT_SNOWSTORM_IMMUNE "snowstorm_immune"
#define TRAIT_RADSTORM_IMMUNE "radstorm_immune"
+#define TRAIT_SANDSTORM_IMMUNE "sandstorm_immune"
+#define TRAIT_RAINSTORM_IMMUNE "rainstorm_immune"
#define TRAIT_WEATHER_IMMUNE "weather_immune" //Immune to ALL weather effects.
/// Cannot be grabbed by goliath tentacles
diff --git a/code/__DEFINES/weather.dm b/code/__DEFINES/weather.dm
index 18f14d1b45b..cfc163dcdb4 100644
--- a/code/__DEFINES/weather.dm
+++ b/code/__DEFINES/weather.dm
@@ -1,5 +1,7 @@
//A reference to this list is passed into area sound managers, and it's modified in a manner that preserves that reference in ash_storm.dm
GLOBAL_LIST_EMPTY(ash_storm_sounds)
+GLOBAL_LIST_EMPTY(rain_storm_sounds)
+GLOBAL_LIST_EMPTY(sand_storm_sounds)
/// Tracks where we should play snowstorm sounds for the area sound listener
GLOBAL_LIST_EMPTY(snowstorm_sounds)
@@ -7,3 +9,48 @@ GLOBAL_LIST_EMPTY(snowstorm_sounds)
#define MAIN_STAGE 2
#define WIND_DOWN_STAGE 3
#define END_STAGE 4
+
+/// The amount of reagent units that is applied when an object comes into contact with rain
+#define WEATHER_REAGENT_VOLUME 5
+/// Weather reagent volume applied to randomly selected turfs/objects is scaled by this multiplier to compensate for reduced processing frequency
+#define TURF_REAGENT_VOLUME_MULTIPLIER 3
+
+/// 1 / 400 chance for a turf to get a thunder strike per tick (death and destruction to mobs/equipment in area)
+#define THUNDER_CHANCE_INSANE 0.0025
+/// 1 / 1,000 chance for a turf to get a thunder strike per tick (damage to mobs/equipment in area)
+#define THUNDER_CHANCE_HIGH 0.001
+/// 1 / 5,000 chance for a turf to get a thunder strike per tick (sporadic damage to mobs/equipment in area)
+#define THUNDER_CHANCE_AVERAGE 0.0002
+/// 1 / 20,000 chance for a turf to get a thunder strike per tick (rare damage to mobs/equipment in area)
+#define THUNDER_CHANCE_RARE 0.00005
+/// 1 / 50,000 chance for a turf to get a thunder strike per tick (almost no damage to mobs/equipment in area)
+#define THUNDER_CHANCE_VERY_RARE 0.00002
+
+/// admin verb to control thunder via the run_weather command
+GLOBAL_LIST_INIT(thunder_chance_options, list(
+ "Relentless - The Sky is Angry, Very Angry" = THUNDER_CHANCE_INSANE,
+ "Abundant - A shocking amount of thunder" = THUNDER_CHANCE_HIGH,
+ "Regular - Standard Storm Activity" = THUNDER_CHANCE_AVERAGE,
+ "Occasional - A polite amount of thunder" = THUNDER_CHANCE_RARE,
+ "Rare - Like finding a four-leaf clover, but in the sky" = THUNDER_CHANCE_VERY_RARE,
+ "None - Admin Safe Space (Thunder Disabled)" = NONE,
+))
+
+//WEATHER FLAGS
+/// If weather will affect turfs
+#define WEATHER_TURFS (1<<0)
+/// If weather will affect mobs
+#define WEATHER_MOBS (1<<1)
+/// If weather will apply thunder strikes to turfs
+#define WEATHER_THUNDER (1<<2)
+/// If weather will be allowed to affect indoor areas
+#define WEATHER_INDOORS (1<<3)
+/// If weather is endless and can only be stopped manually
+#define WEATHER_ENDLESS (1<<4)
+/// If weather will be detected by a barometer
+#define WEATHER_BAROMETER (1<<5)
+/// If weather temperature ignores clothing insulation when adjusting bodytemperature
+#define WEATHER_TEMPERATURE_BYPASS_CLOTHING (1<<6)
+
+/// Does weather have any type of processing related to mobs, turfs, or thunder?
+#define FUNCTIONAL_WEATHER (WEATHER_TURFS|WEATHER_MOBS|WEATHER_THUNDER)
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index 36a0e9d3593..105645b82fd 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -642,3 +642,13 @@ DEFINE_BITFIELD(construction_upgrades, list(
"RCD_UPGRADE_ANTI_INTERRUPT" = RCD_UPGRADE_ANTI_INTERRUPT,
"RCD_UPGRADE_NO_FREQUENT_USE_COOLDOWN" = RCD_UPGRADE_NO_FREQUENT_USE_COOLDOWN,
))
+
+DEFINE_BITFIELD(weather_flags, list(
+ "WEATHER_TURFS" = WEATHER_TURFS,
+ "WEATHER_MOBS" = WEATHER_MOBS,
+ "WEATHER_THUNDER" = WEATHER_THUNDER,
+ "WEATHER_INDOORS" = WEATHER_INDOORS,
+ "WEATHER_ENDLESS" = WEATHER_ENDLESS,
+ "WEATHER_BAROMETER" = WEATHER_BAROMETER,
+ "WEATHER_TEMPERATURE_BYPASS_CLOTHING" = WEATHER_TEMPERATURE_BYPASS_CLOTHING,
+))
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 99107f33a0f..acd393087eb 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -75,7 +75,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_NOT_ENGRAVABLE" = TRAIT_NOT_ENGRAVABLE,
"TRAIT_ODD_CUSTOMIZABLE_FOOD_INGREDIENT" = TRAIT_ODD_CUSTOMIZABLE_FOOD_INGREDIENT,
"TRAIT_ON_HIT_EFFECT" = TRAIT_ON_HIT_EFFECT,
+ "TRAIT_RAINSTORM_IMMUNE" = TRAIT_RAINSTORM_IMMUNE,
"TRAIT_RUNECHAT_HIDDEN" = TRAIT_RUNECHAT_HIDDEN,
+ "TRAIT_SANDSTORM_IMMUNE" = TRAIT_SANDSTORM_IMMUNE,
"TRAIT_SCARY_FISHERMAN" = TRAIT_SCARY_FISHERMAN,
"TRAIT_SECLUDED_LOCATION" = TRAIT_SECLUDED_LOCATION,
"TRAIT_SILENT_REACTIONS" = TRAIT_SILENT_REACTIONS,
diff --git a/code/_globalvars/traits/admin_tooling.dm b/code/_globalvars/traits/admin_tooling.dm
index 739035e8fb4..93e90333b45 100644
--- a/code/_globalvars/traits/admin_tooling.dm
+++ b/code/_globalvars/traits/admin_tooling.dm
@@ -18,7 +18,9 @@ GLOBAL_LIST_INIT(admin_visible_traits, list(
"TRAIT_MOVE_PHASING" = TRAIT_MOVE_PHASING,
"TRAIT_MOVE_VENTCRAWLING" = TRAIT_MOVE_VENTCRAWLING,
"TRAIT_MOVE_UPSIDE_DOWN" = TRAIT_MOVE_UPSIDE_DOWN,
+ "TRAIT_RAINSTORM_IMMUNE" = TRAIT_RAINSTORM_IMMUNE,
"TRAIT_RUNECHAT_HIDDEN" = TRAIT_RUNECHAT_HIDDEN,
+ "TRAIT_SANDSTORM_IMMUNE" = TRAIT_SANDSTORM_IMMUNE,
"TRAIT_SCARY_FISHERMAN" = TRAIT_SCARY_FISHERMAN,
"TRAIT_SNOWSTORM_IMMUNE" = TRAIT_SNOWSTORM_IMMUNE,
"TRAIT_WEATHER_IMMUNE" = TRAIT_WEATHER_IMMUNE,
diff --git a/code/controllers/subsystem/weather.dm b/code/controllers/subsystem/weather.dm
index 516722c8d00..7c7e8b0cb21 100644
--- a/code/controllers/subsystem/weather.dm
+++ b/code/controllers/subsystem/weather.dm
@@ -11,24 +11,62 @@ SUBSYSTEM_DEF(weather)
var/list/eligible_zlevels = list()
var/list/next_hit_by_zlevel = list() //Used by barometers to know when the next storm is coming
-/datum/controller/subsystem/weather/fire()
+/datum/controller/subsystem/weather/fire(resumed = FALSE)
// process active weather
- for(var/V in processing)
- var/datum/weather/our_event = V
- if(our_event.aesthetic || our_event.stage != MAIN_STAGE)
+ for(var/datum/weather/weather_event as anything in processing)
+ if(!length(weather_event.subsystem_tasks) || weather_event.stage != MAIN_STAGE)
continue
- for(var/mob/act_on as anything in GLOB.mob_living_list)
- if(our_event.can_weather_act(act_on))
- our_event.weather_act(act_on)
+
+ if(weather_event.currentpart == SSWEATHER_MOBS)
+ if(!resumed)
+ weather_event.current_mobs = GLOB.mob_living_list.Copy()
+ var/list/current_mobs_cache = weather_event.current_mobs // cache for performance
+ while(current_mobs_cache.len)
+ var/mob/living/target = current_mobs_cache[current_mobs_cache.len]
+ current_mobs_cache.len--
+ if(QDELETED(target))
+ continue
+ if(weather_event.can_weather_act_mob(target))
+ weather_event.weather_act_mob(target)
+ if(MC_TICK_CHECK)
+ return
+ resumed = FALSE
+ weather_event.currentpart = weather_event.subsystem_tasks[WRAP_UP(weather_event.currentpart, weather_event.subsystem_tasks.len)]
+
+ if(weather_event.currentpart == SSWEATHER_TURFS)
+ if(!resumed)
+ weather_event.turf_iteration = ROUND_PROB(weather_event.weather_turfs_per_tick)
+ while(weather_event.turf_iteration)
+ weather_event.turf_iteration--
+ var/turf/selected_turf = weather_event.pick_turf()
+ if(selected_turf && weather_event.can_weather_act_turf(selected_turf))
+ weather_event.weather_act_turf(selected_turf)
+ if(MC_TICK_CHECK)
+ return
+ resumed = FALSE
+ weather_event.currentpart = weather_event.subsystem_tasks[WRAP_UP(weather_event.currentpart, weather_event.subsystem_tasks.len)]
+
+ if(weather_event.currentpart == SSWEATHER_THUNDER)
+ if(!resumed)
+ weather_event.thunder_iteration = ROUND_PROB(weather_event.thunder_turfs_per_tick)
+ while(weather_event.thunder_iteration)
+ weather_event.thunder_iteration--
+ var/turf/selected_turf = weather_event.pick_turf()
+ if(selected_turf && weather_event.can_weather_act_turf(selected_turf))
+ weather_event.thunder_act_turf(selected_turf)
+ if(MC_TICK_CHECK)
+ return
+ resumed = FALSE
+ weather_event.currentpart = weather_event.subsystem_tasks[WRAP_UP(weather_event.currentpart, weather_event.subsystem_tasks.len)]
// start random weather on relevant levels
for(var/z in eligible_zlevels)
var/possible_weather = eligible_zlevels[z]
- var/datum/weather/our_event = pick_weight(possible_weather)
- run_weather(our_event, list(text2num(z)))
+ var/datum/weather/weather_event = pick_weight(possible_weather)
+ run_weather(weather_event, list(text2num(z)))
eligible_zlevels -= z
- var/randTime = rand(3000, 6000)
- next_hit_by_zlevel["[z]"] = addtimer(CALLBACK(src, PROC_REF(make_eligible), z, possible_weather), randTime + initial(our_event.weather_duration_upper), TIMER_UNIQUE|TIMER_STOPPABLE) //Around 5-10 minutes between weathers
+ var/randTime = rand(5 MINUTES, 10 MINUTES)
+ next_hit_by_zlevel["[z]"] = addtimer(CALLBACK(src, PROC_REF(make_eligible), z, possible_weather), randTime + initial(weather_event.weather_duration_upper), TIMER_UNIQUE|TIMER_STOPPABLE)
/datum/controller/subsystem/weather/Initialize()
for(var/V in subtypesof(/datum/weather))
@@ -52,7 +90,7 @@ SUBSYSTEM_DEF(weather)
LAZYINITLIST(eligible_zlevels["[z]"])
eligible_zlevels["[z]"][weather] = probability
-/datum/controller/subsystem/weather/proc/run_weather(datum/weather/weather_datum_type, z_levels)
+/datum/controller/subsystem/weather/proc/run_weather(datum/weather/weather_datum_type, z_levels, list/weather_data)
if (istext(weather_datum_type))
for (var/V in subtypesof(/datum/weather))
var/datum/weather/W = V
@@ -69,7 +107,8 @@ SUBSYSTEM_DEF(weather)
else if (!islist(z_levels))
CRASH("run_weather called with invalid z_levels: [z_levels || "null"]")
- var/datum/weather/W = new weather_datum_type(z_levels)
+
+ var/datum/weather/W = new weather_datum_type(z_levels, weather_data)
W.telegraph()
/datum/controller/subsystem/weather/proc/make_eligible(z, possible_weather)
@@ -88,11 +127,3 @@ SUBSYSTEM_DEF(weather)
///Returns an active storm by its type
/datum/controller/subsystem/weather/proc/get_weather_by_type(type)
return locate(type) in processing
-
-ADMIN_VERB(stop_weather, R_DEBUG|R_ADMIN, "Stop All Active Weather", "Stop all currently active weather.", ADMIN_CATEGORY_DEBUG)
- log_admin("[key_name(user)] stopped all currently active weather.")
- message_admins("[key_name_admin(user)] stopped all currently active weather.")
- for(var/datum/weather/current_weather as anything in SSweather.processing)
- if(current_weather in SSweather.processing)
- current_weather.end()
- BLACKBOX_LOG_ADMIN_VERB("Stop All Active Weather")
diff --git a/code/datums/components/object_possession.dm b/code/datums/components/object_possession.dm
index 85609c0d6d9..256f2952670 100644
--- a/code/datums/components/object_possession.dm
+++ b/code/datums/components/object_possession.dm
@@ -71,6 +71,8 @@
target.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
target.AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
+ target.AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ target.AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(end_possession))
return TRUE
@@ -83,6 +85,8 @@
var/mob/poltergeist = parent
possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
UnregisterSignal(possessed, COMSIG_QDELETING)
diff --git a/code/datums/components/weatherannouncer.dm b/code/datums/components/weatherannouncer.dm
index 4ac103a7758..b30e636e245 100644
--- a/code/datums/components/weatherannouncer.dm
+++ b/code/datums/components/weatherannouncer.dm
@@ -123,7 +123,7 @@
for(var/datum/weather/check_weather as anything in SSweather.processing)
- if(!check_weather.barometer_predictable || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
+ if(!(check_weather.weather_flags & WEATHER_BAROMETER) || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
continue
for (var/mining_level in mining_z_levels)
if(mining_level in check_weather.impacted_z_levels)
@@ -154,12 +154,12 @@
warning_level = WEATHER_ALERT_IMMINENT_OR_ACTIVE
for(var/datum/weather/check_weather as anything in SSweather.processing)
- if(!check_weather.barometer_predictable || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
+ if(!(check_weather.weather_flags & WEATHER_BAROMETER) || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
continue
var/list/mining_z_levels = SSmapping.levels_by_trait(ZTRAIT_MINING)
for(var/mining_level in mining_z_levels)
if(mining_level in check_weather.impacted_z_levels)
- is_weather_dangerous = !check_weather.aesthetic
+ is_weather_dangerous = !(check_weather.weather_flags & FUNCTIONAL_WEATHER)
return
/datum/component/weather_announcer/proc/on_examine(atom/radio, mob/examiner, list/examine_texts)
diff --git a/code/datums/elements/weather_listener.dm b/code/datums/elements/weather_listener.dm
index d2095ad3546..05a4507f1a4 100644
--- a/code/datums/elements/weather_listener.dm
+++ b/code/datums/elements/weather_listener.dm
@@ -27,7 +27,6 @@
RegisterSignal(target, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(handle_z_level_change), override = TRUE)
RegisterSignal(target, COMSIG_MOB_LOGOUT, PROC_REF(handle_logout), override = TRUE)
-
var/mob/target_mob = target
handle_z_level_change(target_mob, null, target_mob.loc)
diff --git a/code/datums/looping_sounds/weather.dm b/code/datums/looping_sounds/weather.dm
index 4b164865fc9..648651c56d8 100644
--- a/code/datums/looping_sounds/weather.dm
+++ b/code/datums/looping_sounds/weather.dm
@@ -93,3 +93,24 @@
play(picked_sound)
if(sound_to_length[picked_sound])
timer_id = addtimer(CALLBACK(src, PROC_REF(sound_loop)), sound_to_length[picked_sound], TIMER_CLIENT_TIME | TIMER_STOPPABLE | TIMER_DELETE_ME, SSsound_loops)
+
+/datum/looping_sound/rain
+ start_sound = 'sound/ambience/weather/rain/rain_start.ogg'
+ start_length = 12.5 SECONDS
+ mid_sounds = 'sound/ambience/weather/rain/rain_mid.ogg'
+ mid_length = 15 SECONDS
+ end_sound = 'sound/ambience/weather/rain/rain_end.ogg'
+ volume = 70
+ sound_channel = CHANNEL_WEATHER
+
+/datum/looping_sound/rain/start
+ mid_sounds = 'sound/ambience/weather/rain/rain_start.ogg'
+ mid_length = 12.5 SECONDS
+
+/datum/looping_sound/rain/middle
+ mid_sounds = 'sound/ambience/weather/rain/rain_mid.ogg'
+ mid_length = 15 SECONDS
+
+/datum/looping_sound/rain/end
+ mid_sounds = 'sound/ambience/weather/rain/rain_end.ogg'
+ mid_length = 17 SECONDS
diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm
index 7b18e184a59..3128b116ebe 100644
--- a/code/datums/weather/weather.dm
+++ b/code/datums/weather/weather.dm
@@ -1,3 +1,8 @@
+/// The maximum amount of turfs that can be processed in a single tick regardless of
+/// the number of turfs determined by turf_weather_chance and turf_thunder_chance
+/// increasing this too high can result in severe lag so please be careful
+#define MAX_TURFS_PER_TICK 500
+
/**
* Causes weather to occur on a z level in certain area types
*
@@ -31,7 +36,7 @@
var/weather_duration_lower = 2 MINUTES
/// See above - this is the highest possible duration
var/weather_duration_upper = 2.5 MINUTES
- /// Looping sound while weather is occuring
+ /// The sound played to everyone on an affected z-level when weather is occuring (does not loop)
var/weather_sound
/// Area overlay while the weather is occuring
var/weather_overlay
@@ -51,23 +56,25 @@
/// Types of area to affect
var/area_type = /area/space
- /// TRUE value protects areas with outdoors marked as false, regardless of area type
- var/protect_indoors = FALSE
/// Areas to be affected by the weather, calculated when the weather begins
var/list/impacted_areas = list()
+ /// A weighted list of areas impacted by weather, where weights reflect the total turf count in each area.
+ var/list/impacted_areas_weighted = list()
+ /// The total number of turfs impacted by weather across all z-levels and areas.
+ var/total_impacted_turfs = 0
/// Areas affected by weather have their blend modes changed
var/list/impacted_areas_blend_modes = list()
/// Areas that are protected and excluded from the affected areas.
var/list/protected_areas = list()
/// The list of z-levels that this weather is actively affecting
var/impacted_z_levels
+ /// A weighted list of z-levels impacted by weather, where weights reflect the total turf count on each level
+ var/list/impacted_z_levels_weighted = list()
/// Since it's above everything else, this is the layer used by default.
var/overlay_layer = AREA_LAYER
/// Plane for the overlay
var/overlay_plane = WEATHER_PLANE
- /// If the weather has no purpose other than looks
- var/aesthetic = FALSE
/// Used by mobs (or movables containing mobs, such as enviro bags) to prevent them from being affected by the weather.
var/immunity_type
/// If this bit of weather should also draw an overlay that's uneffected by lighting onto the area
@@ -83,17 +90,90 @@
var/probability = 0
/// The z-level trait to affect when run randomly or when not overridden.
var/target_trait = ZTRAIT_STATION
-
- /// Whether a barometer can predict when the weather will happen
- var/barometer_predictable = FALSE
/// For barometers to know when the next storm will hit
var/next_hit_time = 0
- /// This causes the weather to only end if forced to
- var/perpetual = FALSE
+ /// The chance, per tick, a turf will have weather effects applied to it. This is a decimal value, 1.00 = 100%, 0.50 = 50%, etc.
+ /// Recommend setting this low near 0.01 (results in 1 in 100 affected turfs having weather reagents applied per tick)
+ var/turf_weather_chance = 0.01
+ /// The chance, per tick, a turf will have a thunder strike applied to it. This is a decimal value, 1.00 = 100%, 0.50 = 50%, etc.
+ /// Recommend setting this really low near 0.001 (results in 1 in 1000 affected turfs having thunder strikes applied per tick)
+ var/turf_thunder_chance = THUNDER_CHANCE_AVERAGE // does nothing without the WEATHER_THUNDER weather_flag
+ /// The calculated amount of turfs that get weather effects processed each tick (this gets calculated do not manually set this var)
+ var/weather_turfs_per_tick = 0
+ /// The calculated amount of turfs that get thunder effects processed each tick (this gets calculated do not manually set this var)
+ var/thunder_turfs_per_tick = 0
+ /// Color to apply to thunder while weather is occuring
+ var/thunder_color = null
-/datum/weather/New(z_levels)
+ /// List of weather bitflags that determines effects (see \code\__DEFINES\weather.dm)
+ var/weather_flags = NONE
+
+ /// List of current mobs being processed by weather
+ var/list/current_mobs = list()
+ /// The weather turf counter to keep track of how many turfs we have processed so far
+ var/turf_iteration = 0
+ /// The weather thunder counter to keep track of how much thunder we have processed so far
+ var/thunder_iteration = 0
+ /// The current section our weather subsystem is processing
+ var/currentpart
+ /// The list of allowed tasks our weather subsystem is allowed to process (determined by weather_flags)
+ var/list/subsystem_tasks = list()
+
+ /// The temperature of our weather that is applied to weather reagents and mobs using adjust_bodytemperature()
+ var/weather_temperature = T20C
+
+ /// A list (supports regular, nested, and weighted) of possible reagents that will rain down from the sky.
+ /// Only one of these will be selected to be used as the reagent
+ var/list/whitelist_weather_reagents
+ /// A list of reagents that are forbidden from being selected when there is no
+ /// whitelist and the reagents are randomized
+ var/list/blacklist_weather_reagents
+ /// The selected reagent that will be rained down
+ var/datum/reagent/weather_reagent
+ /// The actual atom that holds our reagents that is held in nullspace
+ var/obj/effect/abstract/weather_reagent_holder
+
+/datum/weather/New(z_levels, list/weather_data)
..()
+
impacted_z_levels = z_levels
+ area_type = weather_data?["area"] || area_type
+ weather_flags = weather_data?["weather_flags"] || weather_flags
+ turf_thunder_chance = isnull(weather_data?["thunder_chance"]) ? turf_thunder_chance : weather_data?["thunder_chance"]
+
+ var/datum/reagent/custom_reagent = weather_data?["reagent"]
+ var/reagent_id
+ if(custom_reagent)
+ reagent_id = custom_reagent
+ else if(whitelist_weather_reagents)
+ reagent_id = pick_weight_recursive(whitelist_weather_reagents)
+ else if(blacklist_weather_reagents) // randomized
+ reagent_id = get_random_reagent_id(blacklist_weather_reagents)
+
+ if(reagent_id)
+ weather_reagent = find_reagent_object_from_type(reagent_id)
+ weather_color = weather_reagent.color
+ weather_reagent_holder = new(null) // spawns in nullspace
+ weather_reagent_holder.create_reagents(WEATHER_REAGENT_VOLUME, NO_REACT)
+ weather_reagent_holder.reagents.add_reagent(reagent_id, WEATHER_REAGENT_VOLUME)
+ weather_reagent_holder.reagents.set_temperature(weather_temperature)
+
+ if(weather_flags & (WEATHER_MOBS))
+ subsystem_tasks += SSWEATHER_MOBS
+ if(weather_flags & (WEATHER_TURFS))
+ subsystem_tasks += SSWEATHER_TURFS
+ if(weather_flags & (WEATHER_THUNDER))
+ subsystem_tasks += SSWEATHER_THUNDER
+
+ if(length(subsystem_tasks))
+ currentpart = subsystem_tasks[1]
+
+ setup_weather_areas()
+ setup_weather_turfs()
+
+/datum/weather/Destroy()
+ QDEL_NULL(weather_reagent_holder)
+ return ..()
/**
* Telegraphs the beginning of the weather on the impacted z levels
@@ -105,22 +185,8 @@
/datum/weather/proc/telegraph()
if(stage == STARTUP_STAGE)
return
- SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_TELEGRAPH(type), src)
stage = STARTUP_STAGE
- var/list/affectareas = list()
- for(var/area/selected_area as anything in get_areas(area_type))
- affectareas += selected_area
- for(var/area/protected_area as anything in protected_areas)
- affectareas -= get_areas(protected_area)
- affectareas = enhanced_roleplay_filter(affectareas) // BUBBER EDIT ADDITION - enhanced roleplay check - modular_zubbers/code/modules/events/ev_roleplay_check.dm
- for(var/area/affected_area as anything in affectareas)
- if(protect_indoors && !affected_area.outdoors)
- continue
-
- for(var/z in impacted_z_levels)
- if(length(affected_area.turfs_by_zlevel) >= z && length(affected_area.turfs_by_zlevel[z]))
- impacted_areas |= affected_area
- continue
+ SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_TELEGRAPH(type), src)
weather_duration = rand(weather_duration_lower, weather_duration_upper)
SSweather.processing |= src
@@ -129,6 +195,63 @@
send_alert(telegraph_message, telegraph_sound, telegraph_sound_vol)
addtimer(CALLBACK(src, PROC_REF(start)), telegraph_duration)
+/datum/weather/proc/setup_weather_areas()
+ var/list/affectareas = list()
+ for(var/area/selected_area as anything in get_areas(area_type))
+ affectareas += selected_area
+ for(var/area/protected_area as anything in protected_areas)
+ affectareas -= get_areas(protected_area)
+ affectareas = enhanced_roleplay_filter(affectareas) // BUBBER EDIT ADDITION - enhanced roleplay check - modular_zubbers/code/modules/events/ev_roleplay_check.dm
+ for(var/area/affected_area as anything in affectareas)
+ if(!(weather_flags & WEATHER_INDOORS) && !affected_area.outdoors)
+ continue
+
+ for(var/z in impacted_z_levels)
+ var/total_turfs = length(affected_area.turfs_by_zlevel) >= z && length(affected_area.turfs_by_zlevel[z])
+ if(!total_turfs)
+ continue
+
+ impacted_areas |= affected_area
+
+ if(!(weather_flags & (WEATHER_THUNDER|WEATHER_TURFS)))
+ continue
+
+ var/z_string = num2text(z)
+ if(!impacted_z_levels_weighted[z_string])
+ impacted_z_levels_weighted[z_string] = 0
+ if(!impacted_areas_weighted[z_string])
+ impacted_areas_weighted[z_string] = list()
+
+ impacted_z_levels_weighted[z_string] += total_turfs
+ impacted_areas_weighted[z_string][affected_area] = total_turfs
+ total_impacted_turfs += total_turfs
+
+/// Selects a turf impacted by weather, if available, otherwise returns null
+/datum/weather/proc/pick_turf()
+ var/z_string = pick_weight_recursive(impacted_z_levels_weighted)
+ var/area/selected_area = pick_weight_recursive(impacted_areas_weighted[z_string])
+ var/z = text2num(z_string)
+ var/list/available_turfs = selected_area.get_turfs_by_zlevel(z)
+ // Areas or turfs may change during weather events. For example, a shuttle
+ // landing and departing might leave an area in 'impacted_areas' but without
+ // turfs on the expected z-level, resulting in an empty 'available_turfs' list.
+ if(length(available_turfs))
+ return pick(available_turfs)
+ return
+
+/datum/weather/proc/setup_weather_turfs()
+ if(!(weather_flags & (WEATHER_TURFS|WEATHER_THUNDER)))
+ return
+ if(!total_impacted_turfs)
+ return
+
+ if(weather_flags & (WEATHER_TURFS))
+ weather_turfs_per_tick = total_impacted_turfs * turf_weather_chance
+ weather_turfs_per_tick = min(weather_turfs_per_tick, MAX_TURFS_PER_TICK)
+ if(weather_flags & (WEATHER_THUNDER))
+ thunder_turfs_per_tick = total_impacted_turfs * turf_thunder_chance
+ thunder_turfs_per_tick = min(thunder_turfs_per_tick, MAX_TURFS_PER_TICK)
+
/**
* Starts the actual weather and effects from it
*
@@ -143,7 +266,7 @@
stage = MAIN_STAGE
update_areas()
send_alert(weather_message, weather_sound)
- if(!perpetual)
+ if(!(weather_flags & (WEATHER_ENDLESS)))
addtimer(CALLBACK(src, PROC_REF(wind_down)), weather_duration)
for(var/area/impacted_area as anything in impacted_areas)
SEND_SIGNAL(impacted_area, COMSIG_WEATHER_BEGAN_IN_AREA(type), src)
@@ -200,9 +323,8 @@
/**
* Returns TRUE if the living mob can be affected by the weather
- *
*/
-/datum/weather/proc/can_weather_act(mob/living/mob_to_check)
+/datum/weather/proc/can_weather_act_mob(mob/living/mob_to_check)
var/turf/mob_turf = get_turf(mob_to_check)
if(!mob_turf)
@@ -226,15 +348,95 @@
return TRUE
/**
- * Affects the mob with whatever the weather does
- *
+ * Returns TRUE if the turf can be affected by the weather
*/
-/datum/weather/proc/weather_act(mob/living/L)
- return
+/datum/weather/proc/can_weather_act_turf(turf/valid_weather_turf)
+ // applying weather effects to solid walls is a waste since nothing will happen
+ if(isclosedturf(valid_weather_turf))
+ return
+ // same logic for space and openspace turfs
+ if(is_space_or_openspace(valid_weather_turf))
+ return
+ // solid windows are also worth skipping
+ var/obj/structure/window/window = locate() in valid_weather_turf
+ if(window?.fulltile)
+ return
+
+ return TRUE
+
+/**
+ * Affects the mob with whatever the weather does
+ */
+/datum/weather/proc/weather_act_mob(mob/living/living)
+ var/temperature_delta = weather_temperature - living.bodytemperature
+ if(iscarbon(living))
+ var/mob/living/carbon/carbon_living = living
+ var/insulation_flag = !(weather_flags & WEATHER_TEMPERATURE_BYPASS_CLOTHING)
+ carbon_living.adjust_bodytemperature(temperature_delta, use_insulation=insulation_flag, use_steps=TRUE)
+ else // stolen from carbon/adjust_bodytemperature() which should really be universally applied to living/adjust_bodytemperature()
+ // Use the bodytemp divisors to get the change step, with max step size
+ temperature_delta = (temperature_delta > 0) ? (temperature_delta / BODYTEMP_HEAT_DIVISOR) : (temperature_delta / BODYTEMP_COLD_DIVISOR)
+ // Clamp the results to the min and max step size
+ temperature_delta = (temperature_delta > 0) ? min(temperature_delta, BODYTEMP_HEATING_MAX) : max(temperature_delta, BODYTEMP_COOLING_MAX)
+ living.adjust_bodytemperature(temperature_delta)
+
+ if(!weather_reagent || !weather_reagent_holder || living.IsObscured())
+ return
+
+ if(istype(weather_reagent, /datum/reagent/water))
+ living.wash()
+
+ weather_reagent_holder.reagents.expose(living, TOUCH)
+
+/**
+ * Affects the turf with whatever the weather does
+ */
+/datum/weather/proc/weather_act_turf(turf/open/weather_turf)
+ if(!weather_reagent || !weather_reagent_holder)
+ return
+
+ weather_reagent_holder.reagents.expose(weather_turf, TOUCH, TURF_REAGENT_VOLUME_MULTIPLIER)
+ for(var/atom/thing as anything in weather_turf)
+ if(thing.IsObscured() || isliving(thing))
+ continue
+
+ weather_reagent_holder.reagents.expose(thing, TOUCH, TURF_REAGENT_VOLUME_MULTIPLIER)
+
+ // Time for the sophisticated art of catching sky-booze
+ if(!is_reagent_container(thing))
+ continue
+
+ var/obj/item/reagent_containers/container = thing
+ if(!container.is_open_container() || container.reagents.holder_full())
+ continue
+
+ container.reagents.add_reagent(weather_reagent.type, WEATHER_REAGENT_VOLUME, TURF_REAGENT_VOLUME_MULTIPLIER)
+
+ if(istype(weather_reagent, /datum/reagent/water))
+ weather_turf.wash(CLEAN_ALL, TRUE)
+
+/**
+ * Affects the turf with thunder
+ */
+/datum/weather/proc/thunder_act_turf(turf/open/weather_turf)
+ var/obj/effect/temp_visual/thunderbolt/thunder = new(weather_turf)
+ thunder.flash_lighting_fx(6, 2, duration = thunder.duration)
+
+ if(thunder_color)
+ thunder.color = thunder_color
+
+ for(var/mob/living/hit_mob in weather_turf)
+ to_chat(hit_mob, span_userdanger("You've been struck by lightning!"))
+ hit_mob.electrocute_act(50, "thunder", flags = SHOCK_TESLA|SHOCK_NOGLOVES)
+
+ for(var/obj/hit_thing in weather_turf)
+ hit_thing.take_damage(20, BURN, ENERGY, FALSE)
+ playsound(weather_turf, 'sound/effects/magic/lightningbolt.ogg', 100, extrarange = 10, falloff_distance = 10)
+ weather_turf.visible_message(span_danger("A thunderbolt strikes [weather_turf]!"))
+ explosion(weather_turf, light_impact_range = 1, flame_range = 1, silent = TRUE, adminlog = FALSE)
/**
* Updates the overlays on impacted areas
- *
*/
/datum/weather/proc/update_areas()
var/list/new_overlay_cache = generate_overlay_cache()
@@ -289,3 +491,5 @@
gen_overlay_cache += new_weather_overlay
return gen_overlay_cache
+
+#undef MAX_TURFS_PER_TICK
diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm
index 6ada1044e62..78b084e2aad 100644
--- a/code/datums/weather/weather_types/ash_storm.dm
+++ b/code/datums/weather/weather_types/ash_storm.dm
@@ -17,34 +17,27 @@
end_overlay = "light_ash"
area_type = /area
- protect_indoors = TRUE
target_trait = ZTRAIT_ASHSTORM
-
immunity_type = TRAIT_ASHSTORM_IMMUNE
-
probability = 90
- barometer_predictable = TRUE
+ weather_flags = (WEATHER_MOBS | WEATHER_BAROMETER)
+
var/list/weak_sounds = list()
var/list/strong_sounds = list()
/datum/weather/ash_storm/telegraph()
- var/list/eligible_areas = list()
- for (var/z in impacted_z_levels)
- eligible_areas += SSmapping.areas_in_z["[z]"]
- for(var/i in 1 to eligible_areas.len)
- var/area/place = eligible_areas[i]
+ for(var/area/impacted_area as anything in impacted_areas)
//BUBBER ADDITION BEGIN - This is a HORRIBLE HACK to stop the weather from triggering for these locations
- if(place.ignore_weather_sfx)
+ if(impacted_area.ignore_weather_sfx)
continue
//BUBBER ADDITION END
- if(place.outdoors)
- weak_sounds[place] = /datum/looping_sound/weak_outside_ashstorm
- strong_sounds[place] = /datum/looping_sound/active_outside_ashstorm
+ if(impacted_area.outdoors)
+ weak_sounds[impacted_area] = /datum/looping_sound/weak_outside_ashstorm
+ strong_sounds[impacted_area] = /datum/looping_sound/active_outside_ashstorm
else
- weak_sounds[place] = /datum/looping_sound/weak_inside_ashstorm
- strong_sounds[place] = /datum/looping_sound/active_inside_ashstorm
- CHECK_TICK
+ weak_sounds[impacted_area] = /datum/looping_sound/weak_inside_ashstorm
+ strong_sounds[impacted_area] = /datum/looping_sound/active_inside_ashstorm
//We modify this list instead of setting it to weak/stron sounds in order to preserve things that hold a reference to it
//It's essentially a playlist for a bunch of components that chose what sound to loop based on the area a player is in
@@ -61,7 +54,7 @@
GLOB.ash_storm_sounds += weak_sounds
return ..()
-/datum/weather/ash_storm/can_weather_act(mob/living/mob_to_check)
+/datum/weather/ash_storm/can_weather_act_mob(mob/living/mob_to_check)
. = ..()
if(!. || !ishuman(mob_to_check))
return
@@ -69,8 +62,9 @@
if(human_to_check.get_thermal_protection() >= FIRE_IMMUNITY_MAX_TEMP_PROTECT)
return FALSE
-/datum/weather/ash_storm/weather_act(mob/living/victim)
+/datum/weather/ash_storm/weather_act_mob(mob/living/victim)
victim.adjustFireLoss(4, required_bodytype = BODYTYPE_ORGANIC)
+ return ..()
/datum/weather/ash_storm/end()
GLOB.ash_storm_sounds -= weak_sounds
@@ -91,6 +85,6 @@
end_message = span_notice("The emberfall slows, stops. Another layer of hardened soot to the basalt beneath your feet.")
end_sound = null
- aesthetic = TRUE
+ weather_flags = parent_type::weather_flags & ~WEATHER_MOBS
probability = 10
diff --git a/code/datums/weather/weather_types/floor_is_lava.dm b/code/datums/weather/weather_types/floor_is_lava.dm
index 2541463f405..14130421e6e 100644
--- a/code/datums/weather/weather_types/floor_is_lava.dm
+++ b/code/datums/weather/weather_types/floor_is_lava.dm
@@ -25,9 +25,10 @@
/// Can't really use like, the emissive system here because I am not about to make
/// all walls block emissive
use_glow = FALSE
+ weather_flags = (WEATHER_MOBS | WEATHER_INDOORS)
-/datum/weather/floor_is_lava/can_weather_act(mob/living/mob_to_check)
+/datum/weather/floor_is_lava/can_weather_act_mob(mob/living/mob_to_check)
if(!mob_to_check.client) //Only sentient people are going along with it!
return FALSE
. = ..()
@@ -42,5 +43,6 @@
if(mob_to_check.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
return FALSE
-/datum/weather/floor_is_lava/weather_act(mob/living/victim)
+/datum/weather/floor_is_lava/weather_act_mob(mob/living/victim)
victim.adjustFireLoss(3)
+ return ..()
diff --git a/code/datums/weather/weather_types/radiation_storm.dm b/code/datums/weather/weather_types/radiation_storm.dm
index dc4722f3d02..409a1893057 100644
--- a/code/datums/weather/weather_types/radiation_storm.dm
+++ b/code/datums/weather/weather_types/radiation_storm.dm
@@ -24,6 +24,7 @@
target_trait = ZTRAIT_STATION
immunity_type = TRAIT_RADSTORM_IMMUNE
+ weather_flags = (WEATHER_MOBS | WEATHER_INDOORS)
/// Chance we get a negative mutation, if we fail we get a positive one
var/negative_mutation_chance = 90
/// Chance we mutate
@@ -34,7 +35,7 @@
status_alarm(TRUE)
-/datum/weather/rad_storm/weather_act(mob/living/living)
+/datum/weather/rad_storm/weather_act_mob(mob/living/living)
if(!prob(mutate_chance))
return
@@ -57,6 +58,8 @@
if(prob(50))
do_mutate(human)
+ return ..()
+
/datum/weather/rad_storm/end()
if(..())
return
@@ -90,17 +93,14 @@
protected_areas = list(/area/shuttle, /area/station/maintenance/radshelter)
weather_overlay = "nebula_radstorm"
- weather_duration_lower = 100 HOURS
- weather_duration_upper = 100 HOURS
-
end_message = null
+ weather_flags = parent_type::weather_flags | WEATHER_ENDLESS
mutate_chance = 0.1
-
///Chance we pulse a living during the storm
var/radiation_chance = 5
-/datum/weather/rad_storm/nebula/weather_act(mob/living/living)
+/datum/weather/rad_storm/nebula/weather_act_mob(mob/living/living)
..()
if(!prob(radiation_chance))
diff --git a/code/datums/weather/weather_types/rain_storm.dm b/code/datums/weather/weather_types/rain_storm.dm
new file mode 100644
index 00000000000..db97b1df50e
--- /dev/null
+++ b/code/datums/weather/weather_types/rain_storm.dm
@@ -0,0 +1,90 @@
+/datum/weather/rain_storm
+ name = "rain"
+ desc = "Heavy thunderstorms rain down below, drenching anyone caught in it."
+
+ telegraph_message = span_danger("Thunder rumbles far above. You hear droplets drumming against the canopy.")
+ telegraph_overlay = "rain_low"
+ telegraph_duration = 30 SECONDS
+
+ weather_message = span_userdanger("Rain pours down around you!")
+ weather_overlay = "rain_high"
+
+ end_message = span_bolddanger("The downpour gradually slows to a light shower.")
+ end_overlay = "rain_low"
+ end_duration = 30 SECONDS
+
+ weather_duration_lower = 3 MINUTES
+ weather_duration_upper = 5 MINUTES
+
+ weather_color = null
+ thunder_color = null
+
+ area_type = /area
+ target_trait = ZTRAIT_RAINSTORM
+ immunity_type = TRAIT_RAINSTORM_IMMUNE
+ probability = 90
+
+ weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_THUNDER | WEATHER_BAROMETER)
+ whitelist_weather_reagents = list(/datum/reagent/water)
+
+/datum/weather/rain_storm/telegraph()
+ GLOB.rain_storm_sounds.Cut()
+ for(var/area/impacted_area as anything in impacted_areas)
+ GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/start
+ return ..()
+
+/datum/weather/rain_storm/start()
+ GLOB.rain_storm_sounds.Cut()
+ for(var/area/impacted_area as anything in impacted_areas)
+ GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/middle
+ return ..()
+
+/datum/weather/rain_storm/wind_down()
+ GLOB.rain_storm_sounds.Cut()
+ for(var/area/impacted_area as anything in impacted_areas)
+ GLOB.rain_storm_sounds[impacted_area] = /datum/looping_sound/rain/end
+ return ..()
+
+/datum/weather/rain_storm/end()
+ GLOB.rain_storm_sounds.Cut()
+ return ..()
+
+/datum/weather/rain_storm/blood
+ whitelist_weather_reagents = list(/datum/reagent/blood)
+ probability = 0 // admeme event
+
+// Fun fact - if you increase the weather_temperature higher than LIQUID_PLASMA_BP
+// the plasma rain will vaporize into a gas on whichever turf it lands on
+/datum/weather/rain_storm/plasma
+ whitelist_weather_reagents = list(/datum/reagent/toxin/plasma)
+ probability = 0 // maybe for icebox maps one day?
+
+/datum/weather/rain_storm/deep_fried
+ weather_temperature = 455 // just hot enough to apply the fried effect
+ whitelist_weather_reagents = list(/datum/reagent/consumable/nutriment/fat/oil)
+ weather_flags = (WEATHER_TURFS | WEATHER_INDOORS)
+ probability = 0 // admeme event
+
+/datum/weather/rain_storm/acid
+ desc = "The planet's thunderstorms are by nature acidic, and will incinerate anyone standing beneath them without protection."
+
+ telegraph_duration = 40 SECONDS
+ telegraph_message = span_warning("Thunder rumbles far above. You hear acidic droplets hissing against the canopy. Seek shelter!")
+ telegraph_sound = 'sound/effects/siren.ogg'
+
+ weather_message = span_userdanger("Acidic rain pours down around you! Get inside!")
+ weather_duration_lower = 1 MINUTES
+ weather_duration_upper = 2 MINUTES
+
+ end_duration = 10 SECONDS
+ end_message = span_bolddanger("The downpour gradually slows to a light shower. It should be safe outside now.")
+
+ // these are weighted by acidpwr which causes more damage the higher it is
+ whitelist_weather_reagents = list(
+ /datum/reagent/toxin/acid/nitracid = 3,
+ /datum/reagent/toxin/acid = 2,
+ /datum/reagent/toxin/acid/fluacid = 1,
+ )
+ probability = 0
+
+
diff --git a/code/datums/weather/weather_types/sand_storm.dm b/code/datums/weather/weather_types/sand_storm.dm
new file mode 100644
index 00000000000..89d71c09ae5
--- /dev/null
+++ b/code/datums/weather/weather_types/sand_storm.dm
@@ -0,0 +1,64 @@
+//Darude sandstorm starts playing
+/datum/weather/sand_storm
+ name = "severe sandstorm"
+ desc = "A severe dust storm that engulfs an area, dealing intense damage to the unprotected."
+
+ telegraph_message = span_danger("You see a dust cloud rising over the horizon. That can't be good...")
+ telegraph_duration = 30 SECONDS
+ telegraph_overlay = "dust_med"
+ telegraph_sound = 'sound/effects/siren.ogg'
+
+ weather_message = span_userdanger("Hot sand and wind batter you! Get inside!")
+ weather_duration_lower = 1 MINUTES
+ weather_duration_upper = 2 MINUTES
+ weather_overlay = "dust_high"
+
+ end_message = span_bolddanger("The shrieking wind whips away the last of the sand and falls to its usual murmur. It should be safe to go outside now.")
+ end_duration = 30 SECONDS
+ end_overlay = "dust_med"
+
+ area_type = /area
+ target_trait = ZTRAIT_SANDSTORM
+ immunity_type = TRAIT_SANDSTORM_IMMUNE
+ probability = 90
+
+ weather_flags = (WEATHER_MOBS | WEATHER_BAROMETER)
+
+/datum/weather/sand_storm/telegraph()
+ GLOB.sand_storm_sounds.Cut()
+ for(var/area/impacted_area as anything in impacted_areas)
+ GLOB.sand_storm_sounds[impacted_area] = /datum/looping_sound/weak_outside_ashstorm
+ return ..()
+
+/datum/weather/sand_storm/start()
+ GLOB.sand_storm_sounds.Cut()
+ for(var/area/impacted_area as anything in impacted_areas)
+ GLOB.sand_storm_sounds[impacted_area] = /datum/looping_sound/active_outside_ashstorm
+ return ..()
+
+/datum/weather/sand_storm/wind_down()
+ GLOB.sand_storm_sounds.Cut()
+ for(var/area/impacted_area as anything in impacted_areas)
+ GLOB.sand_storm_sounds[impacted_area] = /datum/looping_sound/weak_outside_ashstorm
+ return ..()
+
+/datum/weather/sand_storm/weather_act_mob(mob/living/victim)
+ victim.adjustBruteLoss(5, required_bodytype = BODYTYPE_ORGANIC)
+ return ..()
+
+/datum/weather/sand_storm/harmless
+ name = "sandfall"
+ desc = "A passing sandstorm blankets the area in sand."
+
+ telegraph_message = span_danger("The wind begins to intensify, blowing sand up from the ground...")
+ telegraph_overlay = "dust_low"
+ telegraph_sound = null
+
+ weather_message = span_notice("Gentle sand wafts down around you like grotesque snow. The storm seems to have passed you by...")
+ weather_overlay = "dust_med"
+
+ end_message = span_notice("The sandfall slows, stops. Another layer of sand on the mesa beneath your feet.")
+ end_overlay = "dust_low"
+
+ probability = 10
+ weather_flags = parent_type::weather_flags & ~WEATHER_MOBS
diff --git a/code/datums/weather/weather_types/snow_storm.dm b/code/datums/weather/weather_types/snow_storm.dm
index 92551db3043..01a07df6133 100644
--- a/code/datums/weather/weather_types/snow_storm.dm
+++ b/code/datums/weather/weather_types/snow_storm.dm
@@ -21,20 +21,14 @@
end_sound_vol = /datum/looping_sound/snowstorm::volume + 10
area_type = /area
- protect_indoors = TRUE
target_trait = ZTRAIT_SNOWSTORM
immunity_type = TRAIT_SNOWSTORM_IMMUNE
- barometer_predictable = TRUE
-
- ///Lowest we can cool someone randomly per weather act. Positive values only
- var/cooling_lower = 5
- ///Highest we can cool someone randomly per weather act. Positive values only
- var/cooling_upper = 15
-
-/datum/weather/snow_storm/weather_act(mob/living/living)
- living.adjust_bodytemperature(-rand(cooling_lower, cooling_upper))
+ // snowstorms should be colder than default icebox atmos
+ weather_temperature = ICEBOX_MIN_TEMPERATURE - 40
+ // snowstorms temperature ignores any clothing insulation
+ weather_flags = (WEATHER_MOBS | WEATHER_BAROMETER | WEATHER_TEMPERATURE_BYPASS_CLOTHING)
/datum/weather/snow_storm/start()
GLOB.snowstorm_sounds.Cut() // it's passed by ref
@@ -72,9 +66,7 @@
///A storm that doesn't stop storming, and is a bit stronger
/datum/weather/snow_storm/forever_storm
telegraph_duration = 0 SECONDS
- perpetual = TRUE
+ weather_flags = parent_type::weather_flags | WEATHER_ENDLESS
probability = 0
-
- cooling_lower = 5
- cooling_upper = 18
+ weather_temperature = parent_type::weather_temperature - 40 // faster cooling effects at lower temps
diff --git a/code/datums/weather/weather_types/void_storm.dm b/code/datums/weather/weather_types/void_storm.dm
index eb43198e191..fd68b818152 100644
--- a/code/datums/weather/weather_types/void_storm.dm
+++ b/code/datums/weather/weather_types/void_storm.dm
@@ -16,8 +16,6 @@
end_duration = 10 SECONDS
area_type = /area
- protect_indoors = FALSE
target_trait = ZTRAIT_VOIDSTORM
- barometer_predictable = FALSE
- perpetual = TRUE
+ weather_flags = (WEATHER_INDOORS | WEATHER_BAROMETER | WEATHER_ENDLESS)
diff --git a/code/game/objects/items/devices/scanners/gas_analyzer.dm b/code/game/objects/items/devices/scanners/gas_analyzer.dm
index 1c389739700..161d9b9a88e 100644
--- a/code/game/objects/items/devices/scanners/gas_analyzer.dm
+++ b/code/game/objects/items/devices/scanners/gas_analyzer.dm
@@ -80,7 +80,7 @@
for(var/V in SSweather.processing)
var/datum/weather/W = V
- if(W.barometer_predictable && (T.z in W.impacted_z_levels) && W.area_type == user_area.type && !(W.stage == END_STAGE))
+ if((W.weather_flags & WEATHER_BAROMETER) && (T.z in W.impacted_z_levels) && W.area_type == user_area.type && !(W.stage == END_STAGE))
ongoing_weather = W
break
@@ -90,7 +90,7 @@
return CLICK_ACTION_BLOCKING
to_chat(user, span_notice("The next [ongoing_weather] will hit in [butchertime(ongoing_weather.next_hit_time - world.time)]."))
- if(ongoing_weather.aesthetic)
+ if(!(ongoing_weather.weather_flags & FUNCTIONAL_WEATHER))
to_chat(user, span_warning("[src]'s barometer function says that the next storm will breeze on by."))
else
var/next_hit = SSweather.next_hit_by_zlevel["[T.z]"]
diff --git a/code/modules/admin/verbs/adminevents.dm b/code/modules/admin/verbs/adminevents.dm
index 649ae3e3a34..b3af08d8889 100644
--- a/code/modules/admin/verbs/adminevents.dm
+++ b/code/modules/admin/verbs/adminevents.dm
@@ -152,22 +152,6 @@ ADMIN_VERB(change_sec_level, R_ADMIN, "Set Security Level", "Changes the securit
message_admins("[key_name_admin(user)] changed the security level to [level]")
BLACKBOX_LOG_ADMIN_VERB("Set Security Level [capitalize(level)]")
-ADMIN_VERB(run_weather, R_FUN, "Run Weather", "Triggers specific weather on the z-level you choose.", ADMIN_CATEGORY_EVENTS)
- var/weather_type = input(user, "Choose a weather", "Weather") as null|anything in sort_list(subtypesof(/datum/weather), GLOBAL_PROC_REF(cmp_typepaths_asc))
- if(!weather_type)
- return
-
- var/turf/T = get_turf(user.mob)
- var/z_level = input(user, "Z-Level to target?", "Z-Level", T?.z) as num|null
- if(!isnum(z_level))
- return
-
- SSweather.run_weather(weather_type, z_level)
-
- message_admins("[key_name_admin(user)] started weather of type [weather_type] on the z-level [z_level].")
- log_admin("[key_name(user)] started weather of type [weather_type] on the z-level [z_level].")
- BLACKBOX_LOG_ADMIN_VERB("Run Weather")
-
ADMIN_VERB(command_report_footnote, R_ADMIN, "Command Report Footnote", "Adds a footnote to the roundstart command report.", ADMIN_CATEGORY_EVENTS)
var/datum/command_footnote/command_report_footnote = new /datum/command_footnote()
GLOB.communications_controller.block_command_report += 1 //Add a blocking condition to the counter until the inputs are done.
diff --git a/code/modules/admin/verbs/adminweather.dm b/code/modules/admin/verbs/adminweather.dm
new file mode 100644
index 00000000000..e5c77625b80
--- /dev/null
+++ b/code/modules/admin/verbs/adminweather.dm
@@ -0,0 +1,93 @@
+ADMIN_VERB(run_weather, R_ADMIN|R_FUN, "Run Weather", "Triggers specific weather on the z-level you choose.", ADMIN_CATEGORY_EVENTS)
+
+ var/list/weather_choices = list()
+ if(!length(weather_choices))
+ for(var/datum/weather/weather_type as anything in subtypesof(/datum/weather))
+ weather_choices[initial(weather_type.type)] = weather_type
+
+ var/datum/weather/weather_choice = tgui_input_list(user, "Choose a weather to run", "Weather", weather_choices)
+ if(!weather_choice)
+ return
+ weather_choice = weather_choices[weather_choice]
+
+ var/turf/current_turf = get_turf(user.mob)
+ var/z_level = tgui_input_number(user, "Z-Level to target", "Z-Level", min_value = 1, max_value = world.maxz, default = current_turf?.z)
+ if(!isnum(z_level))
+ return
+
+ var/static/list/custom_options = list("Default", "Custom", "Cancel")
+ var/custom_choice = tgui_alert(user, "How would you like to run the weather settings?", "Custom Weather", custom_options)
+ switch(custom_choice)
+ if("Default")
+ SSweather.run_weather(weather_choice, z_level) // default settings
+ message_admins("[key_name_admin(user)] started weather of type [weather_choice] on the z-level [z_level].")
+ log_admin("[key_name(user)] started weather of type [weather_choice] on the z-level [z_level].")
+ BLACKBOX_LOG_ADMIN_VERB("Run Weather")
+ return
+ if("Cancel")
+ return
+
+ var/list/area_choices = list()
+ if(!length(area_choices))
+ for(var/area/area_type as anything in typesof(/area))
+ area_choices[initial(area_type.type)] = area_type
+
+ var/area/area_choice = tgui_input_list(user, "Select an area for weather to target", "Target Area", area_choices)
+ if(!area_choice)
+ return
+ area_choice = area_choices[area_choice]
+
+ var/weather_bitflags = input_bitfield(
+ user,
+ "Weather flags - Select the flags for your weather event",
+ "weather_flags",
+ weather_choice::weather_flags,
+ )
+
+ var/datum/reagent/reagent_choice
+ if((weather_bitflags & (WEATHER_TURFS|WEATHER_MOBS)))
+ var/static/list/reagent_options = list("Yes", "No", "Cancel")
+ var/reagent_option = tgui_alert(user, "Would you like to make the weather use a custom reagent?", "Weather Reagent", reagent_options)
+ switch(reagent_option)
+ if("Cancel")
+ return
+ if("Yes")
+ var/static/list/reagent_choices = list()
+ if(!length(reagent_choices))
+ for(var/datum/reagent/reagent_type as anything in subtypesof(/datum/reagent))
+ reagent_choices[initial(reagent_type.type)] = reagent_type
+
+ reagent_choice = tgui_input_list(user, "Select a reagent for the rain", "Rain Reagent", reagent_choices)
+ if(!reagent_choice)
+ return
+ reagent_choice = reagent_choices[reagent_choice]
+
+ var/thunder_value
+ if(weather_bitflags & (WEATHER_THUNDER))
+ var/static/list/thunder_choices = GLOB.thunder_chance_options
+
+ var/thunder_choice = tgui_input_list(user, "How much thunder would you like", "Thunder", thunder_choices)
+ if(!thunder_choice)
+ return
+ thunder_value = GLOB.thunder_chance_options[thunder_choice]
+
+ var/list/weather_data = list(
+ area = area_choice,
+ weather_flags = weather_bitflags,
+ thunder_chance = thunder_value,
+ reagent = reagent_choice,
+ )
+
+ SSweather.run_weather(weather_choice, z_level, weather_data)
+
+ message_admins("[key_name_admin(user)] started weather of type [weather_choice] on the z-level [z_level].")
+ log_admin("[key_name(user)] started weather of type [weather_choice] on the z-level [z_level].")
+ BLACKBOX_LOG_ADMIN_VERB("Run Weather")
+
+ADMIN_VERB(stop_weather, R_ADMIN|R_DEBUG, "Stop All Active Weather", "Stop all currently active weather.", ADMIN_CATEGORY_EVENTS)
+ log_admin("[key_name(user)] stopped all currently active weather.")
+ message_admins("[key_name_admin(user)] stopped all currently active weather.")
+ for(var/datum/weather/current_weather as anything in SSweather.processing)
+ if(current_weather in SSweather.processing)
+ current_weather.end()
+ BLACKBOX_LOG_ADMIN_VERB("Stop All Active Weather")
diff --git a/code/modules/hydroponics/hydroponics_chemreact.dm b/code/modules/hydroponics/hydroponics_chemreact.dm
index c05e0ab0ff4..0af7c258f8f 100644
--- a/code/modules/hydroponics/hydroponics_chemreact.dm
+++ b/code/modules/hydroponics/hydroponics_chemreact.dm
@@ -11,6 +11,21 @@
continue
chem.on_hydroponics_apply(src, user)
+/obj/machinery/hydroponics/expose_reagents(list/reagents, datum/reagents/source, methods = TOUCH, volume_modifier = 1, show_message = TRUE)
+ . = ..()
+ if(. & COMPONENT_NO_EXPOSE_REAGENTS)
+ return
+
+ if(src.reagents.holder_full())
+ return
+
+ for(var/datum/reagent/reagent as anything in reagents)
+ if(istype(reagent, /datum/reagent/water))
+ adjust_waterlevel(round(reagents[reagent]))
+ else
+ src.reagents.add_reagent(reagent.type, reagents[reagent])
+ update_appearance()
+
/obj/machinery/hydroponics/proc/mutation_roll(mob/user)
switch(rand(100))
if(91 to 100)
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index cd661869463..46772ecda51 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -132,6 +132,8 @@
client.init_verbs()
AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds)
+ AddElement(/datum/element/weather_listener, /datum/weather/rain_storm, ZTRAIT_RAINSTORM, GLOB.rain_storm_sounds)
+ AddElement(/datum/element/weather_listener, /datum/weather/sand_storm, ZTRAIT_SANDSTORM, GLOB.sand_storm_sounds)
AddElement(/datum/element/weather_listener, /datum/weather/snow_storm, ZTRAIT_SNOWSTORM, GLOB.snowstorm_sounds)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_MOB_LOGGED_IN, src)
diff --git a/icons/effects/weather_effects.dmi b/icons/effects/weather_effects.dmi
index fa21b2d12eb..1b21f852dd7 100644
Binary files a/icons/effects/weather_effects.dmi and b/icons/effects/weather_effects.dmi differ
diff --git a/sound/ambience/weather/rain/attribution.txt b/sound/ambience/weather/rain/attribution.txt
new file mode 100644
index 00000000000..7a3c1cc76f9
--- /dev/null
+++ b/sound/ambience/weather/rain/attribution.txt
@@ -0,0 +1,7 @@
+These sounds were previously on tg but got deleted at one point and have now been added again. See - https://github.com/tgstation/tgstation/pull/25222#discussion_r106794579
+
+Sounds provided by Cuboos, using Royalty Free sounds. Presumed under default tg sound license - Creative Commons 3.0 BY-SA
+
+sound/effects/weather/rain/rain_end.ogg
+sound/effects/weather/rain/rain_mid.ogg
+sound/effects/weather/rain/rain_start.ogg
diff --git a/sound/ambience/weather/rain/rain_end.ogg b/sound/ambience/weather/rain/rain_end.ogg
new file mode 100644
index 00000000000..d02299c6c0d
Binary files /dev/null and b/sound/ambience/weather/rain/rain_end.ogg differ
diff --git a/sound/ambience/weather/rain/rain_mid.ogg b/sound/ambience/weather/rain/rain_mid.ogg
new file mode 100644
index 00000000000..13dd3a59cb8
Binary files /dev/null and b/sound/ambience/weather/rain/rain_mid.ogg differ
diff --git a/sound/ambience/weather/rain/rain_start.ogg b/sound/ambience/weather/rain/rain_start.ogg
new file mode 100644
index 00000000000..27e47372944
Binary files /dev/null and b/sound/ambience/weather/rain/rain_start.ogg differ
diff --git a/sound/attributions.txt b/sound/attributions.txt
index dd1da1897a1..2a08140df95 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -229,3 +229,6 @@ sound/items/weapons/crystal_dagger_sound.ogg -- https://pixabay.com/sound-effect
'sound/items/weapons/effects/blood_wisp_explode.ogg' -- https://pixabay.com/sound-effects/fire-sound-effects-224089/ by Alice-soundz
sound/effect/plasticflaps.ogg -- door_plastic_tapes.wav by estupe -- https://freesound.org/s/186357/ -- License: Creative Commons 0
+
+
+sound/effects/siren.ogg -- siren.wav by IFartInUrGeneralDirection -- https://freesound.org/s/46092/ -- License: Attribution 4.0
diff --git a/sound/effects/siren.ogg b/sound/effects/siren.ogg
new file mode 100644
index 00000000000..b5cabfadced
Binary files /dev/null and b/sound/effects/siren.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index 5fb6f9d4bf2..e135f6765a0 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2144,6 +2144,8 @@
#include "code\datums\weather\weather_types\ash_storm.dm"
#include "code\datums\weather\weather_types\floor_is_lava.dm"
#include "code\datums\weather\weather_types\radiation_storm.dm"
+#include "code\datums\weather\weather_types\rain_storm.dm"
+#include "code\datums\weather\weather_types\sand_storm.dm"
#include "code\datums\weather\weather_types\snow_storm.dm"
#include "code\datums\weather\weather_types\void_storm.dm"
#include "code\datums\wires\_wires.dm"
@@ -3167,6 +3169,7 @@
#include "code\modules\admin\verbs\adminpm.dm"
#include "code\modules\admin\verbs\adminsay.dm"
#include "code\modules\admin\verbs\adminshuttle.dm"
+#include "code\modules\admin\verbs\adminweather.dm"
#include "code\modules\admin\verbs\ai_triumvirate.dm"
#include "code\modules\admin\verbs\anonymousnames.dm"
#include "code\modules\admin\verbs\atmosdebug.dm"