Module:BassaridiaForecast: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
(Created page with "--------------------------------------------------------- -- Module:WeatherForecast -- Provides a daily weather forecast for each city -- using spelled-out climate names, a PSSC date system, -- and random event selection by season. --------------------------------------------------------- local p = {} --------------------------------------------------------- -- 1. Calendar System --------------------------------------------------------- local function getCurrentDateIn...")
 
No edit summary
 
(34 intermediate revisions by the same user not shown)
Line 1: Line 1:
---------------------------------------------------------
---------------------------------------------------------
-- Module:WeatherForecast
-- Module: BassaridiaForecast
-- Provides a daily weather forecast for each city
-- Provides three functions:
-- using spelled-out climate names, a PSSC date system,
--  1) weatherForecast(frame) -> returns a single
-- and random event selection by season.
--    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).
---------------------------------------------------------
---------------------------------------------------------


Line 9: Line 14:


---------------------------------------------------------
---------------------------------------------------------
-- 1. Calendar System
-- 1. Determine Today's Date for PSSC
--    Also used to create a daily random seed.
---------------------------------------------------------
---------------------------------------------------------
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})
     local secondsInDay = 86400
     local secondsInDay= 86400
     local daysPerYear = 183
     local daysPerYear = 183


     local currentTime = os.time()
     local currentTime = os.time()
     local totalDaysElapsed = math.floor((currentTime - startDate) / secondsInDay)
     local totalDays  = math.floor((currentTime - startDate) / secondsInDay)


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


     return {
     return {
         psscYear = psscYear,
         psscYear = psscYear,
         dayOfYear = dayOfYear
         dayOfYear= dayOfYear
     }
     }
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 2. Determine the Season
-- 2. Determine Season (Atosiel=1..61, Thalassiel=62..122, Opsitheiel=123..183)
--    Spring: 1..61
--    Summer: 62..122
--    Fall/Winter: 123..183
---------------------------------------------------------
---------------------------------------------------------
 
