Module:MissionaryDailyOpsBVR

From MicrasWiki
Revision as of 02:45, 31 December 2025 by NewZimiaGov (talk | contribs)
Jump to navigationJump to search

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

-- Module:MissionaryDailyOpsBVR
-- Daily (PSSC-calendar keyed) missionary tasking schedule for the Temple Bank of the Reformed Stripping Path.
-- Deterministic per-day output (no math.random / no os.time seeding inside this module).
--
-- Color coding: row background reflects mission urgency (Green/Yellow/Red),
-- matching the palette used in Module:HatchMinistryDailyOps.
--
-- Optional integration:
--   |includeThreat=yes  -> imports Module:ReligiousEvents and parses today's table once,
--                          using it to bump severity and bias order selection (opt-in only).
--
-- Usage examples:
--   {{#invoke:MissionaryDailyOpsBVR|makeTable}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}}
--   {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}}
--   {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}}

local p = {}

local cal = require('Module:BassaridianCalendar')

local DAYS_IN_YEAR = 183

--------------------------------------------------------------------------------
-- 0) UTIL: calendar parsing (mirrors HatchMinistryDailyOps)
--------------------------------------------------------------------------------
local function parseCalendarString(s)
	if type(s) ~= "string" then return nil end
	local dayOfYear, month, year = s:match("^(%d+),%s+([%a]+)%s+%b()%s*,%s*(%d+)%s+PSSC")
	if dayOfYear and year then
		return tonumber(year), tonumber(dayOfYear), month
	end
	return nil
end

local function extractEventText(s)
	if type(s) ~= "string" then return "" end
	local ev = s:match("PSSC%s+–%s+(.-)%s+–%s+Proverb:")
	return ev or ""
end

local function doyToMonthDom(doy)
	if doy <= 61 then
		return 1, doy
	elseif doy <= 122 then
		return 2, doy - 61
	else
		return 3, doy - 122
	end
end

local function getDateInfo()
	local raw = cal.getCurrentDate()
	local y, doy = parseCalendarString(raw)
	if not y or not doy then
		y, doy = 0, 1
		raw = tostring(raw or "")
	end
	local m, dom = doyToMonthDom(doy)
	return {
		year = y,
		dayOfYear = doy,
		monthNum = m,
		dom = dom,
		text = string.format("%d/%d/%d PSSC", dom, m, y),
		raw = raw,
		calendarEvent = extractEventText(raw),
	}
end

--------------------------------------------------------------------------------
-- 1) DETERMINISTIC HASH + RNG (NO bit32, NO math.random)
--------------------------------------------------------------------------------
local function hash31(str)
	local h = 0
	for i = 1, #str do
		h = (h * 131 + str:byte(i)) % 2147483647
	end
	return h
end

local function lcg(seed)
	local s = seed % 2147483647
	if s <= 0 then s = s + 2147483646 end
	return function()
		s = (1103515245 * s + 12345) % 2147483647
		return s
	end
end

local function pickFromList(nextInt, list, avoidValue)
	if #list == 0 then return nil end
	if #list == 1 then return list[1] end
	local tries = 0
	while tries < 12 do
		local idx = (nextInt() % #list) + 1
		local v = list[idx]
		if v ~= avoidValue then return v end
		tries = tries + 1
	end
	for _, v in ipairs(list) do
		if v ~= avoidValue then return v end
	end
	return list[1]
end

--------------------------------------------------------------------------------
-- 2) COLOR CODING (same palette logic as HatchMinistryDailyOps)
--------------------------------------------------------------------------------
local function sevStyle(sev)
	if sev == "red" then
		return ' style="background-color:#f8d7da;"'
	elseif sev == "yellow" then
		return ' style="background-color:#fff3cd;"'
	else
		return ' style="background-color:#d4edda;"'
	end
end

local function bumpSev(sev)
	if sev == "green" then return "yellow" end
	if sev == "yellow" then return "red" end
	return "red"
end

--------------------------------------------------------------------------------
-- 3) CITY NETWORK (canonical list page)
--------------------------------------------------------------------------------
local function cityLink(name)
	return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", name, name)
end

