import { getMoonIllumination, getMoonTimes, getTimes } from "./suncalc.js";
import { lineLoader } from "./data_loaders.js";
import { logger } from "./utils.js";
import Weather from "./models/weather.js";
// Weather data is a CSV export of 2020-2023 data from
// https://open-meteo.com/en/docs/historical-weather-api?daily=weather_code%2Cwind_speed_10m_max%2Cwind_gusts_10m_max%2Csnowfall_sum%2Crain_sum%2Capparent_temperature_max%2Capparent_temperature_min&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&start_date=2023-01-01&end_date=2023-12-31&bounding_box=-90%2C-180%2C90%2C180&hourly&latitude=35.4025&longitude=-99.4462&timezone=America%2FDenver#hourly_weather_variables
// The link encodes the fields and their order: weather code, max apparent temperature, min apparent temperature, max wind
// speed, max wind gusts, rain sum, snowfall sum. The query parameters may be dictating the order which is not the order given
// here (see the nameArray function below)
const ONE_YEAR_DATA = await lineLoader("weather.data");
// somewhere in Oklahoma, because the weather is more dramatic there than to the west
const LAT = 35.4025;
const LONG = -99.4462;
// we don't see most of these occurring in the weather from this station
const WMO_CODES = {
0: "Clear skies",
1: "Just a few clouds in the sky",
2: "Clear skies",
3: "Clouds in the sky",
4: "Visibility reduced by fire smoke",
5: "Haze",
6: "Widespread dust in the air",
7: "Dust or sand raised by wind",
8: "Well developed dust whirls",
9: "Duststorms in the distance",
10: "Mist",
11: "Patchy fog in the morning",
12: "Patchy fog",
13: "Lightning visible, no thunder heard",
14: "Precipitation within sight in the sky, not reaching the ground",
15: "Precipitation within sight, reaching the ground in the distance",
16: "Precipitation within sight, reaching the ground near location",
17: "Thunderstorm, but no rain",
18: "Squalls",
19: "Tornadoes",
20: "Drizzle (not freezing) or snow grains not falling as shower(s)",
21: "Rain (not freezing) not falling as shower(s)",
22: "Snow not falling as shower(s)",
23: "Rain and snow or ice pellets not falling as shower(s)",
24: "Freezing drizzle or freezing rain not falling as shower(s)",
25: "Shower(s) of rain",
26: "Shower(s) of snow, or of rain and snow",
27: "Shower(s) of hail*, or of rain and hail",
28: "Fog or ice fog",
29: "Thunderstorm (with or without precipitation)",
30: "Slight or moderate duststorm or sandstorm - has decreased during the preceding hour",
31: "Slight or moderate duststorm or sandstorm - no appreciable change during the preceding hour",
32: "Slight or moderate duststorm or sandstorm - has begun or has increased during the preceding hour",
33: "Severe duststorm or sandstorm - has decreased during the preceding hour",
34: "Severe duststorm or sandstorm- no appreciable change during the preceding hour",
35: "Severe duststorm or sandstorm- has begun or has increased during the preceding hour",
36: "Slight or moderate blowing snow generally low (below eye level)",
37: "Heavy drifting snow generally low (below eye level)",
38: "Slight or moderate blowing snow generally high (above eye level)",
39: "Heavy drifting snow generally high (above eye level)",
40: "Fog or ice fog at a distance at the time of observation, but not at the station during the preceding hour, the fog or ice fog extending to a level above that of the observer",
41: "Fog or ice fog in patches",
42: "Fog or ice fog, sky visible, has become thinner during the preceding hour",
43: "Fog or ice fog, sky invisible, has become thinner during the preceding hour",
44: "Fog or ice fog, sky visible, no appreciable change during the preceding hour",
45: "Fog or ice fog, sky invisible, no appreciable change during the preceding hour",
46: "Fog or ice fog, sky visible, has begun or has become thicker during the preceding hour",
47: "Fog or ice fog, sky invisible, has begun or has become thicker during the preceding hour",
48: "Fog, depositing rime, sky visible",
49: "Fog, depositing rime, sky invisible",
50: "Light drizzle",
51: "Light drizzle",
52: "Moderate drizzle",
53: "Moderate drizzle",
54: "Heavy drizzle",
55: "Heavy drizzle",
56: "Light freezing drizzle",
57: "Freezing drizzle",
58: "Light drizzle and rain",
59: "Drizzle and rain",
60: "Light rain",
61: "Light rain",
62: "Moderate rain",
63: "Moderate rain",
64: "Heavy rain",
65: "Heavy rain",
66: "Slight freezing rain",
67: "Freezing rain",
68: "Rain or drizzle and snow, slight",
69: "Rain or drizzle and snow, moderate or heavy",
70: "Light snowfall",
71: "Light snowfall",
72: "Moderate snowfall",
73: "Moderate snowfall",
74: "Heavy snowfall",
75: "Heavy snowfall",
76: "Diamond dust (with or without fog)",
77: "Snow grains (with or without fog)",
78: "Isolated star-like snow crystals (with or without fog)",
79: "Ice pellets",
80: "Rain shower(s), slight",
81: "Rain shower(s), moderate or heavy",
82: "Rain shower(s), violent",
83: "Shower(s) of rain and snow mixed, slight",
84: "Shower(s) of rain and snow mixed, moderate or heavy",
85: "Snow shower(s), slight",
86: "Snow shower(s), moderate or heavy",
87: "Shower(s) of snow pellets or small hail, with or without rain or rain and snow mixed - slight",
88: "Shower(s) of snow pellets or small hail, with or without rain or rain and snow mixed - moderate or heavy",
89: "Shower(s) of hail*, with or without rain or rain and snow mixed, not associated with thunder - slight",
90: "Shower(s) of hail*, with or without rain or rain and snow mixed, not associated with thunder - moderate or heavy",
91: "Slight rain at time of observation, Thunderstorm during the preceding hour but not at time of observation",
92: "Moderate or heavy rain at time of observation",
93: "Slight snow, or rain and snow mixed or hail** at time of observation",
94: "Moderate or heavy snow, or rain and snow mixed or hail** at time of observation",
95: "Thunderstorm, slight or moderate, without hail** but with rain and/or snow at time of observation Thunderstorm at time of observation",
96: "Thunderstorm, slight or moderate, with hail** at time of observation",
97: "Thunderstorm, heavy, without hail** but with rain and/or snow at time of observation",
98: "Thunderstorm combined with duststorm or sandstorm at time of observation",
99: "Thunderstorm, heavy, with hail** at time of observation",
};
const MOONS = [
{ icon: "🌑", text: "new moon", light: "dark" },
{ icon: "🌒", text: "waxing crescent moon", light: "dark" },
{ icon: "🌓", text: "first quarter moon", light: "dim light" },
{ icon: "🌔", text: "waxing gibbous moon", light: "light" },
{ icon: "🌕", text: "full moon", light: "light" },
{ icon: "🌖", text: "waning gibbous moon", light: "light" },
{ icon: "🌗", text: "last quarter moon", light: "dim light" },
{ icon: "🌘", text: "waning crescent moon", light: "dark" },
];
function toMoon(date) {
const phase = getMoonIllumination(date).phase;
return MOONS[Math.round(7 * phase)];
}
const WEATHER_HISTORY = new Map();
ONE_YEAR_DATA.map((line) => line.split(","))
.map(nameArray)
.forEach((obj) => WEATHER_HISTORY.set(obj.time, obj));
function nameArray(arr) {
return {
time: arr[0],
sky: WMO_CODES[arr[1]] + " today",
tempMin: Math.round(parseFloat(arr[7])),
tempMax: Math.round(parseFloat(arr[6])),
windMax: Math.round(parseFloat(arr[2])),
windGusts: Math.round(parseFloat(arr[3])),
snowfall: parseFloat(arr[4]).toFixed(1),
rain: parseFloat(arr[5]).toFixed(1),
};
}
const FORMATTER = new Intl.DateTimeFormat("en-US", {
timeZone: "America/Denver",
hour: "numeric",
minute: "2-digit",
hour12: false,
});
function getLocalTime(now) {
const parts = FORMATTER.formatToParts(now);
const hour = parts[0].value.replace(/^0/, "");
return `${hour}:${parts[2].value}`;
}
function boundedBySunset(sunset, moonrise) {
return !moonrise || moonrise.getTime() < sunset.getTime() ? sunset : moonrise;
}
function boundedBySunrise(sunrise, moonset) {
return !moonset || moonset.getTime() > sunrise.getTime() ? sunrise : moonset;
}
/**
* Creates a weather report for a given date and location, including temperature, wind,
* precipitation, sun/moon times, and game-relevant effects (cold/heat warnings, travel impacts).
*
* @param {Object} [options={}] - Weather creation options
* @param {string} [options.date] - ISO 8601 date string (e.g. "2026-03-10"). If not provided,
* defaults to today's date.
* @param {number} [options.lat=38.24475] - Latitude for sun/moon calculations.
* @param {number} [options.long=-98.657227] - Longitude for sun/moon calculations.
* @returns {Weather} A Weather instance with temperature, wind, precipitation, sky conditions,
* sun times, moon phase, and gameplay effect warnings.
*/
export function createWeather({ date, lat = LAT, long = LONG } = {}) {
logger.start("createWeather", { date, lat, long });
const dateOnly = date ? date : new Date().toISOString().split("T")[0]; // 2026-01-02
const yearNum = parseInt(dateOnly.split("-")[0].substring(3)) % 4; // 0, 1, 2, 3
const day = dateOnly.substring(5) !== "02-29" ? dateOnly.substring(5) : "02-28"; // 01-02
const astroTime = new Date(`199${yearNum}-${day}T12:00:00.000-06:00`);
const times = getTimes(astroTime, lat, long);
const moonTimes = getMoonTimes(astroTime, lat, long);
const dawn = getLocalTime(times.dawn);
const sunrise = getLocalTime(times.sunrise);
const sunset = getLocalTime(times.sunset);
const dusk = getLocalTime(times.dusk);
const moon = toMoon(astroTime);
const moonRise = getLocalTime(boundedBySunset(times.dusk, moonTimes.rise));
const moonSet = getLocalTime(boundedBySunrise(times.dawn, moonTimes.set));
const weather = WEATHER_HISTORY.get(`${yearNum}-${day}`);
return logger.end(
new Weather({
...weather,
dawn,
sunrise,
sunset,
dusk,
moonPhase: moon.icon,
moonText: moon.text,
moonLight: moon.light,
moonRise,
moonSet,
}),
);
}