Module:MissionaryDailyOpsBVR: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
Undo revision 387805 by NewZimiaGov (talk)
Tag: Undo
NewZimiaGov (talk | contribs)
No edit summary
Line 1: Line 1:
-- Module:MissionaryDailyOpsBVR
-- Module:MissionaryDailyOpsBVR
-- Daily missionary tasking ledger (Temple Bank / RSP).
-- Daily missionary tasking ledger (Temple Bank / Reformed Stripping Path).
--
--
-- WHAT’S FIXED / UPDATED (per your latest notes):
-- Embedded requirements:
--  • Correct CITY flags and CULT flags:
--  1) CITY FLAGS: follow Module:GeneralPortOperations flag-naming pattern, with overrides,
--     - City flag filename logic + Skýrophos override matches Module:GeneralPortOperations
--     BUT any missing flag file falls back to the Imperial Trade Union flag.
--     - Cult flag/logo filenames are taken directly from Module:GeneralPortOperations addLogo() mappings
-- 2) CULT FLAGS (logos): taken directly from Module:GeneralPortOperations addLogo() mappings
--     - No file-existence checks (avoids “too many expensive function calls”)
--     for ritual cults (and Temple of Aprobelle logo). Missing files fall back to ITU flag.
--  • Ops Alert integration (Module:BassaridiaForecast):
--  3) CULT LINKS: each cult links to its canonical “Mystery” section target (Divine page section)
--     - Adds Ops Alert column (badge + condition)
--     per Reformed_Stripping_Path#Planetary_Devotion (including the special “The …” headings).
--     - Orange/Red bumps severity + appends a short weather-driven overlay (Orange/Red only)
--     Temple Aprobelle links to [[Temple_Aprobelle]] (not a Mystery section).
--  • Includes Temple Aprobelle with dedicated task pool (10+ tasks)
--  4) OPS ALERT: integrates Module:BassaridiaForecast getOpsData() (badge + condition + overlays).
--  • Optional econ scan (EXPENSIVE) ONLY runs if showEcon=yes
--  5) Avoid “too many expensive function calls”:
-- • Optional Threat Watch (Module:ReligiousEvents) only if includeThreat=yes
--    - No candidate-search loops.
--    - Flag existence checks are cached and done at most once per unique file used.
--     - General Port content scan is ONLY executed if showEcon=yes.
--
--
-- Usage:
-- Usage:
Line 28: Line 30:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 0) SMALL UTILS
-- 0) BASIC UTILS
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function trim(s)
local function trim(s)
Line 146: Line 148:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 3) STYLE / SEVERITY
-- 3) EXPENSIVE-CALL CONTROL: cached file existence checks (1 per unique file)
--------------------------------------------------------------------------------
local fileExistsCache = {}
local function fileExists(fileName)
fileName = tostring(fileName or "")
if fileName == "" then return false end
if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
local t = mw.title.new("File:" .. fileName)
local ok = (t and t.exists) or false
fileExistsCache[fileName] = ok
return ok
end
 
--------------------------------------------------------------------------------
-- 4) STYLE / SEVERITY
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function sevStyle(sev)
local function sevStyle(sev)
Line 168: Line 184:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 4) CITY NETWORK
-- 5) CITY NETWORK
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function cityLink(name)
local function cityLink(name)
Line 253: Line 269:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 4.1) CITY FLAGS (matches GeneralPortOperations naming; NO existence checks)
-- 5.1) CITY FLAGS (GeneralPortOperations style + fallback-to-ITU if missing)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local DEFAULT_CITY_FLAG_FILE = "Imperial Trade Union flag.png"
local ITU_FLAG = "Imperial Trade Union flag.png"


local function normCityForFlag(name)
local function normCityForFlag(name)
-- Matches Module:GeneralPortOperations
-- Exactly the normalization used in Module:GeneralPortOperations
return (name or "")
return (name or "")
:gsub("[%s%-’]", "")
:gsub("[%s%-’]", "")
Line 267: Line 283:


local cityFlagOverrides = {
local cityFlagOverrides = {
-- Same override used in GeneralPortOperations
["Skýrophos"] = "SkyrophosFlag.png", -- override used in GeneralPortOperations
["Skýrophos"] = "SkyrophosFlag.png",
["Slevik"]   = "SlevikFlag.png.png", -- known oddity you’ve used elsewhere
-- Known filename oddity (you noted this explicitly)
["Sårensby"] = "SårensbyFlag.png",   -- known to exist in your notes
["Slevik"]   = "SlevikFlag.png.png",
}
}


local cityFlagCache = {}
local cityFlagMarkupCache = {}
local function getCityFlagMarkup(cityName)
local function getCityFlagMarkup(cityName)
if cityFlagCache[cityName] then return cityFlagCache[cityName] end
if cityFlagMarkupCache[cityName] then return cityFlagMarkupCache[cityName] end
local f = cityFlagOverrides[cityName] or (normCityForFlag(cityName) .. "Flag.png")
-- If a city isn't on the list or is a special case, fallback is still safe:
if not cityName or cityName == "" then f = DEFAULT_CITY_FLAG_FILE end
local mk = string.format("[[File:%s|20px]]", f)
cityFlagCache[cityName] = mk
return mk
end


--------------------------------------------------------------------------------
local candidate = cityFlagOverrides[cityName] or (normCityForFlag(cityName) .. "Flag.png")
-- 5) ORG UNITS (Missionary echelons)
local file = fileExists(candidate) and candidate or ITU_FLAG
--------------------------------------------------------------------------------
local org = {
Lampade    = { code="L", size=8  },
Hetairoi    = { code="H", size=12 },
Dodekade    = { code="D", size=18 },
Kleisthenes = { code="K", size=25 },
Stoa        = { code="S", size=40 },
Thiasos    = { code="T", size=60 },
}


local function pickOrgUnit(nextInt, sev)
local mk = string.format("[[File:%s|20px]]", file)
if sev == "green" then
cityFlagMarkupCache[cityName] = mk
return pickFromList(nextInt, {"Lampade","Hetairoi"}, nil)
return mk
elseif sev == "yellow" then
return pickFromList(nextInt, {"Hetairoi","Dodekade","Kleisthenes"}, nil)
else
return pickFromList(nextInt, {"Kleisthenes","Stoa","Thiasos"}, nil)
end
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 6) CULT FLAGS (taken from Module:GeneralPortOperations addLogo mappings)
-- 6) CULT FLAGS (logos) from Module:GeneralPortOperations addLogo() + fallback
-- NO existence checks. These filenames are the source of truth.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local DEFAULT_CULT_FLAG_FILE = "Imperial Trade Union flag.png"
local cultLogoFile = {
-- Ritual cult logos from Module:GeneralPortOperations
["Order of Aurora Mystica"]        = "Auroramystica.png",
["Harmony Sanctum"]              = "Harmonysanctum.png",
["Ignis Aeternum"]                = "Ignis Aeternum.png",
["Celestial Harmony Sect"]        = "HarmonySect.png",
["Guild of Golden Shadows"]      = "GoldenShadows.png",
["Azure Sentinel Sect"]          = "AzureSentinel.png",
["Reverie Nebulous"]              = "RevNeb.png",
["Eon Fellowship"]                = "EonFel.png",
["Order of the Umbral Oracle"]    = "Umbral.png",
["Mystery of the Verdant Embrace"]= "Verdant.png",
["Conclave Illuminara"]          = "Conclave.png",
["Sanctum Vitalis"]              = "Vitalis.png",
["Temple Alabaster"]              = "Alabaster.png",
["Court of the Ironclad"]        = "Ironclad.png",
["Accord Concordia"]              = "Concordia.png",
["Mystery of the Stygian Veil"]  = "StygianVe.png",
["Sylvan Fellowship"]            = "Sylvan.png",
["Mystery of Red Mirth"]          = "MirthFlag.png",
["Rex Catonis"]                  = "Catonis.png",
["Sanctum Delphica"]              = "SanctumDel.png",
["Ordo Amicitia"]                = "OrdoFl.png",
["Temple Illuminata"]            = "AureumFlag.png",


local cultLogoFile = {
-- Temple Aprobelle logo is mapped in GeneralPortOperations under serviceCompanies name:
-- From GeneralPortOperations addLogo() lines
["Temple Aprobelle"]             = "TempleAprobelleLogo.png",
["Order of Aurora Mystica"]           = "Auroramystica.png",
["Temple of Aprobelle"]           = "TempleAprobelleLogo.png",
["Harmony Sanctum"]                  = "Harmonysanctum.png",
["Celestial Harmony Sect"]            = "HarmonySect.png",
["Guild of Golden Shadows"]          = "GoldenShadows.png",
["Azure Sentinel Sect"]              = "AzureSentinel.png",
["Reverie Nebulous"]                  = "RevNeb.png",
["Mystery of the Verdant Embrace"]    = "Verdant.png",
["Conclave Illuminara"]              = "Conclave.png",
["Sanctum Vitalis"]                  = "Vitalis.png",
["Court of the Ironclad"]            = "Ironclad.png",
["Sanctum Delphica"]                  = "SanctumDel.png",
-- Not in ritualCults list, but GeneralPortOperations also adds a logo for Temple of Aprobelle
["Temple Aprobelle"]                 = "TempleAprobelleLogo.png",
-- Pharmacon Sect is in serviceCompanies; GeneralPortOperations doesn’t map it in the cult list
-- but you likely have a logo file; set it explicitly here:
["Pharmacon Sect"]                    = "PharmaconSect.png",
}
}


