Module:MissionaryDailyOpsBVR: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
Undo revision 387792 by NewZimiaGov (talk)
Tag: Undo
Line 1: Line 1:
-- Module:MissionaryDailyOpsBVR
-- Module:MissionaryDailyOpsBVR (REWRITE + WX OPS ALERT + TEMPLE APROBELLE)
-- Daily missionary tasking ledger for Bassaridia Vaeringheim (Temple Bank / RSP).
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Deterministic output (no math.random / no os.time seeding).
--
--
-- FIXES EMBEDDED (latest):
-- Features:
--  1) Ops Alert integration (Module:BassaridiaForecast):
--  • Canonical city network (regions + tiers)
--     - Adds Ops Alert column (badge + condition)
--  • City flags: existence-checked; fallback to Imperial Trade Union flag when missing.
--     - Orange/Red weather can bump severity + append a short weather-driven overlay (Orange/Red only)
--  • Orders + tasks (tagged, 10+ per order)
--  2) Temple Aprobelle added as an Order with 10+ tasks.
--  • Optional Threat Watch integration from Module:ReligiousEvents (opt-in)
--  3) Restores CULT FLAGS (order flags) WITHOUT expensive file existence checks:
--  • Optional Economic signal parsing from General Port of Lake Morovia (opt-in; safe fallback)
--    - Uses explicit mappings; everything else falls back to Imperial Trade Union flag.
--  • WX Ops Alert integration from Module:BassaridiaForecast:
--  4) Removes “too many expensive function calls” risk:
--     - Adds “Ops Alert” column (badge + condition)
--    - NO mw.title.exists checks for city/cult flags
--     - Bumps severity (Orange/Red) + appends weather-driven task overlay (Orange/Red only)
--    - NO page getContent unless showEcon=yes
--
--
-- Usage:
-- Usage:
Line 32: Line 32:
if s == nil then return "" end
if s == nil then return "" end
s = tostring(s)
s = tostring(s)
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
s = s:gsub("^%s+", ""):gsub("%s+$", "")
return s
end
end


Line 41: Line 42:
local function stripWiki(s)
local function stripWiki(s)
s = tostring(s or "")
s = tostring(s or "")
-- templates
-- Remove files/templates quickly
s = s:gsub("%[%[File:[^%]]+%]%]", "")
s = s:gsub("%{%b{}%}", "")
s = s:gsub("%{%b{}%}", "")
-- links: [[A|B]] => B, [[A]] => A
-- Links: [[A|B]] => B, [[A]] => A
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
-- italics/bold
-- Bold/italics
s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
return trim(s)
return trim(s)
Line 54: Line 56:
if not tags or #tags == 0 then return "" end
if not tags or #tags == 0 then return "" end
return table.concat(tags, ", ")
return table.concat(tags, ", ")
end
local function safeTitleContent(titleText)
local ok, t = pcall(mw.title.new, titleText)
if not ok or not t then return nil end
local ok2, content = pcall(function() return t:getContent() end)
if not ok2 then return nil end
return content
end
end


Line 154: Line 164:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 3) COLOR CODING
-- 3) COLOR CODING (Hatch-style)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function sevStyle(sev)
local function sevStyle(sev)
Line 167: Line 177:


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


-- 54 canonical cities; tier used for "major city" baseline.
local cityOrder = {
local cityOrder = {
-- Core majors
-- Core majors
Line 252: Line 263:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 4.1) CITY FLAGS (NO existence checks; override-or-fallback)
-- 4.1) CITY FLAGS (existence-checked + fallback)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local DEFAULT_CITY_FLAG_FILE = "Imperial Trade Union flag.png"
local DEFAULT_CITY_FLAG_FILE = "Imperial Trade Union flag.png"


local function normalizeForFlag(name)
return (name or "")
:gsub("[%s%-’]", "")
:gsub("[ÁáÀàÂâÄäÃãÅåĀā]", "A")
:gsub("[ÉéÈèÊêËëĒē]", "E")
:gsub("[ÍíÌìÎîÏïĪī]", "I")
:gsub("[ÓóÒòÔôÖöÕõØøŌō]", "O")
:gsub("[ÚúÙùÛûÜüŪū]", "U")
:gsub("[ÝýŸÿ]", "Y")
:gsub("[Ææ]", "AE")
end
local fileExistsCache = {}
local function fileExists(fileName)
if not fileName or fileName == "" then return false end
if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
local ok, t = pcall(mw.title.new, "File:" .. fileName)
local exists = (ok and t and t.exists == true) or false
fileExistsCache[fileName] = exists
return exists
end
-- Explicit odd filenames you noted (plus Skyrophos).
local cityFlagOverrides = {
local cityFlagOverrides = {
["Skýrophos"] = "SkyrophosFlag.png",
["Skýrophos"] = "SkyrophosFlag.png",
["Sårensby"]  = "SårensbyFlag.png",
["Sårensby"]  = "SårensbyFlag.png",
["Slevik"]    = "SlevikFlag.png.png",
["Slevik"]    = "SlevikFlag.png.png",
-- Add more known-good city flag filenames here.
["Kaledonija"]= "KaledonijaFlag.png",
}
}


Line 266: Line 300:
local function getCityFlagMarkup(cityName)
local function getCityFlagMarkup(cityName)
if cityFlagMarkupCache[cityName] then return cityFlagMarkupCache[cityName] end
if cityFlagMarkupCache[cityName] then return cityFlagMarkupCache[cityName] end
local f = cityFlagOverrides[cityName] or DEFAULT_CITY_FLAG_FILE
 
local mk = string.format("[[File:%s|20px]]", f)
-- 1) explicit override
local override = cityFlagOverrides[cityName]
if override and fileExists(override) then
local mk = string.format("[[File:%s|20px]]", override)
cityFlagMarkupCache[cityName] = mk
return mk
end
 
-- 2) try normalized candidates
local norm = normalizeForFlag(cityName)
local candidates = {
norm .. "Flag.png",
cityName:gsub(" ", "") .. "Flag.png",
cityName:gsub(" ", "") .. "flag.png",
}
for _, f in ipairs(candidates) do
if fileExists(f) then
local mk = string.format("[[File:%s|20px]]", f)
cityFlagMarkupCache[cityName] = mk
return mk
end
end
 
-- 3) fallback
local mk = string.format("[[File:%s|20px]]", DEFAULT_CITY_FLAG_FILE)
cityFlagMarkupCache[cityName] = mk
cityFlagMarkupCache[cityName] = mk
return mk
return mk
Line 276: Line 334:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local org = {
local org = {
Lampade     = { code="L", size=8  },
Lampade   = { code="L", size=8  },
Hetairoi   = { code="H", size=12 },
Hetairoi   = { code="H", size=12 },
Dodekade   = { code="D", size=18 },
Dodekade   = { code="D", size=18 },
Kleisthenes = { code="K", size=25 },
Kleisthenes= { code="K", size=25 },
Stoa       = { code="S", size=40 },
Stoa       = { code="S", size=40 },
Thiasos     = { code="T", size=60 },
Thiasos   = { code="T", size=60 },
}
}


Line 295: Line 353:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 6) CULT / ORDER FLAGS (NO existence checks; override-or-fallback)
-- 6) ORDER ICONS (optional; safe if file missing)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local DEFAULT_ORDER_FLAG_FILE = "Imperial Trade Union flag.png"
local orderIconFile = {
 
["Ordo Solis Invicti"]       = "OrdoSolisInvicti.png",
local orderFlagOverrides = {
["Order of Aurora Mystica"]   = "OrderOfAuroraMystica.png",
["Ordo Solis Invicti"]             = "OrdoSolisInvictiFlag.png",
["Conclave Illuminara"]       = "ConclaveIlluminara.png",
["Order of Aurora Mystica"]         = "OrderOfAuroraMysticaFlag.png",
["Sanctum Delphica"]         = "SanctumDelphica.png",
["Conclave Illuminara"]             = "ConclaveIlluminaraFlag.png",
["Sanctum Vitalis"]           = "SanctumVitalis.png",
["Sanctum Delphica"]               = "SanctumDelphicaFlag.png",
["Pharmacon Sect"]           = "PharmaconSect.png",
["Sanctum Vitalis"]                 = "SanctumVitalisFlag.png",
["Mystery of the Verdant Embrace"] = "VerdantEmbrace.png",
["Pharmacon Sect"]                 = "PharmaconSectFlag.png",
["Court of the Ironclad"]     = "CourtOfTheIronclad.png",
["Mystery of the Verdant Embrace"] = "VerdantEmbraceFlag.png",
["Reverie Nebulous"]         = "ReverieNebulous.png",
["Court of the Ironclad"]           = "CourtOfTheIroncladFlag.png",
["Azure Sentinel Sect"]       = "AzureSentinelSect.png",
["Reverie Nebulous"]               = "ReverieNebulousFlag.png",
["Temple Aprobelle"]         = "TempleAprobelle.png", -- NEW
["Azure Sentinel Sect"]             = "AzureSentinelSectFlag.png",
["Temple Aprobelle"]               = "TempleAprobelleFlag.png",
-- Add more as you create the files.
}
}