-- 54 canonical cities, grouped for filtering
local cityOrder = {
	-- Core majors
	{name="Vaeringheim",      region="Core"},
	{name="Luminaria",        region="Core"},
	{name="Serena",           region="Core"},
	{name="Pyralis",          region="Core"},
	{name="Symphonara",       region="Core"},
	{name="Aurelia",          region="Core"},
	{name="Somniumpolis",     region="Core"},
	{name="Nexa",             region="Core"},
	{name="Lunalis Sancta",   region="Core"},
	{name="Sylvapolis",       region="Core"},

	-- Core minors
	{name="Saluria",          region="Core"},
	{name="Aetherium",        region="Core"},
	{name="Ferrum Citadel",   region="Core"},
	{name="Acheron",          region="Core"},
	{name="Erythros",         region="Core"},
	{name="Catonis Atrium",   region="Core"},
	{name="Delphica",         region="Core"},
	{name="Koinonía",         region="Core"},
	{name="Aureum",           region="Core"},

	-- Alperkin
	{name="The Alpazkigz",    region="Alperkin"},
	{name="Odiferia",         region="Alperkin"},

	-- New South Jangsong – major
	{name="Skýrophos",        region="New South Jangsong"},
	{name="Bjornopolis",      region="New South Jangsong"},
	{name="Aegirheim",        region="New South Jangsong"},
	{name="Norsolyra",        region="New South Jangsong"},
	{name="Thorsalon",        region="New South Jangsong"},

	-- New South Jangsong – minor
	{name="Pelagia",          region="New South Jangsong"},
	{name="Myrene",           region="New South Jangsong"},
	{name="Thyrea",           region="New South Jangsong"},
	{name="Ephyra",           region="New South Jangsong"},
	{name="Halicarn",         region="New South Jangsong"},

	-- Haifan Bassaridia – major
	{name="Keybir-Aviv",      region="Haifan Bassaridia"},
	{name="Tel-Amin",         region="Haifan Bassaridia"},
	{name="Diamandis",        region="Haifan Bassaridia"},
	{name="Jogi",             region="Haifan Bassaridia"},
	{name="Lewisburg",        region="Haifan Bassaridia"},

	-- Haifan Bassaridia – minor
	{name="Thermosalem",      region="Haifan Bassaridia"},
	{name="Akróstadium",      region="Haifan Bassaridia"},
	{name="Sufriya",          region="Haifan Bassaridia"},
	{name="Lykopolis",        region="Haifan Bassaridia"},

	-- Normark
	{name="Ardclach",         region="Normark"},
	{name="Riddersborg",      region="Normark"},

	-- Ouriana
	{name="Bashkim",          region="Ouriana"},
	{name="Ourid",            region="Ouriana"},
	{name="Tonar",            region="Ouriana"},

	-- Caledonian dependencies
	{name="Fanghorn",         region="Caledonia"},
	{name="Eikbu",            region="Caledonia"},
	{name="Galvø",            region="Caledonia"},
	{name="Sårensby",         region="Caledonia"},
	{name="Slevik",           region="Caledonia"},
	{name="Sjøsborg",         region="Caledonia"},
	{name="Storesund",        region="Caledonia"},
	{name="Notranskja",       region="Caledonia"},
	{name="Kaledonija",       region="Caledonia"},
}

local function regionPass(filter, cityRegion)
	if not filter or filter == "" or filter == "All" then return true end
	-- Allow a few convenient aliases
	if filter == "Core" then return cityRegion == "Core" end
	if filter == "NSJ" then return cityRegion == "New South Jangsong" end
	if filter == "Haifan" then return cityRegion == "Haifan Bassaridia" end
	if filter == "Caledonian" then return cityRegion == "Caledonia" end
	return cityRegion == filter
end

--------------------------------------------------------------------------------
-- 4) MISSIONARY ORGANIZATION (sizes align to RSP org table)
--------------------------------------------------------------------------------
local org = {
	Lampade   = {size=3,   code="L"},
	Hetairoi  = {size=6,   code="H"},
	Dodekade  = {size=12,  code="D"},
	Kleisthenes = {size=25,  code="K"},
	Stoa      = {size=50,  code="S"},
	Thiasos   = {size=100, code="T"},
}

local function pickOrgUnit(nextInt, sev)
	if sev == "green" then
		return pickFromList(nextInt, {"Lampade","Hetairoi"}, nil)
	elseif sev == "yellow" then
		return pickFromList(nextInt, {"Hetairoi","Dodekade","Kleisthenes"}, nil)
	else
		return pickFromList(nextInt, {"Kleisthenes","Stoa","Thiasos"}, nil)
	end
end

--------------------------------------------------------------------------------
-- 5) CULT/ORDER PROFILES + TASK LIBRARIES
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
	return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
end

local function T(text, sev, tag)
	return {text=text, sev=sev, tag=tag or ""}
end