local cultFlagMarkupCache = {}
local function getCultFlagMarkup(cultName)
local function getCultFlagMarkup(cultName)
local f = cultLogoFile[cultName] or DEFAULT_CULT_FLAG_FILE
if cultFlagMarkupCache[cultName] then return cultFlagMarkupCache[cultName] end
return string.format("[[File:%s|18px]] ", f)
local candidate = cultLogoFile[cultName]
local file = (candidate and fileExists(candidate)) and candidate or ITU_FLAG
local mk = string.format("[[File:%s|18px]] ", file)
cultFlagMarkupCache[cultName] = mk
return mk
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 7) ORDERS + TASKS
-- 7) CULT LINKS (canonical “Mystery” section targets)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
local cultTargets = {
return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
["Order of Aurora Mystica"]        = { page="Eos_(Reformed_Stripping_Path)",     anchor="Order_of_Aurora_Mystica" },
end
["Harmony Sanctum"]               = { page="Micras_(Reformed_Stripping_Path)",   anchor="Harmony_Sanctum" },
 
["Ignis Aeternum"]               = { page="Pyros_(Reformed_Stripping_Path)",   anchor="Ignis_Aeternum" },
local function T(text, sev, tags) return { text=text, sev=sev, tags=tags or {} } end
-- Special headings on those pages include “The …”
 
["Celestial Harmony Sect"]        = { page="Indigo_(Reformed_Stripping_Path)",   anchor="The_Celestial_Harmony_Sect" },
-- Orders: chosen to match “cult logo” coverage from GeneralPortOperations.
["Guild of Golden Shadows"]       = { page="Chrysos_(Reformed_Stripping_Path)", anchor="The_Guild_of_Golden_Shadows" },
local orders = {
{key="AUM", name="Order of Aurora Mystica",
link=orderLink("Eos (Reformed Stripping Path)","Order_of_Aurora_Mystica","Order of Aurora Mystica"),
tasks={
T("Dawn reconciliation rite for competing guilds; written pledges sealed with Temple Bank clerks.","green",{"mediation","guild"}),
T("Sunrise ‘first-light’ counseling circle for strained households; referrals issued through shrine stewards.","green",{"care","mediation"}),
T("Dawn-watch bulletin: correct false omen claims and stabilize shift work across canal gates.","green",{"omen","public"}),
T("Crowd choreography support at morning festivals to prevent stampede behavior and pickpocket chaos.","yellow",{"crowd","festival"}),
T("Targeted rumor-clearing procession after unsigned pamphlets; witness statements taken and archived.","yellow",{"rumor","investigation"}),
T("Joint patrol with civic stewards through tunnels; lighting faults logged; intimidation incidents recorded.","yellow",{"security","infrastructure"}),
T("Emergency reconciliation summit after street violence; truce terms read and posted in public squares.","red",{"unrest","mediation"}),
T("Crisis dawn vigil after night incident; grief support + ‘no retaliation’ pledge drive.","red",{"crisis","grief","public"}),
T("Escort of a vulnerable witness to hearing; route screened for ambush or intimidation.","red",{"security","crisis"}),
T("Post-incident narrative repair: speaker coaching and public clarity bulletin to prevent reprisals.","yellow",{"public","analysis"}),
}
},
 
{key="HSA", name="Harmony Sanctum",
link="[[Harmony_Sanctum|Harmony Sanctum]]",
tasks={
T("Neighborhood harmony circle: mediated contract dispute between vendors and dock labor overseers.","green",{"mediation","trade"}),
T("Public etiquette instruction at shrine square; queue discipline and respectful speech enforcement.","green",{"public","order"}),
T("Household conflict de-escalation visits after complaint surge; written behavioral plan issued.","green",{"care","mediation"}),
T("Festival crowd-lane planning with stewards to prevent crush risks and rumor cascades.","yellow",{"festival","crowd"}),
T("Rumor triage: identify source nodes, issue corrective bulletins, and coordinate speaker messaging.","yellow",{"rumor","public"}),
T("Joint calming patrol at transit choke point; separation corridor opened for commuters.","yellow",{"transit","crowd"}),
T("Emergency stabilization during riot flare: dispersal corridors + visible care stations deployed.","red",{"unrest","crowd"}),
T("Night calm-litany enforcement after panic episode; coordinated supervised dispersal.","red",{"panic","public"}),
T("Public apology ceremony after inter-group violence; pledge collection and restitution ledger filed.","yellow",{"mediation","governance"}),
T("Oversight coaching for district clerks after audit findings; corrective protocol training delivered.","yellow",{"audit","governance"}),
}
},
 
{key="CHS", name="Celestial Harmony Sect",
link="[[Celestial_Harmony_Sect|Celestial Harmony Sect]]",
tasks={
T("Cosmic-order lecture at shrine school: reduce anxiety about star-omens and market rumors.","green",{"education","omen"}),
T("Guided meditation session for canal workers; stress reduction paired with safety brief.","green",{"care","public"}),
T("Omen bulletin update: correct a false eclipse claim before it drives hoarding behavior.","yellow",{"rumor","omen"}),
T("Quiet-zone enforcement near hospitals; controlled chanting to prevent crowd agitation.","yellow",{"health","order"}),
T("Investigation of counterfeit star-charts being sold to pilgrims; vendor interviews conducted.","yellow",{"investigation","trade"}),
T("Crisis counseling during mass scare; supervised calm stations established at shrine gates.","red",{"crisis","public"}),
T("Emergency messaging team embedded with War League stewards to prevent retaliatory panic.","red",{"crisis","rumor"}),
T("Nighttime stabilization patrol during fog warnings; guidance posted and speakers briefed.","yellow",{"transit","public"}),
T("Post-event debrief: update approved narratives; speaker scripts distributed to shrines.","yellow",{"analysis","public"}),
T("Festival harmony protocol review: ensure rites align with civic order requirements.","green",{"festival","audit"}),
}
},
 
{key="GGS", name="Guild of Golden Shadows",
link="[[Guild_of_Golden_Shadows|Guild of Golden Shadows]]",
tasks={
T("Quiet intelligence intake: compile rumor sources and identify forged vouchers without public escalation.","green",{"analysis","investigation"}),
T("Market observation rotation to spot coercion and counterfeit seals; reports filed with Temple Bank.","green",{"audit","trade"}),
T("Discreet witness protection arrangement for a vendor threatened by smugglers.","yellow",{"security","investigation"}),
T("Forgery trace: identify printing stall distributing schismatic pamphlets; evidence packet assembled.","yellow",{"investigation","archive"}),
T("Undercover purchase of fake shrine seals; seize ledgers and hand off to clerks.","yellow",{"trade","security"}),
T("Night extraction of a compromised informant; safe route and temporary housing arranged.","red",{"crisis","security"}),
T("Emergency suppression of panic narrative: remove false broadsheets and replace with approved bulletin.","red",{"panic","rumor"}),
T("Coordination with Temple Aprobelle on narrative discipline during crowd surge.","yellow",{"analysis","public"}),
T("Intercepted contraband manifest review; cross-check with corridor registry for evasions.","yellow",{"audit","logistics"}),
T("Post-incident attribution report delivered to War League liaison; next-day prevention plan drafted.","yellow",{"analysis","governance"}),
}
},
 
{key="ASS", name="Azure Sentinel Sect",
link=orderLink("Aether (Reformed Stripping Path)","Azure_Sentinel_Sect","Azure Sentinel Sect"),
tasks={
T("Waterfront perimeter walk and safety rope checks along canal districts.","green",{"infrastructure","safety"}),
T("Storm-drain inspection with civil works; blockages cleared and logged.","green",{"infrastructure","service"}),
T("Port-house briefing on embarkation discipline and baggage rules; theft prevention guidance posted.","green",{"public","transit"}),
T("Escort for medical supplies across canal bridges; accountability signatures obtained.","yellow",{"logistics","security"}),
T("Harbor rumor suppression: correct false ‘quarantine’ claims to prevent stampede and hoarding.","yellow",{"rumor","public"}),
T("Search-and-rescue standby after storm warnings; muster roster and equipment check.","yellow",{"safety","crisis"}),
T("Emergency flood response: evacuation guidance at waterfront shrines; calm channels maintained.","red",{"crisis","relief"}),
T("High-risk escort of officials through crowd at port gate; separation corridor enforced.","red",{"security","crowd"}),
T("Debris clearance coordination after gale; hazard markings placed on docks.","yellow",{"infrastructure","safety"}),
T("Crisis harbor closure messaging: distribute official notices; prevent vigilante enforcement.","red",{"crisis","public"}),
}
},
 
{key="RNB", name="Reverie Nebulous",
link=orderLink("Nephele (Reformed Stripping Path)","Reverie_Nebulous","Reverie Nebulous"),
tasks={
T("Dream interpretation circle with strict moderation; anxiety de-escalation emphasized.","green",{"care","public"}),
T("Quiet art therapy session for children after a scare; parent coaching provided.","green",{"care","education"}),
T("Public ‘sleep hygiene’ teaching for shift workers; rest scheduling guidance posted.","green",{"health","education"}),
T("Rumor mapping: identify which dreams are feeding a harmful narrative; corrective story deployed.","yellow",{"rumor","analysis"}),
T("Witness interviews after trance-gathering complaint; compliance conditions issued.","yellow",{"audit","public"}),
T("Nighttime crowd calming near a transit node; chants regulated to prevent stampede behavior.","yellow",{"crowd","transit"}),
T("Containment support for mass panic dream episode; calm stations and supervised rest zones established.","red",{"crisis","crowd"}),
T("Emergency counseling for families after death; grief rites paired with aid referrals.","red",{"grief","crisis"}),
T("High-risk ‘dream breach’ report assessed with Aprobelle liaisons; misinformation suppressed.","red",{"crisis","rumor"}),
T("Post-event debrief: public narrative edited to reduce retaliatory impulses; speaker training conducted.","yellow",{"public","analysis"}),
}
},
 
{key="VDE", name="Mystery of the Verdant Embrace",
link=orderLink("Thalassa (Reformed Stripping Path)","Mystery_of_the_Verdant_Embrace","Mystery of the Verdant Embrace"),
tasks={
T("Irrigation ritual + practical canal cleanup; silt removed and flow restored.","green",{"infrastructure","agriculture"}),
T("Seed blessing for market gardeners; pest-prevention guidance distributed.","green",{"agriculture","public"}),
T("Wetland path maintenance; travel warnings posted to prevent drownings.","green",{"safety","infrastructure"}),
T("Waterbank audit at an aqueduct; leakage reported and repair crews coordinated.","yellow",{"audit","infrastructure"}),
T("Field mediation over irrigation rights; shared schedule drafted and signed.","yellow",{"mediation","governance"}),
T("Brushfire risk patrol in dry districts; controlled burns prohibited and notices issued.","yellow",{"fire","public"}),
T("Disease vector mitigation in swamp edges; standing water treated and guidance posted.","yellow",{"health","infrastructure"}),
T("Post-storm debris clearance with shrine volunteers; hazard markings placed.","yellow",{"safety","infrastructure"}),
T("Emergency flood relief coordination: sandbag lines, food staging, and shelter support.","red",{"crisis","relief"}),
T("Crisis canal breach response: temporary closure, water reroute, and crowd calming.","red",{"crisis","infrastructure","crowd"}),
}
},
 
{key="CIL", name="Conclave Illuminara",
link="[[Conclave_Illuminara|Conclave Illuminara]]",
tasks={
T("Night-sky lecture and calendar correction posted to calm omen anxiety.","green",{"education","omen"}),
T("Quiet meditation ward established at a shrine; controlled breathing protocols taught.","green",{"care","public"}),
T("Lantern-route inspection for canal patrols; reflective markers placed to reduce accidents.","green",{"infrastructure","transit"}),
T("Dream-space audit: mirror pools cleaned, offerings catalogued, safety signage posted.","green",{"shrine","audit"}),
T("Investigation of counterfeit moon tokens; vendor interviews and seizure log.","yellow",{"investigation","trade"}),
T("Controlled night gathering after panic rumor; calm-litany enforced; dispersal plan coordinated.","yellow",{"rumor","crowd"}),
T("Nighttime crowd calming near transit node; chants regulated to prevent stampede behavior.","yellow",{"crowd","transit"}),
T("Emergency anti-panic messaging after ‘demonic sign’ claim; script distributed to shrine speakers.","red",{"crisis","rumor"}),
T("Escort for sensitive nocturnal rite during unrest; perimeter observers posted.","red",{"security","unrest"}),
T("Blackout safety drill during storm season (lanterns, guides, canal warning ropes).","yellow",{"infrastructure","safety"}),
}
},
 
{key="SVI", name="Sanctum Vitalis",
link="[[Sanctum_Vitalis|Sanctum Vitalis]]",
tasks={
T("Clinic resupply run: inventory reconciled with Temple Bank vouchers.","green",{"health","logistics"}),
T("Water-quality checks at canal taps; purification guidance posted.","green",{"health","infrastructure"}),
T("School hygiene lecture paired with modest blessing service.","green",{"health","education"}),
T("Routine fever screening at market gate; clinic referral slips issued.","green",{"health","public"}),
T("Temporary clinic tent near shrine square; triage lines marked and staffed.","yellow",{"health","crowd"}),
T("Contamination rumor assessment: test kits deployed to prevent panic.","yellow",{"health","rumor"}),
T("Heat illness prevention training and hydration discipline coaching.","yellow",{"health","public"}),
T("Mass casualty support after accident; triage, referral, and grief counseling coordination.","red",{"crisis","health"}),
T("Quarantine support in dormitory ward; supplies custody and compliance checks.","red",{"crisis","health","audit"}),
T("Night clinic staffing for unrest injuries; neutral triage posture announced.","red",{"unrest","health"}),
}
},
 
{key="SDE", name="Sanctum Delphica",
link="[[Sanctum_Delphica|Sanctum Delphica]]",
tasks={
T("Archive custody inspection: seals verified; storage conditions checked; ledger updated.","green",{"archive","audit"}),
T("Scribe training on custody chain; convoy paperwork rehearsed for shrine transfers.","green",{"education","logistics"}),
T("Pilgrim etiquette workshop at library shrine; damage prevention and quiet-zone enforcement.","green",{"public","education"}),
T("Index confiscated pamphlets for signature motifs; forward packet for correlation.","yellow",{"investigation","archive"}),
T("Witness interviews after shrine vandalism; sworn statements taken and sealed.","yellow",{"investigation","security"}),
T("Doctrinal quarantine assistance: entry controls and rumor suppression around restricted ward.","yellow",{"security","rumor"}),
T("Court packet assembly for desecration suspects; evidence collated with clerks.","yellow",{"governance","audit"}),
T("Night watch rotation at archive stacks following arson threat.","red",{"security","unrest"}),
T("Escort a sensitive artifact transfer; route denial markers and perimeter rites deployed.","red",{"security","logistics"}),
T("Emergency custody lockdown after attempted breach; rapid relocation plan activated.","red",{"crisis","security","archive"}),
}
},
 
{key="PHA", name="Pharmacon Sect",
link="[[Pharmacon_Sect|Pharmacon Sect]]",
tasks={
T("Dispensary audit: dosage seals verified; controlled compounds logged and countersigned.","green",{"health","audit"}),
T("Field compounding session with herb vendors; purity protocols enforced.","green",{"health","industry"}),
T("Clinic support for chronic cases; treatment plans documented in Temple Bank ledgers.","green",{"health","service"}),
T("Water purification consult at canal gate; intake screens inspected for sabotage.","yellow",{"infrastructure","investigation"}),
T("Extraction team dispatched to secure a medically useful mineral/resin source under escort.","yellow",{"logistics","security"}),
T("Rumor suppression briefing about ‘cursed medicine’; evidence packet and calm script issued.","yellow",{"rumor","public"}),
T("Emergency antidote preparation after suspected poisoning; vendor supply chain traced.","red",{"crisis","investigation","health"}),
T("Outbreak response: isolation guidance and clinic routing control at transit nodes.","red",{"crisis","health","public"}),
T("Hospital reinforcement for Eidolan exposure aftershock cases; monitoring schedule created.","red",{"crisis","health"}),
T("Post-incident debrief with Temple Aprobelle on panic dynamics; clinic scripts updated.","yellow",{"analysis","rumor"}),
}
},


{key="TAP", name="Temple Aprobelle",
["Azure Sentinel Sect"]          = { page="Thalassa_(Reformed_Stripping_Path)", anchor="Azure_Sentinel_Sect" },
link="[[Temple_Aprobelle|Temple Aprobelle]]",
["Reverie Nebulous"]             = { page="Nephele_(Reformed_Stripping_Path)", anchor="Reverie_Nebulous" },
tasks={
["Eon Fellowship"]                = { page="Glinos_(Reformed_Stripping_Path)",   anchor="Eon_Fellowship" },
T("Festival hygiene audit: vendor protocols verified; seals issued to compliant stalls.","green",{"health","audit","festival","public"}),
["Order of the Umbral Oracle"]    = { page="Noctis_(Reformed_Stripping_Path)",   anchor="Order_of_the_Umbral_Oracle" },
T("Rumor-control bulletin cycle: shrine-leader briefing + posted clarifications.","green",{"rumor","public","service"}),
["Mystery of the Verdant Embrace"]= { page="Opsithe_(Reformed_Stripping_Path)", anchor="Mystery_of_the_Verdant_Embrace" },
T("Grief-cadre home visits after accident; de-escalation coaching for relatives and stewards.","green",{"grief","mediation","service"}),
["Conclave Illuminara"]          = { page="Tarsica_(Reformed_Stripping_Path)", anchor="Conclave_Illuminara" },
T("School immunization coordination: clinic schedule posted and consent ledgers reconciled.","green",{"health","school","coordination"}),
["Sanctum Vitalis"]              = { page="Plateau_(Reformed_Stripping_Path)", anchor="Sanctum_Vitalis" },
T("Clinic-queue stabilization drill: entry metering and conflict prevention training.","yellow",{"queue","crowd","health","order"}),
["Temple Alabaster"]              = { page="Ivory_(Reformed_Stripping_Path)",   anchor="Temple_Alabaster" },
T("Loyalty and behavior audit of district office after complaint surge; corrective training issued.","yellow",{"audit","governance","compliance"}),
["Court of the Ironclad"]        = { page="Fenrir_(Reformed_Stripping_Path)",   anchor="Court_of_the_Ironclad" },
T("Anomaly-response dispatch after ‘demonic contamination’ claims; care paired with narrative discipline.","yellow",{"anomaly","rumor","public","health"}),
["Accord Concordia"]              = { page="Lukedu_(Reformed_Stripping_Path)",   anchor="Accord_Concordia" },
T("Pattern-analysis intake: confiscated schism texts logged and forwarded for signature correlation.","yellow",{"analysis","schism","security","archive"}),
["Mystery of the Stygian Veil"]  = { page="Styx_(Reformed_Stripping_Path)",     anchor="Mystery_of_the_Stygian_Veil" },
T("Transit node stabilization after crowd surge: calm stations and signage; steward handoffs established.","yellow",{"transit","crowd","stabilization"}),
["Sylvan Fellowship"]            = { page="Faun_(Reformed_Stripping_Path)",     anchor="Sylvan_Fellowship" },
T("Silencing-cadre activated after panic flare; controlled messaging + visible care stations deployed.","red",{"panic","rumor","stabilization"}),
["Mystery of Red Mirth"]          = { page="Silenus_(Reformed_Stripping_Path)", anchor="Mystery_of_Red_Mirth" },
T("Emergency stabilization in evacuation/fever center: calming rites, grief triage, rumor suppression.","red",{"crisis","health","crowd","stabilization"}),
["Rex Catonis"]                  = { page="Cato_(Reformed_Stripping_Path)",     anchor="Rex_Catonis" },
}
["Sanctum Delphica"]              = { page="Sisera_(Reformed_Stripping_Path)",   anchor="Sanctum_Delphica" },
},
["Ordo Amicitia"]                = { page="Erasmus_(Reformed_Stripping_Path)", anchor="Ordo_Amicitia" },
["Temple Illuminata"]            = { page="Nikolaj_(Reformed_Stripping_Path)", anchor="Temple_Illuminata" },
}
}


