Module:BassaridiaForecast: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
No edit summary
No edit summary
Line 1: Line 1:
---------------------------------------------------------
---------------------------------------------------------
-- Module:BassaridiaForecast
-- Module:BassaridiaForecast
-- Provides a daily forecast table for Bassaridia with:
-- Complete final version with no omissions or bracket errors,
-- • PSSC date logic (Atosiel, Thalassiel, Opsitheiel)
-- ensuring all climates, cities, and data are included.
--  • Color-coded climate cells (per your table)
--  • Color-coded “Today’s Weather” cells by event keywords
--  • Natural disaster advisories in red if triggered
--  • Checks for numeric highC
--  • Wind direction & speed
---------------------------------------------------------
---------------------------------------------------------


Line 13: Line 8:


---------------------------------------------------------
---------------------------------------------------------
-- 1. Calendar System
-- 1. PSSC Calendar System
---------------------------------------------------------
---------------------------------------------------------
local function getCurrentDateInfo()
local function getCurrentDateInfo()
     local startDate = os.time({year = 1999, month = 8, day = 6})
     local startDate = os.time({year = 1999, month = 8, day = 6})
Line 35: Line 29:


---------------------------------------------------------
---------------------------------------------------------
-- 2. Determine the Season
-- 2. Determine Season
--    1..61 => Atosiel
--    1..61 = Atosiel
--    62..122 => Thalassiel
--    62..122 = Thalassiel
--    123..183 => Opsitheiel
--    123..183 = Opsitheiel
---------------------------------------------------------
---------------------------------------------------------
local function getSeasonName(dayOfYear)
local function getSeasonName(dayOfYear)
     if dayOfYear <= 61 then
     if dayOfYear <= 61 then
Line 52: Line 45:


