Module:BassaridiaForecast

From MicrasWiki
Revision as of 19:37, 2 January 2025 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
--  2) weatherForCity(frame)   -> single table for a city
--  3) starsForTonight(frame)  -> example star-visibility
--
-- All cells are re-randomized once per day
-- by seeding math.random with (year*1000 + dayOfYear).
---------------------------------------------------------

local p = {}

---------------------------------------------------------
-- 1. Determine Today's Date 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 totalDays   = math.floor((currentTime - startDate) / secondsInDay)

    local fractionYear= totalDays / daysPerYear
    local psscYear    = math.floor(fractionYear)
    local dayOfYear   = math.floor((fractionYear - psscYear) * daysPerYear) + 1

    return {
        psscYear = psscYear,
        dayOfYear= dayOfYear
    }
end

---------------------------------------------------------
-- 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 = {
    -- (unchanged climate data)
    ["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 event text)
---------------------------------------------------------
local climateEvents = {
    -- (unchanged big table of strings)
    ["Humid Subtropical"] = {
        Atosiel = {
            "Morning drizzle and warm afternoon sunshine",
            "Mild thunderstorm building by midday",
            "Patchy fog at dawn, clearing toward lunch",
            "Light rain with sunny breaks after noon",
            "Gentle breezes, blossoming warmth, low humidity",
            "Scattered clouds with a brief shower by dusk",
            "Humid sunrise, comfortable high around midday",
            "Overcast for part of the day, mild temperatures",
            "Warm breezes carrying faint floral scents",
            "Partial sun, warm with a slight chance of rain"
        },
        Thalassiel = {
            "Hot, steamy day with intense midday heat",
            "Tropical-like humidity, afternoon thunder possible",
            "Intermittent heavy downpours, muggy evenings",
            "High humidity and patchy thunderstorms late",
            "Sun-scorched morning, scattered storms by dusk",
            "Hazy sunshine, extremely warm all day",
            "Thick humidity, chance of lightning late evening",
            "Sticky air, short downpours in isolated spots",
            "Heat advisory with only brief cooling at night",
            "Nighttime storms lingering into early morning"
        },
        Opsitheiel = {
            "Warm daytime, gentle evening breezes",
            "Occasional rain, otherwise mild temperatures",
            "Late-season warmth, scattered rain after sunset",
            "Cooler mornings, returning to warmth by midday",
            "Sparse cloud cover, tranquil weather overall",
            "Fog at dawn, warm midday, pleasant night",
            "Partial sun, comfortable humidity levels",
            "Evening drizzle with mild breezes",
            "Patchy haze, moderate warmth throughout the day",
            "Short-lived shower, then clearing skies"
        }
    },
    ["Oceanic"] = {
        -- ... etc ...
    },
    -- (rest of climateEvents unchanged)
}

---------------------------------------------------------
-- 5. triggeredDisasters, getAdvisory (unchanged)
---------------------------------------------------------
local triggeredDisasters = {
    -- (unchanged hazard keywords)
}

local function getAdvisory(forecastStr)
    local textLower = forecastStr:lower()
    local extras = {}

    local randomChance = 0.40
    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

---------------------------------------------------------
-- 7. Conversions and Random Stats
---------------------------------------------------------
local function cToF(c)
    if type(c) == "number" then
        return math.floor(c * 9/5 + 32 + 0.5)
    end
    return c
end

-- Updated function: narrower typical wind speeds (1..25 km/h).
-- We'll adjust the code so we can later override chanceOfRain if precipitation is mentioned.
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)  -- default random chance of rain
    local wDir = windDirections[math.random(#windDirections)]
    local wSpd = math.random(1,25)   -- narrower range for more realistic wind speeds

    return {
        high        = hi,
        low         = lo,
        humidity    = hum,
        chanceOfRain= cRain,
        windDir     = wDir,
        windSpeed   = wSpd
    }
end

---------------------------------------------------------
-- 8. City Data (unchanged)
---------------------------------------------------------
local cityData = {
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Vaeringheim Vaeringheim]",       climate = "Humid Subtropical"},
    -- (rest of the city list unchanged)
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Lykopolis Lykopolis]",         climate = "Oceanic"}
}

---------------------------------------------------------
-- 9. Color-coded cell background (unchanged)
---------------------------------------------------------
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

---------------------------------------------------------
-- 10A. ALL-CITIES FORECAST FUNCTION (modified to enforce higher chance of rain if precipitation is detected)
---------------------------------------------------------
function p.weatherForecast(frame)
    -- Random seed changes daily
    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 cityLink    = entry.city
        local climateName = entry.climate

        -- 1) Random weather event
        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

        -- 2) Stats
        local stats = getRandomWeatherStats(climateName, seasonName)

        -- If the daily weather text has precipitation words, override chanceOfRain with a high value:
        local forecastLower = fStr:lower()
        if forecastLower:find("rain")
           or forecastLower:find("drizzle")
           or forecastLower:find("downpour")
           or forecastLower:find("shower")
           or forecastLower:find("snow")
           or forecastLower:find("storm")
           or forecastLower:find("thunder")
           or forecastLower:find("sleet")
        then
            stats.chanceOfRain = math.random(70, 99)
        end

        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 rowColor = getEventColor(fStr)
        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, climateName, seasonName,
            hiF, loF, hum, cRain, wDir, wSpd,
            rowColor, fStr, advisory
        ))
    end

    table.insert(out, "|}\n")
    return table.concat(out)
