local lib = starlit.mod.lib
local world = starlit.world
function world.date()
local days = minetest.get_day_count()
local year = math.floor(days / world.planet.orbit);
local day = days % world.planet.orbit;
return {
year = year, day = day;
season = day / world.planet.orbit + 0.5; -- begin summer
}
end
local lerp = lib.math.lerp
local gradient = lib.math.gradient
local altitudeCooling = 10 / 100
local heatRange = {min = -50, max = 50} -- translate mt temps into real temps
-- this function provides the basis for temperature calculation,
-- which is performed by adding this value to the ambient temperature,
-- determined by querying nearby group:heatSource items in accordance
-- with the inverse-square law
function world.climate.eval(pos, tod, season)
local data = minetest.get_biome_data(pos)
local biome = world.ecology.biomes.db[minetest.get_biome_name(data.biome)]
-- print('climate:', dump(data))
local heat, humid = data.heat, data.humidity
heat = lerp(heat/100, heatRange.min, heatRange.max)
tod = tod or minetest.get_timeofday()
heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta)
-- print('base heat', heat)
local td = world.date()
heat = heat + gradient(biome.seasonalTemp, season or td.season)
-- print('seasonal heat', heat)
if pos.y > 0 then
heat = heat - pos.y*altitudeCooling
end
-- print('altitude heat', heat)
return {
surfaceTemp = heat;
waterTemp = heat + biome.waterTempDelta;
surfaceHumid = humid;
}
end
local vdsq = lib.math.vdsq
function world.climate.temp(pos, timeshift) --> irradiance at pos in W
local cl = world.climate.eval(pos)
local radCenters = starlit.region.radiator.store:get_areas_for_pos(pos, false, true)
local irradiance = 0
for _,e in pairs(radCenters) do
local rpos = minetest.string_to_pos(e.data)
local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name])
local rc = rdef._starlit.radiator
local r_max = rc.radius(rpos)
local dist_sq = vdsq(rpos,pos)
if dist_sq <= r_max^2 then
-- cheap bad way
-- if minetest.line_of_sight(rpos,pos) then
--
-- expensive way
local obstruct = 0
local ray = Raycast(rpos, pos, true, true)
for p in ray do
if p.type == 'node' then obstruct = obstruct + 1 end
end
if obstruct < 4 then
local power, customFalloff = rc.radiate(rpos, pos)
-- okay this isn't the real inverse square law but i
-- couldn't figure out a better way to simplify the
-- model without checking an ENORMOUS number of nodes
-- maybe someone else who isn't completely
-- mathtarded can do better.
if not customFalloff then
power = power * (1 - (dist_sq / ((r_max+1)^2)))
end
power = power * (1 - (obstruct/5))
irradiance = irradiance + power
end
end
end
local w = world.climate.weatherAt(pos, timeshift)
return irradiance + cl.surfaceTemp
end
function world.ecology.biomeAt(pos)
return world.ecology.biomes.db[minetest.get_biome_name(minetest.get_biome_data(pos).biome)]
end
minetest.after(0, function()
world.climate.weatherMap.kind = minetest.get_perlin {
seed = 0x925afe;
octaves = 2;
spread = vector.new(256,256,120);
};
world.climate.weatherMap.severity = minetest.get_perlin {
seed = 0x39de1d;
octaves = 1;
spread = vector.new(256,256,60);
};
end)
function world.climate.weatherAt(pos, timeshift)
timeshift = timeshift or 0
local wv = world.climate.weatherMap.kind:get_3d(vector.new(pos.x, pos.z, minetest.get_gametime() + timeshift))
local sev = world.climate.weatherMap.severity:get_3d(vector.new(pos.x, pos.z, minetest.get_gametime() + timeshift))
local b = world.ecology.biomeAt(pos)
local w = 'starlit:clear'
for i,v in ipairs(b.weather) do
if wv < v[1] then
w = v[2]
break
end
end
local mods = {
cloudCover = 0;
rain = 0; -- affects plant growth
snow = 0; -- spawns snow layer
fog = 0;
temp = 0;
hum = 0;
rad = 0;
}
return world.climate.weather.db[w], sev
end
-- weather manages particle systems, and provides modifiers for
-- temp, cloud cover, received precipitation, and fog
world.climate.weather.link('starlit:clear', {
name = 'Clear';
})
world.climate.weather.link('starlit:cloudy', {
name = 'Cloudy';
mod = function(m, temp, hum, sev)
m.cloudCover = math.max(m.cloudCover, sev)
end;
})
world.climate.weather.link('starlit:precip', {
name = function(temp, hum, sev)
if temp < 0 then return 'Snow' else return 'Rain' end
end;
mod = function(m, temp, hum, sev)
m.cloudCover = math.max(m.cloudCover, sev)
if temp < 0 then
m.snow = math.max(m.snow, sev/2)
else
m.rain = math.max(m.rain, sev/2)
end
end;
})
world.climate.weather.link('starlit:storm', {
name = function(temp, hum, sev)
if temp < 0 then
if sev > .5
then return 'Blizzard'
else return 'Snowstorm'
end
else
if sev > .5
then return 'Monsoon'
else return 'Rainstorm'
end
end
end;
mod = function(m, temp, hum, sev)
m.cloudCover = math.max(m.cloudCover, sev)
if temp < 0 then
m.snow = math.max(m.snow, sev/2 + .5)
else
m.rain = math.max(m.rain, sev/2 + .5)
end
end;
})
world.climate.weather.link('starlit:tstorm', {
name = 'Thunderstorm';
danger = 1;
mod = function(m, temp, hum, sev)
m.cloudCover = math.max(m.cloudCover, sev)
m.danger = (sev>.5) and 2 or 1
end;
})
world.climate.weather.link('starlit:sstorm', {
name = 'Solar Storm';
danger = 2;
})
world.climate.weather.link('starlit:meteorShower', {
name = 'Meteor Shower';
danger = 2;
})
world.ecology.biomes.foreach('starlit:biome-gen', {}, function(id, b)
b.def.name = id
minetest.register_biome(b.def)
end)
world.ecology.plants.foreach('starlit:plant-gen', {}, function(id, b)
local stageCt = #b.stages
local function stageID(n)
if n == stageCt then return id end
return id .. string.format('_stage_%s', n)
end
b.stageNodes = {}
b.req = b.req or {}
local function regStage(n, st)
local base = {
description = b.name;
drawtype = "plantlike";
tiles = { tostring(st.tex) };
paramtype = "light";
paramtype2 = "meshoptions";
place_param2 = b.meshOpt;
walkable = false;
buildable_to = true;
groups = {
plant = 1;
plant_grow = stageCt ~= n and 1 or 0;
attached_node = 3;
};
drop = st.drop;
_starlit = {
plant = {
id = id, stage = n;
};
recover = b.recover or starlit.type.fab {
time = { shred = .3; };
cost = { shredPower = 1; };
};
recover_vary = b.recover_vary or function(rng, ctx)
return starlit.type.fab {
element = {
carbon = rng:int(0,2);
potassium = rng:int(0,1);
magnesium = rng:int(0,b.biolum and 2 or 1);
}
};
end;
};
}
if st.swap then
base.node_dig_prediction = ""
function base.after_dig_node(pos, node, digger)
node.name = stageID(st.swap)
minetest.swap_node(pos, node)
return true
end
end
if st.biolum then base.light_source = st.biolum; end
return base
end
for i, v in ipairs(b.stages) do
local n = regStage(i, v)
minetest.register_node(stageID(i), n)
b.stageNodes[i] = stageID(i)
end
b.fullyGrown = stageID(stageCt)
local dec = {
deco_type = 'simple';
decoration = b.stageNodes;
height = 1;
param2 = b.meshOpt or 0;
}
for k,v in pairs(b.decoration) do dec[k] = v end
b.decoration = minetest.register_decoration(dec)
end)
local toward = lib.math.toward
local hfinterval = 1.5
starlit.startJob('starlit:temps', hfinterval, function(delta)
-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
-- player is in -30°C weather, and has an internal temperature of
-- 10°C. then:
-- κ = .1°C/C/s (which is apparently 100mHz)
-- Tₚ = 10°C
-- Tₑ = -30°C
-- d = Tₑ − Tₚ = -40°C
-- ΔT = κ×d = -.4°C/s
-- too cold:
-- x = beginning of danger zone
-- κ × (x - Tₚ) = y where y < Tₚ
-- our final change in temperature is computed as tΔC where t is time
local kappa = starlit.constant.heat.thermalConductivity
local now = minetest.get_gametime()
for name,user in pairs(starlit.activeUsers) do
local tr = user:species().tempRange
local t = starlit.world.climate.temp(user.entity:get_pos())
local weather,wsev = starlit.world.climate.weatherAt(user.entity:get_pos())
local wfac
if user.env.weather == nil
then wfac = 1
else wfac = (now - user.env.weather.when) / 10
end
if user.env.weather == nil or now - user.env.weather.when >= 10 then
user.env.weather = {when = now, what = weather}
end
do -- this bit probably belongs in starlit:bio but we do it here in order
-- to spare ourselves another call into the dark swamp of climate.temp
local urg = 1
local hz = user:tempHazard(t)
local tr = user:species().tempRange.survivable
if hz == 'cold' then
if tr[1] - t > 7 then urg = 2 end
user:alarm(urg, 'freeze', 3)
elseif hz == 'hot' then
if t - tr[2] > 7 then urg = 2 end
user:alarm(urg, 'overheat', 3)
end
end
local insul = 0
local naked = user:naked()
local suitDef
if not naked then
suitDef = user:suitStack():get_definition()
insul = suitDef._starlit.suit.temp.insulation
end
local warm = user:effectiveStat 'warmth'
local tSafeMin, tSafeMax = tr.survivable[1], tr.survivable[2]
local tComfMin, tComfMax = tr.comfort[1], tr.comfort[2]
local tDelta = (kappa * (1-insul)) * (t - warm) * hfinterval
local tgt = warm + tDelta
-- old logic: we move the user towards the exterior temperature, modulated
-- by her suit insulation.
--local tgt = toward(warm, t, hfinterval * thermalConductivity * (1 - insul))
if not naked then
local suit = user:getSuit()
local suitPower = suit:powerState()
local suitPowerLeft = suit:powerLeft()
if suitPower ~= 'off' then
local coilPower = 1.0
local st = suitDef._starlit.suit.temp
if suitPower == 'powerSave' and (tgt >= tSafeMin and tgt <= tSafeMax) then coilPower = 0.5 end
if tgt < tComfMin and st.maxHeat > 0 then
local availPower = user:suitDrawCurrent(st.heatPower*coilPower, hfinterval)
tgt = tgt + (availPower / st.heatPower) * st.maxHeat * coilPower * hfinterval
end
if tgt > tComfMax and st.maxCool > 0 then
local availPower = user:suitDrawCurrent(st.coolPower*coilPower, hfinterval)
tgt = tgt - (availPower / st.coolPower) * st.maxCool * coilPower * hfinterval
end
end
end
user:statDelta('warmth', tgt - warm) -- dopey but w/e
warm = tgt -- for the sake of readable code
-- does this belong in starlit:bio? unsure tbh
if warm < tSafeMin or warm > tSafeMax then
local dv
if warm < tSafeMin then
dv = math.abs(warm - tSafeMin)
else
dv = math.abs(warm - tSafeMax)
end
-- for every degree of difference you suffer 2 points of damage/s
local dmg = math.ceil(dv * 2)
user:statDelta('health', -dmg)
end
end
end)
world.ecology.trees.foreach('starlit:tree-gen', {}, function(id, t)
for i,td in ipairs(t.decorate) do
local dec = {
deco_type = 'lsystem';
treedef = t.def;
}
for k,v in pairs(td) do dec[k]=v end
minetest.register_decoration(dec)
end
end)
minetest.register_abm {
label = "plant growth";
nodenames = {'group:plant_grow'};
chance = 15;
interval = 20;
catch_up = true;
action = function(pos, node)
local def = minetest.registered_nodes[node.name]._starlit.plant
-- 5 W: maximum power for UV lamps
-- 7 W: maximum solar power
local uv = (minetest.get_natural_light(pos) / 15) * 7
-- TODO compute artificial contribution
local req = lib.tbl.defaults({
uv = 3;
soil = 'soil';
temp = -10;
humid = nil;
}, def.growReq);
-- TODO check other reqs
if uv > req.uv then
local plant = starlit.world.ecology.plants.db[def.id]
local nextStage = plant.stageNodes[def.stage + 1]
minetest.swap_node(pos, {name=nextStage})
end
end;
}