diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 4d368ccc670..6bc5b96e0e0 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -13,6 +13,9 @@ #define PLANE_SPACE -21 #define PLANE_SPACE_PARALLAX -20 +#define WEATHER_MASK_PLANE -13 +#define WEATHER_MASK_RENDER_TARGET "*WEATHER_MASK_RENDER_TARGET" + #define DISPLACEMENT_PLANE -12 #define DISPLACEMENT_RENDER_TARGET "*DISPLACEMENT_RENDER_TARGET" @@ -65,76 +68,79 @@ #define RENDER_PLANE_SPECULAR_MASK 17 #define SPECULAR_MASK_RENDER_TARGET "*RENDER_PLANE_SPECULAR_MASK" +#define RENDER_PLANE_PARTICLE_WEATHER 18 +#define RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER 19 + //-------------------- Lighting --------------------- /// Main game plane to which everything renders, which then is multiplied by light /// Should not be lit directly as it is sourced for emissive bloom -#define RENDER_PLANE_UNLIT_GAME 18 +#define RENDER_PLANE_UNLIT_GAME 20 -#define RENDER_PLANE_O_LIGHTING 19 +#define RENDER_PLANE_O_LIGHTING 21 -#define RENDER_PLANE_LIGHTING 20 +#define RENDER_PLANE_LIGHTING 22 /// Masks the lighting plane with turfs, so we never light up the void /// Failing that, masks emissives and the overlay lighting plane -#define RENDER_PLANE_LIGHT_MASK 21 +#define RENDER_PLANE_LIGHT_MASK 23 #define LIGHT_MASK_RENDER_TARGET "*RENDER_PLANE_LIGHT_MASK" /// We cannot render speculars to ABOVE_LIGHTING, as then they give it alpha and end up masking things in darkness /// So we need to render it directly to RENDER_PLANE_GAME above RENDER_PLANE_LIGHTING -#define RENDER_PLANE_SPECULAR 22 +#define RENDER_PLANE_SPECULAR 24 /// Things that should render ignoring lighting -#define ABOVE_LIGHTING_PLANE 23 +#define ABOVE_LIGHTING_PLANE 25 -#define WEATHER_GLOW_PLANE 24 +#define WEATHER_GLOW_PLANE 26 ///---------------- MISC ----------------------- ///Pipecrawling images -#define PIPECRAWL_IMAGES_PLANE 25 +#define PIPECRAWL_IMAGES_PLANE 30 ///AI Camera Static -#define CAMERA_STATIC_PLANE 26 +#define CAMERA_STATIC_PLANE 31 ///Anything that wants to be part of the game plane, but also wants to draw above literally everything else -#define HIGH_GAME_PLANE 27 +#define HIGH_GAME_PLANE 32 -#define FULLSCREEN_PLANE 28 +#define FULLSCREEN_PLANE 33 ///--------------- FULLSCREEN RUNECHAT BUBBLES ------------ ///Popup Chat Messages -#define RUNECHAT_PLANE 30 +#define RUNECHAT_PLANE 34 /// Plane for balloon text (text that fades up) -#define BALLOON_CHAT_PLANE 31 +#define BALLOON_CHAT_PLANE 35 //-------------------- HUD --------------------- //HUD layer defines -#define HUD_PLANE 35 -#define ABOVE_HUD_PLANE 36 +#define HUD_PLANE 40 +#define ABOVE_HUD_PLANE 41 ///Plane of the "splash" icon used that shows on the lobby screen -#define SPLASHSCREEN_PLANE 37 +#define SPLASHSCREEN_PLANE 42 // The largest plane here must still be less than RENDER_PLANE_GAME //-------------------- Rendering --------------------- -#define RENDER_PLANE_GAME 40 +#define RENDER_PLANE_GAME 50 /// If fov is enabled we'll draw game to this and do shit to it -#define RENDER_PLANE_GAME_MASKED 41 +#define RENDER_PLANE_GAME_MASKED 51 /// The bit of the game plane that is let alone is sent here -#define RENDER_PLANE_GAME_UNMASKED 42 +#define RENDER_PLANE_GAME_UNMASKED 52 -#define RENDER_PLANE_NON_GAME 45 +#define RENDER_PLANE_NON_GAME 55 // Only VERY special planes should be here, as they are above not just the game, but the UI planes as well. /// Plane related to the menu when pressing Escape. /// Needed so that we can apply a blur effect to EVERYTHING, and guarantee we are above all UI. -#define ESCAPE_MENU_PLANE 46 +#define ESCAPE_MENU_PLANE 56 -#define RENDER_PLANE_MASTER 50 +#define RENDER_PLANE_MASTER 57 // Lummox I swear to god I will find you // NOTE! You can only ever have planes greater then -10000, if you add too many with large offsets you will brick multiz diff --git a/code/__DEFINES/weather.dm b/code/__DEFINES/weather.dm index 0bd041a497c..ae2f50401e0 100644 --- a/code/__DEFINES/weather.dm +++ b/code/__DEFINES/weather.dm @@ -67,3 +67,6 @@ GLOBAL_LIST_INIT(thunder_chance_options, list( #define WEATHER_FORCED_TELEGRAPH "Telegraph Duration" #define WEATHER_FORCED_END "End Duration" #define WEATHER_FORCED_DURATION "Weather Duration" + +/// Maximum possible weather severity value +#define MAXIMUM_WEATHER_SEVERITY 100 diff --git a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm index 6cfafcd6797..094371cc9e6 100644 --- a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm +++ b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm @@ -328,6 +328,22 @@ return home.AddComponent(/datum/component/hide_weather_planes, src) +/atom/movable/screen/plane_master/weather_mask + name = "Weather Mask" + documentation = "Used to mask particle weather effects to cut out areas unaffected by weather." + plane = WEATHER_MASK_PLANE + appearance_flags = PLANE_MASTER | NO_CLIENT_COLOR + render_target = WEATHER_MASK_RENDER_TARGET + render_relay_planes = list() + start_hidden = TRUE + critical = PLANE_CRITICAL_DISPLAY + +/atom/movable/screen/plane_master/weather_mask/set_home(datum/plane_master_group/home) + . = ..() + if(!.) + return + home.AddComponent(/datum/component/hide_weather_planes, src, TRUE) + /atom/movable/screen/plane_master/massive_obj name = "Massive object" documentation = "Huge objects need to render above everything else on the game plane, otherwise they'd well, get clipped and look not that huge. This does that." diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm index 9d42784bb2a..11979dde2ac 100644 --- a/code/_onclick/hud/rendering/render_plate.dm +++ b/code/_onclick/hud/rendering/render_plate.dm @@ -72,6 +72,45 @@ . = ..() add_relay_to(GET_NEW_PLANE(RENDER_PLANE_EMISSIVE_BLOOM, offset), blend_override = BLEND_MULTIPLY) +/atom/movable/screen/plane_master/rendering_plate/particle_weather + name = "Particle Weather" + documentation = "Plane used to render particle weather, masked by WEATHER_MASK_PLANE. \ + Cannot be a single screen object as it needs to be a planemaster in order to be properly masked by the weather mask." + plane = RENDER_PLANE_PARTICLE_WEATHER + start_hidden = TRUE + critical = PLANE_CRITICAL_DISPLAY + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/Initialize(mapload, datum/hud/hud_owner, datum/plane_master_group/home, offset) + . = ..() + // We can actually do this just fine as we do not render anything onto ourselves but our particles + add_filter("weather_mask", 1, alpha_mask_filter(render_source = OFFSET_RENDER_TARGET(WEATHER_MASK_RENDER_TARGET, offset))) + SSweather.particle_planemasters += src + // And add all ongoing weather to ourselves + for (var/holder_offset, holder_list in SSweather.particle_holders) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + if (holder.plane == plane) + vis_contents += holder + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/Destroy() + SSweather.particle_planemasters -= src + return ..() + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/set_home(datum/plane_master_group/home) + . = ..() + if(!.) + return + home.AddComponent(/datum/component/hide_weather_planes, src, TRUE) + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/emissive + name = "Emissive Particle Weather" + documentation = "Secondary particle weather plane for emissive parts of weather, which is additionally rendered onto the emissive plane after being masked." + plane = RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER + +/atom/movable/screen/plane_master/rendering_plate/particle_weather/emissive/Initialize(mapload, datum/hud/hud_owner, datum/plane_master_group/home, offset) + . = ..() + // Render a copy of ourselves onto the emissive plane encoded into the bloom channel + add_relay_to(GET_NEW_PLANE(EMISSIVE_PLANE, offset), relay_color = GLOB.emissive_color) + /atom/movable/screen/plane_master/rendering_plate/turf_lighting name = "Turf lighting post-processing plate" documentation = "Used by overlay lighting, and possibly over plates, to mask out turf lighting." diff --git a/code/controllers/subsystem/weather.dm b/code/controllers/subsystem/weather.dm index 7a89236e08a..d50013bacee 100644 --- a/code/controllers/subsystem/weather.dm +++ b/code/controllers/subsystem/weather.dm @@ -8,13 +8,26 @@ SUBSYSTEM_DEF(weather) wait = 10 runlevels = RUNLEVEL_GAME var/list/processing = list() + /// Z levels on which weather can occur -> weather that can occur -> probability of said weather occuring var/list/eligible_zlevels = list() - var/list/next_hit_by_zlevel = list() //Used by barometers to know when the next storm is coming + /// Used by barometers to know when the next storm is coming + var/list/next_hit_by_zlevel = list() + /// Alist of all particle holders per Z-stack offset for particle weather to be shown to clients + var/alist/particle_holders = alist() + /// List of all RENDER_PLANE_PARTICLE_WEATHER and RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER planes + var/list/particle_planemasters = list() /datum/controller/subsystem/weather/fire(resumed = FALSE) // process active weather for(var/datum/weather/weather_event as anything in processing) - if(!length(weather_event.subsystem_tasks) || weather_event.stage != MAIN_STAGE) + if(!length(weather_event.subsystem_tasks)) + continue + + if(istype(weather_event, /datum/weather/particle)) + var/datum/weather/particle/particle_event = weather_event + particle_event.process_particles() + + if(weather_event.stage != MAIN_STAGE) continue if(weather_event.subsystem_tasks[weather_event.task_index] == SSWEATHER_MOBS) @@ -69,21 +82,42 @@ SUBSYSTEM_DEF(weather) 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)) - var/datum/weather/W = V - var/probability = initial(W.probability) - var/target_trait = initial(W.target_trait) + for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather)) + var/probability = initial(weather.probability) + var/target_trait = initial(weather.target_trait) // any weather with a probability set may occur at random if (probability) for(var/z in SSmapping.levels_by_trait(target_trait)) LAZYINITLIST(eligible_zlevels["[z]"]) - eligible_zlevels["[z]"][W] = probability + eligible_zlevels["[z]"][weather] = probability return SS_INIT_SUCCESS +/datum/controller/subsystem/weather/proc/add_weather_objects(list/new_holders, z_level) + for (var/offset in 1 to length(new_holders)) + var/list/holder_list = new_holders[offset] + if (isnull(particle_holders[offset])) + particle_holders[offset] = list() + particle_holders[offset] += holder_list + + // We add it to vis_contents of planemasters rather than client screen as planemasters already + // manage their own visibility based on owner's z level + for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + if (holder.plane == plane_master.plane) + plane_master.vis_contents |= holder + +/datum/controller/subsystem/weather/proc/remove_weather_objects(list/old_holders) + for (var/offset in 1 to length(old_holders)) + var/list/holder_list = old_holders[offset] + particle_holders[offset] -= holder_list + + for (var/atom/movable/screen/plane_master/plane_master as anything in particle_planemasters) + plane_master.vis_contents -= holder_list + /datum/controller/subsystem/weather/proc/update_z_level(datum/space_level/level) var/z = level.z_value - for(var/datum/weather/weather as anything in subtypesof(/datum/weather)) + for(var/datum/weather/weather as anything in valid_subtypesof(/datum/weather)) var/probability = initial(weather.probability) var/target_trait = initial(weather.target_trait) if(probability && level.traits[target_trait]) @@ -92,10 +126,9 @@ SUBSYSTEM_DEF(weather) /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 - if (initial(W.name) == weather_datum_type) - weather_datum_type = V + for (var/datum/weather/weather as anything in valid_subtypesof(/datum/weather)) + if (initial(weather.name) == weather_datum_type) + weather_datum_type = weather break if (!ispath(weather_datum_type, /datum/weather)) CRASH("run_weather called with invalid weather_datum_type: [weather_datum_type || "null"]") @@ -107,10 +140,9 @@ 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, weather_data) - W.telegraph(weather_data) - return W + var/datum/weather/weather = new weather_datum_type(z_levels, weather_data) + weather.telegraph(weather_data) + return weather /datum/controller/subsystem/weather/proc/make_eligible(z, possible_weather) eligible_zlevels[z] = possible_weather diff --git a/code/datums/components/ai_listen_to_weather.dm b/code/datums/components/ai_listen_to_weather.dm index a7bb95ee8c1..29bcda3839b 100644 --- a/code/datums/components/ai_listen_to_weather.dm +++ b/code/datums/components/ai_listen_to_weather.dm @@ -7,7 +7,7 @@ ///what blackboard key are we setting var/weather_key -/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/ash_storm, weather_key = BB_STORM_APPROACHING) +/datum/component/ai_listen_to_weather/Initialize(weather_type = /datum/weather/particle/ash_storm, weather_key = BB_STORM_APPROACHING) if(!isliving(parent)) return COMPONENT_INCOMPATIBLE src.weather_type = weather_type diff --git a/code/datums/components/hide_weather_planes.dm b/code/datums/components/hide_weather_planes.dm index 5b7addb199e..7a7cb3655b8 100644 --- a/code/datums/components/hide_weather_planes.dm +++ b/code/datums/components/hide_weather_planes.dm @@ -8,17 +8,20 @@ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS var/list/datum/weather/active_weather = list() var/list/atom/movable/screen/plane_master/plane_masters = list() + /// Do we care about all weather or only particle weather? + var/particle_only = FALSE -/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about) +/datum/component/hide_weather_planes/Initialize(atom/movable/screen/plane_master/care_about, particle_only = FALSE) if(!istype(parent, /datum/plane_master_group)) return COMPONENT_INCOMPATIBLE + src.particle_only = particle_only var/datum/plane_master_group/home = parent plane_masters += care_about RegisterSignal(care_about, COMSIG_QDELETING, PROC_REF(plane_master_deleted)) var/list/starting_signals = list() var/list/ending_signals = list() - for(var/datum/weather/weather_type as anything in typesof(/datum/weather)) + for(var/datum/weather/weather_type as anything in valid_subtypesof(particle_only ? /datum/weather/particle : /datum/weather)) starting_signals += COMSIG_WEATHER_TELEGRAPH(weather_type) ending_signals += COMSIG_WEATHER_END(weather_type) @@ -103,6 +106,8 @@ var/list/connected_levels = SSmapping.get_connected_levels(new_z) for(var/datum/weather/active as anything in SSweather.processing) + if(particle_only && !istype(active, /datum/weather/particle)) + continue if(length(connected_levels & active.impacted_z_levels)) active_weather += WEAKREF(active) diff --git a/code/datums/components/object_possession.dm b/code/datums/components/object_possession.dm index 256f2952670..f13acee56c2 100644 --- a/code/datums/components/object_possession.dm +++ b/code/datums/components/object_possession.dm @@ -69,9 +69,9 @@ user.name = target.name user.reset_perspective(target) - target.AddElement(/datum/element/weather_listener, /datum/weather/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + target.AddElement(/datum/element/weather_listener, /datum/weather/particle/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/particle/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)) @@ -84,8 +84,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/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + possessed.RemoveElement(/datum/element/weather_listener, /datum/weather/particle/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/storm_hating.dm b/code/datums/components/storm_hating.dm index 62a59a0ad71..0b4edbe84e7 100644 --- a/code/datums/components/storm_hating.dm +++ b/code/datums/components/storm_hating.dm @@ -4,7 +4,7 @@ /datum/component/storm_hating /// Types of weather which trigger the effect var/static/list/stormy_weather = list( - /datum/weather/ash_storm, + /datum/weather/particle/ash_storm, /datum/weather/snow_storm, /datum/weather/void_storm, ) diff --git a/code/datums/weather/particle_weather.dm b/code/datums/weather/particle_weather.dm new file mode 100644 index 00000000000..b8e1a5ac3fe --- /dev/null +++ b/code/datums/weather/particle_weather.dm @@ -0,0 +1,145 @@ +/// Subtype which uses particle systems instead of overlays to display its effects +/datum/weather/particle + abstract_type = /datum/weather/particle + /// Particles used to display the weather visuals + var/particles/weather/particle_type = null + /// Secondary (layered ontop) particle type which is rendered as emissive in case you want embers or whatever + var/particles/weather/emissive_type = null + /// Minimum possible severity for the weather, (0~100] + var/min_severity = 1 + /// Maximum possible severity for the weather, (0~100] + var/max_severity = MAXIMUM_WEATHER_SEVERITY + /// Maximum variation in severity each weather frame + var/severity_variation = 5 + /// Optimal point towards which severity will try to gravitate by influencing random values + var/optimal_severity = 70 + /// How often can we change our severity? + /// Don't set this too low or it'll look jank + var/severity_cooldown = 5 SECONDS + + /// Current weather severity + var/severity = 0 + /// Last tick during which we've changed our visual severity + var/last_severity_tick = 0 + /// List of weather object lists (as we can have multiple if emissives are involved) by plane offset + var/list/weather_objects = list() + /// Direction of our wind + var/wind_sign = 0 + +/datum/weather/particle/New(z_levels, list/weather_data) + . = ..() + if (isnull(particle_type) && isnull(emissive_type)) + CRASH("[src] ([type]) attempted to initialize without normal or emissive particle types!") + + for (var/offset in 0 to SSmapping.max_plane_offset) + var/list/object_list = list() + if (particle_type) + var/obj/effect/abstract/weather_holder/holder = new() + SET_PLANE_W_SCALAR(holder, RENDER_PLANE_PARTICLE_WEATHER, offset) + holder.particles = new particle_type() + object_list += holder + + if (emissive_type) + var/obj/effect/abstract/weather_holder/holder = new() + holder.particles = new emissive_type() + SET_PLANE_W_SCALAR(holder, RENDER_PLANE_EMISSIVE_PARTICLE_WEATHER, offset) + object_list += holder + + weather_objects += list(object_list) + + SSweather.add_weather_objects(weather_objects) + +/datum/weather/particle/Destroy() + SSweather.remove_weather_objects(weather_objects) + for(var/list/object_list as anything in weather_objects) + QDEL_LIST(object_list) + weather_objects = null + return ..() + +/datum/weather/particle/telegraph(list/weather_data) + . = ..() + if (!.) + return + animate_severity(0) + +/datum/weather/particle/end() + . = ..() + if (!.) + return + animate_severity(0) + +/// Adjust our severity by a random number based on our stage +/datum/weather/particle/proc/process_particles() + if (last_severity_tick + severity_cooldown > world.time) + return + + last_severity_tick = world.time + var/new_severity = severity + switch (stage) + if (STARTUP_STAGE) + // Aims at minimum severity, and can only go up + new_severity += rand() * severity_variation * (1 - severity / min_severity) + if (MAIN_STAGE) + // Tries to stay close to optimal severity + new_severity += LERP(-severity_variation * clamp(INVERSE_LERP(min_severity, optimal_severity, severity), 0, 1), severity_variation * clamp(INVERSE_LERP(max_severity, optimal_severity, severity), 0, 1), rand()) + new_severity = clamp(new_severity, min(new_severity, min_severity), max_severity) + if (WIND_DOWN_STAGE) + // Slowly goes down to zero + new_severity += rand() * -severity_variation * (severity / min_severity) + + animate_severity(new_severity) + +/datum/weather/particle/proc/animate_severity(new_severity) + if (!wind_sign) + wind_sign = pick(-1, 1) + + severity = new_severity + for (var/list/holder_list as anything in weather_objects) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + var/particles/weather/particle_effect = holder.particles + particle_effect.animate_severity(severity / MAXIMUM_WEATHER_SEVERITY, wind_sign) + +/datum/weather/particle/generate_overlay_cache() + . = ..() + for (var/offset in 0 to SSmapping.max_plane_offset) + . += mutable_appearance('icons/effects/weather_overlay.dmi', "weather_overlay", overlay_layer, null, WEATHER_MASK_PLANE, offset_const = offset) + +/obj/effect/abstract/weather_holder + icon = null + appearance_flags = TILE_BOUND | PIXEL_SCALE + blocks_emissive = EMISSIVE_BLOCK_NONE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + +/particles/weather + spawning = 0 + // Wider than our view due to weird BYOND jank + width = 800 + height = 800 + count = 8000 + + lifespan = 30 SECONDS + fade = 1 SECONDS + fadein = 0.5 SECONDS + + /// Increase in speed per tick + var/wind_strength = 0 + /// Minimum number of spawned particles per tick for easing + var/min_spawn = 0 + /// Maximum amount of spawned particles at full strength + var/max_spawn = 0 + +/// Changes the strength of the weather visual effect, severity should be between 0 and 1 +/particles/weather/proc/animate_severity(severity, wind_sign) + // Stop spawning if severity is zero or negative + if (severity <= 0) + spawning = 0 + return + + var/wind = wind_strength * severity * wind_sign + spawning = LERP(min_spawn, max_spawn, severity) + // Might already be set on init, in which case we preserve y and z components + if (islist(gravity) && length(gravity)) + gravity[1] = wind + else + gravity = list(wind) + diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm index 50fa90c79e2..de0d5cfc057 100644 --- a/code/datums/weather/weather.dm +++ b/code/datums/weather/weather.dm @@ -13,9 +13,10 @@ */ /datum/weather - /// name of weather + abstract_type = /datum/weather + /// Name of weather var/name = "space wind" - /// description of weather + /// Description of weather var/desc = "Heavy gusts of wind blanket the area, periodically knocking down anyone caught in the open." /// The message displayed in chat to foreshadow the weather's beginning var/telegraph_message = span_warning("The wind begins to pick up.") @@ -42,6 +43,8 @@ var/weather_overlay /// Color to apply to the area while weather is occuring var/weather_color = null + /// Alpha of the weather overlay + var/weather_alpha = 255 /// Displayed once the weather is over var/end_message = span_danger("The wind relents its assault.") @@ -326,7 +329,7 @@ */ /datum/weather/proc/start() if(stage >= MAIN_STAGE) - return + return FALSE SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_START(type), src) stage = MAIN_STAGE update_areas() @@ -335,6 +338,7 @@ addtimer(CALLBACK(src, PROC_REF(wind_down)), weather_duration, TIMER_UNIQUE) for(var/area/impacted_area as anything in impacted_areas) SEND_SIGNAL(impacted_area, COMSIG_WEATHER_BEGAN_IN_AREA(type), src) + return TRUE /** * Weather enters the winding down phase, stops effects @@ -345,12 +349,13 @@ */ /datum/weather/proc/wind_down() if(stage >= WIND_DOWN_STAGE) - return + return FALSE SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_WINDDOWN(type), src) stage = WIND_DOWN_STAGE update_areas() send_alert(end_message, end_sound, end_sound_vol) addtimer(CALLBACK(src, PROC_REF(end)), end_duration, TIMER_UNIQUE) + return TRUE /** * Fully ends the weather @@ -361,7 +366,7 @@ */ /datum/weather/proc/end() if(stage == END_STAGE) - return + return FALSE SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_END(type), src) UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_CREATED) stage = END_STAGE @@ -373,6 +378,7 @@ if(target_trait) for(var/mob/living/affected as anything in GLOB.mob_living_list | GLOB.dead_mob_list) UnregisterSignal(affected, COMSIG_MOB_LOGIN) + return TRUE // handles sending all alerts /datum/weather/proc/send_alert(alert_msg, alert_sfx, alert_sfx_vol = 100) @@ -576,11 +582,11 @@ // I prefer it to creating 2 extra plane masters however, so it's a cost I'm willing to pay // LU if(use_glow) - var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100, offset_const = offset) + var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', weather_state, overlay_layer, null, WEATHER_GLOW_PLANE, 100 / 255 * weather_alpha, offset_const = offset) glow_overlay.color = weather_color gen_overlay_cache += glow_overlay - var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, offset_const = offset) + var/mutable_appearance/new_weather_overlay = mutable_appearance('icons/effects/weather_effects.dmi', weather_state, overlay_layer, plane = overlay_plane, alpha = weather_alpha, offset_const = offset) new_weather_overlay.color = weather_color gen_overlay_cache += new_weather_overlay diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm index 367977a90bc..ae12115e367 100644 --- a/code/datums/weather/weather_types/ash_storm.dm +++ b/code/datums/weather/weather_types/ash_storm.dm @@ -1,8 +1,15 @@ -//Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside. -/datum/weather/ash_storm +/// Ash storms happen frequently on lavaland. They heavily obscure vision, and cause high fire damage to anyone caught outside. +/datum/weather/particle/ash_storm name = "ash storm" desc = "An intense atmospheric storm lifts ash off of the planet's surface and billows it down across the area, dealing intense fire damage to the unprotected." + particle_type = /particles/weather/ash_storm + emissive_type = /particles/weather/ash_storm/embers + min_severity = 60 + optimal_severity = 80 + weather_alpha = 100 + wind_sign = -1 // Always blows left to sync with the animated overlays + telegraph_message = span_boldwarning("An eerie moan rises on the wind. Sheets of burning ash blacken the horizon. Seek shelter.") telegraph_duration = 30 SECONDS telegraph_overlay = "light_ash" @@ -28,10 +35,51 @@ var/list/weak_sounds = list() var/list/strong_sounds = list() -/datum/weather/ash_storm/get_playlist_ref() +/particles/weather/ash_storm + icon = 'icons/effects/particles/smoke.dmi' + icon_state = list("chill_1" = 4, "chill_2" = 3, "chill_3" = 2) + position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0)) + grow = list(-0.01, -0.01) + gravity = list(0, -7, 0.5) + drift = generator(GEN_BOX, list(-1, -1, 0), list(1, 0, 0)) + friction = 0.3 + min_spawn = 20 + max_spawn = 400 + wind_strength = 16 + spin = generator(GEN_NUM, -5, 5, NORMAL_RAND) + /// Y-coordinate of our gravity + var/gravity_power = -12 + +/particles/weather/ash_storm/animate_severity(severity) + . = ..() + if (length(gravity) > 1) + gravity[2] = gravity_power * severity + else + gravity = list(gravity[1], gravity_power * severity, 0.5) + +/particles/weather/ash_storm/embers + icon = 'icons/effects/particles/generic.dmi' + icon_state = list("dot" = 4, "cross" = 2, "curl" = 1) + color = LIGHT_COLOR_FIRE + min_spawn = 10 + max_spawn = 80 + +/particles/weather/ash_storm/emberfall + min_spawn = 20 + max_spawn = 200 + wind_strength = 3 + gravity_power = -2 + +/particles/weather/ash_storm/embers/emberfall + min_spawn = 20 + max_spawn = 200 + wind_strength = 3 + gravity_power = -2 + +/datum/weather/particle/ash_storm/get_playlist_ref() return GLOB.ash_storm_sounds -/datum/weather/ash_storm/telegraph() +/datum/weather/particle/ash_storm/telegraph() for(var/area/impacted_area as anything in impacted_areas) if(impacted_area.outdoors) weak_sounds[impacted_area] = /datum/looping_sound/weak_outside_ashstorm @@ -45,17 +93,17 @@ GLOB.ash_storm_sounds += weak_sounds return ..() -/datum/weather/ash_storm/start() +/datum/weather/particle/ash_storm/start() GLOB.ash_storm_sounds -= weak_sounds GLOB.ash_storm_sounds += strong_sounds return ..() -/datum/weather/ash_storm/wind_down() +/datum/weather/particle/ash_storm/wind_down() GLOB.ash_storm_sounds -= strong_sounds GLOB.ash_storm_sounds += weak_sounds return ..() -/datum/weather/ash_storm/recursive_weather_protection_check(atom/to_check) +/datum/weather/particle/ash_storm/recursive_weather_protection_check(atom/to_check) . = ..() if(. || !ishuman(to_check)) return @@ -63,12 +111,13 @@ if(human_to_check.get_thermal_protection() >= FIRE_IMMUNITY_MAX_TEMP_PROTECT) return TRUE -/datum/weather/ash_storm/weather_act_mob(mob/living/victim) +/datum/weather/particle/ash_storm/weather_act_mob(mob/living/victim) victim.adjust_fire_loss(4, required_bodytype = BODYTYPE_ORGANIC) return ..() -/datum/weather/ash_storm/end() +/datum/weather/particle/ash_storm/end() GLOB.ash_storm_sounds -= weak_sounds + GLOB.ash_storm_sounds -= strong_sounds for(var/turf/open/misc/asteroid/basalt/basalt as anything in GLOB.dug_up_basalt) if(!(basalt.loc in impacted_areas) || !(basalt.z in impacted_z_levels)) continue @@ -76,7 +125,7 @@ return ..() //Emberfalls are the result of an ash storm passing by close to the playable area of lavaland. They have a 10% chance to trigger in place of an ash storm. -/datum/weather/ash_storm/emberfall +/datum/weather/particle/ash_storm/emberfall name = "emberfall" desc = "A passing ash storm blankets the area in harmless embers." @@ -89,3 +138,6 @@ weather_flags = parent_type::weather_flags & ~(WEATHER_MOBS|WEATHER_THUNDER) probability = 10 + + particle_type = /particles/weather/ash_storm/emberfall + emissive_type = /particles/weather/ash_storm/embers/emberfall diff --git a/code/datums/weather/weather_types/rain_storm.dm b/code/datums/weather/weather_types/rain_storm.dm index 4516112824c..2b2998fff72 100644 --- a/code/datums/weather/weather_types/rain_storm.dm +++ b/code/datums/weather/weather_types/rain_storm.dm @@ -1,16 +1,16 @@ -/datum/weather/rain_storm +/datum/weather/particle/rain_storm name = "rain" desc = "Heavy thunderstorms rain down below, drenching anyone caught in it." + particle_type = /particles/weather/rain_storm + min_severity = 30 + 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 @@ -27,10 +27,20 @@ weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_THUNDER | WEATHER_BAROMETER) whitelist_weather_reagents = list(/datum/reagent/water) -/datum/weather/rain_storm/get_playlist_ref() +/datum/weather/particle/rain_storm/New(z_levels, list/weather_data) + . = ..() + if (isnull(weather_reagent) || istype(weather_reagent, /datum/reagent/water) || !weather_color) + return + + // Non-water rain gets colored into their reagent's color + for (var/list/holder_list as anything in weather_objects) + for (var/obj/effect/abstract/weather_holder/holder as anything in holder_list) + holder.particles.color = weather_color + +/datum/weather/particle/rain_storm/get_playlist_ref() return GLOB.rain_storm_sounds -/datum/weather/rain_storm/telegraph() +/datum/weather/particle/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 @@ -43,39 +53,53 @@ return ..() -/datum/weather/rain_storm/start() +/datum/weather/particle/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() +/datum/weather/particle/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() +/datum/weather/particle/rain_storm/end() GLOB.rain_storm_sounds.Cut() return ..() -/datum/weather/rain_storm/blood +/particles/weather/rain_storm + icon = 'icons/effects/particles/generic.dmi' + icon_state = "drop" + color = "#ccffff" + position = generator(GEN_BOX, list(-510, -256, 0), list(400, 512, 0)) + grow = list(-0.01, -0.01) + gravity = list(0, -10, 0.5) + drift = generator(GEN_CIRCLE, 0, 1) + friction = 0.3 + min_spawn = 50 + max_spawn = 300 + wind_strength = 5 + spin = 0 + +/datum/weather/particle/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 +/datum/weather/particle/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 +/datum/weather/particle/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 +/datum/weather/particle/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 @@ -97,7 +121,7 @@ ) probability = 0 -/datum/weather/rain_storm/wizard +/datum/weather/particle/rain_storm/wizard name = "magical rain" desc = "A magical thunderstorm rains down below, drenching anyone caught in it with mysterious rain." @@ -116,7 +140,7 @@ probability = 0 // shouldn't spawn normally weather_flags = (WEATHER_TURFS | WEATHER_MOBS | WEATHER_INDOORS | WEATHER_BAROMETER) -/datum/weather/rain_storm/wizard/New(z_levels, list/weather_data) +/datum/weather/particle/rain_storm/wizard/New(z_levels, list/weather_data) if(length(GLOB.wizard_rain_reagents)) // the wizard event has already been run once and setup the whitelist whitelist_weather_reagents = GLOB.wizard_rain_reagents return ..() diff --git a/code/game/machinery/mining_weather_monitor.dm b/code/game/machinery/mining_weather_monitor.dm index c057b44f56c..402a40dd8f1 100644 --- a/code/game/machinery/mining_weather_monitor.dm +++ b/code/game/machinery/mining_weather_monitor.dm @@ -346,8 +346,8 @@ GLOBAL_LIST_EMPTY(weather_towers) /// Return a list of weather typepaths that this tower can summon when given a weather core. /obj/machinery/power/weather_tower/proc/get_summonable_weather_types() . = list( - /datum/weather/ash_storm, - /datum/weather/rain_storm, + /datum/weather/particle/ash_storm, + /datum/weather/particle/rain_storm, /datum/weather/sand_storm, /datum/weather/snow_storm, ) diff --git a/code/game/objects/effects/anomalies/anomalies_weather.dm b/code/game/objects/effects/anomalies/anomalies_weather.dm index ea407acfa3a..17e6f415af5 100644 --- a/code/game/objects/effects/anomalies/anomalies_weather.dm +++ b/code/game/objects/effects/anomalies/anomalies_weather.dm @@ -55,7 +55,7 @@ /obj/effect/anomaly/weather/proc/select_weather() return pick( - /datum/weather/rain_storm, + /datum/weather/particle/rain_storm, /datum/weather/snow_storm, /datum/weather/sand_storm, ) @@ -97,4 +97,4 @@ // maybe we can put acid rain in this later? // though it'd feel unfair if it showed up and immediately dumped acid on people. // we would need an even longer telegraphing time for that - weather_type = /datum/weather/rain_storm + weather_type = /datum/weather/particle/rain_storm diff --git a/code/game/objects/effects/particles/fire.dm b/code/game/objects/effects/particles/fire.dm index 481849c00eb..c35b1d64548 100644 --- a/code/game/objects/effects/particles/fire.dm +++ b/code/game/objects/effects/particles/fire.dm @@ -31,7 +31,7 @@ gradient = list("#FBAF4D", "#FCE6B6", "#FD481C") position = generator(GEN_BOX, list(-12,-16,0), list(12,16,0), NORMAL_RAND) drift = generator(GEN_VECTOR, list(-0.1,0), list(0.1,0.025), UNIFORM_RAND) - spin = generator(GEN_NUM, list(-15,15), NORMAL_RAND) + spin = generator(GEN_NUM, -15, 15, NORMAL_RAND) scale = generator(GEN_VECTOR, list(0.5,0.5), list(2,2), NORMAL_RAND) /particles/embers/minor diff --git a/code/game/objects/effects/particles/smoke.dm b/code/game/objects/effects/particles/smoke.dm index c5626ef646c..cd7766ccff7 100644 --- a/code/game/objects/effects/particles/smoke.dm +++ b/code/game/objects/effects/particles/smoke.dm @@ -128,7 +128,7 @@ position = generator(GEN_BOX, list(-17,-15,0), list(24,15,0), NORMAL_RAND) scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND) drift = generator(GEN_SPHERE, list(-0.01,0), list(0.01,0.01), UNIFORM_RAND) - spin = generator(GEN_NUM, list(-2,2), NORMAL_RAND) + spin = generator(GEN_NUM, -2, 2, NORMAL_RAND) gravity = list(0.05, 0.28) friction = 0.3 grow = 0.037 diff --git a/code/modules/admin/verbs/adminweather.dm b/code/modules/admin/verbs/adminweather.dm index 8721a1aabb2..290e52e5ee7 100644 --- a/code/modules/admin/verbs/adminweather.dm +++ b/code/modules/admin/verbs/adminweather.dm @@ -2,7 +2,7 @@ ADMIN_VERB(run_weather, R_ADMIN|R_FUN, "Run Weather", "Triggers specific weather var/list/weather_choices = list() if(!length(weather_choices)) - for(var/datum/weather/weather_type as anything in subtypesof(/datum/weather)) + for(var/datum/weather/weather_type as anything in valid_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) diff --git a/code/modules/antagonists/voidwalker/voidwalker_window.dm b/code/modules/antagonists/voidwalker/voidwalker_window.dm index b6dad0722da..c62dbf68c52 100644 --- a/code/modules/antagonists/voidwalker/voidwalker_window.dm +++ b/code/modules/antagonists/voidwalker/voidwalker_window.dm @@ -26,7 +26,7 @@ velocity = list(0, 0.2, 0) position = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND) drift = generator(GEN_BOX, list(-8,-16,0), list(8,16,0), NORMAL_RAND) - spin = generator(GEN_NUM, list(-5,5), NORMAL_RAND) + spin = generator(GEN_NUM, -5, 5, NORMAL_RAND) friction = 0.5 gravity = list(0, 0) grow = 0.05 diff --git a/code/modules/events/wizard/magical_rain.dm b/code/modules/events/wizard/magical_rain.dm index 203a960d36e..0885c983064 100644 --- a/code/modules/events/wizard/magical_rain.dm +++ b/code/modules/events/wizard/magical_rain.dm @@ -21,7 +21,7 @@ if(!started) started = TRUE - SSweather.run_weather(/datum/weather/rain_storm/wizard) + SSweather.run_weather(/datum/weather/particle/rain_storm/wizard) /datum/round_event/wizard/magical_rain/end() for(var/mob/living/wizard in GLOB.alive_mob_list) diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 541734ff8e4..6c32c7a0e79 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -131,8 +131,8 @@ SEND_SIGNAL(client, COMSIG_CLIENT_MOB_LOGIN, src) 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/particle/ash_storm, ZTRAIT_ASHSTORM, GLOB.ash_storm_sounds) + AddElement(/datum/element/weather_listener, /datum/weather/particle/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) diff --git a/icons/effects/weather_overlay.dmi b/icons/effects/weather_overlay.dmi new file mode 100644 index 00000000000..7dc15fbe40c Binary files /dev/null and b/icons/effects/weather_overlay.dmi differ diff --git a/tgstation.dme b/tgstation.dme index f310217da6a..fd48e58b266 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2145,6 +2145,7 @@ #include "code\datums\votes\custom_vote.dm" #include "code\datums\votes\map_vote.dm" #include "code\datums\votes\restart_vote.dm" +#include "code\datums\weather\particle_weather.dm" #include "code\datums\weather\weather.dm" #include "code\datums\weather\weather_types\ash_storm.dm" #include "code\datums\weather\weather_types\floor_is_lava.dm"