local orders = {
	{
		name="Ordo Solis Invicti",
		key="OSI",
		link=orderLink("Atos (Reformed Stripping Path)","Ordo Solis Invicti","Ordo Solis Invicti"),
		tasks={
			T("Dawn rites and public illumination drill to restore civic clarity and discourage eclipse-rumors in the market wards.", "green", "rumor"),
			T("Temple-school literacy session paired with cosmology instruction and rooftop shrine maintenance inspections.", "green", "school"),
			T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.", "yellow", "infrastructure"),
			T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.", "yellow", "shrine"),
			T("Emergency morale procession after night unrest; hymn leadership and calm restoration at canal crossings.", "red", "unrest"),
		}
	},
	{
		name="Order of Aurora Mystica",
		key="OAM",
		link=orderLink("Eos (Reformed Stripping Path)","Order of Aurora Mystica","Order of Aurora Mystica"),
		tasks={
			T("Sunrise vigil and reconciliation rite for competing guilds; signed harmony pledges archived by Temple Bank clerks.", "green", "mediation"),
			T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.", "green", "omen"),
			T("Civic blessing for new housing blocks; routing of pilgrimage foot-traffic to prevent crush points.", "yellow", "crowd"),
			T("Counter-heresy sermon cycle following illicit broadsides; witness interviews conducted with local elders.", "yellow", "heresy"),
			T("Rapid-response pacification after arson rumor; dawn procession under guard with emergency stipend release.", "red", "arson"),
		}
	},
	{
		name="Harmony Sanctum",
		key="HSA",
		link=orderLink("Micras (Reformed Stripping Path)","Harmony Sanctum","Harmony Sanctum"),
		tasks={
			T("Neighborhood mediation session for family disputes and trade-feud spillover; binding accords notarized.", "green", "mediation"),
			T("Harmony clinic for stress and rumor contagion; quiet-room rotations and guided breathing rites.", "green", "health"),
			T("Loyalty and morale audit of a civic office after complaint surge; corrective training scheduled.", "yellow", "audit"),
			T("Containment of a cult schism meeting; supervised dialogue and sanction warnings delivered.", "yellow", "schism"),
			T("Major crowd-stabilization operation after riot signals; staged dispersal with peace-liturgy and escorts.", "red", "riot"),
		}
	},
	{
		name="Ignis Aeternum",
		key="IGA",
		link=orderLink("Pyros (Reformed Stripping Path)","Ignis Aeternum","Ignis Aeternum"),
		tasks={
			T("Controlled fire blessing for workshops; safety rite that doubles as an inspection of fuel and storage.", "green", "fire"),
			T("Purification burn at a compromised shrine-store; ash sealed and catalogued for Temple Bank disposal.", "yellow", "contam"),
			T("Investigation of incendiary threats near a gate-temple; firebreak plan issued to local wardens.", "yellow", "arson"),
			T("Emergency response to active arson or explosive rumor; sanctified suppression teams deployed with escorts.", "red", "explosive"),
			T("Post-incident purification circuit across three oratories, restoring ritual confidence by nightfall.", "red", "incident"),
		}
	},
	{
		name="Celestial Harmony Sect",
		key="CHS",
		link=orderLink("Indigo (Reformed Stripping Path)","Celestial Harmony Sect","Celestial Harmony Sect"),
		tasks={
			T("Diplomatic courtesy visit to a city council; alignments review and oath renewal with minimal spectacle.", "green", "diplomacy"),
			T("Star-chart instruction for apprentices; quiet observatory checks and record sealing.", "green", "astronomy"),
			T("Faction-balance mediation between rival shrines; written compromise placed in Temple Bank archive.", "yellow", "balance"),
			T("Rumor forensics on ‘forbidden rite’ claims; witness tree compiled and forwarded to Temple authorities.", "yellow", "rumor"),
			T("High-risk stabilization after cult confrontation; negotiators embedded with escorts to prevent escalation.", "red", "confrontation"),
		}
	},
	{
		name="Guild of Golden Shadows",
		key="GGS",
		link=orderLink("Chrysos (Reformed Stripping Path)","Guild of Golden Shadows","Guild of Golden Shadows"),
		tasks={
			T("Discrete audit of offertory ledgers and voucher seals; minor variances reconciled quietly.", "green", "audit"),
			T("Merchant vetting interviews for a sensitive consignment; anonymity rites certified.", "yellow", "merchant"),
			T("Counter-counterfeit sweep targeting forged stamps and false Temple consignments; evidence boxed and logged.", "yellow", "forgery"),
			T("Recovery detail for stolen reliquary property; shadow-following surveillance and lawful seizure.", "red", "theft"),
			T("Crisis finance stabilization after panic; emergency stipend routing and rumor suppression coordination.", "red", "panic"),
		}
	},
	{
		name="Azure Sentinel Sect",
		key="ASS",
		link=orderLink("Thalassa (Reformed Stripping Path)","Azure Sentinel Sect","Azure Sentinel Sect"),
		tasks={
			T("Canal and fountain sanctification circuit; water-quality blessings paired with public reassurance.", "green", "water"),
			T("Harbor-side watch for sabotage indicators; shrine-wardens trained on lock and pier checks.", "yellow", "sabotage"),
			T("Storm or flood readiness rite; evacuation hymn routes verified with civic stewards.", "yellow", "flood"),
			T("Quarantine support for suspected waterborne illness; controlled distribution points blessed and monitored.", "red", "outbreak"),
			T("Major incident response: water infrastructure damage; emergency sanctification and ration-order enforcement.", "red", "infrastructure"),
		}
	},
	{
		name="Reverie Nebulous",
		key="REN",
		link=orderLink("Nephele (Reformed Stripping Path)","Reverie Nebulous","Reverie Nebulous"),
		tasks={
			T("Dream-interpretation clinic to reduce anxiety; rumor narratives redirected into sanctioned symbolism.", "green", "dream"),
			T("Nocturne counseling for insomnia cluster; soft-lit rites and chant rotation schedules.", "green", "sleep"),
			T("Investigation of forbidden homily broadcasts; witness interviews and calm messaging drafted.", "yellow", "broadcast"),
			T("Containment of mass-vision panic; staged public reassurance with tightly controlled procession.", "red", "panic"),
			T("Stabilization after hallucination-linked violence; liaison with Harmony Sanctum for long-tail care.", "red", "violence"),
		}
	},
	{
		name="Eon Fellowship",
		key="EOF",
		link=orderLink("Glinos (Reformed Stripping Path)","Eon Fellowship","Eon Fellowship"),
		tasks={
			T("Cold-weather relief checks and ration fairness review; storage seals verified.", "green", "relief"),
			T("Infrastructure endurance audit: roofs, walls, and bridges; frost damage triage list issued.", "yellow", "infrastructure"),
			T("Conflict de-escalation for winter resource disputes; binding queue rules posted.", "yellow", "queue"),
			T("Emergency shelter activation after storm loss; warm-hall rites and controlled entry.", "red", "storm"),
			T("High-risk escort for frontier convoys under threat; endurance cadres embedded with wardens.", "red", "frontier"),
		}
	},
	{
		name="Order of the Umbral Oracle",
		key="UOR",
		link=orderLink("Noctis (Reformed Stripping Path)","Order of the Umbral Oracle","Order of the Umbral Oracle"),
		tasks={
			T("Night watch and omen-reading in the temple quarter; guidance slips sealed for officials.", "green", "omen"),
			T("Discrete consultation for a magistrate; truth-testing rite witnessed by two scribes.", "yellow", "inquiry"),
			T("Shadow-route mapping after unexplained disappearances; safe-path markers installed.", "yellow", "disappear"),
			T("Emergency intervention after ‘demonic sign’ sightings; controlled crowd handling and ward placement.", "red", "demonic"),
			T("High-risk extraction of a threatened cantor; night escort to secure sanctuary.", "red", "abduction"),
		}
	},
	{
		name="Mystery of the Verdant Embrace",
		key="MVE",
		link=orderLink("Opsithe (Reformed Stripping Path)","Mystery of the Verdant Embrace","Mystery of the Verdant Embrace"),
		tasks={
			T("Harvest blessing and storage rite; spoilage prevention guidance issued to granaries.", "green", "harvest"),
			T("Garden-shrine restoration day with community labor; devotional planting aligned to calendar.", "green", "garden"),
			T("Food shortage mediation and ration arbitration; Temple Bank stipend adjustments proposed.", "yellow", "shortage"),
			T("Pest or blight containment rites; field inspections with farmers’ associations.", "yellow", "blight"),
			T("Famine-risk surge response; emergency distribution queues sanctified and monitored.", "red", "famine"),
		}
	},
	{
		name="Conclave Illuminara",
		key="CIL",
		link=orderLink("Tarsica (Reformed Stripping Path)","Conclave Illuminara","Conclave Illuminara"),
		tasks={
			T("Moonlit instruction circle for apprentices; eclipse-watch protocol rehearsed.", "green", "lunar"),
			T("Archive sealing of sensitive liturgies; access logs reviewed for anomalies.", "yellow", "archive"),
			T("Public lantern rite to counter rumor cascades; controlled routes and timing.", "yellow", "rumor"),
			T("Emergency omen bulletin after night panic; official statement drafted and delivered.", "red", "panic"),
			T("High-risk protection of an observatory site under threat; wards reinforced and watchers stationed.", "red", "threat"),
		}
	},
	{
		name="Sanctum Vitalis",
		key="SCV",
		link=orderLink("Plateau (Reformed Stripping Path)","Sanctum Vitalis","Sanctum Vitalis"),
		tasks={
			T("Clinic day: basic health screenings and hygiene instruction; water points checked.", "green", "health"),
			T("Supplement and ration fortification distribution under Temple Bank authorization.", "yellow", "nutrition"),
			T("Outbreak protocol rehearsal with civic healers; spacing rules posted at shrines.", "yellow", "outbreak"),
			T("Active outbreak response: triage lanes established; quarantine support and calm messaging.", "red", "outbreak"),
			T("Mass-casualty contingency drill after violence reports; intake points and transport routes mapped.", "red", "violence"),
		}
	},
	{
		name="Temple Alabaster",
		key="ALB",
		link=orderLink("Ivory (Reformed Stripping Path)","Temple Alabaster","Temple Alabaster"),
		tasks={
			T("Purity inspection of ritual vessels and incense stock; minor replacements approved.", "green", "purity"),
			T("Doctrinal correction session after unsigned broadsides; sanctioned texts distributed.", "yellow", "heresy"),
			T("Sanctuary security review for valuables; locking discipline and witness rules enforced.", "yellow", "security"),
			T("Emergency purification after poisoned incense discovery; wards placed and offenders sought.", "red", "poison"),
			T("High-risk protection of a central temple during unrest; entry control and calm liturgy.", "red", "unrest"),
		}
	},
	{
		name="Court of the Ironclad",
		key="CIC",
		link=orderLink("Fenrir (Reformed Stripping Path)","Court of the Ironclad","Court of the Ironclad"),
		tasks={
			T("Discipline drill for shrine wardens; non-lethal control and restraint practices.", "green", "training"),
			T("Escort for a threatened courier; route cleared and handover documented.", "yellow", "escort"),
			T("Guard training in a frontier district; coordination with civic militia.", "yellow", "militia"),
			T("Immediate reinforcement during riot signals; cordons established and safe egress opened.", "red", "riot"),
			T("High-risk interdiction support for a seized oratory; secure perimeter until authorities arrive.", "red", "siege"),
		}
	},
	{
		name="Accord Concordia",
		key="ACC",
		link=orderLink("Lukedu (Reformed Stripping Path)","The Accord Concordia","Accord Concordia"),
		tasks={
			T("Public harmony dialogue in a garden pavilion; disputants guided to a signed concord.", "green", "mediation"),
			T("Catacomb Harmonic Communion support: logistics, spacing, and participant verification.", "yellow", "rite"),
			T("Civic cooperation workshop for rival guilds; shared calendar commitments set.", "yellow", "guild"),
			T("Emergency reconciliation after faction clash; mediators embedded with escorts.", "red", "clash"),
			T("High-risk crowd calming after panic; sound-liturgical dispersal and safe routes enforced.", "red", "panic"),
		}
	},
	{
		name="Mystery of the Stygian Veil",
		key="MSV",
		link=orderLink("Styx (Reformed Stripping Path)","Mystery of the Stygian Veil","Mystery of the Stygian Veil"),
		tasks={
			T("Funerary liaison and cemetery vigil support; grief rites standardized.", "green", "funeral"),
			T("Transition counseling for displaced families; resettlement blessings recorded.", "yellow", "displacement"),
			T("Investigation of graveyard desecration rumor; ward placement and witness interviews.", "yellow", "desecration"),
			T("Emergency containment after death-cult provocation; controlled rites to prevent escalation.", "red", "provocation"),
			T("High-risk escort of relics through hostile streets; night route and guards coordinated.", "red", "escort"),
		}
	},
	{
		name="Sylvan Fellowship",
		key="SYF",
		link=orderLink("Faun (Reformed Stripping Path)","Sylvan Fellowship","Sylvan Fellowship"),
		tasks={
			T("Forest-edge shrine maintenance and trail safety check; minor repairs logged.", "green", "trail"),
			T("Ecology rite with farmers: pest balance and water stewardship guidance.", "yellow", "ecology"),
			T("Containment of illegal logging or poaching rumor; community patrol coordination.", "yellow", "poaching"),
			T("Emergency response to wildfire risk; firebreak rites and evacuation path marking.", "red", "wildfire"),
			T("High-risk protection for a threatened grove shrine; cordon and ward reinforcement.", "red", "threat"),
		}
	},
	{
		name="Mystery of Red Mirth",
		key="MRM",
		link=orderLink("Silenus (Reformed Stripping Path)","Myster of Red Mirth","Mystery of Red Mirth"),
		tasks={
			T("Morale rite and sanctioned celebration oversight; prevent disorder while restoring joy.", "green", "morale"),
			T("Festival logistics support with crowd timing; safe routes and water points verified.", "yellow", "festival"),
			T("Containment of illicit revel causing unrest; correction by sanctioned rite and fines.", "yellow", "illicit"),
			T("Emergency morale stabilization after violence; controlled gatherings and escorts.", "red", "violence"),
			T("High-risk de-escalation during riot-prone festival; staged dispersal and peace hymns.", "red", "riot"),
		}
	},
	{
		name="Rex Catonis",
		key="RCA",
		link=orderLink("Cato (Reformed Stripping Path)","Rex Catonis","Rex Catonis"),
		tasks={
			T("Civic order inspection: queue rules, market discipline, and shrine traffic flow.", "green", "order"),
			T("Administrative compliance review after complaints; corrective edicts posted.", "yellow", "compliance"),
			T("Guard liaison for a threatened civic hall; protocol rehearsal and watch rotation.", "yellow", "security"),
			T("Emergency restoration of order after mob action; curfew-litany and escorted dispersal.", "red", "mob"),
			T("High-risk support to authorities during barricade incident; perimeter discipline enforced.", "red", "barricade"),
		}
	},
	{
		name="Sanctum Delphica",
		key="SDL",
		link=orderLink("Sisera (Reformed Stripping Path)","Sanctum Delphica","Sanctum Delphica"),
		tasks={
			T("Oracle consultation clinic for officials; questions logged and sealed.", "green", "oracle"),
			T("Archive and library integrity check; catalog drift corrected and access verified.", "yellow", "archive"),
			T("Investigation of forged prophecy pamphlets; counterfeit sources traced.", "yellow", "forgery"),
			T("Emergency counter-homily after loudspeaker seizure; sanctioned address delivered.", "red", "broadcast"),
			T("High-risk protection for a key temple scholar; escort and secure lodging arranged.", "red", "threat"),
		}
	},
	{
		name="Ordo Amicitia",
		key="OAI",
		link=orderLink("Erasmus (Reformed Stripping Path)","Ordo Amicitia","Ordo Amicitia"),
		tasks={
			T("Charity distribution and household check-ins; kindness register updated for stipend support.", "green", "charity"),
			T("Offering logistics for a major rite; livestock and treasure accounted and sealed.", "yellow", "offering"),
			T("Mediation for neighborhood poverty disputes; aid allocations made transparent.", "yellow", "aid"),
			T("Emergency relief surge after incident; soup lines sanctified and guarded.", "red", "relief"),
			T("High-risk protection for a vulnerable procession route; escorts and safe shelters staged.", "red", "procession"),
		}
	},
	{
		name="Temple Illuminata",
		key="TIL",
		link=orderLink("Nikolaj (Reformed Stripping Path)","Temple Illuminata","Temple Illuminata"),
		tasks={
			T("Lamp and beacon maintenance blessing; public wayfinding clarity restored.", "green", "beacon"),
			T("Infrastructure illumination audit for tunnels and arcologies; repair notices issued.", "yellow", "infrastructure"),
			T("Investigation of signal tampering near a transit node; seals replaced and watchers posted.", "yellow", "tamper"),
			T("Emergency lighting deployment after sabotage; temporary beacons installed with escorts.", "red", "sabotage"),
			T("High-risk stabilization at a transport hub after panic; controlled flow and sanctuary lanes.", "red", "panic"),
		}
	},
}

