Module:MissionaryDeploymentCorum

From MicrasWiki
Jump to navigationJump to search

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

-- Module: MissionaryDaily
-- Purpose: Daily, in-world report of missionary activities (pre-CRH), aligned with guardrails + WL expansion + transparency [[Baratar Scandal of 52 PSSC]]
local p = {}

-------------------------------------------------------------------------------
-- (A) PSSC DATE
-------------------------------------------------------------------------------
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

-- Health surge activates on/after 36/2/52 PSSC and remains active thereafter
local function surgeActive(di)
    -- 36/2 => dayOfYear = 61 + 36 = 97
    if di.year > 52 then return true end
    if di.year < 52 then return false end
    return di.dayOfYear >= 97
end

-------------------------------------------------------------------------------
-- (B) STATIC DATA
-------------------------------------------------------------------------------
-- Units (fixed baseline; headcount will display +50% under surge)
local units = {
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=3,  code="CHS-K1"},
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=2,  code="CHS-K2"},
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=3,  code="CHS-K3"},
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=2,  code="CHS-K4"},
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=3,  code="CHS-K5"},
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=2,  code="CHS-K6"},
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=5,  code="CHS-K7"},
    {order="Celestial Harmony Sect",        unit="Kleisthenes", headcount=5,  code="CHS-K8"},

    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=3,  code="REN-K1"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=2,  code="REN-K2"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=3,  code="REN-K3"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=2,  code="REN-K4"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=3,  code="REN-K5"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=2,  code="REN-K6"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=3,  code="REN-K7"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=2,  code="REN-K8"},
    {order="Reverie Nebulous",              unit="Kleisthenes", headcount=5,  code="REN-K9"},

    {order="Temple Alabaster",              unit="Kleisthenes", headcount=5,  code="ALB-K1"},
    {order="Temple Alabaster",              unit="Kleisthenes", headcount=5,  code="ALB-K2"},
    {order="Temple Alabaster",              unit="Kleisthenes", headcount=5,  code="ALB-K3"},
    {order="Temple Alabaster",              unit="Kleisthenes", headcount=5,  code="ALB-K4"},
    {order="Temple Alabaster",              unit="Kleisthenes", headcount=5,  code="ALB-K5"},

    {order="Sanctum Delphica",              unit="Kleisthenes", headcount=3,  code="DEL-K1"},
    {order="Sanctum Delphica",              unit="Kleisthenes", headcount=2,  code="DEL-K2"},
    {order="Sanctum Delphica",              unit="Kleisthenes", headcount=3,  code="DEL-K3"},
    {order="Sanctum Delphica",              unit="Kleisthenes", headcount=2,  code="DEL-K4"},
    {order="Sanctum Delphica",              unit="Kleisthenes", headcount=5,  code="DEL-K5"},
    {order="Sanctum Delphica",              unit="Kleisthenes", headcount=5,  code="DEL-K6"},
    {order="Sanctum Delphica",              unit="Kleisthenes", headcount=5,  code="DEL-K7"},

    {order="Order of the Umbral Oracle",    unit="Dodekade",    headcount=12, code="UMO-D1"},
    {order="Order of the Umbral Oracle",    unit="Hetairos",    headcount=6,  code="UMO-H1"},

    {order="Sanctum Vitalis",               unit="Kleisthenes", headcount=3,  code="SCV-K1"},
    {order="Sanctum Vitalis",               unit="Kleisthenes", headcount=2,  code="SCV-K2"},
    {order="Sanctum Vitalis",               unit="Kleisthenes", headcount=5,  code="SCV-K3"},
    {order="Sanctum Vitalis",               unit="Kleisthenes", headcount=5,  code="SCV-K4"},
    {order="Sanctum Vitalis",               unit="Kleisthenes", headcount=10, code="SCV-K5"},

    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=3,  code="AZS-K1"},
    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=2,  code="AZS-K2"},
    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=3,  code="AZS-K3"},
    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=2,  code="AZS-K4"},
    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=3,  code="AZS-K5"},
    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=2,  code="AZS-K6"},
    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=5,  code="AZS-K7"},
    {order="Azure Sentinel Sect",           unit="Kleisthenes", headcount=5,  code="AZS-K8"},
}

