diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a027921 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Artūras Norkus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 09d83e3..d413213 100644 --- a/README.md +++ b/README.md @@ -4,44 +4,46 @@ Weather mod for Minetest (http://minetest.net/) Weathers included ----------------------- -* rain +* light_rain, rain, heavy_rain * snow -* thunder +* thunder (works together with heavy_rain) Commands ----------------------- -`set_weather ` requires `weather_manager` privilege. +requires `weather_manager` privilege. + + * `start_weather ` + * `stop_weather ` Dependencies ----------------------- Thunder weather requres [lightning](https://github.com/minetest-mods/lightning) mod. -Configuration properties ------------------------ -Weather mod for indoor check depends on sunlight propogation check. Some nodes (e.g. glass block) propogates sunlight and thus weather particles will go through it. To change that set `weather_allow_override_nodes=true` in `minetest.conf` file. Be aware that just few nodes will be override and these blocks needs to be re-builded to take effect. Maybe in future other 'cheap' way to check indoor will be available. - -Weather mod mostly relies on particles generation however for some small things ABM may be used. Users which do not want it can disable ABM with property `weather_allow_abm=false`. - License of source code: ----------------------- -LGPL 2.1+ +MIT Authors of media files: ----------------------- -TeddyDesTodes: -Snowflakes licensed under CC-BY-SA 3.0 by from weather branch at https://github.com/TeddyDesTodes/minetest/tree/weather - - * `weather_pack_snow_snowflake1.png` - CC-BY-SA 3.0 - * `weather_pack_snow_snowflake2.png` - CC-BY-SA 3.0 - xeranas: - * `weather_pack_rain_raindrop_1.png` - CC-0 - * `weather_pack_rain_raindrop_2.png` - CC-0 - * `weather_pack_rain_raindrop_3.png` - CC-0 + * `happy_weather_heavy_rain_drops.png` - CC-0 + * `happy_weather_light_rain_raindrop_1.png` - CC-0 + * `happy_weather_light_rain_raindrop_2.png` - CC-0 + * `happy_weather_light_rain_raindrop_3.png` - CC-0 + * `happy_weather_light_snow_snowflake_1.png` - CC-0 + * `happy_weather_light_snow_snowflake_2.png` - CC-0 + * `happy_weather_light_snow_snowflake_3.png` - CC-0 inchadney (http://freesound.org/people/inchadney/): - * `weather_rain.ogg` - CC-BY-SA 3.0 (cut from http://freesound.org/people/inchadney/sounds/58835/) + * `rain_drop.ogg` - CC-BY-SA 3.0 (cut from http://freesound.org/people/inchadney/sounds/58835/) +rcproductions54 (http://freesound.org/people/rcproductions54/): + + * `light_rain_drop.ogg` - CC-0 (http://freesound.org/people/rcproductions54/sounds/265045/) + +uberhuberman + + * `heavy_rain_drop.ogg` - CC BY 3.0 (https://www.freesound.org/people/uberhuberman/sounds/21189/) diff --git a/abm.lua b/abm.lua new file mode 100644 index 0000000..3d5e847 --- /dev/null +++ b/abm.lua @@ -0,0 +1,21 @@ +-------------------------------- +-- Happy Weather: ABM registers + +-- License: MIT + +-- Credits: xeranas +-------------------------------- + +-- ABM for extinguish fire +minetest.register_abm({ + nodenames = {"fire:basic_flame"}, + interval = 4.0, + chance = 2, + action = function(pos, node, active_object_count, active_object_count_wider) + if happy_weather.is_weather_active("heavy_rain") or happy_weather.is_weather_active("rain") then + if hw_utils.is_outdoor(pos) then + minetest.remove_node(pos) + end + end + end +}) \ No newline at end of file diff --git a/commands.lua b/commands.lua new file mode 100644 index 0000000..b7701cd --- /dev/null +++ b/commands.lua @@ -0,0 +1,36 @@ +------------------------------------ +-- Happy Weather API Chat Commands + +-- License: MIT + +-- Credits: xeranas +------------------------------------ + +minetest.register_privilege("weather_manager", { + description = "Gives ability to control weather", + give_to_singleplayer = false +}) + +minetest.register_chatcommand("start_weather", { + params = "", + description = "Starts weather by given weather code.", + privs = {weather_manager = true}, + func = function(name, param) + if param ~= nil then + happy_weather.request_to_start(param) + minetest.log("action", name .. " requested weather '" .. param .. "' from chat command") + end + end +}) + +minetest.register_chatcommand("stop_weather", { + params = "", + description = "Ends weather by given weather code.", + privs = {weather_manager = true}, + func = function(name, param) + if param ~= nil then + happy_weather.request_to_end(param) + minetest.log("action", name .. " requested weather '" .. param .. "' ending from chat command") + end + end +}) \ No newline at end of file diff --git a/embedded_happy_weather_api.lua b/embedded_happy_weather_api.lua new file mode 100644 index 0000000..29dcde0 --- /dev/null +++ b/embedded_happy_weather_api.lua @@ -0,0 +1,271 @@ +--------------------------- +-- Happy Weather API + +-- License: MIT +-- Credits: xeranas +--------------------------- + +-- Main object which will be used in Weather API lifecycle +happy_weather = {} + +-- Local variables which helps organize active and deactive weahers +local registered_weathers = {} +local active_weathers = {} + +--------------------------- +-- Weather API functions -- +--------------------------- + +-- Adds weather to register_weathers table +happy_weather.register_weather = function(weather_obj) + table.insert(registered_weathers, weather_obj) +end + +-- Returns true if weather is active right now, false otherwise +happy_weather.is_weather_active = function(weather_code) + if #active_weathers == 0 then + return false + end + + for k, weather_ in ipairs(active_weathers) do + if weather_.code == weather_code then + return true + end + end + return false +end + +-- Requests weaher to start +happy_weather.request_to_start = function(weather_code, position) + if #registered_weathers == 0 then + return + end + + for k, weather_ in ipairs(registered_weathers) do + if weather_.code == weather_code and weather_.start ~= nil then + weather_.start(position) + return + end + end +end + +-- Requests weaher to end +happy_weather.request_to_end = function(weather_code) + if #active_weathers == 0 then + return + end + + for k, weather_ in ipairs(active_weathers) do + if weather_.code == weather_code and weather_.stop ~= nil then + weather_.stop() + return + end + end +end + +------------------------------------ +-- Local helper / utility methods -- +------------------------------------ + +-- Adds weather to active_weathers table +local add_active_weather = function(weather_obj) + table.insert(active_weathers, weather_obj) +end + +-- Remove weather from active_weathers table +local remove_active_weather = function(weather_code) + if #active_weathers == 0 then + return + end + + for k, weather_ in ipairs(active_weathers) do + if weather_.code == weather_code then + table.remove(active_weathers, k) + return + end + end +end + +-- adds player to affected_players table +local add_player = function(affected_players, player) + table.insert(affected_players, player) +end + +-- remove player from affected_players table +local remove_player = function(affected_players, player_name) + if #affected_players == 0 then + return + end + + for k, player_ in ipairs(affected_players) do + if player_:get_player_name() == player_name then + table.remove(affected_players, k) + return + end + end +end + +local is_player_affected = function(affected_players, player_name) + if #affected_players == 0 then + return false + end + + for k, player_ in ipairs(affected_players) do + if player_:get_player_name() == player_name then + return true + end + end +end + +----------------------------------------------------------------------------- +-- Weather object callback wrappers to avoid issues from undefined methods -- +----------------------------------------------------------------------------- + +-- Weather is_starting method nil-safe wrapper +local weather_is_starting = function(weather_obj, dtime, position) + if weather_obj.is_starting == nil then + return false + end + return weather_obj.is_starting(dtime, position) +end + +-- Weather is_starting method nil-safe wrapper +local weather_is_ending = function(weather_obj, dtime) + if weather_obj.is_ending == nil then + return false + end + return weather_obj.is_ending(dtime) +end + +-- Weather add_player method nil-safe wrapper +local weather_add_player = function(weather_obj, player) + if weather_obj.add_player == nil then + return + end + weather_obj.add_player(player) +end + +-- Weather remove_player method nil-safe wrapper +local weather_remove_player = function(weather_obj, player) + if weather_obj.remove_player == nil then + return + end + weather_obj.remove_player(player) +end + +-- Weather remove_player method nil-safe wrapper +local weather_in_area = function(weather_obj, position) + if weather_obj.in_area == nil then + return true + end + return weather_obj.in_area(position) +end + +-- Weather render method nil-safe wrapper +local weather_render = function(weather_obj, dtime, player) + if weather_obj.render == nil then + return + end + weather_obj.render(dtime, player) +end + +-- Weather start method nil-safe wrapper +local weather_start = function(weather_obj, player) + if weather_obj.start == nil then + return + end + weather_obj.start(player) +end + +-- Weather stop method nil-safe wrapper +local weather_stop = function(weather_obj, player) + if weather_obj.stop == nil then + return + end + weather_obj.stop(player) +end + +-- Perform clean-up callbacks calls sets flags upon weaher end +local prepare_ending = function(weather_obj) + weather_obj.active = false + remove_active_weather(weather_obj.code) +end + +-- Perform weather setup for certain player +local prepare_starting = function(weather_obj) + weather_obj.active = true + weather_obj.affected_players = {} + add_active_weather(weather_obj) +end + +-- While still active weather can or can not affect players based on area they are +local render_if_in_area = function(weather_obj, dtime, player) + if is_player_affected(weather_obj.affected_players, player:get_player_name()) then + if weather_in_area(weather_obj, player:getpos()) then + weather_render(weather_obj, dtime, player) + else + weather_remove_player(weather_obj, player) + remove_player(weather_obj.affected_players, player:get_player_name()) + end + else + if weather_in_area(weather_obj, player:getpos()) then + add_player(weather_obj.affected_players, player) + weather_add_player(weather_obj, player) + end + end +end + + +-------------------------- +-- Global step function -- +-------------------------- + +minetest.register_globalstep(function(dtime) + + if #registered_weathers == 0 then + -- no registered weathers, do nothing. + return + end + + if #minetest.get_connected_players() == 0 then + -- no actual players, do nothing. + return + end + + -- Loop through registered weathers + for i, weather_ in ipairs(registered_weathers) do + local deactivate_weather = false + local activate_weather = false + + -- Loop through connected players + for ii, player in ipairs(minetest.get_connected_players()) do + + -- Weaher is active checking if it about to end + if weather_.active then + if weather_is_ending(weather_, dtime) or deactivate_weather then + weather_remove_player(weather_, player) + remove_player(weather_.affected_players, player:get_player_name()) + deactivate_weather = true -- should remain true until all players will be removed from weather + + -- Weather still active updating it + else + render_if_in_area(weather_, dtime, player) + end + + -- Weaher is not active checking if it about to start + else + if weather_.is_starting(dtime, player:getpos()) then + activate_weather = true + end + end + end + + if deactivate_weather then + prepare_ending(weather_) + end + + if activate_weather then + prepare_starting(weather_) + end + end +end) diff --git a/embedded_sky_layer_api.lua b/embedded_sky_layer_api.lua new file mode 100644 index 0000000..d1ecf4d --- /dev/null +++ b/embedded_sky_layer_api.lua @@ -0,0 +1,265 @@ +------------------------- +-- Sky Layers: API + +-- License: MIT +-- Credits: xeranas +------------------------- + +skylayer = {} + +-- flag for enable / disable skylayer temporally if needed +skylayer.enabled = true + +-- supported skylayer types +skylayer.SKY_PLAIN = "plain" +skylayer.SKY_SOLID_COLOR = "solid_color" +skylayer.SKY_SKYBOX = "skybox" + +-- helps track total dtime +local timer = 0 + +local gradient_default_min_value = 0 +local gradient_default_max_value = 1000 + +-- how often sky will be updated in seconds +skylayer.update_interval = 4 + +-- keeps player related data such as player itself and own sky layers +local sky_players = {} + +-- adds player to sky layer affected players list +local add_player = function(player) + local data = {} + data.id = player:get_player_name() + data.player = player + data.skylayers = {} + table.insert(sky_players, data) +end + +-- remove player from sky layer affected players list +local remove_player = function(player_name) + if #sky_players == 0 then + return + end + + for k, player_data in ipairs(sky_players) do + if player_data.id == player_name then + set_default_sky(player_data.player) + table.remove(sky_players, k) + return + end + end +end + +local get_player_by_name = function(player_name) + if player_name == nil then + return nil + end + + if #minetest.get_connected_players() == 0 then + return nil + end + + for i, player in ipairs(minetest.get_connected_players()) do + if player:get_player_name() == player_name then + return player + end + end + + return nil +end + +local get_player_data = function(player_name) + if #sky_players == 0 then + return nil + end + + for k, player_data in ipairs(sky_players) do + if player_data.id == player_name then + return player_data + end + end +end + +local create_new_player_data = function(player_name) + local player_data = get_player_data(player_name) + if player_data == nil then + local player = get_player_by_name(player_name) + if player == nil then + minetest.log("error", "Fail to resolve player '" .. player_name .. "'") + return + end + add_player(player) + return get_player_data(player_name) + end + return player_data +end + +-- sets default / regular sky for player +local set_default_sky = function(player) + player:set_sky(nil, "regular", nil) +end + +-- resolves latest skylayer based on added layer time +local get_latest_layer = function(layers) + if #layers == 0 then + return nil + end + + local latest_layer = nil + for k, layer in ipairs(layers) do + if latest_layer == nil then + latest_layer = layer + else + if layer.added_time >= latest_layer.added_time then + latest_layer = layer + end + end + end + + return latest_layer +end + +local convert_to_rgb = function(minval, maxval, current_val, colors) + local max_index = #colors - 1 + local val = (current_val-minval) / (maxval-minval) * max_index + 1.0 + local index1 = math.floor(val) + local index2 = math.min(math.floor(val)+1, max_index + 1) + local f = val - index1 + local c1 = colors[index1] + local c2 = colors[index2] + + return { + r=math.floor(c1.r + f*(c2.r - c1.r)), + g=math.floor(c1.g + f*(c2.g-c1.g)), + b=math.floor(c1.b + f*(c2.b - c1.b)) + } +end + +-- Returns current layer color in {r, g, b} format +local get_current_layer_color = function(layer_data) + -- min timeofday value 0; max timeofday value 1. So sky color gradient range will be between 0 and 1 * skycolor.max_value. + local timeofday = minetest.get_timeofday() + local min_val = layer_data.gradient_data.min_value + if min_val == nil then + min_val = gradient_default_min_value + end + local max_val = layer_data.gradient_data.max_value + if max_val == nil then + max_val = gradient_default_max_value + end + local rounded_time = math.floor(timeofday * max_val) + local gradient_colors = layer_data.gradient_data.colors + local color = convert_to_rgb(min_val, max_val, rounded_time, gradient_colors) + return color +end + +local update_plain_sky = function(player, layer_data) + local color = get_current_layer_color(layer_data) + player:set_sky(color, "plain", nil) +end + +local update_solid_color_sky = function(player, layer_data) + player:set_sky(layer_data.color, "plain", nil) +end + +local update_skybox_sky = function(player, layer_data) + player:set_sky(layer_data.skybox[1], layer_data.skybox[2], layer_data.skybox[3]) +end + +local update_sky = function(player, timer) + local player_data = get_player_data(player:get_player_name()) + if player_data == nil then return end + + local current_layer = get_latest_layer(player_data.skylayers) + if current_layer == nil then + return + end + + if current_layer.updated == false or timer >= skylayer.update_interval then + current_layer.updated = os.time() + + if current_layer.layer_type == skylayer.SKY_PLAIN then + update_plain_sky(player, current_layer.data) + return + end + + if current_layer.layer_type == skylayer.SKY_SOLID_COLOR then + update_solid_color_sky(player, current_layer.data) + return + end + + if current_layer.layer_type == skylayer.SKY_SKYBOX then + update_skybox_sky(player, current_layer.data) + return + end + end +end + +skylayer.add_layer = function(player_name, layer) + if layer == nil or layer.name == nil then + minetest.log("error", "Incorrect skylayer definition") + return + end + + local player_data = get_player_data(player_name) + if player_data == nil then + player_data = create_new_player_data(player_name) + end + + if player_data == nil then + minetest.log("error", "Fail to add skylayer to player '" .. player_name .. "'") + return + end + layer.added_time = os.time() + layer.updated = false + table.insert(player_data.skylayers, layer) +end + +skylayer.remove_layer = function(player_name, layer_name) + local player_data = get_player_data(player_name) + if player_data == nil or player_data.skylayers == nil then + return + end + + if #player_data.skylayers == 0 then + return + end + + for k, layer in ipairs(player_data.skylayers) do + if layer.name == layer_name then + table.remove(player_data.skylayers, k) + if #player_data.skylayers == 0 then + local player = get_player_by_name(player_name) + if player ~= nil then + set_default_sky(player) + end + end + return + end + end + +end + +minetest.register_globalstep(function(dtime) + if skylayer.enabled == false then + return + end + + if #minetest.get_connected_players() == 0 then + return + end + + -- timer addition calculated outside of players loop + timer = timer + dtime; + + for k, player in ipairs(minetest.get_connected_players()) do + update_sky(player, timer) + end + + -- reset timer outside of loop to make sure that all players sky will be updated + if timer >= skylayer.update_interval then + timer = 0 + end +end) + diff --git a/heavy_rain.lua b/heavy_rain.lua new file mode 100644 index 0000000..53d59a0 --- /dev/null +++ b/heavy_rain.lua @@ -0,0 +1,187 @@ +------------------------------ +-- Happy Weather: Heavy Rain + +-- License: MIT + +-- Credits: xeranas +------------------------------ + +local heavy_rain = {} +heavy_rain.last_check = 0 +heavy_rain.check_interval = 600 + +-- Weather identification code +heavy_rain.code = "heavy_rain" + +-- Keeps sound handler references +local sound_handlers = {} + +-- Manual triggers flags +local manual_trigger_start = false +local manual_trigger_end = false + +-- Skycolor layer id +local SKYCOLOR_LAYER = "happy_weather_heavy_rain_sky" + +heavy_rain.is_starting = function(dtime, position) + if heavy_rain.last_check + heavy_rain.check_interval < os.time() then + heavy_rain.last_check = os.time() + if math.random() < 0.05 then + happy_weather.request_to_end("light_rain") + happy_weather.request_to_end("rain") + return true + end + end + + if manual_trigger_start then + manual_trigger_start = false + return true + end + + return false +end + +heavy_rain.is_ending = function(dtime) + if heavy_rain.last_check + heavy_rain.check_interval < os.time() then + heavy_rain.last_check = os.time() + if math.random() < 0.7 then + happy_weather.request_to_start("rain") + return true + end + end + + if manual_trigger_end then + manual_trigger_end = false + return true + end + + return false +end + +local set_sky_box = function(player_name) + local sl = {} + sl.layer_type = skylayer.SKY_PLAIN + sl.name = SKYCOLOR_LAYER + sl.data = {gradient_data={}} + sl.data.gradient_data.colors = { + {r=0, g=0, b=0}, + {r=65, g=66, b=78}, + {r=112, g=110, b=119}, + {r=65, g=66, b=78}, + {r=0, g=0, b=0} + } + skylayer.add_layer(player_name, sl) +end + +local set_rain_sound = function(player) + return minetest.sound_play("heavy_rain_drop", { + object = player, + max_hear_distance = 2, + loop = true, + }) +end + +local remove_rain_sound = function(player) + local sound = sound_handlers[player:get_player_name()] + if sound ~= nil then + minetest.sound_stop(sound) + sound_handlers[player:get_player_name()] = nil + end +end + +heavy_rain.add_player = function(player) + sound_handlers[player:get_player_name()] = set_rain_sound(player) + set_sky_box(player:get_player_name()) +end + +heavy_rain.remove_player = function(player) + remove_rain_sound(player) + skylayer.remove_layer(player:get_player_name(), SKYCOLOR_LAYER) +end + +local rain_drop_texture = "happy_weather_heavy_rain_drops.png" + +local add_close_range_rain_particle = function(player) + local offset = { + front = 1, + back = 0, + top = 6 + } + + local random_pos = hw_utils.get_random_pos(player, offset) + local rain_texture_size_offset_y = -1 + + if hw_utils.is_outdoor(random_pos, rain_texture_size_offset_y) then + minetest.add_particle({ + pos = {x=random_pos.x, y=random_pos.y, z=random_pos.z}, + velocity = {x=0, y=-10, z=0}, + acceleration = {x=0, y=-10, z=0}, + expirationtime = 5, + size = 30, + collisiondetection = true, + collision_removal = true, + vertical = true, + texture = rain_drop_texture, + playername = player:get_player_name() + }) + end +end + +local add_wide_range_rain_particle = function(player) + local offset = { + front = 10, + back = 5, + top = 8 + } + + local random_pos = hw_utils.get_random_pos(player, offset) + + if hw_utils.is_outdoor(random_pos) then + minetest.add_particle({ + pos = {x=random_pos.x, y=random_pos.y, z=random_pos.z}, + velocity = {x=0, y=-10, z=0}, + acceleration = {x=0, y=-15, z=0}, + expirationtime = 5, + size = 30, + collisiondetection = true, + collision_removal = true, + vertical = true, + texture = rain_drop_texture, + playername = player:get_player_name() + }) + end +end + +local display_rain_particles = function(player) + if hw_utils.is_underwater(player) then + return + end + + add_close_range_rain_particle(player) + + local particles_number_per_update = 5 + for i=particles_number_per_update, 1,-1 do + add_wide_range_rain_particle(player) + end +end + +heavy_rain.render = function(dtime, player) + display_rain_particles(player) +end + +heavy_rain.start = function() + manual_trigger_start = true +end + +heavy_rain.stop = function() + manual_trigger_end = true +end + +heavy_rain.in_area = function(position) + if position.y > -10 then + return true + end + return false +end + +happy_weather.register_weather(heavy_rain) diff --git a/init.lua b/init.lua index f740cc0..e59c0ef 100644 --- a/init.lua +++ b/init.lua @@ -1,13 +1,29 @@ local modpath = minetest.get_modpath("weather_pack"); -dofile(modpath.."/weather_core.lua") -dofile(modpath.."/snow.lua") + +-- If skylayer mod not located then embeded version will be loaded. +if minetest.get_modpath("skylayer") == nil then + dofile(modpath.."/embedded_sky_layer_api.lua") +end + +-- If happy_weather_api mod not located then embeded version will be loaded. +if minetest.get_modpath("happy_weather_api") == nil then + dofile(modpath.."/embedded_happy_weather_api.lua") + dofile(modpath.."/commands.lua") +end + +-- Happy Weather utilities +dofile(modpath.."/utils.lua") + +dofile(modpath.."/light_rain.lua") dofile(modpath.."/rain.lua") +dofile(modpath.."/heavy_rain.lua") +dofile(modpath.."/snow.lua") if minetest.get_modpath("lightning") ~= nil then dofile(modpath.."/thunder.lua") end --- If not located then embeded skycolor mod version will be loaded. -if minetest.get_modpath("skycolor") == nil then - dofile(modpath.."/skycolor.lua") -end +-- Turn off lightning mod 'auto mode' +lightning.auto = false + +dofile(modpath.."/abm.lua") diff --git a/light_rain.lua b/light_rain.lua new file mode 100644 index 0000000..2a65055 --- /dev/null +++ b/light_rain.lua @@ -0,0 +1,165 @@ +------------------------------ +-- Happy Weather: Light Rain + +-- License: MIT + +-- Credits: xeranas +------------------------------ + +local light_rain = {} +light_rain.last_check = 0 +light_rain.check_interval = 300 + +-- Weather identification code +light_rain.code = "light_rain" + +-- Keeps sound handler references +local sound_handlers = {} + +-- Manual triggers flags +local manual_trigger_start = false +local manual_trigger_end = false + +-- Skycolor layer id +local SKYCOLOR_LAYER = "happy_weather_light_rain_sky" + +light_rain.is_starting = function(dtime, position) + if light_rain.last_check + light_rain.check_interval < os.time() then + light_rain.last_check = os.time() + if math.random() < 0.2 then + return true + end + end + + if manual_trigger_start then + manual_trigger_start = false + return true + end + + return false +end + +light_rain.is_ending = function(dtime) + if light_rain.last_check + light_rain.check_interval < os.time() then + light_rain.last_check = os.time() + if math.random() < 0.4 then + return true + end + end + + if manual_trigger_end then + manual_trigger_end = false + return true + end + + return false +end + +local set_sky_box = function(player_name) + local sl = {} + sl.layer_type = skylayer.SKY_PLAIN + sl.name = SKYCOLOR_LAYER + sl.data = {gradient_data={}} + sl.data.gradient_data.colors = { + {r=0, g=0, b=0}, + {r=85, g=86, b=98}, + {r=152, g=150, b=159}, + {r=85, g=86, b=98}, + {r=0, g=0, b=0} + } + skylayer.add_layer(player_name, sl) +end + +local set_rain_sound = function(player) + return minetest.sound_play("light_rain_drop", { + object = player, + max_hear_distance = 2, + loop = true, + }) +end + +local remove_rain_sound = function(player) + local sound = sound_handlers[player:get_player_name()] + if sound ~= nil then + minetest.sound_stop(sound) + sound_handlers[player:get_player_name()] = nil + end +end + +light_rain.add_player = function(player) + sound_handlers[player:get_player_name()] = set_rain_sound(player) + set_sky_box(player:get_player_name()) +end + +light_rain.remove_player = function(player) + remove_rain_sound(player) + skylayer.remove_layer(player:get_player_name(), SKYCOLOR_LAYER) +end + +-- Random texture getter +local choice_random_rain_drop_texture = function() + local texture_name + local random_number = math.random() + if random_number > 0.33 then + texture_name = "happy_weather_light_rain_raindrop_1.png" + elseif random_number > 0.66 then + texture_name = "happy_weather_light_rain_raindrop_2.png" + else + texture_name = "happy_weather_light_rain_raindrop_3.png" + end + return texture_name; +end + +local add_rain_particle = function(player) + local offset = { + front = 5, + back = 2, + top = 4 + } + + local random_pos = hw_utils.get_random_pos(player, offset) + + if hw_utils.is_outdoor(random_pos) then + minetest.add_particle({ + pos = {x=random_pos.x, y=random_pos.y, z=random_pos.z}, + velocity = {x=0, y=-10, z=0}, + acceleration = {x=0, y=-30, z=0}, + expirationtime = 2, + size = math.random(0.5, 3), + collisiondetection = true, + collision_removal = true, + vertical = true, + texture = choice_random_rain_drop_texture(), + playername = player:get_player_name() + }) + end +end + +local display_rain_particles = function(player) + if hw_utils.is_underwater(player) then + return + end + + add_rain_particle(player) +end + +light_rain.in_area = function(position) + if position.y > -10 then + return true + end + return false +end + +light_rain.render = function(dtime, player) + display_rain_particles(player) +end + +light_rain.start = function() + manual_trigger_start = true +end + +light_rain.stop = function() + manual_trigger_end = true +end + +happy_weather.register_weather(light_rain) \ No newline at end of file diff --git a/rain.lua b/rain.lua index cd2bb78..f1fee73 100644 --- a/rain.lua +++ b/rain.lua @@ -1,186 +1,172 @@ -rain = { - -- max rain particles created at time - particles_count = 35, +------------------------------ +-- Happy Weather: Rain - -- flag to turn on/off extinguish fire for rain - extinguish_fire = true, - - -- flag useful when mixing weathers - raining = false, +-- License: MIT - -- keeping last timeofday value (rounded). - -- Defaulted to non-existing value for initial comparing. - sky_last_update = -1, +-- Credits: xeranas +------------------------------ - init_done = false, -} +local rain = {} +rain.last_check = 0 +rain.check_interval = 400 -rain.sound_handler = function(player) - return minetest.sound_play("weather_rain", { - object = player, - max_hear_distance = 2, - loop = true, - }) -end +-- Weather identification code +rain.code = "rain" --- set skybox based on time (uses skycolor api) -rain.set_sky_box = function() - skycolor.add_layer( - "weather-pack-rain-sky", - {{r=0, g=0, b=0}, - {r=85, g=86, b=98}, - {r=152, g=150, b=159}, - {r=85, g=86, b=98}, - {r=0, g=0, b=0}}) - skycolor.active = true -end +-- Keeps sound handler references +local sound_handlers = {} --- creating manually parctiles instead of particles spawner because of easier to control --- spawn position. -rain.add_rain_particles = function(player) +-- Manual triggers flags +local manual_trigger_start = false +local manual_trigger_end = false - rain.last_rp_count = 0 - for i=rain.particles_count, 1,-1 do - local random_pos_x, random_pos_y, random_pos_z = weather.get_random_pos_by_player_look_dir(player) - if minetest.get_node_light({x=random_pos_x, y=random_pos_y, z=random_pos_z}, 0.5) == 15 then - rain.last_rp_count = rain.last_rp_count + 1 - minetest.add_particle({ - pos = {x=random_pos_x, y=random_pos_y, z=random_pos_z}, - velocity = {x=0, y=-10, z=0}, - acceleration = {x=0, y=-30, z=0}, - expirationtime = 0.2, - size = math.random(0.5, 3), - collisiondetection = true, - collision_removal = true, - vertical = true, - texture = rain.get_texture(), - playername = player:get_player_name() - }) +-- Skycolor layer id +local SKYCOLOR_LAYER = "happy_weather_rain_sky" + +rain.is_starting = function(dtime, position) + if rain.last_check + rain.check_interval < os.time() then + rain.last_check = os.time() + if math.random() < 0.1 then + happy_weather.request_to_end("light_rain") + happy_weather.request_to_end("heavy_rain") + return true end end + + if manual_trigger_start then + manual_trigger_start = false + return true + end + + return false end --- Simple random texture getter -rain.get_texture = function() - local texture_name - local random_number = math.random() - if random_number > 0.33 then - texture_name = "weather_pack_rain_raindrop_1.png" - elseif random_number > 0.66 then - texture_name = "weather_pack_rain_raindrop_2.png" - else - texture_name = "weather_pack_rain_raindrop_3.png" +rain.is_ending = function(dtime) + if rain.last_check + rain.check_interval < os.time() then + rain.last_check = os.time() + if math.random() < 0.3 then + happy_weather.request_to_start("light_rain") + return true + end end - return texture_name; + + if manual_trigger_end then + manual_trigger_end = false + return true + end + + return false +end + +local set_sky_box = function(player_name) + local sl = {} + sl.layer_type = skylayer.SKY_PLAIN + sl.name = SKYCOLOR_LAYER + sl.data = {gradient_data={}} + sl.data.gradient_data.colors = { + {r=0, g=0, b=0}, + {r=85, g=86, b=98}, + {r=152, g=150, b=159}, + {r=85, g=86, b=98}, + {r=0, g=0, b=0} + } + skylayer.add_layer(player_name, sl) +end + +local set_rain_sound = function(player) + return minetest.sound_play("rain_drop", { + object = player, + max_hear_distance = 2, + loop = true, + }) +end + +local remove_rain_sound = function(player) + local sound = sound_handlers[player:get_player_name()] + if sound ~= nil then + minetest.sound_stop(sound) + sound_handlers[player:get_player_name()] = nil + end end --- register player for rain weather. --- basically needs for origin sky reference and rain sound controls. rain.add_player = function(player) - if weather.players[player:get_player_name()] == nil then - local player_meta = {} - player_meta.origin_sky = {player:get_sky()} - weather.players[player:get_player_name()] = player_meta - end + sound_handlers[player:get_player_name()] = set_rain_sound(player) + set_sky_box(player:get_player_name()) end --- remove player from player list effected by rain. --- be sure to remove sound before removing player otherwise soundhandler reference will be lost. rain.remove_player = function(player) - local player_meta = weather.players[player:get_player_name()] - if player_meta ~= nil and player_meta.origin_sky ~= nil then - player:set_sky(player_meta.origin_sky[1], player_meta.origin_sky[2], player_meta.origin_sky[3]) - weather.players[player:get_player_name()] = nil - end + remove_rain_sound(player) + skylayer.remove_layer(player:get_player_name(), SKYCOLOR_LAYER) end --- adds and removes rain sound depending how much rain particles around player currently exist. --- have few seconds delay before each check to avoid on/off sound too often --- when player stay on 'edge' where sound should play and stop depending from random raindrop appearance. -rain.update_sound = function(player) - local player_meta = weather.players[player:get_player_name()] - if player_meta ~= nil then - if player_meta.sound_updated ~= nil and player_meta.sound_updated + 5 > os.time() then - return false - end - - if player_meta.sound_handler ~= nil then - if rain.last_rp_count == 0 then - minetest.sound_stop(player_meta.sound_handler) - player_meta.sound_handler = nil - end - elseif rain.last_rp_count > 0 then - player_meta.sound_handler = rain.sound_handler(player) - end - - player_meta.sound_updated = os.time() +-- Random texture getter +local choice_random_rain_drop_texture = function() + local texture_name + local random_number = math.random() + if random_number > 0.33 then + texture_name = "happy_weather_light_rain_raindrop_1.png" + elseif random_number > 0.66 then + texture_name = "happy_weather_light_rain_raindrop_2.png" + else + texture_name = "happy_weather_light_rain_raindrop_3.png" end + return texture_name; end --- rain sound removed from player. -rain.remove_sound = function(player) - local player_meta = weather.players[player:get_player_name()] - if player_meta ~= nil and player_meta.sound_handler ~= nil then - minetest.sound_stop(player_meta.sound_handler) - player_meta.sound_handler = nil - end +local add_rain_particle = function(player) + local offset = { + front = 5, + back = 2, + top = 6 + } + + local random_pos = hw_utils.get_random_pos(player, offset) + + if hw_utils.is_outdoor(random_pos) then + minetest.add_particle({ + pos = {x=random_pos.x, y=random_pos.y, z=random_pos.z}, + velocity = {x=0, y=-15, z=0}, + acceleration = {x=0, y=-35, z=0}, + expirationtime = 2, + size = math.random(1, 6), + collisiondetection = true, + collision_removal = true, + vertical = true, + texture = choice_random_rain_drop_texture(), + playername = player:get_player_name() + }) + end end --- callback function for removing rain -rain.clear = function() - rain.raining = false - rain.sky_last_update = -1 - rain.init_done = false - skycolor.remove_layer("weather-pack-rain-sky") - for _, player in ipairs(minetest.get_connected_players()) do - rain.remove_sound(player) - rain.remove_player(player) - end +local display_rain_particles = function(player) + if hw_utils.is_underwater(player) then + return + end + + add_rain_particle(player) end -minetest.register_globalstep(function(dtime) - if weather.state ~= "rain" then - return false - end - - rain.make_weather() -end) - -rain.make_weather = function() - if rain.init_done == false then - rain.raining = true - rain.set_sky_box() - end - - for _, player in ipairs(minetest.get_connected_players()) do - if (weather.is_underwater(player)) then - return false - end - rain.add_player(player) - rain.add_rain_particles(player) - rain.update_sound(player) - end +rain.in_area = function(position) + if position.y > -10 then + return true + end + return false end -if weather.reg_weathers.rain == nil then - weather.reg_weathers.rain = { - chance = 15, - clear = rain.clear - } +local particles_number_per_update = 10 +rain.render = function(dtime, player) + + for i=particles_number_per_update, 1,-1 do + display_rain_particles(player) + end end --- ABM for extinguish fire -if weather.allow_abm then - minetest.register_abm({ - nodenames = {"fire:basic_flame"}, - interval = 4.0, - chance = 2, - action = function(pos, node, active_object_count, active_object_count_wider) - if rain.raining and rain.extinguish_fire then - if weather.is_outdoor(pos) then - minetest.remove_node(pos) - end - end - end - }) -end \ No newline at end of file +rain.start = function() + manual_trigger_start = true +end + +rain.stop = function() + manual_trigger_end = true +end + +happy_weather.register_weather(rain) \ No newline at end of file diff --git a/skycolor.lua b/skycolor.lua deleted file mode 100644 index b5005ba..0000000 --- a/skycolor.lua +++ /dev/null @@ -1,212 +0,0 @@ -skycolor = { - -- Should be activated before do any effect. - active = false, - - -- To skip update interval - force_update = true, - - -- Update interval. - update_interval = 15, - - -- Main sky colors: starts from midnight to midnight. - -- Please do not set directly. Use add_layer instead. - colors = {}, - - -- min value which will be used in color gradient, usualy its first user given color in 'pure' color. - min_val = 0, - - -- number of colors while constructing gradient of user given colors - max_val = 1000, - - -- Enables smooth transition between existing sky color and target. - smooth_transitions = true, - - -- Transition between current sky color and new user given. - transition_in_progress = false, - - -- Transition colors are generated automaticly during initialization. - transition_colors = {}, - - -- Time where transition between current color and user given will be done - transition_time = 15, - - -- Tracks how much time passed during transition - transition_timer = 0, - - -- Table for tracking layer order - layer_names = {}, - - -- To layer to colors table - add_layer = function(layer_name, layer_color, instant_update) - skycolor.colors[layer_name] = layer_color - table.insert(skycolor.layer_names, layer_name) - if (instant_update ~= true) then - skycolor.init_transition() - end - skycolor.force_update = true - end, - - -- Retrieve layer from colors table - retrieve_layer = function() - local last_layer = skycolor.layer_names[#skycolor.layer_names] - return skycolor.colors[last_layer] - end, - - -- Remove layer from colors table - remove_layer = function(layer_name) - for k, name in ipairs(skycolor.layer_names) do - if name == layer_name then - table.remove(skycolor.layer_names, k) - skycolor.force_update = true - return - end - end - end, - - -- Update sky color. If players not specified update sky for all players. - update_sky_color = function(players) - local color = skycolor.current_sky_layer_color() - if (color == nil) then - skycolor.active = false - skycolor.set_default_sky() - return - end - - players = skycolor.utils.get_players(players) - for _, player in ipairs(players) do - player:set_sky(color, "plain", nil) - end - end, - - -- Returns current layer color in {r, g, b} format - current_sky_layer_color = function() - if #skycolor.layer_names == 0 then - return nil - end - - -- min timeofday value 0; max timeofday value 1. So sky color gradient range will be between 0 and 1 * skycolor.max_value. - local timeofday = minetest.get_timeofday() - local rounded_time = math.floor(timeofday * skycolor.max_val) - local color = skycolor.utils.convert_to_rgb(skycolor.min_val, skycolor.max_val, rounded_time, skycolor.retrieve_layer()) - return color - end, - - -- Initialy used only on - update_transition_sky_color = function() - if #skycolor.layer_names == 0 then - skycolor.active = false - skycolor.set_default_sky() - return - end - - local multiplier = 100 - local rounded_time = math.floor(skycolor.transition_timer * multiplier) - if rounded_time >= skycolor.transition_time * multiplier then - skycolor.stop_transition() - return - end - - local color = skycolor.utils.convert_to_rgb(0, skycolor.transition_time * multiplier, rounded_time, skycolor.transition_colors) - - local players = skycolor.utils.get_players(nil) - for _, player in ipairs(players) do - player:set_sky(color, "plain", nil) - end - end, - - -- Reset sky color to game default. If players not specified update sky for all players. - -- Could be sometimes useful but not recomended to use in general case as there may be other color layers - -- which needs to preserve. - set_default_sky = function(players) - local players = skycolor.utils.get_players(players) - for _, player in ipairs(players) do - player:set_sky(nil, "regular", nil) - end - end, - - init_transition = function() - -- sadly default sky returns unpredictible colors so transition mode becomes usable only for user defined color layers - -- Here '2' means that one color layer existed before new added and transition is posible. - if #skycolor.layer_names < 2 then - return - end - - local transition_start_color = skycolor.utils.get_current_bg_color() - if (transition_start_color == nil) then - return - end - local transition_end_color = skycolor.current_sky_layer_color() - skycolor.transition_colors = {transition_start_color, transition_end_color} - skycolor.transition_in_progress = true - end, - - stop_transition = function() - skycolor.transition_in_progress = false - skycolor.transition_colors = {} - skycolor.transition_timer = 0 - end, - - utils = { - convert_to_rgb = function(minval, maxval, current_val, colors) - local max_index = #colors - 1 - local val = (current_val-minval) / (maxval-minval) * max_index + 1.0 - local index1 = math.floor(val) - local index2 = math.min(math.floor(val)+1, max_index + 1) - local f = val - index1 - local c1 = colors[index1] - local c2 = colors[index2] - return {r=math.floor(c1.r + f*(c2.r - c1.r)), g=math.floor(c1.g + f*(c2.g-c1.g)), b=math.floor(c1.b + f*(c2.b - c1.b))} - end, - - -- Simply getter. Ether returns user given players list or get all connected players if none provided - get_players = function(players) - if players == nil or #players == 0 then - players = minetest.get_connected_players() - end - return players - end, - - -- Returns first player sky color. I assume that all players are in same color layout. - get_current_bg_color = function() - local players = skycolor.utils.get_players(nil) - for _, player in ipairs(players) do - return player:get_sky() - end - return nil - end - }, - -} - -local timer = 0 -minetest.register_globalstep(function(dtime) - if skycolor.active ~= true or #minetest.get_connected_players() == 0 then - return - end - - if skycolor.smooth_transitions and skycolor.transition_in_progress then - skycolor.transition_timer = skycolor.transition_timer + dtime - skycolor.update_transition_sky_color() - return - end - - if skycolor.force_update then - skycolor.update_sky_color() - skycolor.force_update = false - return - end - - -- regular updates based on iterval - timer = timer + dtime; - if timer >= skycolor.update_interval then - skycolor.update_sky_color() - timer = 0 - end - -end) - -minetest.register_on_joinplayer(function(player) - if (skycolor.active) then - skycolor.update_sky_color({player}) - end -end) \ No newline at end of file diff --git a/snow.lua b/snow.lua index 7b4ec19..25e56df 100644 --- a/snow.lua +++ b/snow.lua @@ -1,90 +1,122 @@ -snow = {} +------------------------------ +-- Happy Weather: Light Rain -snow.particles_count = 15 -snow.init_done = false +-- License: MIT --- calculates coordinates and draw particles for snow weather -snow.add_rain_particles = function(player) - rain.last_rp_count = 0 - for i=snow.particles_count, 1,-1 do - local random_pos_x, random_pos_y, random_pos_z = weather.get_random_pos_by_player_look_dir(player) - random_pos_y = math.random() + math.random(player:getpos().y - 1, player:getpos().y + 7) - if minetest.get_node_light({x=random_pos_x, y=random_pos_y, z=random_pos_z}, 0.5) == 15 then - rain.last_rp_count = rain.last_rp_count + 1 - minetest.add_particle({ - pos = {x=random_pos_x, y=random_pos_y, z=random_pos_z}, - velocity = {x = math.random(-1,-0.5), y = math.random(-2,-1), z = math.random(-1,-0.5)}, - acceleration = {x = math.random(-1,-0.5), y=-0.5, z = math.random(-1,-0.5)}, - expirationtime = 2.0, - size = math.random(0.5, 2), - collisiondetection = true, - collision_removal = true, - vertical = true, - texture = snow.get_texture(), - playername = player:get_player_name() - }) - end +-- Credits: xeranas +------------------------------ + +local snow = {} + +-- Weather identification code +snow.code = "snow" + +-- Manual triggers flags +local manual_trigger_start = false +local manual_trigger_end = false + +-- Skycolor layer id +local SKYCOLOR_LAYER = "happy_weather_snow_sky" + +snow.is_starting = function(dtime, position) + if manual_trigger_start then + manual_trigger_start = false + return true + end + + return false +end + +snow.is_ending = function(dtime) + if manual_trigger_end then + manual_trigger_end = false + return true + end + + return false +end + +local set_sky_box = function(player_name) + local sl = {} + sl.layer_type = skylayer.SKY_PLAIN + sl.name = SKYCOLOR_LAYER + sl.data = {gradient_data={}} + sl.data.gradient_data.colors = { + {r=0, g=0, b=0}, + {r=241, g=244, b=249}, + {r=0, g=0, b=0} + } + skylayer.add_layer(player_name, sl) +end + +snow.add_player = function(player) + set_sky_box(player:get_player_name()) +end + +snow.remove_player = function(player) + skylayer.remove_layer(player:get_player_name(), SKYCOLOR_LAYER) +end + +-- Random texture getter +local choice_random_rain_drop_texture = function() + local texture_name + local random_number = math.random() + if random_number > 0.33 then + texture_name = "happy_weather_light_snow_snowflake_1.png" + elseif random_number > 0.66 then + texture_name = "happy_weather_light_snow_snowflake_2.png" + else + texture_name = "happy_weather_light_snow_snowflake_3.png" + end + return texture_name; +end + +local add_particle = function(player) + local offset = { + front = 5, + back = 2, + top = 4 + } + + local random_pos = hw_utils.get_random_pos(player, offset) + + if hw_utils.is_outdoor(random_pos) then + minetest.add_particle({ + pos = {x=random_pos.x, y=random_pos.y, z=random_pos.z}, + velocity = {x = math.random(-1,-0.5), y = math.random(-2,-1), z = math.random(-1,-0.5)}, + acceleration = {x = math.random(-1,-0.5), y=-0.5, z = math.random(-1,-0.5)}, + expirationtime = 2.0, + size = math.random(0.5, 2), + collisiondetection = true, + collision_removal = true, + vertical = true, + texture = choice_random_rain_drop_texture(), + playername = player:get_player_name() + }) + end +end + +local display_particles = function(player) + if hw_utils.is_underwater(player) then + return + end + + add_particle(player) +end + +local particles_number_per_update = 10 +snow.render = function(dtime, player) + for i=particles_number_per_update, 1,-1 do + display_particles(player) end end -snow.set_sky_box = function() - skycolor.add_layer( - "weather-pack-snow-sky", - {{r=0, g=0, b=0}, - {r=241, g=244, b=249}, - {r=0, g=0, b=0}} - ) - skycolor.active = true +snow.start = function() + manual_trigger_start = true end -snow.clear = function() - skycolor.remove_layer("weather-pack-snow-sky") - snow.init_done = false -end - --- Simple random texture getter -snow.get_texture = function() - local texture_name - local random_number = math.random() - if random_number > 0.5 then - texture_name = "weather_pack_snow_snowflake1.png" - else - texture_name = "weather_pack_snow_snowflake2.png" - end - return texture_name; -end - -local timer = 0 -minetest.register_globalstep(function(dtime) - if weather.state ~= "snow" then - return false - end - - timer = timer + dtime; - if timer >= 0.5 then - timer = 0 - else - return - end - - if snow.init_done == false then - snow.set_sky_box() - snow.init_done = true - end - - for _, player in ipairs(minetest.get_connected_players()) do - if (weather.is_underwater(player)) then - return false - end - snow.add_rain_particles(player) - end -end) - --- register snow weather -if weather.reg_weathers.snow == nil then - weather.reg_weathers.snow = { - chance = 10, - clear = snow.clear - } +snow.stop = function() + manual_trigger_end = true end +happy_weather.register_weather(snow) \ No newline at end of file diff --git a/sounds/heavy_rain_drop.ogg b/sounds/heavy_rain_drop.ogg new file mode 100644 index 0000000..a5a0558 Binary files /dev/null and b/sounds/heavy_rain_drop.ogg differ diff --git a/sounds/light_rain_drop.ogg b/sounds/light_rain_drop.ogg new file mode 100644 index 0000000..cc2efaf Binary files /dev/null and b/sounds/light_rain_drop.ogg differ diff --git a/sounds/weather_rain.ogg b/sounds/rain_drop.ogg similarity index 100% rename from sounds/weather_rain.ogg rename to sounds/rain_drop.ogg diff --git a/textures/happy_weather_heavy_rain_drops.png b/textures/happy_weather_heavy_rain_drops.png new file mode 100644 index 0000000..6b8b7ec Binary files /dev/null and b/textures/happy_weather_heavy_rain_drops.png differ diff --git a/textures/happy_weather_light_rain_raindrop_1.png b/textures/happy_weather_light_rain_raindrop_1.png new file mode 100644 index 0000000..708794a Binary files /dev/null and b/textures/happy_weather_light_rain_raindrop_1.png differ diff --git a/textures/happy_weather_light_rain_raindrop_2.png b/textures/happy_weather_light_rain_raindrop_2.png new file mode 100644 index 0000000..10f6521 Binary files /dev/null and b/textures/happy_weather_light_rain_raindrop_2.png differ diff --git a/textures/happy_weather_light_rain_raindrop_3.png b/textures/happy_weather_light_rain_raindrop_3.png new file mode 100644 index 0000000..f7b3b56 Binary files /dev/null and b/textures/happy_weather_light_rain_raindrop_3.png differ diff --git a/textures/happy_weather_light_snow_snowflake_1.png b/textures/happy_weather_light_snow_snowflake_1.png new file mode 100644 index 0000000..bfdc6b7 Binary files /dev/null and b/textures/happy_weather_light_snow_snowflake_1.png differ diff --git a/textures/happy_weather_light_snow_snowflake_2.png b/textures/happy_weather_light_snow_snowflake_2.png new file mode 100644 index 0000000..d2c8d81 Binary files /dev/null and b/textures/happy_weather_light_snow_snowflake_2.png differ diff --git a/textures/happy_weather_light_snow_snowflake_3.png b/textures/happy_weather_light_snow_snowflake_3.png new file mode 100644 index 0000000..591a245 Binary files /dev/null and b/textures/happy_weather_light_snow_snowflake_3.png differ diff --git a/textures/weather_pack_rain_raindrop_1.png b/textures/weather_pack_rain_raindrop_1.png deleted file mode 100644 index ab18333..0000000 Binary files a/textures/weather_pack_rain_raindrop_1.png and /dev/null differ diff --git a/textures/weather_pack_rain_raindrop_2.png b/textures/weather_pack_rain_raindrop_2.png deleted file mode 100644 index fb37100..0000000 Binary files a/textures/weather_pack_rain_raindrop_2.png and /dev/null differ diff --git a/textures/weather_pack_rain_raindrop_3.png b/textures/weather_pack_rain_raindrop_3.png deleted file mode 100644 index 4432b35..0000000 Binary files a/textures/weather_pack_rain_raindrop_3.png and /dev/null differ diff --git a/textures/weather_pack_snow_snowflake1.png b/textures/weather_pack_snow_snowflake1.png deleted file mode 100644 index 8604f5d..0000000 Binary files a/textures/weather_pack_snow_snowflake1.png and /dev/null differ diff --git a/textures/weather_pack_snow_snowflake2.png b/textures/weather_pack_snow_snowflake2.png deleted file mode 100644 index bea317e..0000000 Binary files a/textures/weather_pack_snow_snowflake2.png and /dev/null differ diff --git a/thunder.lua b/thunder.lua index 3ff6453..90056ab 100644 --- a/thunder.lua +++ b/thunder.lua @@ -1,37 +1,86 @@ --- turn off lightning mod 'auto mode' -lightning.auto = false +---------------------------------------------------------------- +-- Happy Weather: Thunder -thunder = { - next_strike = 0, - min_delay = 3, - max_delay = 12, -} +-- License: MIT -minetest.register_globalstep(function(dtime) - if weather.state ~= "thunder" then - return false - end - - rain.make_weather() - - if (thunder.next_strike <= os.time()) then - lightning.strike() - local delay = math.random(thunder.min_delay, thunder.max_delay) - thunder.next_strike = os.time() + delay - end +-- Credits: xeranas -end) +-- See also: lightning mod for actual lightning effect, sounds. +---------------------------------------------------------------- -thunder.clear = function() - rain.clear() +local thunder = {} +thunder.last_check = 0 +thunder.check_interval = 100 + +-- Weather identification code +thunder.code = "thunder" + +local thunder_target_weather_code = "heavy_rain" + +-- Manual triggers flags +local manual_trigger_start = false +local manual_trigger_end = false + +-- Thunder weather appearance control +local thunder_weather_chance = 5 -- 5 percent appearance during heavy rain +local thunder_weather_next_check = 0 +local thunder_weather_check_delay = 600 -- to avoid checks continuously + +thunder.is_starting = function(dtime) + checked = false + thunder.next_strike = 0 + thunder.min_delay = 5 + thunder.max_delay = math.random(5, 45) + + if thunder.last_check + thunder.check_interval < os.time() then + thunder.last_check = os.time() + if math.random() < 0.8 and happy_weather.is_weather_active("heavy_rain") then + return true + end + end + + if manual_trigger_start then + manual_trigger_start = false + return true + end + + return false end --- register thunderstorm weather -if weather.reg_weathers.thunder == nil then - weather.reg_weathers.thunder = { - chance = 5, - clear = thunder.clear, - min_duration = 120, - max_duration = 600, - } -end \ No newline at end of file +thunder.is_ending = function(dtime) + if thunder.last_check + thunder.check_interval < os.time() then + thunder.last_check = os.time() + if math.random() < 0.4 or happy_weather.is_weather_active("heavy_rain") == false then + return true + end + end + + if manual_trigger_end then + manual_trigger_end = false + return true + end + + return false +end + +local calculate_thunder_strike_delay = function() + local delay = math.random(thunder.min_delay, thunder.max_delay) + thunder.next_strike = os.time() + delay +end + +thunder.render = function(dtime, player) + if thunder.next_strike <= os.time() then + lightning.strike() + calculate_thunder_strike_delay() + end +end + +thunder.start = function() + manual_trigger_start = true +end + +thunder.stop = function() + manual_trigger_end = true +end + +happy_weather.register_weather(thunder) \ No newline at end of file diff --git a/utils.lua b/utils.lua new file mode 100644 index 0000000..02ff509 --- /dev/null +++ b/utils.lua @@ -0,0 +1,76 @@ +--------------------------------------- +-- Happy Weather: Utilities / Helpers + +-- License: MIT + +-- Credits: xeranas +--------------------------------------- + +if hw_utils == nil then + hw_utils = {} +end + +-- outdoor check based on node light level +hw_utils.is_outdoor = function(pos, offset_y) + if offset_y == nil then + offset_y = 0 + end + + if minetest.get_node_light({x=pos.x, y=pos.y + offset_y, z=pos.z}, 0.5) == 15 then + return true + end + return false +end + +-- checks if player is undewater. This is needed in order to +-- turn off weather particles generation. +hw_utils.is_underwater = function(player) + local ppos = player:getpos() + local offset = player:get_eye_offset() + local player_eye_pos = {x = ppos.x + offset.x, + y = ppos.y + offset.y + 1.5, + z = ppos.z + offset.z} + local node_level = minetest.get_node_level(player_eye_pos) + if node_level == 8 or node_level == 7 then + return true + end + return false +end + +-- trying to locate position for particles by player look direction for performance reason. +-- it is costly to generate many particles around player so goal is focus mainly on front view. +hw_utils.get_random_pos = function(player, offset) + local look_dir = player:get_look_dir() + local player_pos = player:getpos() + + local random_pos_x = 0 + local random_pos_y = 0 + local random_pos_z = 0 + + if look_dir.x > 0 then + if look_dir.z > 0 then + random_pos_x = math.random(player_pos.x - offset.back, player_pos.x + offset.front) + math.random() + random_pos_z = math.random(player_pos.z - offset.back, player_pos.z + offset.front) + math.random() + else + random_pos_x = math.random(player_pos.x - offset.back, player_pos.x + offset.front) + math.random() + random_pos_z = math.random(player_pos.z - offset.front, player_pos.z + offset.back) + math.random() + end + else + if look_dir.z > 0 then + random_pos_x = math.random(player_pos.x - offset.front, player_pos.x + offset.back) + math.random() + random_pos_z = math.random(player_pos.z - offset.back, player_pos.z + offset.front) + math.random() + else + random_pos_x = math.random(player_pos.x - offset.front, player_pos.x + offset.back) + math.random() + random_pos_z = math.random(player_pos.z - offset.front, player_pos.z + offset.back) + math.random() + end + end + + if offset.bottom ~= nil then + random_pos_y = math.random(player_pos.y - offset.bottom, player_pos.y + offset.top) + else + random_pos_y = player_pos.y + offset.top + end + + + return {x=random_pos_x, y=random_pos_y, z=random_pos_z} +end \ No newline at end of file diff --git a/weather_core.lua b/weather_core.lua deleted file mode 100644 index 66466be..0000000 --- a/weather_core.lua +++ /dev/null @@ -1,175 +0,0 @@ -weather = { - -- weather states, 'none' is default, other states depends from active mods - state = "none", - - -- player list for saving player meta info - players = {}, - - -- time when weather should be re-calculated - next_check = 0, - - -- default weather recalculation interval - check_interval = 300, - - -- weather min duration - min_duration = 240, - - -- weather max duration - max_duration = 3600, - - -- weather calculated end time - end_time = nil, - - -- registered weathers - reg_weathers = {}, - - -- automaticly calculates intervals and swap weathers - auto_mode = true, - - -- global flag to disable/enable ABM logic. - allow_abm = true, -} - -weather.get_rand_end_time = function(min_duration, max_duration) - if min_duration ~= nil and max_duration ~= nil then - return os.time() + math.random(min_duration, max_duration); - else - return os.time() + math.random(weather.min_duration, weather.max_duration); - end -end - -weather.is_outdoor = function(pos) - if minetest.get_node_light({x=pos.x, y=pos.y + 1, z=pos.z}, 0.5) == 15 then - return true - end - return false -end - --- checks if player is undewater. This is needed in order to --- turn off weather particles generation. -weather.is_underwater = function(player) - local ppos = player:getpos() - local offset = player:get_eye_offset() - local player_eye_pos = {x = ppos.x + offset.x, - y = ppos.y + offset.y + 1.5, - z = ppos.z + offset.z} - local node_level = minetest.get_node_level(player_eye_pos) - if node_level == 8 or node_level == 7 then - return true - end - return false -end - --- trying to locate position for particles by player look direction for performance reason. --- it is costly to generate many particles around player so goal is focus mainly on front view. -weather.get_random_pos_by_player_look_dir = function(player) - local look_dir = player:get_look_dir() - local player_pos = player:getpos() - - local random_pos_x = 0 - local random_pos_y = 0 - local random_pos_z = 0 - - if look_dir.x > 0 then - if look_dir.z > 0 then - random_pos_x = math.random() + math.random(player_pos.x - 2.5, player_pos.x + 5) - random_pos_z = math.random() + math.random(player_pos.z - 2.5, player_pos.z + 5) - else - random_pos_x = math.random() + math.random(player_pos.x - 2.5, player_pos.x + 5) - random_pos_z = math.random() + math.random(player_pos.z - 5, player_pos.z + 2.5) - end - else - if look_dir.z > 0 then - random_pos_x = math.random() + math.random(player_pos.x - 5, player_pos.x + 2.5) - random_pos_z = math.random() + math.random(player_pos.z - 2.5, player_pos.z + 5) - else - random_pos_x = math.random() + math.random(player_pos.x - 5, player_pos.x + 2.5) - random_pos_z = math.random() + math.random(player_pos.z - 5, player_pos.z + 2.5) - end - end - - random_pos_y = math.random() + math.random(player_pos.y + 1, player_pos.y + 3) - return random_pos_x, random_pos_y, random_pos_z -end - -minetest.register_globalstep(function(dtime) - if weather.auto_mode == false then - return 0 - end - - -- recalculate weather only when there aren't currently any - if (weather.state ~= "none") then - if (weather.end_time ~= nil and weather.end_time <= os.time()) then - weather.reg_weathers[weather.state].clear() - weather.state = "none" - end - elseif (weather.next_check <= os.time()) then - for weather_name, weather_meta in pairs(weather.reg_weathers) do - weather.set_random_weather(weather_name, weather_meta) - end - -- fallback next_check set, weather 'none' will be. - weather.next_check = os.time() + weather.check_interval - end -end) - --- sets random weather (which could be 'regular' (no weather)). -weather.set_random_weather = function(weather_name, weather_meta) - if weather.next_check > os.time() then return 0 end - - if (weather_meta ~= nil and weather_meta.chance ~= nil) then - local random_roll = math.random(0,100) - if (random_roll <= weather_meta.chance) then - weather.state = weather_name - weather.end_time = weather.get_rand_end_time(weather_meta.min_duration, weather_meta.max_duration) - weather.next_check = os.time() + weather.check_interval - end - end -end - -minetest.register_privilege("weather_manager", { - description = "Gives ability to control weather", - give_to_singleplayer = false -}) - --- Weather command definition. Set -minetest.register_chatcommand("set_weather", { - params = "", - description = "Changes weather by given param, parameter none will remove weather.", - privs = {weather_manager = true}, - func = function(name, param) - if (param == "none") then - if (weather.state ~= nil and weather.reg_weathers[weather.state] ~= nil) then - weather.reg_weathers[weather.state].clear() - weather.state = param - end - weather.state = "none" - end - - if (weather.reg_weathers ~= nil and weather.reg_weathers[param] ~= nil) then - if (weather.state ~= nil and weather.state ~= "none" and weather.reg_weathers[weather.state] ~= nil) then - weather.reg_weathers[weather.state].clear() - end - weather.state = param - end - end -}) - --- Configuration setting which allows user to disable ABM for weathers (if they use it). --- Weather mods expected to be use this flag before registering ABM. -local weather_allow_abm = minetest.setting_getbool("weather_allow_abm") -if weather_allow_abm ~= nil and weather_allow_abm == false then - weather.allow_abm = false -end - --- Overrides nodes 'sunlight_propagates' attribute for efficient indoor check (e.g. for glass roof). --- Controlled from minetest.conf setting and by default it is disabled. --- To enable set weather_allow_override_nodes to true. --- Only new nodes will be effected (glass roof needs to be rebuilded). -if minetest.setting_getbool("weather_allow_override_nodes") then - if minetest.registered_nodes["default:glass"] then - minetest.override_item("default:glass", {sunlight_propagates = false}) - end - if minetest.registered_nodes["default:meselamp"] then - minetest.override_item("default:meselamp", {sunlight_propagates = false}) - end -end \ No newline at end of file