end

---------------------------------------------------------
-- 10B. SINGLE-CITY FORECAST FUNCTION (similarly updated)
---------------------------------------------------------
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

    -- daily seed
    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
        local plainCity = entry.city:match("%s+([A-Za-zÀ-ž%-]+)%]?$")
        if entry.city == cityRequested or plainCity == cityRequested then
            foundEntry = entry
            break
        end
    end

    if not foundEntry then
        return "Error: City '" .. cityRequested .. "' not found in cityData."
    end

    local cityLink   = 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 stats = getRandomWeatherStats(climateName, seasonName)

    -- Override chance of rain if we detect precipitation in fStr
    local forecastLower = fStr:lower()
    if forecastLower:find("rain")
       or forecastLower:find("drizzle")
       or forecastLower:find("downpour")
       or forecastLower:find("shower")
       or forecastLower:find("snow")
       or forecastLower:find("storm")
       or forecastLower:find("thunder")
       or forecastLower:find("sleet")
    then
        stats.chanceOfRain = math.random(70, 99)
    end

    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 rowColor = getEventColor(fStr)
    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

---------------------------------------------------------
-- 10C. STARS FOR TONIGHT (unchanged)
---------------------------------------------------------
local starChartData = {
    -- (unchanged star data)
}

local observerLat = 49 -- Vaeringheim ~49°N

local function isVisibleAt630(lat)
    local offset = math.random(5,55)
    return (lat < (observerLat + offset))
end

function p.starsForTonight(frame)
    local date  = os.date("*t")
    local dailySeed = (date.year * 1000) + date.yday + 777
    math.randomseed(dailySeed)

    local out = {}
    table.insert(out, "=== Northern Host Stars Visible at ~6:30 PM ===\n\n")

    table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n')
    table.insert(out, "! Star !! Class !! Star-Lat !! Visible? !! Approx. Altitude\n")

    for _, star in ipairs(starChartData) do
        local yesNo = "No"
        local alt   = 0
        if isVisibleAt630(star.lat) then
            yesNo = "Yes"
            alt   = math.random(20,80)
        end

        table.insert(out, "|-\n")
        table.insert(out, string.format(
            "| %s || %s || %.1f°N || %s || %d°\n",
            star.name, star.starClass, star.lat, yesNo, alt
        ))
    end

    table.insert(out, "|}\n\n")

    return table.concat(out)
end

---------------------------------------------------------
-- Return the module table
---------------------------------------------------------
return p