local orderByName = {}
local function cultLink(cultName)
for _, o in ipairs(orders) do orderByName[o.name] = o end
if cultName == "Temple Aprobelle" or cultName == "Temple of Aprobelle" then
return "[[Temple_Aprobelle|Temple Aprobelle]]"
end
local t = cultTargets[cultName]
if not t then
return string.format("[[%s]]", cultName)
end
return string.format("[[%s#%s|%s]]", t.page, t.anchor, cultName)
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 8) CITY PROFILES (bias order selection)
-- 8) ORG UNITS (Missionary echelons)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local cityProfiles = {
local org = {
["Vaeringheim"]    = {weights={AUM=2,HSA=2,CHS=1,GGS=1,ASS=2,RNB=1,VDE=2,CIL=2,SVI=2,SDE=2,PHA=1,TAP=3}},
Lampade    = { code="L", size=8  },
["Somniumpolis"]  = {weights={RNB=3,SVI=2,PHA=2,TAP=3,GGS=2,ASS=1}},
Hetairoi   = { code="H", size=12 },
["Luminaria"]      = {weights={AUM=3,CIL=2,HSA=2,TAP=2,SVI=1}},
Dodekade   = { code="D", size=18 },
["Lunalis Sancta"] = {weights={SDE=3,CIL=2,TAP=2,HSA=1}},
Kleisthenes = { code="K", size=25 },
["Delphica"]      = {weights={SDE=3,TAP=2,CIL=1,SVI=1}},
Stoa       = { code="S", size=40 },
["Thermosalem"]   = {weights={SVI=2,PHA=2,TAP=2,ASS=2,HSA=1}},
Thiasos    = { code="T", size=60 },
["Pyralis"]        = {weights={SVI=2,TAP=2,ASS=2,AUM=2,HSA=1}},
["Keybir-Aviv"]   = {weights={ASS=3,TAP=2,GGS=2,HSA=1,SVI=1}},
["Lewisburg"]      = {weights={ASS=2,GGS=2,TAP=2,HSA=1,SVI=1}},
["Diamandis"]      = {weights={TAP=2,SVI=2,AUM=1,RNB=1,HSA=1}},
["Tonar"]          = {weights={SDE=2,SVI=2,VDE=2,TAP=1,ASS=1}},
["Bashkim"]       = {weights={VDE=3,SVI=2,TAP=1,HSA=1}},
["Ourid"]          = {weights={VDE=3,SVI=2,TAP=1}},
["Ardclach"]      = {weights={ASS=2,SVI=2,TAP=1,CIL=1}},
["Riddersborg"]    = {weights={ASS=2,HSA=2,TAP=1,SVI=1}},
}
}


local function getProfile(cityName)
local function pickOrgUnit(nextInt, sev)
return cityProfiles[cityName] or {weights={AUM=1,HSA=1,ASS=1,SVI=1,SDE=1,TAP=1}}
if sev == "green" then
end
return pickFromList(nextInt, {"Lampade","Hetairoi"}, nil)
 
elseif sev == "yellow" then
--------------------------------------------------------------------------------
return pickFromList(nextInt, {"Hetairoi","Dodekade","Kleisthenes"}, nil)
-- 9) OPTIONAL: ECON SIGNALS (EXPENSIVE; only run when showEcon=yes)
else
--------------------------------------------------------------------------------
return pickFromList(nextInt, {"Kleisthenes","Stoa","Thiasos"}, nil)
local function getEconSignals()
local title = mw.title.new("General Port of Lake Morovia")
if not title then return {} end
local ok, content = pcall(function() return title:getContent() end)
if not ok or not content or content == "" then return {} end
 
local signals = {}
local text = normalizeSpaces(content)
 
for _, c in ipairs(cityOrder) do
local n = c.name
local hits = 0
local pattern = n:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])","%%%1")
for _ in text:gmatch(pattern) do
hits = hits + 1
if hits >= 6 then break end
end
if hits >= 3 then
signals[n] = {summary=string.format("High Port linkage (mentions: %d)", hits)}
end
end
end
return signals
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 10) OPTIONAL: THREAT WATCH (Module:ReligiousEvents)
-- 9) OPS ALERT (Module:BassaridiaForecast)
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
local map = {}
wikitext = tostring(wikitext or "")
for line in wikitext: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 = stripWiki(parts[2])
local ev = stripWiki(parts[3])
local urg = stripWiki(parts[4])
if city ~= "" and ev ~= "" then
map[city] = {event=ev, urg=urg}
end
end
end
end
return map
end
 
--------------------------------------------------------------------------------
-- 11) WX OPS ALERT INTEGRATION (Module:BassaridiaForecast)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
Line 667: Line 454:
if not op or not op.alert then return "" end
if not op or not op.alert then return "" end
local lvl = tonumber(op.alert.level) or 0
local lvl = tonumber(op.alert.level) or 0
if lvl < 2 then return "" end -- only Orange/Red append
if lvl < 2 then return "" end -- Orange/Red only


local haz = tostring(op.alert.hazard or "weather disruption")
local haz = tostring(op.alert.hazard or "weather disruption")
Line 690: Line 477:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 12) ORDER/TASK CHOICE
-- 10) OPTIONAL: THREAT WATCH (Module:ReligiousEvents)
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
local map = {}
wikitext = tostring(wikitext or "")
for line in wikitext: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 = stripWiki(parts[2])
local ev = stripWiki(parts[3])
local urg = stripWiki(parts[4])
if city ~= "" and ev ~= "" then
map[city] = {event=ev, urg=urg}
end
end
end
end
return map
end
 
--------------------------------------------------------------------------------
-- 11) OPTIONAL: ECON SIGNALS (EXPENSIVE; ONLY if showEcon=yes)
--------------------------------------------------------------------------------
local function getEconSignals()
local title = mw.title.new("General Port of Lake Morovia")
if not title then return {} end
local ok, content = pcall(function() return title:getContent() end)
if not ok or not content or content == "" then return {} end
 
local signals = {}
local text = normalizeSpaces(content)
 
for _, c in ipairs(cityOrder) do
local n = c.name
local hits = 0
local pattern = n:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])","%%%1")
for _ in text:gmatch(pattern) do
hits = hits + 1
if hits >= 6 then break end
end
if hits >= 3 then
signals[n] = {summary=string.format("High Port linkage (mentions: %d)", hits)}
end
end
return signals
end
 
--------------------------------------------------------------------------------
-- 12) TASK HELPERS
--------------------------------------------------------------------------------
local function T(text, sev) return {text=text, sev=sev} end
 