local function getOrderFlagMarkup(orderName)
local function getOrderIconMarkup(orderName)
local f = orderFlagOverrides[orderName] or DEFAULT_ORDER_FLAG_FILE
local f = orderIconFile[orderName]
return string.format("[[File:%s|18px]] ", f)
if f and fileExists(f) then
return string.format("[[File:%s|18px]] ", f)
end
return ""
end
end


Line 340: Line 398:
T("Public lamp and beacon blessing for tunnel nodes; wayfinding clarified for night labor shifts.","green",{"infrastructure","transit"}),
T("Public lamp and beacon blessing for tunnel nodes; wayfinding clarified for night labor shifts.","green",{"infrastructure","transit"}),
T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit"}),
T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit"}),
T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd"}),
T("Counter-broadsheet sermon cycle following unsigned pamphlets; witness interviews conducted with local elders.","yellow",{"investigation","public"}),
T("Counter-broadsheet sermon cycle following unsigned pamphlets; witness interviews conducted with local elders.","yellow",{"investigation","public"}),
T("Joint patrol-litany along rail platforms to reduce theft and panic during peak transit hours.","yellow",{"rail","security"}),
T("Joint patrol-litany along rail platforms to reduce theft and panic during peak transit hours.","yellow",{"rail","security"}),
Line 360: Line 419:
T("Emergency reconciliation summit after a street brawl between factions; formal truce terms read aloud.","red",{"unrest","mediation"}),
T("Emergency reconciliation summit after a street brawl between factions; formal truce terms read aloud.","red",{"unrest","mediation"}),
T("Crisis dawn vigil after violent night incident; grief support, calm messaging, and ‘no retaliation’ pledge drive.","red",{"crisis","public","grief"}),
T("Crisis dawn vigil after violent night incident; grief support, calm messaging, and ‘no retaliation’ pledge drive.","red",{"crisis","public","grief"}),
T("Stabilization briefings for district clerks following audit findings; correction plan countersigned.","yellow",{"audit","governance"}),
}
}
},
},
Line 370: Line 430:
T("Lantern-route inspection for canal patrols; reflective markers placed to reduce night accidents.","green",{"infrastructure","transit"}),
T("Lantern-route inspection for canal patrols; reflective markers placed to reduce night accidents.","green",{"infrastructure","transit"}),
T("Dream-space audit: mirror pools cleaned, offerings catalogued, and safety signage posted.","green",{"shrine","audit"}),
T("Dream-space audit: mirror pools cleaned, offerings catalogued, and safety signage posted.","green",{"shrine","audit"}),
T("Star-chart reading for civic council; guidance recorded for later reference by scribes.","green",{"governance","education"}),
T("Investigation of counterfeit ‘moon tokens’ being sold to pilgrims; vendor interviews and seizure log.","yellow",{"investigation","trade"}),
T("Investigation of counterfeit ‘moon tokens’ being sold to pilgrims; vendor interviews and seizure log.","yellow",{"investigation","trade"}),
T("Controlled night gathering after panic rumor; calm-litany enforced, dispersal plan coordinated with stewards.","yellow",{"rumor","crowd"}),
T("Controlled night gathering after panic rumor; calm-litany enforced, dispersal plan coordinated with stewards.","yellow",{"rumor","crowd"}),
Line 375: Line 436:
T("Emergency anti-panic messaging after a ‘demonic sign’ claim; script distributed to shrine speakers.","red",{"crisis","rumor"}),
T("Emergency anti-panic messaging after a ‘demonic sign’ claim; script distributed to shrine speakers.","red",{"crisis","rumor"}),
T("Coordinated blackout safety drill (lanterns, guides, canal warning ropes) during storm season.","yellow",{"infrastructure","safety"}),
T("Coordinated blackout safety drill (lanterns, guides, canal warning ropes) during storm season.","yellow",{"infrastructure","safety"}),
T("Nighttime crowd calming near a transit node; chants regulated to prevent stampede behavior.","yellow",{"crowd","transit"}),
}
}
},
},
Line 382: Line 442:
link=orderLink("Sisera (Reformed Stripping Path)","The_Cult_of_Sanctum_Delphica","Sanctum Delphica"),
link=orderLink("Sisera (Reformed Stripping Path)","The_Cult_of_Sanctum_Delphica","Sanctum Delphica"),
tasks={
tasks={
T("Archive custody inspection: artifact seals verified, witness ledger updated, and storage conditions checked.","green",{"archive","audit"}),
T("Archive custody inspection: artifact seals verified, witness ledger updated, and storage humidity checked.","green",{"archive","audit"}),
T("Scribe training on doctrinal custody chain; convoy paperwork rehearsed for shrine transfers.","green",{"education","logistics"}),
T("Scribe training on doctrinal custody chain; convoy paperwork rehearsed for shrine transfers.","green",{"education","logistics"}),
T("Pilgrim etiquette workshop at a library shrine; damage prevention and quiet-zone enforcement.","green",{"public","education"}),
T("Pilgrim etiquette workshop at a library shrine; damage prevention and quiet-zone enforcement.","green",{"public","education"}),
Line 399: Line 459:
tasks={
tasks={
T("Clinic resupply run: bandage, tonic, and clean-water amphorae inventory reconciled with Temple Bank vouchers.","green",{"health","logistics"}),
T("Clinic resupply run: bandage, tonic, and clean-water amphorae inventory reconciled with Temple Bank vouchers.","green",{"health","logistics"}),
T("Water-quality checks at canal taps; purification guidance posted for household stewards.","green",{"health","infrastructure"}),
T("Water-quality checks at canal taps; chlorination guidance posted for household stewards.","green",{"health","infrastructure"}),
T("School hygiene lecture (handwashing, food safety) paired with a modest blessing service.","green",{"health","education"}),
T("School hygiene lecture (handwashing, food safety) paired with gentle blessing service.","green",{"health","education"}),
T("Routine fever screening at a market gate; referral slips issued to clinics.","green",{"health","public"}),
T("Routine fever screening at a market gate during festival season; referral slips issued.","green",{"health","public"}),
T("Temporary clinic tent established near shrine square; triage lines marked and staffed.","yellow",{"health","crowd"}),
T("Temporary clinic tent established near shrine square; triage lines marked and staffed.","yellow",{"health","crowd"}),
T("Contamination rumor assessment: test kits deployed to prove/disprove claims and prevent panic.","yellow",{"health","rumor"}),
T("Contamination rumor assessment: test kits deployed to prove/disprove claims and prevent panic.","yellow",{"health","rumor"}),
Line 415: Line 475:
tasks={
tasks={
T("Dispensary audit: dosage seals verified; controlled compounds logged and countersigned.","green",{"health","audit"}),
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("Field compounding session with local 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("Clinic support for chronic cases; treatment plans documented in Temple Bank ledgers.","green",{"health","service"}),
T("Water purification engineering consult at a canal gate; intake screens inspected for sabotage.","yellow",{"infrastructure","investigation"}),
T("Water purification engineering consult at a canal gate; intake screens inspected for sabotage.","yellow",{"infrastructure","investigation"}),
Line 439: Line 499:
T("Post-storm debris clearance with shrine volunteers; hazard markings placed.","yellow",{"safety","infrastructure"}),
T("Post-storm debris clearance with shrine volunteers; hazard markings placed.","yellow",{"safety","infrastructure"}),
T("Disease vector mitigation in swamp edges; standing water treated and guidance posted.","yellow",{"health","infrastructure"}),
T("Disease vector mitigation in swamp edges; standing water treated and guidance posted.","yellow",{"health","infrastructure"}),
T("Crisis canal breach response: temporary closure, water reroute, and crowd calming in shrine districts.","red",{"crisis","infrastructure","crowd"}),
T("Crisis canal breach response: temporary closure, reroute of water flow, and crowd calming.","red",{"crisis","infrastructure","crowd"}),
}
}
},
},
Line 450: Line 510:
T("Workshop on oath-keeping and duty; pledge cards sealed for civic stewards.","green",{"governance","education"}),
T("Workshop on oath-keeping and duty; pledge cards sealed for civic stewards.","green",{"governance","education"}),
T("Route hardening walk: identify vulnerable alleys, broken lamps, and unsafe crossings.","yellow",{"security","infrastructure"}),
T("Route hardening walk: identify vulnerable alleys, broken lamps, and unsafe crossings.","yellow",{"security","infrastructure"}),
T("Contraband confiscation support at a market cluster; evidence logged for Temple Bank clerks.","yellow",{"security","investigation"}),
T("Contraband confiscation support at a market stall cluster; evidence logged for Temple Bank clerks.","yellow",{"security","investigation"}),
T("High-risk escort for a witness leaving a hearing; safehouse transfer arranged.","red",{"security","crisis"}),
T("High-risk escort for a witness leaving a hearing; safehouse transfer arranged.","red",{"security","crisis"}),
T("Emergency deterrence patrol after riot flare; separation lines and dispersal corridor opened.","red",{"unrest","crowd"}),
T("Emergency deterrence patrol after riot flare; separation lines and dispersal corridor opened.","red",{"unrest","crowd"}),
Line 465: Line 525:
T("Quiet art therapy session for children after a minor scare; parents briefed on calm routines.","green",{"care","education"}),
T("Quiet art therapy session for children after a minor scare; parents briefed on calm routines.","green",{"care","education"}),
T("Public ‘sleep hygiene’ teaching for shift workers; rest scheduling guidance posted.","green",{"health","education"}),
T("Public ‘sleep hygiene’ teaching for shift workers; rest scheduling guidance posted.","green",{"health","education"}),
T("Rumor mapping: identify which dreams are spreading a harmful narrative; corrective story deployed.","yellow",{"rumor","analysis"}),
T("Rumor mapping exercise: identify which dreams are spreading a harmful narrative; corrective story deployed.","yellow",{"rumor","analysis"}),
T("Witness interviews after a trance-gathering complaint; compliance conditions issued.","yellow",{"audit","public"}),
T("Witness interviews after a trance-gathering complaint; compliance conditions issued.","yellow",{"audit","public"}),
T("Containment support for mass panic dream episode; calm stations, water, and supervised rest zones established.","red",{"crisis","crowd"}),
T("Containment support for mass panic dream episode; calm stations, water, and supervised rest zones established.","red",{"crisis","crowd"}),
Line 491: Line 551:
},
},


-- NEW ORDER: Temple Aprobelle
-- NEW: Temple Aprobelle
{key="TAP", name="Temple Aprobelle",
{key="TAP", name="Temple Aprobelle",
link="[[Temple_Aprobelle|Temple Aprobelle]]",
link="[[Temple_Aprobelle|Temple Aprobelle]]",
tasks={
tasks={
T("Festival hygiene audit: vendor protocols verified (water handling, waste control); seals issued to compliant stalls.","green",{"health","audit","festival","public"}),
T("Festival hygiene audit: vendor protocols verified (ladle rotation, water handling, waste control); seals issued to compliant stalls.","green",{"health","audit","festival","public"}),
T("Rumor-control bulletin cycle: shrine-leader briefing + posted clarifications to prevent panic narratives from hardening.","green",{"rumor","public","service"}),
T("Rumor-control bulletin cycle: shrine-leader briefing + posted clarifications to prevent panic narratives from hardening.","green",{"rumor","public","service"}),
T("Grief-cadre home visits after an accident; de-escalation coaching for relatives and neighborhood stewards.","green",{"grief","mediation","service"}),
T("Grief-cadre home visits after a recent accident; de-escalation coaching for relatives and neighborhood stewards.","green",{"grief","mediation","service"}),
T("School immunization coordination: clinic schedule posted, consent ledgers reconciled, reassurance rites conducted.","green",{"health","school","coordination"}),
T("School immunization coordination: clinic schedule posted, consent ledgers reconciled, and reassurance rites conducted.","green",{"health","school","coordination"}),
T("Clinic-queue stabilization drill: entry metering, calm-litany, conflict prevention training for volunteers.","yellow",{"queue","crowd","health","order"}),
T("Clinic-queue stabilization drill: entry metering, calm-litany, and conflict prevention training for volunteers.","yellow",{"queue","crowd","health","order"}),
T("Loyalty and behavior audit of a district office after complaint surge; corrective training issued with Temple Bank clerk witness.","yellow",{"audit","governance","compliance"}),
T("Loyalty and behavior audit of a district office after complaint surge; corrective training issued with Temple Bank clerk witness.","yellow",{"audit","governance","compliance"}),
T("Anomaly-response dispatch after ‘demonic contamination’ claims; care provision paired with narrative discipline.","yellow",{"anomaly","rumor","public","health"}),
T("Anomaly-response dispatch to a shrine ward after ‘demonic contamination’ claims; care provision paired with narrative discipline.","yellow",{"anomaly","rumor","public","health"}),
T("Pattern-analysis intake: confiscated schism texts logged, indexed, forwarded for doctrinal signature correlation.","yellow",{"analysis","schism","security","archive"}),
T("Pattern-analysis intake: confiscated schism texts and recordings logged, indexed, and forwarded for doctrinal signature correlation.","yellow",{"analysis","schism","security","archive"}),
T("Transit node stabilization after crowd surge: calm stations, signage, steward handoffs established.","yellow",{"transit","crowd","stabilization"}),
T("Transit node stabilization after crowd surge: calm stations, signage, and steward handoffs established.","yellow",{"transit","crowd","stabilization"}),
T("Silencing-cadre activated after panic flare; controlled messaging + visible care stations deployed under escort.","red",{"panic","rumor","stabilization"}),
T("Silencing-cadre activated after panic flare; controlled messaging + visible care stations deployed under escort.","red",{"panic","rumor","stabilization"}),
T("Emergency stabilization in evacuation/fever center: calming rites, grief triage, rumor suppression until order restored.","red",{"crisis","health","crowd","stabilization"}),
T("Emergency stabilization in evacuation/fever center: calming rites, grief triage, and rumor suppression until order restored.","red",{"crisis","health","crowd","stabilization"}),
T("Post-incident narrative repair: rumor sources mapped, apology ceremonies arranged, and retaliation prevention pledges collected.","yellow",{"analysis","mediation","public"}),
}
}
},
},
Line 514: Line 575:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 8) CITY PROFILES (order bias weights)
-- 8) CITY PROFILES (tags + order bias weights)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local cityProfiles = {
local cityProfiles = {
["Vaeringheim"]     = {tags={"capital","port","canals"}, weights={OSI=2,OAM=2,CIC=2,SCD=2,SCV=2,ASS=2,TAP=3}},
["Vaeringheim"] = {tags={"capital","port","canals"}, weights={OSI=2,OAM=2,CIC=2,SCD=2,SCV=2,ASS=2,TAP=3}},
["Somniumpolis"]   = {tags={"wetlands","dream-space","panic-risk"}, weights={RNB=3,SCV=2,PHA=2,TAP=3,OSI=1}},
["Somniumpolis"] = {tags={"wetlands","dream-space","panic-risk"}, weights={RNB=3,SCV=2,PHA=2,TAP=3,OSI=1}},
["Luminaria"]       = {tags={"dawn","tunnels"}, weights={OAM=3,CIC=2,OSI=2,TAP=2}},
["Luminaria"] = {tags={"dawn","tunnels","enlightenment"}, weights={OAM=3,CIC=2,OSI=2,TAP=2}},
["Lunalis Sancta"] = {tags={"libraries","court"}, weights={SCD=3,CIC=2,TAP=2,OSI=1}},
["Lunalis Sancta"] = {tags={"libraries","court","ritual"}, weights={SCD=3,CIC=2,TAP=2,OSI=1}},
["Delphica"]       = {tags={"archive","custody"}, weights={SCD=3,TAP=2,CIC=1}},
["Delphica"] = {tags={"archive","custody"}, weights={SCD=3,TAP=2,CIC=1}},
["Acheron"]         = {tags={"canyonlands","medicine"}, weights={PHA=3,SCV=2,TAP=1,CICL=1}},
["Acheron"] = {tags={"canyonlands","medicine"}, weights={PHA=3,SCV=2,TAP=1,CICL=1}},
["Thermosalem"]     = {tags={"springs","crowds"}, weights={SCV=2,PHA=2,TAP=2,ASS=1}},
["Thermosalem"] = {tags={"springs","crowds"}, weights={SCV=2,PHA=2,TAP=2,ASS=1}},
["Pyralis"]         = {tags={"festival","risk"}, weights={SCV=2,TAP=2,ASS=2,OAM=1}},
["Pyralis"] = {tags={"festival","risk"}, weights={SCV=2,TAP=2,ASS=2,OAM=1}},
["Keybir-Aviv"]     = {tags={"harbor","compliance"}, weights={ASS=3,TAP=2,OSI=1,CIC=1}},
["Keybir-Aviv"] = {tags={"harbor","compliance"}, weights={ASS=3,TAP=2,OSI=1,CIC=1}},
["Tel-Amin"]       = {tags={"coastal","guilds"}, weights={OAM=2,TAP=2,ASS=1,SCV=1}},
["Tel-Amin"] = {tags={"coastal","guilds"}, weights={OAM=2,TAP=2,ASS=1,SCV=1}},
["Diamandis"]       = {tags={"festival","cemeteries"}, weights={TAP=2,SCV=2,OAM=1,RNB=1}},
["Diamandis"] = {tags={"festival","cemeteries"}, weights={TAP=2,SCV=2,OAM=1,RNB=1}},
["Lewisburg"]       = {tags={"port-node","interdiction"}, weights={ASS=2,CICL=2,TAP=2,OSI=1}},
["Lewisburg"] = {tags={"port-node","interdiction"}, weights={ASS=2,CICL=2,TAP=2,OSI=1}},
["Lykopolis"]       = {tags={"trade","crowds"}, weights={ASS=2,TAP=2,CICL=1,OSI=1}},
["Lykopolis"] = {tags={"trade","crowds"}, weights={ASS=2,TAP=2,CICL=1,OSI=1}},
["Tonar"]           = {tags={"occupation","corridors"}, weights={SCD=2,SCV=2,VEG=2,TAP=1,CICL=1}},
["Tonar"] = {tags={"occupation","canals"}, weights={SCD=2,SCV=2,VEG=2,TAP=1,CICL=1}},
["Bashkim"]         = {tags={"steppe","irrigation"}, weights={VEG=3,SCV=2,TAP=1,CICL=1}},
["Bashkim"] = {tags={"steppe","irrigation"}, weights={VEG=3,SCV=2,TAP=1,CICL=1}},
["Ourid"]           = {tags={"river","irrigation"}, weights={VEG=3,SCV=2,TAP=1}},
["Ourid"] = {tags={"river","irrigation"}, weights={VEG=3,SCV=2,TAP=1}},
["Ardclach"]       = {tags={"north","cold"}, weights={ASS=2,SCV=2,TAP=1,CIC=1}},
["Ardclach"] = {tags={"north","cold"}, weights={ASS=2,SCV=2,TAP=1,CIC=1}},
["Riddersborg"]     = {tags={"north","order"}, weights={CICL=2,OSI=2,TAP=1,SCV=1}},
["Riddersborg"] = {tags={"north","order"}, weights={CICL=2,OSI=2,TAP=1,SCV=1}},
}
}