-- Districts (NZ = New Zimia heritage; GM = Bayen/Gamesman)
-- Give Nortonian & Braxian higher weight under respiratory-risk focus
local districts = {
    {name="Lyvian Zones",     kind="NZ", weight=1},
    {name="Lucinarian Zones", kind="NZ", weight=1},
    {name="Brentonian Zones", kind="NZ", weight=1},
    {name="Ossyani Zones",    kind="NZ", weight=1},
    {name="Sinclarian Zones", kind="GM", weight=1},
    {name="Nortonian Zones",  kind="GM", weight=3}, -- focus
    {name="Wallisian Zones",  kind="GM", weight=1},
    {name="Alindan Zones",    kind="GM", weight=1},
    {name="Aijiac Zones",     kind="GM", weight=1},
    {name="Braxian Zones",    kind="GM", weight=3}, -- focus
}

-- Activities (add nutrition)
local activities = {
    {name="relief"},
    {name="education"},
    {name="reconciliation"},
    {name="nutrition"},
    -- internal key used for logic; label used for display
    {name="diaspora_outreach", label="diaspora outreach"},
}

-- Observers (neutral/independent)
local observerPool = {
    {label="Neutral temple auditor",                weight=4},
    {label="Independent port compliance inspector", weight=3},
    {label="Community elder (witness)",             weight=3},
    {label="Oportian observer",                     weight=4},
    {label="Nouvelle Alexandrie observer",          weight=4},
    {label="Imperial Federation observer",          weight=4},
    {label="Matamoros observer",                    weight=4},
    {label="Straits Convention monitor (civilian)", weight=4},
    {label="DAC Delegate observer",                 weight=4},
    {label="No observer (weather delay)",           weight=1},
}

-- Compliance notes (HCE & kit checks)
local kitNotes = {
    "HCE log filed; roster posted; kit verified (no encryption/UAV/sensors)",
    "Kit verified; spot-check passed; HCE reference filed",
    "HCE log filed; minor inventory variance (expiry) corrected same-day",
}

-- Expanded White-Lane corridors (kind-aware)
local wlCorridors = {
    -- New Zimian (NZ)
    {code="WL-NZ-1", name="New Zimian Humanitarian Belt",  kind="NZ",    window="0700–1400 (expanded)"},
    {code="WL-NZ-2", name="East Zimia Service Loop",       kind="NZ",    window="1200–1900 (expanded)"},
    -- Gamesman/Bayen (GM)
    {code="WL-GM-1", name="Bayen Relief Spur",             kind="GM",    window="0900–1600 (expanded)"},
    {code="WL-GM-2", name="Lake Norton South Chain",       kind="GM",    window="1300–2000 (expanded)"},
    -- Mixed coastal shuttle usable by either
    {code="WL-SEA-1", name="Corum Coastal Shuttle",        kind="Mixed", window="0600–1800 (expanded)"},
}