-- 10 tasks per cult (kept compact so the module remains maintainable)
local tasksByCult = {
["Order of Aurora Mystica"] = {
T("Dawn reconciliation rite for competing guilds; pledges sealed with Temple Bank clerks.","green"),
T("Sunrise bulletin: correct false omen claims; stabilize work-shift handoffs.","green"),
T("First-light counseling circle; referrals issued through shrine stewards.","green"),
T("Crowd choreography support at dawn festival; stampede risk reduced.","yellow"),
T("Targeted rumor-clearing procession after unsigned pamphlets; statements archived.","yellow"),
T("Joint tunnel patrol with civic stewards; lighting faults logged; intimidation reports filed.","yellow"),
T("Emergency truce summit after street violence; terms read and posted.","red"),
T("Crisis dawn vigil after night incident; grief support + no-retaliation pledges.","red"),
T("Escort for a vulnerable witness; route screened for ambush pressure.","red"),
T("Post-incident narrative repair: speaker coaching and clarity bulletin issued.","yellow"),
},
["Harmony Sanctum"] = {
T("Neighborhood harmony circle: mediated contract dispute between vendors and labor overseers.","green"),
T("Public etiquette instruction at shrine square; queue discipline enforced.","green"),
T("Household conflict visits after complaint surge; written behavioral plan issued.","green"),
T("Festival crowd-lane planning with stewards to prevent crush risks.","yellow"),
T("Rumor triage: source nodes identified; corrective bulletins posted.","yellow"),
T("Transit choke-point calming patrol; separation corridor opened for commuters.","yellow"),
T("Emergency stabilization during riot flare; dispersal lanes + care stations deployed.","red"),
T("Night calm-litany enforcement after panic episode; supervised dispersal coordinated.","red"),
T("Public apology ceremony after inter-group violence; restitution ledger filed.","yellow"),
T("Oversight coaching for district clerks after audit findings; corrective training delivered.","yellow"),
},
["Ignis Aeternum"] = {
T("Controlled forge-rite for municipal smiths; safety discipline and firewatch roster set.","green"),
T("Heat-hazard guidance posted for labor crews; hydration discipline enforced.","green"),
T("Festival brazier inspection: fuel handling and crowd distance compliance verified.","yellow"),
T("Brushfire-risk patrol: illegal burns suppressed; emergency water points marked.","yellow"),
T("Rumor containment after ‘cursed flame’ claims; sanctioned narrative posted.","yellow"),
T("Night watch at fuel depots after arson threat; perimeter stewards assigned.","red"),
T("Emergency response to flare incident: evacuation lanes opened; burn triage support.","red"),
T("Inspection of kiln/oven quarter after sabotage report; witnesses interviewed.","yellow"),
T("Firebreak coordination with Verdant Embrace; debris removed and lanes cleared.","yellow"),
T("Crisis crowd control during pyre procession; stampede pressure relieved.","red"),
},
["Celestial Harmony Sect"] = {
T("Cosmic-order lecture at shrine school; reduce anxiety about star-omens.","green"),
T("Guided meditation session for canal workers; stress reduction paired with safety brief.","green"),
T("Omen bulletin update: correct false eclipse claim before hoarding begins.","yellow"),
T("Quiet-zone enforcement near hospitals; regulated chanting to prevent agitation.","yellow"),
T("Investigation of counterfeit star-charts; vendor interviews and seizures logged.","yellow"),
T("Night guidance during fog warnings; speakers briefed and routes marked.","yellow"),
T("Crisis counseling during mass scare; supervised calm stations established.","red"),
T("Emergency messaging team embedded with stewards to prevent retaliatory panic.","red"),
T("Post-event debrief: approved narratives updated; scripts distributed to shrines.","yellow"),
T("Festival harmony protocol review: rites aligned with civic order requirements.","green"),
},
["Guild of Golden Shadows"] = {
T("Quiet intake: compile rumor sources and identify forged vouchers without escalation.","green"),
T("Market observation rotation; coercion and counterfeit seals flagged to Temple Bank.","green"),
T("Discreet witness protection arrangement for vendor threatened by smugglers.","yellow"),
T("Forgery trace: identify print stall distributing schismatic pamphlets; evidence packet built.","yellow"),
T("Undercover purchase of fake shrine seals; ledgers seized and handed to clerks.","yellow"),
T("Intercept contraband manifest; cross-check corridor registry for evasions.","yellow"),
T("Night extraction of compromised informant; safe route and temporary housing arranged.","red"),
T("Emergency suppression of panic narrative: false broadsheets removed, official bulletin posted.","red"),
T("Coordination with Temple Aprobelle on narrative discipline during crowd surge.","yellow"),
T("Attribution report delivered to War League liaison; prevention plan drafted.","yellow"),
},
["Azure Sentinel Sect"] = {
T("Waterfront perimeter walk and safety rope checks along canal districts.","green"),
T("Storm-drain inspection with civil works; blockages cleared and logged.","green"),
T("Port-house briefing on embarkation discipline; theft prevention guidance posted.","green"),
T("Escort for medical supplies across canal bridges; accountability signatures obtained.","yellow"),
T("Harbor rumor suppression: correct false quarantine claims to prevent stampede.","yellow"),
T("Search-and-rescue standby after storm warnings; muster roster and equipment checked.","yellow"),
T("Emergency flood response: evacuation guidance at waterfront shrines; calm channels maintained.","red"),
T("High-risk escort of officials through crowd at port gate; separation corridor enforced.","red"),
T("Debris clearance coordination after gale; hazard markings placed on docks.","yellow"),
T("Crisis harbor closure messaging: distribute notices; prevent vigilante enforcement.","red"),
},
["Reverie Nebulous"] = {
T("Dream interpretation circle with strict moderation; anxiety de-escalation emphasized.","green"),
T("Quiet art therapy for children after scare; parent coaching provided.","green"),
T("Public sleep-hygiene teaching for shift workers; rest scheduling guidance posted.","green"),
T("Rumor mapping: identify dream narratives feeding panic; corrective story deployed.","yellow"),
T("Witness interviews after trance-gathering complaint; compliance conditions issued.","yellow"),
T("Night crowd calming near transit node; chants regulated to prevent stampede behavior.","yellow"),
T("Containment support for mass panic dream episode; supervised rest zones established.","red"),
T("Emergency counseling for families after death; grief rites paired with aid referrals.","red"),
T("High-risk dream-breach report assessed with Aprobelle liaisons; misinformation suppressed.","red"),
T("Post-event debrief: narrative edited to reduce retaliatory impulses; speaker training conducted.","yellow"),
},
["Eon Fellowship"] = {
T("Timekeeping and schedule audit at market gates; shift rotations clarified to reduce conflict.","green"),
T("Public calendar correction bulletin; prevent rumor-driven delays and missed rites.","green"),
T("Queue timing discipline drill at clinic or shrine; entry metering tested and refined.","yellow"),
T("Witness coordination for a hearing; arrival windows staged to prevent intimidation.","yellow"),
T("Transit-flow smoothing at rail/canal chokepoint; signage updated for timing cadence.","yellow"),
T("Record review: repeated late-stipend disputes analyzed; corrective SOP issued.","yellow"),
T("Emergency stabilization after cascading delays; volunteer surge roster synchronized.","red"),
T("Night patrol timing plan during fog; safe intervals posted and enforced.","yellow"),
T("Audit of clerk office after missed filings; corrective training delivered.","yellow"),
T("Crisis schedule triage during unrest; safe hours for movement posted publicly.","red"),
},
["Order of the Umbral Oracle"] = {
T("Quiet-night rite with strict entry control; visitor conduct posted to reduce disturbances.","green"),
T("Risk bulletin for shadow-sign nights; discourage panic gatherings at transit nodes.","yellow"),
T("Investigation of anonymous pamphlets claiming ‘oracle sanction’; counterfeit marks documented.","yellow"),
T("Counseling for anxious households after nightmare spike; referral paths established.","green"),
T("Night watch at shrine steps to deter harassment of pilgrims; steward coordination.","yellow"),
T("Containment support for ‘demonic sign’ rumor; official messaging delivered in controlled cadence.","red"),
T("Witness protection for informant threatened by cult rival; safehouse route planned.","red"),
T("Crisis nocturnal dispersal plan after crowd surge; safe corridors opened.","red"),
T("Post-incident narrative discipline: speaker scripts updated; rumor sources mapped.","yellow"),
T("Audit of unsanctioned night gatherings; compliance restrictions issued.","yellow"),
},
["Mystery of the Verdant Embrace"] = {
T("Irrigation rite + canal cleanup; silt removed and flow restored.","green"),
T("Seed blessing for gardeners; pest-prevention guidance distributed.","green"),
T("Wetland path maintenance; travel warnings posted to prevent drownings.","green"),
T("Aqueduct audit: leakage reported; repair crews coordinated.","yellow"),
T("Field mediation over irrigation rights; shared schedule drafted and signed.","yellow"),
T("Brushfire risk patrol; illegal burns prohibited and notices issued.","yellow"),
T("Vector mitigation at swamp edges; standing water treated and guidance posted.","yellow"),
T("Post-storm debris clearance with volunteers; hazard markings placed.","yellow"),
T("Emergency flood relief: sandbag lines, food staging, shelter support.","red"),
T("Crisis canal breach response: closure, reroute, crowd calming in shrine districts.","red"),
},
["Conclave Illuminara"] = {
T("Night-sky lecture; correct omen anxiety and explain eclipse calendars.","green"),
T("Quiet meditation ward at shrine; breathing protocols taught.","green"),
T("Lantern-route inspection; reflective markers placed to reduce accidents.","green"),
T("Dream-space audit: offerings catalogued; safety signage posted.","green"),
T("Counterfeit moon-token investigation; vendor interviews and seizure log.","yellow"),
T("Controlled night gathering after rumor; calm-litany enforced; dispersal coordinated.","yellow"),
T("Night crowd calming near transit node; chants regulated to prevent stampede behavior.","yellow"),
T("Emergency anti-panic messaging after ‘demonic sign’ claim; scripts distributed.","red"),
T("Escort for sensitive nocturnal rite during unrest; perimeter observers posted.","red"),
T("Blackout safety drill during storm season; guides and warning ropes staged.","yellow"),
},
["Sanctum Vitalis"] = {
T("Clinic resupply: inventory reconciled with Temple Bank vouchers.","green"),
T("Water-quality checks at canal taps; purification guidance posted.","green"),
T("School hygiene lecture; handwashing and food safety emphasized.","green"),
T("Routine fever screening at market gate; clinic referrals issued.","green"),
T("Temporary clinic tent near shrine square; triage lines marked and staffed.","yellow"),
T("Contamination rumor assessment: test kits deployed to prevent panic.","yellow"),
T("Heat illness prevention training; hydration discipline coached.","yellow"),
T("Mass casualty support after accident; triage and grief coordination.","red"),
T("Quarantine support in dormitory ward; custody and compliance checks.","red"),
T("Night clinic staffing for unrest injuries; neutral triage posture announced.","red"),
},
["Temple Alabaster"] = {
T("Sanctuary cleanliness audit; white-stone precincts washed and offerings catalogued.","green"),
T("Pilgrim conduct briefing; quiet-zone and nonviolence rules posted.","green"),
T("Artifact handling training for novice stewards; custody marks standardized.","yellow"),
T("Dispute mediation over shrine access rights; schedule drafted and signed.","yellow"),
T("Rumor suppression after ‘tainted stone’ claim; evidence posted and speaker briefed.","yellow"),
T("Escort of a sacred item to storage; chain-of-custody verified.","yellow"),
T("Emergency crowd control during pilgrimage surge; lanes marked and calm stations opened.","red"),
T("Night watch on shrine steps after vandal threat; stewards rotated.","yellow"),
T("Crisis shelter activation in stone precinct during storm; intake and calm discipline enforced.","red"),
T("Post-incident restitution hearing prep; witness statements sealed with clerks.","yellow"),
},
["Court of the Ironclad"] = {
T("Discipline training for volunteers; nonviolent restraint emphasized.","green"),
T("Public order presence at shrine steps; crowd etiquette enforced.","green"),
T("Oath-keeping workshop; pledge cards sealed for civic stewards.","green"),
T("Route hardening walk: broken lamps and unsafe crossings flagged.","yellow"),
T("Contraband confiscation support; evidence logged for Temple Bank clerks.","yellow"),
T("Night watch on canal bridges after sabotage attempt; checkpoint discipline enforced.","yellow"),
T("High-risk escort for witness leaving hearing; safehouse transfer arranged.","red"),
T("Emergency deterrence patrol after riot flare; separation corridors opened.","red"),
T("Crisis reinforcement at clinic gate during panic surge; queue discipline established.","red"),
T("Post-incident intimidation review; complaints investigated and discipline applied.","yellow"),
},
["Accord Concordia"] = {
T("Council mediation session between neighborhood elders; written concord signed.","green"),
T("Public forum moderation; speaking order enforced to reduce fights.","green"),
T("Dispute settlement over vendor stalls; restitution terms logged.","green"),
T("Civic etiquette training for steward teams; de-escalation scripts practiced.","yellow"),
T("Inter-cult boundary meeting; shared space rules posted at shrine gates.","yellow"),
T("Rumor containment after inflammatory broadsides; corrective bulletin cycle launched.","yellow"),
T("Emergency truce enforcement during street clash; separation lines held.","red"),
T("Crisis reconciliation hearing after riot; pledge collection and witness protection.","red"),
T("Audit of clerk office after complaint surge; corrective plan countersigned.","yellow"),
T("Post-event community repair ceremony; restitution ledger filed with Temple Bank.","yellow"),
},
["Mystery of the Stygian Veil"] = {
T("Funerary custody check: offerings catalogued; tomb precinct rules posted.","green"),
T("Grief counseling circle; practical aid referrals arranged.","green"),
T("Mortuary hygiene audit; disease-prevention protocols verified.","yellow"),
T("Rumor suppression after ‘restless dead’ claims; evidence bulletin issued.","yellow"),
T("Escort for body transfer; route secured and witnesses logged.","yellow"),
T("Night vigil supervision to prevent stampede or retaliation violence.","yellow"),
T("Crisis response after mass death incident; triage of grief + order restoration.","red"),
T("Emergency calm stations at cemetery gate during panic; dispersal coordinated.","red"),
T("Witness interviews after desecration; sworn statements sealed.","yellow"),
T("Post-incident reconciliation rite; retaliation prevention pledges collected.","yellow"),
},
["Sylvan Fellowship"] = {
T("Forest-path maintenance; travel warnings posted for pilgrims.","green"),
T("Wildlife safety briefing; reduce accidents on shrine trails.","green"),
T("Herb and timber vendor conduct audit; sustainable collection rules enforced.","yellow"),
T("Brushfire-watch patrol; illegal burns suppressed and water points marked.","yellow"),
T("Mediation between trappers and villagers; shared hunting schedule signed.","yellow"),
T("Stormfall debris cleanup with volunteers; hazard markings placed.","yellow"),
T("Emergency shelter staging in forest precinct during storm; intake controlled.","red"),
T("Crisis search for missing pilgrim after fog; guides deployed with calm messaging.","red"),
T("Rumor containment after ‘forest curse’ claim; corrective bulletin cycle run.","yellow"),
T("Post-event ecological repair plan filed; volunteer crews scheduled.","green"),
},
["Mystery of Red Mirth"] = {
T("Festival conduct briefing; prevent brawls and dangerous dares.","green"),
T("Public merriment patrol: de-escalate fights before they spread.","yellow"),
T("Vendor compliance audit for intoxicants; counterfeit batches seized.","yellow"),
T("Rumor suppression after scandalous broadsides; approved narrative posted.","yellow"),
T("Crowd choreography for parade route; crush-risk minimized.","yellow"),
T("Grief-to-mirth counseling after loss; prevent destructive spirals.","green"),
T("Emergency riot containment after festival violence; separation corridors opened.","red"),
T("Crisis clinic support for injuries after revel incident; triage and calm stations.","red"),
T("Witness statements collected after assault; handoff to clerks.","yellow"),
T("Post-incident restitution and apology rite; pledges collected.","yellow"),
},
["Rex Catonis"] = {
T("Civic order proclamation at court square; rules of conduct reissued.","green"),
T("Petty dispute docket triage; mediation before formal hearings.","green"),
T("Audit of municipal fines ledger; correct corrupt entries.","yellow"),
T("Vendor permit compliance sweep; counterfeit permits seized.","yellow"),
T("Rumor suppression after ‘illegal decrees’ claim; clarifying bulletin posted.","yellow"),
T("Escort of court clerks during sensitive hearing; intimidation deterrence.","yellow"),
T("Emergency order restoration after riot; curfew messaging and dispersal corridors.","red"),
T("Crisis protection of court archives after arson threat; perimeter posted.","red"),
T("Witness interviews after vandalism of civic symbol; statements sealed.","yellow"),
T("Post-event governance training for stewards; corrective SOP issued.","yellow"),
},
["Sanctum Delphica"] = {
T("Archive custody inspection: seals verified; storage conditions checked.","green"),
T("Scribe training on custody chain; convoy paperwork rehearsed.","green"),
T("Pilgrim etiquette workshop at library shrine; quiet-zone enforced.","green"),
T("Index confiscated pamphlets for signature motifs; correlation packet forwarded.","yellow"),
T("Witness interviews after shrine vandalism; sworn statements taken.","yellow"),
T("Doctrinal quarantine assistance: entry controls and rumor suppression.","yellow"),
T("Court packet assembly for desecration suspects; evidence collated.","yellow"),
T("Night watch rotation at archive stacks following arson threat.","red"),
T("Escort sensitive artifact transfer; route denial markers deployed.","red"),
T("Emergency custody lockdown after breach attempt; relocation plan activated.","red"),
},
["Ordo Amicitia"] = {
T("Public kindness drive: food distribution at shrine steps; ledger reconciled.","green"),
T("Mediation between families after dispute; amity pledges collected.","green"),
T("Volunteer training: calm communication and nonviolent intervention.","green"),
T("Rumor containment after defamatory pamphlets; corrective bulletin cycle launched.","yellow"),
T("Clinic queue kindness patrol; conflict prevention and assistance to elderly.","yellow"),
T("Audit of relief supplies to prevent theft; custody seals verified.","yellow"),
T("Emergency care stations during unrest; neutral aid posture announced.","red"),
T("Crisis grief counseling after fatal incident; aid referrals coordinated.","red"),
T("Witness protection for targeted volunteer; safe route arranged.","yellow"),
T("Post-event reconciliation rite; retaliation prevention pledges collected.","yellow"),
},
["Temple Illuminata"] = {
T("Illumination vigil: lantern discipline and public calm guidance posted.","green"),
T("Night watch of sacred precinct; entry control and conduct rules enforced.","yellow"),
T("Omen bulletin: correct false ‘dark star’ claim; discourage panic.","yellow"),
T("Archive of testimonies and signs; witness statements catalogued.","yellow"),
T("Escort for a sensitive nocturnal rite during unrest; perimeter observers posted.","red"),
T("Emergency messaging after ‘demonic symbol’ report; scripts distributed to speakers.","red"),
T("Crisis crowd dispersal after night surge; calm stations and guidance routes opened.","red"),
T("Audit of unsanctioned night gatherings; compliance restrictions issued.","yellow"),
T("Counseling for anxious households; referral paths established.","green"),
T("Post-incident narrative discipline; rumor sources mapped and neutralized.","yellow"),
},
["Temple Aprobelle"] = {
T("Festival hygiene audit: vendor protocols verified; seals issued to compliant stalls.","green"),
T("Rumor-control bulletin cycle: shrine-leader briefing + posted clarifications.","green"),
T("Grief-cadre home visits after accident; de-escalation coaching delivered.","green"),
T("School immunization coordination: clinic schedule posted; consent ledgers reconciled.","green"),
T("Clinic-queue stabilization drill: entry metering + conflict prevention training.","yellow"),
T("Behavior audit of district office after complaint surge; corrective training issued.","yellow"),
T("Anomaly-response dispatch after contamination claims; care paired with narrative discipline.","yellow"),
T("Pattern-analysis intake: schism texts logged and forwarded for signature correlation.","yellow"),
T("Silencing-cadre activated after panic flare; controlled messaging + care stations deployed.","red"),
T("Emergency stabilization in evacuation/fever center: calming rites + rumor suppression.","red"),
},
}
 
--------------------------------------------------------------------------------
-- 13) CITY PROFILES (bias order selection)
--------------------------------------------------------------------------------
local cityProfiles = {
["Vaeringheim"]    = {weights={AUM=3,HSA=2,ASS=2,GGS=2,SVI=2,SDE=2,TAP=3}},
["Somniumpolis"]  = {weights={RNB=3,SVI=2,GGS=2,TAP=3,ASS=1}},
["Luminaria"]      = {weights={AUM=3,CON=2,HSA=2,TAP=2,SVI=1}},
["Lunalis Sancta"] = {weights={SDE=3,CON=2,TAP=2,HSA=1}},
["Delphica"]      = {weights={SDE=3,TAP=2,CON=1,SVI=1}},
["Thermosalem"]    = {weights={SVI=2,TAP=2,ASS=2,HSA=1}},
["Pyralis"]        = {weights={IGA=2,SVI=2,TAP=2,ASS=2,AUM=1}},
["Keybir-Aviv"]    = {weights={ASS=3,TAP=2,GGS=2,HSA=1}},
["Diamandis"]      = {weights={TAP=2,SVI=2,AUM=1,RNB=1,MIR=1}},
["Tonar"]          = {weights={SDE=2,SVI=2,VDE=2,TAP=1,CICL=1}},
["Bashkim"]        = {weights={VDE=3,SVI=2,TAP=1}},
["Ourid"]          = {weights={VDE=3,SVI=2,TAP=1}},
["Ardclach"]      = {weights={ASS=2,SVI=2,TAP=1,CON=1}},
}
 
local function getProfile(cityName)
return cityProfiles[cityName] or {weights={AUM=1,HSA=1,IGA=1,CHS=1,GGS=1,ASS=1,RNB=1,EOF=1,UMB=1,VDE=1,CON=1,SVI=1,ALB=1,CICL=1,ACC=1,STY=1,SYL=1,MIR=1,CAT=1,SDE=1,AMI=1,ILL=1,TAP=1}}
end
 
--------------------------------------------------------------------------------
-- 14) ORDERS (missionary-capable cult list)
-- NOTE: These names must match GeneralPortOperations ritualCults list + Temple Aprobelle.
--------------------------------------------------------------------------------
local orders = {
{key="AUM", name="Order of Aurora Mystica"},
{key="HSA", name="Harmony Sanctum"},
{key="IGA", name="Ignis Aeternum"},
{key="CHS", name="Celestial Harmony Sect"},
{key="GGS", name="Guild of Golden Shadows"},
{key="ASS", name="Azure Sentinel Sect"},
{key="RNB", name="Reverie Nebulous"},
{key="EOF", name="Eon Fellowship"},
{key="UMB", name="Order of the Umbral Oracle"},
{key="VDE", name="Mystery of the Verdant Embrace"},
{key="CON", name="Conclave Illuminara"},
{key="SVI", name="Sanctum Vitalis"},
{key="ALB", name="Temple Alabaster"},
{key="CICL", name="Court of the Ironclad"},
{key="ACC", name="Accord Concordia"},
{key="STY", name="Mystery of the Stygian Veil"},
{key="SYL", name="Sylvan Fellowship"},
{key="MIR", name="Mystery of Red Mirth"},
{key="CAT", name="Rex Catonis"},
{key="SDE", name="Sanctum Delphica"},
{key="AMI", name="Ordo Amicitia"},
{key="ILL", name="Temple Illuminata"},
{key="TAP", name="Temple Aprobelle"},
}
 
local orderByName = {}
for _, o in ipairs(orders) do orderByName[o.name] = o end
 