Line 544: Line 605:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 9) OPTIONAL: ECON SIGNALS (expensive; only when showEcon=yes)
-- 9) OPTIONAL: ECON SIGNALS (safe fallback)
-- Returns a map: econSignals[cityName] = {summary="...", tags={"industry","agri"}}
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function getEconSignals()
local function getEconSignals()
local title = mw.title.new("General Port of Lake Morovia")
local title = mw.title.new("General Port of Lake Morovia")
if not title then return {} end
if not title then return {} end
local ok, content = pcall(function() return title:getContent() end)
local content = safeTitleContent(title.fullText)
if not ok or not content or content == "" then return {} end
if not content or content == "" then return {} end


-- Lightweight heuristic: find occurrences of city names near “Company” blocks.
local signals = {}
local signals = {}
local text = normalizeSpaces(content)
local text = normalizeSpaces(content)
Line 558: Line 621:
local n = c.name
local n = c.name
local hits = 0
local hits = 0
local pattern = n:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])","%%%1")
-- count mentions as proxy for “activity”
for _ in text:gmatch(pattern) do
for _ in text:gmatch(n:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])","%%%1")) do
hits = hits + 1
hits = hits + 1
if hits >= 6 then break end
if hits >= 6 then break end
Line 584: Line 647:
if line:match("^|%s") and not line:match("^|%-%s*$") then
if line:match("^|%s") and not line:match("^|%-%s*$") then
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
-- expected: Date || City || Event || Urgency
if #parts >= 4 then
if #parts >= 4 then
local city = stripWiki(parts[2])
local city = stripWiki(parts[2])
Line 607: Line 671:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 11) WX OPS ALERT INTEGRATION (Module:BassaridiaForecast)
-- 11) WX (Ops Alert) integration from Module:BassaridiaForecast
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
Line 642: Line 706:
local imp = op.impacts or {}
local imp = op.impacts or {}


