diff --git a/code/__DEFINES/research/anomalies.dm b/code/__DEFINES/research/anomalies.dm index f0dabae0250..b2b39d4d08c 100644 --- a/code/__DEFINES/research/anomalies.dm +++ b/code/__DEFINES/research/anomalies.dm @@ -8,6 +8,7 @@ #define MAX_CORES_BIOSCRAMBLER 8 #define MAX_CORES_DIMENSIONAL 8 #define MAX_CORES_ECTOPLASMIC 8 +#define MAX_CORES_WEATHER 8 ///Defines for the different types of explosion a flux anomaly can have #define FLUX_NO_EMP 0 diff --git a/code/__DEFINES/weather.dm b/code/__DEFINES/weather.dm index 7cc84cc8a4e..0bd041a497c 100644 --- a/code/__DEFINES/weather.dm +++ b/code/__DEFINES/weather.dm @@ -35,7 +35,7 @@ GLOBAL_LIST_INIT(thunder_chance_options, list( "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, + "None - Admin Safe Space (Thunder Disabled)" = 0, )) //WEATHER FLAGS @@ -53,6 +53,17 @@ GLOBAL_LIST_INIT(thunder_chance_options, list( #define WEATHER_BAROMETER (1<<5) /// If weather temperature ignores clothing insulation when adjusting bodytemperature #define WEATHER_TEMPERATURE_BYPASS_CLOTHING (1<<6) +/// Alerts are only sent to people within affected area rather than on affected z-levels +#define WEATHER_STRICT_ALERT (1<<7) /// Does weather have any type of processing related to mobs, turfs, or thunder? #define FUNCTIONAL_WEATHER (WEATHER_TURFS|WEATHER_MOBS|WEATHER_THUNDER) + +// Indexes for weather_data list to override behaviors +#define WEATHER_FORCED_AREAS "Areas" +#define WEATHER_FORCED_FLAGS "Flags" +#define WEATHER_FORCED_REAGENT "Reagent" +#define WEATHER_FORCED_THUNDER "Thunder Chance" +#define WEATHER_FORCED_TELEGRAPH "Telegraph Duration" +#define WEATHER_FORCED_END "End Duration" +#define WEATHER_FORCED_DURATION "Weather Duration" diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index 88e622add8c..7046a2f8f4d 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -689,6 +689,7 @@ DEFINE_BITFIELD(weather_flags, list( "WEATHER_ENDLESS" = WEATHER_ENDLESS, "WEATHER_BAROMETER" = WEATHER_BAROMETER, "WEATHER_TEMPERATURE_BYPASS_CLOTHING" = WEATHER_TEMPERATURE_BYPASS_CLOTHING, + "WEATHER_STRICT_ALERT" = WEATHER_STRICT_ALERT, )) DEFINE_BITFIELD(visible_info, list( diff --git a/code/controllers/subsystem/networks/research.dm b/code/controllers/subsystem/networks/research.dm index dece0f78e01..8a48e90c266 100644 --- a/code/controllers/subsystem/networks/research.dm +++ b/code/controllers/subsystem/networks/research.dm @@ -66,6 +66,7 @@ SUBSYSTEM_DEF(research) /obj/item/assembly/signaler/anomaly/bioscrambler = MAX_CORES_BIOSCRAMBLER, /obj/item/assembly/signaler/anomaly/dimensional = MAX_CORES_DIMENSIONAL, /obj/item/assembly/signaler/anomaly/ectoplasm = MAX_CORES_ECTOPLASMIC, + /obj/item/assembly/signaler/anomaly/weather = MAX_CORES_WEATHER, ) /// Lookup list for ordnance briefers. diff --git a/code/controllers/subsystem/weather.dm b/code/controllers/subsystem/weather.dm index 0882720cdfb..c330455fb15 100644 --- a/code/controllers/subsystem/weather.dm +++ b/code/controllers/subsystem/weather.dm @@ -109,7 +109,8 @@ SUBSYSTEM_DEF(weather) var/datum/weather/W = new weather_datum_type(z_levels, weather_data) - W.telegraph() + W.telegraph(weather_data) + return W /datum/controller/subsystem/weather/proc/make_eligible(z, possible_weather) eligible_zlevels[z] = possible_weather diff --git a/code/datums/components/area_sound_manager.dm b/code/datums/components/area_sound_manager.dm index 8dfb7895699..f712fc8043b 100644 --- a/code/datums/components/area_sound_manager.dm +++ b/code/datums/components/area_sound_manager.dm @@ -43,7 +43,7 @@ /datum/component/area_sound_manager/proc/react_to_z_move(datum/source, turf/old_turf, turf/new_turf) SIGNAL_HANDLER - if(!length(accepted_zs) || (new_turf.z in accepted_zs)) + if(!length(accepted_zs) || (new_turf?.z in accepted_zs)) return qdel(src) diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm index a8d7a83ea0d..f0a87956c56 100644 --- a/code/datums/weather/weather.dm +++ b/code/datums/weather/weather.dm @@ -57,19 +57,19 @@ /// Types of area to affect var/area_type = /area/space /// Areas to be affected by the weather, calculated when the weather begins - var/list/impacted_areas = list() + VAR_FINAL/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() + VAR_FINAL/list/impacted_areas_weighted = list() /// The total number of turfs impacted by weather across all z-levels and areas. - var/total_impacted_turfs = 0 + VAR_FINAL/total_impacted_turfs = 0 /// Areas affected by weather have their blend modes changed - var/list/impacted_areas_blend_modes = list() + VAR_FINAL/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 + VAR_FINAL/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() + VAR_FINAL/list/impacted_z_levels_weighted = list() /// Since it's above everything else, this is the layer used by default. var/overlay_layer = AREA_LAYER @@ -109,15 +109,15 @@ var/weather_flags = NONE /// List of current mobs being processed by weather - var/list/current_mobs = list() + VAR_FINAL/list/current_mobs = list() /// The weather turf counter to keep track of how many turfs we have processed so far - var/turf_iteration = 0 + VAR_FINAL/turf_iteration = 0 /// The weather thunder counter to keep track of how much thunder we have processed so far - var/thunder_iteration = 0 + VAR_FINAL/thunder_iteration = 0 /// Index of the current section our weather subsystem is processing from our subsystem_tasks - var/task_index = 1 + VAR_FINAL/task_index = 1 /// The list of allowed tasks our weather subsystem is allowed to process (determined by weather_flags) - var/list/subsystem_tasks = list() + VAR_FINAL/list/subsystem_tasks = list() /// The temperature of our weather that is applied to weather reagents and mobs using adjust_bodytemperature() var/weather_temperature = T20C @@ -137,11 +137,12 @@ ..() 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"] + weather_flags = isnull(weather_data?[WEATHER_FORCED_FLAGS]) ? weather_flags : weather_data?[WEATHER_FORCED_FLAGS] + turf_thunder_chance = isnull(weather_data?[WEATHER_FORCED_THUNDER]) ? turf_thunder_chance : weather_data?[WEATHER_FORCED_THUNDER] + telegraph_duration = isnull(weather_data?[WEATHER_FORCED_TELEGRAPH]) ? telegraph_duration : weather_data?[WEATHER_FORCED_TELEGRAPH] + end_duration = isnull(weather_data?[WEATHER_FORCED_END]) ? end_duration : weather_data?[WEATHER_FORCED_END] - var/datum/reagent/custom_reagent = weather_data?["reagent"] + var/datum/reagent/custom_reagent = weather_data?[WEATHER_FORCED_REAGENT] var/reagent_id if(custom_reagent) reagent_id = custom_reagent @@ -165,7 +166,7 @@ if(weather_flags & (WEATHER_THUNDER)) subsystem_tasks += SSWEATHER_THUNDER - setup_weather_areas() + setup_weather_areas(weather_data?[WEATHER_FORCED_AREAS]) setup_weather_turfs() /datum/weather/Destroy() @@ -179,26 +180,94 @@ * Calculates duration and hit areas, and makes a callback for the actual weather to start * */ -/datum/weather/proc/telegraph() +/datum/weather/proc/telegraph(list/weather_data) if(stage == STARTUP_STAGE) return stage = STARTUP_STAGE SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_TELEGRAPH(type), src) - - weather_duration = rand(weather_duration_lower, weather_duration_upper) + weather_duration = isnull(weather_data?[WEATHER_FORCED_DURATION]) ? rand(weather_duration_lower, weather_duration_upper) : weather_data?[WEATHER_FORCED_DURATION] SSweather.processing |= src update_areas() if(telegraph_duration) send_alert(telegraph_message, telegraph_sound, telegraph_sound_vol) - addtimer(CALLBACK(src, PROC_REF(start)), telegraph_duration) + addtimer(CALLBACK(src, PROC_REF(start)), telegraph_duration, TIMER_UNIQUE) -/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) - for(var/area/affected_area as anything in affectareas) + // so /datum/element/weather_listener relies on z traits to determine when to set up sound managers + // thus if we force weather on a z level that doesn't match the target trait, we need to manually set up sound managers + if(target_trait) + manually_setup_sound_manager() + +/// Manually add a sound manager to all mobs +/datum/weather/proc/manually_setup_sound_manager() + var/list/filtered_zs = get_impacted_zs_without_trait() + var/list/playlist = get_playlist_ref() + // we only need to manually handle sound managers if there are zs without the trait being affected, fortunately + if(!length(filtered_zs) || isnull(playlist)) + return + + // add in dead mobs so we can get observers covered too + for(var/mob/living/affected as anything in GLOB.mob_living_list | GLOB.dead_mob_list) + if(isnull(affected.client)) + // this registers 400+ odd signals... maybe we should reconsider + RegisterSignal(affected, COMSIG_MOB_LOGIN, PROC_REF(handle_mob_log_in)) + continue + + manually_setup_sound_manager_on_mob(affected, playlist, filtered_zs) + + RegisterSignal(SSdcs, COMSIG_GLOB_MOB_CREATED, PROC_REF(handle_new_mob_sound_manager)) + +/// Returns a reference to the "sound playlist" for this weather type +/datum/weather/proc/get_playlist_ref() + return null + +/// Returns a list of z-levels impacted that do not have the target trait +/datum/weather/proc/get_impacted_zs_without_trait() + var/list/zs_without_trait = list() + for(var/z in impacted_z_levels) + if(!SSmapping.level_trait(z, target_trait)) + zs_without_trait += z + return zs_without_trait + +/datum/weather/proc/handle_new_mob_sound_manager(datum/source, mob/the_mob) + SIGNAL_HANDLER + + if(isnull(the_mob.client)) + RegisterSignal(the_mob, COMSIG_MOB_LOGIN, PROC_REF(handle_mob_log_in)) + return + + manually_setup_sound_manager_on_mob(the_mob) + +/datum/weather/proc/handle_mob_log_in(mob/source) + SIGNAL_HANDLER + + if(stage >= END_STAGE) + stack_trace("Attempted to add a sound manager to a mob after weather ended") + UnregisterSignal(source, COMSIG_MOB_LOGIN) + return + + manually_setup_sound_manager_on_mob(source) + +/datum/weather/proc/manually_setup_sound_manager_on_mob(mob/living/affected, list/playlist = get_playlist_ref(), list/filtered_zs = get_impacted_zs_without_trait()) + PRIVATE_PROC(TRUE) + + var/list/sound_change_signals = list( + COMSIG_WEATHER_TELEGRAPH(type), + COMSIG_WEATHER_START(type), + COMSIG_WEATHER_WINDDOWN(type), + ) + + var/datum/component/our_comp = affected.AddComponent( \ + /datum/component/area_sound_manager, \ + area_loop_pairs = playlist, \ + acceptable_zs = filtered_zs, \ + ) + our_comp.RegisterSignals(SSdcs, sound_change_signals, TYPE_PROC_REF(/datum/component/area_sound_manager, handle_change)) + our_comp.RegisterSignal(SSdcs, COMSIG_WEATHER_END(type), TYPE_PROC_REF(/datum/component/area_sound_manager, handle_removal)) + +/datum/weather/proc/setup_weather_areas(list/forced_areas) + for(var/area/affected_area as anything in (forced_areas || get_areas(area_type))) + if(is_type_in_list(affected_area, protected_areas)) + continue if(!(weather_flags & WEATHER_INDOORS) && !affected_area.outdoors) continue @@ -263,7 +332,7 @@ update_areas() send_alert(weather_message, weather_sound) if(!(weather_flags & (WEATHER_ENDLESS))) - addtimer(CALLBACK(src, PROC_REF(wind_down)), weather_duration) + 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) @@ -281,7 +350,7 @@ stage = WIND_DOWN_STAGE update_areas() send_alert(end_message, end_sound, end_sound_vol) - addtimer(CALLBACK(src, PROC_REF(end)), end_duration) + addtimer(CALLBACK(src, PROC_REF(end)), end_duration, TIMER_UNIQUE) /** * Fully ends the weather @@ -294,12 +363,17 @@ if(stage == END_STAGE) return SEND_GLOBAL_SIGNAL(COMSIG_WEATHER_END(type), src) + UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_CREATED) stage = END_STAGE SSweather.processing -= src update_areas() for(var/area/impacted_area as anything in impacted_areas) SEND_SIGNAL(impacted_area, COMSIG_WEATHER_ENDED_IN_AREA(type), src) + if(target_trait) + for(var/mob/living/affected as anything in GLOB.mob_living_list | GLOB.dead_mob_list) + UnregisterSignal(affected, COMSIG_MOB_LOGIN) + // handles sending all alerts /datum/weather/proc/send_alert(alert_msg, alert_sfx, alert_sfx_vol = 100) for(var/z_level in impacted_z_levels) @@ -315,7 +389,24 @@ // the checks for if a mob should receive alerts, returns TRUE if can /datum/weather/proc/can_get_alert(mob/player) var/turf/mob_turf = get_turf(player) - return !isnull(mob_turf) + if(isnull(mob_turf)) + return FALSE + + if((weather_flags & WEATHER_STRICT_ALERT) && !can_see_weather(player)) + return FALSE + + return TRUE + +/// Checks if the player is in or can see an area affected by the weather +/datum/weather/proc/can_see_weather(mob/player) + if(HAS_MIND_TRAIT(player, TRAIT_DETECT_STORM)) + return TRUE + + for(var/area/nearby in view(player)) + if(nearby in impacted_areas) + return TRUE + + return FALSE /** * Returns TRUE if the living mob can be affected by the weather diff --git a/code/datums/weather/weather_types/ash_storm.dm b/code/datums/weather/weather_types/ash_storm.dm index f3e9f3c4b04..367977a90bc 100644 --- a/code/datums/weather/weather_types/ash_storm.dm +++ b/code/datums/weather/weather_types/ash_storm.dm @@ -28,6 +28,9 @@ var/list/weak_sounds = list() var/list/strong_sounds = list() +/datum/weather/ash_storm/get_playlist_ref() + return GLOB.ash_storm_sounds + /datum/weather/ash_storm/telegraph() for(var/area/impacted_area as anything in impacted_areas) if(impacted_area.outdoors) diff --git a/code/datums/weather/weather_types/rain_storm.dm b/code/datums/weather/weather_types/rain_storm.dm index b1386d84587..4516112824c 100644 --- a/code/datums/weather/weather_types/rain_storm.dm +++ b/code/datums/weather/weather_types/rain_storm.dm @@ -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() + return GLOB.rain_storm_sounds + /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 + + // change the message for if rain is triggered inside the station (no canopy of course) + for(var/z in impacted_z_levels) + if(is_station_level(z)) + telegraph_message = span_warning("Thunder rumbles from above. You hear droplets hitting the floor around you.") + break + return ..() /datum/weather/rain_storm/start() diff --git a/code/datums/weather/weather_types/sand_storm.dm b/code/datums/weather/weather_types/sand_storm.dm index c346c536a91..fdac54739e3 100644 --- a/code/datums/weather/weather_types/sand_storm.dm +++ b/code/datums/weather/weather_types/sand_storm.dm @@ -24,6 +24,9 @@ weather_flags = (WEATHER_MOBS | WEATHER_BAROMETER) +/datum/weather/sand_storm/get_playlist_ref() + return GLOB.sand_storm_sounds + /datum/weather/sand_storm/telegraph() GLOB.sand_storm_sounds.Cut() for(var/area/impacted_area as anything in impacted_areas) diff --git a/code/datums/weather/weather_types/snow_storm.dm b/code/datums/weather/weather_types/snow_storm.dm index 01a07df6133..f4a3baaa190 100644 --- a/code/datums/weather/weather_types/snow_storm.dm +++ b/code/datums/weather/weather_types/snow_storm.dm @@ -28,7 +28,10 @@ // 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) + weather_flags = (WEATHER_MOBS | WEATHER_BAROMETER | WEATHER_TEMPERATURE_BYPASS_CLOTHING | WEATHER_STRICT_ALERT) + +/datum/weather/snow_storm/get_playlist_ref() + return GLOB.snowstorm_sounds /datum/weather/snow_storm/start() GLOB.snowstorm_sounds.Cut() // it's passed by ref @@ -40,29 +43,6 @@ GLOB.snowstorm_sounds.Cut() return ..() -// since snowstorm is on a station z level, add extra checks to not annoy everyone -/datum/weather/snow_storm/can_get_alert(mob/player) - if(!..()) - return FALSE - - if(!is_station_level(player.z)) - return TRUE // bypass checks - - if(isobserver(player)) - return TRUE - - if(HAS_MIND_TRAIT(player, TRAIT_DETECT_STORM)) - return TRUE - - if(istype(get_area(player), /area/mine)) - return TRUE - - for(var/area/snow_area in impacted_areas) - if(locate(snow_area) in view(player)) - return TRUE - - return FALSE - ///A storm that doesn't stop storming, and is a bit stronger /datum/weather/snow_storm/forever_storm telegraph_duration = 0 SECONDS diff --git a/code/game/objects/effects/anomalies/anomalies_weather.dm b/code/game/objects/effects/anomalies/anomalies_weather.dm new file mode 100644 index 00000000000..ea407acfa3a --- /dev/null +++ b/code/game/objects/effects/anomalies/anomalies_weather.dm @@ -0,0 +1,100 @@ +/obj/effect/anomaly/weather + name = "weather anomaly" + anomaly_core = /obj/item/assembly/signaler/anomaly/weather + lifespan = ANOMALY_COUNTDOWN_TIMER * 2.5 + + /// Chance per turf per second that we will produce thunder. Use the defines + var/thunder_chance = 0 + /// The type of weather this anomaly will produce. If unset, will be picked from select_weather() + var/datum/weather/weather_type + /// List of active weathers spawned by this anomaly + VAR_PRIVATE/list/active_weathers + +/obj/effect/anomaly/weather/Initialize(mapload, new_lifespan, drops_core, forced_anomaly_type = src.weather_type, forced_thunder_chance = src.thunder_chance) + . = ..() + + add_atom_colour(COLOR_MATRIX_INVERT, FIXED_COLOUR_PRIORITY) + + weather_type = forced_anomaly_type || select_weather() + thunder_chance = forced_thunder_chance + + active_weathers = list() + + var/telegraph = lifespan / 5 // 1/5th of the time is dedicated to telegraphing, to give people time to find and defuse it + var/end_dur = lifespan / 15 // then 1/15th of the time is dedicated to winding down + var/total_dur = lifespan - telegraph - end_dur + + var/list/affected_areas = list(impact_area) + var/list/num_turfs = length(impact_area.get_turfs_from_all_zlevels()) + for(var/spread_dir in GLOB.alldirs) + var/area/nearby = find_adjacent_impacted_area(spread_dir) + // prevents nearby the central hallway from basically always being included + if(isnull(nearby) || length(nearby.get_turfs_from_all_zlevels()) > num_turfs * 1.5) + continue + affected_areas |= nearby + + var/datum/weather/weather = SSweather.run_weather( + weather_datum_type = weather_type, + z_levels = z, + weather_data = list( + WEATHER_FORCED_AREAS = affected_areas, + WEATHER_FORCED_FLAGS = weather_type::weather_flags | WEATHER_INDOORS | WEATHER_THUNDER | WEATHER_STRICT_ALERT, + WEATHER_FORCED_THUNDER = thunder_chance, + WEATHER_FORCED_TELEGRAPH = telegraph, + WEATHER_FORCED_END = end_dur, + WEATHER_FORCED_DURATION = total_dur, + ) + ) + RegisterSignal(weather, COMSIG_QDELETING, PROC_REF(clear_ref)) + active_weathers += weather + +/obj/effect/anomaly/weather/proc/clear_ref(datum/weather/weather_datum) + SIGNAL_HANDLER + active_weathers -= weather_datum + UnregisterSignal(weather_datum, COMSIG_QDELETING) + +/obj/effect/anomaly/weather/proc/select_weather() + return pick( + /datum/weather/rain_storm, + /datum/weather/snow_storm, + /datum/weather/sand_storm, + ) + +/// steps outward from src to find an area that is not the impact_area. +/obj/effect/anomaly/weather/proc/find_adjacent_impacted_area(check_dir) + var/limit = 9 + var/turf/next_turf = get_step(src, check_dir) + while(next_turf.loc == impact_area && limit > 0) + next_turf = get_step(next_turf, check_dir) + if(isnull(next_turf)) + return null + limit -= 1 + + return next_turf.loc + +/obj/effect/anomaly/weather/anomalyNeutralize() + for(var/datum/weather/weather_datum as anything in active_weathers) + weather_datum?.wind_down() + return ..() + +/obj/effect/anomaly/weather/detonate() + playsound(src, 'sound/effects/magic/repulse.ogg', 100, TRUE) + for(var/atom/movable/repulsed in range(src, 5)) + if(repulsed == src || repulsed.anchored) + continue + var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(repulsed, src))) + repulsed.safe_throw_at(throwtarget, 6, 2, src, force = MOVE_FORCE_EXTREMELY_STRONG) + +/obj/effect/anomaly/weather/Destroy() + for(var/datum/weather/weather_datum as anything in active_weathers) + clear_ref(weather_datum) + return ..() + +/obj/effect/anomaly/weather/thundering + name = "thundering weather anomaly" + + thunder_chance = THUNDER_CHANCE_HIGH + // 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 diff --git a/code/modules/admin/verbs/adminweather.dm b/code/modules/admin/verbs/adminweather.dm index e5c77625b80..8721a1aabb2 100644 --- a/code/modules/admin/verbs/adminweather.dm +++ b/code/modules/admin/verbs/adminweather.dm @@ -28,9 +28,9 @@ ADMIN_VERB(run_weather, R_ADMIN|R_FUN, "Run Weather", "Triggers specific weather 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 + for(var/area/area_instance as anything in GLOB.areas) + area_choices[area_instance.type] ||= list() + area_choices[area_instance.type] |= area_instance var/area/area_choice = tgui_input_list(user, "Select an area for weather to target", "Target Area", area_choices) if(!area_choice) @@ -72,10 +72,10 @@ ADMIN_VERB(run_weather, R_ADMIN|R_FUN, "Run Weather", "Triggers specific weather 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, + WEATHER_FORCED_AREAS = area_choice, + WEATHER_FORCED_FLAGS = weather_bitflags, + WEATHER_FORCED_THUNDER = thunder_value, + WEATHER_FORCED_REAGENT = reagent_choice, ) SSweather.run_weather(weather_choice, z_level, weather_data) diff --git a/code/modules/clothing/suits/reactive_armour.dm b/code/modules/clothing/suits/reactive_armour.dm index 44ba487bccd..d6d28df30d5 100644 --- a/code/modules/clothing/suits/reactive_armour.dm +++ b/code/modules/clothing/suits/reactive_armour.dm @@ -15,7 +15,8 @@ /obj/effect/anomaly/hallucination = /obj/item/clothing/suit/armor/reactive/hallucinating, /obj/effect/anomaly/dimensional = /obj/item/clothing/suit/armor/reactive/barricade, /obj/effect/anomaly/ectoplasm = /obj/item/clothing/suit/armor/reactive/ectoplasm, - ) + /obj/effect/anomaly/weather = /obj/item/clothing/suit/armor/reactive/weather, + ) if(istype(tool, /obj/item/assembly/signaler/anomaly)) var/obj/item/assembly/signaler/anomaly/anomaly = tool @@ -496,3 +497,76 @@ /obj/item/clothing/suit/armor/reactive/ectoplasm/emp_activation(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK) owner.reagents?.add_reagent(/datum/reagent/inverse/helgrasp, 20) + +/obj/item/clothing/suit/armor/reactive/weather + name = "reactive weather armor" + desc = "An experimental suit of armor that manipulates the weather around the wearer when in danger." + emp_message = span_warning("The reactive armor's weather control unit sputters and groans...") + cooldown_message = span_danger("The reactive weather system is still recharging! It fails to activate!") + reactivearmor_cooldown_duration = 30 SECONDS + +/obj/item/clothing/suit/armor/reactive/weather/reactive_activation(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK) + owner.visible_message(span_danger("The reactive armor alters the weather around [owner], shielding [owner.p_them()] from [attack_text]!")) + playsound(src, 'sound/effects/magic/lightningshock.ogg', 33, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) + + var/datum/effect_system/steam_spread/steam = new() + steam.set_up(10, FALSE, owner.loc) + steam.start() + + var/list/affected_turfs = list() + for(var/mob/living/attacker in oview(2, owner)) + attacker.adjust_wet_stacks(5) + attacker.extinguish_mob() + if((attacker.loc in affected_turfs) || !isopenturf(attacker.loc)) + continue + + for(var/turf/open/to_affect in view(1, attacker.loc)) + affected_turfs += to_affect + + shock_turf_windup(attacker.loc) + + reactivearmor_cooldown = world.time + reactivearmor_cooldown_duration + return TRUE + +/obj/item/clothing/suit/armor/reactive/weather/emp_activation(mob/living/carbon/human/owner, atom/movable/hitby, attack_text, final_block_chance, damage, attack_type) + owner.visible_message(span_danger("The reactive armor malfunctions, calling down a storm upon [owner.p_them()]!")) + playsound(src, 'sound/effects/magic/lightningshock.ogg', 33, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) + + var/datum/effect_system/steam_spread/steam = new() + steam.set_up(2, FALSE, owner.loc) + steam.start() + + owner.adjust_wet_stacks(10) + owner.extinguish_mob() + if(!isopenturf(owner.loc)) + return + shock_turf_windup(owner.loc) + +/obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf_windup(turf/target) + new /obj/effect/temp_visual/telegraphing/thunderbolt(target) + addtimer(CALLBACK(src, PROC_REF(shock_turf), target), 1 SECONDS) + +/obj/item/clothing/suit/armor/reactive/weather/proc/shock_turf(turf/target) + playsound(target, 'sound/effects/magic/lightningbolt.ogg', 66, TRUE) + new /obj/effect/temp_visual/thunderbolt(target) + for(var/turf/open/adjacent_turf in oview(1, target)) + new /obj/effect/temp_visual/electricity(adjacent_turf) + + for(var/mob/living/hit_mob in target) + if(hit_mob == loc) // avoid hitting the wearer + continue + to_chat(hit_mob, span_userdanger("You've been struck by lightning!")) + hit_mob.electrocute_act(30, src, flags = SHOCK_TESLA|SHOCK_NOSTUN) + hit_mob.Knockdown(2.5 SECONDS, 10 SECONDS) + + for(var/mob/living/nearby_target in oview(1, target)) + if(nearby_target == loc) // avoid hitting the wearer + continue + to_chat(nearby_target, span_userdanger("You've been struck by an arc of lightning!")) + nearby_target.electrocute_act(10, src, flags = SHOCK_TESLA|SHOCK_NOSTUN) + + for(var/obj/hit_thing in target) + hit_thing.take_damage(20, BURN, ENERGY, FALSE) + + for(var/obj/nearby_thing in oview(1, target)) + nearby_thing.take_damage(5, BURN, ENERGY, FALSE) diff --git a/code/modules/events/anomaly/_anomaly.dm b/code/modules/events/anomaly/_anomaly.dm index 4d1916158f2..2f55338c3d6 100644 --- a/code/modules/events/anomaly/_anomaly.dm +++ b/code/modules/events/anomaly/_anomaly.dm @@ -40,11 +40,14 @@ var/newAnomaly if(anomaly_turf) - newAnomaly = new anomaly_path(anomaly_turf) + newAnomaly = make_anomaly(anomaly_turf) if (newAnomaly) apply_anomaly_properties(newAnomaly) announce_to_ghosts(newAnomaly) +/datum/round_event/anomaly/proc/make_anomaly(turf/anomaly_turf) + return new anomaly_path(anomaly_turf) + /// Make any further post-creation modifications to the anomaly /datum/round_event/anomaly/proc/apply_anomaly_properties(obj/effect/anomaly/new_anomaly) return @@ -54,4 +57,3 @@ /datum/event_admin_setup/set_location/anomaly/apply_to_event(datum/round_event/anomaly/event) event.spawn_location = chosen_turf - diff --git a/code/modules/events/anomaly/anomaly_weather.dm b/code/modules/events/anomaly/anomaly_weather.dm new file mode 100644 index 00000000000..3ce303cf3df --- /dev/null +++ b/code/modules/events/anomaly/anomaly_weather.dm @@ -0,0 +1,77 @@ +/datum/round_event_control/anomaly/anomaly_weather + name = "Anomaly: Weather" + typepath = /datum/round_event/anomaly/anomaly_weather + + max_occurrences = 2 + weight = 10 + description = "This anomaly causes weather effects to manifest indoors. \ + It can be cause completely harmless weather like light rain, or something which could harm unprotected individuals like snowstorms. \ + Note, triggering multiple at once will likely break weather sound effects." + min_wizard_trigger_potency = 0 + max_wizard_trigger_potency = 5 + admin_setup = list( + /datum/event_admin_setup/set_location/anomaly, + /datum/event_admin_setup/listed_options/weather_anomaly, + /datum/event_admin_setup/listed_options/weather_thunder, + ) + +/datum/round_event_control/anomaly/anomaly_weather/can_spawn_event(players_amt, allow_magic = FALSE) + // weathers have some funky global state that may break if multiple are running. better safe than sorry. + return ..() && !length(SSweather.processing) + +/datum/round_event/anomaly/anomaly_weather + start_when = ANOMALY_START_HARMFUL_TIME + announce_when = ANOMALY_ANNOUNCE_HARMFUL_TIME + anomaly_path = /obj/effect/anomaly/weather + + var/forced_weather_type = null + var/forced_thunder_chance = null + +/datum/round_event/anomaly/anomaly_weather/announce(fake) + if(isnull(impact_area)) + impact_area = placer.findValidArea() + priority_announce("Barometric anomaly detected on [ANOMALY_ANNOUNCE_HARMFUL_TEXT] [impact_area.name].", "Anomaly Alert") + +/datum/round_event/anomaly/anomaly_weather/make_anomaly(turf/anomaly_turf) + return new anomaly_path(anomaly_turf, null, null, forced_weather_type, forced_thunder_chance) + +/datum/round_event_control/anomaly/anomaly_weather/thundering + name = "Anomaly: Thundering Weather" + typepath = /datum/round_event/anomaly/anomaly_weather/thundering + + max_occurrences = 1 + weight = 5 + description = "This anomaly causes more hazardous weather effects to manifest indoors, like thunderstorms with frequent lightning strikes. \ + This version will trigger lightning strikes which can cause decent damage to people and equipment alike." + min_wizard_trigger_potency = 2 + max_wizard_trigger_potency = 7 + +/datum/round_event/anomaly/anomaly_weather/thundering + start_when = ANOMALY_START_DANGEROUS_TIME + announce_when = ANOMALY_ANNOUNCE_DANGEROUS_TIME + anomaly_path = /obj/effect/anomaly/weather/thundering + +/datum/round_event/anomaly/anomaly_weather/thundering/announce(fake) + if(isnull(impact_area)) + impact_area = placer.findValidArea() + priority_announce("Severe barometric anomaly detected on [ANOMALY_ANNOUNCE_DANGEROUS_TEXT] [impact_area.name].", "Anomaly Alert") + +/datum/event_admin_setup/listed_options/weather_anomaly + input_text = "Weather type? Be very careful with the dangerous ones!" + normal_run_option = "Default" + +/datum/event_admin_setup/listed_options/weather_anomaly/get_list() + return valid_subtypesof(/datum/weather) + +/datum/event_admin_setup/listed_options/weather_anomaly/apply_to_event(datum/round_event/anomaly/anomaly_weather/event) + event.forced_weather_type = chosen + +/datum/event_admin_setup/listed_options/weather_thunder + input_text = "Thunder chance? Be careful with high values!" + normal_run_option = "Default" + +/datum/event_admin_setup/listed_options/weather_thunder/get_list() + return GLOB.thunder_chance_options.Copy() + +/datum/event_admin_setup/listed_options/weather_thunder/apply_to_event(datum/round_event/anomaly/anomaly_weather/event) + event.forced_thunder_chance = GLOB.thunder_chance_options[chosen] diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 4fb1f734741..869d30b2fdc 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -150,6 +150,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) SSpoints_of_interest.make_point_of_interest(src) ADD_TRAIT(src, TRAIT_HEAR_THROUGH_DARKNESS, INNATE_TRAIT) ADD_TRAIT(src, TRAIT_GOOD_HEARING, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_DETECT_STORM, INNATE_TRAIT) /mob/dead/observer/get_photo_description(obj/item/camera/camera) if(!invisibility || camera.see_ghosts) diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm index 16a9277c26e..a9d3b7f4d71 100644 --- a/code/modules/research/anomaly/anomaly_core.dm +++ b/code/modules/research/anomaly/anomaly_core.dm @@ -94,3 +94,9 @@ desc = "The neutralized core of an ectoplasmic anomaly. When you hold it close, you can hear faint murmuring from inside. It'd probably be valuable for research." icon_state = "dimensional_core" anomaly_type = /obj/effect/anomaly/ectoplasm + +/obj/item/assembly/signaler/anomaly/weather + name = "\improper weather anomaly core" + desc = "The neutralized core of a weather anomaly. The sound of thunder can be heard in the distance. It'd probably be valuable for research." + icon_state = "weather_core" + anomaly_type = /obj/effect/anomaly/weather diff --git a/code/modules/research/anomaly/raw_anomaly.dm b/code/modules/research/anomaly/raw_anomaly.dm index d86ed1f1d9d..e33a3adca3e 100644 --- a/code/modules/research/anomaly/raw_anomaly.dm +++ b/code/modules/research/anomaly/raw_anomaly.dm @@ -74,6 +74,12 @@ anomaly_type = /obj/item/assembly/signaler/anomaly/ectoplasm icon_state = "rawcore_dimensional" +/obj/item/raw_anomaly_core/weather + name = "raw weather core" + desc = "The raw core of a weather anomaly. It makes you wish for a rainy day." + anomaly_type = /obj/item/assembly/signaler/anomaly/weather + icon_state = "rawcore_weather" + /obj/item/raw_anomaly_core/random/Initialize(mapload) . = ..() var/path = pick(subtypesof(/obj/item/raw_anomaly_core)) diff --git a/icons/obj/devices/new_assemblies.dmi b/icons/obj/devices/new_assemblies.dmi index f80f89ce20a..55ac60442c8 100644 Binary files a/icons/obj/devices/new_assemblies.dmi and b/icons/obj/devices/new_assemblies.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 214aaa21427..c4796785ecf 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2417,6 +2417,7 @@ #include "code\game\objects\effects\anomalies\anomalies_hallucination.dm" #include "code\game\objects\effects\anomalies\anomalies_pyroclastic.dm" #include "code\game\objects\effects\anomalies\anomalies_vortex.dm" +#include "code\game\objects\effects\anomalies\anomalies_weather.dm" #include "code\game\objects\effects\decals\chemspray.dm" #include "code\game\objects\effects\decals\cleanable.dm" #include "code\game\objects\effects\decals\crayon.dm" @@ -4313,6 +4314,7 @@ #include "code\modules\events\anomaly\anomaly_placer.dm" #include "code\modules\events\anomaly\anomaly_pyro.dm" #include "code\modules\events\anomaly\anomaly_vortex.dm" +#include "code\modules\events\anomaly\anomaly_weather.dm" #include "code\modules\events\ghost_role\_ghost_role.dm" #include "code\modules\events\ghost_role\operative.dm" #include "code\modules\events\ghost_role\sentience.dm"