Files
last-hearth/mods/hearth/model.lua
T
2026-11-03 12:41:09 -05:00

77 lines
3.7 KiB
Lua
Executable File

-- Pure simulation functions: no engine state, I/O, network, or mutable clock.
local M = {}
function M.clamp(n, low, high) return math.max(low, math.min(high, n)) end
function M.location(lat, lon)
lat, lon = tonumber(lat), tonumber(lon)
if not lat or not lon or lat ~= lat or lon ~= lon or math.abs(lat) > 90
or math.abs(lon) > 180 then return nil end
return {latitude = lat, longitude = lon}
end
function M.solar(timestamp, latitude, longitude)
local t = os.date("!*t", timestamp)
local leap = t.year % 4 == 0 and (t.year % 100 ~= 0 or t.year % 400 == 0)
local days = leap and 366 or 365
local utc = t.hour + t.min / 60 + t.sec / 3600
local gamma = 2 * math.pi / days * (t.yday - 1 + (utc - 12) / 24)
-- NOAA fractional-year approximation; weather is synthetic, not a forecast.
local equation = 229.18 * (0.000075 + 0.001868 * math.cos(gamma)
- 0.032077 * math.sin(gamma) - 0.014615 * math.cos(2 * gamma)
- 0.040849 * math.sin(2 * gamma))
local declination = 0.006918 - 0.399912 * math.cos(gamma)
+ 0.070257 * math.sin(gamma) - 0.006758 * math.cos(2 * gamma)
+ 0.000907 * math.sin(2 * gamma) - 0.002697 * math.cos(3 * gamma)
+ 0.00148 * math.sin(3 * gamma)
local solar_hour = (utc + longitude / 15 + equation / 60) % 24
local lat = math.rad(latitude)
local sin_elevation = math.sin(lat) * math.sin(declination)
+ math.cos(lat) * math.cos(declination) * math.cos(math.rad((solar_hour - 12) * 15))
local elevation = math.deg(math.asin(M.clamp(sin_elevation, -1, 1)))
-- Adapt the engine's fixed equinox orbit so its daylight matches this latitude.
local engine_time
if solar_hour <= 12 then engine_time = (elevation + 90) / 360
else engine_time = (270 - elevation) / 360 end
local south = latitude < 0 and 1 or 0
local index = (math.floor((t.month % 12) / 3) + south * 2) % 4 + 1
local season = ({"Winter", "Spring", "Summer", "Autumn"})[index]
local annual = math.cos(2 * math.pi * (t.yday - 205) / days) * (south == 1 and -1 or 1)
local mean = 27 - math.abs(latitude) * 0.43
local amplitude = 3 + math.abs(latitude) * 0.22
return {hour = solar_hour, timeofday = engine_time, elevation = elevation,
daylight = elevation > -0.833, season = season,
temperature = mean + annual * amplitude + math.sin(math.rad(elevation)) * 4,
date = os.date("!%Y-%m-%d %H:%M UTC", timestamp)}
end
function M.weather(timestamp, latitude, longitude)
-- UTC-indexed deterministic six-hour fronts: restart/unloading do not reroll.
local front = math.floor(timestamp / 21600)
local wave = math.sin(front * 12.9898 + latitude * 0.1 + longitude * 0.03) * 43758.5453
return wave - math.floor(wave) > 0.68 and "rain" or "clear"
end
function M.needs(state, env, dt)
dt = M.clamp(dt, 0, 2) -- stalled frames never replay lethal elapsed time
local s = {}
for k, v in pairs(state) do s[k] = v end
local shelter = M.clamp(env.shelter or 0, 0, 1)
local exposed = env.rain and shelter < 0.6
local wet_rate = exposed and (env.clothing and 0.35 or 1.2)
or (env.drying and -1.5 or -0.35)
s.wet = M.clamp(s.wet + wet_rate * dt, 0, 100)
local feels = env.temperature - s.wet * 0.09 + (env.heat or 0) * 14 + shelter * 5
+ (env.clothing and 4 or 0)
local target = M.clamp(60 + feels * 1.5, 0, 100)
s.warmth = M.clamp(s.warmth + M.clamp(target - s.warmth, -0.16, 0.3) * dt, 0, 100)
s.food = M.clamp(s.food - (env.sprinting and 0.035 or 0.012) * dt, 0, 100)
s.water = M.clamp(s.water - (env.sprinting and 0.06 or 0.025) * dt, 0, 100)
s.stamina = M.clamp(s.stamina + (env.sprinting and -13 or
(s.food > 0 and s.water > 0 and 9 or 2)) * dt, 0, 100)
return s, (s.warmth < 8 or s.food <= 0 or s.water <= 0) and dt or 0
end
function M.fuel_remaining(deadline, now)
return M.clamp(deadline - now, 0, 600)
end
return M