-- Only Orange/Red bump severity
-- Only Orange/Red force severity bumps (Yellow stays informational)
if lvl == 2 and sev == "green" then sev = "yellow" end
if lvl == 2 and sev == "green" then sev = "yellow" end
if lvl >= 3 then sev = "red" end
if lvl >= 3 then sev = "red" end
Line 656: Line 720:
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 -- only Orange/Red appends operational language


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


if cond == "Storm" or cond == "Rain" then
if cond == "Storm" or cond == "Rain" then
return base .. " Clinic/relief queues stabilized; rumor bulletins posted; shrine-district lanes marked."
return base .. " Queue stabilization at clinics/relief points; rumor containment bulletins posted; shrine-district patrol lanes marked."
elseif cond == "Blizzard" or cond == "Snow" then
elseif cond == "Blizzard" or cond == "Snow" then
return base .. " Warming-shelter checks; fuel/medicine custody verified; travel guidance posted at shrine gates."
return base .. " Warming-shelter checks; fuel and medicine custody verification; travel guidance posted at shrine gates."
elseif cond == "Fog" then
elseif cond == "Fog" then
return base .. " Navigation warnings and crowd guidance in canal/port corridors; panic narratives suppressed early."
return base .. " Navigation warnings and crowd guidance in canal/port corridors; panic narratives suppressed early."
elseif cond == "Dust" then
elseif cond == "Dust" then
return base .. " Mask discipline and vendor hygiene enforcement prioritized; indoor refuge advisories posted."
return base .. " Mask discipline, indoor refuge advisories, and vendor hygiene enforcement prioritized."
else
else
return base .. " Welfare checks and public messaging prioritized."
return base .. " Welfare checks and public messaging prioritized."
Line 682: Line 746:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function inferSeverity(cityTier, taskSev, threatUrg)
local function inferSeverity(cityTier, taskSev, threatUrg)
-- base from task text classification
local sev = tostring(taskSev or "green")
local sev = tostring(taskSev or "green")
-- major cities run “hotter”
if cityTier == "major" and sev == "green" then sev = "yellow" end
if cityTier == "major" and sev == "green" then sev = "yellow" end
if urgencyScore(threatUrg) >= 3 then
-- threat override
sev = "red"
if urgencyScore(threatUrg) >= 3 then sev = "red"
elseif urgencyScore(threatUrg) == 2 and sev == "green" then
elseif urgencyScore(threatUrg) == 2 and sev == "green" then sev = "yellow" end
sev = "yellow"
end
return sev
return sev
end
end
Line 694: Line 758:
local function pickOrder(nextInt, profile, econSig, threat)
local function pickOrder(nextInt, profile, econSig, threat)
local weighted = {}
local weighted = {}
-- start with profile weights
for key, w in pairs(profile.weights or {}) do
for key, w in pairs(profile.weights or {}) do
weighted[#weighted+1] = {key=key, w=tonumber(w) or 0}
weighted[#weighted+1] = {key=key, w=tonumber(w) or 0}
end
end


-- econ bump: port-linked => compliance/stability
-- econ bump: if port-linked, bias to compliance/stability orders
if econSig and econSig.tags and econSig.tags[1] == "port" then
if econSig and econSig.tags and econSig.tags[1] == "port" then
for _, it in ipairs(weighted) do
for _, it in ipairs(weighted) do
Line 705: Line 770:
end
end


-- threat bump: prefer Aprobelle/security/medical
-- threat bump: prefer Aprobelle / security / medical on threat days
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
Line 720: Line 785:


local function pickTask(nextInt, o, sev)
local function pickTask(nextInt, o, sev)
-- bias toward sev-matching tasks
local pool = {}
local pool = {}
for _, t in ipairs(o.tasks or {}) do
for _, t in ipairs(o.tasks or {}) do
Line 728: Line 794:
pool[#pool+1] = {t=t, w=w}
pool[#pool+1] = {t=t, w=w}
end
end
return pickWeighted(nextInt, pool).t
local chosen = pickWeighted(nextInt, pool).t
return chosen
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 13) TEMPLE BANK NOTES
-- 13) TEMPLE BANK NOTES (flavor + compliance)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local bankNotes = {
local bankNotes = {
Line 755: Line 822:


local profile = getProfile(cityName)
local profile = getProfile(cityName)
local sig = (showEcon and econSignals) and econSignals[cityName] or nil
local sig = econSignals and econSignals[cityName] or nil
local th = includeThreat and threatMap and threatMap[cityName] or nil
local th = includeThreat and threatMap and threatMap[cityName] or nil


-- choose order, then task
local o = pickOrder(nextInt, profile, sig, th)
local o = pickOrder(nextInt, profile, sig, th)
local baseTask = pickTask(nextInt, o, "yellow") -- initial; severity recalculated below
local task = pickTask(nextInt, o, "yellow") -- starting bias; severity may shift below


local threatUrg = th and th.urg or ""
local threatUrg = th and th.urg or ""
local sev = inferSeverity(tier, baseTask.sev, threatUrg)
local sev = inferSeverity(tier, task.sev, threatUrg)


-- WX: ops badge + severity bump + overlay
-- WX: ops badge + severity bump + overlay (Orange/Red)
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)
local taskText = baseTask.text .. weatherTaskOverlay(wxData)
local taskText = task.text .. weatherTaskOverlay(wxData)


local unitName = pickOrgUnit(nextInt, sev)
local unitName = pickOrgUnit(nextInt, sev)
Line 778: Line 846:
local tags = {}
local tags = {}
for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
for _, t in ipairs(baseTask.tags or {}) do tags[#tags+1] = t end
for _, t in ipairs(task.tags or {}) do tags[#tags+1] = t end
if th and th.event and th.event ~= "" then tags[#tags+1] = "threat-watch" end
if th and th.event and th.event ~= "" then tags[#tags+1] = "threat-watch" end
local threatText = ""
if includeThreat and th then threatText = th.event or "" end


return {
return {
Line 786: Line 857:
region = cityRegion,
region = cityRegion,
city = string.format("%s %s", getCityFlagMarkup(cityName), cityLink(cityName)),
city = string.format("%s %s", getCityFlagMarkup(cityName), cityLink(cityName)),
ops = opsBadge,
ops = opsBadge, -- NEW
order = getOrderFlagMarkup(o.name) .. o.link,
order = getOrderIconMarkup(o.name) .. o.link,
unit = string.format("%s (%d) [%s]", unitName, unitMeta.size, unitCode),
unit = string.format("%s (%d) [%s]", unitName, unitMeta.size, unitCode),
task = taskText,
task = taskText,
tags = joinTags(tags),
econ = (showEcon and sig) and econSummary(sig) or "",
econ = (showEcon and sig) and econSummary(sig) or "",
bank = bank,
bank = bank,
threat = (includeThreat and th and th.event) and th.event or "",
threat = threatText,
threatUrg = (includeThreat and th and th.urg) and th.urg or "",
threatUrg = threatUrg,
sev = sev,
sev = sev,
tags = joinTags(tags),
}
}
end
end
Line 811: Line 882:


local di = getDateInfo()
local di = getDateInfo()
 
local econSignals = getEconSignals()
-- IMPORTANT: expensive econ scan ONLY when explicitly requested
local econSignals = {}
if showEcon then econSignals = getEconSignals() end


local threatMap = {}
local threatMap = {}
Line 826: Line 894:
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 !! Cult Order !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
if showEcon then header = header .. " !! Economic signal" end
Line 866: Line 935:
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:#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, "<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/>Ops Alert badge reflects forecast-driven operational risk (Green/Yellow/Orange/Red) and aligns with War League / Hatch posture.")
table.insert(out, "<br/>For overseas deployments, see [[Bassaridian_involvement_in_Corum#Corumian_Missionary_Deployment_Reports|Corumian Missionary Deployment Reports]].")
table.insert(out, "</div>")
table.insert(out, "</div>")
end
end
Line 891: Line 959:


local di = getDateInfo()
local di = getDateInfo()
local econSignals = {}
local econSignals = getEconSignals()
if showEcon then econSignals = getEconSignals() end


local threatMap = {}
local threatMap = {}
Line 952: Line 1,019:


local di = getDateInfo()
local di = getDateInfo()
local econSignals = {}
local econSignals = getEconSignals()
if showEcon then econSignals = getEconSignals() end


local threatMap = {}
local threatMap = {}
Line 974: Line 1,040:
for _, c in ipairs(cityOrder) do
for _, c in ipairs(cityOrder) do
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
-- only keep rows where chosen order matches the wanted one
if row.order:find(wantedOrder.link, 1, true) or row.order:find(wantedOrder.name, 1, true) then
if row.order:find(wantedOrder.link, 1, true) or row.order:find(wantedOrder.name, 1, true) then
table.insert(out, "|-" .. row.style)
table.insert(out, "|-" .. row.style)

Revision as of 02:18, 2 January 2026

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

-- Module:MissionaryDailyOpsBVR (REWRITE + WX OPS ALERT + TEMPLE APROBELLE)
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Deterministic output (no math.random / no os.time seeding).
--
-- Features:
--  • Canonical city network (regions + tiers)
--  • City flags: existence-checked; fallback to Imperial Trade Union flag when missing.
--  • Orders + tasks (tagged, 10+ per order)
--  • Optional Threat Watch integration from Module:ReligiousEvents (opt-in)
--  • Optional Economic signal parsing from General Port of Lake Morovia (opt-in; safe fallback)
--  • WX Ops Alert integration from Module:BassaridiaForecast:
--      - Adds “Ops Alert” column (badge + condition)
--      - Bumps severity (Orange/Red) + appends weather-driven task overlay (Orange/Red only)
--
-- 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) SMALL UTILS
--------------------------------------------------------------------------------
local function trim(s)
	if s == nil then return "" end
	s = tostring(s)
	s = s:gsub("^%s+", ""):gsub("%s+$", "")
	return s
end

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

local function stripWiki(s)
	s = tostring(s or "")
	-- Remove files/templates quickly
	s = s:gsub("%[%[File:[^%]]+%]%]", "")
	s = s:gsub("%{%b{}%}", "")
	-- Links: [[A|B]] => B, [[A]] => A
	s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
	s = s:gsub("%[%[([^%]]+)%]%]", "%1")
	-- Bold/italics
	s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
	return trim(s)
end

local function joinTags(tags)
	if not tags or #tags == 0 then return "" end
	return table.concat(tags, ", ")
end

local function safeTitleContent(titleText)
	local ok, t = pcall(mw.title.new, titleText)
	if not ok or not t then return nil end
	local ok2, content = pcall(function() return t:getContent() end)
	if not ok2 then return nil end
	return content
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) COLOR CODING (Hatch-style)
--------------------------------------------------------------------------------
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

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

-- 54 canonical cities; tier used for "major city" baseline.
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

--------------------------------------------------------------------------------
-- 4.1) CITY FLAGS (existence-checked + fallback)
--------------------------------------------------------------------------------
local DEFAULT_CITY_FLAG_FILE = "Imperial Trade Union flag.png"

local function normalizeForFlag(name)
	return (name or "")
		:gsub("[%s%-’]", "")
		:gsub("[ÁáÀàÂâÄäÃãÅåĀā]", "A")
		:gsub("[ÉéÈèÊêËëĒē]", "E")
		:gsub("[ÍíÌìÎîÏïĪī]", "I")
		:gsub("[ÓóÒòÔôÖöÕõØøŌō]", "O")
		:gsub("[ÚúÙùÛûÜüŪū]", "U")
		:gsub("[ÝýŸÿ]", "Y")
		:gsub("[Ææ]", "AE")
end

local fileExistsCache = {}
local function fileExists(fileName)
	if not fileName or fileName == "" then return false end
	if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
	local ok, t = pcall(mw.title.new, "File:" .. fileName)
	local exists = (ok and t and t.exists == true) or false
	fileExistsCache[fileName] = exists
	return exists
end

-- Explicit odd filenames you noted (plus Skyrophos).
local cityFlagOverrides = {
	["Skýrophos"] = "SkyrophosFlag.png",
	["Sårensby"]  = "SårensbyFlag.png",
	["Slevik"]    = "SlevikFlag.png.png",
	["Kaledonija"]= "KaledonijaFlag.png",
}

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

	-- 1) explicit override
	local override = cityFlagOverrides[cityName]
	if override and fileExists(override) then
		local mk = string.format("[[File:%s|20px]]", override)
		cityFlagMarkupCache[cityName] = mk
		return mk
	end

	-- 2) try normalized candidates
	local norm = normalizeForFlag(cityName)
	local candidates = {
		norm .. "Flag.png",
		cityName:gsub(" ", "") .. "Flag.png",
		cityName:gsub(" ", "") .. "flag.png",
	}
	for _, f in ipairs(candidates) do
		if fileExists(f) then
			local mk = string.format("[[File:%s|20px]]", f)
			cityFlagMarkupCache[cityName] = mk
			return mk
		end
	end

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

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

--------------------------------------------------------------------------------
-- 6) ORDER ICONS (optional; safe if file missing)
--------------------------------------------------------------------------------
local orderIconFile = {
	["Ordo Solis Invicti"]        = "OrdoSolisInvicti.png",
	["Order of Aurora Mystica"]   = "OrderOfAuroraMystica.png",
	["Conclave Illuminara"]       = "ConclaveIlluminara.png",
	["Sanctum Delphica"]          = "SanctumDelphica.png",
	["Sanctum Vitalis"]           = "SanctumVitalis.png",
	["Pharmacon Sect"]            = "PharmaconSect.png",
	["Mystery of the Verdant Embrace"] = "VerdantEmbrace.png",
	["Court of the Ironclad"]     = "CourtOfTheIronclad.png",
	["Reverie Nebulous"]          = "ReverieNebulous.png",
	["Azure Sentinel Sect"]       = "AzureSentinelSect.png",
	["Temple Aprobelle"]          = "TempleAprobelle.png", -- NEW
}

local function getOrderIconMarkup(orderName)
	local f = orderIconFile[orderName]
	if f and fileExists(f) then
		return string.format("[[File:%s|18px]] ", f)
	end
	return ""
end

--------------------------------------------------------------------------------
-- 7) ORDERS + TASKS (tagged)
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
	return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