-- Build index maps
local orderByName = {}
local orderNames = {}
for i, o in ipairs(orders) do
	orderByName[o.name] = o
	orderNames[i] = o.name
end

--------------------------------------------------------------------------------
-- 6) OPTIONAL: pull today's ReligiousEvents table and map per-city urgency
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
	local out = {}
	if type(wikitext) ~= "string" then return out end
	-- Row pattern: | date || City || Event || Urgency
	for line in wikitext:gmatch("[^\n]+") do
		if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then
			local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$")
			if city and urg then
				out[city] = {urg=urg, event=event or ""}
			end
		end
	end
	return out
end

local function threatSev(th)
	if not th or not th.urg then return nil end
	local u = th.urg
	if u == "N/A" or u == "LOW" then return "green" end
	if u == "MEDIUM" then return "yellow" end
	if u == "HIGH" or u == "CRITICAL" then return "red" end
	return "yellow"
end

local function biasOrderByThreat(nextInt, th)
	if not th or type(th.event) ~= "string" then
		return pickFromList(nextInt, orderNames, nil)
	end
	local e = th.event:lower()
	-- A few simple keyword biases
	if e:match("incendiary") or e:match("arson") or e:match("explosive") then return "Ignis Aeternum" end
	if e:match("poison") or e:match("outbreak") or e:match("treated") or e:match("hygiene") then return "Sanctum Vitalis" end
	if e:match("rumor") or e:match("panic") or e:match("dream") then return "Reverie Nebulous" end
	if e:match("demonic") or e:match("exorc") or e:match("symbols") then return "Order of the Umbral Oracle" end
	if e:match("offertory") or e:match("ledger") or e:match("counterfeit") or e:match("forged") then return "Guild of Golden Shadows" end
	if e:match("barricade") or e:match("armed") or e:match("zealots") then return "Court of the Ironclad" end
	if e:match("funeral") or e:match("cortège") or e:match("reliquary") then return "Mystery of the Stygian Veil" end
	return pickFromList(nextInt, orderNames, nil)
