Module:BassaridiaForecast

From MicrasWiki
Revision as of 06:39, 27 December 2024 by NewZimiaGov (talk | contribs)
Jump to navigationJump to search

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