77 lines
3.7 KiB
Lua
Executable File
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
|