Complete rewrite
@ -1,9 +0,0 @@
|
|||||||
## Assets
|
|
||||||
- Rain sounds CC0 by Q.K., taken from mymonths
|
|
||||||
- Thunder sound and puddle texture DWYWPL by Don, Nathan from mymonths at https://github.com/minetest-mods/mymonths
|
|
||||||
- Snow cover texture WTFPL, taken from mymonths
|
|
||||||
- Rain texture CC-BY-SA 3.0 from TeddyDesTodes, taken from his weather branch at https://github.com/TeddyDesTodes/minetest/tree/weather
|
|
||||||
- Snow flake and rain drop textures CC BY-SA (3.0) by paramat, found in snowdrift mod at https://github.com/paramat/snowdrift
|
|
||||||
- Snow texture composited from individual snow flakes by paramat. CC-BY-SA (3.0)
|
|
||||||
- Wind sound CC-BY (3.0) by InspectorJ from https://freesound.org/people/InspectorJ/sounds/376415/
|
|
||||||
- Hail sound CC0 by ikayuka from https://freesound.org/people/ikayuka/sounds/240742/
|
|
55
TODO.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## Required for MVP
|
||||||
|
- Fix obvious bugs that prevent weathers from being selected
|
||||||
|
- Test if day progression works
|
||||||
|
- Test non-random heat and humidity values
|
||||||
|
- Fix noise function
|
||||||
|
- Implement sounds
|
||||||
|
- Test if sky changes are working
|
||||||
|
- Implement start/end events
|
||||||
|
- Make sure all weather presets are working
|
||||||
|
- Implement chat commands
|
||||||
|
|
||||||
|
## Planned for first release
|
||||||
|
- Improve test_condition function
|
||||||
|
- Implement ABM system for additional effects
|
||||||
|
- Add light level to possible conditions
|
||||||
|
- Implement fallback for sky changes without skylayer
|
||||||
|
- Configurable number of days in a year
|
||||||
|
- Add license information to source files
|
||||||
|
- Write helpful README
|
||||||
|
- Find good values for weather conditions
|
||||||
|
- Make switches between effects more smooth
|
||||||
|
|
||||||
|
## Nice to have
|
||||||
|
- Write documentation on how to add weathers and effects
|
||||||
|
- Register *Moon Phases* as a weather preset using the *skybox* effect
|
||||||
|
- Assign meta data (like "downfall", "wind", etc.) to weather presets
|
||||||
|
- Fog effects
|
||||||
|
|
||||||
|
## Future Plans & Ideas
|
||||||
|
- Complete season system
|
||||||
|
- crops grow better in their respective season or worse in winter
|
||||||
|
- regrowing apples, lemons, etc. in their respective months
|
||||||
|
- holidays and reminders via chat commands
|
||||||
|
- day/night nycle adjusted to season
|
||||||
|
- special events like a bloodmoon -> increased mob spawns
|
||||||
|
- Fantasy weather effects
|
||||||
|
- pretty skyboxes
|
||||||
|
- swirling flower petals, mushroom spores, etc. in fitting biomes
|
||||||
|
- underground environment effects
|
||||||
|
- space effects at high y-level (meteors, magnetic storms, different skybox)
|
||||||
|
- Eco Pack (with climate change)
|
||||||
|
- Integration with Technic mods, etc.
|
||||||
|
- Track planted trees vs chopped wood, killed mobs, etc.
|
||||||
|
- Ecological footprint influences weather
|
||||||
|
- Extreme weather scenarios and desasters
|
||||||
|
- Tornados, poisonous rain that kills crops and animals, bush fires,...
|
||||||
|
- Survival Pack
|
||||||
|
- Body temperature becomes survival aspect
|
||||||
|
- Use local heat as base value
|
||||||
|
- swimming will cool down
|
||||||
|
- standing near fire will warm up
|
||||||
|
- craftable warm clothes
|
||||||
|
- metal armor will worsen heat issues
|
65
ca_effects/particles.lua
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
if not climate_mod.settings.particles then return end
|
||||||
|
|
||||||
|
local function get_particle_texture(particles)
|
||||||
|
if type(particles.textures) == "nil" or next(particles.textures) == nil then
|
||||||
|
return particles.texture
|
||||||
|
end
|
||||||
|
return particles.textures[math.random(#particles.textures)]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function spawn_particles(player, particles)
|
||||||
|
local ppos = player:getpos()
|
||||||
|
local wind_x = climate_mod.state:get_int("wind_x")
|
||||||
|
local wind_z = climate_mod.state:get_int("wind_z")
|
||||||
|
local wind = vector.new(wind_x, 0, wind_z)
|
||||||
|
local wind_pos = vector.multiply(wind, -1)
|
||||||
|
local wind_speed = vector.length(wind)
|
||||||
|
|
||||||
|
local texture = get_particle_texture(particles)
|
||||||
|
|
||||||
|
local minp = vector.add(vector.add(ppos, particles.min_pos), wind_pos)
|
||||||
|
local maxp = vector.add(vector.add(ppos, particles.max_pos), wind_pos)
|
||||||
|
|
||||||
|
local vel = vector.new({
|
||||||
|
x = wind.x,
|
||||||
|
y = -particles.falling_speed,
|
||||||
|
z = wind.z
|
||||||
|
})
|
||||||
|
local acc = vector.new({x=0, y=0, z=0})
|
||||||
|
|
||||||
|
local exp = particles.exptime
|
||||||
|
local vertical = math.abs(vector.normalize(vel).y) >= 0.6
|
||||||
|
|
||||||
|
minetest.add_particlespawner({
|
||||||
|
amount = particles.amount,
|
||||||
|
time = 0.5,
|
||||||
|
minpos = minp,
|
||||||
|
maxpos = maxp,
|
||||||
|
minvel = vel,
|
||||||
|
maxvel = vel,
|
||||||
|
minacc = acc,
|
||||||
|
maxacc = acc,
|
||||||
|
minexptime = exp,
|
||||||
|
maxexptime = exp,
|
||||||
|
minsize = particles.size,
|
||||||
|
maxsize = particles.size,
|
||||||
|
collisiondetection = true,
|
||||||
|
collision_removal = true,
|
||||||
|
vertical = vertical,
|
||||||
|
texture = texture,
|
||||||
|
player = player:get_player_name()
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handle_effect(player_data)
|
||||||
|
for playername, data in pairs(player_data) do
|
||||||
|
minetest.chat_send_player(playername, "spam")
|
||||||
|
local player = minetest.get_player_by_name(playername)
|
||||||
|
for weather, value in pairs(data) do
|
||||||
|
spawn_particles(player, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
climate_api.register_effect("climate_api:particles", handle_effect, "tick")
|
||||||
|
climate_api.set_effect_cycle("climate_api:particles", climate_api.SHORT_CYCLE)
|
35
ca_effects/skybox.lua
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
if not climate_mod.settings.skybox then return end
|
||||||
|
if not minetest.get_modpath("skylayer") then return end
|
||||||
|
|
||||||
|
local SKYBOX_NAME = "climate_api:skybox"
|
||||||
|
|
||||||
|
local function set_skybox(player, sky)
|
||||||
|
sky.name = SKYBOX_NAME
|
||||||
|
skylayer.add_layer(player:get_player_name(), sky)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_skybox(player)
|
||||||
|
skylayer.remove_layer(player:get_player_name(), SKYBOX_NAME)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function handle_effect(player_data)
|
||||||
|
for playername, data in pairs(player_data) do
|
||||||
|
local player = minetest.get_player_by_name(playername)
|
||||||
|
local sky = {}
|
||||||
|
for weather, value in pairs(data) do
|
||||||
|
climate_api.utility.merge_tables(sky, value)
|
||||||
|
end
|
||||||
|
set_skybox(player, sky)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remove_effect(player_data)
|
||||||
|
for playername, data in ipairs(player_data) do
|
||||||
|
local player = minetest.get_player_by_name(playername)
|
||||||
|
remove_skybox(player)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
climate_api.register_effect("climate_api:skybox", handle_effect, "tick")
|
||||||
|
climate_api.register_effect("climate_api:skybox", remove_effect, "end")
|
||||||
|
climate_api.set_effect_cycle("climate_api:skybox", climate_api.LONG_CYCLE)
|
14
ca_effects/sound.lua
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
if not climate_mod.settings.sound then return end
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
local function update_effect(player_data)
|
||||||
|
for playername, data in pairs(player_data) do
|
||||||
|
for weather, value in pairs(data) do
|
||||||
|
climate_mod.effects.play_sound(player, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
climate_api.register_effect("climate_api:sound", update_effect, "change")
|
||||||
|
climate_api.set_effect_cycle("climate_api:skybox", climate_api.LONG_CYCLE)
|
@ -1,5 +0,0 @@
|
|||||||
default
|
|
||||||
datastorage?
|
|
||||||
lightning?
|
|
||||||
player_monoids?
|
|
||||||
playerphysics?
|
|
@ -1,2 +0,0 @@
|
|||||||
The ultimate weather mod with support not only for rain, snow, and hail,
|
|
||||||
but also seasons, dynamic puddles and snow layers, wind, regrowing fruit and much more.
|
|
62
init.lua
@ -1,56 +1,42 @@
|
|||||||
assert(minetest.add_particlespawner, "Believable Weather requires a more current version of Minetest")
|
assert(minetest.add_particlespawner, "[Climate API] This mod requires a more current version of Minetest")
|
||||||
weather_mod = {}
|
|
||||||
|
|
||||||
weather_mod.modname = "believable_weather"
|
climate_api = {}
|
||||||
weather_mod.modpath = minetest.get_modpath(weather_mod.modname)
|
climate_mod = {}
|
||||||
|
|
||||||
|
local modname = minetest.get_current_modname()
|
||||||
|
local modpath = minetest.get_modpath(modname)
|
||||||
|
|
||||||
local function getBoolSetting(name, default)
|
local function getBoolSetting(name, default)
|
||||||
return minetest.is_yes(minetest.settings:get_bool(weather_mod.modname .. "_" .. name) or default)
|
return minetest.is_yes(minetest.settings:get_bool("climate_api_" .. name) or default)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getNumericSetting(name, default)
|
local function getNumericSetting(name, default)
|
||||||
return tonumber(minetest.settings:get(weather_mod.modname .. "_" .. name) or default)
|
return tonumber(minetest.settings:get("climate_api_" .. name) or default)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- load settings from config file
|
-- load settings from config file
|
||||||
weather_mod.settings = {
|
climate_mod.settings = {
|
||||||
damage = getBoolSetting("damage", true),
|
|
||||||
particles = getBoolSetting("particles", true),
|
particles = getBoolSetting("particles", true),
|
||||||
leaves = getBoolSetting("leaves", true),
|
|
||||||
snow = getBoolSetting("snow_layers", true),
|
|
||||||
puddles = getBoolSetting("puddles", true),
|
|
||||||
skybox = getBoolSetting("skybox", true),
|
skybox = getBoolSetting("skybox", true),
|
||||||
raycasting = getBoolSetting("raycasting", true),
|
sound = getBoolSetting("sound", true),
|
||||||
wind = getBoolSetting("wind", true),
|
wind = getBoolSetting("wind", true),
|
||||||
wind_slow = getBoolSetting("wind_slow", true),
|
|
||||||
flowers = getBoolSetting("flowers", true),
|
|
||||||
fruit = getBoolSetting("fruit", true),
|
|
||||||
soil = getBoolSetting("soil", true),
|
|
||||||
seasons = getBoolSetting("seasons", true),
|
seasons = getBoolSetting("seasons", true),
|
||||||
heat = getNumericSetting("base_heat", 0),
|
heat = getNumericSetting("heat_base", 0),
|
||||||
humidity = getNumericSetting("base_humidity", 0),
|
humidity = getNumericSetting("humidity_base", 0)
|
||||||
max_height = getNumericSetting("max_height", 120),
|
|
||||||
min_height = getNumericSetting("min_height", -50)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dofile(weather_mod.modpath.."/lib/datastorage.lua")
|
climate_mod.current_weather = {}
|
||||||
weather_mod.state = weather_mod.get_storage()
|
climate_mod.current_effects = {}
|
||||||
|
|
||||||
-- import core API
|
-- import core API
|
||||||
dofile(weather_mod.modpath.."/lib/player.lua")
|
climate_mod.state = dofile(modpath .. "/lib/datastorage.lua")
|
||||||
dofile(weather_mod.modpath.."/lib/environment.lua")
|
climate_api = dofile(modpath .. "/lib/api.lua")
|
||||||
dofile(weather_mod.modpath.."/lib/wind.lua")
|
climate_api.utility = dofile(modpath .. "/lib/api_utility.lua")
|
||||||
dofile(weather_mod.modpath.."/lib/calendar_dictionary.lua")
|
climate_api.environment = dofile(modpath .. "/lib/environment.lua")
|
||||||
dofile(weather_mod.modpath.."/lib/calendar.lua")
|
climate_mod.world = dofile(modpath .. "/lib/world.lua")
|
||||||
dofile(weather_mod.modpath.."/lib/main.lua")
|
climate_mod.trigger = dofile(modpath .. "/lib/trigger.lua")
|
||||||
dofile(weather_mod.modpath.."/lib/commands.lua")
|
dofile(modpath.."/lib/main.lua")
|
||||||
|
|
||||||
-- import individual weather types
|
-- import predefined environment effects
|
||||||
dofile(weather_mod.modpath.."/weathers/rain.lua")
|
dofile(modpath .. "/ca_effects/particles.lua")
|
||||||
dofile(weather_mod.modpath.."/weathers/rain_heavy.lua")
|
dofile(modpath .. "/ca_effects/skybox.lua")
|
||||||
dofile(weather_mod.modpath.."/weathers/snow.lua")
|
|
||||||
dofile(weather_mod.modpath.."/weathers/snow_heavy.lua")
|
|
||||||
dofile(weather_mod.modpath.."/weathers/storm.lua")
|
|
||||||
dofile(weather_mod.modpath.."/weathers/sandstorm.lua")
|
|
||||||
dofile(weather_mod.modpath.."/weathers/hail.lua")
|
|
||||||
dofile(weather_mod.modpath.."/weathers/pollen.lua")
|
|
||||||
|
39
lib/api.lua
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
local api = {}
|
||||||
|
|
||||||
|
api.SHORT_CYCLE = 0 -- for particles and fast animations
|
||||||
|
api.DEFAULT_CYCLE = 0 -- for most effect types
|
||||||
|
api.LONG_CYCLE = 0 -- for write operations and skybox changes
|
||||||
|
|
||||||
|
climate_mod.weathers = {}
|
||||||
|
climate_mod.effects = {}
|
||||||
|
climate_mod.cycles = {}
|
||||||
|
|
||||||
|
function api.register_weather(name, conditions, effects)
|
||||||
|
-- TODO: check and sanitize
|
||||||
|
climate_mod.weathers[name] = {
|
||||||
|
conditions = conditions,
|
||||||
|
effects = effects,
|
||||||
|
active_players = {}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.register_effect(name, handler, htype)
|
||||||
|
-- check for valid handler types
|
||||||
|
if htype ~= "start" and htype ~= "tick" and htype ~= "stop" then
|
||||||
|
minetest.log("warn", "[Climate API] Invalid effect handler type: " .. htype)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- create effect handler registry if not existent yet
|
||||||
|
if type(climate_mod.effects[name]) == "nil" then
|
||||||
|
climate_mod.effects[name] = { start = {}, tick = {}, stop = {} }
|
||||||
|
climate_mod.cycles[name] = { timespan = DEFAULT_CYCLE, timer = 0 }
|
||||||
|
end
|
||||||
|
-- store effect handler
|
||||||
|
table.insert(climate_mod.effects[name][htype], handler)
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.set_effect_cycle(name, cycle)
|
||||||
|
climate_mod.cycles[name].timespan = cycle
|
||||||
|
end
|
||||||
|
|
||||||
|
return api
|
63
lib/api_utility.lua
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
local mod_player_monoids = minetest.get_modpath("player_monoids") ~= nil
|
||||||
|
local mod_playerphysics = minetest.get_modpath("playerphysics") ~= nil
|
||||||
|
|
||||||
|
local utility = {}
|
||||||
|
|
||||||
|
function utility.rangelim(value, min, max)
|
||||||
|
return math.min(math.max(value, min), max)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- from https://stackoverflow.com/a/29133654
|
||||||
|
-- merges two tables together
|
||||||
|
-- if in conflict, b will override values of a
|
||||||
|
function utility.merge_tables(a, b)
|
||||||
|
if type(a) == "table" and type(b) == "table" then
|
||||||
|
for k,v in pairs(b) do
|
||||||
|
if type(v)=="table" and type(a[k] or false)=="table" then
|
||||||
|
merge(a[k],v)
|
||||||
|
else a[k]=v end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return a
|
||||||
|
end
|
||||||
|
|
||||||
|
-- see https://en.wikipedia.org/wiki/Logistic_function
|
||||||
|
function utility.logistic_growth(value, max, growth, midpoint)
|
||||||
|
return max / (1 + math.exp(-growth * (value - midpoint)))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- generates a wave of cycle length 1
|
||||||
|
-- maps parameters to values between 0 and 1
|
||||||
|
-- 0 is mapped to 0 and 0.5 to 1
|
||||||
|
function utility.normalized_cycle(value)
|
||||||
|
return math.cos((2 * value + 1) * math.pi) / 2 + 0.5
|
||||||
|
end
|
||||||
|
|
||||||
|
-- override player physics
|
||||||
|
-- use utility mod if possible to avoid conflict
|
||||||
|
function utility.add_physics(id, player, effect, value)
|
||||||
|
if mod_player_monoids then
|
||||||
|
player_monoids[effect]:add_change(player, value, id)
|
||||||
|
elseif mod_playerphysics then
|
||||||
|
playerphysics.add_physics_factor(player, effect, id, value)
|
||||||
|
else
|
||||||
|
local override = {}
|
||||||
|
override[effect] = value
|
||||||
|
player:set_physics_override(override)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- reset player phsysics to normal
|
||||||
|
function utility.remove_physics(id, player, effect)
|
||||||
|
if mod_player_monoids then
|
||||||
|
player_monoids[effect]:del_change(player, id)
|
||||||
|
elseif mod_playerphysics then
|
||||||
|
playerphysics.remove_physics_factor(player, effect, id)
|
||||||
|
else
|
||||||
|
local override = {}
|
||||||
|
override[effect] = 1
|
||||||
|
player:set_physics_override(override)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return utility
|
@ -1,18 +0,0 @@
|
|||||||
function weather_mod.get_time_heat()
|
|
||||||
local time = minetest.get_timeofday()
|
|
||||||
return math.cos((2 * time + 1) * math.pi) / 3 + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.get_calendar_heat()
|
|
||||||
-- European heat center in August instead of June
|
|
||||||
local progression = (weather_mod.state.time.day + 61) / 365
|
|
||||||
return math.cos((2 * progression + 1) * math.pi) / 3 + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.handle_time_progression()
|
|
||||||
local time = minetest.get_timeofday()
|
|
||||||
if time < weather_mod.state.time.last_check then
|
|
||||||
weather_mod.state.time.day = weather_mod.state.time.day + 1
|
|
||||||
end
|
|
||||||
weather_mod.state.time.last_check = time
|
|
||||||
end
|
|
@ -1,58 +0,0 @@
|
|||||||
weather_mod.weekdays = {
|
|
||||||
"Monday",
|
|
||||||
"Tuesday",
|
|
||||||
"Wednesday",
|
|
||||||
"Thursday",
|
|
||||||
"Friday",
|
|
||||||
"Saturday",
|
|
||||||
"Sunday"
|
|
||||||
}
|
|
||||||
|
|
||||||
weather_mod.months = {
|
|
||||||
{ name = "January", days = 31 },
|
|
||||||
{ name = "February", days = 28 },
|
|
||||||
{ name = "March", days = 31 },
|
|
||||||
{ name = "April", days = 30 },
|
|
||||||
{ name = "May", days = 31 },
|
|
||||||
{ name = "June", days = 30 },
|
|
||||||
{ name = "July", days = 31 },
|
|
||||||
{ name = "August", days = 31 },
|
|
||||||
{ name = "September", days = 30 },
|
|
||||||
{ name = "October", days = 31 },
|
|
||||||
{ name = "November", days = 30 },
|
|
||||||
{ name = "December", days = 31 }
|
|
||||||
}
|
|
||||||
|
|
||||||
weather_mod.seasons = {
|
|
||||||
{ name = "spring" },
|
|
||||||
{ name = "summer" },
|
|
||||||
{ name = "autumn" },
|
|
||||||
{ name = "winter" }
|
|
||||||
}
|
|
||||||
|
|
||||||
function weather_mod.get_weekday()
|
|
||||||
return (weather_mod.state.time.day - 1) % 7 + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.get_month()
|
|
||||||
local day = (weather_mod.state.time.day - 1) % 365 + 1
|
|
||||||
local sum = 0
|
|
||||||
for i, month in ipairs(weather_mod.months) do
|
|
||||||
sum = sum + month.days
|
|
||||||
if sum >= day then
|
|
||||||
return i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.get_season()
|
|
||||||
local month = weather_mod.get_month()
|
|
||||||
return math.floor((month - 1) / 3 + 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.print_date()
|
|
||||||
local weekday = weather_mod.weekdays[weather_mod.get_weekday()]
|
|
||||||
local date = (weather_mod.state.time.day - 1) % 365 + 1
|
|
||||||
local month = weather_mod.months[weather_mod.get_month()].name
|
|
||||||
return weekday .. ", " .. date .. ". " .. month
|
|
||||||
end
|
|
@ -1,94 +0,0 @@
|
|||||||
minetest.register_privilege("weather", {
|
|
||||||
description = "Change the weather",
|
|
||||||
give_to_singleplayer = false
|
|
||||||
})
|
|
||||||
|
|
||||||
minetest.register_chatcommand("date", {
|
|
||||||
func = function(playername, param)
|
|
||||||
local date = weather_mod.print_date()
|
|
||||||
minetest.chat_send_player(playername, date)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Force a weather effect to override environment
|
|
||||||
minetest.register_chatcommand("set_weather", {
|
|
||||||
params = "<weather>",
|
|
||||||
description = "Set weather to a registered type of effect\
|
|
||||||
show all types when no parameters are given", -- full description
|
|
||||||
privs = {weather = true},
|
|
||||||
func = function(name, param)
|
|
||||||
if param == nil or param == "" or param == "?" then
|
|
||||||
local types="auto"
|
|
||||||
for i,_ in pairs(weather_mod.weathers) do
|
|
||||||
types=types..", "..i
|
|
||||||
end
|
|
||||||
minetest.chat_send_player(name, "avalible weather types: "..types)
|
|
||||||
else
|
|
||||||
if type(weather_mod.weathers[param]) == "nil" and param ~= "auto" then
|
|
||||||
minetest.chat_send_player(name, "This type of weather is not registered.\n"..
|
|
||||||
"To list all types of weather run the command without parameters.")
|
|
||||||
else
|
|
||||||
weather_mod.state.current_weather = param
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Set wind speed and direction
|
|
||||||
minetest.register_chatcommand("set_wind", {
|
|
||||||
params = "<wind>",
|
|
||||||
description = "Set wind to the given x,z direction", -- full description
|
|
||||||
privs = {weather = true},
|
|
||||||
func = function(name, param)
|
|
||||||
if param==nil or param=="" then
|
|
||||||
minetest.chat_send_player(name, "please provide two comma seperated numbers")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local x,z = string.match(param, "^([%d.-]+)[, ] *([%d.-]+)$")
|
|
||||||
x=tonumber(x)
|
|
||||||
z=tonumber(z)
|
|
||||||
if x and z then
|
|
||||||
weather_mod.state.wind = vector.new(x,0,z)
|
|
||||||
else
|
|
||||||
minetest.chat_send_player(name, param.." are not two comma seperated numbers")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Set base value of global heat level
|
|
||||||
minetest.register_chatcommand("set_heat", {
|
|
||||||
params = "<heat>",
|
|
||||||
description = "Set base value of global heat level", -- full description
|
|
||||||
privs = {weather = true},
|
|
||||||
func = function(name, param)
|
|
||||||
if param==nil or param=="" then
|
|
||||||
minetest.chat_send_player(name, "please provide a heat value")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
v = tonumber(param)
|
|
||||||
if v then
|
|
||||||
weather_mod.state.heat = v
|
|
||||||
else
|
|
||||||
minetest.chat_send_player(name, param.." is not a valid heat level")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Set base value of global humidity level
|
|
||||||
minetest.register_chatcommand("set_humidity", {
|
|
||||||
params = "<humidity>",
|
|
||||||
description = "Set base value of global humidity level", -- full description
|
|
||||||
privs = {weather = true},
|
|
||||||
func = function(name, param)
|
|
||||||
if param==nil or param=="" then
|
|
||||||
minetest.chat_send_player(name, "please provide a humidity value")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
v = tonumber(param)
|
|
||||||
if v then
|
|
||||||
weather_mod.state.humidity = v
|
|
||||||
else
|
|
||||||
minetest.chat_send_player(name, param.." is not a valid humidity level")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
})
|
|
@ -1,48 +1,16 @@
|
|||||||
local default_state = {
|
local default_state = {
|
||||||
heat = 1,
|
heat = 1,
|
||||||
humidity = 1,
|
humidity = 1,
|
||||||
wind = vector.new(0, 0, -0.25),
|
wind_x = 0.5,
|
||||||
current_weather = "auto",
|
wind_z = 0.5,
|
||||||
time = {
|
time_last_check = 0,
|
||||||
last_check = 0,
|
time_current_day = 1
|
||||||
day = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local function use_datastorage()
|
local state = minetest.get_mod_storage()
|
||||||
local state = datastorage.get(weather_mod.modname, "weather_state")
|
|
||||||
for key, val in pairs(default_state) do
|
if not state:contains("time_last_check") then
|
||||||
if type(state[key]) == "nil" then
|
state:from_table({ fields = default_state })
|
||||||
state[key] = val
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return state
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function use_filesystem()
|
return state
|
||||||
local file_name = minetest.get_worldpath() .. "/" .. weather_mod.modname
|
|
||||||
minetest.register_on_shutdown(function()
|
|
||||||
local file = io.open(file_name, "w")
|
|
||||||
file:write(minetest.serialize(weather_mod.state))
|
|
||||||
file:close()
|
|
||||||
end)
|
|
||||||
|
|
||||||
local file = io.open(file_name, "r")
|
|
||||||
if file ~= nil then
|
|
||||||
local storage = minetest.deserialize(file:read("*a"))
|
|
||||||
file:close()
|
|
||||||
if type(storage) == "table" then
|
|
||||||
return storage
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return default_state
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.get_storage()
|
|
||||||
local mod_datastorage = minetest.get_modpath("datastorage") ~= nil
|
|
||||||
if mod_datastorage then
|
|
||||||
return use_datastorage()
|
|
||||||
else
|
|
||||||
return use_filesystem()
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,80 +1,36 @@
|
|||||||
local mod_lightning = minetest.get_modpath("lightning")
|
local environment = {}
|
||||||
|
|
||||||
local LIGHTNING_CHANCE = 1000
|
local function get_heat_time()
|
||||||
if mod_lightning then
|
local time = minetest.get_timeofday()
|
||||||
lightning.auto = false
|
return climate_api.utility.normalized_cycle(time) * 0.4 + 0.3
|
||||||
end
|
end
|
||||||
|
|
||||||
function weather_mod.get_heat(pos)
|
local function get_heat_calendar()
|
||||||
local base = weather_mod.settings.heat;
|
-- European heat center in August instead of June
|
||||||
|
local day = climate_mod.state:get("time_current_day")
|
||||||
|
local progression = ((day + 61) % 365) / 365
|
||||||
|
return climate_api.utility.normalized_cycle(progression) * 0.4 + 0.3
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_heat_height(y)
|
||||||
|
return climate_api.utility.rangelim(-y / 15, -10, 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
function environment.get_heat(pos)
|
||||||
|
local base = climate_mod.settings.heat
|
||||||
local biome = minetest.get_heat(pos)
|
local biome = minetest.get_heat(pos)
|
||||||
local height = math.min(math.max(-pos.y / 15, -10), 10)
|
local height = get_heat_height(pos.y)
|
||||||
local time = weather_mod.get_time_heat()
|
local time = get_heat_time()
|
||||||
local date = weather_mod.get_calendar_heat()
|
local date = get_heat_calendar()
|
||||||
local random = weather_mod.state.heat;
|
local random = climate_mod.state:get_int("heat_random");
|
||||||
return (base + biome + height) * time * date + random
|
return (base + biome + height) * time * date
|
||||||
end
|
end
|
||||||
|
|
||||||
function weather_mod.get_humidity(pos)
|
function environment.get_humidity(pos)
|
||||||
local base = weather_mod.settings.humidity
|
local base = climate_mod.settings.humidity
|
||||||
local biome = minetest.get_humidity(pos)
|
local biome = minetest.get_humidity(pos)
|
||||||
local random = weather_mod.state.humidity;
|
local random = climate_mod.state:get_int("humidity_random");
|
||||||
return (base + biome) + random
|
return (base + biome)
|
||||||
end
|
end
|
||||||
|
|
||||||
function weather_mod.get_climate(pos)
|
return environment
|
||||||
local climate = {pos = pos}
|
|
||||||
climate.heat = weather_mod.get_heat(pos)
|
|
||||||
climate.humidity = weather_mod.get_humidity(pos)
|
|
||||||
climate.windspeed = vector.length(weather_mod.state.wind)
|
|
||||||
return climate
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_acceptable_weather_param(value, attr, config)
|
|
||||||
local min = config.conditions["min_" .. attr] or -math.huge
|
|
||||||
local max = config.conditions["max_" .. attr] or math.huge
|
|
||||||
return value > min and value <= max
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.get_effects(climate)
|
|
||||||
local forced_weather = weather_mod.state.current_weather
|
|
||||||
if type(forced_weather) ~= nil and forced_weather ~= "auto" then
|
|
||||||
return { forced_weather }
|
|
||||||
end
|
|
||||||
local params = {}
|
|
||||||
params.heat = climate.heat
|
|
||||||
params.humidity = climate.humidity
|
|
||||||
params.windspeed = vector.length(weather_mod.state.wind)
|
|
||||||
params.height = climate.pos.y
|
|
||||||
|
|
||||||
local effects = {}
|
|
||||||
local attributes = { "heat", "humidity", "windspeed", "height" }
|
|
||||||
for name, effect in pairs(weather_mod.weathers) do
|
|
||||||
if type(effect.config.conditions) == "nil" then
|
|
||||||
table.insert(effects, name)
|
|
||||||
goto continue
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, attr in ipairs(attributes) do
|
|
||||||
if not is_acceptable_weather_param(params[attr], attr, effect.config) then
|
|
||||||
goto continue
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.insert(effects, name)
|
|
||||||
::continue::
|
|
||||||
end
|
|
||||||
return effects
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.handle_events(player, flags)
|
|
||||||
local ppos = player:get_pos()
|
|
||||||
if mod_lightning and weather_mod.settings.lightning and type(flags["lightning"]) ~= "nil" then
|
|
||||||
local random = rng:next(1, LIGHTNING_CHANCE)
|
|
||||||
if random == 1 then
|
|
||||||
lightning.strike(ppos)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if type(flags["damage"]) ~= "nil" then
|
|
||||||
weather_mod.damage_player(player, 1)
|
|
||||||
end
|
|
||||||
end
|
|
141
lib/main.lua
@ -1,124 +1,27 @@
|
|||||||
local GSCYCLE = 0.05
|
local GSCYCLE = 0
|
||||||
local RECALCCYCLE = 0.2
|
local WORLD_CYCLE = 0
|
||||||
|
|
||||||
weather_mod.weathers = {}
|
local gs_timer = 0
|
||||||
function weather_mod.register_effect(name, config, override)
|
local world_timer = 0
|
||||||
-- TODO: check and sanitize
|
|
||||||
weather_mod.weathers[name] = {
|
|
||||||
config = config,
|
|
||||||
override = override,
|
|
||||||
sound_handles = {},
|
|
||||||
sound_volumes = {}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- from https://stackoverflow.com/a/29133654
|
|
||||||
local function merge(a, b)
|
|
||||||
if type(a) == 'table' and type(b) == 'table' then
|
|
||||||
for k,v in pairs(b) do if type(v)=='table' and type(a[k] or false)=='table' then merge(a[k],v) else a[k]=v end end
|
|
||||||
end
|
|
||||||
return a
|
|
||||||
end
|
|
||||||
|
|
||||||
local function build_effect_config(weather, climate)
|
|
||||||
local config = weather.config
|
|
||||||
local override = weather.override
|
|
||||||
if type(override) == "nil" then
|
|
||||||
return config
|
|
||||||
end
|
|
||||||
local dynamic_config = override(climate)
|
|
||||||
return merge(config, dynamic_config)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_texture(particles)
|
|
||||||
if type(particles.textures) == "nil" or next(particles.textures) == nil then
|
|
||||||
return particles.texture
|
|
||||||
end
|
|
||||||
return particles.textures[math.random(#particles.textures)]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function spawn_particles(player, particles, wind)
|
|
||||||
local ppos = player:getpos()
|
|
||||||
local wind_pos = vector.multiply(weather_mod.state.wind,-1)
|
|
||||||
local wind_speed = vector.length(weather_mod.state.wind)
|
|
||||||
|
|
||||||
local texture = get_texture(particles)
|
|
||||||
|
|
||||||
local minp = vector.add(vector.add(ppos, particles.min_pos),wind_pos)
|
|
||||||
local maxp = vector.add(vector.add(ppos, particles.max_pos),wind_pos)
|
|
||||||
|
|
||||||
local vel = vector.new({
|
|
||||||
x=weather_mod.state.wind.x,
|
|
||||||
y=-particles.falling_speed,
|
|
||||||
z=weather_mod.state.wind.z
|
|
||||||
})
|
|
||||||
local acc = vector.new({x=0, y=0, z=0})
|
|
||||||
|
|
||||||
local exp = particles.exptime
|
|
||||||
local vertical = math.abs(vector.normalize(vel).y) >= 0.6
|
|
||||||
|
|
||||||
minetest.add_particlespawner({
|
|
||||||
amount=particles.amount,
|
|
||||||
time=0.5,
|
|
||||||
minpos=minp,
|
|
||||||
maxpos=maxp,
|
|
||||||
minvel=vel,
|
|
||||||
maxvel=vel,
|
|
||||||
minacc=acc,
|
|
||||||
maxacc=acc,
|
|
||||||
minexptime=exp,
|
|
||||||
maxexptime=exp,
|
|
||||||
minsize=particles.size,
|
|
||||||
maxsize=particles.size,
|
|
||||||
collisiondetection=true,
|
|
||||||
collision_removal=true,
|
|
||||||
vertical=vertical,
|
|
||||||
texture=texture,
|
|
||||||
player=player:get_player_name()
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
local function handle_weather_effects(player)
|
|
||||||
local ppos = player:getpos()
|
|
||||||
local climate = weather_mod.get_climate(ppos)
|
|
||||||
local active_effects = weather_mod.get_effects(climate)
|
|
||||||
local environment_flags = {}
|
|
||||||
local sounds = {}
|
|
||||||
|
|
||||||
for _, effect in ipairs(active_effects) do
|
|
||||||
local weather = weather_mod.weathers[effect]
|
|
||||||
local config = build_effect_config(weather, climate)
|
|
||||||
|
|
||||||
local outdoors = weather_mod.is_outdoors(player)
|
|
||||||
if type(config.particles) ~= "nil" and outdoors then
|
|
||||||
spawn_particles(player, config.particles, weather_mod.state.wind)
|
|
||||||
end
|
|
||||||
if type(config.sound) ~= "nil" and outdoors then
|
|
||||||
sounds[effect] = config.sound
|
|
||||||
end
|
|
||||||
if type(config.environment) ~= "nil" and outdoors then
|
|
||||||
for flag, value in pairs(config.environment) do
|
|
||||||
if value ~= false then
|
|
||||||
environment_flags[flag] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
weather_mod.handle_sounds(player, sounds)
|
|
||||||
weather_mod.handle_events(player, environment_flags)
|
|
||||||
end
|
|
||||||
|
|
||||||
local timer = 0
|
|
||||||
minetest.register_globalstep(function(dtime)
|
minetest.register_globalstep(function(dtime)
|
||||||
timer = timer + dtime
|
gs_timer = gs_timer + dtime
|
||||||
if timer < GSCYCLE then return end
|
world_timer = world_timer + dtime
|
||||||
for _, player in ipairs(minetest.get_connected_players()) do
|
|
||||||
handle_weather_effects(player)
|
if gs_timer + dtime < GSCYCLE then return else gs_timer = 0 end
|
||||||
if timer >= RECALCCYCLE then
|
|
||||||
weather_mod.set_clouds(player)
|
if world_timer >= WORLD_CYCLE then
|
||||||
weather_mod.set_headwind(player)
|
local noise_timer = climate_mod.state:get_float("noise_timer") + world_timer
|
||||||
weather_mod.handle_time_progression()
|
world_timer = 0
|
||||||
|
climate_mod.state:set_float("noise_timer", noise_timer)
|
||||||
|
climate_mod.world.update_status(noise_timer)
|
||||||
|
end
|
||||||
|
|
||||||
|
local effects = climate_mod.trigger.get_active_effects()
|
||||||
|
|
||||||
|
for name, effect in pairs(climate_mod.effects) do
|
||||||
|
if climate_mod.cycles[name].timespan < climate_mod.cycles[name].timer + dtime then
|
||||||
|
climate_mod.cycles[name].timer = 0
|
||||||
|
climate_mod.trigger.call_handlers(name, effects[name])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
timer = 0
|
|
||||||
end)
|
end)
|
@ -1,92 +0,0 @@
|
|||||||
local mod_player_monoids = minetest.get_modpath("player_monoids") ~= nil
|
|
||||||
local mod_playerphysics = minetest.get_modpath("playerphysics") ~= nil
|
|
||||||
|
|
||||||
function weather_mod.add_physics(player, effect, value)
|
|
||||||
local id = weather_mod.modname .. ":" .. effect
|
|
||||||
if mod_player_monoids then
|
|
||||||
player_monoids[effect]:add_change(player, value, id)
|
|
||||||
elseif mod_playerphysics then
|
|
||||||
playerphysics.add_physics_factor(player, effect, id, value)
|
|
||||||
else
|
|
||||||
local override = {}
|
|
||||||
override[effect] = value
|
|
||||||
player:set_physics_override(override)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.remove_physics(player, effect)
|
|
||||||
local id = weather_mod.modname .. ":" .. effect
|
|
||||||
if mod_player_monoids then
|
|
||||||
player_monoids[effect]:del_change(player, id)
|
|
||||||
elseif mod_playerphysics then
|
|
||||||
playerphysics.remove_physics_factor(player, effect, id)
|
|
||||||
else
|
|
||||||
local override = {}
|
|
||||||
override[effect] = 1
|
|
||||||
player:set_physics_override(override)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.is_outdoors(player)
|
|
||||||
return minetest.get_node_light(player:getpos(), 0.5) == 15
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.set_clouds(player)
|
|
||||||
if not weather_mod.settings.skybox then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local ppos = player:get_pos()
|
|
||||||
local humidity = weather_mod.get_humidity(ppos) / 100
|
|
||||||
local clouds = {}
|
|
||||||
clouds.speed = vector.multiply(weather_mod.state.wind, 2)
|
|
||||||
clouds.color = "#fff0f0c5"
|
|
||||||
clouds.density = math.max(math.min(humidity, 0.8), 0.1)
|
|
||||||
player:set_clouds(clouds)
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.play_sound(player, effect)
|
|
||||||
local playername = player:get_player_name()
|
|
||||||
if not effect.sound_handles[playername] then
|
|
||||||
local handle = minetest.sound_play(effect.config.sound, {
|
|
||||||
to_player = playername,
|
|
||||||
loop = true
|
|
||||||
})
|
|
||||||
if handle then
|
|
||||||
effect.sound_handles[playername] = handle
|
|
||||||
effect.sound_volumes[playername] = effect.config.sound.gain or 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.stop_sound(player, effect)
|
|
||||||
local playername = player:get_player_name()
|
|
||||||
if effect.sound_handles[playername] then
|
|
||||||
minetest.sound_stop(effect.sound_handles[playername])
|
|
||||||
effect.sound_handles[playername] = nil
|
|
||||||
effect.sound_volumes[playername] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.handle_sounds(player, sounds)
|
|
||||||
local playername = player:get_player_name()
|
|
||||||
for name, effect in pairs(weather_mod.weathers) do
|
|
||||||
if type(effect.sound_handles[playername]) == "nil" and type(sounds[name]) ~= "nil" then
|
|
||||||
weather_mod.play_sound(player, effect)
|
|
||||||
elseif type(effect.sound_handles[playername]) ~= "nil" and type(sounds[name]) == "nil" then
|
|
||||||
weather_mod.stop_sound(player, effect)
|
|
||||||
elseif type(effect.sound_handles[playername]) ~= "nil" and type(sounds[name]) ~= "nil" then
|
|
||||||
local volume = sounds[name].gain or 1
|
|
||||||
if effect.sound_volumes[playername] ~= volume then
|
|
||||||
minetest.sound_fade(effect.sound_handles[playername], 1, volume)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function weather_mod.damage_player(player, amount, reason)
|
|
||||||
if not weather_mod.settings.damage then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local hp = player:get_hp()
|
|
||||||
player:set_hp(hp - amount, reason)
|
|
||||||
end
|
|
89
lib/trigger.lua
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
local trigger = {}
|
||||||
|
|
||||||
|
local function get_player_environment(player)
|
||||||
|
local ppos = player:get_pos()
|
||||||
|
local wind_x = climate_mod.state:get_int("wind_x")
|
||||||
|
local wind_z = climate_mod.state:get_int("wind_z")
|
||||||
|
|
||||||
|
local env = {}
|
||||||
|
env.player = player
|
||||||
|
env.pos = pos
|
||||||
|
env.wind = vector.new(wind_x, 0, wind_z)
|
||||||
|
env.windspeed = vector.length(env.wind)
|
||||||
|
env.heat = climate_api.environment.get_heat(ppos)
|
||||||
|
env.humidity = climate_api.environment.get_humidity(ppos)
|
||||||
|
env.time = minetest.get_timeofday()
|
||||||
|
env.date = climate_mod.state:get_int("time_current_day")
|
||||||
|
return env
|
||||||
|
end
|
||||||
|
|
||||||
|
local function test_condition(condition, env, goal)
|
||||||
|
local value = env[condition:sub(5)]
|
||||||
|
if condition:sub(1, 4) == "min_" then
|
||||||
|
for _, player in ipairs(minetest.get_connected_players()) do
|
||||||
|
minetest.chat_send_player(player:get_player_name(), dump2(value, goal))
|
||||||
|
end
|
||||||
|
return type(value) ~= "nil" and goal <= value
|
||||||
|
elseif condition:sub(1, 4) == "max_" then
|
||||||
|
return type(value) ~= "nil" and goal > value
|
||||||
|
else
|
||||||
|
Minetest.log("warning", "[Climate API] Invalid effect condition")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_weather_active(player, weather_config, env)
|
||||||
|
if type(weather_config.conditions) == "function" then
|
||||||
|
return weather_config.conditions(env)
|
||||||
|
end
|
||||||
|
for condition, goal in pairs(weather_config.conditions) do
|
||||||
|
if not test_condition(condition, env, goal) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_weather_effects(player, weather_config, env)
|
||||||
|
if type(weather_config.effects) == "function" then
|
||||||
|
return weather_config.effects(env)
|
||||||
|
end
|
||||||
|
return weather_config.effects
|
||||||
|
end
|
||||||
|
|
||||||
|
function trigger.get_active_effects()
|
||||||
|
local environments = {}
|
||||||
|
for _, player in ipairs(minetest.get_connected_players()) do
|
||||||
|
environments[player:get_player_name()] = get_player_environment(player)
|
||||||
|
end
|
||||||
|
|
||||||
|
local effects = {}
|
||||||
|
for wname, wconfig in pairs(climate_mod.weathers) do
|
||||||
|
for _, player in ipairs(minetest.get_connected_players()) do
|
||||||
|
local pname = player:get_player_name()
|
||||||
|
local env = environments[pname]
|
||||||
|
if is_weather_active(player, wconfig, env) then
|
||||||
|
local player_effects = get_weather_effects(player, wconfig, env)
|
||||||
|
for effect, value in pairs(player_effects) do
|
||||||
|
if type(effects[effect]) == "nil" then
|
||||||
|
effects[effect] = {}
|
||||||
|
end
|
||||||
|
if type(effects[effect][pname]) == "nil" then
|
||||||
|
effects[effect][pname] = {}
|
||||||
|
end
|
||||||
|
effects[effect][pname][wname] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return effects
|
||||||
|
end
|
||||||
|
|
||||||
|
function trigger.call_handlers(name, effect)
|
||||||
|
if type(effect) == "nil" then return end
|
||||||
|
for _, handler in ipairs(climate_mod.effects[name]["tick"]) do
|
||||||
|
handler(effect)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return trigger
|
12
lib/wind.lua
@ -1,12 +0,0 @@
|
|||||||
function weather_mod.set_headwind(player)
|
|
||||||
local movement = vector.normalize(player:get_player_velocity())
|
|
||||||
local product = vector.dot(movement, weather_mod.state.wind)
|
|
||||||
-- logistic function, scales between 0 and 2
|
|
||||||
-- see https://en.wikipedia.org/wiki/Logistic_function
|
|
||||||
local L = 1.6 -- maximum value
|
|
||||||
local k = 0.15 -- growth rate
|
|
||||||
local z = 0.8 -- midpoint
|
|
||||||
local o = 0.1 -- y offset
|
|
||||||
local factor = L / (1 + math.exp(-k * (product - z))) + o
|
|
||||||
weather_mod.add_physics(player, "speed", factor)
|
|
||||||
end
|
|
92
lib/world.lua
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
local world = {}
|
||||||
|
|
||||||
|
local WIND_SPREAD = 600
|
||||||
|
local WIND_SCALE = 3
|
||||||
|
local HEAT_SPREAD = 2400
|
||||||
|
local HEAT_SCALE = 0.2
|
||||||
|
local HUMIDITY_SPREAD = 1200
|
||||||
|
local HUMIDITY_SCALE = 0.18
|
||||||
|
|
||||||
|
local nobj_wind_x
|
||||||
|
local nobj_wind_z
|
||||||
|
local nobj_heat
|
||||||
|
local nobj_humidity
|
||||||
|
|
||||||
|
local pn_wind_speed_x = {
|
||||||
|
offset = 0,
|
||||||
|
scale = WIND_SCALE,
|
||||||
|
spread = {x = WIND_SPREAD, y = WIND_SPREAD, z = WIND_SPREAD},
|
||||||
|
seed = 31441,
|
||||||
|
octaves = 2,
|
||||||
|
persist = 0.5,
|
||||||
|
lacunarity = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
local pn_wind_speed_z = {
|
||||||
|
offset = 0,
|
||||||
|
scale = WIND_SCALE,
|
||||||
|
spread = {x = WIND_SPREAD, y = WIND_SPREAD, z = WIND_SPREAD},
|
||||||
|
seed = 938402,
|
||||||
|
octaves = 2,
|
||||||
|
persist = 0.5,
|
||||||
|
lacunarity = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
local pn_heat = {
|
||||||
|
offset = 0,
|
||||||
|
scale = HEAT_SCALE,
|
||||||
|
spread = {x = HEAT_SPREAD, y = HEAT_SPREAD, z = HEAT_SPREAD},
|
||||||
|
seed = 24,
|
||||||
|
octaves = 2,
|
||||||
|
persist = 0.5,
|
||||||
|
lacunarity = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
local pn_humidity = {
|
||||||
|
offset = 0,
|
||||||
|
scale = HUMIDITY_SCALE,
|
||||||
|
spread = {x = HUMIDITY_SPREAD, y = HUMIDITY_SPREAD, z = HUMIDITY_SPREAD},
|
||||||
|
seed = 8374061,
|
||||||
|
octaves = 3,
|
||||||
|
persist = 0.5,
|
||||||
|
lacunarity = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
local function update_wind(timer)
|
||||||
|
nobj_wind_x = nobj_wind_x or minetest.get_perlin(pn_wind_speed_x)
|
||||||
|
nobj_wind_z = nobj_wind_z or minetest.get_perlin(pn_wind_speed_z)
|
||||||
|
local n_wind_x = nobj_wind_x:get_2d({x = timer, y = 0})
|
||||||
|
local n_wind_z = nobj_wind_z:get_2d({x = timer, y = 0})
|
||||||
|
climate_mod.state:set_int("wind_x", n_wind_x)
|
||||||
|
climate_mod.state:set_int("wind_z", n_wind_z)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_heat(timer)
|
||||||
|
nobj_heat = nobj_heat or minetest.get_perlin(pn_heat)
|
||||||
|
local n_heat = nobj_heat:get_2d({x = timer, y = 0})
|
||||||
|
climate_mod.state:set_int("heat_random", n_heat)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_humidity(timer)
|
||||||
|
nobj_humidity = nobj_humidity or minetest.get_perlin(pn_humidity)
|
||||||
|
local n_humidity = nobj_humidity:get_2d({x = timer, y = 0})
|
||||||
|
climate_mod.state:set_int("humidity_random", n_humidity)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_date()
|
||||||
|
local time = minetest.get_timeofday()
|
||||||
|
if time < climate_mod.state:get_int("time_last_check") then
|
||||||
|
local day = climate_mod.state:get_int("time_current_day")
|
||||||
|
climate_mod.state:set_int("time_current_day ", day + 1)
|
||||||
|
end
|
||||||
|
climate_mod.state:set_int("time_last_check", time)
|
||||||
|
end
|
||||||
|
|
||||||
|
function world.update_status(timer)
|
||||||
|
update_date()
|
||||||
|
update_wind(timer)
|
||||||
|
update_heat(timer)
|
||||||
|
update_humidity(timer)
|
||||||
|
end
|
||||||
|
|
||||||
|
return world
|
9
mod.conf
@ -1,4 +1,9 @@
|
|||||||
name = believable_weather
|
name = climate_api
|
||||||
title = Believable Weather
|
title = Climate API
|
||||||
author = TestificateMods
|
author = TestificateMods
|
||||||
release = 1
|
release = 1
|
||||||
|
optional_depends = skylayer, player_monoids, playerphysics
|
||||||
|
description = """
|
||||||
|
The ultimate weather mod with support not only for rain, snow, and hail,
|
||||||
|
but also seasons, dynamic puddles and snow layers, wind, regrowing fruit and much more.
|
||||||
|
"""
|
@ -1,17 +1,8 @@
|
|||||||
believable_weather_damage (Storms and hail cause damage) bool true
|
climate_api_particles (Show particle effects) bool true
|
||||||
believable_weather_particles (Show particle effects) bool true
|
climate_api_skybox (Allow weather effects to modify the skybox) bool true
|
||||||
believable_weather_leaves (Leave color changes in autumn) bool true
|
climate_api_sound (Allow weather presets to play ambient sounds) bool true
|
||||||
believable_weather_snow_layers (Place snow layers on ground) bool true
|
climate_api_clouds (Override clouds to represent current humidity) bool true
|
||||||
believable_weather_puddles (Place rain puddles on ground) bool true
|
climate_api_wind (Allow wind to angle rainfall) bool true
|
||||||
believable_weather_skybox (Darken sky during rain) bool true
|
climate_api_seasons (Change global temperature based on an annual cycle) bool true
|
||||||
believable_weather_raycasting (Use more accurate indoors check) bool false
|
climate_api_heat_base (Global base temperature) float 0
|
||||||
believable_weather_wind (Allow wind to angle rainfall) bool true
|
climate_api_humidity_base (Global base humidity) float 0
|
||||||
believable_weather_wind_slow (Allow wind to impact movement speed) bool true
|
|
||||||
believable_weather_flowers (Flowers will respawn in spring and die in winter) bool true
|
|
||||||
believable_weather_fruit (Apples and other fruits will regrow) bool true
|
|
||||||
believable_weather_soil (Soil turns wet during rain) bool true
|
|
||||||
believable_weather_seasons (Use seasons instead of permanent summer) bool true
|
|
||||||
believable_weather_base_heat (Base temperature) float 0
|
|
||||||
believable_weather_base_humidity (Base humidity) float 0
|
|
||||||
believable_weather_max_height (Maximum height of weather effects) int 120
|
|
||||||
believable_weather_min_height (Minimum height of weather effects) int -50
|
|
Before Width: | Height: | Size: 121 B |
Before Width: | Height: | Size: 134 B |
Before Width: | Height: | Size: 136 B |
Before Width: | Height: | Size: 204 B |
Before Width: | Height: | Size: 311 B |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 556 B |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 148 B |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 647 B |
Before Width: | Height: | Size: 104 B |
Before Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 104 B |
Before Width: | Height: | Size: 102 B |
Before Width: | Height: | Size: 104 B |
Before Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 104 B |
Before Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 105 B |
@ -1,38 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":hail"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.environment = {
|
|
||||||
spawn_puddles = true,
|
|
||||||
lightning = true,
|
|
||||||
damage = true
|
|
||||||
}
|
|
||||||
|
|
||||||
config.sound = {
|
|
||||||
name = "weather_hail",
|
|
||||||
gain = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
config.particles = {
|
|
||||||
min_pos = {x=-9, y=7, z=-9},
|
|
||||||
max_pos = {x= 9, y=7, z= 9},
|
|
||||||
falling_speed=15,
|
|
||||||
amount=5,
|
|
||||||
exptime=0.8,
|
|
||||||
size=1,
|
|
||||||
textures = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i = 1,5,1 do
|
|
||||||
config.particles.textures[i] = "weather_hail" .. i .. ".png"
|
|
||||||
end
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
max_heat = 45,
|
|
||||||
min_humidity = 65,
|
|
||||||
min_windspeed = 2.5
|
|
||||||
}
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config)
|
|
@ -1,26 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":pollen"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.environment = {}
|
|
||||||
|
|
||||||
config.particles = {
|
|
||||||
min_pos = {x=-12, y=-4, z=-12},
|
|
||||||
max_pos = {x= 12, y= 1, z= 12},
|
|
||||||
falling_speed=-0.1,
|
|
||||||
amount=2,
|
|
||||||
exptime=5,
|
|
||||||
size=1,
|
|
||||||
texture="weather_pollen.png"
|
|
||||||
}
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
min_heat = 40,
|
|
||||||
min_humidity = 30,
|
|
||||||
max_humidity = 40,
|
|
||||||
max_windspeed = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config)
|
|
@ -1,48 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":rain"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.environment = {
|
|
||||||
spawn_puddles = true,
|
|
||||||
wetten_farmland = true
|
|
||||||
}
|
|
||||||
|
|
||||||
config.sound = {
|
|
||||||
name = "weather_rain",
|
|
||||||
gain = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
config.particles = {
|
|
||||||
min_pos = {x=-9, y=7, z=-9},
|
|
||||||
max_pos = {x= 9, y=7, z= 9},
|
|
||||||
falling_speed=10,
|
|
||||||
amount=40,
|
|
||||||
exptime=0.8,
|
|
||||||
size=1,
|
|
||||||
texture = "weather_raindrop.png"
|
|
||||||
}
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
min_heat = 30,
|
|
||||||
min_humidity = 40,
|
|
||||||
max_humidity = 60
|
|
||||||
}
|
|
||||||
|
|
||||||
local function override(params)
|
|
||||||
local avg_humidity = 40
|
|
||||||
local intensity = params.humidity / avg_humidity
|
|
||||||
local dynamic_config = {
|
|
||||||
sound = {
|
|
||||||
gain = math.min(intensity, 1.2)
|
|
||||||
},
|
|
||||||
particles = {
|
|
||||||
amount = 20 * math.min(intensity, 1.5),
|
|
||||||
falling_speed = 10 / math.min(intensity, 1.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dynamic_config
|
|
||||||
end
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config, override)
|
|
@ -1,33 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":rain_heavy"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.environment = {
|
|
||||||
spawn_puddles = true,
|
|
||||||
wetten_farmland = true,
|
|
||||||
lightning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
config.sound = {
|
|
||||||
name = "weather_rain",
|
|
||||||
gain = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
config.particles = {
|
|
||||||
min_pos = {x=-9, y=7, z=-9},
|
|
||||||
max_pos = {x= 9, y=7, z= 9},
|
|
||||||
falling_speed=10,
|
|
||||||
amount=20,
|
|
||||||
exptime=0.8,
|
|
||||||
size=25,
|
|
||||||
texture="weather_rain.png"
|
|
||||||
}
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
min_heat = 30,
|
|
||||||
min_humidity = 60
|
|
||||||
}
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config)
|
|
@ -1,27 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":sandstorm"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.environment = {
|
|
||||||
damage = true
|
|
||||||
}
|
|
||||||
|
|
||||||
config.particles = {
|
|
||||||
min_pos = {x=-9, y=-5, z=-9},
|
|
||||||
max_pos = {x= 9, y= 5, z= 9},
|
|
||||||
falling_speed=1,
|
|
||||||
amount=40,
|
|
||||||
exptime=0.8,
|
|
||||||
size=15,
|
|
||||||
texture="weather_sand.png"
|
|
||||||
}
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
min_heat = 50,
|
|
||||||
max_humidity = 25,
|
|
||||||
min_windspeed = 6
|
|
||||||
}
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config)
|
|
@ -1,46 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":snow"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.environment = {
|
|
||||||
spawn_snow = true
|
|
||||||
}
|
|
||||||
|
|
||||||
config.particles = {
|
|
||||||
min_pos = {x=-20, y= 3, z=-20},
|
|
||||||
max_pos = {x= 20, y=12, z= 20},
|
|
||||||
falling_speed=1,
|
|
||||||
amount=40,
|
|
||||||
exptime=8,
|
|
||||||
size=1,
|
|
||||||
textures = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i = 1,12,1 do
|
|
||||||
config.particles.textures[i] = "weather_snowflake" .. i .. ".png"
|
|
||||||
end
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
max_heat = 40,
|
|
||||||
min_humidity = 40,
|
|
||||||
max_humidity = 55
|
|
||||||
}
|
|
||||||
|
|
||||||
local function override(params)
|
|
||||||
local avg_humidity = 40
|
|
||||||
local intensity = params.humidity / avg_humidity
|
|
||||||
local dynamic_config = {
|
|
||||||
sound = {
|
|
||||||
gain = math.min(intensity, 1.2)
|
|
||||||
},
|
|
||||||
particles = {
|
|
||||||
amount = 50 * math.min(intensity, 1.5),
|
|
||||||
falling_speed = 1 / math.min(intensity, 1.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dynamic_config
|
|
||||||
end
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config, override)
|
|
@ -1,41 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":snow_heavy"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.environment = {
|
|
||||||
spawn_snow = true
|
|
||||||
}
|
|
||||||
|
|
||||||
config.particles = {
|
|
||||||
min_pos = {x=-12, y= 5, z=-12},
|
|
||||||
max_pos = {x= 12, y=9, z= 12},
|
|
||||||
falling_speed=1,
|
|
||||||
amount=1,
|
|
||||||
exptime=8,
|
|
||||||
size=12,
|
|
||||||
texture="weather_snow.png"
|
|
||||||
}
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
max_heat = 40,
|
|
||||||
min_humidity = 55
|
|
||||||
}
|
|
||||||
|
|
||||||
local function override(params)
|
|
||||||
local avg_humidity = 55
|
|
||||||
local intensity = params.humidity / avg_humidity
|
|
||||||
local dynamic_config = {
|
|
||||||
sound = {
|
|
||||||
gain = math.min(intensity, 1.2)
|
|
||||||
},
|
|
||||||
particles = {
|
|
||||||
amount = 50 * math.min(intensity, 1.5),
|
|
||||||
falling_speed = 1 / math.min(intensity, 1.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dynamic_config
|
|
||||||
end
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config, override)
|
|
@ -1,27 +0,0 @@
|
|||||||
local name = weather_mod.modname .. ":storm"
|
|
||||||
|
|
||||||
local config = {}
|
|
||||||
|
|
||||||
config.sound = {
|
|
||||||
name = "weather_wind",
|
|
||||||
gain = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
config.conditions = {
|
|
||||||
min_height = weather_mod.settings.min_height,
|
|
||||||
max_height = weather_mod.settings.max_height,
|
|
||||||
min_windspeed = 3.5
|
|
||||||
}
|
|
||||||
|
|
||||||
local function override(params)
|
|
||||||
local avg_windspeed = 5
|
|
||||||
local intensity = params.windspeed / avg_windspeed
|
|
||||||
local dynamic_config = {
|
|
||||||
sound = {
|
|
||||||
gain = math.min(intensity, 1.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dynamic_config
|
|
||||||
end
|
|
||||||
|
|
||||||
weather_mod.register_effect(name, config, override)
|
|