end

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

local orders = {
	{key="OSI", name="Ordo Solis Invicti",
		link=orderLink("Atos (Reformed Stripping Path)","Ordo_Solis_Invicti","Ordo Solis Invicti"),
		tasks={
			T("Dawn rites and public illumination drill to restore civic clarity and discourage rumor cascades in market wards.","green",{"public","education","service"}),
			T("Temple-school literacy session paired with cosmology instruction and shrine maintenance inspections.","green",{"education","shrine","service"}),
			T("Solar route inspection at major junctions; traffic etiquette posted for pilgrims and commuters.","green",{"transit","public"}),
			T("Sun-pledge renewal for civic clerks; oath sheets sealed and logged with Temple Bank scribes.","green",{"governance","audit"}),
			T("Public lamp and beacon blessing for tunnel nodes; wayfinding clarified for night labor shifts.","green",{"infrastructure","transit"}),
			T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit"}),
			T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd"}),
			T("Counter-broadsheet sermon cycle following unsigned pamphlets; witness interviews conducted with local elders.","yellow",{"investigation","public"}),
			T("Joint patrol-litany along rail platforms to reduce theft and panic during peak transit hours.","yellow",{"rail","security"}),
			T("Emergency morale procession after night unrest; hymn leadership and calm restoration at transit choke points.","red",{"unrest","crowd","transit"}),
			T("Rapid de-escalation team embedded in a contentious council session; staged ‘clarity interval’ to prevent walkouts.","red",{"governance","mediation"}),
		}
	},

	{key="OAM", name="Order of Aurora Mystica",
		link=orderLink("Eos (Reformed Stripping Path)","Order_of_Aurora_Mystica","Order of Aurora Mystica"),
		tasks={
			T("Sunrise reconciliation rite for competing guilds; signed harmony pledges archived by Temple Bank clerks.","green",{"mediation","guild"}),
			T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.","green",{"omen","public"}),
			T("Neighborhood ‘first-light’ counseling circle for strained households; referrals placed with shrine caretakers.","green",{"care","mediation"}),
			T("Public blessings for early-shift workers at canal gates; accident-prevention guidance posted.","green",{"public","infrastructure"}),
			T("Aurora choir service at hospital ward; morale uplift paired with practical volunteer roster coordination.","green",{"health","service"}),
			T("Targeted rumor-clearing procession after a false ‘divine abandonment’ claim; witness statements collected.","yellow",{"rumor","investigation"}),
			T("Crowd choreography support at a dawn festival to prevent stampedes and reduce pickpocket opportunities.","yellow",{"crowd","festival"}),
			T("Joint patrol with civic stewards through tunnel districts; wayfinding, lighting, and intimidation checks.","yellow",{"security","infrastructure"}),
			T("Emergency reconciliation summit after a street brawl between factions; formal truce terms read aloud.","red",{"unrest","mediation"}),
			T("Crisis dawn vigil after violent night incident; grief support, calm messaging, and ‘no retaliation’ pledge drive.","red",{"crisis","public","grief"}),
			T("Stabilization briefings for district clerks following audit findings; correction plan countersigned.","yellow",{"audit","governance"}),
		}
	},

	{key="CIC", name="Conclave Illuminara",
		link=orderLink("Tarsica (Reformed Stripping Path)","Conclave_Illuminara","Conclave Illuminara"),
		tasks={
			T("Night-sky lecture and calendar correction posted to calm public anxiety about omens and eclipses.","green",{"education","omen"}),
			T("Quiet meditation ward established at a busy shrine; controlled breathing protocols taught to visitors.","green",{"care","public"}),
			T("Lantern-route inspection for canal patrols; reflective markers placed to reduce night accidents.","green",{"infrastructure","transit"}),
			T("Dream-space audit: mirror pools cleaned, offerings catalogued, and safety signage posted.","green",{"shrine","audit"}),
			T("Star-chart reading for civic council; guidance recorded for later reference by scribes.","green",{"governance","education"}),
			T("Investigation of counterfeit ‘moon tokens’ being sold to pilgrims; vendor interviews and seizure log.","yellow",{"investigation","trade"}),
			T("Controlled night gathering after panic rumor; calm-litany enforced, dispersal plan coordinated with stewards.","yellow",{"rumor","crowd"}),
			T("Escort for a sensitive nocturnal rite during unrest; perimeter observers posted.","red",{"security","unrest"}),
			T("Emergency anti-panic messaging after a ‘demonic sign’ claim; script distributed to shrine speakers.","red",{"crisis","rumor"}),
			T("Coordinated blackout safety drill (lanterns, guides, canal warning ropes) during storm season.","yellow",{"infrastructure","safety"}),
		}
	},

	{key="SCD", name="Sanctum Delphica",
		link=orderLink("Sisera (Reformed Stripping Path)","The_Cult_of_Sanctum_Delphica","Sanctum Delphica"),
		tasks={
			T("Archive custody inspection: artifact seals verified, witness ledger updated, and storage humidity checked.","green",{"archive","audit"}),
			T("Scribe training on doctrinal custody chain; convoy paperwork rehearsed for shrine transfers.","green",{"education","logistics"}),
			T("Pilgrim etiquette workshop at a library shrine; damage prevention and quiet-zone enforcement.","green",{"public","education"}),
			T("Indexing confiscated pamphlets for signature motifs; forwarded to Temple Bank clerks 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, signage, and rumor suppression around a restricted ward.","yellow",{"security","rumor"}),
			T("Escort a sensitive artifact transfer under heightened risk; perimeter rites and route denial markers deployed.","red",{"security","logistics"}),
			T("Emergency custody lockdown after attempted breach; rapid relocation plan activated.","red",{"crisis","security","archive"}),
			T("Court hearing preparation for accused desecrators; evidence packet assembled with scribal counsel.","yellow",{"governance","audit"}),
			T("Night watch rotation at archive stacks following arson threat; coordination with civic guards.","red",{"security","unrest"}),
		}
	},

	{key="SCV", name="Sanctum Vitalis",
		link=orderLink("Plateau (Reformed Stripping Path)","Sanctum_Vitalis","Sanctum Vitalis"),
		tasks={
			T("Clinic resupply run: bandage, tonic, and clean-water amphorae inventory reconciled with Temple Bank vouchers.","green",{"health","logistics"}),
			T("Water-quality checks at canal taps; chlorination guidance posted for household stewards.","green",{"health","infrastructure"}),
			T("School hygiene lecture (handwashing, food safety) paired with gentle blessing service.","green",{"health","education"}),
			T("Routine fever screening at a market gate during festival season; referral slips issued.","green",{"health","public"}),
			T("Temporary clinic tent established near shrine square; triage lines marked and staffed.","yellow",{"health","crowd"}),
			T("Contamination rumor assessment: test kits deployed to prove/disprove claims and prevent panic.","yellow",{"health","rumor"}),
			T("Mass casualty support after an accident; triage, referral, and grief counseling coordination.","red",{"crisis","health"}),
			T("Quarantine support in a dormitory ward; supplies custody and compliance checks.","red",{"crisis","health","audit"}),
			T("Joint training with local stewards on heat illness prevention and hydration discipline.","yellow",{"health","public"}),
			T("Night clinic staffing for unrest injuries; neutral triage posture announced to reduce retaliation violence.","red",{"unrest","health"}),
		}
	},

	{key="PHA", name="Pharmacon Sect",
		link=orderLink("Styx (Reformed Stripping Path)","The_Pharmacon_Sect","Pharmacon Sect"),
		tasks={
			T("Dispensary audit: dosage seals verified; controlled compounds logged and countersigned.","green",{"health","audit"}),
			T("Field compounding session with local 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 engineering consult at a 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("Emergency antidote preparation after suspected poisoning event; vendor supply chain traced.","red",{"crisis","investigation","health"}),
			T("Outbreak response: isolation guidance, symptom screening, and clinic routing control at transit nodes.","red",{"crisis","health","public"}),
			T("Rumor suppression briefing about ‘cursed medicine’; evidence packet and calm script issued.","yellow",{"rumor","public"}),
			T("Hospital ward reinforcement for Eidolan exposure aftershock cases; monitoring schedule created.","red",{"crisis","health"}),
			T("Post-incident debrief with Temple Aprobelle on panic dynamics; messaging updated for clinics.","yellow",{"analysis","rumor"}),
		}
	},

	{key="VEG", 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 with steward crews.","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("Emergency flood relief coordination: sandbag lines, food staging, and shelter support.","red",{"crisis","relief"}),
			T("Brushfire risk patrol in dry districts; controlled burns prohibited and notices issued.","yellow",{"fire","public"}),
			T("Post-storm debris clearance with shrine volunteers; hazard markings placed.","yellow",{"safety","infrastructure"}),
			T("Disease vector mitigation in swamp edges; standing water treated and guidance posted.","yellow",{"health","infrastructure"}),
			T("Crisis canal breach response: temporary closure, reroute of water flow, and crowd calming.","red",{"crisis","infrastructure","crowd"}),
		}
	},

	{key="CICL", name="Court of the Ironclad",
		link=orderLink("Fenrir (Reformed Stripping Path)","Court_of_the_Ironclad","Court of the Ironclad"),
		tasks={
			T("Strength-discipline training for youth patrol volunteers; nonviolent restraint emphasized.","green",{"education","security"}),
			T("Public order presence at shrine steps; crowd etiquette enforced with calm posture.","green",{"security","public"}),
			T("Workshop on oath-keeping and duty; pledge cards sealed for civic stewards.","green",{"governance","education"}),
			T("Route hardening walk: identify vulnerable alleys, broken lamps, and unsafe crossings.","yellow",{"security","infrastructure"}),
			T("Contraband confiscation support at a market stall cluster; evidence logged for Temple Bank clerks.","yellow",{"security","investigation"}),
			T("High-risk escort for a witness leaving a hearing; safehouse transfer arranged.","red",{"security","crisis"}),
			T("Emergency deterrence patrol after riot flare; separation lines and dispersal corridor opened.","red",{"unrest","crowd"}),
			T("Night watch on canal bridges after sabotage attempt; checkpoint discipline enforced.","yellow",{"security","infrastructure"}),
			T("Crisis reinforcement at a hospital gate during panic surge; access and queue discipline established.","red",{"crisis","security","health"}),
			T("Post-incident intimidation review; complaints investigated and corrective discipline applied.","yellow",{"audit","governance"}),
		}
	},

	{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 minor scare; parents briefed on calm routines.","green",{"care","education"}),
			T("Public ‘sleep hygiene’ teaching for shift workers; rest scheduling guidance posted.","green",{"health","education"}),
			T("Rumor mapping exercise: identify which dreams are spreading a harmful narrative; corrective story deployed.","yellow",{"rumor","analysis"}),
			T("Witness interviews after a trance-gathering complaint; compliance conditions issued.","yellow",{"audit","public"}),
			T("Containment support for mass panic dream episode; calm stations, water, and supervised rest zones established.","red",{"crisis","crowd"}),
			T("Emergency counseling for families after death; grief rites paired with practical aid referrals.","red",{"grief","crisis"}),
			T("Nighttime crowd calming near a transit node; chants regulated to prevent stampede behavior.","yellow",{"crowd","transit"}),
			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="ASS", name="Azure Sentinel Sect",
		link=orderLink("Aether (Reformed Stripping Path)","Azure_Sentinel_Sect","Azure Sentinel Sect"),
		tasks={
			T("Waterfront perimeter walk and ‘blue line’ marking refresh; safety rope checks in canal districts.","green",{"infrastructure","safety"}),
			T("Port-house briefing on safe embarkation and baggage rules; theft prevention guidance posted.","green",{"public","transit"}),
			T("Storm-drain inspection with civil works; blockages reported and cleared.","green",{"infrastructure","service"}),
			T("Escort for medical supplies across canal bridges; accountability signatures obtained.","yellow",{"logistics","security"}),
			T("Harbor rumor suppression: clarify false ‘quarantine’ claims to prevent stampede and hoarding.","yellow",{"rumor","public"}),
			T("Emergency flood response: evacuation guidance at waterfront shrines; calm channels maintained.","red",{"crisis","relief"}),
			T("Search-and-rescue standby after storm warnings; muster roster and equipment check.","yellow",{"safety","crisis"}),
			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"}),
		}
	},

	-- NEW: Temple Aprobelle
	{key="TAP", name="Temple Aprobelle",
		link="[[Temple_Aprobelle|Temple Aprobelle]]",
		tasks={
			T("Festival hygiene audit: vendor protocols verified (ladle rotation, water handling, waste control); seals issued to compliant stalls.","green",{"health","audit","festival","public"}),
			T("Rumor-control bulletin cycle: shrine-leader briefing + posted clarifications to prevent panic narratives from hardening.","green",{"rumor","public","service"}),
			T("Grief-cadre home visits after a recent accident; de-escalation coaching for relatives and neighborhood stewards.","green",{"grief","mediation","service"}),
			T("School immunization coordination: clinic schedule posted, consent ledgers reconciled, and reassurance rites conducted.","green",{"health","school","coordination"}),
			T("Clinic-queue stabilization drill: entry metering, calm-litany, and conflict prevention training for volunteers.","yellow",{"queue","crowd","health","order"}),
			T("Loyalty and behavior audit of a district office after complaint surge; corrective training issued with Temple Bank clerk witness.","yellow",{"audit","governance","compliance"}),
			T("Anomaly-response dispatch to a shrine ward after ‘demonic contamination’ claims; care provision paired with narrative discipline.","yellow",{"anomaly","rumor","public","health"}),
			T("Pattern-analysis intake: confiscated schism texts and recordings logged, indexed, and forwarded for doctrinal signature correlation.","yellow",{"analysis","schism","security","archive"}),
			T("Transit node stabilization after crowd surge: calm stations, signage, and steward handoffs established.","yellow",{"transit","crowd","stabilization"}),
			T("Silencing-cadre activated after panic flare; controlled messaging + visible care stations deployed under escort.","red",{"panic","rumor","stabilization"}),
			T("Emergency stabilization in evacuation/fever center: calming rites, grief triage, and rumor suppression until order restored.","red",{"crisis","health","crowd","stabilization"}),
			T("Post-incident narrative repair: rumor sources mapped, apology ceremonies arranged, and retaliation prevention pledges collected.","yellow",{"analysis","mediation","public"}),
		}
	},
}

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