local function getSeasonName(dayOfYear)
local function getSeason(dayOfYear)
     if dayOfYear <= 61 then
     if dayOfYear <= 61 then
         return "Spring"
         return "Atosiel"
     elseif dayOfYear <= 122 then
     elseif dayOfYear <= 122 then
         return "Summer"
         return "Thalassiel"
     else
     else
         return "Fall/Winter"
         return "Opsitheiel"
     end
     end
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 3. Weather Events Table
-- 3. climateTemperature (°C behind scenes, displayed °F)
--    Keyed by spelled-out climate name -> season -> array of events
---------------------------------------------------------
---------------------------------------------------------
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
--    (all text remains unchanged)
---------------------------------------------------------
local climateEvents = {
local climateEvents = {
     ["Humid Subtropical"] = {
     ["Humid Subtropical"] = {
         Spring = {
         Atosiel = {
             "Light rain in the morning, sun breaks by afternoon",
             "Morning drizzle and warm afternoon sunshine",
             "Warm, breezy day with scattered sprinkles",
            "Mild thunderstorm building by midday",
             "Cloudy skies, chance of mild thunderstorms",
             "Patchy fog at dawn, clearing toward lunch",
             "Sunny spells with rising humidity",
            "Light rain with sunny breaks after noon",
             "Patchy fog early, clearing toward midday",
             "Gentle breezes, blossoming warmth, low humidity",
             "Gentle breeze, comfortable temperatures",
             "Scattered clouds with a brief shower by dusk",
             "Overcast afternoon with drizzle"
             "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"
         },
         },
         Summer = {
         Thalassiel = {
             "Hot and humid with afternoon thunderstorms",
             "Hot, steamy day with intense midday heat",
             "Steamy day, high humidity, potential heat index",
             "Tropical-like humidity, afternoon thunder possible",
             "Intermittent downpours, warm all day",
             "Intermittent heavy downpours, muggy evenings",
             "Sultry evening with lightning in the distance",
             "High humidity and patchy thunderstorms late",
             "Sunny morning, hazy midday, scattered storms",
             "Sun-scorched morning, scattered storms by dusk",
             "Damp overnight, lingering warm humidity",
             "Hazy sunshine, extremely warm all day",
             "Heat advisory in effect, minimal clouds"
            "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"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Warm daytime temperatures, mild nights",
             "Warm daytime, gentle evening breezes",
             "Occasional rainfall with persistent warmth",
            "Occasional rain, otherwise mild temperatures",
             "Cooler mornings, sunny afternoons",
             "Late-season warmth, scattered rain after sunset",
             "Low humidity, slight chance of drizzle by dusk",
             "Cooler mornings, returning to warmth by midday",
             "Partial cloud cover, gentle breezes",
            "Sparse cloud cover, tranquil weather overall",
             "Warm midday, late-evening shower chance",
             "Fog at dawn, warm midday, pleasant night",
             "Dawn fog, warm haze through late afternoon"
             "Partial sun, comfortable humidity levels",
            "Evening drizzle with mild breezes",
             "Patchy haze, moderate warmth throughout the day",
             "Short-lived shower, then clearing skies"
         }
         }
     },
     },
     ["Oceanic"] = {
     ["Oceanic"] = {
         Spring = {
         Atosiel = {
             "Frequent light showers, fresh breeze",
             "Frequent light rain, cool breezes all day",
             "Cool morning mist, moderate afternoon sun",
             "Foggy dawn, mild midmorning sunshine",
             "Variable cloudiness, gentle drizzle off and on",
             "Short sunbreaks among passing showers",
             "Partly cloudy with hints of sunshine",
            "Consistent drizzle, fresh winds from the sea",
             "Occasional gusts, temperatures staying mild",
             "Cloudy intervals with a few bright spells",
             "Patchy fog early, scattered rain midday",
            "Late-afternoon clearing, crisp evening air",
             "Steady breeze keeping temperatures moderate"
             "Slow-moving clouds, mild temperatures",
             "Gentle but persistent rain, green foliage thriving",
            "Off-and-on showers, brief sunny interludes",
             "Steady breeze, moderate humidity levels"
         },
         },
         Summer = {
         Thalassiel = {
             "Mild temperatures, steady rain intervals",
             "Light rain off and on, mild temperatures",
             "Overcast skies with pleasant breezes",
             "Overcast with comfortable breezes throughout",
             "Intermittent drizzle, comfortable humidity",
             "Drizzly morning, partly cloudy afternoon",
             "Sun breaks between passing showers",
            "Soft rains, lush vegetation, stable temps",
             "Cloudy spells, occasional light rain",
             "Cool, damp start, drying toward evening",
             "Long periods of soft rainfall, mild all day",
             "Frequent cloud cover, gentle summer breeze",
             "Morning and evening drizzle, mild midday sun"
            "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"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Cool and damp, heavy clouds overhead",
             "Overcast skies, cool drizzle through the day",
             "Prolonged drizzle, breezy conditions",
             "Sporadic showers, chillier in the evening",
             "Low visibility in morning fog, gradual clearing",
             "Low visibility in morning fog, slow clearing",
             "Passing showers with occasional gusts",
             "Gray skies, frequent rain squalls, cool wind",
             "Chilly evenings, overcast all day",
             "Chilly nightfall, damp ground all day",
             "Frequent light rain, temperatures staying cool",
             "Heavier showers arriving late afternoon",
             "Intermittent wind-driven rain, gray skies"
            "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"] = {
     ["Subpolar Oceanic"] = {
         Spring = {
         Atosiel = {
             "Cold mornings, patchy snow melt by afternoon",
             "Near-freezing dawn, cold drizzle by midday",
             "Light rain mixed with wet flurries",
            "Mist over melting snow patches, breezy afternoon",
             "Breezy, near-freezing dawn warming slightly",
             "Overcast skies, mix of rain and sleet",
             "Overcast skies, damp winds through valleys",
             "Cool, damp air, slight warmup after noon",
             "Cool drizzle turning to sleet in late day",
            "Late-day sleet turning to light rain",
             "Sun occasionally peeking through cloud deck",
             "Mountaintop flurries, valley drizzle",
             "Early frost, mild midday with possible showers"
             "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"
         },
         },
         Summer = {
         Thalassiel = {
             "Short, cool days with off-and-on drizzle",
             "Cool, short days with periodic drizzle",
             "Overcast skies, constant low-hanging clouds",
             "Heavy low clouds, pockets of bright breaks",
             "Light rain, scattered breaks of weak sunshine",
             "Gentle rain, evening chill deeper than normal",
             "Moist air with frequent mist in foothills",
            "Intermittent showers, damp wind from hills",
             "Gentle breezes, chilly evenings despite summer",
            "Cloud deck lingering, occasional lighter drizzle",
             "Occasional bright spells, but generally gray",
             "Faint sunlight overshadowed by thick cloud",
             "Rain showers followed by cool, breezy nights"
             "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"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Snow flurries mixing with cold rain",
             "Steady cold rain mixing with wet snow",
             "Prolonged cold, occasional freezing rain",
            "Freezing drizzle at dawn, raw gusty winds",
             "Heavy cloud cover, sleet in the afternoon",
             "Snowfall likely at higher elevations, rain below",
             "Frequent snow showers, gusty winds",
             "Cloudy, damp day with potential sleet storms",
             "Thick overcast, subfreezing nights",
             "Early morning frost turning to slush midday",
             "Lake-effect rain-snow mix near large water bodies",
            "Occasional heavy showers with flurry bursts",
             "Wind chill making days feel colder"
             "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)"] = {
     ["Mediterranean (Hot Summer)"] = {
         Spring = {
         Atosiel = {
             "Mild day, bright sun, low humidity",
             "Mild temperatures, bright sunshine, dry air",
             "Occasional shower, but mostly sunny",
             "Morning dew, warming fast under clear skies",
             "Warm afternoon, cool evening breezes",
             "Cool breezes off the sea, comfortable afternoon",
             "Clear sunrise, gentle inland wind",
            "Sunny midday with a crisp, light wind",
             "Budding vegetation after light morning dew",
             "Short drizzly spell, then mostly clear",
             "Sunny midday, scattered clouds by nightfall",
             "Early bloom across hills, moderate warmth",
             "Light breeze, temperature steadily rising"
             "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"
         },
         },
         Summer = {
         Thalassiel = {
             "Hot, dry heat with strong midday sun",
             "Very hot midday sun, minimal clouds",
             "Clear skies, intense afternoon warmth",
             "Scorching days, slight breeze at dusk",
             "Minimal rain, soaring temperatures",
            "Parched hillsides, no sign of rainfall",
             "Late evening breeze offering slight relief",
            "High UV index, bright, glaring sunlight",
             "High UV index under cloudless skies",
             "Coastal dryness, strong midday glare",
             "Occasional dust carried by inland winds",
             "Overnight relief, but intense heat returns early",
             "Parched landscapes, zero precipitation"
            "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"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Mild to cool days, some light rainfall",
             "Cooler spells, brief showery intervals",
             "Cloudy periods with gentle sea breezes",
             "Cloudy periods bringing mild relief from heat",
             "Moderate temperatures, short bursts of rain",
            "Pleasant afternoons, occasional dusk rains",
             "Crisp mornings, pleasant afternoons",
             "Moderate days, crisp mornings, calm nights",
             "Overcast nights, dew forming on fields",
             "Sporadic light rainfall, refreshing breezes",
             "Rain-laden front passing once a week",
             "Cloudbanks drifting inland, mild drizzle at times",
             "Light wind with occasional heavier showers"
             "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 Desert"] = {
     ["Hot Desert"] = {
         Spring = {
         Atosiel = {
             "Warm days, cooler nights, cloudless skies",
             "Hot afternoons, cooler late nights, wide temp swing",
             "Low humidity, moderate daytime heat",
            "Dry air, bright sunshine, no clouds in sight",
             "Occasional dust devil, mostly clear conditions",
             "Gentle breezes raising light sand by midday",
             "Dry air with gentle warming trend",
            "Evening chill setting in after hot daytime",
             "Sunny skies, evenings dropping in temperature",
             "Minor dust devil scouring the dunes",
             "Gusty winds stirring light sand",
            "Visibility excellent, stable high pressure",
             "Visibility high, no rainfall"
             "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"
         },
         },
         Summer = {
         Thalassiel = {
             "Intense daytime heat, well above average",
             "Extreme heat from mid-morning onward",
             "Scorching midday, zero cloud cover",
            "Blistering midday sun, risk of heat stroke",
             "Possible heat advisories, shimmering horizons",
             "Near-record highs, swirling dust at times",
             "Extremely dry air with frequent dust storms",
             "Virtually no clouds, scorching open sands",
             "Sun sets late, relief only after dark",
             "Occasional dust storm cutting visibility briefly",
             "Mirage-like conditions over open dunes",
             "Mirage-like shimmer across flat terrain",
             "No precipitation, parched desert floor"
            "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"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Warm sunlight, chilly nights",
             "Warm, mostly sunny with cooler nights",
             "Occasional wind picking up sand in gusts",
             "Sparse winds carrying fine sand sporadically",
             "Sparse clouds, moderate daytime temps",
             "Large day-night temperature gap, no precipitation",
             "Stable conditions, large day-night range",
             "Stable conditions, sunlit afternoons, brisk evenings",
             "Sun-drenched afternoons, mild late evenings",
            "Mildly hot days, star-filled clear skies at night",
             "Periodic dust storms after frontal passages",
             "Shallow dust clouds after brief winds",
             "Minimal chance of any rain"
            "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"] = {
     ["Cold Steppe"] = {
         Spring = {
         Atosiel = {
             "Cool morning, warm midday sun, breezy",
             "Cool mornings, moderate midday warmth, breezy",
             "Light rain showers across grasslands",
             "Light rain passing through grasslands midday",
             "Variable cloud cover with mild gusts",
             "Variable cloud cover, mild temperature swings",
             "Patchy frost early, gradually warming",
             "Patchy frost at dawn, warming soon after",
             "Short bursts of sunshine, possible hail",
             "Crisp air, possible small hail if storms appear",
             "Clearing by afternoon, crisp dry air",
            "Sunny afternoon, cooler late evening",
             "Temperature swings dawn to dusk"
            "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"
         },
         },
         Summer = {
         Thalassiel = {
             "Warm daytime, occasional thunderstorms",
             "Warm day, potential for gusty thunderstorms",
             "Hot afternoons, breezy, slight dryness",
             "Hot midday sun, dryness intensifying by late day",
             "Cloud buildup leading to scattered showers",
             "Cloud buildup, short but intense showers possible",
             "Clear early morning, gusty storms later",
             "Clear morning, chance of late afternoon storms",
             "Warm overall, cooler at night in open steppe",
            "Long daylight hours, rolling thunder near dusk",
             "Long sunlit evenings, thunder at dusk",
            "Slight dryness, patchy heat with limited shade",
             "Rainfall minimal but not absent"
            "Periodic storms bringing relief from heat",
             "Warm evening, slight nighttime cooldown",
             "Sky remains mostly clear, grassland shimmering",
             "Thunderheads visible in distance, might pass by"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Cool to cold days, frosty nights",
             "Cool to cold days, frosty nights on open steppe",
             "Occasional snow flurries on grassland",
             "Possible light snowfall, especially overnight",
             "Wind-driven chill under gray skies",
             "Wind-driven chill under gray skies",
             "Sparse precipitation, dryness persists",
             "Sparse precipitation, dryness persists, cold air",
             "Temps below freezing after sunset",
             "Subfreezing after sunset, sometimes sunny midday",
             "Brisk winds, dusting of snow at times",
             "Short bursts of sunshine, otherwise chilly conditions",
             "Sunny breaks mixed with sharp cold air"
            "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"
         }
         }
     },
     },
     ["Hot Steppe"] = {
     ["Hot Steppe"] = {
         Spring = {
         Atosiel = {
             "Hot day, low humidity, strong sunshine",
             "Hot daytime sun, slight breeze, no rain",
             "Gusty wind with dust swirling midday",
             "Gusty wind with dust swirling near midday",
             "Warm mornings, hotter afternoons, no rain",
             "Warm mornings, scorching afternoon, very low humidity",
            "Sun exposure high, sparse vegetation",
             "Light breeze after sundown, starry overhead",
             "Light breeze after sunset, starry night",
             "Some haze from heated ground, minimal cloud cover",
             "Some haze on horizon from dry soils",
            "Sun intensity rising toward midday peak",
             "Dry conditions, mild warming trend"
             "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"
         },
         },
         Summer = {
         Thalassiel = {
             "Very hot, sporadic dust storms",
             "Brutally hot midday sun, patchy dust storms",
             "Scorching midday sun, high evaporation",
             "Scorching conditions, faint breeze offers minimal relief",
             "Light breezes but lingering heat",
            "Occasional swirling wind gusts, drifting dust",
             "Dust-laden air, minimal rainfall chance",
            "Heated ground emits rippling mirages at distance",
             "Warm nights with limited relief",
            "Persistent dryness, minimal nighttime cooldown",
             "Periodic windy gusts raising dust plumes",
             "Skies remain cloudless, high evaporation rate",
             "Enduring dryness, heat extremes persist"
             "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"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Hot days, warm evenings, stable dryness",
             "Hot days, moderate evenings, stable dryness",
             "Rare rainfall, short bursts if clouds appear",
             "Rare short rains if storm fronts approach, mostly none",
             "Temps remain high, some cooler nights",
             "Daytime heat lingering, nights slightly cooler",
             "Periodic hazy mornings from airborne dust",
             "Periodically hazy morning from airborne dust",
             "Mild breezes, daytime remains hot",
             "Mild breezes, daytime remains intensely hot",
             "Cloudless skies, slight drop in humidity",
             "Cloudless skies, gentle drop in temps overnight",
             "Wind shifts occasionally bringing cooler gusts"
             "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"
         }
         }
     },
     },
     ["Subarctic"] = {
     ["Subarctic"] = {
         Spring = {
         Atosiel = {
             "Snow gradually melting, slushy paths",
             "Snow melting, slushy paths, daytime sunshine",
             "Chilly breezes, frequent flurries possible",
             "Chilly breezes, frequent flurry patches possible",
             "Long nights slowly shortening, crisp mornings",
             "Long nights gradually shortening, crisp mornings",
             "Light snowfall in higher elevations, cold rain lower",
             "Mixed precipitation, cold rain at lower altitudes",
             "Sunny breaks on clear days, sub-zero nights",
            "Slush build-up midmorning, partial sun midday",
             "Occasional storms with mixed precipitation",
             "Sunny breaks around noon, subzero by late night",
             "Icy patches remain until late season"
             "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"
         },
         },
         Summer = {
         Thalassiel = {
             "Short, cool days, moderate sunshine",
             "Short, cool days, moderate sunshine at times",
             "Rainy spells, possible sleet in higher areas",
             "Rainy intervals, potential sleet in highest peaks",
             "Snow line retreating, green valleys emerging",
             "Snow line retreating, green valleys emerging slowly",
             "Lengthy twilight, crisp air after sundown",
             "Extended twilight, crisp air after sundown",
             "Sudden temperature dips even midday",
             "Sudden midafternoon chill despite mid-summer period",
             "Drizzle common, heavy rainfall occasional",
             "Drizzle commonly, heavier rainfall occasionally",
             "Cool breezes from mountain passes"
            "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"
         },
         },
         ["Fall/Winter"] = {
         Opsitheiel = {
             "Early snowfall, ground freezing quickly",
             "Early snowfall returning, freezing ground rapidly",
             "Prolonged darkness, bitter cold sets in",
             "Prolonged darkness, bitterness sets in quickly",
             "Gusty winds carrying ice crystals",
             "Gusty winds carrying ice crystals through valleys",
             "Heavy accumulations, drifting snowbanks",
             "Heavy drifts forming, multiple snowfall events",
             "Frequent sub-zero nights, minimal thaw",
             "Frequent subzero lows, no hint of thaw",
             "Blizzard conditions with strong fronts",
             "Blizzard-like conditions with large snow accumulations",
             "Persistent ice coverage on lakes and rivers"
             "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"
         }
         }
     }
     }
Line 295: Line 406:


---------------------------------------------------------
---------------------------------------------------------
-- 4. List of Cities, spelled out, no abbreviations
-- 5. triggeredDisasters, getAdvisory
---------------------------------------------------------
---------------------------------------------------------
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"      },
    { keywords = {"landslides","steep slopes"},            hazard = "Landslides on steep slopes"          },
    { keywords = {"droughts","brushfires"},                hazard = "Droughts and brushfires"              },
    { keywords = {"recurrent flooding in marshes","flooding in marshes"}, hazard = "Recurrent flooding in marshes"},
    { keywords = {"localized mudslides"},                  hazard = "Localized mudslides"                  },
    { keywords = {"fog%-related travel hazards"},          hazard = "Fog-related travel hazards"          },
    { keywords = {"swamp flooding"},                        hazard = "Swamp flooding"                      },
    { keywords = {"river floods"},                          hazard = "River floods"                        },
    { keywords = {"blizzards"},                            hazard = "Blizzards"                            },
    { keywords = {"dust storms","heatwaves"},              hazard = "Dust storms and heatwaves"            },
    { keywords = {"flash floods"},                          hazard = "Occasional flash floods"              },
    { keywords = {"tar%-pit gas releases"},                hazard = "Tar-pit gas releases"                },
    { keywords = {"landslides on steep slopes"},            hazard = "Landslides on steep slopes"          },
    { keywords = {"coastal storms"},                        hazard = "Coastal storms"                      },
    { keywords = {"mild landslides"},                      hazard = "Mild landslides"                      },
    { keywords = {"summer wildfires"},                      hazard = "Summer wildfires"                    },
    { keywords = {"cliff erosion","sea storms"},            hazard = "Cliff erosion and sea storms"        },
    { keywords = {"flood surges near waterfalls"},          hazard = "Flood surges near waterfalls"        },
    { keywords = {"coastal gales","blizzards"},            hazard = "Coastal gales, blizzards"            },
    { keywords = {"marsh flooding"},                        hazard = "Marsh flooding"                      },
    { keywords = {"cliff collapses"},                      hazard = "Occasional cliff collapses"          },
    { keywords = {"sandstorms"},                            hazard = "Sandstorms"                          },
    { keywords = {"minor landslides"},                      hazard = "Minor landslides"                    },
    { keywords = {"lake overflows","marsh flooding"},      hazard = "Lake overflows, marsh flooding"      },
    { keywords = {"avalanches in higher passes"},          hazard = "Avalanches in higher passes"          },
    { keywords = {"shoreline flooding"},                    hazard = "Shoreline flooding"                  },
    { keywords = {"wildfires in dry spells"},              hazard = "Wildfires in dry spells"              },
    { keywords = {"droughts"},                              hazard = "Droughts"                            },
    { keywords = {"swamp overflows"},                      hazard = "Swamp overflows"                      },
    { keywords = {"landslides near hot springs"},          hazard = "Occasional landslides near hot springs"},
    { keywords = {"dust devils on dry slopes"},            hazard = "Dust devils on dry slopes"            },
    { keywords = {"storm surges from inlet"},              hazard = "Storm surges from inlet"              },
    { keywords = {"mine collapses","gas leaks"},            hazard = "Mine collapses, gas leaks"            },
    { keywords = {"frequent rain squalls", "heavy downpours", "intermittent heavy downpours"}, hazard = "Localized mudslides" },
    { keywords = {"blizzard%-like conditions"}, hazard = "Blizzards" },
    { keywords = {"extreme heat", "intense heat", "heat advisory"}, hazard = "Heat wave" },
    { keywords = {"rain squalls", "persistent dryness", "dry conditions"}, hazard = "Risk of brushfires" },
    { keywords = {"foggy dawn", "low visibility", "dense fog"}, hazard = "Fog-related hazards" },
    { keywords = {"heavy snowfall", "gusty winds carrying ice", "deep winter mode"}, hazard = "Possible avalanche" },
    { keywords = {"strong desert winds", "dust storm", "mirage%-like shimmer"}, hazard = "Sandstorm risk" },
    { keywords = {"swamp flooding", "marsh flooding"}, hazard = "Potential flood surge" }
}
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
-- We limit wind speeds to 0-15 km/h for more Earth-like mild wind conditions
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,15)  -- narrower Earth-like range
    return {
        high        = hi,
        low        = lo,
        humidity    = hum,
        chanceOfRain= cRain,
        windDir    = wDir,
        windSpeed  = wSpd
    }
end
---------------------------------------------------------
-- 8. City Data
---------------------------------------------------------
local cityData = {
local cityData = {
    -- Major Cities in Bassaridia Vaeringheim
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Vaeringheim Vaeringheim]",      climate = "Humid Subtropical"},
     {city = "Vaeringheim",      climate = "Humid Subtropical"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Luminaria Luminaria]",        climate = "Oceanic"},
     {city = "Luminaria",        climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Serena Serena]",               climate = "Subpolar Oceanic"},
     {city = "Serena",           climate = "Subpolar Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Pyralis Pyralis]",             climate = "Oceanic"},
     {city = "Pyralis",           climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Symphonara Symphonara]",       climate = "Oceanic"},
     {city = "Symphonara",       climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aurelia Aurelia]",             climate = "Mediterranean (Hot Summer)"},
     {city = "Aurelia",           climate = "Mediterranean (Hot Summer)"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Somniumpolis Somniumpolis]",   climate = "Humid Subtropical"},
     {city = "Somniumpolis",     climate = "Humid Subtropical"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Nexa Nexa]",                   climate = "Oceanic"},
     {city = "Nexa",             climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Lunalis_Sancta Lunalis Sancta]", climate = "Oceanic"},
     {city = "Lunalis Sancta",   climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Sylvapolis Sylvapolis]",       climate = "Humid Subtropical"},
     {city = "Sylvapolis",       climate = "Humid Subtropical"},


    -- Minor Cities in Bassaridia Vaeringheim
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Saluria Saluria]",             climate = "Oceanic"},
     {city = "Saluria",           climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aetherium Aetherium]",        climate = "Subarctic"},
     {city = "Aetherium",        climate = "Subarctic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Ferrum_Citadel Ferrum Citadel]", climate = "Hot Desert"},
     {city = "Ferrum Citadel",   climate = "Hot Desert"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Acheron Acheron]",             climate = "Cold Steppe"},
     {city = "Acheron",           climate = "Cold Steppe"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Erythros Erythros]",           climate = "Oceanic"},
     {city = "Erythros",         climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Catonis_Atrium Catonis Atrium]", climate = "Oceanic"},
     {city = "Catonis Atrium",   climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Delphica Delphica]",           climate = "Oceanic"},
     {city = "Delphica",         climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Koinonía Koinonía]",           climate = "Oceanic"},
     {city = "Koinonía",         climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aureum Aureum]",               climate = "Mediterranean (Hot Summer)"},
     {city = "Aureum",           climate = "Mediterranean (Hot Summer)"},


    -- Major Cities in New South Jangsong
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Skýrophos Skýrophos]",        climate = "Oceanic"},
     {city = "Skýrophos",        climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Bjornopolis Bjornopolis]",     climate = "Oceanic"},
     {city = "Bjornopolis",       climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aegirheim Aegirheim]",        climate = "Subarctic"},
     {city = "Aegirheim",        climate = "Subarctic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Norsolyra Norsolyra]",        climate = "Oceanic"},
     {city = "Norsolyra",        climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Thorsalon Thorsalon]",        climate = "Oceanic"},
     {city = "Thorsalon",        climate = "Oceanic"},


    -- Minor Cities in New South Jangsong
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Pelagia Pelagia]",             climate = "Hot Steppe"},
     {city = "Pelagia",           climate = "Hot Steppe"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Myrene Myrene]",               climate = "Oceanic"},
     {city = "Myrene",           climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Thyrea Thyrea]",               climate = "Humid Subtropical"},
     {city = "Thyrea",           climate = "Humid Subtropical"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Ephyra Ephyra]",               climate = "Subpolar Oceanic"},
     {city = "Ephyra",           climate = "Subpolar Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Halicarn Halicarn]",           climate = "Mediterranean (Hot Summer)"},
     {city = "Halicarn",         climate = "Mediterranean (Hot Summer)"},


    -- Major Cities in Haifan Bassaridia
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Keybir-Aviv Keybir-Aviv]",     climate = "Humid Subtropical"},
     {city = "Keybir-Aviv",       climate = "Humid Subtropical"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Tel-Amin Tel-Amin]",           climate = "Mediterranean (Hot Summer)"},
     {city = "Tel-Amin",         climate = "Mediterranean (Hot Summer)"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Diamandis Diamandis]",        climate = "Mediterranean (Hot Summer)"},
     {city = "Diamandis",        climate = "Mediterranean (Hot Summer)"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Jogi Jogi]",                   climate = "Oceanic"},
     {city = "Jogi",             climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Lewisburg Lewisburg]",        climate = "Humid Subtropical"},
     {city = "Lewisburg",        climate = "Humid Subtropical"},


    -- Minor Cities in Haifan Bassaridia
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Thermosalem Thermosalem]",     climate = "Oceanic"},
     {city = "Thermosalem",       climate = "Oceanic"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Akróstadium Akróstadium]",     climate = "Cold Steppe"},
     {city = "Akróstadium",       climate = "Cold Steppe"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Sufriya Sufriya]",             climate = "Humid Subtropical"},
     {city = "Sufriya",           climate = "Humid Subtropical"},
     {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Lykopolis Lykopolis]",        climate = "Oceanic"}
     {city = "Lykopolis",        climate = "Oceanic"}
}
}


---------------------------------------------------------
---------------------------------------------------------
-- 5. Main Weather Forecast Function
-- 9. Color-coded cell background
--    Called via {{#invoke:WeatherForecast|weatherForecast}}
---------------------------------------------------------
---------------------------------------------------------
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
--    Overriding chance of rain if precipitation is mentioned
---------------------------------------------------------
function p.weatherForecast(frame)
function p.weatherForecast(frame)
     -- A) Determine the date and season
     -- Random seed changes daily
     local dateInfo = getCurrentDateInfo()
     local date = os.date("*t")
     local season = getSeason(dateInfo.dayOfYear)
     local dailySeed = (date.year * 1000) + date.yday
    math.randomseed(dailySeed)


     local output = {}
     local dateInfo  = getCurrentDateInfo()
     table.insert(output, "== Daily Weather Forecast ==\n")
    local dayOfYear  = dateInfo.dayOfYear
     table.insert(output, "''(Day " .. dateInfo.dayOfYear
    local yearNumber = dateInfo.psscYear
        .. " of Year " .. dateInfo.psscYear
    local seasonName = getSeasonName(dayOfYear)
        .. " PSSC, " .. season .. ")''\n\n"
 
    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
        )
     )
     )


     -- B) Loop through each city
     table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n')
     for _, cityEntry in ipairs(cityData) do
    table.insert(out, [[
         local cityName = cityEntry.city
! City
         local climateName = cityEntry.climate
! 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


         local seasonTable = climateEvents[climateName] and climateEvents[climateName][season]
         -- 2) Stats
         local forecast = "No forecast data."
         local stats = getRandomWeatherStats(climateName, seasonName)


         if seasonTable and #seasonTable > 0 then
        -- If the daily weather text has precipitation, override chanceOfRain:
             local rndIndex = math.random(#seasonTable)
        local forecastLower = fStr:lower()
            forecast = seasonTable[rndIndex]
         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
         end


         table.insert(output, "=== " .. cityName .. " ===\n")
         local hiF  = cToF(stats.high)
         table.insert(output, "* '''Climate:''' " .. climateName .. "\n")
        local loF  = cToF(stats.low)
         table.insert(output, "* '''Season:''' " .. season .. "\n")
        local hum  = stats.humidity
         table.insert(output, "* '''Today's Weather:''' " .. forecast .. "\n\n")
        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
     end


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


---------------------------------------------------------
-- 10B. SINGLE-CITY FORECAST FUNCTION
--    Similarly overrides chance of rain if precipitation is found
---------------------------------------------------------
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
    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 = {
    { name="Agave",    starClass="M", lat=29.5 },
    { name="Amaäz",    starClass="K", lat=31  },
    { name="Amáenu",  starClass="G", lat=45  },
    { name="Amap",    starClass="A", lat=55  },
    { name="Amazä",    starClass="F", lat=39  },
    { name="Atämios",  starClass="F", lat=56  },
    { name="Aprobelle",starClass="A", lat=59  },
    { name="Bebeakaus",starClass="K", lat=30.5 },
    { name="Bulhanu",  starClass="A", lat=40  },
    { name="Crösacío", starClass="K", lat=17  },
    { name="Danaß",    starClass="A", lat=67  },
    { name="Dilëtaz",  starClass="G", lat=36  },
    { name="Dranamos", starClass="A", lat=58  },
    { name="Gaht",    starClass="G", lat=29  },
    { name="Häpi",    starClass="F", lat=78  },
    { name="Hazaméos", starClass="A", lat=72  },
    { name="Liléigos", starClass="F", lat=18  },
    { name="Nyama",    starClass="F", lat=64  },
    { name="Ocananus", starClass="F", lat=60.5 },
    { name="Orebele",  starClass="F", lat=22  },
    { name="Osiríos",  starClass="G", lat=53  },
    { name="Pythe",    starClass="A", lat=45  },
    { name="Sanashalo",starClass="G", lat=27  },
    { name="Tä",      starClass="F", lat=39.5 },
    { name="Vï",      starClass="A", lat=46  },
    { name="Wedíos",  starClass="A", lat=54  },
}
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
return p

Latest revision as of 19:50, 2 January 2025

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 = {
    ["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
--    (all text remains unchanged)
---------------------------------------------------------
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 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"
        }
    },
    ["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"
        }
    },
    ["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. triggeredDisasters, getAdvisory
---------------------------------------------------------
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"      },
    { keywords = {"landslides","steep slopes"},             hazard = "Landslides on steep slopes"           },
    { keywords = {"droughts","brushfires"},                 hazard = "Droughts and brushfires"              },
    { keywords = {"recurrent flooding in marshes","flooding in marshes"}, hazard = "Recurrent flooding in marshes"},
    { keywords = {"localized mudslides"},                   hazard = "Localized mudslides"                  },
    { keywords = {"fog%-related travel hazards"},           hazard = "Fog-related travel hazards"           },
    { keywords = {"swamp flooding"},                        hazard = "Swamp flooding"                       },
    { keywords = {"river floods"},                          hazard = "River floods"                         },
    { keywords = {"blizzards"},                             hazard = "Blizzards"                            },
    { keywords = {"dust storms","heatwaves"},               hazard = "Dust storms and heatwaves"            },
    { keywords = {"flash floods"},                          hazard = "Occasional flash floods"              },
    { keywords = {"tar%-pit gas releases"},                 hazard = "Tar-pit gas releases"                 },
    { keywords = {"landslides on steep slopes"},            hazard = "Landslides on steep slopes"           },
    { keywords = {"coastal storms"},                        hazard = "Coastal storms"                       },
    { keywords = {"mild landslides"},                       hazard = "Mild landslides"                      },
    { keywords = {"summer wildfires"},                      hazard = "Summer wildfires"                     },
    { keywords = {"cliff erosion","sea storms"},            hazard = "Cliff erosion and sea storms"         },
    { keywords = {"flood surges near waterfalls"},          hazard = "Flood surges near waterfalls"         },
    { keywords = {"coastal gales","blizzards"},             hazard = "Coastal gales, blizzards"             },
    { keywords = {"marsh flooding"},                        hazard = "Marsh flooding"                       },
    { keywords = {"cliff collapses"},                       hazard = "Occasional cliff collapses"           },
    { keywords = {"sandstorms"},                            hazard = "Sandstorms"                           },
    { keywords = {"minor landslides"},                      hazard = "Minor landslides"                     },
    { keywords = {"lake overflows","marsh flooding"},       hazard = "Lake overflows, marsh flooding"       },
    { keywords = {"avalanches in higher passes"},           hazard = "Avalanches in higher passes"          },
    { keywords = {"shoreline flooding"},                    hazard = "Shoreline flooding"                   },
    { keywords = {"wildfires in dry spells"},               hazard = "Wildfires in dry spells"              },
    { keywords = {"droughts"},                              hazard = "Droughts"                             },
    { keywords = {"swamp overflows"},                       hazard = "Swamp overflows"                      },
    { keywords = {"landslides near hot springs"},           hazard = "Occasional landslides near hot springs"},
    { keywords = {"dust devils on dry slopes"},             hazard = "Dust devils on dry slopes"            },
    { keywords = {"storm surges from inlet"},               hazard = "Storm surges from inlet"              },
    { keywords = {"mine collapses","gas leaks"},            hazard = "Mine collapses, gas leaks"            },
    { keywords = {"frequent rain squalls", "heavy downpours", "intermittent heavy downpours"}, hazard = "Localized mudslides" },
    { keywords = {"blizzard%-like conditions"}, hazard = "Blizzards" },
    { keywords = {"extreme heat", "intense heat", "heat advisory"}, hazard = "Heat wave" },
    { keywords = {"rain squalls", "persistent dryness", "dry conditions"}, hazard = "Risk of brushfires" },
    { keywords = {"foggy dawn", "low visibility", "dense fog"}, hazard = "Fog-related hazards" },
    { keywords = {"heavy snowfall", "gusty winds carrying ice", "deep winter mode"}, hazard = "Possible avalanche" },
    { keywords = {"strong desert winds", "dust storm", "mirage%-like shimmer"}, hazard = "Sandstorm risk" },
    { keywords = {"swamp flooding", "marsh flooding"}, hazard = "Potential flood surge" }
}

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

-- We limit wind speeds to 0-15 km/h for more Earth-like mild wind conditions
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,15)  -- narrower Earth-like range

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

---------------------------------------------------------
-- 8. City Data
---------------------------------------------------------
local cityData = {
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Vaeringheim Vaeringheim]",       climate = "Humid Subtropical"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Luminaria Luminaria]",         climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Serena Serena]",               climate = "Subpolar Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Pyralis Pyralis]",             climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Symphonara Symphonara]",       climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aurelia Aurelia]",             climate = "Mediterranean (Hot Summer)"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Somniumpolis Somniumpolis]",   climate = "Humid Subtropical"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Nexa Nexa]",                   climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Lunalis_Sancta Lunalis Sancta]", climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Sylvapolis Sylvapolis]",       climate = "Humid Subtropical"},

    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Saluria Saluria]",             climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aetherium Aetherium]",         climate = "Subarctic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Ferrum_Citadel Ferrum Citadel]", climate = "Hot Desert"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Acheron Acheron]",             climate = "Cold Steppe"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Erythros Erythros]",           climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Catonis_Atrium Catonis Atrium]", climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Delphica Delphica]",           climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Koinonía Koinonía]",           climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aureum Aureum]",               climate = "Mediterranean (Hot Summer)"},

    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Skýrophos Skýrophos]",         climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Bjornopolis Bjornopolis]",     climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Aegirheim Aegirheim]",         climate = "Subarctic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Norsolyra Norsolyra]",         climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Thorsalon Thorsalon]",         climate = "Oceanic"},

    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Pelagia Pelagia]",             climate = "Hot Steppe"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Myrene Myrene]",               climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Thyrea Thyrea]",               climate = "Humid Subtropical"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Ephyra Ephyra]",               climate = "Subpolar Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Halicarn Halicarn]",           climate = "Mediterranean (Hot Summer)"},

    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Keybir-Aviv Keybir-Aviv]",     climate = "Humid Subtropical"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Tel-Amin Tel-Amin]",           climate = "Mediterranean (Hot Summer)"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Diamandis Diamandis]",         climate = "Mediterranean (Hot Summer)"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Jogi Jogi]",                   climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Lewisburg Lewisburg]",         climate = "Humid Subtropical"},

    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Thermosalem Thermosalem]",     climate = "Oceanic"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Akróstadium Akróstadium]",     climate = "Cold Steppe"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Sufriya Sufriya]",             climate = "Humid Subtropical"},
    {city = "[https://micras.org/mwiki/List_of_cities_in_Bassaridia_Vaeringheim#Lykopolis Lykopolis]",         climate = "Oceanic"}
}