--------------------------------------------------------------------------------
-- 15) SEVERITY + ORDER/TASK CHOICE
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function inferSeverity(cityTier, taskSev, threatUrg)
local function inferSeverity(cityTier, taskSev, threatUrg)
Line 709: Line 886:
weighted[#weighted+1] = {key=key, w=tonumber(w) or 0}
weighted[#weighted+1] = {key=key, w=tonumber(w) or 0}
end
end
 
-- threat bump: prefer Aprobelle + safety/health + shadows
-- threat bump: prefer Aprobelle / health / security / shadows
if threat and urgencyScore(threat.urg) >= 2 then
if threat and urgencyScore(threat.urg) >= 2 then
for _, it in ipairs(weighted) do
for _, it in ipairs(weighted) do
if it.key == "TAP" or it.key == "SVI" or it.key == "PHA" or it.key == "ASS" or it.key == "GGS" then
if it.key == "TAP" or it.key == "SVI" or it.key == "ASS" or it.key == "GGS" or it.key == "SDE" then
it.w = it.w + 2
it.w = it.w + 2
end
end
end
end
end
end
 
local pickedKey = pickWeighted(nextInt, weighted).key
local picked = pickWeighted(nextInt, weighted).key
for _, o in ipairs(orders) do
for _, o in ipairs(orders) do
if o.key == picked then return o end
if o.key == pickedKey then return o end
end
end
return orders[1]
return orders[1]
end
end


local function pickTask(nextInt, o, sev)
local function pickTask(nextInt, cultName, sev)
local pool = {}
local pool = tasksByCult[cultName] or tasksByCult["Harmony Sanctum"] or {}
for _, t in ipairs(o.tasks or {}) do
-- bias severity matching
local weighted = {}
for _, t in ipairs(pool) do
local w = 1
local w = 1
if t.sev == sev then w = 4
if t.sev == sev then w = 4
elseif sev == "red" and t.sev == "yellow" then w = 2
elseif sev == "red" and t.sev == "yellow" then w = 2
elseif sev == "yellow" and t.sev == "green" then w = 2 end
elseif sev == "yellow" and t.sev == "green" then w = 2 end
pool[#pool+1] = {t=t, w=w}
table.insert(weighted, {t=t, w=w})
end
if #weighted == 0 then
return {text="Routine rites and civic support (no task pool configured).", sev="green"}
end
end
return pickWeighted(nextInt, pool).t
return pickWeighted(nextInt, weighted).t
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 13) BANK NOTES
-- 16) TEMPLE BANK NOTES
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local bankNotes = {
local bankNotes = {
Line 751: Line 931:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 14) BUILD ONE CITY ROW
-- 17) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function buildCityRow(di, c, threatMap, includeThreat, showEcon, econSignals)
local function buildCityRow(di, c, threatMap, includeThreat, showEcon, econSignals)
Line 761: Line 941:
local o = pickOrder(nextInt, cityName, th)
local o = pickOrder(nextInt, cityName, th)


-- task + severity
local baseTask = pickTask(nextInt, o.name, "yellow")
local baseTask = pickTask(nextInt, o, "yellow")
local threatUrg = th and th.urg or ""
local threatUrg = th and th.urg or ""
local sev = inferSeverity(c.tier, baseTask.sev, threatUrg)
local sev = inferSeverity(c.tier, baseTask.sev, threatUrg)


-- wx overlay
-- WX overlay
local opsBadge, wxData = opsCell(cityName, di.year, di.dayOfYear)
local opsBadge, wxData = opsCell(cityName, di.year, di.dayOfYear)
sev = bumpSevWeather(sev, wxData)
sev = bumpSevWeather(sev, wxData)
Line 782: Line 961:
-- econ
-- econ
local econ = ""
local econ = ""
if showEcon and econSignals and econSignals[cityName] then econ = econSignals[cityName].summary or "" end
if showEcon and econSignals and econSignals[cityName] then
econ = econSignals[cityName].summary or ""
end


return {
return {
Line 790: Line 971:
region = c.region,
region = c.region,
ops = opsBadge,
ops = opsBadge,
order = getCultFlagMarkup(o.name) .. o.link,
order = getCultFlagMarkup(o.name) .. cultLink(o.name),
unit = string.format("%s (%d) [%s]", unitName, unitMeta.size, unitCode),
unit = string.format("%s (%d) [%s]", unitName, unitMeta.size, unitCode),
task = taskText,
task = taskText,
Line 802: Line 983:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 15) MAIN TABLE
-- 18) MAIN TABLE
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeTable(frame)
function p.makeTable(frame)
Line 814: Line 995:
local di = getDateInfo()
local di = getDateInfo()


-- Threat map
-- Threat map (only if requested)
local threatMap = {}
local threatMap = {}
if includeThreat then
if includeThreat then
Line 823: Line 1,004:
end
end


-- Econ signals (expensive; only when showEcon=yes)
-- Econ signals (expensive; only if requested)
local econSignals = {}
local econSignals = {}
if showEcon then
if showEcon then
Line 831: Line 1,012:
local out = {}
local out = {}
table.insert(out, "{| class='wikitable sortable'")
table.insert(out, "{| class='wikitable sortable'")
local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Ops Alert !! Cult Order !! Unit !! Tasking"
local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Ops Alert !! Mystery (Cult) !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
header = header .. " !! Temple Bank Notes"
Line 847: Line 1,028:
row.date, row.city, row.region, row.ops, row.order, row.unit, row.task
row.date, row.city, row.region, row.ops, row.order, row.unit, row.task
)
)
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
if showEcon then
line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a")
end
line = line .. " || " .. row.bank
line = line .. " || " .. row.bank
if includeThreat then
if includeThreat then
Line 882: Line 1,065:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 16) SINGLE CITY VIEW
-- 19) SINGLE CITY VIEW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeCity(frame)
function p.makeCity(frame)
Line 915: Line 1,098:
local out = {}
local out = {}
table.insert(out, "{| class='wikitable'")
table.insert(out, "{| class='wikitable'")
local header = "! Date !! City !! Region !! Ops Alert !! Cult Order !! Unit !! Tasking"
local header = "! Date !! City !! Region !! Ops Alert !! Mystery (Cult) !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
header = header .. " !! Temple Bank Notes"
Line 940: Line 1,123:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 17) ORDER-ONLY VIEW
-- 20) ORDER-ONLY VIEW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeOrder(frame)
function p.makeOrder(frame)
Line 980: Line 1,163:
for _, c in ipairs(cityOrder) do
for _, c in ipairs(cityOrder) do
local row = buildCityRow(di, c, threatMap, includeThreat, showEcon, econSignals)
local row = buildCityRow(di, c, threatMap, includeThreat, showEcon, econSignals)
if row.order:find(wantedOrder.link, 1, true) or row.order:find(wantedOrder.name, 1, true) then
-- Compare by cultLink target (stable) rather than raw text
if row.order:find(cultLink(wantedOrder.name), 1, true) or row.order:find(wantedOrder.name, 1, true) then
table.insert(out, "|-" .. row.style)
table.insert(out, "|-" .. row.style)
local line = string.format("| %s || %s || %s || %s || %s || %s",
local line = string.format("| %s || %s || %s || %s || %s || %s",
row.date, row.city, row.region, row.ops, row.unit, row.task
row.date, row.city, row.region, row.ops, row.unit, row.task
)
)
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
if showEcon then
line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a")
end
line = line .. " || " .. row.bank
line = line .. " || " .. row.bank
if includeThreat then
if includeThreat then

Revision as of 03:03, 2 January 2026

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

-- Module:MissionaryDailyOpsBVR
-- Daily missionary tasking ledger (Temple Bank / Reformed Stripping Path).
--
-- Embedded requirements:
--  1) CITY FLAGS: follow Module:GeneralPortOperations flag-naming pattern, with overrides,
--     BUT any missing flag file falls back to the Imperial Trade Union flag.
--  2) CULT FLAGS (logos): taken directly from Module:GeneralPortOperations addLogo() mappings
--     for ritual cults (and Temple of Aprobelle logo). Missing files fall back to ITU flag.
--  3) CULT LINKS: each cult links to its canonical “Mystery” section target (Divine page section)
--     per Reformed_Stripping_Path#Planetary_Devotion (including the special “The …” headings).
--     Temple Aprobelle links to [[Temple_Aprobelle]] (not a Mystery section).
--  4) OPS ALERT: integrates Module:BassaridiaForecast getOpsData() (badge + condition + overlays).
--  5) Avoid “too many expensive function calls”:
--     - No candidate-search loops.
--     - Flag existence checks are cached and done at most once per unique file used.
--     - General Port content scan is ONLY executed if showEcon=yes.
--
-- Usage:
--   {{#invoke:MissionaryDailyOpsBVR|makeTable}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}}
--   {{#invoke:MissionaryDailyOpsBVR|makeTable|showEcon=yes}}
--   {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}}
--   {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Temple Aprobelle}}

local p = {}

local cal = require('Module:BassaridianCalendar')
local DAYS_IN_YEAR = 183

--------------------------------------------------------------------------------
-- 0) BASIC UTILS
--------------------------------------------------------------------------------
local function trim(s)
	s = tostring(s or "")
	return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function normalizeSpaces(s)
	return (s or ""):gsub("[%s\u00A0]+", " ")
end

local function stripWiki(s)
	s = tostring(s or "")
	s = s:gsub("%{%b{}%}", "")
	s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
	s = s:gsub("%[%[([^%]]+)%]%]", "%1")
	s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
	return trim(s)
end

--------------------------------------------------------------------------------
-- 1) CALENDAR PARSING
--------------------------------------------------------------------------------
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

--------------------------------------------------------------------------------
-- 2) DETERMINISTIC RNG
--------------------------------------------------------------------------------
local function hash31(str)
	str = tostring(str or "")
	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 not list or #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

local function pickWeighted(nextInt, weighted)
	local total = 0
	for _, it in ipairs(weighted) do total = total + (it.w or 0) end
	if total <= 0 then return weighted[1] end
	local r = nextInt() % total
	local acc = 0
	for _, it in ipairs(weighted) do
		acc = acc + (it.w or 0)
		if r < acc then return it end
	end
	return weighted[#weighted]
end

--------------------------------------------------------------------------------
-- 3) EXPENSIVE-CALL CONTROL: cached file existence checks (1 per unique file)
--------------------------------------------------------------------------------
local fileExistsCache = {}
local function fileExists(fileName)
	fileName = tostring(fileName or "")
	if fileName == "" then return false end
	if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
	local t = mw.title.new("File:" .. fileName)
	local ok = (t and t.exists) or false
	fileExistsCache[fileName] = ok
	return ok
end

--------------------------------------------------------------------------------
-- 4) STYLE / SEVERITY
--------------------------------------------------------------------------------
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 urgencyScore(u)
	u = tostring(u or ""):upper()
	if u == "CRITICAL" then return 4 end
	if u == "HIGH"     then return 3 end
	if u == "MEDIUM"   then return 2 end
	if u == "LOW"      then return 1 end
	return 0
end

--------------------------------------------------------------------------------
-- 5) CITY NETWORK
--------------------------------------------------------------------------------
local function cityLink(name)
	return string.format("[[List_of_cities_in_Bassaridia_Vaeringheim#%s|%s]]", name, name)
end

local cityOrder = {
	-- Core majors
	{name="Vaeringheim",      region="Core", tier="major"},
	{name="Luminaria",        region="Core", tier="major"},
	{name="Serena",           region="Core", tier="major"},
	{name="Pyralis",          region="Core", tier="major"},
	{name="Symphonara",       region="Core", tier="major"},
	{name="Aurelia",          region="Core", tier="major"},
	{name="Somniumpolis",     region="Core", tier="major"},
	{name="Nexa",             region="Core", tier="major"},
	{name="Lunalis Sancta",   region="Core", tier="major"},
	{name="Sylvapolis",       region="Core", tier="major"},
	-- Core minors
	{name="Saluria",          region="Core", tier="minor"},
	{name="Aetherium",        region="Core", tier="minor"},
	{name="Ferrum Citadel",   region="Core", tier="minor"},
	{name="Acheron",          region="Core", tier="minor"},
	{name="Erythros",         region="Core", tier="minor"},
	{name="Catonis Atrium",   region="Core", tier="minor"},
	{name="Delphica",         region="Core", tier="minor"},
	{name="Koinonía",         region="Core", tier="minor"},
	{name="Aureum",           region="Core", tier="minor"},
	-- Alperkin
	{name="The Alpazkigz",    region="Alperkin", tier="minor"},
	{name="Odiferia",         region="Alperkin", tier="minor"},
	-- New South Jangsong
	{name="Skýrophos",        region="New South Jangsong", tier="major"},
	{name="Bjornopolis",      region="New South Jangsong", tier="major"},
	{name="Aegirheim",        region="New South Jangsong", tier="major"},
	{name="Norsolyra",        region="New South Jangsong", tier="major"},
	{name="Thorsalon",        region="New South Jangsong", tier="major"},
	{name="Pelagia",          region="New South Jangsong", tier="minor"},
	{name="Myrene",           region="New South Jangsong", tier="minor"},
	{name="Thyrea",           region="New South Jangsong", tier="minor"},
	{name="Ephyra",           region="New South Jangsong", tier="minor"},
	{name="Halicarn",         region="New South Jangsong", tier="minor"},
	-- Haifan Bassaridia
	{name="Keybir-Aviv",      region="Haifan Bassaridia", tier="major"},
	{name="Tel-Amin",         region="Haifan Bassaridia", tier="major"},
	{name="Diamandis",        region="Haifan Bassaridia", tier="major"},
	{name="Jogi",             region="Haifan Bassaridia", tier="major"},
	{name="Lewisburg",        region="Haifan Bassaridia", tier="major"},
	{name="Thermosalem",      region="Haifan Bassaridia", tier="minor"},
	{name="Akróstadium",      region="Haifan Bassaridia", tier="minor"},
	{name="Sufriya",          region="Haifan Bassaridia", tier="minor"},
	{name="Lykopolis",        region="Haifan Bassaridia", tier="minor"},
	-- Normark
	{name="Ardclach",         region="Normark", tier="major"},
	{name="Riddersborg",      region="Normark", tier="major"},
	-- Ouriana
	{name="Bashkim",          region="Ouriana", tier="major"},
	{name="Ourid",            region="Ouriana", tier="major"},
	{name="Tonar",            region="Ouriana", tier="minor"},
	-- Caledonia
	{name="Fanghorn",         region="Caledonia", tier="major"},
	{name="Eikbu",            region="Caledonia", tier="major"},
	{name="Galvø",            region="Caledonia", tier="major"},
	{name="Sårensby",         region="Caledonia", tier="minor"},
	{name="Slevik",           region="Caledonia", tier="minor"},
	{name="Sjøsborg",         region="Caledonia", tier="minor"},
	{name="Storesund",        region="Caledonia", tier="minor"},
	{name="Notranskja",       region="Caledonia", tier="minor"},
	{name="Kaledonija",       region="Caledonia", tier="minor"},
}

local citySet = {}
for _, c in ipairs(cityOrder) do citySet[c.name] = true end

local function regionPass(filter, cityRegion)
	filter = tostring(filter or "All")
	if filter == "" or filter == "All" then return true end
	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

--------------------------------------------------------------------------------
-- 5.1) CITY FLAGS (GeneralPortOperations style + fallback-to-ITU if missing)
--------------------------------------------------------------------------------
local ITU_FLAG = "Imperial Trade Union flag.png"