--------------------------------------------------------------------------------
-- 8) CITY PROFILES (tags + order bias weights)
--------------------------------------------------------------------------------
local cityProfiles = {
	["Vaeringheim"] = {tags={"capital","port","canals"}, weights={OSI=2,OAM=2,CIC=2,SCD=2,SCV=2,ASS=2,TAP=3}},
	["Somniumpolis"] = {tags={"wetlands","dream-space","panic-risk"}, weights={RNB=3,SCV=2,PHA=2,TAP=3,OSI=1}},
	["Luminaria"] = {tags={"dawn","tunnels","enlightenment"}, weights={OAM=3,CIC=2,OSI=2,TAP=2}},
	["Lunalis Sancta"] = {tags={"libraries","court","ritual"}, weights={SCD=3,CIC=2,TAP=2,OSI=1}},
	["Delphica"] = {tags={"archive","custody"}, weights={SCD=3,TAP=2,CIC=1}},
	["Acheron"] = {tags={"canyonlands","medicine"}, weights={PHA=3,SCV=2,TAP=1,CICL=1}},
	["Thermosalem"] = {tags={"springs","crowds"}, weights={SCV=2,PHA=2,TAP=2,ASS=1}},
	["Pyralis"] = {tags={"festival","risk"}, weights={SCV=2,TAP=2,ASS=2,OAM=1}},
	["Keybir-Aviv"] = {tags={"harbor","compliance"}, weights={ASS=3,TAP=2,OSI=1,CIC=1}},
	["Tel-Amin"] = {tags={"coastal","guilds"}, weights={OAM=2,TAP=2,ASS=1,SCV=1}},
	["Diamandis"] = {tags={"festival","cemeteries"}, weights={TAP=2,SCV=2,OAM=1,RNB=1}},
	["Lewisburg"] = {tags={"port-node","interdiction"}, weights={ASS=2,CICL=2,TAP=2,OSI=1}},
	["Lykopolis"] = {tags={"trade","crowds"}, weights={ASS=2,TAP=2,CICL=1,OSI=1}},
	["Tonar"] = {tags={"occupation","canals"}, weights={SCD=2,SCV=2,VEG=2,TAP=1,CICL=1}},
	["Bashkim"] = {tags={"steppe","irrigation"}, weights={VEG=3,SCV=2,TAP=1,CICL=1}},
	["Ourid"] = {tags={"river","irrigation"}, weights={VEG=3,SCV=2,TAP=1}},
	["Ardclach"] = {tags={"north","cold"}, weights={ASS=2,SCV=2,TAP=1,CIC=1}},
	["Riddersborg"] = {tags={"north","order"}, weights={CICL=2,OSI=2,TAP=1,SCV=1}},
}

