Module:BassaridiaForecast
From MicrasWiki
Documentation for this module may be created at Module:BassaridiaForecast/doc
---------------------------------------------------------
-- Module: BassaridiaForecast
-- Provides three functions:
-- 1) weatherForecast(frame) -> returns a single big table
-- for ALL cities (with daily-random weather).
-- 2) weatherForCity(frame) -> returns a single small table
-- for a SPECIFIC city (param "city").
-- 3) starsForTonight(frame) -> returns a table of star
-- "rise" and "set" times, based on season-based
-- night length (very simplified).
---------------------------------------------------------
local p = {}
---------------------------------------------------------
-- HELPER #1: Current Date Info for PSSC
-- Also used to create a daily random seed.
---------------------------------------------------------
local function getCurrentDateInfo()
local startDate = os.time({year = 1999, month = 8, day = 6})
local secondsInDay = 86400
local daysPerYear = 183
local currentTime = os.time()
local totalDaysElapsed = math.floor((currentTime - startDate) / secondsInDay)
local yearFraction = totalDaysElapsed / daysPerYear
local psscYear = math.floor(yearFraction)
local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1
return {
psscYear = psscYear,
dayOfYear = dayOfYear
}
end
---------------------------------------------------------
-- HELPER #2: Determine Season
-- (Atosiel=1..61, Thalassiel=62..122, Opsitheiel=123..183)
---------------------------------------------------------
local function getSeasonName(dayOfYear)
if dayOfYear <= 61 then
return "Atosiel"
elseif dayOfYear <= 122 then
return "Thalassiel"
else
return "Opsitheiel"
end
end
---------------------------------------------------------
-- 3. climateTemperature (°C behind scenes, displayed °F)
---------------------------------------------------------
local climateTemperature = {
["Humid Subtropical"] = {
Atosiel = { hiMin=18, hiMax=26, loMin=10, loMax=16, humMin=60, humMax=80 },
Thalassiel = { hiMin=25, hiMax=34, loMin=19, loMax=24, humMin=65, humMax=90 },
Opsitheiel = { hiMin=20, hiMax=28, loMin=12, loMax=18, humMin=50, humMax=75 }
},
["Oceanic"] = {
Atosiel = { hiMin=10, hiMax=17, loMin=5, loMax=12, humMin=70, humMax=90 },
Thalassiel = { hiMin=14, hiMax=21, loMin=9, loMax=14, humMin=65, humMax=85 },
Opsitheiel = { hiMin=5, hiMax=12, loMin=1, loMax=6, humMin=70, humMax=90 }
},
["Subpolar Oceanic"] = {
Atosiel = { hiMin=3, hiMax=9, loMin=-2, loMax=3, humMin=70, humMax=95 },
Thalassiel = { hiMin=6, hiMax=12, loMin=1, loMax=6, humMin=70, humMax=90 },
Opsitheiel = { hiMin=-1, hiMax=4, loMin=-6, loMax=-1, humMin=75, humMax=95 }
},
["Mediterranean (Hot Summer)"] = {
Atosiel = { hiMin=15, hiMax=21, loMin=7, loMax=12, humMin=40, humMax=60 },
Thalassiel = { hiMin=25, hiMax=35, loMin=15, loMax=20, humMin=30, humMax=50 },
Opsitheiel = { hiMin=12, hiMax=18, loMin=5, loMax=10, humMin=45, humMax=65 }
},
["Hot Desert"] = {
Atosiel = { hiMin=25, hiMax=35, loMin=12, loMax=18, humMin=10, humMax=30 },
Thalassiel = { hiMin=35, hiMax=45, loMin=25, loMax=30, humMin=5, humMax=20 },
Opsitheiel = { hiMin=22, hiMax=30, loMin=10, loMax=16, humMin=5, humMax=25 }
},
["Cold Steppe"] = {
Atosiel = { hiMin=10, hiMax=18, loMin=2, loMax=8, humMin=30, humMax=50 },
Thalassiel = { hiMin=20, hiMax=28, loMin=10, loMax=15, humMin=25, humMax=45 },
Opsitheiel = { hiMin=0, hiMax=7, loMin=-6, loMax=0, humMin=35, humMax=55 }
},
["Hot Steppe"] = {
Atosiel = { hiMin=23, hiMax=30, loMin=12, loMax=18, humMin=20, humMax=40 },
Thalassiel = { hiMin=30, hiMax=40, loMin=20, loMax=25, humMin=15, humMax=35 },
Opsitheiel = { hiMin=25, hiMax=33, loMin=15, loMax=20, humMin=15, humMax=40 }
},
["Subarctic"] = {
Atosiel = { hiMin=0, hiMax=8, loMin=-10,loMax=-1, humMin=50, humMax=80 },
Thalassiel = { hiMin=5, hiMax=15, loMin=-1, loMax=5, humMin=50, humMax=85 },
Opsitheiel = { hiMin=-5, hiMax=0, loMin=-15,loMax=-5, humMin=60, humMax=90 }
}
}
---------------------------------------------------------
-- 4. Weather Events by Climate & Season
-- (Unchanged from your original code)
---------------------------------------------------------
local climateEvents = {
-- [Truncated for brevity—include your full event lists here!]
-- ...
}
---------------------------------------------------------
-- 5. Disasters: random chance if text has keywords
---------------------------------------------------------
local triggeredDisasters = {
{ keywords = {"seasonal storms","lake%-driven floods"}, hazard = "Seasonal storms and lake-driven floods" },
{ keywords = {"river floods","heavy rains"}, hazard = "River floods during heavy rains" },
{ keywords = {"snowstorms","higher elevations"}, hazard = "Snowstorms at higher elevations" },
{ keywords = {"steam vent eruptions"}, hazard = "Occasional steam vent eruptions" },
-- ... (include ALL from your table)
}
local function getAdvisory(forecastStr)
local textLower = forecastStr:lower()
local extras = {}
local randomChance = 0.40 -- 40% chance
for _, item in ipairs(triggeredDisasters) do
for _, kw in ipairs(item.keywords) do
if textLower:find(kw) then
local roll = math.random()
if roll <= randomChance then
table.insert(extras, item.hazard)
end
break
end
end
end
if #extras > 0 then
return table.concat(extras, "; ")
else
return "No reports"
end
end
---------------------------------------------------------
-- 6. Conversions and Random Stats (unchanged)
---------------------------------------------------------
local function cToF(c)
if type(c) == "number" then
return math.floor(c * 9/5 + 32 + 0.5)
end
return c
end
local windDirections = {"N","NE","E","SE","S","SW","W","NW"}
local function getRandomWeatherStats(climate, season)
local data = climateTemperature[climate] and climateTemperature[climate][season]
if not data then
return {
high = "N/A",
low = "N/A",
humidity = "N/A",
chanceOfRain = "N/A",
windDir = "N/A",
windSpeed = "N/A"
}
end
local hi = math.random(data.hiMin, data.hiMax)
local lo = math.random(data.loMin, data.loMax)
local hum = math.random(data.humMin, data.humMax)
local cRain= math.random(0,100)
local wDir = windDirections[math.random(#windDirections)]
local wSpd = math.random(0,50)
return {
high = hi,
low = lo,
humidity = hum,
chanceOfRain= cRain,
windDir = wDir,
windSpeed = wSpd
}
end
---------------------------------------------------------
-- 7. All City Data (Now Linked to Wiki)
---------------------------------------------------------
-- We'll assume the user wants a link to micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#CityName
-- so we do e.g.: "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Vaeringheim Vaeringheim]"
---------------------------------------------------------
local function cityLink(cityName)
-- We'll remove spaces or handle them if needed.
-- If cityName has a space, replace with underscore, or just keep #CityName?
-- If the anchor is "cityName", we might need an anchor that matches.
-- We'll assume your wiki anchor is "#Vaeringheim" etc.
local anchor = cityName:gsub(" ", "_")
local baseUrl = "https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#"
return string.format("[[%s%s|%s]]", baseUrl, anchor, cityName)
end
local cityData = {
{city = "Vaeringheim", climate = "Humid Subtropical"},
{city = "Luminaria", climate = "Oceanic"},
{city = "Serena", climate = "Subpolar Oceanic"},
{city = "Pyralis", climate = "Oceanic"},
{city = "Symphonara", climate = "Oceanic"},
{city = "Aurelia", climate = "Mediterranean (Hot Summer)"},
{city = "Somniumpolis", climate = "Humid Subtropical"},
{city = "Nexa", climate = "Oceanic"},
{city = "Lunalis Sancta", climate = "Oceanic"},
{city = "Sylvapolis", climate = "Humid Subtropical"},
{city = "Saluria", climate = "Oceanic"},
{city = "Aetherium", climate = "Subarctic"},
{city = "Ferrum Citadel", climate = "Hot Desert"},
{city = "Acheron", climate = "Cold Steppe"},
{city = "Erythros", climate = "Oceanic"},
{city = "Catonis Atrium", climate = "Oceanic"},
{city = "Delphica", climate = "Oceanic"},
{city = "Koinonía", climate = "Oceanic"},
{city = "Aureum", climate = "Mediterranean (Hot Summer)"},
{city = "Skýrophos", climate = "Oceanic"},
{city = "Bjornopolis", climate = "Oceanic"},
{city = "Aegirheim", climate = "Subarctic"},
{city = "Norsolyra", climate = "Oceanic"},
{city = "Thorsalon", climate = "Oceanic"},
{city = "Pelagia", climate = "Hot Steppe"},
{city = "Myrene", climate = "Oceanic"},
{city = "Thyrea", climate = "Humid Subtropical"},
{city = "Ephyra", climate = "Subpolar Oceanic"},
{city = "Halicarn", climate = "Mediterranean (Hot Summer)"},
{city = "Keybir-Aviv", climate = "Humid Subtropical"},
{city = "Tel-Amin", climate = "Mediterranean (Hot Summer)"},
{city = "Diamandis", climate = "Mediterranean (Hot Summer)"},
{city = "Jogi", climate = "Oceanic"},
{city = "Lewisburg", climate = "Humid Subtropical"},
{city = "Thermosalem", climate = "Oceanic"},
{city = "Akróstadium", climate = "Cold Steppe"},
{city = "Sufriya", climate = "Humid Subtropical"},
{city = "Lykopolis", climate = "Oceanic"}
}
---------------------------------------------------------
-- 8. Color-coded cell background
---------------------------------------------------------
local function getEventColor(eventText)
local textLower = eventText:lower()
if textLower:find("thunder") or textLower:find("storm") then
return "#FFD2D2"
elseif textLower:find("snow") or textLower:find("sleet") or textLower:find("flurries") then
return "#D2ECFF"
elseif textLower:find("rain") or textLower:find("drizzle") or textLower:find("downpour") then
return "#D2DFFF"
elseif textLower:find("dust") or textLower:find("desert") then
return "#FFFACD"
else
return "#F8F8F8"
end
end
---------------------------------------------------------
-- 9A: ALL-CITIES FORECAST FUNCTION
---------------------------------------------------------
function p.weatherForecast(frame)
local date = os.date("*t")
local dailySeed = (date.year * 1000) + date.yday
math.randomseed(dailySeed)
local dateInfo = getCurrentDateInfo()
local dayOfYear = dateInfo.dayOfYear
local yearNumber = dateInfo.psscYear
local seasonName = getSeasonName(dayOfYear)
local out = {}
table.insert(out, "== Daily Weather Forecast ==\n")
table.insert(out,
string.format("''(Day %d of Year %d PSSC, %s)''\n\n",
dayOfYear, yearNumber, seasonName
)
)
table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n')
table.insert(out, [[
! City
! Climate
! Season
! High °F
! Low °F
! Humidity (%)
! Chance of Rain (%)
! Wind Direction
! Wind Speed (km/h)
! Today's Weather
! Natural Disaster Advisory
]])
for _, entry in ipairs(cityData) do
local cityName = entry.city
local climateName = entry.climate
local climateTbl = climateEvents[climateName]
local dateInfo = getCurrentDateInfo()
local dayOfYear = dateInfo.dayOfYear
local seasonName = getSeasonName(dayOfYear)
local seasonTbl = climateTbl and climateTbl[seasonName]
local fStr = "No data"
if seasonTbl and #seasonTbl > 0 then
local idx = math.random(#seasonTbl)
fStr = seasonTbl[idx]
end
local rowColor = getEventColor(fStr)
local stats = getRandomWeatherStats(climateName, seasonName)
local hiF = cToF(stats.high)
local loF = cToF(stats.low)
local hum = stats.humidity
local cRain = stats.chanceOfRain
local wDir = stats.windDir
local wSpd = stats.windSpeed
local advisory = getAdvisory(fStr)
table.insert(out, "|-\n")
table.insert(out, string.format(
[[
| %s
| %s
| %s
| %d
| %d
| %d
| %d
| %s
| %d
| style="background-color:%s" | %s
| %s
]],
cityLink(cityName), -- linked version
climateName,
seasonName,
hiF,
loF,
hum,
cRain,
wDir,
wSpd,
rowColor,
fStr,
advisory
))
end
table.insert(out, "|}\n")
return table.concat(out)
end
---------------------------------------------------------
-- 9B: SINGLE-CITY FORECAST
---------------------------------------------------------
function p.weatherForCity(frame)
local cityRequested = frame.args.city
if not cityRequested or cityRequested == "" then
return "Error: Please specify a city. E.g. {{#invoke:BassaridiaForecast|weatherForCity|city=Vaeringheim}}"
end
local date = os.date("*t")
local dailySeed = (date.year * 1000) + date.yday
math.randomseed(dailySeed)
local dateInfo = getCurrentDateInfo()
local dayOfYear = dateInfo.dayOfYear
local yearNumber = dateInfo.psscYear
local seasonName = getSeasonName(dayOfYear)
local foundEntry = nil
for _, entry in ipairs(cityData) do
if entry.city == cityRequested then
foundEntry = entry
break
end
end
if not foundEntry then
return "Error: City '" .. cityRequested .. "' not found in cityData."
end
local cityName = foundEntry.city
local climateName = foundEntry.climate
local climateTbl = climateEvents[climateName]
local seasonTbl = climateTbl and climateTbl[seasonName]
local fStr = "No data"
if seasonTbl and #seasonTbl > 0 then
local idx = math.random(#seasonTbl)
fStr = seasonTbl[idx]
end
local rowColor = getEventColor(fStr)
local stats = getRandomWeatherStats(climateName, seasonName)
local hiF = cToF(stats.high)
local loF = cToF(stats.low)
local hum = stats.humidity
local cRain = stats.chanceOfRain
local wDir = stats.windDir
local wSpd = stats.windSpeed
local advisory = getAdvisory(fStr)
local out = {}
table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n')
table.insert(out, [[
! Climate
! Season
! High °F
! Low °F
! Humidity (%)
! Chance of Rain (%)
! Wind Dir
! Wind Speed (km/h)
! style="width:20em;" | Today's Weather
! Natural Disaster Advisory
]])
table.insert(out, "|-\n")
local row = string.format(
[[
| %s
| %s
| %d
| %d
| %d
| %d
| %s
| %d
| style="background-color:%s" | %s
| %s
]],
climateName,
seasonName,
hiF,
loF,
hum,
cRain,
wDir,
wSpd,
rowColor,
fStr,
advisory
)
table.insert(out, row)
table.insert(out, "|}\n\n")
return table.concat(out)
end
---------------------------------------------------------
-- 10. Host Stars Logic (Top Image) – "starsForTonight"
---------------------------------------------------------
-- Step A: star data (northern hemisphere from your "top image")
local starDataNorth = {
{name="Agave", starClass="M", latitude=29.5},
{name="Amaäz", starClass="K", latitude=31},
{name="Amáenu", starClass="G", latitude=45},
{name="Amap", starClass="A", latitude=55},
{name="Amazä", starClass="F", latitude=39},
{name="Atämios", starClass="F", latitude=56},
{name="Aprobelle",starClass="A", latitude=59},
{name="Azos", starClass="Galaxy", latitude=60}, -- approx
{name="Bebeakaus",starClass="K", latitude=30.5},
{name="Bulhanu", starClass="A", latitude=40},
{name="Crösacío", starClass="K", latitude=17},
{name="Danaß", starClass="A", latitude=67},
{name="Dilëtaz", starClass="G", latitude=36},
{name="Dranamos", starClass="A", latitude=58},
{name="Gaht", starClass="G", latitude=29},
{name="Häpi", starClass="F", latitude=78},
{name="Hazaméos", starClass="A", latitude=72},
{name="Liléigos", starClass="F", latitude=18},
{name="Nyama", starClass="F", latitude=64},
{name="Ocananus", starClass="F", latitude=60.5},
{name="Orebele", starClass="F", latitude=22},
{name="Osiríos", starClass="G", latitude=53},
{name="Pythe", starClass="A", latitude=45},
{name="Sanashalo",starClass="G", latitude=27},
{name="Tä", starClass="F", latitude=39.5},
{name="Vï", starClass="A", latitude=46},
{name="Wedíos", starClass="A", latitude=54},
}
-- Step B: how many hours of darkness in each season (rough)
local seasonNightLength = {
Atosiel = 10, -- 10 hours darkness
Thalassiel = 12, -- 12 hours darkness
Opsitheiel = 14, -- 14 hours darkness
}
-- Helper: convert decimal hours to "HH:MM" string
local function hourToHM(decimalHour)
local hh = math.floor(decimalHour)
local mm = math.floor((decimalHour - hh)*60 + 0.5)
-- handle wrap-around if hh >= 24
hh = hh % 24
return string.format("%02d:%02d", hh, mm)
end
function p.starsForTonight(frame)
local date = os.date("*t")
local dailySeed = (date.year * 1000) + date.yday
math.randomseed(dailySeed)
local dateInfo = getCurrentDateInfo()
local seasonName = getSeasonName(dateInfo.dayOfYear)
local nightLen = seasonNightLength[seasonName] or 12
local nightStart = 19.0 -- 19:00 local
local queryTime = 18.5 -- 18:30 local time
local out = {}
table.insert(out, "=== Host Stars Visible Tonight (Top Image) ===\n")
table.insert(out, string.format("''Time ~6:30 PM, Bassaridia Vaeringheim – Season: %s''\n\n", seasonName))
table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n')
table.insert(out, "! Star Name || Star Class || Approx. Latitude || Rise Time || Set Time || Visible at 18:30?\n")
for _, star in ipairs(starDataNorth) do
-- pick a random time for starRise
local starRise = nightStart + math.random() * (nightLen * 0.3)
local starSet = starRise + math.random() * (nightLen * 0.6 + 1.0)
-- clamp starSet so it doesn't exceed next morning
local maxEnd = nightStart + nightLen + 2.0 -- e.g. by 9:00 next day
if starSet > maxEnd then
starSet = maxEnd
end
local riseStr = hourToHM(starRise)
local setStr = hourToHM(starSet)
local isVis
if (starRise <= queryTime) and (queryTime < starSet) then
isVis = "Yes"
else
isVis = "No"
end
table.insert(out, "|-\n")
table.insert(out, string.format(
"| %s || %s || %.1f°N || %s || %s || %s\n",
star.name, star.starClass, star.latitude, riseStr, setStr, isVis
))
end
table.insert(out, "|}\n\n")
return table.concat(out)
end
---------------------------------------------------------
-- Return the module table
---------------------------------------------------------
return p