end

--------------------------------------------------------------------------------
-- 7) TEMPLE BANK NOTES (deterministic snippets)
--------------------------------------------------------------------------------
local bankNotes = {
	"Temple Bank stipend verified; local shrine receipts reconciled; next audit window scheduled.",
	"Voucher authorizations issued for ritual materials; chain-of-custody notes logged for any seized items.",
	"Investor liaison note filed; payroll routing confirmed for the deployed unit; compliance clerk assigned.",
	"Donation flow stabilized; offertory seals inspected; variance thresholds set for the next three days.",
	"Emergency stipend line opened for crowd-control and relief logistics; disbursement witnessed by two scribes.",
}

--------------------------------------------------------------------------------
-- 8) BUILD ONE CITY ROW (deterministic per city/day)
--------------------------------------------------------------------------------
local function buildCityRow(di, cityName, cityRegion, threatMap, includeThreat)
	local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
	local nextInt = lcg(hash31(seedStr))

	local th = includeThreat and threatMap and threatMap[cityName] or nil
	local chosenOrderName
	if includeThreat and th then
		chosenOrderName = biasOrderByThreat(nextInt, th)
	else
		-- Gentle geographic bias: some regions pick slightly different mixes
		local bias = cityRegion .. ":" .. cityName .. ":" .. tostring(di.dayOfYear)
		local idx = (hash31(bias) % #orderNames) + 1
		chosenOrderName = orderNames[idx]
	end

	local o = orderByName[chosenOrderName] or orders[1]

	-- Pick task; avoid repeating the same tag if possible (simple)
	local task = pickFromList(nextInt, o.tasks, nil)

	local sev = task.sev or "green"
	-- If threat is worse than task, bump severity
	if includeThreat and th then
		local ts = threatSev(th)
		if ts == "yellow" and sev == "green" then sev = "yellow" end
		if ts == "red" and sev ~= "red" then sev = "red" end
	end

	local unitName = pickOrgUnit(nextInt, sev)
	local unitMeta = org[unitName]
	local unitIndex = (nextInt() % 12) + 1
	local unitCode = string.format("%s-%s%d", o.key, unitMeta.code, unitIndex)
	local headcount = unitMeta.size

	local bank = pickFromList(nextInt, bankNotes, nil)

	local threatText = ""
	local threatUrg = ""
	if includeThreat and th then
		threatText = th.event or ""
		threatUrg = th.urg or ""
	end

	return {
		style = sevStyle(sev),
		date = di.text,
		region = cityRegion,
		city = cityLink(cityName),
		order = o.link,
		unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode),
		task = task.text,
		bank = bank,
		threat = threatText,
		threatUrg = threatUrg,
		sev = sev,
	}