local function getProfile(cityName)
	local p0 = cityProfiles[cityName]
	if p0 then return p0 end
	return {tags={"standard"}, weights={OSI=1,OAM=1,CIC=1,SCV=1,SCD=1,TAP=1}}
end

--------------------------------------------------------------------------------
-- 9) OPTIONAL: ECON SIGNALS (safe fallback)
-- Returns a map: econSignals[cityName] = {summary="...", tags={"industry","agri"}}
--------------------------------------------------------------------------------
local function getEconSignals()
	local title = mw.title.new("General Port of Lake Morovia")
	if not title then return {} end
	local content = safeTitleContent(title.fullText)
	if not content or content == "" then return {} end

	-- Lightweight heuristic: find occurrences of city names near “Company” blocks.
	local signals = {}
	local text = normalizeSpaces(content)

	for _, c in ipairs(cityOrder) do
		local n = c.name
		local hits = 0
		-- count mentions as proxy for “activity”
		for _ in text:gmatch(n:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])","%%%1")) 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), tags={"port"}}
		end
	end
	return signals
end

local function econSummary(sig)
	if not sig then return "" end
	return sig.summary or ""
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*")
			-- expected: Date || City || Event || Urgency
			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

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

--------------------------------------------------------------------------------
-- 11) WX (Ops Alert) integration from 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 {}

	-- Only Orange/Red force severity bumps (Yellow stays informational)
	if lvl == 2 and sev == "green" then sev = "yellow" end
	if lvl >= 3 then sev = "red" end

	-- Hard constraints should push to at least yellow
	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 -- only Orange/Red appends operational language

	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 .. " Queue stabilization at clinics/relief points; rumor containment bulletins posted; shrine-district patrol lanes marked."
	elseif cond == "Blizzard" or cond == "Snow" then
		return base .. " Warming-shelter checks; fuel and medicine custody verification; travel guidance posted at shrine gates."
	elseif cond == "Fog" then
		return base .. " Navigation warnings and crowd guidance in canal/port corridors; panic narratives suppressed early."
	elseif cond == "Dust" then
		return base .. " Mask discipline, indoor refuge advisories, and vendor hygiene enforcement prioritized."
	else
		return base .. " Welfare checks and public messaging prioritized."
	end