-------------------------------------------------------------------------------
-- (C) HELPERS
-------------------------------------------------------------------------------
local function pick(list, seedRoll)
    local idx = (seedRoll and ((seedRoll % #list) + 1)) or math.random(#list)
    return list[idx]
end

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

-- Render a simple HTML bullet list from an array of strings
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 outputsFor(activityName, districtName)
    if activityName == "relief" then
        local patients = math.random(22, 72)
        local parcels  = math.random(60, 180)
        if districtName == "Nortonian Zones" or districtName == "Braxian Zones" then
            patients = math.floor(patients * 1.15)
            parcels  = math.floor(parcels  * 1.15)
        end
        return bulletList({
            string.format("patients seen: %d", patients),
            string.format("relief parcels: %d", parcels),
        })

    elseif activityName == "education" then
        local classes  = math.random(1, 3)
        local students = math.random(28, 110)
        return bulletList({
            string.format("classes: %d", classes),
            string.format("learners served: %d", students),
        })

    elseif activityName == "nutrition" then
        local rations     = math.random(120, 320)
        local screened    = math.random(35, 120)
        local supplements = math.random(80, 220)
        if districtName == "Nortonian Zones" or districtName == "Braxian Zones" then
            rations     = math.floor(rations    * 1.20)
            screened    = math.floor(screened   * 1.20)
            supplements = math.floor(supplements* 1.20)
        end
        return bulletList({
            string.format("fortified rations: %d", rations),
            string.format("health screenings: %d", screened),
            string.format("supplements issued: %d", supplements),
        })

    elseif activityName == "diaspora_outreach" then
        local registrants   = math.random(12, 60)
        local consultations = math.random(4, 15)
        local updates       = math.random(2, 8)
        return bulletList({
            string.format("diaspora households registered: %d", registrants),
            string.format("DAC consultations: %d", consultations),
            string.format("registry updates: %d", updates),
        })

    elseif activityName == "trade_facilitation" then
        local manifests   = math.random(5, 25)
        local inspections = math.random(3, 10)
        local disputes    = math.random(0, 3)
        return bulletList({
            string.format("manifests verified: %d", manifests),
            string.format("shipments inspected: %d", inspections),
            string.format("tariff disputes mediated: %d", disputes),
        })

    elseif activityName == "cultural_exchange" then
        local events       = math.random(1, 4)
        local participants = math.random(40, 160)
        local materials    = math.random(60, 200)
        return bulletList({
            string.format("events hosted: %d", events),
            string.format("participants engaged: %d", participants),
            string.format("heritage materials distributed: %d", materials),
        })

    elseif activityName == "legal_aid" then
        local cases        = math.random(2, 10)
        local certificates = math.random(1, 6)
        local mediations   = math.random(0, 4)
        return bulletList({
            string.format("legal cases assisted: %d", cases),
            string.format("compliance certificates issued: %d", certificates),
            string.format("mediations completed: %d", mediations),
        })

    else -- reconciliation (fallback/default)
        local circles    = math.random(1, 4)
        local agreements = math.random(0, math.min(circles, 3))
        return bulletList({
            string.format("mediation circles: %d", circles),
            string.format("agreements reached: %d", agreements),
        })
    end
end

local function observerLine()
    local obs = pickWeighted(observerPool)
    if obs.label == "No observer (weather delay)" then
        return "Observer: n/a (weather delay); affidavits to follow"
    else
        return "Observer present: " .. obs.label
    end
end

local function hceRef(dateInfo, unitCode)
    return string.format("HCE-%03d.%03d-%s", dateInfo.year, dateInfo.dayOfYear, unitCode)
end

local function guardrailsStatus()
    local roll = math.random()
    if roll < 0.08 then
        return "Geofence: route adjusted (+1.6 km buffer); Separation: ✓; No-contact log: signed"
    elseif roll < 0.95 then
        return "Geofence: ✓; Separation: ✓; No-contact log: signed"
    else
        return "Mission deferred (no-go buffer triggered); rescheduled next window"
    end
end

-- Row color based on compliance outcomes
local function rowColor(hceLine, guards, obsLine)
    if guards:match("deferred") or hceLine:match("pre%-mission") then
        return ' style="background-color:#f8d7da;"' -- red
    end
    if hceLine:match("variance") or guards:match("adjusted") or obsLine:match("weather delay") then
        return ' style="background-color:#fff3cd;"' -- yellow
    end
    return ' style="background-color:#d4edda;"'     -- green
end

-- Pick a White-Lane corridor suitable for a given district (by kind)
local function pickWLCorridorForDistrict(district)
    local kind = district.kind
    local pool = {}
    for _, wl in ipairs(wlCorridors) do
        if wl.kind == kind or wl.kind == "Mixed" then
            table.insert(pool, wl)
        end
    end
    if #pool == 0 then
        pool = wlCorridors
    end
    return pool[math.random(#pool)]
end

-- Generate a simple geofence profile ID by kind/day
local function geofenceProfileId(district, dateInfo)
    local k = district.kind or "NZ"
    return string.format("GF-%s-%03d", k, dateInfo.dayOfYear)
end

-- Build a compact transparency trigger line per row
local function buildTriggers(u, d, act, obsLine, hceLine, guards, wl, di, surge)
    local t = {}
    -- observer presence / delay
    if obsLine:match("Observer present") then
        local who = obsLine:gsub("Observer present: ","")
        table.insert(t, "observer: " .. who)
    elseif obsLine:match("weather delay") then
        table.insert(t, "observer: weather delay")
    else
        table.insert(t, "observer: n/a")
    end
    -- kit status
    if hceLine:match("minor inventory variance") then
        table.insert(t, "kit: variance")
    elseif hceLine:match("pre%-mission") then
        table.insert(t, "kit: pre-mission only")
    else
        table.insert(t, "kit: verified")
    end
    -- guardrails
    if guards:match("adjusted") then
        table.insert(t, "geofence: adjusted")
    elseif guards:match("deferred") then
        table.insert(t, "geofence: deferred")
    else
        table.insert(t, "geofence: OK")
    end
    -- lane/district/activity
    table.insert(t, "WL " .. wl.code)
    table.insert(t, "district " .. (d.kind or "NZ"))
    table.insert(t, "activity " .. act.name)
    -- surge flags
    if surge then
        table.insert(t, "health surge: active")
        if d.name == "Nortonian Zones" or d.name == "Braxian Zones" then
            table.insert(t, "focus: " .. d.name)
        end
    end
    return table.concat(t, "; ")
end

-------------------------------------------------------------------------------
-- (D) MAIN TABLE
-------------------------------------------------------------------------------
function p.makeTable(frame)
    local dateInfo = getPSSCDate()
    math.randomseed(dateInfo.year * 1000 + dateInfo.dayOfYear)

    local rows       = tonumber(frame and frame.args and frame.args.rows) or 48
    local showLegend = (frame and frame.args and tostring(frame.args.showlegend) or "yes"):lower() ~= "no"
    local healthSurge = surgeActive(dateInfo)

    local out = {}
    table.insert(out, "{| class='wikitable sortable'")
    table.insert(out,
        "! style='white-space:nowrap;' | Date" ..
        " !! District" ..
        " !! Order / Unit (Headcount)" ..
        " !! Activity" ..
        " !! Outputs" ..
        " !! Observer Verification" ..
        " !! HCE / Kit Compliance" ..
        " !! Guardrails / Routing Notes"
    )

    for i = 1, rows do
        -- Prefer weighted districts (Braxian/Nortonian focus)
        local d = pickWeighted(districts)

        -- Bias activities in focus zones toward relief/nutrition
        local act
        if (d.name == "Nortonian Zones" or d.name == "Braxian Zones") and math.random() < 0.65 then
            act = ({ {name="relief"}, {name="nutrition"} })[math.random(2)]
        else
            act = pick(activities)
        end

        local u = pick(units)

        -- Display +50% headcount during surge
        local displayedHeadcount = u.headcount
        if healthSurge then
            displayedHeadcount = math.floor(u.headcount * 1.5 + 0.5)
        end

        local outputs = outputsFor(act.name, d.name)
        local obsLine = observerLine()

        local hceLine = bulletList({
            hceRef(dateInfo, u.code),
            pick(kitNotes),
        })

        local guards  = guardrailsStatus()

        -- WL annotation
        local wl   = pickWLCorridorForDistrict(d)
        local gfId = geofenceProfileId(d, dateInfo)

        if guards:match("deferred") then
            outputs = "n/a (deferred)"
            obsLine = "Observer: n/a (deferred)"
            hceLine = bulletList({
                hceRef(dateInfo, u.code),
                "pre-mission check only",
            })
        end

        local unitStr = string.format("%s — %s (%d) [%s]", u.order, u.unit, displayedHeadcount, u.code)

        -- Color
        local style = rowColor(hceLine, guards, obsLine)
        table.insert(out, "|-" .. style)

        -- Case & triggers
        local caseId   = string.format("MD-%03d.%03d-%02d", dateInfo.year, dateInfo.dayOfYear, i)
        local triggers = buildTriggers(u, d, act, obsLine, hceLine, guards, wl, dateInfo, healthSurge)

        -- Notes (append surge note when active)
        local surgeNote = healthSurge and "Public health surge: respiratory risk mitigation & nutrition support" or nil
        local guardrailsParts = {
            guards,
            string.format("WL %s (%s)", wl.code, wl.name),
            "Window: " .. wl.window,
            "Geofence Profile: " .. gfId,
            "Case: " .. caseId,
            "Triggers: " .. triggers,
        }
        if surgeNote then
            table.insert(guardrailsParts, surgeNote)
        end
        local guardrailsNotes = bulletList(guardrailsParts)

        local activityLabel = act.label or act.name

        table.insert(out, string.format(
            "| %s || %s || %s || %s || %s || %s || %s || %s",
            dateInfo.text,
            d.name,
            unitStr,
            activityLabel,
            outputs,
            obsLine,
            hceLine,
            guardrailsNotes
        ))
    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> = 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> = Deferred/Pre-mission only")
        table.insert(out, "</div>")
    end

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

return p