---------------------------------------------------------
-- 9. 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

---------------------------------------------------------
-- 10A. ALL-CITIES FORECAST FUNCTION
--     Overriding chance of rain if precipitation is mentioned
---------------------------------------------------------
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, override chanceOfRain:
        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 overrides chance of rain if precipitation is found
---------------------------------------------------------
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
    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 = {
    { name="Agave",    starClass="M", lat=29.5 },
    { name="Amaäz",    starClass="K", lat=31   },
    { name="Amáenu",   starClass="G", lat=45   },
    { name="Amap",     starClass="A", lat=55   },
    { name="Amazä",    starClass="F", lat=39   },
    { name="Atämios",  starClass="F", lat=56   },
    { name="Aprobelle",starClass="A", lat=59   },
    { name="Bebeakaus",starClass="K", lat=30.5 },
    { name="Bulhanu",  starClass="A", lat=40   },
    { name="Crösacío", starClass="K", lat=17   },
    { name="Danaß",    starClass="A", lat=67   },
    { name="Dilëtaz",  starClass="G", lat=36   },
    { name="Dranamos", starClass="A", lat=58   },
    { name="Gaht",     starClass="G", lat=29   },
    { name="Häpi",     starClass="F", lat=78   },
    { name="Hazaméos", starClass="A", lat=72   },
    { name="Liléigos", starClass="F", lat=18   },
    { name="Nyama",    starClass="F", lat=64   },
    { name="Ocananus", starClass="F", lat=60.5 },
    { name="Orebele",  starClass="F", lat=22   },
    { name="Osiríos",  starClass="G", lat=53   },
    { name="Pythe",    starClass="A", lat=45   },
    { name="Sanashalo",starClass="G", lat=27   },
    { name="Tä",       starClass="F", lat=39.5 },
    { name="Vï",       starClass="A", lat=46   },
    { name="Wedíos",   starClass="A", lat=54   },
}

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