end

--------------------------------------------------------------------------------
-- 9) MAIN TABLE
--------------------------------------------------------------------------------
function p.makeTable(frame)
	local args = (frame and frame.args) or {}
	local regionFilter = tostring(args.region or "All")
	local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
	local limit = tonumber(args.limit or "") -- optional max rows

	local di = getDateInfo()

	-- Optional: build threatMap once
	local threatMap = {}
	if includeThreat then
		-- NOTE: ReligiousEvents.makeTable uses math.randomseed internally; opt-in only.
		local ok, rel = pcall(require, "Module:ReligiousEvents")
		if ok and rel and type(rel.makeTable) == "function" then
			local t = rel.makeTable()
			threatMap = parseReligiousEventsTable(t)
		end
	end

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	table.insert(out, string.format("! style='white-space:nowrap;' | Date !! City !! Region !! Cult Order !! Unit !! Tasking !! Temple Bank Notes%s !! Severity",
		includeThreat and " !! Threat Watch" or ""
	))

	local count = 0
	for _, c in ipairs(cityOrder) do
		if regionPass(regionFilter, c.region) then
			local row = buildCityRow(di, c.name, c.region, threatMap, includeThreat)
			table.insert(out, "|-" .. row.style)
			if includeThreat then
				local threatCell = row.threat ~= "" and (row.threat .. " (" .. (row.threatUrg or "") .. ")") or "No reports / n.a."
				table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s || %s",
					row.date, row.city, row.region, row.order, row.unit, row.task, row.bank, threatCell, row.sev
				))
			else
				table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s",
					row.date, row.city, row.region, row.order, row.unit, row.task, row.bank, row.sev
				))
			end

			count = count + 1
			if limit and count >= limit then break end
		end
	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 rites / civic support&nbsp; ")
		table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Heightened oversight / rumor control / targeted intervention&nbsp; ")
		table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Crisis response / unrest / outbreak / high-risk operations")
		table.insert(out, "</div>")
	end

	-- Optional header note (calendar event)
	if di.calendarEvent and di.calendarEvent ~= "" then
		table.insert(out, string.format("\n<div style='margin-top:0.5em; font-size:90%%;'><b>Calendar note:</b> %s</div>", di.calendarEvent))
	end

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