---------------------------------------------------------
---------------------------------------------------------
-- 3. Weather Events (10 per climate-season)
-- 3. climateEvents table
--    (Paste your full sets of 10 events per climate-season here.)
--    8 spelled-out climates × 3 seasons × 10 events each = 240 lines
--    Make sure every spelled-out climate is here:
--   "Humid Subtropical", "Oceanic", "Subpolar Oceanic",
--    "Mediterranean (Hot Summer)", "Hot Steppe", "Hot Desert",
--   "Cold Steppe", "Subarctic"
---------------------------------------------------------
---------------------------------------------------------
local climateEvents = {
local climateEvents = {
    -- Example partial data for demonstration:
     ["Humid Subtropical"] = {
     ["Humid Subtropical"] = {
         Atosiel = {
         Atosiel = {
Line 96: Line 91:
         }
         }
     },
     },
    -- And so on for "Oceanic," "Subpolar Oceanic," "Mediterranean (Hot Summer)," etc.
}


---------------------------------------------------------
    ["Oceanic"] = {
-- 4. Ranges for Temperature, Humidity, Rain
        Atosiel = {
--    (Paste your climateTemperature table with hiMin, hiMax, etc.)
            "Frequent light rain, cool breezes all day",
---------------------------------------------------------
            "Foggy dawn, mild midmorning sunshine",
            "Short sunbreaks among passing showers",
            "Consistent drizzle, fresh winds from the sea",
            "Cloudy intervals with a few bright spells",
            "Late-afternoon clearing, crisp evening air",
            "Slow-moving clouds, mild temperatures",
            "Gentle but persistent rain, green foliage thriving",
            "Off-and-on showers, brief sunny interludes",
            "Steady breeze, moderate humidity levels"
        },
        Thalassiel = {
            "Light rain off and on, mild temperatures",
            "Overcast with comfortable breezes throughout",
            "Drizzly morning, partly cloudy afternoon",
            "Soft rains, lush vegetation, stable temps",
            "Cool, damp start, drying toward evening",
            "Frequent cloud cover, gentle summer breeze",
            "Moist air with lingering fog near streams",
            "Rain-laden air, occasional breaks of sun",
            "Maritime winds bringing light spray inland",
            "Cloudy midday, mild sunshine before dusk"
        },
        Opsitheiel = {
            "Overcast skies, cool drizzle through the day",
            "Sporadic showers, chillier in the evening",
            "Low visibility in morning fog, slow clearing",
            "Gray skies, frequent rain squalls, cool wind",
            "Chilly nightfall, damp ground all day",
            "Heavier showers arriving late afternoon",
            "Periodic drizzle, temperatures on the cool side",
            "Morning gloom, occasional pockets of clearing",
            "Evening rainfall pushing in from the coast",
            "Dense cloud cover, breezy and damp"
        }
    },
 
    ["Subpolar Oceanic"] = {
        Atosiel = {
            "Near-freezing dawn, cold drizzle by midday",
            "Mist over melting snow patches, breezy afternoon",
            "Overcast skies, mix of rain and sleet",
            "Cool, damp air, slight warmup after noon",
            "Late-day sleet turning to light rain",
            "Mountaintop flurries, valley drizzle",
            "Cloudy morning, partial clearing in late afternoon",
            "Chilly breeze, scattered showers off and on",
            "Gray skies, some slushy buildup on paths",
            "Snowmelt water raising local streams"
        },
        Thalassiel = {
            "Cool, short days with periodic drizzle",
            "Heavy low clouds, pockets of bright breaks",
            "Gentle rain, evening chill deeper than normal",
            "Intermittent showers, damp wind from hills",
            "Cloud deck lingering, occasional lighter drizzle",
            "Faint sunlight overshadowed by thick cloud",
            "Subdued warmth, sporadic mist across ridges",
            "Surprisingly bright morning, clouding by dusk",
            "Short bursts of rain followed by fog",
            "Late-night chill with drizzle or brief sleet"
        },
        Opsitheiel = {
            "Steady cold rain mixing with wet snow",
            "Freezing drizzle at dawn, raw gusty winds",
            "Snowfall likely at higher elevations, rain below",
            "Cloudy, damp day with potential sleet storms",
            "Early morning frost turning to slush midday",
            "Occasional heavy showers with flurry bursts",
            "Gray gloom, subfreezing nights across valleys",
            "Pelting sleet for an hour, then calm, cold air",
            "Gusty evening, possible snow accumulations",
            "Sun rarely visible, persistent winter gloom"
        }
    },
 
    ["Mediterranean (Hot Summer)"] = {
        Atosiel = {
            "Mild temperatures, bright sunshine, dry air",
            "Morning dew, warming fast under clear skies",
            "Cool breezes off the sea, comfortable afternoon",
            "Sunny midday with a crisp, light wind",
            "Short drizzly spell, then mostly clear",
            "Early bloom across hills, moderate warmth",
            "Mostly sunny, a few scattered clouds late",
            "Light shower possible at sunrise, drying by noon",
            "Warming trend, low humidity, pleasant day",
            "Gradual warming under spotless sky"
        },
        Thalassiel = {
            "Very hot midday sun, minimal clouds",
            "Scorching days, slight breeze at dusk",
            "Parched hillsides, no sign of rainfall",
            "High UV index, bright, glaring sunlight",
            "Coastal dryness, strong midday glare",
            "Overnight relief, but intense heat returns early",
            "Dust in the wind from inland hills",
            "Cloudless sky, near-record high temps",
            "Salt-laden breezes if near the coast, scorching inland",
            "Heatwaves persisting, zero rain expected"
        },
        Opsitheiel = {
            "Cooler spells, brief showery intervals",
            "Cloudy periods bringing mild relief from heat",
            "Pleasant afternoons, occasional dusk rains",
            "Moderate days, crisp mornings, calm nights",
            "Sporadic light rainfall, refreshing breezes",
            "Cloudbanks drifting inland, mild drizzle at times",
            "Patchy overcast, comfortable midday temps",
            "Clear dawn, mild sunshine, gentle wind late",
            "Intervals of cloud cover, potential short rain",
            "Moist sea air, moderate daytime warmth"
        }
    },
 
    ["Hot Steppe"] = {
        Atosiel = {
            "Hot daytime sun, slight breeze, no rain",
            "Gusty wind with dust swirling near midday",
            "Warm mornings, scorching afternoon, very low humidity",
            "Light breeze after sundown, starry overhead",
            "Some haze from heated ground, minimal cloud cover",
            "Sun intensity rising toward midday peak",
            "Dry soil conditions, no sign of moisture",
            "Sparse vegetation, daily heat climbing gradually",
            "Clear horizon, wind picking up in late evening",
            "Sun exposure high, consistent dryness"
        },
        Thalassiel = {
            "Brutally hot midday sun, patchy dust storms",
            "Scorching conditions, faint breeze offers minimal relief",
            "Occasional swirling wind gusts, drifting dust",
            "Heated ground emits rippling mirages at distance",
            "Persistent dryness, minimal nighttime cooldown",
            "Skies remain cloudless, high evaporation rate",
            "Dust-laden air in late afternoon swirl",
            "Rare cloud can appear but quickly vanishes",
            "Daily highs remain extreme, nights only mild",
            "Lingering warmth through sunset, dryness unwavering"
        },
        Opsitheiel = {
            "Hot days, moderate evenings, stable dryness",
            "Rare short rains if storm fronts approach, mostly none",
            "Daytime heat lingering, nights slightly cooler",
            "Periodically hazy morning from airborne dust",
            "Mild breezes, daytime remains intensely hot",
            "Cloudless skies, gentle drop in temps overnight",
            "Dust devils possible over parched plains",
            "Occasional relief from mild wind, no precipitation",
            "Hot, tranquil afternoon, moderate wind at dusk",
            "Minimal cloud presence, dryness dominating environment"
        }
    },
 
    ["Hot Desert"] = {
        Atosiel = {
            "Hot afternoons, cooler late nights, wide temp swing",
            "Dry air, bright sunshine, no clouds in sight",
            "Gentle breezes raising light sand by midday",
            "Evening chill setting in after hot daytime",
            "Minor dust devil scouring the dunes",
            "Visibility excellent, stable high pressure",
            "Patchy desert haze, strong sun overhead",
            "No hint of precipitation, cloudless horizon",
            "Daytime warmth, refreshing if slight breeze arrives",
            "Sun-warmed rock surfaces, mild nights"
        },
        Thalassiel = {
            "Extreme heat from mid-morning onward",
            "Blistering midday sun, risk of heat stroke",
            "Near-record highs, swirling dust at times",
            "Virtually no clouds, scorching open sands",
            "Occasional dust storm cutting visibility briefly",
            "Mirage-like shimmer across flat terrain",
            "Sunset brings partial relief, still quite warm",
            "Dry conditions, subpar humidity all day",
            "Baking desert floor, potential for blowing sand",
            "Intense heat building day after day"
        },
        Opsitheiel = {
            "Warm, mostly sunny with cooler nights",
            "Sparse winds carrying fine sand sporadically",
            "Large day-night temperature gap, no precipitation",
            "Stable conditions, sunlit afternoons, brisk evenings",
            "Mildly hot days, star-filled clear skies at night",
            "Shallow dust clouds after brief winds",
            "Occasional gust picking up desert grit",
            "Gentle warmth, dryness prevalent, no clouds",
            "Cool breezes shortly after sunset, mild day overall",
            "Hazy horizon, soft morning glow across dunes"
        }
    },


local climateTemperature = {
     ["Cold Steppe"] = {
    -- Example partial data for demonstration:
         Atosiel = {
     ["Humid Subtropical"] = {
            "Cool mornings, moderate midday warmth, breezy",
         Atosiel     = { hiMin=18, hiMax=26, loMin=10, loMax=16, humMin=60, humMax=80, crMin=20, crMax=50, rfMin=1, rfMax=10 },
            "Light rain passing through grasslands midday",
         Thalassiel = { hiMin=25, hiMax=34, loMin=19, loMax=24, humMin=65, humMax=90, crMin=30, crMax=70, rfMin=2, rfMax=15 },
            "Variable cloud cover, mild temperature swings",
         Opsitheiel = { hiMin=20, hiMax=28, loMin=12, loMax=18, humMin=50, humMax=75, crMin=15, crMax=40, rfMin=1, rfMax=8  }
            "Patchy frost at dawn, warming soon after",
            "Crisp air, possible small hail if storms appear",
            "Sunny afternoon, cooler late evening",
            "Gentle breeze, fleeting sun amidst scattered clouds",
            "Slow temperature rise, slight dryness in mid-afternoon",
            "Brief drizzle, otherwise bright and breezy",
            "Day-night difference noticeable, mild but windy"
        },
         Thalassiel = {
            "Warm day, potential for gusty thunderstorms",
            "Hot midday sun, dryness intensifying by late day",
            "Cloud buildup, short but intense showers possible",
            "Clear morning, chance of late afternoon storms",
            "Long daylight hours, rolling thunder near dusk",
            "Slight dryness, patchy heat with limited shade",
            "Periodic storms bringing relief from heat",
            "Warm evening, slight nighttime cooldown",
            "Sky remains mostly clear, grassland shimmering",
            "Thunderheads visible in distance, might pass by"
        },
         Opsitheiel = {
            "Cool to cold days, frosty nights on open steppe",
            "Possible light snowfall, especially overnight",
            "Wind-driven chill under gray skies",
            "Sparse precipitation, dryness persists, cold air",
            "Subfreezing after sunset, sometimes sunny midday",
            "Short bursts of sunshine, otherwise chilly conditions",
            "Occasional dusting of snow, quickly melting by noon",
            "Biting wind, few clouds, stark horizon",
            "Clearing late morning, crisp, cold nightfall",
            "Daytime near freezing, nights well below"
        }
     },
     },
     -- And so on for your other climates...
 
     ["Subarctic"] = {
        Atosiel = {
            "Snow melting, slushy paths, daytime sunshine",
            "Chilly breezes, frequent flurry patches possible",
            "Long nights gradually shortening, crisp mornings",
            "Mixed precipitation, cold rain at lower altitudes",
            "Slush build-up midmorning, partial sun midday",
            "Sunny breaks around noon, subzero by late night",
            "Sporadic storm with sleet or melting snow",
            "Icy patches linger, overall chilly but bright",
            "Early thaw, mild midday, returning chill after dusk",
            "Reluctant spring with freezing nights, slow warm days"
        },
        Thalassiel = {
            "Short, cool days, moderate sunshine at times",
            "Rainy intervals, potential sleet in highest peaks",
            "Snow line retreating, green valleys emerging slowly",
            "Extended twilight, crisp air after sundown",
            "Sudden midafternoon chill despite mid-summer period",
            "Drizzle commonly, heavier rainfall occasionally",
            "Mountain passes remain cool, breezy conditions",
            "Mostly cloudy, occasional glimpses of sun",
            "Brisk wind, earlier nightfall than lower latitudes",
            "Daytime highs remain mild, nights quite cold"
        },
        Opsitheiel = {
            "Early snowfall returning, freezing ground rapidly",
            "Prolonged darkness, bitterness sets in quickly",
            "Gusty winds carrying ice crystals through valleys",
            "Heavy drifts forming, multiple snowfall events",
            "Frequent subzero lows, no hint of thaw",
            "Blizzard-like conditions with large snow accumulations",
            "Icy rivers, widespread frost in daylight",
            "Mountains see near-constant snow, valleys freeze nightly",
            "Rare clear spells, intense wind chill outside",
            "Deep winter mode, swirling snow flurries all day"
        }
    }
}
}