local function normCityForFlag(name)
	-- Exactly the normalization used in Module:GeneralPortOperations
	return (name or "")
		:gsub("[%s%-’]", "")
		:gsub("[Áá]", "A"):gsub("[Éé]", "E")
		:gsub("[Íí]", "I"):gsub("[Óó]", "O")
		:gsub("[Úú]", "U"):gsub("[Ýý]", "Y")
end

local cityFlagOverrides = {
	["Skýrophos"] = "SkyrophosFlag.png", -- override used in GeneralPortOperations
	["Slevik"]    = "SlevikFlag.png.png", -- known oddity you’ve used elsewhere
	["Sårensby"]  = "SårensbyFlag.png",   -- known to exist in your notes
}

local cityFlagMarkupCache = {}
local function getCityFlagMarkup(cityName)
	if cityFlagMarkupCache[cityName] then return cityFlagMarkupCache[cityName] end

	local candidate = cityFlagOverrides[cityName] or (normCityForFlag(cityName) .. "Flag.png")
	local file = fileExists(candidate) and candidate or ITU_FLAG

	local mk = string.format("[[File:%s|20px]]", file)
	cityFlagMarkupCache[cityName] = mk
	return mk
end

--------------------------------------------------------------------------------
-- 6) CULT FLAGS (logos) from Module:GeneralPortOperations addLogo() + fallback
--------------------------------------------------------------------------------
local cultLogoFile = {
	-- Ritual cult logos from Module:GeneralPortOperations
	["Order of Aurora Mystica"]        = "Auroramystica.png",
	["Harmony Sanctum"]               = "Harmonysanctum.png",
	["Ignis Aeternum"]                = "Ignis Aeternum.png",
	["Celestial Harmony Sect"]        = "HarmonySect.png",
	["Guild of Golden Shadows"]       = "GoldenShadows.png",
	["Azure Sentinel Sect"]           = "AzureSentinel.png",
	["Reverie Nebulous"]              = "RevNeb.png",
	["Eon Fellowship"]                = "EonFel.png",
	["Order of the Umbral Oracle"]    = "Umbral.png",
	["Mystery of the Verdant Embrace"]= "Verdant.png",
	["Conclave Illuminara"]           = "Conclave.png",
	["Sanctum Vitalis"]               = "Vitalis.png",
	["Temple Alabaster"]              = "Alabaster.png",
	["Court of the Ironclad"]         = "Ironclad.png",
	["Accord Concordia"]              = "Concordia.png",
	["Mystery of the Stygian Veil"]   = "StygianVe.png",
	["Sylvan Fellowship"]             = "Sylvan.png",
	["Mystery of Red Mirth"]          = "MirthFlag.png",
	["Rex Catonis"]                   = "Catonis.png",
	["Sanctum Delphica"]              = "SanctumDel.png",
	["Ordo Amicitia"]                 = "OrdoFl.png",
	["Temple Illuminata"]             = "AureumFlag.png",

	-- Temple Aprobelle logo is mapped in GeneralPortOperations under serviceCompanies name:
	["Temple Aprobelle"]              = "TempleAprobelleLogo.png",
	["Temple of Aprobelle"]           = "TempleAprobelleLogo.png",
}

local cultFlagMarkupCache = {}
local function getCultFlagMarkup(cultName)
	if cultFlagMarkupCache[cultName] then return cultFlagMarkupCache[cultName] end
	local candidate = cultLogoFile[cultName]
	local file = (candidate and fileExists(candidate)) and candidate or ITU_FLAG
	local mk = string.format("[[File:%s|18px]] ", file)
	cultFlagMarkupCache[cultName] = mk
	return mk
end

--------------------------------------------------------------------------------
-- 7) CULT LINKS (canonical “Mystery” section targets)
--------------------------------------------------------------------------------
local cultTargets = {
	["Order of Aurora Mystica"]        = { page="Eos_(Reformed_Stripping_Path)",      anchor="Order_of_Aurora_Mystica" },
	["Harmony Sanctum"]               = { page="Micras_(Reformed_Stripping_Path)",   anchor="Harmony_Sanctum" },
	["Ignis Aeternum"]                = { page="Pyros_(Reformed_Stripping_Path)",    anchor="Ignis_Aeternum" },
	-- Special headings on those pages include “The …”
	["Celestial Harmony Sect"]        = { page="Indigo_(Reformed_Stripping_Path)",   anchor="The_Celestial_Harmony_Sect" },
	["Guild of Golden Shadows"]       = { page="Chrysos_(Reformed_Stripping_Path)",  anchor="The_Guild_of_Golden_Shadows" },

	["Azure Sentinel Sect"]           = { page="Thalassa_(Reformed_Stripping_Path)", anchor="Azure_Sentinel_Sect" },
	["Reverie Nebulous"]              = { page="Nephele_(Reformed_Stripping_Path)",  anchor="Reverie_Nebulous" },
	["Eon Fellowship"]                = { page="Glinos_(Reformed_Stripping_Path)",   anchor="Eon_Fellowship" },
	["Order of the Umbral Oracle"]    = { page="Noctis_(Reformed_Stripping_Path)",   anchor="Order_of_the_Umbral_Oracle" },
	["Mystery of the Verdant Embrace"]= { page="Opsithe_(Reformed_Stripping_Path)",  anchor="Mystery_of_the_Verdant_Embrace" },
	["Conclave Illuminara"]           = { page="Tarsica_(Reformed_Stripping_Path)",  anchor="Conclave_Illuminara" },
	["Sanctum Vitalis"]               = { page="Plateau_(Reformed_Stripping_Path)",  anchor="Sanctum_Vitalis" },
	["Temple Alabaster"]              = { page="Ivory_(Reformed_Stripping_Path)",    anchor="Temple_Alabaster" },
	["Court of the Ironclad"]         = { page="Fenrir_(Reformed_Stripping_Path)",   anchor="Court_of_the_Ironclad" },
	["Accord Concordia"]              = { page="Lukedu_(Reformed_Stripping_Path)",   anchor="Accord_Concordia" },
	["Mystery of the Stygian Veil"]   = { page="Styx_(Reformed_Stripping_Path)",     anchor="Mystery_of_the_Stygian_Veil" },
	["Sylvan Fellowship"]             = { page="Faun_(Reformed_Stripping_Path)",     anchor="Sylvan_Fellowship" },
	["Mystery of Red Mirth"]          = { page="Silenus_(Reformed_Stripping_Path)",  anchor="Mystery_of_Red_Mirth" },
	["Rex Catonis"]                   = { page="Cato_(Reformed_Stripping_Path)",     anchor="Rex_Catonis" },
	["Sanctum Delphica"]              = { page="Sisera_(Reformed_Stripping_Path)",   anchor="Sanctum_Delphica" },
	["Ordo Amicitia"]                 = { page="Erasmus_(Reformed_Stripping_Path)",  anchor="Ordo_Amicitia" },
	["Temple Illuminata"]             = { page="Nikolaj_(Reformed_Stripping_Path)",  anchor="Temple_Illuminata" },
}

local function cultLink(cultName)
	if cultName == "Temple Aprobelle" or cultName == "Temple of Aprobelle" then
		return "[[Temple_Aprobelle|Temple Aprobelle]]"
	end
	local t = cultTargets[cultName]
	if not t then
		return string.format("[[%s]]", cultName)
	end
	return string.format("[[%s#%s|%s]]", t.page, t.anchor, cultName)
end

--------------------------------------------------------------------------------
-- 8) ORG UNITS (Missionary echelons)
--------------------------------------------------------------------------------
local org = {
	Lampade     = { code="L", size=8  },
	Hetairoi    = { code="H", size=12 },
	Dodekade    = { code="D", size=18 },
	Kleisthenes = { code="K", size=25 },
	Stoa        = { code="S", size=40 },
	Thiasos     = { code="T", size=60 },
}

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

--------------------------------------------------------------------------------
-- 9) OPS ALERT (Module:BassaridiaForecast)
--------------------------------------------------------------------------------
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
if not wxOk then wx = nil end

local function wxColor(level)
	level = tonumber(level) or 0
	if level == 3 then return "#f8d7da" end
	if level == 2 then return "#ffe5cc" end
	if level == 1 then return "#fff3cd" end
	return "#d4edda"
end

local function opsCell(cityName, year, dayOfYear)
	if not wx or type(wx.getOpsData) ~= "function" then return "n/a", nil end
	local op = wx.getOpsData(cityName, year, dayOfYear)
	if not op or not op.alert then return "n/a", nil end

	local lvl   = tonumber(op.alert.level) or 0
	local label = tostring(op.alert.label or "Green")
	local cond  = tostring(op.condition or "")
	local bg    = wxColor(lvl)

	local cell = string.format(
		"<span style='background:%s;padding:2px 6px;border:1px solid #ccc;border-radius:10px;white-space:nowrap;'>%s</span>%s",
		bg, label, (cond ~= "" and ("<br/><span style='font-size:90%;'>" .. cond .. "</span>") or "")
	)
	return cell, op
end

local function bumpSevWeather(sev, op)
	if not op or not op.alert then return sev end
	local lvl = tonumber(op.alert.level) or 0
	local imp = op.impacts or {}

	if lvl == 2 and sev == "green" then sev = "yellow" end
	if lvl >= 3 then sev = "red" end

	if (imp.ground == "Hazard" or imp.sea == "Closed" or imp.air == "No-Go") and sev == "green" then
		sev = "yellow"
	end
	return sev
end

local function weatherTaskOverlay(op)
	if not op or not op.alert then return "" end
	local lvl = tonumber(op.alert.level) or 0
	if lvl < 2 then return "" end -- Orange/Red only

	local haz = tostring(op.alert.hazard or "weather disruption")
	local imp = op.impacts or {}
	local cond = tostring(op.condition or "")

	local base = " Weather-driven tasking: " .. haz .. " (" ..
		string.format("Gnd %s / Sea %s / Air %s", imp.ground or "OK", imp.sea or "OK", imp.air or "OK") .. ")."

	if cond == "Storm" or cond == "Rain" then
		return base .. " Clinic/relief queues stabilized; rumor bulletins posted; shrine-district lanes marked."
	elseif cond == "Blizzard" or cond == "Snow" then
		return base .. " Warming-shelter checks; fuel/medicine custody verified; travel guidance posted."
	elseif cond == "Fog" then
		return base .. " Navigation warnings and crowd guidance in port/canal corridors; panic narratives suppressed."
	elseif cond == "Dust" then
		return base .. " Mask discipline and vendor hygiene enforcement prioritized; indoor refuge advisories posted."
	else
		return base .. " Welfare checks and public messaging prioritized."
	end
end

--------------------------------------------------------------------------------
-- 10) OPTIONAL: THREAT WATCH (Module:ReligiousEvents)
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
	local map = {}
	wikitext = tostring(wikitext or "")
	for line in wikitext: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 = stripWiki(parts[2])
				local ev = stripWiki(parts[3])
				local urg = stripWiki(parts[4])
				if city ~= "" and ev ~= "" then
					map[city] = {event=ev, urg=urg}
				end
			end
		end
	end
	return map
end

--------------------------------------------------------------------------------
-- 11) OPTIONAL: ECON SIGNALS (EXPENSIVE; ONLY if showEcon=yes)
--------------------------------------------------------------------------------
local function getEconSignals()
	local title = mw.title.new("General Port of Lake Morovia")
	if not title then return {} end
	local ok, content = pcall(function() return title:getContent() end)
	if not ok or not content or content == "" then return {} end

	local signals = {}
	local text = normalizeSpaces(content)

	for _, c in ipairs(cityOrder) do
		local n = c.name
		local hits = 0
		local pattern = n:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])","%%%1")
		for _ in text:gmatch(pattern) do
			hits = hits + 1
			if hits >= 6 then break end
		end
		if hits >= 3 then
			signals[n] = {summary=string.format("High Port linkage (mentions: %d)", hits)}
		end
	end
	return signals
end

--------------------------------------------------------------------------------
-- 12) TASK HELPERS
--------------------------------------------------------------------------------
local function T(text, sev) return {text=text, sev=sev} end

