diff --git a/aurorastation.dme b/aurorastation.dme index 293ca85ea81..26aedd24cbf 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -344,7 +344,9 @@ #include "code\controllers\subsystems\timer.dm" #include "code\controllers\subsystems\trade.dm" #include "code\controllers\subsystems\virtual_reality.dm" +#include "code\controllers\subsystems\vis_contents.dm" #include "code\controllers\subsystems\vote.dm" +#include "code\controllers\subsystems\weather.dm" #include "code\controllers\subsystems\zcopy.dm" #include "code\controllers\subsystems\evacuation\evacuation.dm" #include "code\controllers\subsystems\evacuation\evacuation_eta.dm" @@ -403,6 +405,7 @@ #include "code\datums\records.dm" #include "code\datums\ruins.dm" #include "code\datums\sound_player.dm" +#include "code\datums\state_machine.dm" #include "code\datums\statistic.dm" #include "code\datums\tgs_event_handler.dm" #include "code\datums\tgui_module.dm" @@ -480,6 +483,8 @@ #include "code\datums\repositories\singletons.dm" #include "code\datums\repositories\sound_channels.dm" #include "code\datums\repositories\unique.dm" +#include "code\datums\state_machine\state.dm" +#include "code\datums\state_machine\transition.dm" #include "code\datums\tips\tips.dm" #include "code\datums\trading\_trading_defines.dm" #include "code\datums\trading\ai.dm" @@ -1052,6 +1057,7 @@ #include "code\game\objects\items\tajara.dm" #include "code\game\objects\items\toys.dm" #include "code\game\objects\items\trash.dm" +#include "code\game\objects\items\umbrella.dm" #include "code\game\objects\items\vaurca.dm" #include "code\game\objects\items\vitals_monitor.dm" #include "code\game\objects\items\devices\aicard.dm" @@ -1419,6 +1425,9 @@ #include "code\game\verbs\who.dm" #include "code\js\byjax.dm" #include "code\js\menus.dm" +#include "code\modules\abstract\_abstract.dm" +#include "code\modules\abstract\abstract_exterior_marker.dm" +#include "code\modules\abstract\abstract_weather_marker.dm" #include "code\modules\acting\acting_items.dm" #include "code\modules\admin\admin.dm" #include "code\modules\admin\admin_attack_log.dm" @@ -3558,6 +3567,15 @@ #include "code\modules\ventcrawl\ventcrawl.dm" #include "code\modules\ventcrawl\ventcrawl_atmospherics.dm" #include "code\modules\ventcrawl\ventcrawl_verb.dm" +#include "code\modules\weather\_weather.dm" +#include "code\modules\weather\weather_debug.dm" +#include "code\modules\weather\weather_effects.dm" +#include "code\modules\weather\weather_fsm.dm" +#include "code\modules\weather\weather_fsm_state_transitions.dm" +#include "code\modules\weather\weather_fsm_states.dm" +#include "code\modules\weather\weather_init.dm" +#include "code\modules\weather\weather_mob_tracking.dm" +#include "code\modules\weather\weather_wind.dm" #include "code\modules\web_interface\webint_procs.dm" #include "code\modules\world_api\api_command.dm" #include "code\modules\world_api\helpers.dm" diff --git a/code/ZAS/Turf.dm b/code/ZAS/Turf.dm index 72c4cff18d0..e78d0054534 100644 --- a/code/ZAS/Turf.dm +++ b/code/ZAS/Turf.dm @@ -6,9 +6,9 @@ /turf/simulated/proc/update_graphic(list/graphic_add = null, list/graphic_remove = null) if(graphic_add && LAZYLEN(graphic_add)) - vis_contents += graphic_add + add_vis_contents(graphic_add) if(graphic_remove && LAZYLEN(graphic_remove)) - vis_contents -= graphic_remove + remove_vis_contents(graphic_remove) /turf/proc/update_air_properties() var/block diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index 326c21193d6..be2495aac44 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -505,3 +505,18 @@ example: #define GEAR_TWEAK_ACCESSORY_SLOT_SUIT "Suit" /// Spawns standalone in the suit slot #define GEAR_TWEAK_ACCESSORY_SLOT_SUIT_STANDALONE "Standalone Suit" + +//Turf/area values for 'this space is outside' checks +#define OUTSIDE_AREA null +#define OUTSIDE_NO FALSE +#define OUTSIDE_YES TRUE +#define OUTSIDE_UNCERTAIN null + +// Weather exposure values for being rained on or hailed on. +#define WEATHER_IGNORE -1 +#define WEATHER_EXPOSED 0 +#define WEATHER_ROOFED 1 +#define WEATHER_PROTECTED 2 + +// arbitrary low pressure bound for wind weather effects +#define MIN_WIND_PRESSURE 10 diff --git a/code/__DEFINES/subsystem-priority.dm b/code/__DEFINES/subsystem-priority.dm index 1e7ac02b016..5a17e0f0082 100644 --- a/code/__DEFINES/subsystem-priority.dm +++ b/code/__DEFINES/subsystem-priority.dm @@ -49,6 +49,7 @@ #define SS_PRIORITY_EFFECTS 20 // New-style effects manager. Timing of effects may be off if this gets too far behind. #define SS_PRIORITY_CHEMISTRY 10 // Multi-tick chemical reactions. #define SS_PRIORITY_SHUTTLE 10 // Shuttle movement. +#define SS_PRIORITY_WEATHER 10 // Weather processing. #define SS_PRIORITY_AIRFLOW 10 // Handles object movement due to ZAS airflow. #define SS_PRIORITY_ZCOPY 10 // Z-mimic icon generation/updates. #define SS_PRIORITY_ARRIVALS 10 // Centcomm arrivals shuttle auto-launch. Usually asleep. diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index cf517475bfa..3c8eb2ac3cb 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -237,8 +237,10 @@ #define INIT_ORDER_VOTE -4 #define INIT_ORDER_ASSETS -5 #define INIT_ORDER_ICON_UPDATE -5.5 //Yet another aurora snowflake, Icon update queue flush. Should run before overlays, probably before smoothing the icon too +#define INIT_ORDER_VISCONTENTS -5.6 // Queued vis_contents updates. #define INIT_ORDER_ICON_SMOOTHING -6 #define INIT_ORDER_OVERLAY -7 +#define INIT_ORDER_WEATHER -9 #define INIT_ORDER_LIGHTING -20 #define INIT_ORDER_ZCOPY -21 //Aurora snowflake, Z-mimic flush. Should run after SSoverlay & SSicon_smooth so it copies the smoothed sprites. #define INIT_ORDER_PATH -50 diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm index a9ac5b43900..c007f121b5e 100644 --- a/code/_onclick/hud/radial.dm +++ b/code/_onclick/hud/radial.dm @@ -260,7 +260,7 @@ GLOBAL_LIST_EMPTY(radial_menus) //Blank menu_holder = image(icon = 'icons/effects/effects.dmi', loc = anchor, icon_state = "nothing", layer = HUD_LAYER) menu_holder.appearance_flags |= KEEP_APART|RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM - menu_holder.vis_contents += elements + close_button + menu_holder.add_vis_contents(elements + close_button) current_user.images += menu_holder /datum/radial_menu/proc/hide() diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 726cfec6a11..5c21866f2f6 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -205,7 +205,7 @@ if(hovering_choice == choice) return - vis_contents -= hover_overlays_cache[hovering_choice] + remove_vis_contents(hover_overlays_cache[hovering_choice]) hovering_choice = choice var/obj/effect/overlay/zone_sel/overlay_object = hover_overlays_cache[choice] @@ -213,7 +213,7 @@ overlay_object = new overlay_object.icon_state = "[choice]" hover_overlays_cache[choice] = overlay_object - vis_contents += overlay_object + add_vis_contents(overlay_object) /obj/effect/overlay/zone_sel @@ -225,7 +225,7 @@ /obj/screen/zone_sel/MouseExited(location, control, params) if(!isobserver(usr) && hovering_choice) - vis_contents -= hover_overlays_cache[hovering_choice] + remove_vis_contents(hover_overlays_cache[hovering_choice]) hovering_choice = null /obj/screen/zone_sel/proc/get_zone_at(icon_x, icon_y) diff --git a/code/controllers/subsystems/vis_contents.dm b/code/controllers/subsystems/vis_contents.dm new file mode 100644 index 00000000000..57787d0576d --- /dev/null +++ b/code/controllers/subsystems/vis_contents.dm @@ -0,0 +1,93 @@ +SUBSYSTEM_DEF(vis_contents_update) + name = "Vis Contents" + flags = SS_BACKGROUND + wait = 1 + init_order = INIT_ORDER_VISCONTENTS + runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT + var/list/queue_refs = list() + +/datum/controller/subsystem/vis_contents_update/stat_entry() + ..("Queue: [queue_refs.len]") + +/datum/controller/subsystem/vis_contents_update/Initialize() + fire(FALSE, TRUE) + return SS_INIT_SUCCESS + +/datum/controller/subsystem/vis_contents_update/StartLoadingMap() + can_fire = FALSE + +/datum/controller/subsystem/vis_contents_update/StopLoadingMap() + can_fire = TRUE + +// Largely copied from SSicon_update. +/datum/controller/subsystem/vis_contents_update/fire(resumed = FALSE, no_mc_tick = FALSE) + if(!queue_refs.len) + can_fire = FALSE + return + var/i = 0 + while (i < queue_refs.len) + i++ + var/atom/A = queue_refs[i] + if(QDELETED(A)) + continue + if(Master.map_loading) + queue_refs.Cut(1, i+1) + return + A.vis_update_queued = FALSE + A.update_vis_contents(force_no_queue = TRUE) + if (no_mc_tick) + CHECK_TICK + else if (MC_TICK_CHECK) + queue_refs.Cut(1, i+1) + return + queue_refs.Cut() + +/atom + var/vis_update_queued = FALSE + +/atom/proc/queue_vis_contents_update() + if(vis_update_queued) + return + vis_update_queued = TRUE + SSvis_contents_update.queue_refs.Add(src) + SSvis_contents_update.can_fire = TRUE + +// Horrible colon syntax below is because vis_contents +// exists in /atom.vars, but will not compile. No idea why. +/atom/proc/add_vis_contents(adding) + src:vis_contents |= adding + +/atom/proc/remove_vis_contents(removing) + src:vis_contents -= removing + +/atom/proc/clear_vis_contents() + src:vis_contents = null + +/atom/proc/set_vis_contents(list/adding) + src:vis_contents = adding + +/atom/proc/get_vis_contents_to_add() + return + +/atom/proc/update_vis_contents(force_no_queue = FALSE) + if(!force_no_queue && (!SSvis_contents_update.initialized || TICK_CHECK)) + queue_vis_contents_update() + return + vis_update_queued = FALSE + var/new_vis_contents = get_vis_contents_to_add() + if(length(new_vis_contents)) + set_vis_contents(new_vis_contents) + else if(length(src:vis_contents)) + clear_vis_contents() + +/image/proc/add_vis_contents(adding) + vis_contents |= adding + +/image/proc/remove_vis_contents(removing) + vis_contents -= removing + +/image/proc/clear_vis_contents() + vis_contents.Cut() + +/image/proc/set_vis_contents(list/adding) + vis_contents = adding diff --git a/code/controllers/subsystems/weather.dm b/code/controllers/subsystems/weather.dm new file mode 100644 index 00000000000..4196f51905c --- /dev/null +++ b/code/controllers/subsystems/weather.dm @@ -0,0 +1,63 @@ +SUBSYSTEM_DEF(weather) + name = "Weather" + wait = 15 SECONDS + init_order = INIT_ORDER_WEATHER + priority = SS_PRIORITY_WEATHER + flags = SS_BACKGROUND + + var/list/weather_systems = list() + var/list/weather_by_z = list() + var/list/processing_systems + +/datum/controller/subsystem/weather/stat_entry() + ..("all systems: [length(weather_systems)], processing systems: [length(processing_systems)]") + +/datum/controller/subsystem/weather/Initialize(start_timeofday) + . = ..() + for(var/obj/abstract/weather_system/weather as anything in weather_systems) + weather.init_weather() + return SS_INIT_SUCCESS + +/datum/controller/subsystem/weather/fire(resumed) + + if(!resumed) + processing_systems = weather_systems.Copy() + + var/obj/abstract/weather_system/weather + while(processing_systems.len) + weather = processing_systems[processing_systems.len] + processing_systems.len-- + weather.tick() + if(MC_TICK_CHECK) + return + +///Sets a weather state to use for a given z level/z level stack. +/datum/controller/subsystem/weather/proc/setup_weather_system(var/topmost_level, var/singleton/state/weather/initial_state) + //First check and clear any existing weather system on the level + var/obj/abstract/weather_system/WS = weather_by_z["[topmost_level]"] + if(WS) + unregister_weather_system(WS) + qdel(WS) + //Create the new weather system and let it register itself + new /obj/abstract/weather_system(locate(1, 1, text2num(topmost_level)), topmost_level, initial_state) + +///Registers a given weather system obj for getting updates by SSweather. +/datum/controller/subsystem/weather/proc/register_weather_system(var/obj/abstract/weather_system/WS) + if(weather_by_z["[WS.z]"]) + CRASH("Trying to register another weather system on the same z-level([WS.z]) as an existing one!") + weather_systems |= WS + + //Mark all affected z-levels + var/list/affected = GetConnectedZlevels(WS.z) + for(var/Z in affected) + if(weather_by_z["[Z]"]) + CRASH("Trying to register another weather system on the same z-level([Z]) as an existing one!") + weather_by_z["[Z]"] = WS + +///Remove a weather systeam from the processing lists. +/datum/controller/subsystem/weather/proc/unregister_weather_system(var/obj/abstract/weather_system/WS) + //Clear any and all references to our weather object + for(var/Z = 1 to length(weather_by_z)) + if(weather_by_z["[Z]"] == WS) + weather_by_z["[Z]"] = null + weather_systems -= WS diff --git a/code/datums/datum.dm b/code/datums/datum.dm index 7d422e90ce8..26512578846 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -34,6 +34,9 @@ /// A weak reference to another datum var/datum/weakref/weak_reference + /// Used to avoid unnecessary refstring creation in Destroy(). + var/tmp/has_state_machine = FALSE + #ifdef REFERENCE_TRACKING /// When was this datum last touched by a reftracker? /// If this value doesn't match with the start of the search diff --git a/code/datums/repositories/sound_channels.dm b/code/datums/repositories/sound_channels.dm index 9a11339f9ad..4529bb0d9d9 100644 --- a/code/datums/repositories/sound_channels.dm +++ b/code/datums/repositories/sound_channels.dm @@ -12,9 +12,12 @@ var/repository/sound_channels/sound_channels = new() var/list/keys_by_channel // So we know who to blame if we run out var/channel_ceiling = 1024 // Initial value is the current BYOND maximum number of channels + var/weather_channel + /repository/sound_channels/New() ..() available_channels = new() + weather_channel = RequestChannel("WEATHER") /repository/sound_channels/proc/RequestChannel(key) . = RequestChannels(key, 1) diff --git a/code/datums/state_machine.dm b/code/datums/state_machine.dm new file mode 100644 index 00000000000..85bc10eb0b5 --- /dev/null +++ b/code/datums/state_machine.dm @@ -0,0 +1,104 @@ +// List and procs for caching state machine instances. +var/global/list/state_machines = list() + +/proc/get_state_machine(var/datum/holder, var/base_type) + if(istype(holder) && base_type && holder.has_state_machine) + var/list/machines = global.state_machines["\ref[holder]"] + return islist(machines) && machines[base_type] + +/proc/add_state_machine(var/datum/holder, var/base_type, var/fsm_type) + if(istype(holder) && base_type) + var/holder_ref = "\ref[holder]" + var/list/machines = global.state_machines[holder_ref] + if(!islist(machines)) + machines = list() + global.state_machines[holder_ref] = machines + if(!machines[base_type]) + if(!fsm_type) + fsm_type = base_type + var/datum/state_machine/machine = new fsm_type(holder) + machines[base_type] = machine + holder.has_state_machine = TRUE + return machine + +/proc/remove_state_machine(var/datum/holder, var/base_type) + if(istype(holder) && base_type && holder.has_state_machine) + var/holder_ref = "\ref[holder]" + var/list/machines = global.state_machines[holder_ref] + if(length(machines)) + machines -= base_type + if(!length(machines)) + global.state_machines -= holder_ref + holder.has_state_machine = FALSE + return TRUE + return FALSE + +// This contains the current state of the FSM and should be held by whatever the FSM is controlling. +// Unlike the individual states and their transitions, the state machine objects are not singletons, and hence aren't `/decl`s. +/datum/state_machine + var/datum/weakref/holder_ref + var/base_type = /datum/state_machine + var/expected_type = /datum + var/singleton/state/current_state = null // Acts both as a ref to the current state and holds which state it will default to on init. + +/datum/state_machine/New(var/datum/_holder) + ..() + if(!istype(_holder)) + stack_trace("Non-datum holder supplied to [type] New().") + else + holder_ref = WEAKREF(_holder) + set_state(current_state) + +/datum/state_machine/Destroy() + current_state = null + return ..() + +// Resets back to our initial state. +/datum/state_machine/proc/reset() + var/datum/holder_instance = get_holder() + if(istype(current_state)) + current_state.exited_state(holder_instance) + current_state = initial(current_state) + if(ispath(current_state, /singleton/state)) + current_state = GET_SINGLETON(current_state) + current_state.entered_state(holder_instance) + else + current_state = null + return current_state + +// Retrieve and validate our holder instance from the cached weakref. +/datum/state_machine/proc/get_holder() + var/datum/holder = holder_ref?.resolve() + if(istype(holder) && !QDELETED(holder)) + return holder + +// Makes the FSM enter a new state, if it can, based on it's current state, that state's transitions, and the holder's status. +// Call it in the holder's `process()`, or whenever you need to. +/datum/state_machine/proc/evaluate() + var/datum/holder_instance = get_holder() + var/list/options = current_state.get_open_transitions(holder_instance) + if(LAZYLEN(options)) + var/singleton/state_transition/choice = choose_transition(options) + current_state.exited_state(holder_instance) + current_state = choice.target + current_state.entered_state(holder_instance) + return current_state + +// Decides which transition to walk into, to the next state. +// By default it chooses the first one on the list. +/datum/state_machine/proc/choose_transition(list/valid_transitions) + return valid_transitions[1] + +// Forces the FSM to switch to a specific state, no matter what. +// Use responsibly. +/datum/state_machine/proc/set_state(new_state_type) + var/datum/holder_instance = get_holder() + if(istype(current_state)) + current_state.exited_state(holder_instance) + if(ispath(new_state_type)) + current_state = GET_SINGLETON(new_state_type) + else // need to include null here, so we can't do an istype + current_state = new_state_type + if(istype(current_state)) + current_state.entered_state(holder_instance) + return current_state diff --git a/code/datums/state_machine/state.dm b/code/datums/state_machine/state.dm new file mode 100644 index 00000000000..8c926b44b75 --- /dev/null +++ b/code/datums/state_machine/state.dm @@ -0,0 +1,30 @@ +// An individual state, defined as a `/singleton` to save memory. +// On a directed graph, these would be the nodes themselves, connected to each other by unidirectional arrows. +/singleton/state + // Transition decl types, which get turned into refs to those types. + // Note that the order DOES matter, as decls earlier in the list have higher priority + // if more than one becomes 'open'. + var/list/transitions = null + +/singleton/state/Initialize() + . = ..() + for(var/i in 1 to LAZYLEN(transitions)) + var/singleton/state_transition/T = GET_SINGLETON(transitions[i]) + T.from += src + transitions[i] = T + +// Returns a list of transitions that a FSM could switch to. +// Note that `holder` is NOT the FSM, but instead the thing the FSM is attached to. +/singleton/state/proc/get_open_transitions(datum/holder) + for(var/singleton/state_transition/T as anything in transitions) + if(T.is_open(holder)) + LAZYADD(., T) + +// Stub for child states to modify the holder when switched to. +// Again, `holder` is not the FSM. +/singleton/state/proc/entered_state(datum/holder) + return + +// Another stub for when leaving a state. +/singleton/state/proc/exited_state(datum/holder) + return diff --git a/code/datums/state_machine/transition.dm b/code/datums/state_machine/transition.dm new file mode 100644 index 00000000000..1467ad99352 --- /dev/null +++ b/code/datums/state_machine/transition.dm @@ -0,0 +1,16 @@ +// Used to connect `/singleton/state`s together so the FSM knows what state to switch to, and on what conditions. +// On a directed graph, these would be the arrows connecting the nodes representing states. +/singleton/state_transition + var/list/from = null + var/singleton/state/target = null + +// Called by one or more state decls acting as nodes in a directed graph. +/singleton/state_transition/Initialize() + . = ..() + LAZYINITLIST(from) + if(ispath(target)) + target = GET_SINGLETON(target) + +// Tells the FSM if it should or should not be allowed to transfer to the target state. +/singleton/state_transition/proc/is_open(datum/holder) + return FALSE diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 230633386fd..56ee7def3b2 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -72,6 +72,8 @@ var/global/list/area_blurb_stated_to = list() /// Used to filter description showing across subareas. var/area_blurb_category + var/tmp/is_outside = OUTSIDE_NO + // Don't move this to Initialize(). Things in here need to run before SSatoms does. /area/New() // DMMS hook - Required for areas to work properly. @@ -462,6 +464,7 @@ var/list/mob/living/forced_ambiance_list = new /proc/ChangeArea(var/turf/T, var/area/A) if(!istype(A)) CRASH("Area change attempt failed: invalid area supplied.") + var/old_outside = T.is_outside() var/area/old_area = get_area(T) if(old_area == A) return @@ -477,6 +480,11 @@ var/list/mob/living/forced_ambiance_list = new for(var/obj/machinery/M in T) M.shuttle_move(T) + T.last_outside_check = OUTSIDE_UNCERTAIN + var/outside_changed = T.is_outside() != old_outside + if(T.is_outside == OUTSIDE_AREA && outside_changed) + T.update_weather() + /** * Displays an area blurb on a mob's screen. * diff --git a/code/game/jobs/faction/faction.dm b/code/game/jobs/faction/faction.dm index 6b0b98a41fa..70eaf663f50 100644 --- a/code/game/jobs/faction/faction.dm +++ b/code/game/jobs/faction/faction.dm @@ -40,6 +40,8 @@ . += role else var/datum/job/role = SSjobs.type_occupations[path] + if(!role) + continue if(LAZYACCESS(job_species_blacklist, role.title)) role.blacklisted_species = job_species_blacklist[role.title] else diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index e056e77c48c..fa0afea637c 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1227,3 +1227,6 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out. /obj/item/proc/is_shovel() return FALSE + +/obj/item/proc/gives_weather_protection() + return FALSE diff --git a/code/game/objects/items/sticker.dm b/code/game/objects/items/sticker.dm index 650e81ae978..bb2d48f76e8 100644 --- a/code/game/objects/items/sticker.dm +++ b/code/game/objects/items/sticker.dm @@ -62,14 +62,14 @@ to_chat(user, SPAN_NOTICE("You attach \the [src] to \the [A].")) user.drop_from_inventory(src, A) attached = WEAKREF(A) - A.vis_contents += src + A.add_vis_contents(src) /obj/item/sticker/proc/remove_sticker(var/mob/user) user.put_in_hands(src) var/atom/movable/attached_atom = attached.resolve() if(attached_atom) to_chat(user, SPAN_NOTICE("You remove \the [src] from \the [attached_atom].")) - attached_atom.vis_contents -= src + attached_atom.remove_vis_contents(src) attached = null /obj/item/sticker/googly_eye diff --git a/code/game/objects/items/umbrella.dm b/code/game/objects/items/umbrella.dm new file mode 100644 index 00000000000..72d36386daa --- /dev/null +++ b/code/game/objects/items/umbrella.dm @@ -0,0 +1,65 @@ +/obj/item/umbrella + name = "umbrella" + desc = "A perfect tool to protect you from the elements." + icon = 'icons/obj/item/umbrellas.dmi' + icon_state = "umbrella_yellow_closed" + contained_sprite = TRUE + w_class = ITEMSIZE_SMALL + matter = list(MATERIAL_PLASTIC = 1000) + force = 5 + sharp = TRUE + hitsound = 'sound/weapons/bladeslice.ogg' + attack_verb = list("pokes", "stabs") + /// If the umbrella is open or not. + var/is_open = FALSE + /// Colour of the umbrella. Must be one present in the dmi. + var/umbrella_color + +/obj/item/umbrella/Initialize(mapload, ...) + . = ..() + if(!umbrella_color) + umbrella_color = pick("black", "red", "yellow", "green") + icon_state = "umbrella_[umbrella_color]" + item_state = icon_state + "_closed" + update_icon() + +/obj/item/umbrella/attack_self(mob/user) + . = ..() + if(!user.incapacitated()) + if(!is_open) + to_chat(user, SPAN_NOTICE("You unfurl \the [src].")) + item_state = "umbrella_[umbrella_color]_open" + w_class = ITEMSIZE_LARGE + is_open = TRUE + force = 1 + sharp = FALSE + attack_verb = list("taps") + hitsound = /singleton/sound_category/punchmiss_sound + else + to_chat(user, SPAN_NOTICE("You close up \the [src].")) + item_state = "umbrella_[umbrella_color]_closed" + w_class = initial(w_class) + is_open = FALSE + force = initial(force) + sharp = initial(sharp) + attack_verb = initial(attack_verb) + hitsound = initial(hitsound) + +/obj/item/umbrella/gives_weather_protection() + return is_open ? TRUE : FALSE + +/obj/item/umbrella/red + umbrella_color = "red" + icon_state = "umbrella_red_closed" + +/obj/item/umbrella/black + umbrella_color = "black" + icon_state = "umbrella_black_closed" + +/obj/item/umbrella/yellow + umbrella_color = "yellow" + icon_state = "umbrella_yellow_closed" + +/obj/item/umbrella/green + umbrella_color = "green" + icon_state = "umbrella_green_closed" diff --git a/code/game/objects/items/weapons/storage/storage.dm b/code/game/objects/items/weapons/storage/storage.dm index b4e78be2b68..4102526d646 100644 --- a/code/game/objects/items/weapons/storage/storage.dm +++ b/code/game/objects/items/weapons/storage/storage.dm @@ -432,7 +432,7 @@ stored_end.transform = M_end storage_screens += list(stored_start, stored_continue, stored_end) - storage_start.vis_contents += list(stored_start, stored_continue, stored_end) + storage_start.add_vis_contents(list(stored_start, stored_continue, stored_end)) O.screen_loc = "4:[round((startpoint+endpoint)/2)+2],2:16" O.maptext = "" diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index df20f2f7f5a..dc5e738fd55 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -608,7 +608,7 @@ /obj/structure/closet/proc/end_door_animation() is_animating_door = FALSE // comment this out and the line below to manually tweak the animation end state by fiddling with the door_anim vars to match the open door icon - vis_contents -= door_obj + remove_vis_contents(door_obj) update_icon() compile_overlays(src) @@ -639,7 +639,7 @@ /obj/structure/closet/proc/end_door_animation_alt() is_animating_door = FALSE // comment this out and the line below to manually tweak the animation end state by fiddling with the door_anim vars to match the open door icon - vis_contents -= door_obj_alt + remove_vis_contents(door_obj_alt) update_icon() compile_overlays(src) diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm index 13d3ed83bf9..a395478c465 100644 --- a/code/game/objects/structures/mirror.dm +++ b/code/game/objects/structures/mirror.dm @@ -148,7 +148,7 @@ for(var/mob/living/carbon/human/H in loc) check_vampire_enter(H.loc, H) - vis_contents += get_turf(mirror) + add_vis_contents(get_turf(mirror)) /obj/effect/reflection/proc/check_vampire_enter(var/turf/T, var/mob/living/carbon/human/H) if(!istype(H)) diff --git a/code/game/objects/structures/stool_bed_chair_nest/bed.dm b/code/game/objects/structures/stool_bed_chair_nest/bed.dm index 954c5369af5..f5f2bf084f7 100644 --- a/code/game/objects/structures/stool_bed_chair_nest/bed.dm +++ b/code/game/objects/structures/stool_bed_chair_nest/bed.dm @@ -415,7 +415,7 @@ add_overlay(light) if(vitals) vitals.update_monitor() - vis_contents += vitals + add_vis_contents(vitals) /obj/structure/bed/roller/attackby(obj/item/attacking_item, mob/user) if(iswrench(attacking_item) || istype(attacking_item, /obj/item/stack) || iswirecutter(attacking_item)) diff --git a/code/game/turfs/simulated/floor.dm b/code/game/turfs/simulated/floor.dm index 0de988c20c5..30612dc6b38 100644 --- a/code/game/turfs/simulated/floor.dm +++ b/code/game/turfs/simulated/floor.dm @@ -3,6 +3,7 @@ desc = "The naked hull." icon = 'icons/turf/flooring/plating.dmi' icon_state = "plating" + is_outside = OUTSIDE_AREA // Damage to flooring. var/broken diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 87f1da81840..7737047c69f 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -62,6 +62,23 @@ /// WARNING: Currently to use a density shortcircuiting this does not support dense turfs with special allow through function var/pathing_pass_method = TURF_PATHING_PASS_DENSITY + // Some quick notes on the vars below: is_outside should be left set to OUTSIDE_AREA unless you + // EXPLICITLY NEED a turf to have a different outside state to its area (ie. you have used a + // roofing tile). By default, it will ask the area for the state to use, and will update on + // area change. When dealing with weather, it will check the entire z-column for interruptions + // that will prevent it from using its own state, so a floor above a level will generally + // override both area is_outside, and turf is_outside. The only time the base value will be used + // by itself is if you are dealing with a non-multiz level, or the top level of a multiz chunk. + + // Weather relies on is_outside to determine if it should apply to a turf or not and will be + // automatically updated on ChangeTurf set_outside etc. Don't bother setting it manually, it will + // get overridden almost immediately. + + // TL;DR: just leave these vars alone. + var/tmp/obj/abstract/weather_system/weather + var/tmp/is_outside = OUTSIDE_AREA + var/tmp/last_outside_check = OUTSIDE_UNCERTAIN + // Parent code is duplicated in here instead of ..() for performance reasons. // There's ALSO a copy of this in mine_turfs.dm! /turf/Initialize(mapload, ...) @@ -516,6 +533,89 @@ var/const/enterloopsanity = 100 /turf/proc/is_floor() return FALSE +/turf/proc/is_outside() + + // Can't rain inside or through solid walls. + // TODO: dense structures like full windows should probably also block weather. + if(density) + return OUTSIDE_NO + + if(last_outside_check != OUTSIDE_UNCERTAIN) + return last_outside_check + + // What is our local outside value? + // Some turfs can be roofed irrespective of the turf above them in multiz. + // I have the feeling this is redundat as a roofed turf below max z will + // have a floor above it, but ah well. + . = is_outside + if(. == OUTSIDE_AREA) + var/area/A = get_area(src) + . = A ? A.is_outside : OUTSIDE_NO + + // If we are in a multiz volume and not already inside, we return + // the outside value of the highest unenclosed turf in the stack. + if(HasAbove(z)) + . = OUTSIDE_YES // assume for the moment we're unroofed until we learn otherwise. + var/turf/top_of_stack = src + while(HasAbove(top_of_stack.z)) + var/turf/next_turf = GetAbove(top_of_stack) + if(!next_turf.is_open()) + return OUTSIDE_NO + top_of_stack = next_turf + // If we hit the top of the stack without finding a roof, we ask the upmost turf if we're outside. + . = top_of_stack.is_outside() + last_outside_check = . // Cache this for later calls. + +/turf/proc/set_outside(var/new_outside, var/skip_weather_update = FALSE) + if(is_outside == new_outside) + return FALSE + + is_outside = new_outside + if(!skip_weather_update) + update_weather() + + last_outside_check = OUTSIDE_UNCERTAIN + + if(!HasBelow(z)) + return TRUE + + // Invalidate the outside check cache for turfs below us. + var/turf/checking = src + while(HasBelow(checking.z)) + checking = GetBelow(checking) + if(!isturf(checking)) + break + checking.last_outside_check = OUTSIDE_UNCERTAIN + if(!checking.is_open()) + break + return TRUE + +/turf/proc/update_weather(var/obj/abstract/weather_system/new_weather, var/force_update_below = FALSE) + + if(isnull(new_weather)) + new_weather = SSweather.weather_by_z["[z]"] + + // We have a weather system and we are exposed to it; update our vis contents. + if(istype(new_weather) && is_outside()) + if(weather != new_weather) + if(weather) + remove_vis_contents(weather.vis_contents_additions) + weather = new_weather + add_vis_contents(weather.vis_contents_additions) + . = TRUE + + // We are indoors or there is no local weather system, clear our vis contents. + else if(weather) + remove_vis_contents(weather.vis_contents_additions) + weather = null + . = TRUE + + // Propagate our weather downwards if we permit it. + if(force_update_below || (is_open() && .)) + var/turf/below = GetBelow(src) + if(below) + below.update_weather(new_weather) + /turf/proc/remove_cleanables() for(var/obj/effect/O in src) if(istype(O,/obj/effect/rune) || istype(O,/obj/effect/decal/cleanable)) diff --git a/code/game/turfs/turf_changing.dm b/code/game/turfs/turf_changing.dm index 1361ef09022..7cdd4ca0650 100644 --- a/code/game/turfs/turf_changing.dm +++ b/code/game/turfs/turf_changing.dm @@ -52,6 +52,8 @@ var/list/old_corners = corners var/list/old_blueprints = blueprints var/list/old_decals = decals + var/old_outside = is_outside + var/old_is_open = is_open() changing_turf = TRUE @@ -105,6 +107,11 @@ if(tell_universe) GLOB.universe.OnTurfChange(W) + // we check the var rather than the proc, because area outside values usually shouldn't be set on turfs + W.last_outside_check = OUTSIDE_UNCERTAIN + if(W.is_outside != old_outside) + W.set_outside(old_outside, skip_weather_update = TRUE) + SSair.mark_for_update(src) //handle the addition of the new turf. if(!W.baseturf) @@ -119,6 +126,8 @@ W.post_change(!mapload) + W.update_weather(force_update_below = W.is_open() != old_is_open) + . = W for(var/turf/T in RANGE_TURFS(1, src)) diff --git a/code/modules/abstract/_abstract.dm b/code/modules/abstract/_abstract.dm new file mode 100644 index 00000000000..8c9bf080c84 --- /dev/null +++ b/code/modules/abstract/_abstract.dm @@ -0,0 +1,21 @@ +/obj/abstract + name = "" + icon = 'icons/effects/landmarks.dmi' + icon_state = "x2" + simulated = FALSE + density = FALSE + anchored = TRUE + abstract_type = /obj/abstract + invisibility = INVISIBILITY_ABSTRACT + +/obj/abstract/Initialize() + . = ..() + verbs.Cut() + //Let mappers see the damn thing by just making them invisible here + opacity = FALSE + alpha = 0 + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + +/obj/abstract/ex_act() + SHOULD_CALL_PARENT(FALSE) + return diff --git a/code/modules/abstract/abstract_exterior_marker.dm b/code/modules/abstract/abstract_exterior_marker.dm new file mode 100644 index 00000000000..8e56ea87c75 --- /dev/null +++ b/code/modules/abstract/abstract_exterior_marker.dm @@ -0,0 +1,21 @@ +/obj/abstract/exterior_marker + var/set_outside + +/obj/abstract/exterior_marker/Initialize() + ..() + var/turf/T = loc + if(istype(T)) + T.set_outside(set_outside) + return INITIALIZE_HINT_QDEL + +/obj/abstract/exterior_marker/outside + name = "Outside" + set_outside = OUTSIDE_NO + +/obj/abstract/exterior_marker/inside + name = "Inside" + set_outside = OUTSIDE_YES + +/obj/abstract/exterior_marker/use_area + name = "Use Area Outside" + set_outside = OUTSIDE_AREA diff --git a/code/modules/abstract/abstract_weather_marker.dm b/code/modules/abstract/abstract_weather_marker.dm new file mode 100644 index 00000000000..3117a79692a --- /dev/null +++ b/code/modules/abstract/abstract_weather_marker.dm @@ -0,0 +1,16 @@ +/obj/abstract/weather_marker + name = "Weather Marker" + /// The weather type we want. This needs to be a weather singleton type, such as /singleton/state/weather/rain/storm. + /// These should be used for forcing weather in an away site or an event map. + var/weather_type + +/obj/abstract/weather_marker/Initialize() + ..() + return INITIALIZE_HINT_LATELOAD + +/obj/abstract/weather_marker/LateInitialize() + . = ..() + if(!ispath(weather_type)) + log_debug("Invalid weather type mapped in at [x] [y] [z]!") + return INITIALIZE_HINT_QDEL + SSweather.setup_weather_system(z, weather_type) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index abb7ef61209..43b2bf2b46b 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -232,7 +232,10 @@ var/list/admin_verbs_debug = list( /turf/proc/view_chunk, /turf/proc/update_chunk, /client/proc/profiler_start, - /client/proc/rustg_send_udp + /client/proc/rustg_send_udp, + /datum/admins/proc/force_initialize_weather, + /datum/admins/proc/force_weather_state, + /datum/admins/proc/force_kill_weather ) var/list/admin_verbs_paranoid_debug = list( diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index c9f9bdfb051..d79d40ba219 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -550,6 +550,7 @@ var/light_applied var/brightness_on var/on = 0 + var/protects_against_weather = FALSE /obj/item/clothing/head/Initialize(mapload, material_key) . = ..() @@ -986,18 +987,19 @@ ) name = "suit" species_sprite_adaption_type = WORN_SUIT - var/fire_resist = T0C+100 body_parts_covered = UPPER_TORSO|LOWER_TORSO|ARMS|LEGS allowed = list(/obj/item/tank/emergency_oxygen) armor = null slot_flags = SLOT_OCLOTHING - var/blood_overlay_type = "suit" siemens_coefficient = 0.9 w_class = ITEMSIZE_NORMAL species_restricted = list("exclude",BODYTYPE_VAURCA_BREEDER,BODYTYPE_VAURCA_WARFORM,BODYTYPE_TESLA_BODY) - valid_accessory_slots = list(ACCESSORY_SLOT_ARMBAND, ACCESSORY_SLOT_GENERIC, ACCESSORY_SLOT_CAPE, ACCESSORY_SLOT_UTILITY_MINOR) + var/fire_resist = T0C+100 + var/blood_overlay_type = "suit" + var/protects_against_weather = FALSE + /obj/item/clothing/suit/return_own_image() var/image/our_image if(contained_sprite) diff --git a/code/modules/clothing/head/helmet.dm b/code/modules/clothing/head/helmet.dm index 023d259b474..65725f26b76 100644 --- a/code/modules/clothing/head/helmet.dm +++ b/code/modules/clothing/head/helmet.dm @@ -24,6 +24,7 @@ var/obj/machinery/camera/camera drop_sound = 'sound/items/drop/helm.ogg' pickup_sound = 'sound/items/pickup/helm.ogg' + protects_against_weather = TRUE var/has_storage = TRUE var/obj/item/storage/internal/helmet/hold diff --git a/code/modules/clothing/head/wide_hats.dm b/code/modules/clothing/head/wide_hats.dm index 945399ad96b..be4a5c66c85 100644 --- a/code/modules/clothing/head/wide_hats.dm +++ b/code/modules/clothing/head/wide_hats.dm @@ -26,6 +26,7 @@ contained_sprite = TRUE icon_state = "cowboyhat" item_state = "cowboyhat" + protects_against_weather = TRUE /obj/item/clothing/head/cowboy/wide name = "wide-brimmed cowboy hat" diff --git a/code/modules/clothing/spacesuits/spacesuits.dm b/code/modules/clothing/spacesuits/spacesuits.dm index bc2817e0bc0..d53be02382e 100644 --- a/code/modules/clothing/spacesuits/spacesuits.dm +++ b/code/modules/clothing/spacesuits/spacesuits.dm @@ -64,6 +64,7 @@ max_pressure_protection = SPACE_SUIT_MAX_PRESSURE siemens_coefficient = 0.5 species_restricted = list("exclude",BODYTYPE_DIONA,BODYTYPE_GOLEM,BODYTYPE_VAURCA_BULWARK) + protects_against_weather = TRUE var/list/supporting_limbs //If not-null, automatically splints breaks. Checked when removing the suit. diff --git a/code/modules/clothing/suits/hoodies.dm b/code/modules/clothing/suits/hoodies.dm index 16a08228f95..01c6cb1dfd7 100644 --- a/code/modules/clothing/suits/hoodies.dm +++ b/code/modules/clothing/suits/hoodies.dm @@ -46,6 +46,7 @@ bio = ARMOR_BIO_MINOR ) siemens_coefficient = 0.75 + protects_against_weather = TRUE /obj/item/clothing/head/winterhood name = "winter hood" @@ -59,6 +60,7 @@ flags_inv = HIDEEARS | BLOCKHAIR | HIDEEARS min_cold_protection_temperature = SPACE_SUIT_MIN_COLD_PROTECTION_TEMPERATURE canremove = 0 + protects_against_weather = TRUE var/hooded = FALSE /obj/item/clothing/head/winterhood/Initialize(mapload, material_key) diff --git a/code/modules/clothing/suits/miscellaneous.dm b/code/modules/clothing/suits/miscellaneous.dm index effd737aaa8..c55c7fa0d59 100644 --- a/code/modules/clothing/suits/miscellaneous.dm +++ b/code/modules/clothing/suits/miscellaneous.dm @@ -354,6 +354,7 @@ contained_sprite = TRUE blood_overlay_type = "coat" body_parts_covered = UPPER_TORSO|ARMS + protects_against_weather = TRUE /obj/item/clothing/suit/storage/toggle/trench/grey name = "grey trenchcoat" diff --git a/code/modules/cooking/trays.dm b/code/modules/cooking/trays.dm index c6c325cd797..1fe3b9973d3 100644 --- a/code/modules/cooking/trays.dm +++ b/code/modules/cooking/trays.dm @@ -104,7 +104,7 @@ I.forceMove(src) auto_align(I, click_params) current_weight += I.w_class - vis_contents += I + add_vis_contents(I) I.vis_flags |= VIS_INHERIT_LAYER | VIS_INHERIT_PLANE item_equipped_event.register(I, src, PROC_REF(pick_up)) GLOB.destroyed_event.register(I, src, PROC_REF(unload_item)) diff --git a/code/modules/effects/map_effects/portal.dm b/code/modules/effects/map_effects/portal.dm index 516f5d8386a..9f92fdbd914 100644 --- a/code/modules/effects/map_effects/portal.dm +++ b/code/modules/effects/map_effects/portal.dm @@ -202,7 +202,7 @@ when portals are shortly lived, or when portals are made to be obvious with spec return var/turf/T = P.counterpart.get_focused_turf() - P.vis_contents += T + P.add_vis_contents(T) var/list/things = list() DVIEW(things, world.view, T, INVISIBILITY_LIGHTING) @@ -211,7 +211,7 @@ when portals are shortly lived, or when portals are made to be obvious with spec if(turf in observed_turfs) // Avoid showing the same turf twice or more for improved performance. continue - P.vis_contents += turf + P.add_vis_contents(turf) observed_turfs += turf P.calculate_dimensions() diff --git a/code/modules/heavy_vehicle/equipment/combat.dm b/code/modules/heavy_vehicle/equipment/combat.dm index 5bf8fc77e4f..21dc5ccd3f9 100644 --- a/code/modules/heavy_vehicle/equipment/combat.dm +++ b/code/modules/heavy_vehicle/equipment/combat.dm @@ -379,7 +379,7 @@ /obj/aura/mechshield/added_to(mob/living/target) ..() - target.vis_contents += src + target.add_vis_contents(src) dir = target.dir /obj/aura/mechshield/proc/set_holder(var/obj/item/mecha_equipment/shield/holder) @@ -387,7 +387,7 @@ /obj/aura/mechshield/Destroy() if(user) - user.vis_contents -= src + user.remove_vis_contents(src) shields = null . = ..() diff --git a/code/modules/heavy_vehicle/mech_life.dm b/code/modules/heavy_vehicle/mech_life.dm index 8bc416d7cd2..5bd0df9264c 100644 --- a/code/modules/heavy_vehicle/mech_life.dm +++ b/code/modules/heavy_vehicle/mech_life.dm @@ -96,6 +96,7 @@ return total_draw /mob/living/heavy_vehicle/handle_environment(var/datum/gas_mixture/environment) + ..() if(!environment) return //Mechs and vehicles in general can be assumed to just tend to whatever ambient temperature if(abs(environment.temperature - bodytemperature) > 10 ) diff --git a/code/modules/mob/living/carbon/alien/diona/life.dm b/code/modules/mob/living/carbon/alien/diona/life.dm index 1918d4b0881..22d280b2d8c 100644 --- a/code/modules/mob/living/carbon/alien/diona/life.dm +++ b/code/modules/mob/living/carbon/alien/diona/life.dm @@ -1,6 +1,7 @@ //Dionaea regenerate health and nutrition in light. /mob/living/carbon/alien/diona/handle_environment(datum/gas_mixture/environment) + ..() if(environment && consume_nutrition_from_air) environment.remove(diona_handle_air(get_dionastats(), pressure)) diff --git a/code/modules/mob/living/carbon/alien/life.dm b/code/modules/mob/living/carbon/alien/life.dm index ba08e1f9850..b24e70e9afb 100644 --- a/code/modules/mob/living/carbon/alien/life.dm +++ b/code/modules/mob/living/carbon/alien/life.dm @@ -143,6 +143,7 @@ return 1 /mob/living/carbon/alien/handle_environment(var/datum/gas_mixture/environment) + ..() // Both alien subtypes survive in vaccum and suffer in high temperatures, // so I'll just define this once, for both (see radiation comment above) if(!environment) return diff --git a/code/modules/mob/living/carbon/brain/life.dm b/code/modules/mob/living/carbon/brain/life.dm index 011c4f405a5..266511428ae 100644 --- a/code/modules/mob/living/carbon/brain/life.dm +++ b/code/modules/mob/living/carbon/brain/life.dm @@ -35,6 +35,7 @@ /mob/living/carbon/brain/handle_environment(datum/gas_mixture/environment) + ..() if(!environment) return var/environment_heat_capacity = environment.heat_capacity() diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index a8ed6a9f399..ba20691a832 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -303,6 +303,8 @@ return breath /mob/living/carbon/human/handle_environment(datum/gas_mixture/environment) + ..() + if(!environment) return diff --git a/code/modules/mob/living/carbon/slime/life.dm b/code/modules/mob/living/carbon/slime/life.dm index 62b27fa611b..b288e76ff00 100644 --- a/code/modules/mob/living/carbon/slime/life.dm +++ b/code/modules/mob/living/carbon/slime/life.dm @@ -16,6 +16,7 @@ handle_speech_and_mood() /mob/living/carbon/slime/handle_environment(datum/gas_mixture/environment) + ..() if(!environment) adjustToxLoss(rand(10,20)) return diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm index 1d7bd462326..759b99ac6ce 100644 --- a/code/modules/mob/living/life.dm +++ b/code/modules/mob/living/life.dm @@ -10,10 +10,10 @@ if(!loc) return - var/datum/gas_mixture/environment = loc.return_air() + var/datum/gas_mixture/gas_environment = loc.return_air() //Handle temperature/pressure differences between body and environment - if(environment) - handle_environment(environment) + if(gas_environment) + handle_environment(gas_environment) blinded = 0 // Placing this here just show how out of place it is. @@ -57,9 +57,6 @@ /mob/living/proc/handle_random_events() return -/mob/living/proc/handle_environment(var/datum/gas_mixture/environment) - return - /mob/living/proc/update_pulling() if(pulling) if(incapacitated()) @@ -189,3 +186,57 @@ /mob/living/proc/handle_hud_icons_health() return + +/mob/living + var/datum/weakref/last_weather + +/mob/living/proc/is_outside() + var/turf/T = loc + return istype(T) && T.is_outside() + +/mob/living/proc/get_affecting_weather() + var/turf/my_turf = get_turf(src) + if(!istype(my_turf)) + return + var/turf/actual_loc = loc + // If we're standing in the rain, use the turf weather. + . = istype(actual_loc) && actual_loc.weather + if(!.) // If we're under or inside shelter, use the z-level rain (for ambience) + . = SSweather.weather_by_z["[my_turf.z]"] + +/mob/living/proc/handle_environment(var/datum/gas_mixture/environment) + + SHOULD_CALL_PARENT(TRUE) + + // Handle physical effects of weather. + var/singleton/state/weather/weather_state + var/obj/abstract/weather_system/weather = get_affecting_weather() + if(weather) + weather_state = weather.weather_system.current_state + if(istype(weather_state)) + weather_state.handle_exposure(src, get_weather_exposure(weather), weather) + + // Refresh weather ambience. + // Show messages and play ambience. + if(client) + + // Work out if we need to change or cancel the current ambience sound. + var/send_sound + var/mob_ref = WEAKREF(src) + if(istype(weather_state)) + var/ambient_sounds = !is_outside() ? weather_state.ambient_indoors_sounds : weather_state.ambient_sounds + var/ambient_sound = length(ambient_sounds) && pick(ambient_sounds) + if(GLOB.current_mob_ambience[mob_ref] == ambient_sound) + return + send_sound = ambient_sound + GLOB.current_mob_ambience[mob_ref] = send_sound + else if(mob_ref in GLOB.current_mob_ambience) + GLOB.current_mob_ambience -= mob_ref + else + return + + // Push sound to client. Pipe dream TODO: crossfade between the new and old weather ambience. + sound_to(src, sound(null, repeat = 0, wait = 0, volume = 0, channel = sound_channels.weather_channel)) + if(send_sound) + sound_to(src, sound(send_sound, repeat = TRUE, wait = 0, volume = 30, channel = sound_channels.weather_channel)) + diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm index 682e7ec19bf..8c208a0818a 100644 --- a/code/modules/mob/living/login.dm +++ b/code/modules/mob/living/login.dm @@ -7,4 +7,13 @@ //If they're SSD, remove it so they can wake back up. update_antag_icons(mind) reset_death_timers() + + // Clear our cosmetic/sound weather cooldowns. + var/obj/abstract/weather_system/weather = get_affecting_weather() + + var/mob_ref = WEAKREF(src) + if(istype(weather)) + weather.mob_shown_weather -= mob_ref + weather.mob_shown_wind -= mob_ref + GLOB.current_mob_ambience -= mob_ref return . diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 0844326fb92..254660f4192 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1395,6 +1395,58 @@ var/list/data = list() return data +/mob/proc/get_weather_protection() + for(var/obj/item/brolly in get_active_hand()) + if(brolly.gives_weather_protection()) + LAZYADD(., brolly) + if(!LAZYLEN(.)) + for(var/turf/T as anything in RANGE_TURFS(1, loc)) + for(var/obj/structure/flora/tree in T) + if(tree.protects_against_weather) + LAZYADD(., tree) + +/mob/living/carbon/human/get_weather_protection() + . = ..() + if(!LAZYLEN(.)) + var/obj/item/clothing/head/check_head = get_equipped_item(slot_head_str) + if(!istype(check_head) || !check_head.protects_against_weather) + return + var/obj/item/clothing/suit/check_body = get_equipped_item(slot_wear_suit_str) + if(!istype(check_body) || !check_body.protects_against_weather) + return + LAZYADD(., check_head) + LAZYADD(., check_body) + +/mob/proc/get_weather_exposure() + + // We're inside something else. + if(!isturf(loc)) + return WEATHER_IGNORE + + var/turf/T = loc + // We're under a roof or otherwise shouldn't be being rained on. + if(!T.is_outside()) + + // For non-multiz we'll give everyone some nice ambience. + if(!HasAbove(T.z)) + return WEATHER_ROOFED + + // For multi-z, check the actual weather on the turf above. + // TODO: maybe make this a property of the z-level marker. + var/turf/above = GetAbove(T) + if(above.weather) + return WEATHER_ROOFED + + // Being more than one level down should exempt us from ambience. + return WEATHER_IGNORE + + // Nothing's protecting us from the rain here + var/list/weather_protection = get_weather_protection() + if(LAZYLEN(weather_protection)) + return WEATHER_PROTECTED + + return WEATHER_EXPOSED + #undef UNBUCKLED #undef PARTIALLY_BUCKLED #undef FULLY_BUCKLED diff --git a/code/modules/overmap/exoplanets/decor/_areas.dm b/code/modules/overmap/exoplanets/decor/_areas.dm index 16788afce4c..c9abfa98c46 100644 --- a/code/modules/overmap/exoplanets/decor/_areas.dm +++ b/code/modules/overmap/exoplanets/decor/_areas.dm @@ -3,6 +3,7 @@ ambience = list('sound/effects/wind/wind_2_1.ogg','sound/effects/wind/wind_2_2.ogg','sound/effects/wind/wind_3_1.ogg','sound/effects/wind/wind_4_1.ogg','sound/effects/wind/wind_4_2.ogg','sound/effects/wind/wind_5_1.ogg') always_unpowered = 1 area_flags = AREA_FLAG_INDESTRUCTIBLE_TURFS + is_outside = OUTSIDE_YES /area/exoplanet/adhomai name = "Adhomian Wilderness" diff --git a/code/modules/overmap/exoplanets/decor/_flora.dm b/code/modules/overmap/exoplanets/decor/_flora.dm index 99e485dba20..ea96b9e631d 100644 --- a/code/modules/overmap/exoplanets/decor/_flora.dm +++ b/code/modules/overmap/exoplanets/decor/_flora.dm @@ -4,6 +4,8 @@ anchored = TRUE density = TRUE var/cutting + /// Whether or not you can shelter under this. + var/protects_against_weather = FALSE /obj/structure/flora/proc/dig_up(mob/user) user.visible_message(SPAN_NOTICE("\The [user] begins digging up \the [src]...")) diff --git a/code/modules/overmap/exoplanets/decor/flora/_tree.dm b/code/modules/overmap/exoplanets/decor/flora/_tree.dm index 2cf7b645ba1..fe24e8f7855 100644 --- a/code/modules/overmap/exoplanets/decor/flora/_tree.dm +++ b/code/modules/overmap/exoplanets/decor/flora/_tree.dm @@ -4,6 +4,7 @@ density = TRUE layer = 9 pixel_x = -16 + protects_against_weather = TRUE var/max_chop_health = 180 var/chop_health = 180 //15 hits with steel hatchet, 5 with wielded fireaxe var/fall_force = 60 diff --git a/code/modules/overmap/exoplanets/exoplanet.dm b/code/modules/overmap/exoplanets/exoplanet.dm index 5883933fea9..c5bee382626 100644 --- a/code/modules/overmap/exoplanets/exoplanet.dm +++ b/code/modules/overmap/exoplanets/exoplanet.dm @@ -54,6 +54,9 @@ var/list/possible_themes = list(/datum/exoplanet_theme) var/datum/exoplanet_theme/theme + ///What weather state to use for this planet initially. If null, will not initialize any weather system. Must be a typepath rather than an instance. + var/singleton/state/weather/initial_weather_state = /singleton/state/weather/calm + var/features_budget = 4 var/list/possible_features = list() var/list/spawned_features @@ -197,6 +200,8 @@ /obj/effect/overmap/visitable/sector/exoplanet/proc/build_level() generate_habitability() generate_atmosphere() + if(ispath(initial_weather_state)) + generate_weather() generate_flora() generate_map() generate_features() @@ -533,3 +538,17 @@ /obj/effect/landmark/exoplanet_spawn/proc/do_spawn(obj/effect/overmap/visitable/sector/exoplanet/planet) return + +///Resets the given weather state to our planet replacing the old one, and trigger updates. Can be a type path or instance. +/obj/effect/overmap/visitable/sector/exoplanet/proc/reset_weather(var/singleton/state/weather/W) + initial_weather_state = W + if(!(z in map_z)) + return //It's entire possible the levels weren't initialized yet, so don't bother. + //Tells all our levels exposed to the sky to force change the weather. + SSweather.setup_weather_system(z, initial_weather_state) + +///Setup the initial weather state for the planet. Doesn't apply it to our z levels however. +/obj/effect/overmap/visitable/sector/exoplanet/proc/generate_weather() + if(ispath(initial_weather_state)) + initial_weather_state = GET_SINGLETON(initial_weather_state) + reset_weather(initial_weather_state) diff --git a/code/modules/tgui_input/say_modal/typing.dm b/code/modules/tgui_input/say_modal/typing.dm index b6b1711fd78..e1ed5e194db 100644 --- a/code/modules/tgui_input/say_modal/typing.dm +++ b/code/modules/tgui_input/say_modal/typing.dm @@ -114,7 +114,7 @@ I IS TYPIN'!' /atom/movable/typing_indicator/Destroy() if(master) - master.vis_contents -= src + master.remove_vis_contents(src) if(ismob(master)) var/mob/owner = master if(owner.typing_indicator == src) @@ -131,7 +131,7 @@ I IS TYPIN'!' set_invisibility(INVISIBILITY_MAXIMUM) if(ismovable(master)) var/atom/movable/owner = master - owner.vis_contents -= src + owner.remove_vis_contents(src) shown = FALSE /atom/movable/typing_indicator/proc/show_typing_indicator(var/thinking = FALSE) @@ -149,7 +149,7 @@ I IS TYPIN'!' if(ismovable(master)) var/atom/movable/owner = master - owner.vis_contents += src + owner.add_vis_contents(src) if(!shown) // Animate it popping up from nowhere. diff --git a/code/modules/weather/_weather.dm b/code/modules/weather/_weather.dm new file mode 100644 index 00000000000..098e7a0a2cc --- /dev/null +++ b/code/modules/weather/_weather.dm @@ -0,0 +1,99 @@ +/* + * Notes on weather: + * + * - Weather is a single object that sits in the vis_contents of all outside turfs on + * its associated z-levels and is removed or added by /turf/proc/update_weather(), + * which is usually called from /turf/proc/set_outside(). + * + * - Weather generally assumes any atom that cares about it will ask it directly and + * mobs do this in /mob/living/proc/handle_environment(). + * + * - For this system to be scalable, it should minimize the amount of list-based + * processing it does and be primarily passive, allowing mobs to ignore it or + * poll it on their own time. + * + * - The weather object is queued on SSweather and is polled every fifteen seconds at time + * of writing. This is handled in /obj/abstract/weather_system/proc/tick(). + * + * - When evaluating, weather will generally get more intense or more severe rather than + * jumping around randomly. Each state will set a minimum duration based on min/max time. + * + * - If polled between weather updates there is a chance of modifying wind speed and direction + * instead. + */ + +/obj/abstract/weather_system + plane = PLANE_DEFAULT + layer = ABOVE_ALL_MOB_LAYER + icon = 'icons/effects/weather.dmi' + icon_state = "blank" + invisibility = 0 + appearance_flags = (RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM) + + var/water_material = null // Material to use for the properties of rain. + var/ice_material = null // Material to use for the properties of snow and hail. + + var/list/affecting_zs // What z-levels are we affecting? + var/datum/state_machine/weather/weather_system // What is our internal state and how do we decide what state to use? + var/next_weather_transition = 0 // What world.time will we next evaluate our state? + + var/obj/abstract/lightning_overlay/lightning_overlay // A visible atom used for animated lighting effects. + var/tmp/list/vis_contents_additions // Holder for a list used to add required atoms to turf vis_contents. + +// Main heartbeat proc, called by SSweather. +/obj/abstract/weather_system/proc/tick() + + // Check if we should move to a new state. + if(world.time >= next_weather_transition) + weather_system.evaluate() + + // Change wind direction and speed. + handle_wind() + + // Handle periodic effects for ticks (like lightning) + var/singleton/state/weather/rain/weather_state = weather_system.current_state + if(istype(weather_state)) + weather_state.tick(src) + + +/obj/abstract/weather_system/Destroy() + // Clean ourselves out of the vis_contents of our affected turfs. + for(var/tz in affecting_zs) + for(var/turf/T as anything in block(locate(1, 1, tz), locate(world.maxx, world.maxy, tz))) + if(T.weather == src) + T.remove_vis_contents(vis_contents_additions) + T.weather = null + vis_contents_additions.Cut() + SSweather.unregister_weather_system(src) + QDEL_NULL(lightning_overlay) + . = ..() + +// Called by /turf/examine() to show current weather status. +/obj/abstract/weather_system/examine(mob/user, distance) + SHOULD_CALL_PARENT(FALSE) + var/singleton/state/weather/weather_state = weather_system.current_state + if(istype(weather_state)) + to_chat(user, weather_state.descriptor) + show_wind(user, force = TRUE) + +// Called by /singleton/state/weather to assess validity of a state in the weather FSM. +/obj/abstract/weather_system/proc/supports_weather_state(var/singleton/state/weather/next_state) + // Exoplanet stuff for the future: + // - TODO: track and check exoplanet temperature. + // - TODO: compare to a list of 'acceptable' states + if(istype(next_state)) + if(next_state.is_liquid) + return !!water_material + if(next_state.is_ice) + return !!ice_material + return TRUE + return FALSE + +// Dummy object for lightning flash animation. +/obj/abstract/lightning_overlay + plane = PLANE_DEFAULT + 1 + layer = EFFECTS_ABOVE_LIGHTING_LAYER + icon = 'icons/effects/weather.dmi' + icon_state = "full" + alpha = 0 + invisibility = 0 diff --git a/code/modules/weather/weather_debug.dm b/code/modules/weather/weather_debug.dm new file mode 100644 index 00000000000..45a521051ec --- /dev/null +++ b/code/modules/weather/weather_debug.dm @@ -0,0 +1,67 @@ +/datum/admins/proc/force_kill_weather() + set name = "Kill Weather For Level" + set desc = "Destroys a weather system for a level if present." + set category = "Debug" + + if(!check_rights(R_DEBUG)) + return + + var/turf/T = get_turf(usr) + if(!istype(T)) + to_chat(usr, SPAN_WARNING("You need to have a turf to use this verb.")) + return + + var/obj/abstract/weather_system/weather = T.weather || SSweather.weather_by_z["[T.z]"] + if(!weather) + to_chat(usr, SPAN_WARNING("This z-level does not have weather.")) + return + + qdel(weather) + to_chat(usr, SPAN_NOTICE("Weather destroyed for z[T.z].")) + +/datum/admins/proc/force_initialize_weather() + set name = "Initialize Weather For Level" + set desc = "Creates a weather system for a level if not present." + set category = "Debug" + + if(!check_rights(R_DEBUG)) + return + + var/turf/T = get_turf(usr) + while(T && HasAbove(T.z)) + T = GetAbove(T) + if(!istype(T)) + to_chat(usr, SPAN_WARNING("You need to have a turf to use this verb.")) + return + + var/obj/abstract/weather_system/weather = T.weather || SSweather.weather_by_z["[T.z]"] + if(weather) + to_chat(usr, SPAN_WARNING("This z-level already has weather.")) + return + SSweather.setup_weather_system(T.z) + to_chat(usr, SPAN_NOTICE("Weather created for z[T.z].")) + +/datum/admins/proc/force_weather_state() + set name = "Force Weather State" + set desc = "Force the local weather to use a given state." + set category = "Debug" + + if(!check_rights(R_DEBUG)) + return + + var/turf/T = get_turf(usr) + if(!istype(T)) + to_chat(usr, SPAN_WARNING("You need to have a turf to use this verb.")) + return + + var/obj/abstract/weather_system/weather = T.weather || SSweather.weather_by_z["[T.z]"] + if(!weather) + to_chat(usr, SPAN_WARNING("This z-level has no weather. Use Initialize Weather For Level if you want to create it.")) + return + + var/use_state = input(usr, "Which state do you wish to use?", "Target State") as null|anything in GET_SINGLETON_SUBTYPE_LIST(/singleton/state/weather) + if(!use_state || weather != (T.weather || SSweather.weather_by_z["[T.z]"])) + return + weather.weather_system.set_state(use_state) + var/singleton/state/weather/weather_state = GET_SINGLETON(use_state) + to_chat(usr, SPAN_NOTICE("Set weather for z[T.z] to [weather_state.name].")) diff --git a/code/modules/weather/weather_effects.dm b/code/modules/weather/weather_effects.dm new file mode 100644 index 00000000000..8261c3dc5c8 --- /dev/null +++ b/code/modules/weather/weather_effects.dm @@ -0,0 +1,48 @@ +/obj/abstract/weather_system/proc/get_movement_delay(var/datum/gas_mixture/env, var/travel_dir) + + // It's quiet. Too quiet. + if(!wind_direction || !base_wind_delay || !travel_dir || !env || env.return_pressure() < MIN_WIND_PRESSURE) + return 0 + + // May the wind be always at your back! + var/current_wind_strength = round(wind_strength * base_wind_delay) + if(wind_direction == travel_dir) + return -(current_wind_strength) + if(travel_dir & wind_direction) + return -(round(current_wind_strength * 0.5)) + + // Never spit into the wind. + var/reversed_wind = GLOB.reverse_dir[wind_direction] + if(reversed_wind == travel_dir) + return current_wind_strength + if(travel_dir & reversed_wind) + return round(current_wind_strength * 0.5) + + return 0 + +/obj/abstract/weather_system/proc/adjust_temperature(initial_temperature) + return initial_temperature + +/obj/abstract/weather_system/proc/show_weather(var/mob/M) + var/mob_ref = WEAKREF(M) + if(mob_shown_weather[mob_ref]) + return FALSE + var/singleton/state/weather/current_weather = weather_system?.current_state + if(!istype(current_weather)) + return FALSE + mob_shown_weather[mob_ref] = TRUE + current_weather.show_to(M, src) + return TRUE + +/obj/abstract/weather_system/proc/lightning_strike() + set waitfor = FALSE + animate(lightning_overlay, alpha = 255, time = 2) + for(var/client/C) + if(!isliving(C.mob)) + continue + var/turf/T = get_turf(C.mob) + if(!(T.z in affecting_zs)) + continue + sound_to(C.mob, sound('sound/effects/weather/thunder.ogg', repeat = FALSE, wait = 0, volume = 100)) + sleep(3) + animate(lightning_overlay, alpha = initial(lightning_overlay.alpha), time = 5 SECONDS) diff --git a/code/modules/weather/weather_fsm.dm b/code/modules/weather/weather_fsm.dm new file mode 100644 index 00000000000..3f8d3e47117 --- /dev/null +++ b/code/modules/weather/weather_fsm.dm @@ -0,0 +1,10 @@ +/datum/state_machine/weather + expected_type = /obj/abstract/weather_system + +/datum/state_machine/weather/choose_transition(list/valid_transitions) + var/list/transitions = list() + for(var/singleton/state_transition/weather/state_transition in valid_transitions) + transitions[state_transition] = state_transition.likelihood_weighting + if(length(transitions)) + return pick(transitions) //pickweight(transitions) + return ..() diff --git a/code/modules/weather/weather_fsm_state_transitions.dm b/code/modules/weather/weather_fsm_state_transitions.dm new file mode 100644 index 00000000000..14ac0ad8b15 --- /dev/null +++ b/code/modules/weather/weather_fsm_state_transitions.dm @@ -0,0 +1,31 @@ +/singleton/state_transition/weather + abstract_type = /singleton/state_transition/weather + var/likelihood_weighting = 100 + +/singleton/state_transition/weather/is_open(datum/holder) + var/obj/abstract/weather_system/weather = holder + return weather.supports_weather_state(target) + +/singleton/state_transition/weather/calm + target = /singleton/state/weather/calm + likelihood_weighting = 50 + +/singleton/state_transition/weather/snow + target = /singleton/state/weather/snow + +/singleton/state_transition/weather/rain + target = /singleton/state/weather/rain + +/singleton/state_transition/weather/snow_medium + target = /singleton/state/weather/snow/medium + +/singleton/state_transition/weather/snow_heavy + target = /singleton/state/weather/snow/heavy + likelihood_weighting = 20 + +/singleton/state_transition/weather/storm + target = /singleton/state/weather/rain/storm + +/singleton/state_transition/weather/hail + target = /singleton/state/weather/rain/hail + likelihood_weighting = 20 diff --git a/code/modules/weather/weather_fsm_states.dm b/code/modules/weather/weather_fsm_states.dm new file mode 100644 index 00000000000..2ee5ffc177f --- /dev/null +++ b/code/modules/weather/weather_fsm_states.dm @@ -0,0 +1,191 @@ +/singleton/state/weather + + abstract_type = /singleton/state/weather + + var/name = "Undefined" + var/descriptor = "The weather is undefined." + + var/cosmetic_message_chance = 5 + var/list/cosmetic_messages + var/list/protected_messages + var/cosmetic_span_class = "notice" + + var/icon = 'icons/effects/weather.dmi' + var/icon_state + + var/alpha = 210 + var/minimum_time = 2 MINUTES + var/maximum_time = 10 MINUTES + var/is_liquid = FALSE + var/is_ice = FALSE + + var/list/ambient_sounds + var/list/ambient_indoors_sounds + +/singleton/state/weather/entered_state(datum/holder) + . = ..() + + var/obj/abstract/weather_system/weather = holder + weather.next_weather_transition = world.time + rand(minimum_time, maximum_time) + weather.mob_shown_weather.Cut() + weather.icon = icon + weather.icon_state = icon_state + weather.alpha = alpha + + if(is_liquid && weather.water_material) + var/material/mat = SSmaterials.get_material_by_name(weather.water_material) + weather.color = mat.icon_colour + else if(is_ice && weather.ice_material) + var/material/mat = SSmaterials.get_material_by_name(weather.ice_material) + weather.color = mat.icon_colour + else + weather.color = COLOR_WHITE + +/singleton/state/weather/proc/tick(var/obj/abstract/weather_system/weather) + return + +/singleton/state/weather/proc/handle_roofed_effects(var/mob/living/M, var/obj/abstract/weather_system/weather) + return + +/singleton/state/weather/proc/handle_protected_effects(var/mob/living/M, var/obj/abstract/weather_system/weather, var/obj/item/protected_by) + if(prob(cosmetic_message_chance)) + if(protected_by && length(protected_messages)) + if(protected_by.loc == M) + to_chat(M, "[replacetext(pick(protected_messages), "$ITEM$", "your [protected_by.name]")]") + else + to_chat(M, "[replacetext(pick(protected_messages), "$ITEM$", "\the [protected_by]")]") + else if(length(cosmetic_messages)) + to_chat(M, "[pick(cosmetic_messages)]") + +/singleton/state/weather/proc/handle_exposure_effects(var/mob/living/M, var/obj/abstract/weather_system/weather) + handle_protected_effects(M, weather) + +/singleton/state/weather/proc/handle_exposure(var/mob/living/M, var/exposure, var/obj/abstract/weather_system/weather) + + // Send strings if we're outside. + if(M.is_outside() && M.client) + if(!weather.show_weather(M)) + weather.show_wind(M) + + if(exposure != WEATHER_IGNORE && weather.set_cooldown(M)) + if(exposure == WEATHER_EXPOSED) + handle_exposure_effects(M, weather) + else if(exposure == WEATHER_ROOFED) + handle_roofed_effects(M, weather) + else if(exposure == WEATHER_PROTECTED) + var/list/protected_by = M.get_weather_protection() + if(LAZYLEN(protected_by)) + handle_protected_effects(M, weather, pick(protected_by)) + +/singleton/state/weather/proc/show_to(var/mob/living/M, var/obj/abstract/weather_system/weather) + to_chat(M, descriptor) + +/singleton/state/weather/calm + name = "Calm" + icon_state = "blank" + descriptor = "The weather is calm." + transitions = list( + /singleton/state_transition/weather/snow, + /singleton/state_transition/weather/rain + ) + +/singleton/state/weather/snow + name = "Light Snow" + icon_state = "snowfall_light" + descriptor = "It is snowing gently." + is_ice = TRUE + transitions = list( + /singleton/state_transition/weather/calm, + /singleton/state_transition/weather/rain, + /singleton/state_transition/weather/snow_medium + ) + ambient_sounds = list('sound/effects/weather/snow.ogg') + protected_messages = list("Snowflakes collect atop $ITEM$.") + cosmetic_messages = list( + "Snowflakes fall slowly around you.", + "Flakes of snow drift gently past." + ) + +/singleton/state/weather/snow/medium + name = "Snow" + icon_state = "snowfall_med" + descriptor = "It is snowing." + transitions = list( + /singleton/state_transition/weather/snow, + /singleton/state_transition/weather/snow_heavy + ) + +/singleton/state/weather/snow/heavy + name = "Heavy Snow" + icon_state = "snowfall_heavy" + descriptor = "It is snowing heavily." + transitions = list(/singleton/state_transition/weather/snow_medium) + cosmetic_messages = list( + "Gusting snow obscures your vision.", + "Thick flurries of snow swirl around you." + ) + cosmetic_span_class = "warning" + +/singleton/state/weather/rain + name = "Light Rain" + icon_state = "rain" + descriptor = "It is raining gently." + cosmetic_span_class = "notice" + is_liquid = TRUE + transitions = list( + /singleton/state_transition/weather/calm, + /singleton/state_transition/weather/storm + ) + ambient_sounds = list('sound/effects/weather/rain.ogg') + ambient_indoors_sounds = list('sound/effects/weather/rain_indoors.ogg') + cosmetic_messages = list("Raindrops patter against you.") + protected_messages = list("Raindrops patter against $ITEM$.") + var/list/roof_messages = list("Rain patters against the roof.") + +/singleton/state/weather/rain/handle_roofed_effects(var/mob/living/M, var/obj/abstract/weather_system/weather) + if(length(roof_messages) && prob(cosmetic_message_chance)) + to_chat(M, "[pick(roof_messages)]") + +/singleton/state/weather/rain/storm + name = "Heavy Rain" + icon_state = "storm" + descriptor = "It is raining heavily." + cosmetic_span_class = "warning" + transitions = list( + /singleton/state_transition/weather/rain, + /singleton/state_transition/weather/hail + ) + cosmetic_messages = list("Torrential rain thunders down around you.") + protected_messages = list("Torrential rain thunders against $ITEM$.") + roof_messages = list("Torrential rain thunders against the roof.") + ambient_sounds = list('sound/effects/weather/rain_heavy.ogg') + +/singleton/state/weather/rain/storm/tick(var/obj/abstract/weather_system/weather) + ..() + if(prob(0.5)) + weather.lightning_strike() + +/singleton/state/weather/rain/hail + name = "Hail" + icon_state = "hail" + descriptor = "It is hailing." + cosmetic_span_class = "danger" + is_liquid = FALSE + is_ice = TRUE + transitions = list(/singleton/state_transition/weather/storm) + cosmetic_messages = list("Hail patters around you.") + protected_messages = list("Hail patters against $ITEM$.") + roof_messages = list("Hail clatters on the roof.") + ambient_sounds = list('sound/effects/weather/rain.ogg') + ambient_indoors_sounds = list('sound/effects/weather/hail_indoors.ogg') + +/singleton/state/weather/rain/hail/handle_exposure_effects(var/mob/living/M, var/obj/abstract/weather_system/weather) + to_chat(M, SPAN_DANGER("You are pelted by a shower of hail!")) + M.adjustBruteLoss(rand(1,3)) + +/singleton/state/weather/ash + name = "Ash" + icon_state = "ashfall_light" + descriptor = "A rain of ash falls from the sky." + cosmetic_span_class = "warning" + cosmetic_messages = list("Drifts of ash fall from the sky.") diff --git a/code/modules/weather/weather_init.dm b/code/modules/weather/weather_init.dm new file mode 100644 index 00000000000..fc34dff97bd --- /dev/null +++ b/code/modules/weather/weather_init.dm @@ -0,0 +1,38 @@ +INITIALIZE_IMMEDIATE(/obj/abstract/weather_system) +/obj/abstract/weather_system/Initialize(var/ml, var/target_z, var/initial_weather) + SSweather.register_weather_system(src) + + . = ..() + + set_invisibility(0) + + // Bookkeeping/rightclick guards. + verbs.Cut() + forceMove(null) + lightning_overlay = new + vis_contents_additions = list(src, lightning_overlay) + + // Initialize our state machine. + weather_system = add_state_machine(src, /datum/state_machine/weather) + weather_system.set_state(initial_weather || /singleton/state/weather/calm) + + // Track our affected z-levels. + affecting_zs = GetConnectedZlevels(target_z) + + // If we're post-init, init immediately. + if(SSweather.initialized) + addtimer(CALLBACK(src, PROC_REF(init_weather)), 0) + +// Start the weather effects from the highest point; they will propagate downwards during update. +/obj/abstract/weather_system/proc/init_weather() + // Track all z-levels. + for(var/highest_z in affecting_zs) + var/turfcount = 0 + if(HasAbove(highest_z)) + continue + // Update turf weather. + for(var/turf/T as anything in block(locate(1, 1, highest_z), locate(world.maxx, world.maxy, highest_z))) + T.update_weather(src) + turfcount++ + CHECK_TICK + log_debug("Initialized weather for [turfcount] turf\s from z[highest_z].") diff --git a/code/modules/weather/weather_mob_tracking.dm b/code/modules/weather/weather_mob_tracking.dm new file mode 100644 index 00000000000..d0e4a9cd43b --- /dev/null +++ b/code/modules/weather/weather_mob_tracking.dm @@ -0,0 +1,23 @@ + +GLOBAL_LIST_EMPTY(current_mob_ambience) +/obj/abstract/weather_system + + // Weakref lists used to track mobs within our weather + // system; alternative to keeping big lists of actual mobs or + // having mobs constantly poked by weather systems. + + var/tmp/list/mobs_on_cooldown = list() // Has this mob recently been messed with by the weather? + var/tmp/list/mob_shown_weather = list() // Has this mob been sent the summary message about the current weather? + var/tmp/list/mob_shown_wind = list() // Has this mob been sent the summary message about the current wind? + +// 'cooldown' in this context refers to weather effects like hail damage or being shown cosmetic messages. +/obj/abstract/weather_system/proc/set_cooldown(var/mob/living/M, var/delay = 5 SECONDS) + var/mobref = WEAKREF(M) + if(!(mobref in mobs_on_cooldown)) + mobs_on_cooldown[mobref] = TRUE + addtimer(CALLBACK(src, PROC_REF(clear_cooldown), mobref), delay) + return TRUE + return FALSE + +/obj/abstract/weather_system/proc/clear_cooldown(var/mobref) + mobs_on_cooldown -= mobref diff --git a/code/modules/weather/weather_wind.dm b/code/modules/weather/weather_wind.dm new file mode 100644 index 00000000000..e4624673dc0 --- /dev/null +++ b/code/modules/weather/weather_wind.dm @@ -0,0 +1,38 @@ +/obj/abstract/weather_system + var/tmp/wind_direction = 0 // Bitflag; current wind direction. + var/tmp/wind_strength = 1 // How string is the wind currently? + var/const/base_wind_delay = 1 // What is the base movement delay or increase applied by wind strength? + +// Randomizes wind speed and direction sometimes. +/obj/abstract/weather_system/proc/handle_wind() + // TODO: turbulence for chance of wind changes, + // ferocity for strength of wind changes + if(prob(66)) + return + if(prob(10)) + if(!wind_direction) + wind_direction = pick(GLOB.alldirs) + else + wind_direction = turn(wind_direction, pick(45, -45)) + mob_shown_wind.Cut() + if(prob(10)) + var/old_strength = wind_strength + wind_strength = clamp(wind_strength + rand(-1, 1), 5, -5) + if(old_strength != wind_strength) + mob_shown_wind.Cut() + +/obj/abstract/weather_system/proc/show_wind(var/mob/M, var/force = FALSE) + var/mob_ref = WEAKREF(M) + if(mob_shown_wind[mob_ref] && !force) + return FALSE + mob_shown_wind[mob_ref] = TRUE + . = TRUE + var/turf/T = get_turf(M) + if(istype(T)) + var/datum/gas_mixture/environment = T.return_air() + if(environment && environment.return_pressure() >= MIN_WIND_PRESSURE) // Arbitrary low pressure bound. + var/absolute_strength = abs(wind_strength) + if(absolute_strength <= 0.5 || !wind_direction) + to_chat(M, SPAN_NOTICE("The wind is calm.")) + else + to_chat(M, SPAN_NOTICE("The wind is blowing[absolute_strength > 2 ? " strongly" : ""] towards the [dir2text(wind_direction)].")) diff --git a/html/changelogs/mattatlas-getyourumbrella.yml b/html/changelogs/mattatlas-getyourumbrella.yml new file mode 100644 index 00000000000..690087792ab --- /dev/null +++ b/html/changelogs/mattatlas-getyourumbrella.yml @@ -0,0 +1,42 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# wip (For works in progress) +# tweak +# soundadd +# sounddel +# rscadd (general adding of nice things) +# rscdel (general deleting of nice things) +# imageadd +# imagedel +# maptweak +# spellcheck (typo fixes) +# experiment +# balance +# admin +# backend +# security +# refactor +################################# + +# Your name. +author: MattAtlas (Porting), Loaf (Original Code), Lancer (Umbrella Sprites) + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries. +# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog. +changes: + - rscadd: "Ported a weather system from Nebula. Original code credit is to Loaf." + - rscadd: "Added umbrellas to the game. Sprites by Lancer." diff --git a/icons/effects/weather.dmi b/icons/effects/weather.dmi new file mode 100644 index 00000000000..01aa9d456f4 Binary files /dev/null and b/icons/effects/weather.dmi differ diff --git a/icons/obj/item/umbrellas.dmi b/icons/obj/item/umbrellas.dmi new file mode 100644 index 00000000000..f7df1b524e7 Binary files /dev/null and b/icons/obj/item/umbrellas.dmi differ diff --git a/maps/_common/areas/areas.dm b/maps/_common/areas/areas.dm index 4507f02ba24..0381220c856 100644 --- a/maps/_common/areas/areas.dm +++ b/maps/_common/areas/areas.dm @@ -37,6 +37,7 @@ Generally you don't want to put your areas in here; if the area is only used in ambience = AMBIENCE_SPACE no_light_control = 1 base_turf = /turf/space + is_outside = OUTSIDE_YES /area/space/atmosalert() return diff --git a/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm b/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm index bc70dc52863..fc8ad61aab9 100644 --- a/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm +++ b/maps/away/away_site/konyang/point_verdant/point_verdant_areas.dm @@ -6,6 +6,7 @@ base_turf = /turf/simulated/floor/exoplanet/dirt_konyang ambience = AMBIENCE_KONYANG_TRAFFIC sound_env = CITY + is_outside = TRUE var/lighting = FALSE //Is this area automatically lit? /area/point_verdant/Initialize() @@ -42,6 +43,7 @@ name = "Point Verdant - Indoors" sound_env = LARGE_SOFTFLOOR ambience = AMBIENCE_KONYANG_RAIN_MUFFLED + is_outside = FALSE //Main city buildings /area/point_verdant/interior/laundromat diff --git a/sound/effects/weather/hail_indoors.ogg b/sound/effects/weather/hail_indoors.ogg new file mode 100644 index 00000000000..e0158fde3a2 Binary files /dev/null and b/sound/effects/weather/hail_indoors.ogg differ diff --git a/sound/effects/weather/rain.ogg b/sound/effects/weather/rain.ogg new file mode 100644 index 00000000000..f65ba956f84 Binary files /dev/null and b/sound/effects/weather/rain.ogg differ diff --git a/sound/effects/weather/rain_heavy.ogg b/sound/effects/weather/rain_heavy.ogg new file mode 100644 index 00000000000..1a51c611c4f Binary files /dev/null and b/sound/effects/weather/rain_heavy.ogg differ diff --git a/sound/effects/weather/rain_indoors.ogg b/sound/effects/weather/rain_indoors.ogg new file mode 100644 index 00000000000..9fdabfe558e Binary files /dev/null and b/sound/effects/weather/rain_indoors.ogg differ diff --git a/sound/effects/weather/snow.ogg b/sound/effects/weather/snow.ogg new file mode 100644 index 00000000000..641d31596a6 Binary files /dev/null and b/sound/effects/weather/snow.ogg differ diff --git a/sound/effects/weather/thunder.ogg b/sound/effects/weather/thunder.ogg new file mode 100644 index 00000000000..2719fcc383e Binary files /dev/null and b/sound/effects/weather/thunder.ogg differ