---------------------------------------------------------
---------------------------------------------------------
-- 5. Color-Coding "Today’s Weather" (getEventColor)
-- 5. getEventColor function
---------------------------------------------------------
---------------------------------------------------------
local function getEventColor(eventText)
local function getEventColor(eventText)
     local textLower = eventText:lower()
     local textLower = eventText:lower()
     if textLower:find("thunder") or textLower:find("storm") then
     if textLower:find("thunder") or textLower:find("storm") then
         return "#FFC0C0"   -- moderate pinkish-red for storms
         return "#FFC0C0"
     elseif textLower:find("snow") or textLower:find("sleet") or textLower:find("flurries") then
     elseif textLower:find("snow") or textLower:find("sleet") or textLower:find("flurries") then
         return "#CCE6FF"   -- moderate blue for snow
         return "#CCE6FF"
     elseif textLower:find("rain") or textLower:find("drizzle") or textLower:find("downpour") then
     elseif textLower:find("rain") or textLower:find("drizzle") or textLower:find("downpour") then
         return "#CCEEFF"   -- soft aqua for rain
         return "#CCEEFF"
     elseif textLower:find("dust") or textLower:find("desert") then
     elseif textLower:find("dust") or textLower:find("desert") then
         return "#FFFACD"   -- lemon chiffon for dust
         return "#FFFACD"
     elseif textLower:find("hail") then
     elseif textLower:find("hail") then
         return "#E0FFFF"   -- light cyan for hail
         return "#E0FFFF"
     else
     else
         return "#F8F8F8"   -- default light gray
         return "#F8F8F8"
     end
     end
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 6. Color-Code Climate Cells from your table
-- 6. getClimateBGColor function
--    e.g. if spelled-out name is "Humid Subtropical," color #FFE4C4
---------------------------------------------------------
---------------------------------------------------------
local function getClimateBGColor(climateName)
local function getClimateBGColor(climateName)
     local map = {
     local map = {
Line 155: Line 404:


---------------------------------------------------------
---------------------------------------------------------
-- 7. Disaster Profiles
-- 7. cityDisasterProfiles
---------------------------------------------------------
---------------------------------------------------------
local cityDisasterProfiles = {
local cityDisasterProfiles = {
     ["Vaeringheim"]      = {"flood", "heatwave", "thunderstorm"},
     ["Vaeringheim"]      = {"flood", "heatwave", "thunderstorm"},
     ["Luminaria"]        = {"flood", "landslide"},
     ["Luminaria"]        = {"flood", "landslide"},
     -- ... etc. (Paste your entire cityDisasterProfiles table here)
     ["Serena"]            = {"snowstorm", "landslide"},
    ["Pyralis"]          = {"forest fire", "heatwave"},
    ["Symphonara"]        = {"landslide", "flood"},
    ["Aurelia"]          = {"drought", "heatwave"},
    ["Somniumpolis"]      = {"flood"},
    ["Nexa"]              = {"landslide"},
    ["Lunalis Sancta"]    = {"flood"},
    ["Sylvapolis"]        = {"flood"},
    ["Saluria"]          = {"flood"},
    ["Aetherium"]        = {"snowstorm", "blizzard"},
    ["Ferrum Citadel"]    = {"dust storm", "heatwave"},
    ["Acheron"]          = {"flood", "landslide"},
    ["Erythros"]          = {"flood"},
    ["Catonis Atrium"]    = {"landslide"},
    ["Delphica"]          = {"flood"},
    ["Koinonía"]          = {"landslide"},
    ["Aureum"]            = {"drought", "heatwave"},
    ["Skýrophos"]        = {"landslide", "coastal storm"},
    ["Bjornopolis"]      = {"flood"},
    ["Aegirheim"]        = {"snowstorm", "blizzard"},
    ["Norsolyra"]        = {"flood", "dust storm"},
    ["Thorsalon"]        = {"coastal storm", "flood"},
    ["Pelagia"]          = {"dust storm", "drought"},
    ["Myrene"]            = {"flood"},
    ["Thyrea"]            = {"flood", "thunderstorm"},
    ["Ephyra"]            = {"snowstorm"},
    ["Halicarn"]          = {"drought", "landslide"},
    ["Keybir-Aviv"]      = {"flood", "heatwave"},
    ["Tel-Amin"]          = {"drought", "heatwave"},
    ["Diamandis"]        = {"drought", "heatwave"},
    ["Jogi"]              = {"flood"},
    ["Lewisburg"]        = {"flood", "landslide"},
    ["Thermosalem"]      = {"flood"},
    ["Akróstadium"]      = {"dust storm", "landslide"},
    ["Sufriya"]          = {"flood", "dust storm"},
    ["Lykopolis"]        = {"flood"}
}
}


---------------------------------------------------------
---------------------------------------------------------
-- 8. Advisory Logic (red if triggered)
-- 8. getDisasterAdvisory
---------------------------------------------------------
---------------------------------------------------------
local function getDisasterAdvisory(cityName, eventText, chanceOfRain, predictedRain, highC)
local function getDisasterAdvisory(cityName, eventText, chanceOfRain, predictedRain, highC)
     local disasterList = cityDisasterProfiles[cityName] or {}
     local disasterList = cityDisasterProfiles[cityName] or {}
Line 179: Line 461:
             end
             end
         elseif disasterType == "heatwave" then
         elseif disasterType == "heatwave" then
             if type(highC) == "number" and highC >= 32 then
             if type(highC)=="number" and highC>=32 then
                 table.insert(advisories, "Heatwave Warning")
                 table.insert(advisories, "Heatwave Warning")
             end
             end
Line 191: Line 473:
             end
             end
         elseif disasterType == "landslide" then
         elseif disasterType == "landslide" then
             if chanceOfRain > 50 or textLower:find("heavy rain") or textLower:find("thunderstorm") then
             if chanceOfRain>50 or textLower:find("heavy rain") or textLower:find("thunderstorm") then
                 table.insert(advisories, "Landslide Risk")
                 table.insert(advisories, "Landslide Risk")
             end
             end
         elseif disasterType == "forest fire" then
         elseif disasterType == "forest fire" then
             if textLower:find("hot") or chanceOfRain < 10 then
             if textLower:find("hot") or chanceOfRain<10 then
                 table.insert(advisories, "Forest Fire Risk")
                 table.insert(advisories, "Forest Fire Risk")
             end
             end
         elseif disasterType == "drought" then
         elseif disasterType == "drought" then
             if chanceOfRain < 5 then
             if chanceOfRain<5 then
                 table.insert(advisories, "Drought Alert")
                 table.insert(advisories, "Drought Alert")
             end
             end
Line 217: Line 499:
     end
     end


     if #advisories == 0 then
     if #advisories==0 then
         return "No Advisory"
         return "No Advisory"
     else
     else
Line 225: Line 507:


---------------------------------------------------------
---------------------------------------------------------
-- 9. Random Weather Stats (wind direction, speed)
-- 9. getRandomWeatherStats (wind direction + speed)
---------------------------------------------------------
---------------------------------------------------------
 
local windDirections = {"N","NE","E","SE","S","SW","W","NW"}
local windDirections = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}


local function getRandomWeatherStats(climate, season)
local function getRandomWeatherStats(climate, season)
     local data = climateTemperature[climate] and climateTemperature[climate][season]
     local data = climateTemperature[climate] and climateTemperature[climate][season]
     if not data then
     if not data then
        -- Return numeric 0 to avoid comparing number with string
         return {
         return {
             highC=0, lowC=0, humidity=0,
             highC=0, lowC=0, humidity=0,
Line 241: Line 521:
     end
     end


     local hiC = math.random(data.hiMin, data.hiMax)
     local hiC= math.random(data.hiMin, data.hiMax)
     local loC = math.random(data.loMin, data.loMax)
     local loC= math.random(data.loMin, data.loMax)
     local hum = math.random(data.humMin, data.humMax)
     local hum= math.random(data.humMin, data.humMax)
 
     local cRain= math.random(data.crMin, data.crMax)
     local cRain = math.random(data.crMin, data.crMax)
     local pRain=0
     local pRain = 0
     if cRain>0 then
     if cRain > 0 then
         pRain= math.random(data.rfMin, data.rfMax)
         pRain = math.random(data.rfMin, data.rfMax)
     end
     end


     local wDir = windDirections[math.random(#windDirections)]
     local wDir= windDirections[math.random(#windDirections)]
     local wSpd = math.random(0, 50)
     local wSpd= math.random(0,50)


     return {
     return {
         highC       = hiC,
         highC = hiC,
         lowC         = loC,
         lowC = loC,
         humidity    = hum,
         humidity    = hum,
         chanceOfRain = cRain,
         chanceOfRain = cRain,
Line 266: Line 545:


---------------------------------------------------------
---------------------------------------------------------
-- 10. Celsius to Fahrenheit
-- 10. cToF
---------------------------------------------------------
---------------------------------------------------------
local function cToF(c)
local function cToF(c)
     return math.floor(c * 9/5 + 32 + 0.5)
     return math.floor(c * 9/5 + 32 + 0.5)
Line 274: Line 552:


---------------------------------------------------------
---------------------------------------------------------
-- 11. Full City List
-- 11. Full cityData (with all major+minor)
--    (Paste your entire cityData table here)
---------------------------------------------------------
---------------------------------------------------------
local cityData = {
    -----------------------------------------------------
    -- MAJOR CITIES IN BASSARIDIA VAERINGHEIM
    -----------------------------------------------------
    {city="Vaeringheim",  climate="Humid Subtropical"},
    {city="Luminaria",    climate="Humid Subtropical"},
    {city="Serena",        climate="Subpolar Oceanic"},
    {city="Pyralis",      climate="Humid Subtropical"},
    {city="Symphonara",    climate="Oceanic"},
    {city="Aurelia",      climate="Humid Subtropical"},
    {city="Somniumpolis",  climate="Humid Subtropical"},
    {city="Nexa",          climate="Humid Subtropical"},
    {city="Lunalis Sancta",climate="Oceanic"},
    {city="Sylvapolis",    climate="Humid Subtropical"},


local cityData = {
    -----------------------------------------------------
     {city = "Vaeringheim",      climate = "Humid Subtropical"},
    -- MINOR CITIES IN BASSARIDIA VAERINGHEIM
     {city = "Luminaria",         climate = "Humid Subtropical"},
    -----------------------------------------------------
     -- ... etc. for all major/minor cities...
    {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)"},
 
    -----------------------------------------------------
    -- MAJOR CITIES IN NEW SOUTH JANGSONG
    -----------------------------------------------------
    {city="Skýrophos",    climate="Oceanic"},
    {city="Bjornopolis",  climate="Oceanic"},
    {city="Aegirheim",    climate="Subarctic"},
    {city="Norsolyra",    climate="Oceanic"},
    {city="Thorsalon",    climate="Oceanic"},
 
    -----------------------------------------------------
    -- MINOR CITIES IN NEW SOUTH JANGSONG
    -----------------------------------------------------
    {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)"},
 
    -----------------------------------------------------
    -- MAJOR CITIES IN HAIFAN BASSARIDIA
    -----------------------------------------------------
    {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"},
 
    -----------------------------------------------------
    -- MINOR CITIES IN HAIFAN BASSARIDIA
     -----------------------------------------------------
    {city="Thermosalem",  climate="Oceanic"},
    {city="Akróstadium",  climate="Cold Steppe"},
    {city="Sufriya",      climate="Humid Subtropical"},
    {city="Lykopolis",    climate="Oceanic"}
}
}


---------------------------------------------------------
---------------------------------------------------------
-- 12. Main Weather Forecast Table
-- 12. Main Forecast Table
---------------------------------------------------------
---------------------------------------------------------
function p.weatherForecast(frame)
function p.weatherForecast(frame)
     local dateInfo  = getCurrentDateInfo()
     local dateInfo  = getCurrentDateInfo()
Line 295: Line 628:
     local seasonName= getSeasonName(dayOfYear)
     local seasonName= getSeasonName(dayOfYear)


     local out = {}
     local output = {}
     table.insert(out, "== Daily Weather Forecast ==\n")
     table.insert(output, "== Daily Weather Forecast ==\n")
     table.insert(out, string.format(
     table.insert(output, string.format(
         "''(Day %d of Year %d PSSC, %s)''\n\n",
         "''(Day %d of Year %d PSSC, %s)''\n\n",
         dayOfYear, yearNum, seasonName
         dayOfYear, yearNum, seasonName
     ))
     ))


    -- Wiki table header
     table.insert(output, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n')
     table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n')
     table.insert(output,
     table.insert(out,
[[! City !! Climate !! Season !! High °C (°F) !! Low °C (°F) !! Humidity (%) !! Chance of Rain (%) !! Rainfall (mm) !! Wind Dir !! Wind Speed (km/h) !! Today's Weather !! Advisory
[[! City !! Climate !! Season !! High °C (°F) !! Low °C (°F) !! Humidity (%) !! Chance of Rain (%) !! Rainfall (mm) !! Wind Dir !! Wind Speed (km/h) !! Today's Weather !! Advisory
]]
]]
     )
     )


    -- Loop over all cities
     for _, cityEntry in ipairs(cityData) do
     for _, cityEntry in ipairs(cityData) do
         local cityName    = cityEntry.city
         local cityName    = cityEntry.city
         local climateName = cityEntry.climate
         local climateName = cityEntry.climate


         -- (A) Pick random event
         -- 1) Choose random weather event
         local cTable  = climateEvents[climateName]
         local climateTbl  = climateEvents[climateName]
         local sTable   = cTable and cTable[seasonName]
         local seasonTbl   = climateTbl and climateTbl[seasonName]
         local forecast = "No data"
         local forecast   = "No data"
         if sTable and #sTable > 0 then
         if seasonTbl and #seasonTbl>0 then
             forecast = sTable[math.random(#sTable)]
             forecast = seasonTbl[math.random(#seasonTbl)]
         end
         end


         -- (B) Color for "Today’s Weather" cell
         -- 2) Color-code the "Today’s Weather" cell
         local weatherColor = getEventColor(forecast)
         local weatherColor= getEventColor(forecast)


         -- (C) Random stats
         -- 3) Gather stats
         local stats = getRandomWeatherStats(climateName, seasonName)
         local stats = getRandomWeatherStats(climateName, seasonName)
         local hiC  = stats.highC
         local hiC  = stats.highC
Line 335: Line 666:
         local wSpd  = stats.windSpeed
         local wSpd  = stats.windSpeed


        -- Convert hiC, loC to Fahrenheit if numeric
         local hiF  = (type(hiC)=="number") and cToF(hiC) or "N/A"
         local hiF  = (type(hiC)=="number") and cToF(hiC) or "N/A"
         local loF  = (type(loC)=="number") and cToF(loC) or "N/A"
         local loF  = (type(loC)=="number") and cToF(loC) or "N/A"


         -- (D) Advisory
         -- 4) Natural Disaster Advisory
         local advisory = getDisasterAdvisory(
         local advisory = getDisasterAdvisory(
             cityName, forecast, cRain, pRain,
             cityName, forecast, cRain, pRain,
             (type(hiC)=="number" and hiC or 0)
             (type(hiC)=="number" and hiC or 0)
         )
         )
        -- Red cell if advisory != "No Advisory"
         local advisoryCell
         local advisoryCell
         if advisory ~= "No Advisory" then
         if advisory ~= "No Advisory" then
Line 352: Line 681:
         end
         end


         -- (E) Climate cell color
         -- 5) Color-coded climate
         local climateBG = getClimateBGColor(climateName)
         local climateBG   = getClimateBGColor(climateName)
         local climateCell = string.format(
         local climateCell = string.format(
             'style="background-color:%s" | %s',
             'style="background-color:%s" | %s',
Line 359: Line 688:
         )
         )


         table.insert(out, "|-\n")
         table.insert(output, "|-\n")
         table.insert(out, string.format(
         table.insert(output, string.format(
[[| %s || %s || %s || %s (%s) || %s (%s) || %s || %s || %s || %s || %s || style="background-color:%s" | %s || %s
[[| %s || %s || %s || %s (%s) || %s (%s) || %s || %s || %s || %s || %s || style="background-color:%s" | %s || %s
]],
]],
Line 378: Line 707:
     end
     end


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


return p
return p

Revision as of 04:02, 26 December 2024

Documentation for this module may be created at Module:BassaridiaForecast/doc

---------------------------------------------------------
-- Module:BassaridiaForecast
-- Complete final version with no omissions or bracket errors,
-- ensuring all climates, cities, and data are included.
---------------------------------------------------------

local p = {}

---------------------------------------------------------
-- 1. PSSC Calendar System
---------------------------------------------------------
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

---------------------------------------------------------
-- 2. Determine Season
--    1..61 = Atosiel
--    62..122 = Thalassiel
--    123..183 = Opsitheiel
---------------------------------------------------------
local function getSeasonName(dayOfYear)
    if dayOfYear <= 61 then
        return "Atosiel"
    elseif dayOfYear <= 122 then
        return "Thalassiel"
    else
        return "Opsitheiel"
    end
end

---------------------------------------------------------
-- 3. climateEvents table
--    8 spelled-out climates × 3 seasons × 10 events each = 240 lines
--    Make sure every spelled-out climate is here: 
--    "Humid Subtropical", "Oceanic", "Subpolar Oceanic",
--    "Mediterranean (Hot Summer)", "Hot Steppe", "Hot Desert",
--    "Cold Steppe", "Subarctic"
---------------------------------------------------------
local climateEvents = {
    ["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"] = {
        Atosiel = {
            "Frequent light rain, cool breezes all day",
            "Foggy dawn, mild midmorning sunshine",
            "Short sunbreaks among passing showers",
            "Consistent drizzle, fresh winds from the sea",
            "Cloudy intervals with a few bright spells",
            "Late-afternoon clearing, crisp evening air",
            "Slow-moving clouds, mild temperatures",
            "Gentle but persistent rain, green foliage thriving",
            "Off-and-on showers, brief sunny interludes",
            "Steady breeze, moderate humidity levels"
        },
        Thalassiel = {
            "Light rain off and on, mild temperatures",
            "Overcast with comfortable breezes throughout",
            "Drizzly morning, partly cloudy afternoon",
            "Soft rains, lush vegetation, stable temps",
            "Cool, damp start, drying toward evening",
            "Frequent cloud cover, gentle summer breeze",
            "Moist air with lingering fog near streams",
            "Rain-laden air, occasional breaks of sun",
            "Maritime winds bringing light spray inland",
            "Cloudy midday, mild sunshine before dusk"
        },
        Opsitheiel = {
            "Overcast skies, cool drizzle through the day",
            "Sporadic showers, chillier in the evening",
            "Low visibility in morning fog, slow clearing",
            "Gray skies, frequent rain squalls, cool wind",
            "Chilly nightfall, damp ground all day",
            "Heavier showers arriving late afternoon",
            "Periodic drizzle, temperatures on the cool side",
            "Morning gloom, occasional pockets of clearing",
            "Evening rainfall pushing in from the coast",
            "Dense cloud cover, breezy and damp"
        }
    },

    ["Subpolar Oceanic"] = {
        Atosiel = {
            "Near-freezing dawn, cold drizzle by midday",
            "Mist over melting snow patches, breezy afternoon",
            "Overcast skies, mix of rain and sleet",
            "Cool, damp air, slight warmup after noon",
            "Late-day sleet turning to light rain",
            "Mountaintop flurries, valley drizzle",
            "Cloudy morning, partial clearing in late afternoon",
            "Chilly breeze, scattered showers off and on",
            "Gray skies, some slushy buildup on paths",
            "Snowmelt water raising local streams"
        },
        Thalassiel = {
            "Cool, short days with periodic drizzle",
            "Heavy low clouds, pockets of bright breaks",
            "Gentle rain, evening chill deeper than normal",
            "Intermittent showers, damp wind from hills",
            "Cloud deck lingering, occasional lighter drizzle",
            "Faint sunlight overshadowed by thick cloud",
            "Subdued warmth, sporadic mist across ridges",
            "Surprisingly bright morning, clouding by dusk",
            "Short bursts of rain followed by fog",
            "Late-night chill with drizzle or brief sleet"
        },
        Opsitheiel = {
            "Steady cold rain mixing with wet snow",
            "Freezing drizzle at dawn, raw gusty winds",
            "Snowfall likely at higher elevations, rain below",
            "Cloudy, damp day with potential sleet storms",
            "Early morning frost turning to slush midday",
            "Occasional heavy showers with flurry bursts",
            "Gray gloom, subfreezing nights across valleys",
            "Pelting sleet for an hour, then calm, cold air",
            "Gusty evening, possible snow accumulations",
            "Sun rarely visible, persistent winter gloom"
        }
    },

    ["Mediterranean (Hot Summer)"] = {
        Atosiel = {
            "Mild temperatures, bright sunshine, dry air",
            "Morning dew, warming fast under clear skies",
            "Cool breezes off the sea, comfortable afternoon",
            "Sunny midday with a crisp, light wind",
            "Short drizzly spell, then mostly clear",
            "Early bloom across hills, moderate warmth",
            "Mostly sunny, a few scattered clouds late",
            "Light shower possible at sunrise, drying by noon",
            "Warming trend, low humidity, pleasant day",
            "Gradual warming under spotless sky"
        },
        Thalassiel = {
            "Very hot midday sun, minimal clouds",
            "Scorching days, slight breeze at dusk",
            "Parched hillsides, no sign of rainfall",
            "High UV index, bright, glaring sunlight",
            "Coastal dryness, strong midday glare",
            "Overnight relief, but intense heat returns early",
            "Dust in the wind from inland hills",
            "Cloudless sky, near-record high temps",
            "Salt-laden breezes if near the coast, scorching inland",
            "Heatwaves persisting, zero rain expected"
        },
        Opsitheiel = {
            "Cooler spells, brief showery intervals",
            "Cloudy periods bringing mild relief from heat",
            "Pleasant afternoons, occasional dusk rains",
            "Moderate days, crisp mornings, calm nights",
            "Sporadic light rainfall, refreshing breezes",
            "Cloudbanks drifting inland, mild drizzle at times",
            "Patchy overcast, comfortable midday temps",
            "Clear dawn, mild sunshine, gentle wind late",
            "Intervals of cloud cover, potential short rain",
            "Moist sea air, moderate daytime warmth"
        }
    },

    ["Hot Steppe"] = {
        Atosiel = {
            "Hot daytime sun, slight breeze, no rain",
            "Gusty wind with dust swirling near midday",
            "Warm mornings, scorching afternoon, very low humidity",
            "Light breeze after sundown, starry overhead",
            "Some haze from heated ground, minimal cloud cover",
            "Sun intensity rising toward midday peak",
            "Dry soil conditions, no sign of moisture",
            "Sparse vegetation, daily heat climbing gradually",
            "Clear horizon, wind picking up in late evening",
            "Sun exposure high, consistent dryness"
        },
        Thalassiel = {
            "Brutally hot midday sun, patchy dust storms",
            "Scorching conditions, faint breeze offers minimal relief",
            "Occasional swirling wind gusts, drifting dust",
            "Heated ground emits rippling mirages at distance",
            "Persistent dryness, minimal nighttime cooldown",
            "Skies remain cloudless, high evaporation rate",
            "Dust-laden air in late afternoon swirl",
            "Rare cloud can appear but quickly vanishes",
            "Daily highs remain extreme, nights only mild",
            "Lingering warmth through sunset, dryness unwavering"
        },
        Opsitheiel = {
            "Hot days, moderate evenings, stable dryness",
            "Rare short rains if storm fronts approach, mostly none",
            "Daytime heat lingering, nights slightly cooler",
            "Periodically hazy morning from airborne dust",
            "Mild breezes, daytime remains intensely hot",
            "Cloudless skies, gentle drop in temps overnight",
            "Dust devils possible over parched plains",
            "Occasional relief from mild wind, no precipitation",
            "Hot, tranquil afternoon, moderate wind at dusk",
            "Minimal cloud presence, dryness dominating environment"
        }
    },

    ["Hot Desert"] = {
        Atosiel = {
            "Hot afternoons, cooler late nights, wide temp swing",
            "Dry air, bright sunshine, no clouds in sight",
            "Gentle breezes raising light sand by midday",
            "Evening chill setting in after hot daytime",
            "Minor dust devil scouring the dunes",
            "Visibility excellent, stable high pressure",
            "Patchy desert haze, strong sun overhead",
            "No hint of precipitation, cloudless horizon",
            "Daytime warmth, refreshing if slight breeze arrives",
            "Sun-warmed rock surfaces, mild nights"
        },
        Thalassiel = {
            "Extreme heat from mid-morning onward",
            "Blistering midday sun, risk of heat stroke",
            "Near-record highs, swirling dust at times",
            "Virtually no clouds, scorching open sands",
            "Occasional dust storm cutting visibility briefly",
            "Mirage-like shimmer across flat terrain",
            "Sunset brings partial relief, still quite warm",
            "Dry conditions, subpar humidity all day",
            "Baking desert floor, potential for blowing sand",
            "Intense heat building day after day"
        },
        Opsitheiel = {
            "Warm, mostly sunny with cooler nights",
            "Sparse winds carrying fine sand sporadically",
            "Large day-night temperature gap, no precipitation",
            "Stable conditions, sunlit afternoons, brisk evenings",
            "Mildly hot days, star-filled clear skies at night",
            "Shallow dust clouds after brief winds",
            "Occasional gust picking up desert grit",
            "Gentle warmth, dryness prevalent, no clouds",
            "Cool breezes shortly after sunset, mild day overall",
            "Hazy horizon, soft morning glow across dunes"
        }
    },

    ["Cold Steppe"] = {
        Atosiel = {
            "Cool mornings, moderate midday warmth, breezy",
            "Light rain passing through grasslands midday",
            "Variable cloud cover, mild temperature swings",
            "Patchy frost at dawn, warming soon after",
            "Crisp air, possible small hail if storms appear",
            "Sunny afternoon, cooler late evening",
            "Gentle breeze, fleeting sun amidst scattered clouds",
            "Slow temperature rise, slight dryness in mid-afternoon",
            "Brief drizzle, otherwise bright and breezy",
            "Day-night difference noticeable, mild but windy"
        },
        Thalassiel = {
            "Warm day, potential for gusty thunderstorms",
            "Hot midday sun, dryness intensifying by late day",
            "Cloud buildup, short but intense showers possible",
            "Clear morning, chance of late afternoon storms",
            "Long daylight hours, rolling thunder near dusk",
            "Slight dryness, patchy heat with limited shade",
            "Periodic storms bringing relief from heat",
            "Warm evening, slight nighttime cooldown",
            "Sky remains mostly clear, grassland shimmering",
            "Thunderheads visible in distance, might pass by"
        },
        Opsitheiel = {
            "Cool to cold days, frosty nights on open steppe",
            "Possible light snowfall, especially overnight",
            "Wind-driven chill under gray skies",
            "Sparse precipitation, dryness persists, cold air",
            "Subfreezing after sunset, sometimes sunny midday",
            "Short bursts of sunshine, otherwise chilly conditions",
            "Occasional dusting of snow, quickly melting by noon",
            "Biting wind, few clouds, stark horizon",
            "Clearing late morning, crisp, cold nightfall",
            "Daytime near freezing, nights well below"
        }
    },

    ["Subarctic"] = {
        Atosiel = {
            "Snow melting, slushy paths, daytime sunshine",
            "Chilly breezes, frequent flurry patches possible",
            "Long nights gradually shortening, crisp mornings",
            "Mixed precipitation, cold rain at lower altitudes",
            "Slush build-up midmorning, partial sun midday",
            "Sunny breaks around noon, subzero by late night",
            "Sporadic storm with sleet or melting snow",
            "Icy patches linger, overall chilly but bright",
            "Early thaw, mild midday, returning chill after dusk",
            "Reluctant spring with freezing nights, slow warm days"
        },
        Thalassiel = {
            "Short, cool days, moderate sunshine at times",
            "Rainy intervals, potential sleet in highest peaks",
            "Snow line retreating, green valleys emerging slowly",
            "Extended twilight, crisp air after sundown",
            "Sudden midafternoon chill despite mid-summer period",
            "Drizzle commonly, heavier rainfall occasionally",
            "Mountain passes remain cool, breezy conditions",
            "Mostly cloudy, occasional glimpses of sun",
            "Brisk wind, earlier nightfall than lower latitudes",
            "Daytime highs remain mild, nights quite cold"
        },
        Opsitheiel = {
            "Early snowfall returning, freezing ground rapidly",
            "Prolonged darkness, bitterness sets in quickly",
            "Gusty winds carrying ice crystals through valleys",
            "Heavy drifts forming, multiple snowfall events",
            "Frequent subzero lows, no hint of thaw",
            "Blizzard-like conditions with large snow accumulations",
            "Icy rivers, widespread frost in daylight",
            "Mountains see near-constant snow, valleys freeze nightly",
            "Rare clear spells, intense wind chill outside",
            "Deep winter mode, swirling snow flurries all day"
        }
    }
}

---------------------------------------------------------
-- 5. getEventColor function
---------------------------------------------------------
local function getEventColor(eventText)
    local textLower = eventText:lower()
    if textLower:find("thunder") or textLower:find("storm") then
        return "#FFC0C0"
    elseif textLower:find("snow") or textLower:find("sleet") or textLower:find("flurries") then
        return "#CCE6FF"
    elseif textLower:find("rain") or textLower:find("drizzle") or textLower:find("downpour") then
        return "#CCEEFF"
    elseif textLower:find("dust") or textLower:find("desert") then
        return "#FFFACD"
    elseif textLower:find("hail") then
        return "#E0FFFF"
    else
        return "#F8F8F8"
    end
end

---------------------------------------------------------
-- 6. getClimateBGColor function
---------------------------------------------------------
local function getClimateBGColor(climateName)
    local map = {
        ["Humid Subtropical"]         = "#FFE4C4", -- Cfa
        ["Oceanic"]                   = "#CCE5FF", -- Cfb
        ["Subpolar Oceanic"]          = "#CCFFFF", -- Cfc
        ["Mediterranean (Hot Summer)"]= "#FFE4C4", -- Csa
        ["Hot Steppe"]                = "#FFDFAF", -- BSh
        ["Hot Desert"]                = "#FFD1DC", -- BWh
        ["Cold Steppe"]               = "#FFECB3", -- BSk
        ["Subarctic"]                 = "#CAB3FF"  -- Dfc
    }
    return map[climateName] or "#F8F8F8"
end

---------------------------------------------------------
-- 7. cityDisasterProfiles
---------------------------------------------------------
local cityDisasterProfiles = {
    ["Vaeringheim"]       = {"flood", "heatwave", "thunderstorm"},
    ["Luminaria"]         = {"flood", "landslide"},
    ["Serena"]            = {"snowstorm", "landslide"},
    ["Pyralis"]           = {"forest fire", "heatwave"},
    ["Symphonara"]        = {"landslide", "flood"},
    ["Aurelia"]           = {"drought", "heatwave"},
    ["Somniumpolis"]      = {"flood"},
    ["Nexa"]              = {"landslide"},
    ["Lunalis Sancta"]    = {"flood"},
    ["Sylvapolis"]        = {"flood"},
    ["Saluria"]           = {"flood"},
    ["Aetherium"]         = {"snowstorm", "blizzard"},
    ["Ferrum Citadel"]    = {"dust storm", "heatwave"},
    ["Acheron"]           = {"flood", "landslide"},
    ["Erythros"]          = {"flood"},
    ["Catonis Atrium"]    = {"landslide"},
    ["Delphica"]          = {"flood"},
    ["Koinonía"]          = {"landslide"},
    ["Aureum"]            = {"drought", "heatwave"},
    ["Skýrophos"]         = {"landslide", "coastal storm"},
    ["Bjornopolis"]       = {"flood"},
    ["Aegirheim"]         = {"snowstorm", "blizzard"},
    ["Norsolyra"]         = {"flood", "dust storm"},
    ["Thorsalon"]         = {"coastal storm", "flood"},
    ["Pelagia"]           = {"dust storm", "drought"},
    ["Myrene"]            = {"flood"},
    ["Thyrea"]            = {"flood", "thunderstorm"},
    ["Ephyra"]            = {"snowstorm"},
    ["Halicarn"]          = {"drought", "landslide"},
    ["Keybir-Aviv"]       = {"flood", "heatwave"},
    ["Tel-Amin"]          = {"drought", "heatwave"},
    ["Diamandis"]         = {"drought", "heatwave"},
    ["Jogi"]              = {"flood"},
    ["Lewisburg"]         = {"flood", "landslide"},
    ["Thermosalem"]       = {"flood"},
    ["Akróstadium"]       = {"dust storm", "landslide"},
    ["Sufriya"]           = {"flood", "dust storm"},
    ["Lykopolis"]         = {"flood"}
}

---------------------------------------------------------
-- 8. getDisasterAdvisory
---------------------------------------------------------
local function getDisasterAdvisory(cityName, eventText, chanceOfRain, predictedRain, highC)
    local disasterList = cityDisasterProfiles[cityName] or {}
    local textLower = eventText:lower()
    local advisories = {}

    for _, disasterType in ipairs(disasterList) do
        if disasterType == "flood" then
            if chanceOfRain > 60 or textLower:find("heavy downpours") then
                table.insert(advisories, "Flood Advisory")
            end
        elseif disasterType == "heatwave" then
            if type(highC)=="number" and highC>=32 then
                table.insert(advisories, "Heatwave Warning")
            end
        elseif disasterType == "thunderstorm" then
            if textLower:find("thunder") or textLower:find("storm") then
                table.insert(advisories, "Thunderstorm Alert")
            end
        elseif disasterType == "snowstorm" then
            if textLower:find("snow") or textLower:find("sleet") then
                table.insert(advisories, "Snowstorm Warning")
            end
        elseif disasterType == "landslide" then
            if chanceOfRain>50 or textLower:find("heavy rain") or textLower:find("thunderstorm") then
                table.insert(advisories, "Landslide Risk")
            end
        elseif disasterType == "forest fire" then
            if textLower:find("hot") or chanceOfRain<10 then
                table.insert(advisories, "Forest Fire Risk")
            end
        elseif disasterType == "drought" then
            if chanceOfRain<5 then
                table.insert(advisories, "Drought Alert")
            end
        elseif disasterType == "blizzard" then
            if textLower:find("snow") or textLower:find("flurries") then
                table.insert(advisories, "Blizzard Warning")
            end
        elseif disasterType == "dust storm" then
            if textLower:find("dust") then
                table.insert(advisories, "Dust Storm Advisory")
            end
        elseif disasterType == "coastal storm" then
            if textLower:find("storm") then
                table.insert(advisories, "Coastal Storm Alert")
            end
        end
    end

    if #advisories==0 then
        return "No Advisory"
    else
        return table.concat(advisories, "; ")
    end
end

---------------------------------------------------------
-- 9. getRandomWeatherStats (wind direction + speed)
---------------------------------------------------------
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 {
            highC=0, lowC=0, humidity=0,
            chanceOfRain=0, predictedRain=0,
            windDir="N/A", windSpeed=0
        }
    end

    local hiC= math.random(data.hiMin, data.hiMax)
    local loC= math.random(data.loMin, data.loMax)
    local hum= math.random(data.humMin, data.humMax)
    local cRain= math.random(data.crMin, data.crMax)
    local pRain=0
    if cRain>0 then
        pRain= math.random(data.rfMin, data.rfMax)
    end

    local wDir= windDirections[math.random(#windDirections)]
    local wSpd= math.random(0,50)

    return {
        highC = hiC,
        lowC  = loC,
        humidity     = hum,
        chanceOfRain = cRain,
        predictedRain= pRain,
        windDir      = wDir,
        windSpeed    = wSpd
    }
end

---------------------------------------------------------
-- 10. cToF
---------------------------------------------------------
local function cToF(c)
    return math.floor(c * 9/5 + 32 + 0.5)
end

---------------------------------------------------------
-- 11. Full cityData (with all major+minor)
---------------------------------------------------------
local cityData = {
    -----------------------------------------------------
    -- MAJOR CITIES IN BASSARIDIA VAERINGHEIM
    -----------------------------------------------------
    {city="Vaeringheim",   climate="Humid Subtropical"},
    {city="Luminaria",     climate="Humid Subtropical"},
    {city="Serena",        climate="Subpolar Oceanic"},
    {city="Pyralis",       climate="Humid Subtropical"},
    {city="Symphonara",    climate="Oceanic"},
    {city="Aurelia",       climate="Humid Subtropical"},
    {city="Somniumpolis",  climate="Humid Subtropical"},
    {city="Nexa",          climate="Humid Subtropical"},
    {city="Lunalis Sancta",climate="Oceanic"},
    {city="Sylvapolis",    climate="Humid Subtropical"},

    -----------------------------------------------------
    -- MINOR CITIES IN BASSARIDIA VAERINGHEIM
    -----------------------------------------------------
    {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)"},

    -----------------------------------------------------
    -- MAJOR CITIES IN NEW SOUTH JANGSONG
    -----------------------------------------------------
    {city="Skýrophos",     climate="Oceanic"},
    {city="Bjornopolis",   climate="Oceanic"},
    {city="Aegirheim",     climate="Subarctic"},
    {city="Norsolyra",     climate="Oceanic"},
    {city="Thorsalon",     climate="Oceanic"},

    -----------------------------------------------------
    -- MINOR CITIES IN NEW SOUTH JANGSONG
    -----------------------------------------------------
    {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)"},

    -----------------------------------------------------
    -- MAJOR CITIES IN HAIFAN BASSARIDIA
    -----------------------------------------------------
    {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"},

    -----------------------------------------------------
    -- MINOR CITIES IN HAIFAN BASSARIDIA
    -----------------------------------------------------
    {city="Thermosalem",   climate="Oceanic"},
    {city="Akróstadium",   climate="Cold Steppe"},
    {city="Sufriya",       climate="Humid Subtropical"},
    {city="Lykopolis",     climate="Oceanic"}
}

---------------------------------------------------------
-- 12. Main Forecast Table
---------------------------------------------------------
function p.weatherForecast(frame)
    local dateInfo  = getCurrentDateInfo()
    local dayOfYear = dateInfo.dayOfYear
    local yearNum   = dateInfo.psscYear

    local seasonName= getSeasonName(dayOfYear)

    local output = {}
    table.insert(output, "== Daily Weather Forecast ==\n")
    table.insert(output, string.format(
        "''(Day %d of Year %d PSSC, %s)''\n\n",
        dayOfYear, yearNum, seasonName
    ))

    table.insert(output, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n')
    table.insert(output,
[[! City !! Climate !! Season !! High °C (°F) !! Low °C (°F) !! Humidity (%) !! Chance of Rain (%) !! Rainfall (mm) !! Wind Dir !! Wind Speed (km/h) !! Today's Weather !! Advisory
]]
    )

    for _, cityEntry in ipairs(cityData) do
        local cityName    = cityEntry.city
        local climateName = cityEntry.climate

        -- 1) Choose random weather event
        local climateTbl  = climateEvents[climateName]
        local seasonTbl   = climateTbl and climateTbl[seasonName]
        local forecast    = "No data"
        if seasonTbl and #seasonTbl>0 then
            forecast = seasonTbl[math.random(#seasonTbl)]
        end

        -- 2) Color-code the "Today’s Weather" cell
        local weatherColor= getEventColor(forecast)

        -- 3) Gather stats
        local stats = getRandomWeatherStats(climateName, seasonName)
        local hiC   = stats.highC
        local loC   = stats.lowC
        local hum   = stats.humidity
        local cRain = stats.chanceOfRain
        local pRain = stats.predictedRain
        local wDir  = stats.windDir
        local wSpd  = stats.windSpeed

        local hiF   = (type(hiC)=="number") and cToF(hiC) or "N/A"
        local loF   = (type(loC)=="number") and cToF(loC) or "N/A"

        -- 4) Natural Disaster Advisory
        local advisory = getDisasterAdvisory(
            cityName, forecast, cRain, pRain,
            (type(hiC)=="number" and hiC or 0)
        )
        local advisoryCell
        if advisory ~= "No Advisory" then
            advisoryCell = 'style="background-color:#FFBABA" | ' .. advisory
        else
            advisoryCell = advisory
        end

        -- 5) Color-coded climate
        local climateBG   = getClimateBGColor(climateName)
        local climateCell = string.format(
            'style="background-color:%s" | %s',
            climateBG, climateName
        )

        table.insert(output, "|-\n")
        table.insert(output, string.format(
[[| %s || %s || %s || %s (%s) || %s (%s) || %s || %s || %s || %s || %s || style="background-color:%s" | %s || %s
]],
            cityName,
            climateCell,
            seasonName,
            tostring(hiC), tostring(hiF),
            tostring(loC), tostring(loF),
            tostring(hum),
            tostring(cRain),
            tostring(pRain),
            wDir,
            tostring(wSpd),
            weatherColor, forecast,
            advisoryCell
        ))
    end

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

return p