-- 10 tasks per cult (kept compact so the module remains maintainable)
local tasksByCult = {
	["Order of Aurora Mystica"] = {
		T("Dawn reconciliation rite for competing guilds; pledges sealed with Temple Bank clerks.","green"),
		T("Sunrise bulletin: correct false omen claims; stabilize work-shift handoffs.","green"),
		T("First-light counseling circle; referrals issued through shrine stewards.","green"),
		T("Crowd choreography support at dawn festival; stampede risk reduced.","yellow"),
		T("Targeted rumor-clearing procession after unsigned pamphlets; statements archived.","yellow"),
		T("Joint tunnel patrol with civic stewards; lighting faults logged; intimidation reports filed.","yellow"),
		T("Emergency truce summit after street violence; terms read and posted.","red"),
		T("Crisis dawn vigil after night incident; grief support + no-retaliation pledges.","red"),
		T("Escort for a vulnerable witness; route screened for ambush pressure.","red"),
		T("Post-incident narrative repair: speaker coaching and clarity bulletin issued.","yellow"),
	},
	["Harmony Sanctum"] = {
		T("Neighborhood harmony circle: mediated contract dispute between vendors and labor overseers.","green"),
		T("Public etiquette instruction at shrine square; queue discipline enforced.","green"),
		T("Household conflict visits after complaint surge; written behavioral plan issued.","green"),
		T("Festival crowd-lane planning with stewards to prevent crush risks.","yellow"),
		T("Rumor triage: source nodes identified; corrective bulletins posted.","yellow"),
		T("Transit choke-point calming patrol; separation corridor opened for commuters.","yellow"),
		T("Emergency stabilization during riot flare; dispersal lanes + care stations deployed.","red"),
		T("Night calm-litany enforcement after panic episode; supervised dispersal coordinated.","red"),
		T("Public apology ceremony after inter-group violence; restitution ledger filed.","yellow"),
		T("Oversight coaching for district clerks after audit findings; corrective training delivered.","yellow"),
	},
	["Ignis Aeternum"] = {
		T("Controlled forge-rite for municipal smiths; safety discipline and firewatch roster set.","green"),
		T("Heat-hazard guidance posted for labor crews; hydration discipline enforced.","green"),
		T("Festival brazier inspection: fuel handling and crowd distance compliance verified.","yellow"),
		T("Brushfire-risk patrol: illegal burns suppressed; emergency water points marked.","yellow"),
		T("Rumor containment after ‘cursed flame’ claims; sanctioned narrative posted.","yellow"),
		T("Night watch at fuel depots after arson threat; perimeter stewards assigned.","red"),
		T("Emergency response to flare incident: evacuation lanes opened; burn triage support.","red"),
		T("Inspection of kiln/oven quarter after sabotage report; witnesses interviewed.","yellow"),
		T("Firebreak coordination with Verdant Embrace; debris removed and lanes cleared.","yellow"),
		T("Crisis crowd control during pyre procession; stampede pressure relieved.","red"),
	},
	["Celestial Harmony Sect"] = {
		T("Cosmic-order lecture at shrine school; reduce anxiety about star-omens.","green"),
		T("Guided meditation session for canal workers; stress reduction paired with safety brief.","green"),
		T("Omen bulletin update: correct false eclipse claim before hoarding begins.","yellow"),
		T("Quiet-zone enforcement near hospitals; regulated chanting to prevent agitation.","yellow"),
		T("Investigation of counterfeit star-charts; vendor interviews and seizures logged.","yellow"),
		T("Night guidance during fog warnings; speakers briefed and routes marked.","yellow"),
		T("Crisis counseling during mass scare; supervised calm stations established.","red"),
		T("Emergency messaging team embedded with stewards to prevent retaliatory panic.","red"),
		T("Post-event debrief: approved narratives updated; scripts distributed to shrines.","yellow"),
		T("Festival harmony protocol review: rites aligned with civic order requirements.","green"),
	},
	["Guild of Golden Shadows"] = {
		T("Quiet intake: compile rumor sources and identify forged vouchers without escalation.","green"),
		T("Market observation rotation; coercion and counterfeit seals flagged to Temple Bank.","green"),
		T("Discreet witness protection arrangement for vendor threatened by smugglers.","yellow"),
		T("Forgery trace: identify print stall distributing schismatic pamphlets; evidence packet built.","yellow"),
		T("Undercover purchase of fake shrine seals; ledgers seized and handed to clerks.","yellow"),
		T("Intercept contraband manifest; cross-check corridor registry for evasions.","yellow"),
		T("Night extraction of compromised informant; safe route and temporary housing arranged.","red"),
		T("Emergency suppression of panic narrative: false broadsheets removed, official bulletin posted.","red"),
		T("Coordination with Temple Aprobelle on narrative discipline during crowd surge.","yellow"),
		T("Attribution report delivered to War League liaison; prevention plan drafted.","yellow"),
	},
	["Azure Sentinel Sect"] = {
		T("Waterfront perimeter walk and safety rope checks along canal districts.","green"),
		T("Storm-drain inspection with civil works; blockages cleared and logged.","green"),
		T("Port-house briefing on embarkation discipline; theft prevention guidance posted.","green"),
		T("Escort for medical supplies across canal bridges; accountability signatures obtained.","yellow"),
		T("Harbor rumor suppression: correct false quarantine claims to prevent stampede.","yellow"),
		T("Search-and-rescue standby after storm warnings; muster roster and equipment checked.","yellow"),
		T("Emergency flood response: evacuation guidance at waterfront shrines; calm channels maintained.","red"),
		T("High-risk escort of officials through crowd at port gate; separation corridor enforced.","red"),
		T("Debris clearance coordination after gale; hazard markings placed on docks.","yellow"),
		T("Crisis harbor closure messaging: distribute notices; prevent vigilante enforcement.","red"),
	},
	["Reverie Nebulous"] = {
		T("Dream interpretation circle with strict moderation; anxiety de-escalation emphasized.","green"),
		T("Quiet art therapy for children after scare; parent coaching provided.","green"),
		T("Public sleep-hygiene teaching for shift workers; rest scheduling guidance posted.","green"),
		T("Rumor mapping: identify dream narratives feeding panic; corrective story deployed.","yellow"),
		T("Witness interviews after trance-gathering complaint; compliance conditions issued.","yellow"),
		T("Night crowd calming near transit node; chants regulated to prevent stampede behavior.","yellow"),
		T("Containment support for mass panic dream episode; supervised rest zones established.","red"),
		T("Emergency counseling for families after death; grief rites paired with aid referrals.","red"),
		T("High-risk dream-breach report assessed with Aprobelle liaisons; misinformation suppressed.","red"),
		T("Post-event debrief: narrative edited to reduce retaliatory impulses; speaker training conducted.","yellow"),
	},
	["Eon Fellowship"] = {
		T("Timekeeping and schedule audit at market gates; shift rotations clarified to reduce conflict.","green"),
		T("Public calendar correction bulletin; prevent rumor-driven delays and missed rites.","green"),
		T("Queue timing discipline drill at clinic or shrine; entry metering tested and refined.","yellow"),
		T("Witness coordination for a hearing; arrival windows staged to prevent intimidation.","yellow"),
		T("Transit-flow smoothing at rail/canal chokepoint; signage updated for timing cadence.","yellow"),
		T("Record review: repeated late-stipend disputes analyzed; corrective SOP issued.","yellow"),
		T("Emergency stabilization after cascading delays; volunteer surge roster synchronized.","red"),
		T("Night patrol timing plan during fog; safe intervals posted and enforced.","yellow"),
		T("Audit of clerk office after missed filings; corrective training delivered.","yellow"),
		T("Crisis schedule triage during unrest; safe hours for movement posted publicly.","red"),
	},
	["Order of the Umbral Oracle"] = {
		T("Quiet-night rite with strict entry control; visitor conduct posted to reduce disturbances.","green"),
		T("Risk bulletin for shadow-sign nights; discourage panic gatherings at transit nodes.","yellow"),
		T("Investigation of anonymous pamphlets claiming ‘oracle sanction’; counterfeit marks documented.","yellow"),
		T("Counseling for anxious households after nightmare spike; referral paths established.","green"),
		T("Night watch at shrine steps to deter harassment of pilgrims; steward coordination.","yellow"),
		T("Containment support for ‘demonic sign’ rumor; official messaging delivered in controlled cadence.","red"),
		T("Witness protection for informant threatened by cult rival; safehouse route planned.","red"),
		T("Crisis nocturnal dispersal plan after crowd surge; safe corridors opened.","red"),
		T("Post-incident narrative discipline: speaker scripts updated; rumor sources mapped.","yellow"),
		T("Audit of unsanctioned night gatherings; compliance restrictions issued.","yellow"),
	},
	["Mystery of the Verdant Embrace"] = {
		T("Irrigation rite + canal cleanup; silt removed and flow restored.","green"),
		T("Seed blessing for gardeners; pest-prevention guidance distributed.","green"),
		T("Wetland path maintenance; travel warnings posted to prevent drownings.","green"),
		T("Aqueduct audit: leakage reported; repair crews coordinated.","yellow"),
		T("Field mediation over irrigation rights; shared schedule drafted and signed.","yellow"),
		T("Brushfire risk patrol; illegal burns prohibited and notices issued.","yellow"),
		T("Vector mitigation at swamp edges; standing water treated and guidance posted.","yellow"),
		T("Post-storm debris clearance with volunteers; hazard markings placed.","yellow"),
		T("Emergency flood relief: sandbag lines, food staging, shelter support.","red"),
		T("Crisis canal breach response: closure, reroute, crowd calming in shrine districts.","red"),
	},
	["Conclave Illuminara"] = {
		T("Night-sky lecture; correct omen anxiety and explain eclipse calendars.","green"),
		T("Quiet meditation ward at shrine; breathing protocols taught.","green"),
		T("Lantern-route inspection; reflective markers placed to reduce accidents.","green"),
		T("Dream-space audit: offerings catalogued; safety signage posted.","green"),
		T("Counterfeit moon-token investigation; vendor interviews and seizure log.","yellow"),
		T("Controlled night gathering after rumor; calm-litany enforced; dispersal coordinated.","yellow"),
		T("Night crowd calming near transit node; chants regulated to prevent stampede behavior.","yellow"),
		T("Emergency anti-panic messaging after ‘demonic sign’ claim; scripts distributed.","red"),
		T("Escort for sensitive nocturnal rite during unrest; perimeter observers posted.","red"),
		T("Blackout safety drill during storm season; guides and warning ropes staged.","yellow"),
	},
	["Sanctum Vitalis"] = {
		T("Clinic resupply: inventory reconciled with Temple Bank vouchers.","green"),
		T("Water-quality checks at canal taps; purification guidance posted.","green"),
		T("School hygiene lecture; handwashing and food safety emphasized.","green"),
		T("Routine fever screening at market gate; clinic referrals issued.","green"),
		T("Temporary clinic tent near shrine square; triage lines marked and staffed.","yellow"),
		T("Contamination rumor assessment: test kits deployed to prevent panic.","yellow"),
		T("Heat illness prevention training; hydration discipline coached.","yellow"),
		T("Mass casualty support after accident; triage and grief coordination.","red"),
		T("Quarantine support in dormitory ward; custody and compliance checks.","red"),
		T("Night clinic staffing for unrest injuries; neutral triage posture announced.","red"),
	},
	["Temple Alabaster"] = {
		T("Sanctuary cleanliness audit; white-stone precincts washed and offerings catalogued.","green"),
		T("Pilgrim conduct briefing; quiet-zone and nonviolence rules posted.","green"),
		T("Artifact handling training for novice stewards; custody marks standardized.","yellow"),
		T("Dispute mediation over shrine access rights; schedule drafted and signed.","yellow"),
		T("Rumor suppression after ‘tainted stone’ claim; evidence posted and speaker briefed.","yellow"),
		T("Escort of a sacred item to storage; chain-of-custody verified.","yellow"),
		T("Emergency crowd control during pilgrimage surge; lanes marked and calm stations opened.","red"),
		T("Night watch on shrine steps after vandal threat; stewards rotated.","yellow"),
		T("Crisis shelter activation in stone precinct during storm; intake and calm discipline enforced.","red"),
		T("Post-incident restitution hearing prep; witness statements sealed with clerks.","yellow"),
	},
	["Court of the Ironclad"] = {
		T("Discipline training for volunteers; nonviolent restraint emphasized.","green"),
		T("Public order presence at shrine steps; crowd etiquette enforced.","green"),
		T("Oath-keeping workshop; pledge cards sealed for civic stewards.","green"),
		T("Route hardening walk: broken lamps and unsafe crossings flagged.","yellow"),
		T("Contraband confiscation support; evidence logged for Temple Bank clerks.","yellow"),
		T("Night watch on canal bridges after sabotage attempt; checkpoint discipline enforced.","yellow"),
		T("High-risk escort for witness leaving hearing; safehouse transfer arranged.","red"),
		T("Emergency deterrence patrol after riot flare; separation corridors opened.","red"),
		T("Crisis reinforcement at clinic gate during panic surge; queue discipline established.","red"),
		T("Post-incident intimidation review; complaints investigated and discipline applied.","yellow"),
	},
	["Accord Concordia"] = {
		T("Council mediation session between neighborhood elders; written concord signed.","green"),
		T("Public forum moderation; speaking order enforced to reduce fights.","green"),
		T("Dispute settlement over vendor stalls; restitution terms logged.","green"),
		T("Civic etiquette training for steward teams; de-escalation scripts practiced.","yellow"),
		T("Inter-cult boundary meeting; shared space rules posted at shrine gates.","yellow"),
		T("Rumor containment after inflammatory broadsides; corrective bulletin cycle launched.","yellow"),
		T("Emergency truce enforcement during street clash; separation lines held.","red"),
		T("Crisis reconciliation hearing after riot; pledge collection and witness protection.","red"),
		T("Audit of clerk office after complaint surge; corrective plan countersigned.","yellow"),
		T("Post-event community repair ceremony; restitution ledger filed with Temple Bank.","yellow"),
	},
	["Mystery of the Stygian Veil"] = {
		T("Funerary custody check: offerings catalogued; tomb precinct rules posted.","green"),
		T("Grief counseling circle; practical aid referrals arranged.","green"),
		T("Mortuary hygiene audit; disease-prevention protocols verified.","yellow"),
		T("Rumor suppression after ‘restless dead’ claims; evidence bulletin issued.","yellow"),
		T("Escort for body transfer; route secured and witnesses logged.","yellow"),
		T("Night vigil supervision to prevent stampede or retaliation violence.","yellow"),
		T("Crisis response after mass death incident; triage of grief + order restoration.","red"),
		T("Emergency calm stations at cemetery gate during panic; dispersal coordinated.","red"),
		T("Witness interviews after desecration; sworn statements sealed.","yellow"),
		T("Post-incident reconciliation rite; retaliation prevention pledges collected.","yellow"),
	},
	["Sylvan Fellowship"] = {
		T("Forest-path maintenance; travel warnings posted for pilgrims.","green"),
		T("Wildlife safety briefing; reduce accidents on shrine trails.","green"),
		T("Herb and timber vendor conduct audit; sustainable collection rules enforced.","yellow"),
		T("Brushfire-watch patrol; illegal burns suppressed and water points marked.","yellow"),
		T("Mediation between trappers and villagers; shared hunting schedule signed.","yellow"),
		T("Stormfall debris cleanup with volunteers; hazard markings placed.","yellow"),
		T("Emergency shelter staging in forest precinct during storm; intake controlled.","red"),
		T("Crisis search for missing pilgrim after fog; guides deployed with calm messaging.","red"),
		T("Rumor containment after ‘forest curse’ claim; corrective bulletin cycle run.","yellow"),
		T("Post-event ecological repair plan filed; volunteer crews scheduled.","green"),
	},
	["Mystery of Red Mirth"] = {
		T("Festival conduct briefing; prevent brawls and dangerous dares.","green"),
		T("Public merriment patrol: de-escalate fights before they spread.","yellow"),
		T("Vendor compliance audit for intoxicants; counterfeit batches seized.","yellow"),
		T("Rumor suppression after scandalous broadsides; approved narrative posted.","yellow"),
		T("Crowd choreography for parade route; crush-risk minimized.","yellow"),
		T("Grief-to-mirth counseling after loss; prevent destructive spirals.","green"),
		T("Emergency riot containment after festival violence; separation corridors opened.","red"),
		T("Crisis clinic support for injuries after revel incident; triage and calm stations.","red"),
		T("Witness statements collected after assault; handoff to clerks.","yellow"),
		T("Post-incident restitution and apology rite; pledges collected.","yellow"),
	},
	["Rex Catonis"] = {
		T("Civic order proclamation at court square; rules of conduct reissued.","green"),
		T("Petty dispute docket triage; mediation before formal hearings.","green"),
		T("Audit of municipal fines ledger; correct corrupt entries.","yellow"),
		T("Vendor permit compliance sweep; counterfeit permits seized.","yellow"),
		T("Rumor suppression after ‘illegal decrees’ claim; clarifying bulletin posted.","yellow"),
		T("Escort of court clerks during sensitive hearing; intimidation deterrence.","yellow"),
		T("Emergency order restoration after riot; curfew messaging and dispersal corridors.","red"),
		T("Crisis protection of court archives after arson threat; perimeter posted.","red"),
		T("Witness interviews after vandalism of civic symbol; statements sealed.","yellow"),
		T("Post-event governance training for stewards; corrective SOP issued.","yellow"),
	},
	["Sanctum Delphica"] = {
		T("Archive custody inspection: seals verified; storage conditions checked.","green"),
		T("Scribe training on custody chain; convoy paperwork rehearsed.","green"),
		T("Pilgrim etiquette workshop at library shrine; quiet-zone enforced.","green"),
		T("Index confiscated pamphlets for signature motifs; correlation packet forwarded.","yellow"),
		T("Witness interviews after shrine vandalism; sworn statements taken.","yellow"),
		T("Doctrinal quarantine assistance: entry controls and rumor suppression.","yellow"),
		T("Court packet assembly for desecration suspects; evidence collated.","yellow"),
		T("Night watch rotation at archive stacks following arson threat.","red"),
		T("Escort sensitive artifact transfer; route denial markers deployed.","red"),
		T("Emergency custody lockdown after breach attempt; relocation plan activated.","red"),
	},
	["Ordo Amicitia"] = {
		T("Public kindness drive: food distribution at shrine steps; ledger reconciled.","green"),
		T("Mediation between families after dispute; amity pledges collected.","green"),
		T("Volunteer training: calm communication and nonviolent intervention.","green"),
		T("Rumor containment after defamatory pamphlets; corrective bulletin cycle launched.","yellow"),
		T("Clinic queue kindness patrol; conflict prevention and assistance to elderly.","yellow"),
		T("Audit of relief supplies to prevent theft; custody seals verified.","yellow"),
		T("Emergency care stations during unrest; neutral aid posture announced.","red"),
		T("Crisis grief counseling after fatal incident; aid referrals coordinated.","red"),
		T("Witness protection for targeted volunteer; safe route arranged.","yellow"),
		T("Post-event reconciliation rite; retaliation prevention pledges collected.","yellow"),
	},
	["Temple Illuminata"] = {
		T("Illumination vigil: lantern discipline and public calm guidance posted.","green"),
		T("Night watch of sacred precinct; entry control and conduct rules enforced.","yellow"),
		T("Omen bulletin: correct false ‘dark star’ claim; discourage panic.","yellow"),
		T("Archive of testimonies and signs; witness statements catalogued.","yellow"),
		T("Escort for a sensitive nocturnal rite during unrest; perimeter observers posted.","red"),
		T("Emergency messaging after ‘demonic symbol’ report; scripts distributed to speakers.","red"),
		T("Crisis crowd dispersal after night surge; calm stations and guidance routes opened.","red"),
		T("Audit of unsanctioned night gatherings; compliance restrictions issued.","yellow"),
		T("Counseling for anxious households; referral paths established.","green"),
		T("Post-incident narrative discipline; rumor sources mapped and neutralized.","yellow"),
	},
	["Temple Aprobelle"] = {
		T("Festival hygiene audit: vendor protocols verified; seals issued to compliant stalls.","green"),
		T("Rumor-control bulletin cycle: shrine-leader briefing + posted clarifications.","green"),
		T("Grief-cadre home visits after accident; de-escalation coaching delivered.","green"),
		T("School immunization coordination: clinic schedule posted; consent ledgers reconciled.","green"),
		T("Clinic-queue stabilization drill: entry metering + conflict prevention training.","yellow"),
		T("Behavior audit of district office after complaint surge; corrective training issued.","yellow"),
		T("Anomaly-response dispatch after contamination claims; care paired with narrative discipline.","yellow"),
		T("Pattern-analysis intake: schism texts logged and forwarded for signature correlation.","yellow"),
		T("Silencing-cadre activated after panic flare; controlled messaging + care stations deployed.","red"),
		T("Emergency stabilization in evacuation/fever center: calming rites + rumor suppression.","red"),
	},
}

