Module:WarLeagueDivisionOpsBVR: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
Created page with "-- Module:WarLeagueDivisionOpsBVR -- Daily, in-world War League divisional operations log. -- Integrates Module:BassaridianDailyEvents by parsing today’s city-event/urgency table output. :contentReference[oaicite:2]{index=2} local p = {} -------------------------------------------------------------------------------- -- (A) PSSC DATE (same logic used by Module:BassaridianDailyEvents) :contentReference[oaicite:3]{index=3} ----------------------------------------------..."
 
NewZimiaGov (talk | contribs)
No edit summary
Line 1: Line 1:
-- Module:WarLeagueDivisionOpsBVR
-- Module:WarLeagueDivisionOpsBVR
-- Daily, in-world War League divisional operations log.
-- Daily, in-world War League divisional operations log.
-- Integrates Module:BassaridianDailyEvents by parsing today’s city-event/urgency table output. :contentReference[oaicite:2]{index=2}
-- Integrates Module:BassaridianDailyEvents by parsing today’s city-event/urgency table output.


local p = {}
local p = {}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (A) PSSC DATE (same logic used by Module:BassaridianDailyEvents) :contentReference[oaicite:3]{index=3}
-- (A) PSSC DATE (same logic used by Module:BassaridianDailyEvents)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function getPSSCDate()
local function getPSSCDate()
Line 63: Line 63:
     local anchor = anchorOverride or cityName
     local anchor = anchorOverride or cityName
     return string.format("[[List_of_cities_in_Bassaridia_Vaeringheim#%s|%s]]", anchor, cityName)
     return string.format("[[List_of_cities_in_Bassaridia_Vaeringheim#%s|%s]]", anchor, cityName)
end
local function hwLink(page, anchor, label)
    if anchor and anchor ~= "" then
        return string.format("[[%s#%s|%s]]", page, anchor, label or anchor)
    end
    return string.format("[[%s|%s]]", page, label or page)
end
end


local function opId(di, divCode, idx)
local function opId(di, divCode, idx)
     return string.format("WL-%03d.%03d-%s-%02d", di.year, di.dayOfYear, divCode, idx)
     return string.format("WL-%03d.%03d-%s-%02d", di.year, di.dayOfYear, divCode, idx)
end
local function clamp(n, lo, hi)
    if n < lo then return lo end
    if n > hi then return hi end
    return n
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (C) INTEGRATE DAILY EVENTS (parse Module:BassaridianDailyEvents.makeTable()) :contentReference[oaicite:4]{index=4}
-- (C) INTEGRATE DAILY EVENTS (parse Module:BassaridianDailyEvents.makeTable())
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function buildEventMapForToday()
local function buildEventMapForToday()
Line 91: Line 78:
     end
     end


    -- DailyEvents reseeds RNG internally for the day (fine).
     local tbl = daily.makeTable() -- returns wikitable text with rows: "| date || city || event || urgency"
     local tbl = daily.makeTable() -- returns wikitable text with rows: "| date || city || event || urgency" :contentReference[oaicite:5]{index=5}
     local eventMap = {}
     local eventMap = {}


     for line in tostring(tbl):gmatch("[^\n]+") do
     for line in tostring(tbl):gmatch("[^\n]+") do
        -- match row lines (ignore headers and separators)
         if line:match("^|%s") and not line:match("^|%-%s*$") then
         if line:match("^|%s") and not line:match("^|%-%s*$") then
            -- split by "||"
             local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
             local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
             if #parts >= 4 then
             if #parts >= 4 then
                local date = parts[1]
                 local city = parts[2]
                 local city = parts[2]
                 local ev  = parts[3]
                 local ev  = parts[3]
                 local urg  = parts[4]
                 local urg  = parts[4]
                -- store only first occurrence per city (DailyEvents is one row per city) :contentReference[oaicite:6]{index=6}
                 if city and city ~= "" and not eventMap[city] then
                 if city and city ~= "" and not eventMap[city] then
                     eventMap[city] = { date = date, desc = ev, urgency = urg }
                     eventMap[city] = { desc = ev, urgency = urg }
                 end
                 end
             end
             end
Line 117: Line 99:


local function urgencyScore(urg)
local function urgencyScore(urg)
    -- align with DailyEvents urgency labels :contentReference[oaicite:7]{index=7}
     if urg == "CRITICAL" then return 4 end
     if urg == "CRITICAL" then return 4 end
     if urg == "HIGH"    then return 3 end
     if urg == "HIGH"    then return 3 end
Line 150: Line 131:


local function pickEchelonForUrgency(echelons, uScore)
local function pickEchelonForUrgency(echelons, uScore)
    -- uScore 0..4: higher -> more weight toward larger formations
     local tuned = {}
     local tuned = {}
     for _, e in ipairs(echelons) do
     for _, e in ipairs(echelons) do
         local w = e.weight or 1
         local w = e.weight or 1
        -- bias: larger formations have naturally lower weights; boost them with urgency
         if uScore >= 3 then
         if uScore >= 3 then
             if e.name:match("Cohortes") or e.name:match("Ordu %(") or e.name:match("Moira") then
             if e.name:match("Cohortes") or e.name:match("Ordu %(") or e.name:match("Moira") then
Line 172: Line 151:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (E) DIVISIONS + CITY POOLS (canonical names; links go to List_of_cities sections) :contentReference[oaicite:8]{index=8}
-- (E) DIVISIONS + CITY POOLS
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local CITY = {
    -- Core majors
    "Vaeringheim","Luminaria","Serena","Pyralis","Symphonara","Aurelia","Somniumpolis","Nexa","Lunalis Sancta","Sylvapolis",
    -- Core minors
    "Saluria","Aetherium","Ferrum Citadel","Acheron","Erythros","Catonis Atrium","Delphica","Koinonía","Aureum",
    -- Alperkin
    "The Alpazkigz","Odiferia",
    -- New South Jangsong
    "Skýrophos","Bjornopolis","Aegirheim","Norsolyra","Thorsalon","Pelagia","Myrene","Thyrea","Ephyra","Halicarn",
    -- Haifan Bassaridia
    "Keybir-Aviv","Tel-Amin","Diamandis","Jogi","Lewisburg","Thermosalem","Akróstadium","Sufriya","Lykopolis",
    -- Normark
    "Ardclach","Riddersborg",
    -- Ouriana
    "Bashkim","Ourid","Tonar",
    -- Caledonian dependencies (canonical 9)
    "Fanghorn","Eikbu","Galvø","Sårensby","Slevik","Sjøsborg","Storesund","Notranskja","Kaledonija",
}
local DIVISIONS = {
local DIVISIONS = {
     { name="Vaeringheim Division",       link="[[Vaeringheim Division]]",       code="VAE", weight=5,
     { name="Vaeringheim Division",       link="[[Vaeringheim Division]]",       code="VAE", weight=5,
       cities={"Vaeringheim","Luminaria","Serena","Symphonara","Aurelia","Somniumpolis","Nexa","Lunalis Sancta","Sylvapolis","Delphica","Catonis Atrium","Ferrum Citadel"} },
       cities={"Vaeringheim","Luminaria","Serena","Symphonara","Aurelia","Somniumpolis","Nexa","Lunalis Sancta","Sylvapolis","Delphica","Catonis Atrium","Ferrum Citadel"} },
     { name="Alpazkigz Division",         link="[[Alpazkigz Division]]",         code="ALP", weight=4,
     { name="Alpazkigz Division",         link="[[Alpazkigz Division]]",         code="ALP", weight=4,
       cities={"The Alpazkigz","Pyralis","Acheron","Erythros","Aureum","Koinonía","Saluria","Aetherium","Fanghorn","Eikbu","Galvø","Sårensby","Slevik","Sjøsborg","Storesund","Notranskja","Kaledonija"} },
       cities={"The Alpazkigz","Pyralis","Acheron","Erythros","Aureum","Koinonía","Saluria","Aetherium","Fanghorn","Eikbu","Galvø","Sårensby","Slevik","Sjøsborg","Storesund","Notranskja","Kaledonija"} },
     { name="Odiferia Division",         link="[[Odiferia Division]]",         code="ODF", weight=3,
     { name="Odiferia Division",           link="[[Odiferia Division]]",           code="ODF", weight=3,
       cities={"Odiferia","Somniumpolis","Aetherium","Ferrum Citadel","Aurelia","Symphonara"} },
       cities={"Odiferia","Somniumpolis","Aetherium","Ferrum Citadel","Aurelia","Symphonara"} },
     { name="Hatch Ministry Division",   link="[[Hatch Ministry Division]]",   code="HAT", weight=3,
     { name="Hatch Ministry Division",     link="[[Hatch Ministry Division]]",     code="HAT", weight=3,
       cities={"Lewisburg","Lykopolis","Jogi","Keybir-Aviv","Tel-Amin","Diamandis","Thermosalem","Akróstadium","Sufriya"} },
       cities={"Lewisburg","Lykopolis","Jogi","Keybir-Aviv","Tel-Amin","Diamandis","Thermosalem","Akróstadium","Sufriya"} },
     { name="New South Jangsong Division",link="[[New South Jangsong Division]]",code="NSJ", weight=3,
     { name="New South Jangsong Division", link="[[New South Jangsong Division]]", code="NSJ", weight=3,
       cities={"Skýrophos","Bjornopolis","Aegirheim","Norsolyra","Thorsalon","Pelagia","Myrene","Thyrea","Ephyra","Halicarn"} },
       cities={"Skýrophos","Bjornopolis","Aegirheim","Norsolyra","Thorsalon","Pelagia","Myrene","Thyrea","Ephyra","Halicarn"} },
     { name="Haifan Bassaridia Division", link="[[Haifan Bassaridia Division]]", code="HFA", weight=3,
     { name="Haifan Bassaridia Division", link="[[Haifan Bassaridia Division]]", code="HFA", weight=3,
       cities={"Keybir-Aviv","Tel-Amin","Diamandis","Jogi","Lewisburg","Thermosalem","Akróstadium","Sufriya","Lykopolis"} },
       cities={"Keybir-Aviv","Tel-Amin","Diamandis","Jogi","Lewisburg","Thermosalem","Akróstadium","Sufriya","Lykopolis"} },
     { name="Bassaridian Normark Division",link="[[Bassaridian Normark Division]]",code="BNO",weight=2,
     { name="Bassaridian Normark Division",link="[[Bassaridian Normark Division]]",code="BNO",weight=2,
       cities={"Ardclach","Riddersborg"} },
       cities={"Ardclach","Riddersborg"} },
     { name="Ouriana Division",           link="[[Ouriana Division]]",           code="OUR", weight=2,
     { name="Ouriana Division",           link="[[Ouriana Division]]",           code="OUR", weight=2,
       cities={"Bashkim","Ourid","Tonar"} },
       cities={"Bashkim","Ourid","Tonar"} },
     { name="Council of Kings Division", link="[[Council of Kings Division]]", code="CKD", weight=1,
     { name="Council of Kings Division",   link="[[Council of Kings Division]]",   code="CKD", weight=1,
       cities={"Vaeringheim","Bashkim","Ourid","Tonar"} },
       cities={"Vaeringheim","Bashkim","Ourid","Tonar"} },
}
}
Line 235: Line 195:


