weather/init.lua

1290 lines
35 KiB
Lua

-- Weather mod by shivajiva for minetest
weather = {}
weather.registered_weather = {}
weather.registered_biomes = {}
weather.status = {}
--weather.types = {}
weather.wind = {}
-- Constants
local MP = minetest.get_modpath(minetest.get_current_modname())
local YMIN = 1 -- effects floor (set to sealevel)
local YMAX = 120 -- effects ceiling (set to cloud height)
local GCYCLE = 60 -- Generator timer interval (seconds)
local PCYCLE = 0.5 -- Player loop interval (seconds)
local RAINGAIN = 0.2 -- Rain sound volume
local CSMAX = 5 -- Cloud speed max nodes/s
local NISVAL = 0 -- plain skybox RGB night value
local DASVAL = 175 -- plain skybox RGB day value
local C_WHITE = {r=240,g=240,b=255,a=229} -- normal colour
local SKY_BLUE = {r=139, g=185, b=249, a=255}
local SKY_GREY = {r=175, g=175, b=191, a=255}
-- local SKY_BLACK = {r=0, g=0, b=16, a=255}
local SKY_WHITE = {r=255, g=255, b=255, a=255}
local STEPS = 13 -- steps for rgb_lerp transitions
local CDEF = {
color = C_WHITE,
density = 0,
height = 120,
thickness = 0
} -- cloud reset
-- variables
local audio_id = {} -- audio handles table
local biome_idx = {} -- biomes current weather index
local c = {} -- cloud transition state
local d = {} -- density transition state
local force = 0 -- trigger
local h = {} -- cloud height transition state
local hud_id = {} -- hud overlay table
local lightning = false -- trigger flag
local l_temp = {} -- temp cache for player skybox state during lightening
local s = {} -- temp transition state for skybox (flag)
local sky = {} -- temp cache for player skybox state
local t = {} -- cloud thickness transition state
-- ### INIT ###
weather.wind.x = 0
weather.wind.z = -2
weather.wind.i = 5
-- ### DEVELOPMENT ###
local dev = {} -- hud debugging
local mod_debug = false
-- REGISTRATION API
-- weather type
weather.register_weather = function(name, def)
weather.registered_weather[name] = def
end
-- biome weather sequence
weather.register_biome = function(name, def)
weather.registered_biomes[name] = def
biome_idx[name] = 1 -- init current index
end
-- register default types
dofile(MP .. "/register.lua")
weather.get_player = function(name)
if type(name) == "string" and weather.status[name] then
return {
biome = weather.status[name].working_biome,
weather = weather.status[name].w,
outside = weather.status[name].outside
}
end
end
-- HELPER FUNCTIONS --
local function round(num)
if num == nil then return end
local mult = 1000
return math.floor(num * mult + 0.5) / mult
end
local function rgba_lerp(st, f, p)
-- linear rgba interpolation
-- st = table; start point
-- f = table; end point
-- p = float; point along the interpolated line
if not st or not f or not p then return end
local r = {}
r.r = math.floor(st.r + (f.r - st.r) * p)
r.g = math.floor(st.g + (f.g - st.g) * p)
r.b = math.floor(st.b + (f.b - st.b) * p)
r.a = math.floor(st.a + (f.a - st.a) * p)
return r
end
local function rgba_diff(a, b)
if a.r ~= b.r or a.g ~= b.g or a.b ~= b.b or a.a ~= b.a then
return true
end
return false
end
local function lerp(st, f, p)
-- linear interpolation
return (st + (f - st) * p)
end
local function particle_pos(pos, velocity)
-- Predict particle position as behind the cycle interval.
local ppos = vector.add(pos, vector.multiply(velocity, 0.85))
local pposx = math.floor(ppos.x)
-- add 2 nodes for precipitation when swimming
local pposy = math.floor(ppos.y) + 2
local pposz = math.floor(ppos.z)
-- particle position
return {x = pposx, y = pposy, z = pposz}
end
-- GENERATORS (timers) --
math.randomseed(os.time())
minetest.register_globalstep(function(dtime)
force = force + math.random(-1,1)
-- assert min/max
if force > 100 then
force = 100
elseif force < 0 then
force = 0
end
end)
local function wind_generator()
local direction = {
[1] = {x = 0, z = 1, hrf = "N"},
[2] = {x = 1, z = 1, hrf = "NE"},
[3] = {x = 1, z = 0, hrf = "E"},
[4] = {x = 1, z = -1, hrf = "SE"},
[5] = {x = 0, z = -1, hrf = "S"},
[6] = {x = -1, z = -1, hrf = "SW"},
[7] = {x = -1, z = 0, hrf = "W"},
[8] = {x = -1, z = 1, hrf = "NW"}
}
local x, z
local sts = math.random(-1,1)
local i = weather.wind.i
if sts > 0 then
-- increase speed
local current = direction[i]
if (weather.wind.x < CSMAX and weather.wind.x > -CSMAX) and
(weather.wind.z < CSMAX and weather.wind.z > -CSMAX) then
x = weather.wind.x + current.x
z = weather.wind.z + current.z
dev.sts = "plus"
end
elseif sts < 0 then
-- decrease speed
local current = direction[i]
if weather.wind.x > -CSMAX and weather.wind.z > -CSMAX then
x = weather.wind.x - current.x
z = weather.wind.z - current.z
dev.sts = "minus"
end
else
-- change direction
local r = math.random(0,1)
local mx, mz = 1,1
local index
if weather.wind.x < -1 then
mx = weather.wind.x * -1
elseif weather.wind.x > 1 then
mx = weather.wind.x
end
if weather.wind.z < -1 then
mz = weather.wind.z * -1
elseif weather.wind.z > 1 then
mz = weather.wind.z
end
if r > 0 then
-- increment current index
index = weather.wind.i + 1
if index > #direction then index = 1 end -- loop
weather.wind.i = index
x = direction[index].x * mx
z = direction[index].z * mz
dev.dts = direction[index].hrf
dev.sts = ""
else
-- decrement current index
index = weather.wind.i - 1
if index < 1 then index = #direction end -- loop
weather.wind.i = index
x = direction[index].x * mx
z = direction[index].z * mz
dev.dts = direction[index].hrf
dev.sts = ""
end
end
if x and z then
weather.wind.x = x
weather.wind.z = z
end
-- random trigger 1-2 mins
minetest.after(math.random(30,60), wind_generator)
end
minetest.after(30, wind_generator)
local function weather_generator()
local time = minetest.get_timeofday()
if time >= 0.5 then time = 1 - time end
if time <= 0.1875 or time >= 0.2396 then
if force > 60 then
for k,v in pairs(weather.registered_biomes) do
local n = math.random(1,10)
if n > 5 then
-- increment or reset if max index reached
if biome_idx[k] < #v then
biome_idx[k] = biome_idx[k] + 1
elseif biome_idx[k] >= #v then -- reset
biome_idx[k] = 1
end
end
end
force = 0 -- reset
end
end
minetest.after(GCYCLE, weather_generator)
end
minetest.after(10, weather_generator)
local function lightning_generator()
-- test code
lightning = math.random(1, 10) > 5
minetest.after(5, lightning_generator)
end
minetest.after(5, lightning_generator)
-- AUDIO --
local function fade_audio(name, sound_clip)
local entry, gain, sound
-- handle specific sound clip?
if sound_clip then
entry = audio_id[name][sound_clip]
if entry then
-- stop current audio
minetest.sound_stop(entry.id)
-- fade at the correct gain
gain = entry.gain
sound = sound_clip .. "_fadeout"
audio_id[name] = nil
minetest.sound_play(sound,
{
to_player = name,
gain = gain,
loop = false
}
)
end
else -- all looping audio!
entry = audio_id[name]
for k,v in pairs(entry) do
-- stop current audio
minetest.sound_stop(v.id)
-- fade at the correct gain
gain = v.gain
sound = k .. "_fadeout"
audio_id[name][k] = nil
minetest.sound_play(sound,
{
to_player = name,
gain = gain,
loop = false
}
)
end
end
end
local function play_audio(name, sound_clip, player_gain)
local id,clip
-- is sound playing?
if audio_id[name][sound_clip] then
clip = audio_id[name][sound_clip]
if clip.gain == player_gain then -- no change?
return
else -- gain change
minetest.sound_stop(clip.id)
audio_id[name][sound_clip] = nil
end
id = minetest.sound_play(sound_clip,
{
to_player = name,
gain = player_gain,
loop = true,
}
)
else
-- add sound clip, fading in
id = minetest.sound_play(sound_clip,
{
to_player = name,
gain = player_gain,
fade = 0.01,
loop = true,
}
)
end
-- stash meta for subsequent control
if id then
audio_id[name][sound_clip] = {id = id, gain = player_gain}
end
end
function weather.audio(name, pos, def, outside)
local pass
local status = weather.status[name]
if pos.y > YMAX or pos.y < YMIN then
fade_audio(name)
return
end
-- does the weather def have sound?
if def.sound then
-- allow rain -> rain transition
if status.p:match("rain") and status.w:match("rain") then
pass = true
else
if s[name] or status.lock then
return
end
pass = true
end
else
fade_audio(name)
return
end
if pass then
local gain
if outside then
gain = RAINGAIN * def.gain
else
gain = RAINGAIN * (def.gain * 0.75)
end
play_audio(name, def.sound, gain)
end
end
-- SKYBOX --
function weather.sky(name, player, def)
-- handle skybox transitions based on time of day
local sval, sky_bg
local psky = {}
local clouds = def.clouds or false
local p_def = weather.registered_weather[weather.status[name].p]
if def.skybox then
-- skybox and no ticket requires a fade in
if not sky[name] then
sky[name] = {fin = true} -- init fade in
end
elseif sky[name] then
-- ticket and no skybox requires fade out
if not sky[name].fout then
sky[name].fout = true -- init fade out
end
end
-- ticket?
if not sky[name] then return end
-- get current player sky
psky.bgcolor, psky.type, psky.textures = player:get_sky()
local time = minetest.get_timeofday()
if time >= 0.5 then time = 1 - time end
if time <= 0.1875 then -- ### NIGHTIME ###
if sky[name].fin then -- fading in?
sky[name].fin = false -- no fade in!
s[name] = nil
elseif sky[name].fout then
-- no fade out, it's dark so return to regular skybox
player:set_sky({}, "regular", {}, clouds)
sky[name] = nil -- remove ticket
s[name] = nil
return
end
sval = NISVAL -- skybox colour
elseif time >= 0.2396 then -- ### DAYTIME ###
-- use players current skybox type to determine
-- the starting point of the rgb_lerp
if psky.type == "regular" and p_def.cloud_def.height == 1 then
sky_bg = SKY_WHITE
elseif psky.type == "regular" then
sky_bg = SKY_BLUE
elseif psky.type == "plain" then
sky_bg = psky.bgcolor
end
-- fade in?
if sky[name].fin then
-- if fade hasn't been initialised then calcuate the increment
-- and initialise t to one increment
if not sky[name].t then
sky[name].inc = 1 / STEPS -- calc increment
sky[name].t = sky[name].inc
sky[name].bg = sky_bg
s[name] = true
end
sval = rgba_lerp(sky[name].bg, SKY_GREY, sky[name].t)
sky[name].t = sky[name].t + sky[name].inc
if sky[name].t > 1 then
sval = SKY_GREY
sky[name].fin = nil
sky[name].t = nil
sky[name].inc = nil
s[name] = nil
end
player:set_sky(sval, "plain", {}, clouds)
return
elseif sky[name].fout then
-- if fade hasn't been initialised then calcuate the increment
-- and initialise t to one increment
if not sky[name].t then
sky[name].inc = 1 / STEPS
sky[name].t = 0
sky[name].bg = sky_bg
s[name] = true
end
sval = rgba_lerp(sky[name].bg, SKY_BLUE, sky[name].t)
sky[name].t = sky[name].t + sky[name].inc
-- overflow?
if sky[name].t < 1 then
player:set_sky(sval, "plain", {}, clouds) -- set
s[name] = nil
else
player:set_sky({}, "regular", {}, clouds)
sky[name] = nil -- remove ticket
s[name] = nil
end
return
end
sval = DASVAL -- set skybox colour
else -- ### SUNRISE ###
-- First transition (24000 -) 4500, (1 -) 0.1875
-- Last transition (24000 -) 5750, (1 -) 0.2396
sval = math.floor(NISVAL + ((time - 0.1875) / 0.0521) * (DASVAL - NISVAL))
--sval = rgb_lerp()
end
-- Set sky; slight blue shift
local rgba = {r = sval, g = sval, b = sval + 16, a = 255}
player:set_sky(rgba, "plain", {}, clouds)
end
local function revertsky()
-- decrement t for skybox lightning tickets
for key, entry in pairs(l_temp) do
entry.t = entry.t - 1
if entry.t > 1 then
break
end
-- revert skybox
local tmp = entry.s
entry.p:set_sky(tmp.bgcolor, tmp.type, tmp.textures, tmp.clouds)
l_temp[key] = nil -- remove ticket
end
end
minetest.register_globalstep(revertsky)
function weather.lightning(name, player, def)
-- only process players in a weather type that uses lightning
if not def.lightning then
return
elseif lightning and
not weather.status[name].lock and
not s[name] then -- lightning event
local delay = math.random(0, 5)
local clouds = def.clouds or false -- default to off
if delay ~= 5 then -- ~ 83% chance of thunder
minetest.after(delay, function()
-- untracked audio sample
minetest.sound_play({
to_player = name,
name = "weather_thunder",
gain = 8
})
end)
end
local psky = {}
psky.bgcolor, psky.type, psky.textures, psky.clouds = player:get_sky()
if not l_temp[name] then
l_temp[name] = {p = player, s = psky, t = 5}
player:set_sky(SKY_WHITE, "plain", {}, clouds)
end
end
end
-- CLOUDS --
function weather.cloud_color(name, player, def)
local current = player:get_clouds()
local cloud_def = def.cloud_def or CDEF -- init if reqd!
local nval
-- weather type has clouds?
if def.clouds then
-- no ticket and skybox colour diff?
if not c[name] and rgba_diff(current.color, cloud_def.color) then
-- sharp transition? [fog > cloud]
if current.height == 1 and cloud_def.height > current.height or
current.height > 1 and cloud_def.height == 1 then
c[name] = {
inc = 1,
t = 1,
s = current.color,
f = cloud_def.color
}
else -- smooth transition
c[name] = {
inc = 1/STEPS,
t = 1/STEPS,
s = current.color,
f = cloud_def.color
}
end
end
end
-- ticket?
if c[name] then
weather.status[name].lock = true -- assert blocking status
nval = rgba_lerp(c[name].s, c[name].f, c[name].t)
c[name].t = c[name].t + c[name].inc
if c[name].t > 1 then
nval = c[name].f
c[name] = nil
end
return nval
end
end
function weather.cloud_density(name, player, def)
local nval
local current = player:get_clouds()
if def.clouds then
-- no ticket and a shift to ground level?
if not d[name] and current.height > 1 and
def.height == 1 then
-- initiate the fade out first and extend the ticket
d[name] = {
inc = 1 / (STEPS*2),
t = 1 / (STEPS*2),
s = current.density,
f = 0
}
-- no ticket and density diff?
elseif not d[name] and
def.cloud_def.density ~= round(current.density) then
d[name] = {
inc = 1 / (STEPS*2),
t = 1 / (STEPS*2),
s = current.density,
f = def.cloud_def.density
}
end
else
-- initialise evaporate transition for no clouds
if not d[name] and current.density and current.density > 0 then
d[name] = {
inc = 1 / (STEPS*2),
t = 1 / (STEPS*2),
s = current.density,
f = 0
}
end
end
if d[name] then -- ticket
weather.status[name].lock = true -- assert blocking status
nval = round(lerp(d[name].s, d[name].f, d[name].t))
d[name].t = d[name].t + d[name].inc
if d[name].t > 1 then -- overflow
nval = d[name].f
d[name] = nil -- remove ticket
end
return nval
end
end
function weather.cloud_thickness(name, player, def)
local nval
local current = player:get_clouds()
if def.clouds then
-- ticket?
if not t[name] and def.cloud_def.thickness ~= current.thickness then
t[name] = {
inc = 1 / (STEPS*2),
t = 1 / (STEPS*2),
s = current.thickness,
f = def.cloud_def.thickness
}
end
else
-- no clouds
if not t[name] and current.thickness and current.thickness > 0 then
-- initialise
t[name] = {
inc = 1 / (STEPS*2),
t = 1 / (STEPS*2),
s = current.thickness,
f = 0
}
end
end
if t[name] then
weather.status[name].lock = true -- assert blocking status
nval = round(lerp(t[name].s, t[name].f, t[name].t))
t[name].t = t[name].t + t[name].inc
-- overflow?
if t[name].t > 1 then
nval = t[name].f
t[name] = nil
end
return nval
end
end
function weather.cloud_height(name, player, def)
local nval
local current = player:get_clouds()
if def.clouds then
-- ticket?
if not h[name] and def.cloud_def.height ~= current.height then
-- sharp transition? [fog <-> cloud]
if current.height == 1 and
def.cloud_def.height > current.height or
current.height > 1 and
def.cloud_def.height == 1 then
h[name] = {
inc = 1,
t = 1,
s = current.height,
f = def.cloud_def.height
}
else -- smooth transition
h[name] = {
inc = 1 / STEPS,
t = 1 / STEPS,
s = current.height,
f = def.cloud_def.height
}
end
end
else
-- no clouds
if not h[name] and current.height and current.height > 0 then
-- initialise
h[name] = {
inc = 1,
t = 1,
s = 120,
f = 120
}
end
end
if h[name] then
weather.status[name].lock = true -- assert blocking status
nval = round(lerp(h[name].s, h[name].f, h[name].t))
h[name].t = h[name].t + h[name].inc
-- overflow?
if h[name].t > 1 then
nval = h[name].f
h[name] = nil
end
return nval
end
end
local function wind_changed(current)
if current and
current.speed.x ~= weather.wind.x or
current.speed.z ~= weather.wind.z then
return true
end
end
function weather.clouds(name, player, def)
--[[
This function attempts to encapsulate the transition sequences
for different types of cloud. It uses the current and previous
type definitions to decide the sequence order.
]]
-- block cloud transitions during skybox transitions
if s[name] then return end
-- localise def for previous weather type
local p_def = weather.registered_weather[weather.status[name].p]
local current = player:get_clouds()
local final = def.cloud_def
local c_def = {}
local m, color, density, thickness, height
if p_def.name:match("fog") or p_def.name:match("sand") or
p_def.name:match("dust") then
-- previous type ground based
if def.name == "clear" then
-- ground based to clear using transition sequence: TDHC
if t[name] or current.thickness ~= final.thickness then
thickness = weather.cloud_thickness(name, player, def)
if thickness then
c_def.thickness = thickness
m = true
end
elseif d[name] or round(current.density) ~= final.density then
density = weather.cloud_density(name, player, def)
if density then
c_def.density = density
m = true
end
elseif h[name] or current.height ~= final.height then
height = weather.cloud_height(name, player, def)
if height then
c_def.height = height
m = true
end
elseif c[name] or rgba_diff(current.color, final.color) then
color = weather.cloud_color(name, player, def)
if color then
c_def.color = color
m = true
end
end
elseif def.name:match("fog") or def.name:match("sand") or
def.name:match("dust") then
-- ground changes using transition sequence: CT
if c[name] or rgba_diff(current.color, final.color) then
color = weather.cloud_color(name, player, def)
if color then
c_def.color = color
m = true
end
elseif t[name] or current.thickness ~= final.thickness then
thickness = weather.cloud_thickness(name, player, def)
if thickness then
c_def.thickness = thickness
m = true
end
end
elseif def.clouds then
-- ground to sky using transition sequence: TDHCDT
if h[name] or current.height ~= final.height then
-- use suitable def for thickness and density
local ndef = weather.registered_weather["clear"]
if t[name] or
round(current.thickness) ~= ndef.cloud_def.thickness then
thickness = weather.cloud_thickness(name, player, ndef)
if thickness then
c_def.thickness = thickness
m = true
end
elseif d[name] or
round(current.density) ~= ndef.cloud_def.density then
density = weather.cloud_density(name, player, ndef)
if density then
c_def.density = density
m = true
end
else
height = weather.cloud_height(name, player, def)
if height then
c_def.height = height
m = true
end
end
elseif c[name] or rgba_diff(current.color, final.color) then
color = weather.cloud_color(name, player, def)
if color then
c_def.color = color
m = true
end
elseif d[name] or round(current.density) ~= final.density then
density = weather.cloud_density(name, player, def)
if density then
c_def.density = density
m = true
end
elseif t[name] or current.thickness ~= final.thickness then
thickness = weather.cloud_thickness(name, player, def)
if thickness then
c_def.thickness = thickness
m = true
end
end
end
elseif def.name:match("fog") or def.name:match("sand") or
def.name:match("dust") then
-- previous type not ground based
if p_def.name == "clear" then
-- clear to ground transition sequence: HCDT
if h[name] or current.height ~= final.height then
height = weather.cloud_height(name, player, def)
if height then
c_def.height = height
m = true
end
elseif c[name] or rgba_diff(current.color, final.color) then
color = weather.cloud_color(name, player, def)
if color then
c_def.color = color
m = true
end
elseif d[name] or round(current.density) ~= final.density then
density = weather.cloud_density(name, player, def)
if density then
c_def.density = density
m = true
end
elseif t[name] or current.thickness ~= final.thickness then
thickness = weather.cloud_thickness(name, player, def)
if thickness then
c_def.thickness = thickness
m = true
end
end
elseif p_def.clouds then
-- sky to ground transition sequence: TDHCDT
if h[name] or current.height ~= final.height then
-- use suitable def for thickness and density
local ndef = weather.registered_weather["clear"]
if t[name] or
round(current.thickness) ~= ndef.cloud_def.thickness then
thickness = weather.cloud_thickness(name, player, ndef)
if thickness then
c_def.thickness = thickness
m = true
end
elseif d[name] or
round(current.density) ~= ndef.cloud_def.density then
density = weather.cloud_density(name, player, ndef)
if density then
c_def.density = density
m = true
end
else
height = weather.cloud_height(name, player, def)
if height then
c_def.height = height
m = true
end
end
elseif c[name] or rgba_diff(current.color, final.color) then
color = weather.cloud_color(name, player, def)
if color then
c_def.color = color
m = true
end
elseif d[name] or round(current.density) ~= final.density then
density = weather.cloud_density(name, player, def)
if density then
c_def.density = density
m = true
end
elseif t[name] or current.thickness ~= final.thickness then
thickness = weather.cloud_thickness(name, player, def)
if thickness then
c_def.thickness = thickness
m = true
end
end
end
elseif p_def.clouds then
-- handle previous cloud with transition sequence: TDCH
if t[name] or current.thickness ~= final.thickness then
thickness = weather.cloud_thickness(name, player, def)
if thickness then
c_def.thickness = thickness
m = true
end
elseif d[name] or round(current.density) ~= final.density then
density = weather.cloud_density(name, player, def)
if density then
c_def.density = density
m = true
end
elseif c[name] or rgba_diff(current.color, final.color) then
color = weather.cloud_color(name, player, def)
if color then
c_def.color = color
m = true
end
elseif h[name] or current.height ~= final.height then
height = weather.cloud_height(name, player, def)
if height then
c_def.height = height
m = true
end
end
elseif def.clouds then
-- handle cloud with no previous using transition sequence: HCDT
if h[name] or current.height ~= final.height then
height = weather.cloud_height(name, player, def)
if height then
c_def.height = height
m = true
end
elseif c[name] or rgba_diff(current.color, final.color) then
color = weather.cloud_color(name, player, def)
if color then
c_def.color = color
m = true
end
elseif d[name] or round(current.density) ~= final.density then
density = weather.cloud_density(name, player, def)
if density then
c_def.density = density
m = true
end
elseif t[name] or current.thickness ~= final.thickness then
thickness = weather.cloud_thickness(name, player, def)
if thickness then
c_def.thickness = thickness
m = true
end
end
end
-- wind change?
if wind_changed(current) then
c_def.speed = weather.wind
m = true
end
-- modify?
if m then
player:set_clouds(c_def)
end
-- unlock when all transitions complete
if not color and not thickness and
not density and not height then
weather.status[name].lock = false
end
end
-- PRECIPITATION --
function weather.precipitation(tbl)
local status = weather.status[tbl.name]
local lock
-- allow rain -> rain
if status.p:match("rain") and status.w:match("rain") then
lock = false
elseif s[tbl.name] then -- skybox lock
return
else -- use status lock
lock = status.lock
end
if lock then return end
-- particle def?
if tbl.def.particle then
-- precipitation is limited by players height
if tbl.player_pos.y > YMAX or tbl.player_pos.y < YMIN then
return
end
local p_obj = tbl.def.particle
local inside = {}
local outside = {}
-- set player in particle def copy
p_obj.playername = tbl.name
-- set velocity
p_obj.velocity.x = weather.wind.x
p_obj.velocity.z = weather.wind.z
-- Rain or snow?
if tbl.weather_name:match("rain") or
tbl.weather_name:match("tropical") then
-- generate positions
for droplet = 1, p_obj.qty do
local pos = {
x = tbl.ppos.x - 8 + math.random(0, 16),
y = tbl.ppos.y + 8 + math.random(0, 5),
z = tbl.ppos.z - 8 + math.random(0, 16)
}
-- create unique key
local key = tostring(pos.x) .. tostring(pos.z)
-- handle signed chars
key = key:gsub("%-", "n")
if not inside[key] then
if not outside[key] then
if minetest.get_node_light(pos, 0.5) == 15 then
outside[key] = true
else
inside[key] = true
end
end
end
if outside[key] then
-- outside, add particle
p_obj.pos = pos
minetest.add_particle(p_obj)
end
end
elseif tbl.weather_name:match("snow") or
tbl.weather_name:match("blizzard") then
-- generate positions
for flake = 1, p_obj.qty do
local pos = {
x = tbl.ppos.x - 24 + math.random(0, 47),
y = tbl.ppos.y + 8 + math.random(0, 1),
z = tbl.ppos.z - 20 + math.random(0, 47)
}
-- create a unique key
local key = tostring(pos.x) .. tostring(pos.z)
-- handle signed chars
key = key:gsub("%-", "n")
if not inside[key] then
if not outside[key] then
if minetest.get_node_light(pos, 0.5) == 15 then
outside[key] = true
else
inside[key] = true
end
end
end
if outside[key] then
-- outside, add particle
p_obj.pos = pos
minetest.add_particle(p_obj)
end
end
end
end
end
-- PLAYER LOOP --
local function player_weather()
local players = minetest.get_connected_players()
for _, player in ipairs(players) do
local name = player:get_player_name()
local player_pos = player:get_pos()
if not player or not player_pos then break end -- sanity check
local ppos = particle_pos(player_pos, player:get_player_velocity())
-- biome detection
local biome_name = minetest.get_biome_name(
minetest.get_biome_data(player_pos).biome)
local status = weather.status[name]
if not status.initialised then
status.working_biome = biome_name
status.initialised = true
end
local pattern = weather.registered_biomes[status.working_biome]
local new_pattern = weather.registered_biomes[biome_name]
-- missing pattern handler
if not pattern or not new_pattern then
player:set_sky({}, "regular", {}, true) -- reset skybox
break -- next!
end
local weather_name = pattern[biome_idx[status.working_biome]]
local def = weather.registered_weather[weather_name]
local outside = minetest.get_node_light(ppos, 0.5) == 15
-- biome transition delay
-- stop transitions toggling at the biome blend regions by using a delay
if status.ttl and status.ttl < 1 then
status.working_biome = biome_name -- update
status.ttl = nil --remove
elseif status.ttl then
if status.working_biome ~= biome_name then
status.ttl = status.ttl - 1 -- decrement
else
status.ttl = nil -- remove
end
elseif status.working_biome ~= biome_name and not status.ttl then
status.ttl = 5 -- reset
end
status.outside = outside
-- update current & previous weather status
if status.w then
if status.w ~= weather_name then
status.p = status.w
status.w = weather_name
end
else
status.p = 'cloudy'
status.w = weather_name
end
weather.status[name] = status -- update global
weather.sky(name, player, def)
weather.clouds(name, player, def)
weather.precipitation({
name = name,
player = player,
def = def,
player_pos = player_pos,
ppos = ppos,
weather_name = weather_name})
weather.lightning(name, player, def)
weather.audio(name, player_pos, def, outside)
-- ### DEVELOPMENT USE ONLY! ###
-- depends on conf setting and dev table
if mod_debug then
local current = player:get_clouds()
local msg = ([[
biome:
current: %s
working: %s
weather:
prev: %s
now: %s
skybox:
plain: %s
lock: %s
cloud:
lock: %s
density: %s
colour: %s, %s, %s, %s
height: %s
thickness: %s
wind:
speed: %s, %s
adj: %s
dir: %s
]]):format(biome_name:gsub("_", " "),
status.working_biome:gsub("_", " "),
weather.status[name].p,
weather.status[name].w,
tostring(def.skybox),
tostring(s[name]),
tostring(weather.status[name].lock),
round(current.density),
tostring(current.color.r),
tostring(current.color.g),
tostring(current.color.b), tostring(current.color.a),
tostring(current.height),
round(current.thickness),
tostring(weather.wind.x), tostring(weather.wind.z),
tostring(dev.sts),
tostring(dev.dts)
)
if dev.id then -- update
player:hud_change(dev.id, "text", msg)
else -- add
dev.id = player:hud_add({
hud_elem_type = "text",
name = "dev_hud",
scale = {x=100, y=100},
text = msg,
number = 0x00FF00,
position = {x=0.8, y=0.5},
alignment = {x=0, y=0},
offset = {x=0, y=0}
})
end
end
end -- player loop
lightning = false -- players processed, reset
minetest.after(PCYCLE, player_weather) -- loop
end
minetest.after(5, player_weather)
-- CALLBACKS --
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
weather.status[name] = {}
audio_id[name] = {}
end)
minetest.register_on_leaveplayer(function(player)
-- cleanup player data
local name = player:get_player_name()
weather.status[name] = nil
if hud_id[name] then hud_id[name] = nil end
local def = audio_id[name]
if def then
-- cleanup player audio entries
for k,v in pairs(def) do
minetest.sound_stop(v.id)
end
audio_id[name] = nil
end
end)
-- DEV COMMANDS --
if mod_debug then
minetest.register_chatcommand("nxt", {
description = "Set next weather in the current biome",
privs = {server = true},
func = function(name)
local player = minetest.get_player_by_name(name)
local pos = player:get_pos()
local biome_name = minetest.get_biome_name(
minetest.get_biome_data(pos).biome)
local pattern = weather.registered_biomes[biome_name]
if pattern then
if biome_idx[biome_name] < #pattern then
biome_idx[biome_name] = biome_idx[biome_name] + 1
else
biome_idx[biome_name] = 1
end
force = 0 -- reset
else
minetest.chat_send_player(name, "No weather pattern for this biome!")
end
end,
})
minetest.register_chatcommand("prev", {
description = "Set previous weather in the current biome",
privs = {server = true},
func = function(name)
local player = minetest.get_player_by_name(name)
local pos = player:get_pos()
local biome_name = minetest.get_biome_name(
minetest.get_biome_data(pos).biome)
local pattern = weather.registered_biomes[biome_name]
if pattern then
if biome_idx[biome_name] == 1 then
biome_idx[biome_name] = #pattern
else
biome_idx[biome_name] = biome_idx[biome_name] - 1
end
force = 0 -- reset
else
minetest.chat_send_player(name, "No weather pattern for this biome!")
end
end,
})
end