--------------------------------------------------------------------------------
-- 13) CITY PROFILES (bias order selection)
--------------------------------------------------------------------------------
local cityProfiles = {
	["Vaeringheim"]    = {weights={AUM=3,HSA=2,ASS=2,GGS=2,SVI=2,SDE=2,TAP=3}},
	["Somniumpolis"]   = {weights={RNB=3,SVI=2,GGS=2,TAP=3,ASS=1}},
	["Luminaria"]      = {weights={AUM=3,CON=2,HSA=2,TAP=2,SVI=1}},
	["Lunalis Sancta"] = {weights={SDE=3,CON=2,TAP=2,HSA=1}},
	["Delphica"]       = {weights={SDE=3,TAP=2,CON=1,SVI=1}},
	["Thermosalem"]    = {weights={SVI=2,TAP=2,ASS=2,HSA=1}},
	["Pyralis"]        = {weights={IGA=2,SVI=2,TAP=2,ASS=2,AUM=1}},
	["Keybir-Aviv"]    = {weights={ASS=3,TAP=2,GGS=2,HSA=1}},
	["Diamandis"]      = {weights={TAP=2,SVI=2,AUM=1,RNB=1,MIR=1}},
	["Tonar"]          = {weights={SDE=2,SVI=2,VDE=2,TAP=1,CICL=1}},
	["Bashkim"]        = {weights={VDE=3,SVI=2,TAP=1}},
	["Ourid"]          = {weights={VDE=3,SVI=2,TAP=1}},
	["Ardclach"]       = {weights={ASS=2,SVI=2,TAP=1,CON=1}},
}

local function getProfile(cityName)
	return cityProfiles[cityName] or {weights={AUM=1,HSA=1,IGA=1,CHS=1,GGS=1,ASS=1,RNB=1,EOF=1,UMB=1,VDE=1,CON=1,SVI=1,ALB=1,CICL=1,ACC=1,STY=1,SYL=1,MIR=1,CAT=1,SDE=1,AMI=1,ILL=1,TAP=1}}
end

--------------------------------------------------------------------------------
-- 14) ORDERS (missionary-capable cult list)
-- NOTE: These names must match GeneralPortOperations ritualCults list + Temple Aprobelle.
--------------------------------------------------------------------------------
local orders = {
	{key="AUM", name="Order of Aurora Mystica"},
	{key="HSA", name="Harmony Sanctum"},
	{key="IGA", name="Ignis Aeternum"},
	{key="CHS", name="Celestial Harmony Sect"},
	{key="GGS", name="Guild of Golden Shadows"},
	{key="ASS", name="Azure Sentinel Sect"},
	{key="RNB", name="Reverie Nebulous"},
	{key="EOF", name="Eon Fellowship"},
	{key="UMB", name="Order of the Umbral Oracle"},
	{key="VDE", name="Mystery of the Verdant Embrace"},
	{key="CON", name="Conclave Illuminara"},
	{key="SVI", name="Sanctum Vitalis"},
	{key="ALB", name="Temple Alabaster"},
	{key="CICL", name="Court of the Ironclad"},
	{key="ACC", name="Accord Concordia"},
	{key="STY", name="Mystery of the Stygian Veil"},
	{key="SYL", name="Sylvan Fellowship"},
	{key="MIR", name="Mystery of Red Mirth"},
	{key="CAT", name="Rex Catonis"},
	{key="SDE", name="Sanctum Delphica"},
	{key="AMI", name="Ordo Amicitia"},
	{key="ILL", name="Temple Illuminata"},
	{key="TAP", name="Temple Aprobelle"},
}

local orderByName = {}
for _, o in ipairs(orders) do orderByName[o.name] = o end

--------------------------------------------------------------------------------
-- 15) SEVERITY + ORDER/TASK CHOICE
--------------------------------------------------------------------------------
local function inferSeverity(cityTier, taskSev, threatUrg)
	local sev = tostring(taskSev or "green")
	if cityTier == "major" and sev == "green" then sev = "yellow" end
	if urgencyScore(threatUrg) >= 3 then
		sev = "red"
	elseif urgencyScore(threatUrg) == 2 and sev == "green" then
		sev = "yellow"
	end
	return sev
end

local function pickOrder(nextInt, cityName, threat)
	local profile = getProfile(cityName)
	local weighted = {}
	for key, w in pairs(profile.weights or {}) do
		weighted[#weighted+1] = {key=key, w=tonumber(w) or 0}
	end
	-- threat bump: prefer Aprobelle + safety/health + shadows
	if threat and urgencyScore(threat.urg) >= 2 then
		for _, it in ipairs(weighted) do
			if it.key == "TAP" or it.key == "SVI" or it.key == "ASS" or it.key == "GGS" or it.key == "SDE" then
				it.w = it.w + 2
			end
		end
	end
	local pickedKey = pickWeighted(nextInt, weighted).key
	for _, o in ipairs(orders) do
		if o.key == pickedKey then return o end
	end
	return orders[1]
end

local function pickTask(nextInt, cultName, sev)
	local pool = tasksByCult[cultName] or tasksByCult["Harmony Sanctum"] or {}
	-- bias severity matching
	local weighted = {}
	for _, t in ipairs(pool) do
		local w = 1
		if t.sev == sev then w = 4
		elseif sev == "red" and t.sev == "yellow" then w = 2
		elseif sev == "yellow" and t.sev == "green" then w = 2 end
		table.insert(weighted, {t=t, w=w})
	end
	if #weighted == 0 then
		return {text="Routine rites and civic support (no task pool configured).", sev="green"}
	end
	return pickWeighted(nextInt, weighted).t
end

--------------------------------------------------------------------------------
-- 16) TEMPLE BANK NOTES
--------------------------------------------------------------------------------
local bankNotes = {
	"Stipend disbursed after checklist verification; receipt countersigned by local steward.",
	"Voucher reconciliation complete; supplies logged and stored under dual-key protocol.",
	"Observer note: crowd discipline improved after posted guidance and steward coaching.",
	"Repair authorization issued; labor scheduled at off-peak hours.",
	"Custody ledger updated; medicines/artifacts sealed and moved to secure storage.",
	"Training memo filed; corrective instruction issued to volunteers for queue discipline.",
}

--------------------------------------------------------------------------------
-- 17) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
local function buildCityRow(di, c, threatMap, includeThreat, showEcon, econSignals)
	local cityName = c.name
	local seed = hash31(string.format("mops:%d:%03d:%s", di.year, di.dayOfYear, cityName))
	local nextInt = lcg(seed)

	local th = includeThreat and threatMap and threatMap[cityName] or nil
	local o = pickOrder(nextInt, cityName, th)

	local baseTask = pickTask(nextInt, o.name, "yellow")
	local threatUrg = th and th.urg or ""
	local sev = inferSeverity(c.tier, baseTask.sev, threatUrg)

	-- WX overlay
	local opsBadge, wxData = opsCell(cityName, di.year, di.dayOfYear)
	sev = bumpSevWeather(sev, wxData)
	local taskText = baseTask.text .. weatherTaskOverlay(wxData)

	-- unit
	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)

	-- bank note
	local bank = pickFromList(nextInt, bankNotes, nil)

	-- econ
	local econ = ""
	if showEcon and econSignals and econSignals[cityName] then
		econ = econSignals[cityName].summary or ""
	end

	return {
		style = sevStyle(sev),
		date = di.text,
		city = string.format("%s %s", getCityFlagMarkup(cityName), cityLink(cityName)),
		region = c.region,
		ops = opsBadge,
		order = getCultFlagMarkup(o.name) .. cultLink(o.name),
		unit = string.format("%s (%d) [%s]", unitName, unitMeta.size, unitCode),
		task = taskText,
		bank = bank,
		threat = (includeThreat and th and th.event) and th.event or "",
		threatUrg = (includeThreat and th and th.urg) and th.urg or "",
		sev = sev,
		econ = econ,
	}
end

--------------------------------------------------------------------------------
-- 18) 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 showEcon = tostring(args.showEcon or "no"):lower() == "yes"
	local limit = tonumber(args.limit or "")

	local di = getDateInfo()

	-- Threat map (only if requested)
	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

	-- Econ signals (expensive; only if requested)
	local econSignals = {}
	if showEcon then
		econSignals = getEconSignals()
	end

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Ops Alert !! Mystery (Cult) !! Unit !! Tasking"
	if showEcon then header = header .. " !! Economic signal" end
	header = header .. " !! Temple Bank Notes"
	if includeThreat then header = header .. " !! Threat Watch" end
	header = header .. " !! Severity"
	table.insert(out, header)

	local count = 0
	for _, c in ipairs(cityOrder) do
		if regionPass(regionFilter, c.region) then
			local row = buildCityRow(di, c, threatMap, includeThreat, showEcon, econSignals)
			table.insert(out, "|-" .. row.style)

			local line = string.format("| %s || %s || %s || %s || %s || %s || %s",
				row.date, row.city, row.region, row.ops, row.order, row.unit, row.task
			)
			if showEcon then
				line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a")
			end
			line = line .. " || " .. row.bank
			if includeThreat then
				local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
				line = line .. " || " .. threatCell
			end
			line = line .. " || " .. row.sev
			table.insert(out, line)

			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, "<br/>Ops Alert badge reflects forecast-driven operational risk and aligns with War League / Hatch posture.")
		table.insert(out, "<br/>For overseas deployments please see [[Bassaridian_involvement_in_Corum#Corumian_Missionary_Deployment_Reports|Corumian Missionary Deployment Reports]].")
		table.insert(out, "</div>")
	end

	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

--------------------------------------------------------------------------------
-- 19) SINGLE CITY 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
	if not citySet[city] then return "''Error: unknown city (must match canonical name).''" end

	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
	local showEcon = tostring(args.showEcon or "no"):lower() == "yes"

	local di = getDateInfo()

	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 econSignals = {}
	if showEcon then econSignals = getEconSignals() end

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

	local row = buildCityRow(di, {name=city, region=region, tier=tier}, threatMap, includeThreat, showEcon, econSignals)

	local out = {}
	table.insert(out, "{| class='wikitable'")
	local header = "! Date !! City !! Region !! Ops Alert !! Mystery (Cult) !! Unit !! Tasking"
	if showEcon then header = header .. " !! Economic signal" end
	header = header .. " !! Temple Bank Notes"
	if includeThreat then header = header .. " !! Threat Watch" end
	header = header .. " !! Severity"
	table.insert(out, header)

	table.insert(out, "|-" .. row.style)
	local line = string.format("| %s || %s || %s || %s || %s || %s || %s",
		row.date, row.city, row.region, row.ops, row.order, row.unit, row.task
	)
	if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
	line = line .. " || " .. row.bank
	if includeThreat then
		local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
		line = line .. " || " .. threatCell
	end
	line = line .. " || " .. row.sev
	table.insert(out, line)

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

--------------------------------------------------------------------------------
-- 20) ORDER-ONLY VIEW
--------------------------------------------------------------------------------
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).''"
	end
	local wantedOrder = orderByName[wanted]
	if not wantedOrder then
		return "''Error: unknown order name (must match module order names).''"
	end

	local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
	local showEcon = tostring(args.showEcon or "no"):lower() == "yes"

	local di = getDateInfo()

	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 econSignals = {}
	if showEcon then econSignals = getEconSignals() end

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	local header = "! Date !! City !! Region !! Ops Alert !! Unit !! Tasking"
	if showEcon then header = header .. " !! Economic signal" end
	header = header .. " !! Temple Bank Notes"
	if includeThreat then header = header .. " !! Threat Watch" end
	header = header .. " !! Severity"
	table.insert(out, header)

	for _, c in ipairs(cityOrder) do
		local row = buildCityRow(di, c, threatMap, includeThreat, showEcon, econSignals)
		-- Compare by cultLink target (stable) rather than raw text
		if row.order:find(cultLink(wantedOrder.name), 1, true) or row.order:find(wantedOrder.name, 1, true) then
			table.insert(out, "|-" .. row.style)
			local line = string.format("| %s || %s || %s || %s || %s || %s",
				row.date, row.city, row.region, row.ops, row.unit, row.task
			)
			if showEcon then
				line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a")
			end
			line = line .. " || " .. row.bank
			if includeThreat then
				local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
				line = line .. " || " .. threatCell
			end
			line = line .. " || " .. row.sev
			table.insert(out, line)
		end
	end

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

return p