end

--------------------------------------------------------------------------------
-- 12) SEVERITY + ORDER/TASK CHOICE
--------------------------------------------------------------------------------
local function inferSeverity(cityTier, taskSev, threatUrg)
	-- base from task text classification
	local sev = tostring(taskSev or "green")
	-- major cities run “hotter”
	if cityTier == "major" and sev == "green" then sev = "yellow" end
	-- threat override
	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, profile, econSig, threat)
	local weighted = {}
	-- start with profile weights
	for key, w in pairs(profile.weights or {}) do
		weighted[#weighted+1] = {key=key, w=tonumber(w) or 0}
	end

	-- econ bump: if port-linked, bias to compliance/stability orders
	if econSig and econSig.tags and econSig.tags[1] == "port" then
		for _, it in ipairs(weighted) do
			if it.key == "ASS" or it.key == "TAP" or it.key == "OSI" then it.w = it.w + 1 end
		end
	end

	-- threat bump: prefer Aprobelle / security / medical on threat days
	if threat and urgencyScore(threat.urg) >= 2 then
		for _, it in ipairs(weighted) do
			if it.key == "TAP" or it.key == "CICL" or it.key == "SCV" or it.key == "PHA" then it.w = it.w + 2 end
		end
	end

	local picked = pickWeighted(nextInt, weighted)
	for _, o in ipairs(orders) do
		if o.key == picked.key then return o end
	end
	return orders[1]
end

local function pickTask(nextInt, o, sev)
	-- bias toward sev-matching tasks
	local pool = {}
	for _, t in ipairs(o.tasks or {}) 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
		pool[#pool+1] = {t=t, w=w}
	end
	local chosen = pickWeighted(nextInt, pool).t
	return chosen
end

--------------------------------------------------------------------------------
-- 13) TEMPLE BANK NOTES (flavor + compliance)
--------------------------------------------------------------------------------
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 for lamps/rail signage; labor scheduled at off-peak hours.",
	"Custody ledger updated; artifacts/medicines sealed and moved to secure storage.",
	"Training memo filed; corrective instruction issued to volunteers for queue discipline.",
}

--------------------------------------------------------------------------------
-- 14) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
	local cityName = c.name
	local cityRegion = c.region
	local tier = c.tier

	local seed = hash31(string.format("mops:%d:%03d:%s", di.year, di.dayOfYear, cityName))
	local nextInt = lcg(seed)

	local profile = getProfile(cityName)
	local sig = econSignals and econSignals[cityName] or nil
	local th = includeThreat and threatMap and threatMap[cityName] or nil

	-- choose order, then task
	local o = pickOrder(nextInt, profile, sig, th)
	local task = pickTask(nextInt, o, "yellow") -- starting bias; severity may shift below

	local threatUrg = th and th.urg or ""
	local sev = inferSeverity(tier, task.sev, threatUrg)

	-- WX: ops badge + severity bump + overlay (Orange/Red)
	local opsBadge, wxData = opsCell(cityName, di.year, di.dayOfYear)
	sev = bumpSevWeather(sev, wxData)
	local taskText = task.text .. weatherTaskOverlay(wxData)

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

	local bank = pickFromList(nextInt, bankNotes, nil)

	local tags = {}
	for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
	for _, t in ipairs(task.tags or {}) do tags[#tags+1] = t end
	if th and th.event and th.event ~= "" then tags[#tags+1] = "threat-watch" end

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

	return {
		style = sevStyle(sev),
		date = di.text,
		region = cityRegion,
		city = string.format("%s %s", getCityFlagMarkup(cityName), cityLink(cityName)),
		ops = opsBadge, -- NEW
		order = getOrderIconMarkup(o.name) .. o.link,
		unit = string.format("%s (%d) [%s]", unitName, unitMeta.size, unitCode),
		task = taskText,
		tags = joinTags(tags),
		econ = (showEcon and sig) and econSummary(sig) or "",
		bank = bank,
		threat = threatText,
		threatUrg = threatUrg,
		sev = sev,
	}
end

--------------------------------------------------------------------------------
-- 15) 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()
	local econSignals = getEconSignals()

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

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")

	local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Ops Alert !! Cult Order !! 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, econSignals, threatMap, includeThreat, showEcon)
			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 (Green/Yellow/Orange/Red) and aligns with War League / Hatch posture.")
		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

--------------------------------------------------------------------------------
-- 16) 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 econSignals = getEconSignals()

	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 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}, econSignals, threatMap, includeThreat, showEcon)

	local out = {}
	table.insert(out, "{| class='wikitable'")
	local header = "! Date !! City !! Region !! Ops Alert !! Cult Order !! 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

--------------------------------------------------------------------------------
-- 17) 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 econSignals = getEconSignals()

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

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	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, econSignals, threatMap, includeThreat, showEcon)
		-- only keep rows where chosen order matches the wanted one
		if row.order:find(wantedOrder.link, 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