--------------------------------------------------------------------------------
-- 10) CITY-ONLY VIEW
--------------------------------------------------------------------------------
function p.makeCity(frame)
	local args = (frame and frame.args) or {}
	local city = tostring(args.city or "")
	if city == "" then
		return "''Error: city parameter required.''"
	end
	args.limit = 1
	-- We reuse makeTable logic by filtering manually
	local di = getDateInfo()
	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"

	local threatMap = {}
	if includeThreat then
		local ok, rel = pcall(require, "Module:ReligiousEvents")
		if ok and rel and type(rel.makeTable) == "function" then
			threatMap = parseReligiousEventsTable(rel.makeTable())
		end
	end

	-- Find region
	local reg = "Core"
	for _, c in ipairs(cityOrder) do
		if c.name == city then reg = c.region break end
	end

	local row = buildCityRow(di, city, reg, threatMap, includeThreat)

	local out = {}
	table.insert(out, "{| class='wikitable'")
	if includeThreat then
		table.insert(out, "! Date !! City !! Region !! Cult Order !! Unit !! Tasking !! Temple Bank Notes !! Threat Watch !! Severity")
		table.insert(out, "|-" .. row.style)
		local threatCell = row.threat ~= "" and (row.threat .. " (" .. (row.threatUrg or "") .. ")") or "No reports / n.a."
		table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s || %s",
			row.date, row.city, row.region, row.order, row.unit, row.task, row.bank, threatCell, row.sev
		))
	else
		table.insert(out, "! Date !! City !! Region !! Cult Order !! Unit !! Tasking !! Temple Bank Notes !! Severity")
		table.insert(out, "|-" .. row.style)
		table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s",
			row.date, row.city, row.region, row.order, row.unit, row.task, row.bank, row.sev
		))
	end
	table.insert(out, "|}")
	return table.concat(out, "\n")