local OP_AIR = {
local OP_AIR = {
     { name="ISR overwatch / route scanning (drones / AEW&C cueing)", weight=4 },
     { name="ISR overwatch / route scanning",                       weight=4 },
     { name="rapid extraction / medevac lift",                      weight=2 },
     { name="rapid extraction / medevac lift",                      weight=2 },
     { name="strike deterrence package (show of force)",            weight=1 },
     { name="strike deterrence package (show of force)",            weight=1 },
Line 241: Line 201:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (G) SYSTEMS / PLATFORMS (Somniant + Eosphorus links)
-- (G) OVERSIGHT + COMPLIANCE
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local SYSTEMS = {
    hwLink("Somniant_Stock_Fund_Military_Hardware", "Syrinx_Class_Armored_Infantry_Vehicle", "Syrinx Class Armored Infantry Vehicle"),
    hwLink("Somniant_Stock_Fund_Military_Hardware", "Thalassa_Class_Main_Battle_Tank", "Thalassa Class Main Battle Tank"),
    hwLink("Somniant_Stock_Fund_Military_Hardware", "Misttalon_Class_Heavy_Bomber", "Misttalon Class Heavy Bomber"),
    hwLink("Somniant_Stock_Fund_Military_Hardware", "Ventiflor_Eye_Class_AEW%26C_Aircraft", "Ventiflor Eye Class AEW&C Aircraft"),
    hwLink("Somniant_Stock_Fund_Military_Hardware", "Cetus_Class_Attack_Craft", "Cetus Class Attack Craft"),
    hwLink("Somniant_Stock_Fund_Military_Hardware", "Aetherium_Class_Patrol_Ship", "Aetherium Class Patrol Ship"),
    hwLink("Somniant_Stock_Fund_Military_Hardware", "Saluria_Class_Gunboat", "Saluria Class Gunboat"),
    hwLink("Eosphorus_Motor_Company", "Model_V-99_Thunderguard_Citadel_Train", "Model V-99 Thunderguard Citadel Train"),
}
local OVERSIGHT = {
local OVERSIGHT = {
     { label="Senate of Elders liaison",                weight=3 },
     { label="Senate of Elders liaison",                weight=3 },
Line 263: Line 212:


local COMPLIANCE = {
local COMPLIANCE = {
     { label="Somniant issue verified; serialized inventory matched; export seal: n/a (domestic deployment)", weight=6 },
     { label="Deployment verified; corridor routing approved; ledger notes filed same-day",                 weight=6 },
     { label="Somniant kit verified; minor variance corrected same-day",                                     weight=2 },
     { label="Minor variance corrected; revised routing issued with escort buffer",                         weight=2 },
     { label="Routing adjusted (+buffer); corridor reopened under escort",                                   weight=2 },
     { label="Weather delay; holding pattern under local command until visibility improves",               weight=1 },
     { label="Mission deferred (no-go); export-control check triggered by false manifest",                   weight=1 },
     { label="Mission deferred (no-go); export-control check triggered by false manifest",                 weight=1 },
     { label="Export request refused (constitutional prohibition); equipment retained under Council custody", weight=1 },
     { label="Export request refused (constitutional prohibition); material retained under Council custody", weight=1 },
}
}


Line 276: Line 225:
         return ' style="background-color:#fff3cd;"'
         return ' style="background-color:#fff3cd;"'
     end
     end
     if c:match("minor variance") or c:match("routing adjusted") then
     if c:match("minor variance") then
         return ' style="background-color:#fff3cd;"'
         return ' style="background-color:#fff3cd;"'
     end
     end
Line 283: Line 232:
     end
     end
     if uScore >= 3 then
     if uScore >= 3 then
         return ' style="background-color:#f8d7da;"' -- treat HIGH/CRITICAL as red attention
         return ' style="background-color:#f8d7da;"'
     end
     end
     return ' style="background-color:#d4edda;"'
     return ' style="background-color:#d4edda;"'
Line 289: Line 238:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (H) MAIN TABLE
-- (H) MAIN TABLE (NO systems/platforms column)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeTable(frame)
function p.makeTable(frame)
     local di = getPSSCDate()
     local di = getPSSCDate()
    local args = frame and frame.args or {}


    local args = frame and frame.args or {}
     local rows = tonumber(args.rows) or 36
     local rows = tonumber(args.rows) or 36
     local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
     local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"


     -- Build daily events mapping first (this may reseed RNG internally; we'll reseed afterwards) :contentReference[oaicite:9]{index=9}
     -- Build daily events mapping first (DailyEvents may reseed RNG internally)
     local eventMap, hasEvents = buildEventMapForToday()
     local eventMap, hasEvents = buildEventMapForToday()


     -- Now seed our own RNG deterministically for the day
     -- Seed our RNG deterministically for the day (or override for testing)
     local seed = tonumber(args.seed) or (di.year * 1000 + di.dayOfYear)
     local seed = tonumber(args.seed) or (di.year * 1000 + di.dayOfYear)
     math.randomseed(seed)
     math.randomseed(seed)
Line 330: Line 279:
         " !! Naval Forces" ..
         " !! Naval Forces" ..
         " !! Aerial Forces" ..
         " !! Aerial Forces" ..
        " !! Systems / Platforms" ..
         " !! Oversight" ..
         " !! Oversight" ..
         " !! Compliance / Notes"
         " !! Compliance / Notes"
Line 339: Line 287:
         local cityName = pick(div.cities)
         local cityName = pick(div.cities)


        -- link to canonical city section
         local cityWikilink = cityLink(cityName, cityName)
         local cityWikilink = cityLink(cityName, cityName)


        -- pull today’s event (if available)
         local ev = eventMap[cityName]
         local ev = eventMap[cityName]
         local evDesc = ev and ev.desc or (hasEvents and "No reports" or "Events module unavailable")
         local evDesc = ev and ev.desc or (hasEvents and "No reports" or "Events module unavailable")
Line 348: Line 294:
         local uScore = urgencyScore(urg)
         local uScore = urgencyScore(urg)


         -- Branch selection: Hatch/Haifan more naval; otherwise ground; add air more as urgency rises
         -- Branch selection: Hatch/Haifan bias naval; higher urgency bias air support
         local branchWeighted = {
         local branchWeighted = {
             { name="ground", weight = 6 },
             { name="ground", weight = 6 },
Line 356: Line 302:
         local primary = pickWeighted(branchWeighted).name
         local primary = pickWeighted(branchWeighted).name


         -- Choose operations by branch, but bias by urgency
         -- Choose operation pool
         local op
         local op
         if primary == "naval" then
         if primary == "naval" then
Line 366: Line 312:
         end
         end


         -- Formation picks per branch
         -- Formations
         local ground = ""
         local ground = "—"
         local naval  = ""
         local naval  = "—"
         local air    = ""
         local air    = "—"


         -- Always include a small ground presence unless the op is pure air ISR
         -- Keep a ground footprint unless it's a pure air ISR day and low urgency
         if primary ~= "naval" then
         if not (primary == "air" and uScore <= 1 and math.random() < 0.35) then
             local gE = pickEchelonForUrgency(GROUND_ECHELONS, uScore)
             local gE = pickEchelonForUrgency(GROUND_ECHELONS, uScore)
             local gUnit = string.format("%s (%s-%02d)", gE, div.code, math.random(1, 24))
             ground = string.format("%s (%s-%02d)", gE, div.code, math.random(1, 24))
            ground = gUnit
         end
         end


Line 387: Line 332:
             air = string.format("%s (%s-A%02d)", aE, div.code, math.random(1, 18))
             air = string.format("%s (%s-A%02d)", aE, div.code, math.random(1, 18))
         end
         end
        -- Systems: pick 1–2; more likely to show 2 at higher urgency
        local sys1 = pick(SYSTEMS)
        local sys2 = (math.random() < (0.35 + (0.10 * uScore))) and pick(SYSTEMS) or nil
        local systemsText = (sys2 and sys2 ~= sys1) and bulletList({sys1, sys2}) or sys1


         local obs = pickWeighted(OVERSIGHT).label
         local obs = pickWeighted(OVERSIGHT).label
         local comp = pickWeighted(COMPLIANCE).label
         local comp = pickWeighted(COMPLIANCE).label


        -- Notes include the force hierarchy identifiers and op id
         local notes = bulletList({
         local notes = bulletList({
             "Op-ID: " .. opId(di, div.code, i),
             "Op-ID: " .. opId(di, div.code, i),
             "Doctrine: corridor integrity and civic stability maintenance",
             "Doctrine: corridor integrity and civic stability maintenance"
            "Org keys: Ground (Turmae→Contubernia→Ordinis→Manipuli→Centuriae→Cohortes); Naval (Birlik-i Gemi→Tabur-i Derya→Ordu); Air (Zeygi→Ptisis→Moira)"
         })
         })


Line 407: Line 345:
         table.insert(out, "|-" .. style)
         table.insert(out, "|-" .. style)
         table.insert(out, string.format(
         table.insert(out, string.format(
             "| %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s",
             "| %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s",
             di.text,
             di.text,
             div.link,
             div.link,
Line 414: Line 352:
             urg,
             urg,
             op.name,
             op.name,
             (ground ~= "" and ground or "—"),
             ground,
             (naval ~= "" and naval  or "—"),
             naval,
             (air    ~= "" and air   or "—"),
             air,
            systemsText,
             obs,
             obs,
             bulletList({ comp, notes })
             bulletList({ comp, notes })
Line 431: Line 368:
         table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = variance/adjustment/weather&nbsp; ")
         table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = variance/adjustment/weather&nbsp; ")
         table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = HIGH/CRITICAL or export-control/deferment")
         table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = HIGH/CRITICAL or export-control/deferment")
        table.insert(out, "<br/><b>Force organization:</b> Ground—Turmae, Contubernia, Ordinis, Manipuli, Centuriae, Cohortes; Naval—Birlik-i Gemi, Tabur-i Derya, Ordu; Air—Zeygi, Ptisis, Moira.")
         table.insert(out, "</div>")
         table.insert(out, "</div>")
     end
     end
Line 439: Line 375:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- (I) OPTIONAL: show org ladders as a compact table for documentation
-- (I) OPTIONAL: show org ladders (for documentation / page footer)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.showOrganization()
function p.showOrganization()

Revision as of 20:05, 31 December 2025

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

-- Module:WarLeagueDivisionOpsBVR
-- Daily, in-world War League divisional operations log.
-- Integrates Module:BassaridianDailyEvents by parsing today’s city-event/urgency table output.

local p = {}

--------------------------------------------------------------------------------
-- (A) PSSC DATE (same logic used by Module:BassaridianDailyEvents)
--------------------------------------------------------------------------------
local function getPSSCDate()
    local startDate     = os.time({year = 1999, month = 8, day = 6})
    local secondsInDay  = 86400
    local daysPerYear   = 183
    local now           = os.time()

    local totalDays     = math.floor((now - startDate) / secondsInDay)
    local yearFraction  = totalDays / daysPerYear
    local psscYear      = math.floor(yearFraction)
    local dayOfYear     = math.floor((yearFraction - psscYear) * daysPerYear) + 1

    local month, dayOfMonth
    if dayOfYear <= 61 then
        month = 1; dayOfMonth = dayOfYear
    elseif dayOfYear <= 122 then
        month = 2; dayOfMonth = dayOfYear - 61
    else
        month = 3; dayOfMonth = dayOfYear - 122
    end

    local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
    return {
        text      = dateString,
        year      = psscYear,
        dayOfYear = dayOfYear,
        month     = month,
        dom       = dayOfMonth
    }
end

--------------------------------------------------------------------------------
-- (B) HELPERS
--------------------------------------------------------------------------------
local function pick(t) return t[math.random(#t)] end

local function pickWeighted(list)
    local total = 0
    for _, it in ipairs(list) do total = total + (it.weight or 1) end
    local r = math.random() * total
    local acc = 0
    for _, it in ipairs(list) do
        acc = acc + (it.weight or 1)
        if r <= acc then return it end
    end
    return list[#list]
end

local function bulletList(items)
    if not items or #items == 0 then return "" end
    return "<ul><li>" .. table.concat(items, "</li><li>") .. "</li></ul>"
end

local function cityLink(cityName, anchorOverride)
    local anchor = anchorOverride or cityName
    return string.format("[[List_of_cities_in_Bassaridia_Vaeringheim#%s|%s]]", anchor, cityName)
end

local function opId(di, divCode, idx)
    return string.format("WL-%03d.%03d-%s-%02d", di.year, di.dayOfYear, divCode, idx)
end

--------------------------------------------------------------------------------
-- (C) INTEGRATE DAILY EVENTS (parse Module:BassaridianDailyEvents.makeTable())
--------------------------------------------------------------------------------
local function buildEventMapForToday()
    local ok, daily = pcall(require, "Module:BassaridianDailyEvents")
    if not ok or not daily or type(daily.makeTable) ~= "function" then
        return {}, false
    end

    local tbl = daily.makeTable() -- returns wikitable text with rows: "| date || city || event || urgency"
    local eventMap = {}

    for line in tostring(tbl):gmatch("[^\n]+") do
        if line:match("^|%s") and not line:match("^|%-%s*$") then
            local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
            if #parts >= 4 then
                local city = parts[2]
                local ev   = parts[3]
                local urg  = parts[4]
                if city and city ~= "" and not eventMap[city] then
                    eventMap[city] = { desc = ev, urgency = urg }
                end
            end
        end
    end

    return eventMap, true
end

local function urgencyScore(urg)
    if urg == "CRITICAL" then return 4 end
    if urg == "HIGH"     then return 3 end
    if urg == "MEDIUM"   then return 2 end
    if urg == "LOW"      then return 1 end
    return 0
end

--------------------------------------------------------------------------------
-- (D) FORCE ORGANIZATION (as requested)
--------------------------------------------------------------------------------
local GROUND_ECHELONS = {
    { name="Turmae",        weight=5 },
    { name="Contubernia",   weight=5 },
    { name="Ordinis",       weight=4 },
    { name="Manipuli",      weight=3 },
    { name="Centuriae",     weight=2 },
    { name="Cohortes",      weight=1 },
}

local NAVAL_ECHELONS = {
    { name="Birlik-i Gemi (Patrol Element)", weight=5 },
    { name="Tabur-i Derya (Task Unit)",      weight=3 },
    { name="Ordu (Task Group)",              weight=1 },
}

local AIR_ECHELONS = {
    { name="Zeygi (Element)",   weight=5 },
    { name="Ptisis (Flight)",   weight=3 },
    { name="Moira (Squadron)",  weight=1 },
}

local function pickEchelonForUrgency(echelons, uScore)
    local tuned = {}
    for _, e in ipairs(echelons) do
        local w = e.weight or 1
        if uScore >= 3 then
            if e.name:match("Cohortes") or e.name:match("Ordu %(") or e.name:match("Moira") then
                w = w + 3
            elseif e.name:match("Centuriae") or e.name:match("Tabur%-i Derya") or e.name:match("Ptisis") then
                w = w + 2
            end
        elseif uScore == 2 then
            if e.name:match("Centuriae") or e.name:match("Tabur%-i Derya") or e.name:match("Ptisis") then
                w = w + 1
            end
        end
        table.insert(tuned, { name = e.name, weight = w })
    end
    return pickWeighted(tuned).name
end

--------------------------------------------------------------------------------
-- (E) DIVISIONS + CITY POOLS
--------------------------------------------------------------------------------
local DIVISIONS = {
    { name="Vaeringheim Division",        link="[[Vaeringheim Division]]",        code="VAE", weight=5,
      cities={"Vaeringheim","Luminaria","Serena","Symphonara","Aurelia","Somniumpolis","Nexa","Lunalis Sancta","Sylvapolis","Delphica","Catonis Atrium","Ferrum Citadel"} },
    { name="Alpazkigz Division",          link="[[Alpazkigz Division]]",          code="ALP", weight=4,
      cities={"The Alpazkigz","Pyralis","Acheron","Erythros","Aureum","Koinonía","Saluria","Aetherium","Fanghorn","Eikbu","Galvø","Sårensby","Slevik","Sjøsborg","Storesund","Notranskja","Kaledonija"} },
    { name="Odiferia Division",           link="[[Odiferia Division]]",           code="ODF", weight=3,
      cities={"Odiferia","Somniumpolis","Aetherium","Ferrum Citadel","Aurelia","Symphonara"} },
    { name="Hatch Ministry Division",     link="[[Hatch Ministry Division]]",     code="HAT", weight=3,
      cities={"Lewisburg","Lykopolis","Jogi","Keybir-Aviv","Tel-Amin","Diamandis","Thermosalem","Akróstadium","Sufriya"} },
    { name="New South Jangsong Division", link="[[New South Jangsong Division]]", code="NSJ", weight=3,
      cities={"Skýrophos","Bjornopolis","Aegirheim","Norsolyra","Thorsalon","Pelagia","Myrene","Thyrea","Ephyra","Halicarn"} },
    { name="Haifan Bassaridia Division",  link="[[Haifan Bassaridia Division]]",  code="HFA", weight=3,
      cities={"Keybir-Aviv","Tel-Amin","Diamandis","Jogi","Lewisburg","Thermosalem","Akróstadium","Sufriya","Lykopolis"} },
    { name="Bassaridian Normark Division",link="[[Bassaridian Normark Division]]",code="BNO",weight=2,
      cities={"Ardclach","Riddersborg"} },
    { name="Ouriana Division",            link="[[Ouriana Division]]",            code="OUR", weight=2,
      cities={"Bashkim","Ourid","Tonar"} },
    { name="Council of Kings Division",   link="[[Council of Kings Division]]",   code="CKD", weight=1,
      cities={"Vaeringheim","Bashkim","Ourid","Tonar"} },
}

--------------------------------------------------------------------------------
-- (F) OPERATION TYPES (branch-aware)
--------------------------------------------------------------------------------
local OP_GROUND = {
    { name="corridor security (rail / canal / road checkpointing)", weight=4 },
    { name="urban stabilization (cordon / curfew enforcement)",      weight=3 },
    { name="anti-smuggling interdiction / contraband seizure",       weight=3 },
    { name="infrastructure protection (port/bridge/aqueduct)",       weight=3 },
    { name="counter-insurgency sweep (cell disruption)",             weight=2 },
    { name="hazard response (evacuation / debris clearance)",        weight=2 },
    { name="convoy escort (supply / detainee / artifact transfer)",  weight=2 },
}

local OP_NAVAL = {
    { name="maritime escort / lake patrol routing",                 weight=3 },
    { name="VBSS / interdiction / contraband boarding",             weight=3 },
    { name="blockade enforcement / straits compliance sweep",       weight=2 },
    { name="harbor security (lock-gates / inner basin patrol)",     weight=2 },
}

local OP_AIR = {
    { name="ISR overwatch / route scanning",                        weight=4 },
    { name="rapid extraction / medevac lift",                       weight=2 },
    { name="strike deterrence package (show of force)",             weight=1 },
}

--------------------------------------------------------------------------------
-- (G) OVERSIGHT + COMPLIANCE
--------------------------------------------------------------------------------
local OVERSIGHT = {
    { label="Senate of Elders liaison",                 weight=3 },
    { label="Port compliance inspector (HCE)",          weight=4 },
    { label="Temple Aprobelle social-harmony auditor",  weight=3 },
    { label="Straits Convention monitor (civilian)",    weight=3 },
    { label="No observer (weather delay)",              weight=1 },
}

local COMPLIANCE = {
    { label="Deployment verified; corridor routing approved; ledger notes filed same-day",                 weight=6 },
    { label="Minor variance corrected; revised routing issued with escort buffer",                         weight=2 },
    { label="Weather delay; holding pattern under local command until visibility improves",               weight=1 },
    { label="Mission deferred (no-go); export-control check triggered by false manifest",                 weight=1 },
    { label="Export request refused (constitutional prohibition); material retained under Council custody", weight=1 },
}

local function rowColor(obs, comp, uScore)
    local o = (obs or ""):lower()
    local c = (comp or ""):lower()
    if o:match("weather delay") then
        return ' style="background-color:#fff3cd;"'
    end
    if c:match("minor variance") then
        return ' style="background-color:#fff3cd;"'
    end
    if c:match("export request refused") or c:match("mission deferred") then
        return ' style="background-color:#f8d7da;"'
    end
    if uScore >= 3 then
        return ' style="background-color:#f8d7da;"'
    end
    return ' style="background-color:#d4edda;"'
end

--------------------------------------------------------------------------------
-- (H) MAIN TABLE (NO systems/platforms column)
--------------------------------------------------------------------------------
function p.makeTable(frame)
    local di = getPSSCDate()
    local args = frame and frame.args or {}

    local rows = tonumber(args.rows) or 36
    local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"

    -- Build daily events mapping first (DailyEvents may reseed RNG internally)
    local eventMap, hasEvents = buildEventMapForToday()

    -- Seed our RNG deterministically for the day (or override for testing)
    local seed = tonumber(args.seed) or (di.year * 1000 + di.dayOfYear)
    math.randomseed(seed)

    local divFilter = tostring(args.division or ""):lower()
    local divPool = {}
    if divFilter ~= "" then
        for _, d in ipairs(DIVISIONS) do
            if d.name:lower():find(divFilter, 1, true) or d.code:lower():find(divFilter, 1, true) then
                table.insert(divPool, d)
            end
        end
        if #divPool == 0 then divPool = DIVISIONS end
    else
        divPool = DIVISIONS
    end

    local out = {}
    table.insert(out, "{| class='wikitable sortable'")
    table.insert(out,
        "! style='white-space:nowrap;' | Date" ..
        " !! Division" ..
        " !! City / Node" ..
        " !! Daily event trigger" ..
        " !! Urgency" ..
        " !! Operation" ..
        " !! Ground Forces" ..
        " !! Naval Forces" ..
        " !! Aerial Forces" ..
        " !! Oversight" ..
        " !! Compliance / Notes"
    )

    for i = 1, rows do
        local div = pickWeighted(divPool)
        local cityName = pick(div.cities)

        local cityWikilink = cityLink(cityName, cityName)

        local ev = eventMap[cityName]
        local evDesc = ev and ev.desc or (hasEvents and "No reports" or "Events module unavailable")
        local urg = ev and ev.urgency or "N/A"
        local uScore = urgencyScore(urg)

        -- Branch selection: Hatch/Haifan bias naval; higher urgency bias air support
        local branchWeighted = {
            { name="ground", weight = 6 },
            { name="naval",  weight = (div.code == "HAT" or div.code == "HFA") and 5 or 2 },
            { name="air",    weight = 2 + uScore },
        }
        local primary = pickWeighted(branchWeighted).name

        -- Choose operation pool
        local op
        if primary == "naval" then
            op = pickWeighted(OP_NAVAL)
        elseif primary == "air" then
            op = pickWeighted(OP_AIR)
        else
            op = pickWeighted(OP_GROUND)
        end

        -- Formations
        local ground = "—"
        local naval  = "—"
        local air    = "—"

        -- Keep a ground footprint unless it's a pure air ISR day and low urgency
        if not (primary == "air" and uScore <= 1 and math.random() < 0.35) then
            local gE = pickEchelonForUrgency(GROUND_ECHELONS, uScore)
            ground = string.format("%s (%s-%02d)", gE, div.code, math.random(1, 24))
        end

        if primary == "naval" or (div.code == "HAT" and math.random() < 0.55) or (uScore >= 3 and math.random() < 0.35) then
            local nE = pickEchelonForUrgency(NAVAL_ECHELONS, uScore)
            naval = string.format("%s (%s-N%02d)", nE, div.code, math.random(1, 12))
        end

        if primary == "air" or (uScore >= 2 and math.random() < 0.55) then
            local aE = pickEchelonForUrgency(AIR_ECHELONS, uScore)
            air = string.format("%s (%s-A%02d)", aE, div.code, math.random(1, 18))
        end

        local obs = pickWeighted(OVERSIGHT).label
        local comp = pickWeighted(COMPLIANCE).label

        local notes = bulletList({
            "Op-ID: " .. opId(di, div.code, i),
            "Doctrine: corridor integrity and civic stability maintenance"
        })

        local style = rowColor(obs, comp, uScore)

        table.insert(out, "|-" .. style)
        table.insert(out, string.format(
            "| %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s",
            di.text,
            div.link,
            cityWikilink,
            evDesc,
            urg,
            op.name,
            ground,
            naval,
            air,
            obs,
            bulletList({ comp, notes })
        ))
    end

    table.insert(out, "|}")

    if showLegend then
        table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>")
        table.insert(out, "<b>Legend:</b> ")
        table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = routine/compliant&nbsp; ")
        table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = variance/adjustment/weather&nbsp; ")
        table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = HIGH/CRITICAL or export-control/deferment")
        table.insert(out, "</div>")
    end

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

--------------------------------------------------------------------------------
-- (I) OPTIONAL: show org ladders (for documentation / page footer)
--------------------------------------------------------------------------------
function p.showOrganization()
    return table.concat({
        '{| class="wikitable" style="width:100%;"',
        '! Branch !! Smallest → Largest',
        '|-',
        '| Ground || Turmae → Contubernia → Ordinis → Manipuli → Centuriae → Cohortes',
        '|-',
        '| Naval || Birlik-i Gemi (Patrol Element) → Tabur-i Derya (Task Unit) → Ordu (Task Group)',
        '|-',
        '| Aerial || Zeygi (Element) → Ptisis (Flight) → Moira (Squadron)',
        '|}',
    }, "\n")
end

return p