--------------------------- -- 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 = {} local meta_plawpos = {} -- meta about Player Last Active Weaher Position ------------------------------------ -- 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 -- Returns active weather local get_active_weather = function(weather_code) if #active_weathers == 0 then return nil end for k, weather_ in ipairs(active_weathers) do if weather_.code == weather_code then return weather_ 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 local remove_meta_plawpos = function(weather_code, player_name) if #meta_plawpos == 0 then return end for k, meta_ in ipairs(meta_plawpos) do if (meta_.name == player_name and meta_.code == weather_code) then table.remove(meta_plawpos, k) end end end local add_meta_plawpos = function(weather_code, player) local meta = {} meta.code = weather_code meta.pos = player:getpos() meta.name = player:get_player_name() remove_meta_plawpos(weather_code, player:get_player_name()) table.insert(meta_plawpos, meta) end local get_meta_plawpos = function(weather_code, player_name) if #meta_plawpos == 0 then return nil end for k, meta_ in ipairs(meta_plawpos) do if (meta_.name == player_name and meta_.code == weather_code) then return meta_.pos end end return nil end --------------------------- -- 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 happy_weather.is_player_in_weather_area = function(player_name, weather_code) if #active_weathers == 0 then return false end local active_weather = get_active_weather(weather_code) if active_weather == nil then return false end return is_player_affected(active_weather.affected_players, player_name) 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 in_area 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 local MAX_DISTANCE_FROM_WEATHER = 35 -- This function aims to remove weather flickering effect when player walks on biome edge. -- To accomlish that extra distance is applied before removing player from weather affection. local is_outside_recent_weather = function(weather_code, player) local pos = get_meta_plawpos(weather_code, player:get_player_name()) if pos == nil then return false end local ppos = player:getpos() local d = ((ppos.x - pos.x)^2 + (ppos.y - pos.y)^2 + (ppos.z - pos.z)^2)^0.5 return MAX_DISTANCE_FROM_WEATHER - d < 0 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) add_meta_plawpos(weather_obj.code, player) else if (is_outside_recent_weather(weather_obj.code, player)) then weather_remove_player(weather_obj, player) remove_player(weather_obj.affected_players, player:get_player_name()) -- render weather until player will be completely outside weather range else weather_render(weather_obj, dtime, player) end 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(weather_, 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)