end

--------------------------------------------------------------------------------
-- 11) ORDER-ONLY VIEW (shows missions for that order across the city network)
--------------------------------------------------------------------------------
function p.makeOrder(frame)
	local args = (frame and frame.args) or {}
	local wanted = tostring(args.order or "")
	if wanted == "" then
		return "''Error: order parameter required (use exact order name, e.g., Order of Aurora Mystica).''"
	end
	local di = getDateInfo()
	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"

	local threatMap = {}
	if includeThreat then
		local ok, rel = pcall(require, "Module:ReligiousEvents")
		if ok and rel and type(rel.makeTable) == "function" then
			threatMap = parseReligiousEventsTable(rel.makeTable())
		end
	end

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	table.insert(out, "! Date !! City !! Region !! Unit !! Tasking !! Temple Bank Notes" .. (includeThreat and " !! Threat Watch" or "") .. " !! Severity")

	for _, c in ipairs(cityOrder) do
		local row = buildCityRow(di, c.name, c.region, threatMap, includeThreat)
		-- Compare by display name (strip wikilink wrapper crudely by checking map key)
		local orderObj = orderByName[wanted]
		if orderObj and row.order == orderObj.link then
			table.insert(out, "|-" .. row.style)
			if includeThreat then
				local threatCell = row.threat ~= "" and (row.threat .. " (" .. (row.threatUrg or "") .. ")") or "No reports / n.a."
				table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s",
					row.date, row.city, row.region, row.unit, row.task, row.bank, threatCell, row.sev
				))
			else
				table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s",
					row.date, row.city, row.region, row.unit, row.task, row.bank, row.sev
				))
			end
		end
	end

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

return p