Module:MissionaryDailyOpsBVR: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Line 1: Line 1:
-- Module:MissionaryDailyOpsBVR
-- Module:MissionaryDailyOpsBVR (REWRITE)
-- Daily (PSSC-calendar keyed) missionary tasking schedule for the Temple Bank of the Reformed Stripping Path.
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Deterministic per-day output (no math.random / no os.time seeding inside this module).
-- Deterministic output (no math.random / no os.time seeding).
--
--
-- Color coding: row background reflects mission urgency (Green/Yellow/Red),
-- NEW IN THIS REWRITE:
-- matching the palette used in Module:HatchMinistryDailyOps.
--  • City profiles (culture/spiritual tags + per-city order bias)
-- • Economic signals pulled from General Port of Lake Morovia "Company Table"
--    (counts by sector + sample HQ companies) and used to bias order + task.
--  • Optional Threat Watch integration from Module:ReligiousEvents (opt-in).
--
--
-- Optional integration:
-- Usage:
--  |includeThreat=yes  -> imports Module:ReligiousEvents and parses today's table once,
--                          using it to bump severity and bias order selection (opt-in only).
--
-- Usage examples:
--  {{#invoke:MissionaryDailyOpsBVR|makeTable}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}}
--  {{#invoke:MissionaryDailyOpsBVR|makeTable|showEcon=yes}}
--  {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}}
--  {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}}
--  {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}}
--  {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}}
Line 20: Line 20:


local cal = require('Module:BassaridianCalendar')
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 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
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("''", "")
-- HTML entities are usually already decoded by MediaWiki; keep safe
return trim(s)
end
local function containsAny(s, words)
s = (s or ""):lower()
for _, w in ipairs(words) do
if s:find(w, 1, true) then return true end
end
return false
end


local DAYS_IN_YEAR = 183
local function joinTags(tags)
if not tags or #tags == 0 then return "" end
local out = {}
for _, t in ipairs(tags) do out[#out+1] = t end
return table.concat(out, ", ")
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 0) UTIL: calendar parsing (mirrors HatchMinistryDailyOps)
-- 1) CALENDAR PARSING (mirrors your HatchMinistryDailyOps approach)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function parseCalendarString(s)
local function parseCalendarString(s)
Line 71: Line 121:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 1) DETERMINISTIC HASH + RNG (NO bit32, NO math.random)
-- 2) DETERMINISTIC RNG (NO bit32, NO math.random)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function hash31(str)
local function hash31(str)
Line 107: Line 157:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 2) COLOR CODING (same palette logic as HatchMinistryDailyOps)
-- 3) COLOR CODING (Hatch-style)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function sevStyle(sev)
local function sevStyle(sev)
Line 119: Line 169:
end
end


local function bumpSev(sev)
--------------------------------------------------------------------------------
if sev == "green" then return "yellow" end
-- 4) CITY NETWORK (canonical list)
if sev == "yellow" then return "red" end
--------------------------------------------------------------------------------
return "red"
local function normalizeForFlag(name)
return (name or "")
:gsub("[%s%-’]", "")
:gsub("[Áá]", "A"):gsub("[Éé]", "E")
:gsub("[Íí]", "I"):gsub("[Óó]", "O")
:gsub("[Úú]", "U"):gsub("[Ýý]", "Y")
end
end


--------------------------------------------------------------------------------
-- 3) CITY NETWORK (canonical list page)
--------------------------------------------------------------------------------
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, grouped for filtering
-- 54 canonical cities; tier used for "major city" spiritual baseline.
local cityOrder = {
local cityOrder = {
-- Core majors
-- Core majors
{name="Vaeringheim",      region="Core"},
{name="Vaeringheim",      region="Core", tier="major"},
{name="Luminaria",        region="Core"},
{name="Luminaria",        region="Core", tier="major"},
{name="Serena",          region="Core"},
{name="Serena",          region="Core", tier="major"},
{name="Pyralis",          region="Core"},
{name="Pyralis",          region="Core", tier="major"},
{name="Symphonara",      region="Core"},
{name="Symphonara",      region="Core", tier="major"},
{name="Aurelia",          region="Core"},
{name="Aurelia",          region="Core", tier="major"},
{name="Somniumpolis",    region="Core"},
{name="Somniumpolis",    region="Core", tier="major"},
{name="Nexa",            region="Core"},
{name="Nexa",            region="Core", tier="major"},
{name="Lunalis Sancta",  region="Core"},
{name="Lunalis Sancta",  region="Core", tier="major"},
{name="Sylvapolis",      region="Core"},
{name="Sylvapolis",      region="Core", tier="major"},


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


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


-- New South Jangsong – major
-- New South Jangsong
{name="Skýrophos",        region="New South Jangsong"},
{name="Skýrophos",        region="New South Jangsong", tier="major"},
{name="Bjornopolis",      region="New South Jangsong"},
{name="Bjornopolis",      region="New South Jangsong", tier="major"},
{name="Aegirheim",        region="New South Jangsong"},
{name="Aegirheim",        region="New South Jangsong", tier="major"},
{name="Norsolyra",        region="New South Jangsong"},
{name="Norsolyra",        region="New South Jangsong", tier="major"},
{name="Thorsalon",        region="New South Jangsong"},
{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"},


-- New South Jangsong – minor
-- Haifan Bassaridia
{name="Pelagia",         region="New South Jangsong"},
{name="Keybir-Aviv",     region="Haifan Bassaridia", tier="major"},
{name="Myrene",          region="New South Jangsong"},
{name="Tel-Amin",         region="Haifan Bassaridia", tier="major"},
{name="Thyrea",           region="New South Jangsong"},
{name="Diamandis",       region="Haifan Bassaridia", tier="major"},
{name="Ephyra",          region="New South Jangsong"},
{name="Jogi",             region="Haifan Bassaridia", tier="major"},
{name="Halicarn",         region="New South Jangsong"},
{name="Lewisburg",       region="Haifan Bassaridia", tier="major"},
 
{name="Thermosalem",      region="Haifan Bassaridia", tier="minor"},
-- Haifan Bassaridia – major
{name="Akróstadium",      region="Haifan Bassaridia", tier="minor"},
{name="Keybir-Aviv",     region="Haifan Bassaridia"},
{name="Sufriya",          region="Haifan Bassaridia", tier="minor"},
{name="Tel-Amin",         region="Haifan Bassaridia"},
{name="Lykopolis",        region="Haifan Bassaridia", tier="minor"},
{name="Diamandis",        region="Haifan Bassaridia"},
{name="Jogi",             region="Haifan Bassaridia"},
{name="Lewisburg",        region="Haifan Bassaridia"},
 
-- Haifan Bassaridia – minor
{name="Thermosalem",      region="Haifan Bassaridia"},
{name="Akróstadium",      region="Haifan Bassaridia"},
{name="Sufriya",          region="Haifan Bassaridia"},
{name="Lykopolis",        region="Haifan Bassaridia"},


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


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


-- Caledonian dependencies
-- Caledonia
{name="Fanghorn",        region="Caledonia"},
{name="Fanghorn",        region="Caledonia", tier="major"},
{name="Eikbu",            region="Caledonia"},
{name="Eikbu",            region="Caledonia", tier="major"},
{name="Galvø",            region="Caledonia"},
{name="Galvø",            region="Caledonia", tier="major"},
{name="Sårensby",        region="Caledonia"},
{name="Sårensby",        region="Caledonia", tier="minor"},
{name="Slevik",          region="Caledonia"},
{name="Slevik",          region="Caledonia", tier="minor"},
{name="Sjøsborg",        region="Caledonia"},
{name="Sjøsborg",        region="Caledonia", tier="minor"},
{name="Storesund",        region="Caledonia"},
{name="Storesund",        region="Caledonia", tier="minor"},
{name="Notranskja",      region="Caledonia"},
{name="Notranskja",      region="Caledonia", tier="minor"},
{name="Kaledonija",      region="Caledonia"},
{name="Kaledonija",      region="Caledonia", tier="minor"},
}
}
local citySet = {}
local cityFlag = {}
for _, c in ipairs(cityOrder) do
citySet[c.name] = true
cityFlag[c.name] = string.format("[[File:%sFlag.png|20px]]", normalizeForFlag(c.name))
end
-- explicit override used elsewhere on the wiki
cityFlag["Skýrophos"] = "[[File:SkyrophosFlag.png|20px]]"


local function regionPass(filter, cityRegion)
local function regionPass(filter, cityRegion)
if not filter or filter == "" or filter == "All" then return true end
if not filter or filter == "" or filter == "All" then return true end
-- Allow a few convenient aliases
if filter == "Core" then return cityRegion == "Core" end
if filter == "Core" then return cityRegion == "Core" end
if filter == "NSJ" then return cityRegion == "New South Jangsong" end
if filter == "NSJ" then return cityRegion == "New South Jangsong" end
Line 220: Line 276:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 4) MISSIONARY ORGANIZATION (sizes align to RSP org table)
-- 5) CORRIDORS / ROLE HINTS (from the city-network page; hard-coded for stability)
--------------------------------------------------------------------------------
-- Rail corridor cities listed on the city network overview.
local RAIL = {
["Lykopolis"]=true, ["Lewisburg"]=true, ["Bashkim"]=true, ["Somniumpolis"]=true,
["Erythros"]=true, ["Vaeringheim"]=true, ["Symphonara"]=true, ["Skýrophos"]=true,
["Norsolyra"]=true, ["Pelagia"]=true, ["Riddersborg"]=true, ["Sårensby"]=true,
}
-- Ferry links noted: Jogi + Ardclach
local FERRY = {["Jogi"]=true, ["Ardclach"]=true}
 
--------------------------------------------------------------------------------
-- 6) MISSIONARY ORGANIZATION (sizes)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local org = {
local org = {
Line 242: Line 310:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 5) CULT/ORDER PROFILES + TASK LIBRARIES
-- 7) ORDERS + TASKS (now with tags to match city profile/economy)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
local function orderLink(pageTitle, anchor, label)
Line 248: Line 316:
end
end


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


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


-- Build index maps
--------------------------------------------------------------------------------
local orderByName = {}
-- 9) ECONOMIC SIGNALS: parse "Company Table" on General Port page
local orderNames = {}
--------------------------------------------------------------------------------
for i, o in ipairs(orders) do
local econCache = nil
orderByName[o.name] = o
 
orderNames[i] = o.name
local function sectorOf(industryCell)
local s = (industryCell or ""):lower()
if s:find("agric", 1, true) then return "Agricultural" end
if s:find("service", 1, true) then return "Service" end
if s:find("manufact", 1, true) or s:find("mining", 1, true) or s:find("energy", 1, true) then
return "Industry"
end
-- fallback
return "Other"
end
 
local function extractCompanyName(cell)
-- Try '''Name''' first
local n = cell:match("'''(.-)'''")
if n and n ~= "" then return stripWiki(n) end
-- Try bold markdown-like **Name** (some pages render that way)
n = cell:match("%*%*(.-)%*%*")
if n and n ~= "" then return stripWiki(n) end
-- fallback: strip wiki and truncate
n = stripWiki(cell)
if #n > 80 then n = n:sub(1, 80) .. "…" end
return n
end
 
local function extractHQ(cell)
local s = stripWiki(cell)
if s == "" then return nil end
-- Ignore obvious non-city HQs
if s:lower():find("fleet", 1, true) then return nil end
if s:lower():find("no fixed", 1, true) then return nil end
-- Many HQ cells are just the city name after stripWiki.
-- Some may include extra notes; take first clause.
s = s:gsub("%s*;%s*.*$", "")
s = s:gsub("%s*%b()%s*", " ")
s = normalizeSpaces(s)
s = trim(s)
if citySet[s] then return s end
-- Sometimes HQ may be "Vaeringheim (General Port)" etc.
for cname, _ in pairs(citySet) do
if s == cname then return cname end
end
return nil
end
 
local function parseCompanyTable(wikitext)
local signals = {}
for cname, _ in pairs(citySet) do
signals[cname] = {Agricultural=0, Service=0, Industry=0, Other=0, samples={}}
end
 
if type(wikitext) ~= "string" then return signals end
 
-- Find "Company Table" section and first wikitable after it
local startPos = wikitext:find("====%s*Company Table%s*====")
if not startPos then
startPos = wikitext:find("####%s*Company Table")
end
if not startPos then
-- If heading not found, bail with empty signals
return signals
end
 
local tablePos = wikitext:find("{|", startPos)
if not tablePos then return signals end
local endPos = wikitext:find("|}", tablePos)
if not endPos then return signals end
 
local tableText = wikitext:sub(tablePos, endPos + 1)
 
-- Split into rows by "|-"
local rows = {}
for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do
rows[#rows+1] = chunk
end
-- Also capture last row before "|}" if not matched by above pattern
local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}")
if lastChunk then rows[#rows+1] = lastChunk end
 
for _, row in ipairs(rows) do
-- ignore header rows that start with "!"
if not row:match("^%s*!") then
-- collapse row to a single line; cells separated by "||"
local line = normalizeSpaces(row:gsub("\n", " "))
-- remove leading single "|"
line = line:gsub("^%s*|%s*", "")
local cells = {}
for cell in line:gmatch("([^|]-)%s*||%s*") do
cells[#cells+1] = trim(cell)
end
-- capture last cell after final "||"
local tail = line:match("||%s*([^|].-)%s*$")
if tail and tail ~= "" then
-- only add if we didn't already (gmatch above may omit last)
if #cells == 0 or cells[#cells] ~= trim(tail) then
cells[#cells+1] = trim(tail)
end
end
 
-- Expect 7 columns; be defensive
-- Industry cell usually around 5; HQ is last.
local industryCell = ""
for _, c in ipairs(cells) do
if containsAny(c, {"agric", "service", "manufact", "mining", "energy"}) then
industryCell = c
break
end
end
local sector = sectorOf(industryCell)
 
local hqCell = cells[#cells] or ""
local hq = extractHQ(hqCell)
if hq and signals[hq] then
signals[hq][sector] = signals[hq][sector] + 1
-- sample company name from the "Name" column (often 3rd cell)
local nameCell = cells[3] or cells[2] or cells[1] or ""
local companyName = extractCompanyName(nameCell)
if companyName ~= "" and #signals[hq].samples < 2 then
signals[hq].samples[#signals[hq].samples+1] = companyName
end
end
end
end
 
return signals
end
 
local function getEconSignals()
if econCache then return econCache end
local content = safeTitleContent("General Port of Lake Morovia")
econCache = parseCompanyTable(content)
return econCache
end
 
local function dominantSector(sig)
if not sig then return "Other" end
local best, bestN = "Other", -1
for _, k in ipairs({"Agricultural","Industry","Service","Other"}) do
local n = sig[k] or 0
if n > bestN then best, bestN = k, n end
end
return best
end
 
local function econTagFromSector(sec)
if sec == "Agricultural" then return "agri" end
if sec == "Industry" then return "industry" end
if sec == "Service" then return "service" end
return "mixed"
end
 
local function econSummary(sig)
if not sig then return "n/a" end
local a = sig.Agricultural or 0
local i = sig.Industry or 0
local s = sig.Service or 0
local o = sig.Other or 0
local dom = dominantSector(sig)
local samp = (sig.samples and #sig.samples > 0) and ("; e.g., " .. table.concat(sig.samples, ", ")) or ""
return string.format("%s (%d agri / %d ind / %d serv / %d other)%s", dom, a, i, s, o, samp)
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 6) OPTIONAL: pull today's ReligiousEvents table and map per-city urgency
-- 10) OPTIONAL: Threat Watch integration from Module:ReligiousEvents
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
local function parseReligiousEventsTable(wikitext)
local out = {}
local out = {}
if type(wikitext) ~= "string" then return out end
if type(wikitext) ~= "string" then return out end
-- Row pattern: | date || City || Event || Urgency
for line in wikitext:gmatch("[^\n]+") do
for line in wikitext:gmatch("[^\n]+") do
if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then
if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then
local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$")
local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$")
if city and urg then
if city and urg then
out[city] = {urg=urg, event=event or ""}
out[stripWiki(city)] = {urg=trim(urg), event=stripWiki(event or "")}
end
end
end
end
Line 559: Line 783:
local function threatSev(th)
local function threatSev(th)
if not th or not th.urg then return nil end
if not th or not th.urg then return nil end
local u = th.urg
local u = (th.urg or ""):upper()
if u == "N/A" or u == "LOW" then return "green" end
if u == "N/A" or u == "LOW" then return "green" end
if u == "MEDIUM" then return "yellow" end
if u == "MEDIUM" then return "yellow" end
Line 566: Line 790:
end
end


local function biasOrderByThreat(nextInt, th)
--------------------------------------------------------------------------------
if not th or type(th.event) ~= "string" then
-- 11) WEIGHTED ORDER PICK + TASK PICK (matches city tags + econ tags + corridor tags)
return pickFromList(nextInt, orderNames, nil)
--------------------------------------------------------------------------------
local function weightedPickKey(nextInt, weights)
-- weights: { [orderKey]=number }
local total = 0
for _, ok in ipairs(orderKeys) do
total = total + (weights[ok] or 0)
end
if total <= 0 then
return pickFromList(nextInt, orderKeys, nil)
end
local r = (nextInt() % total) + 1
local acc = 0
for _, ok in ipairs(orderKeys) do
acc = acc + (weights[ok] or 0)
if r <= acc then return ok end
end
return orderKeys[1]
end
 
local function hasTag(tagSet, t)
return tagSet[t] == true
end
 
local function buildTagSet(tags)
local set = {}
for _, t in ipairs(tags or {}) do set[t] = true end
return set
end
 
local function pickTaskForCity(nextInt, taskList, tagSet)
-- Prefer tasks whose tags intersect city tagset
local candidates = {}
for _, task in ipairs(taskList or {}) do
local ok = false
for _, tg in ipairs(task.tags or {}) do
if tagSet[tg] then ok = true break end
end
if ok then candidates[#candidates+1] = task end
end
if #candidates > 0 then
return pickFromList(nextInt, candidates, nil)
end
return pickFromList(nextInt, taskList, nil)
end
 
local function baseWeights()
local w = {}
for _, ok in ipairs(orderKeys) do w[ok] = 1 end
return w
end
 
local function applyWeights(w, add)
if not add then return end
for k, v in pairs(add) do
if orderByKey[k] then
w[k] = (w[k] or 0) + tonumber(v or 0)
end
end
end
 
local function biasFromEcon(w, econTag)
-- Econ bias is intentionally simple and legible
if econTag == "agri" then
applyWeights(w, {MVE=3,SYF=2,OAI=2,ASS=1})
elseif econTag == "industry" then
applyWeights(w, {TIL=3,EOF=2,IGA=2,CIC=1,RCA=1})
elseif econTag == "service" then
applyWeights(w, {GGS=3,HSA=2,RCA=2,CIL=1,CHS=1})
else
applyWeights(w, {CHS=1,HSA=1,ASS=1})
end
end
 
local function biasFromCorridor(w, isRail, isFerry)
if isRail then
applyWeights(w, {TIL=2,RCA=2,GGS=1,EOF=1,ASS=1})
end
if isFerry then
applyWeights(w, {ASS=2,GGS=1,RCA=1})
end
end
 
local function biasFromTier(w, tier)
if tier == "major" then
-- Major cities: public order + rumor control tends to matter more
applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1})
end
end
local e = th.event:lower()
-- A few simple keyword biases
if e:match("incendiary") or e:match("arson") or e:match("explosive") then return "Ignis Aeternum" end
if e:match("poison") or e:match("outbreak") or e:match("treated") or e:match("hygiene") then return "Sanctum Vitalis" end
if e:match("rumor") or e:match("panic") or e:match("dream") then return "Reverie Nebulous" end
if e:match("demonic") or e:match("exorc") or e:match("symbols") then return "Order of the Umbral Oracle" end
if e:match("offertory") or e:match("ledger") or e:match("counterfeit") or e:match("forged") then return "Guild of Golden Shadows" end
if e:match("barricade") or e:match("armed") or e:match("zealots") then return "Court of the Ironclad" end
if e:match("funeral") or e:match("cortège") or e:match("reliquary") then return "Mystery of the Stygian Veil" end
return pickFromList(nextInt, orderNames, nil)
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 7) TEMPLE BANK NOTES (deterministic snippets)
-- 12) TEMPLE BANK NOTES (deterministic snippets)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local bankNotes = {
local bankNotes = {
Line 594: Line 893:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 8) BUILD ONE CITY ROW (deterministic per city/day)
-- 13) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function buildCityRow(di, cityName, cityRegion, threatMap, includeThreat)
local function inferSeverity(cityProfile, econTag, taskSev, threatLevel)
local sev = taskSev or "green"
 
-- baseline from profile
if cityProfile and cityProfile.baseSev then
sev = cityProfile.baseSev
end
 
-- econ nudge (industry + service hubs get more "yellow" oversight more often)
if econTag == "industry" or econTag == "service" then
if sev == "green" then sev = "yellow" end
end
 
-- threat overrides
if threatLevel == "yellow" and sev == "green" then sev = "yellow" end
if threatLevel == "red" and sev ~= "red" then sev = "red" end
 
return sev
end
 
local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
local cityName, cityRegion, tier = c.name, c.region, c.tier
local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
local nextInt = lcg(hash31(seedStr))
local nextInt = lcg(hash31(seedStr))


local profile = cityProfiles[cityName] or {tags={}, weights=nil}
local tags = {}
-- profile tags
for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
-- corridor tags
if RAIL[cityName] then tags[#tags+1] = "rail" end
if FERRY[cityName] then tags[#tags+1] = "ferry" end
if tier == "major" then tags[#tags+1] = "dreamspace" end
-- econ tags
local sig = econSignals and econSignals[cityName] or nil
local dom = dominantSector(sig)
local econTag = econTagFromSector(dom)
tags[#tags+1] = econTag
local tagSet = buildTagSet(tags)
-- Threat map
local th = includeThreat and threatMap and threatMap[cityName] or nil
local th = includeThreat and threatMap and threatMap[cityName] or nil
local chosenOrderName
local tSev = threatSev(th)
if includeThreat and th then
chosenOrderName = biasOrderByThreat(nextInt, th)
else
-- Gentle geographic bias: some regions pick slightly different mixes
local bias = cityRegion .. ":" .. cityName .. ":" .. tostring(di.dayOfYear)
local idx = (hash31(bias) % #orderNames) + 1
chosenOrderName = orderNames[idx]
end


local o = orderByName[chosenOrderName] or orders[1]
-- Build weights
local w = baseWeights()
biasFromTier(w, tier)
biasFromEcon(w, econTag)
biasFromCorridor(w, RAIL[cityName], FERRY[cityName])
applyWeights(w, profile.weights)


-- Pick task; avoid repeating the same tag if possible (simple)
-- Pick order + task
local task = pickFromList(nextInt, o.tasks, nil)
local orderKey = weightedPickKey(nextInt, w)
local o = orderByKey[orderKey] or orders[1]
local task = pickTaskForCity(nextInt, o.tasks, tagSet)


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


-- Unit
local unitName = pickOrgUnit(nextInt, sev)
local unitName = pickOrgUnit(nextInt, sev)
local unitMeta = org[unitName]
local unitMeta = org[unitName]
Line 630: Line 964:
local headcount = unitMeta.size
local headcount = unitMeta.size


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


local threatText = ""
-- Threat display
local threatUrg = ""
local threatText, threatUrg = "", ""
if includeThreat and th then
if includeThreat and th then
threatText = th.event or ""
threatText = th.event or ""
Line 643: Line 978:
date = di.text,
date = di.text,
region = cityRegion,
region = cityRegion,
city = cityLink(cityName),
city = string.format("%s %s", (cityFlag[cityName] or ""), cityLink(cityName)),
order = o.link,
order = o.link,
unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode),
unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode),
task = task.text,
task = task.text,
tags = joinTags(tags),
econ = (showEcon and sig) and econSummary(sig) or "",
bank = bank,
bank = bank,
threat = threatText,
threat = threatText,
Line 655: Line 992:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 9) MAIN TABLE
-- 14) MAIN TABLE
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeTable(frame)
function p.makeTable(frame)
Line 662: Line 999:
local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local limit = tonumber(args.limit or "") -- optional max rows
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
local limit = tonumber(args.limit or "")


local di = getDateInfo()
local di = getDateInfo()
local econSignals = getEconSignals()


-- Optional: build threatMap once
-- Optional threat map
local threatMap = {}
local threatMap = {}
if includeThreat then
if includeThreat then
-- NOTE: ReligiousEvents.makeTable uses math.randomseed internally; opt-in only.
local ok, rel = pcall(require, "Module:ReligiousEvents")
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
if ok and rel and type(rel.makeTable) == "function" then
Line 679: Line 1,017:
local out = {}
local out = {}
table.insert(out, "{| class='wikitable sortable'")
table.insert(out, "{| class='wikitable sortable'")
table.insert(out, string.format("! style='white-space:nowrap;' | Date !! City !! Region !! Cult Order !! Unit !! Tasking !! Temple Bank Notes%s !! Severity",
local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Cult Order !! Unit !! Tasking"
includeThreat and " !! Threat Watch" or ""
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
local count = 0
for _, c in ipairs(cityOrder) do
for _, c in ipairs(cityOrder) do
if regionPass(regionFilter, c.region) then
if regionPass(regionFilter, c.region) then
local row = buildCityRow(di, c.name, c.region, threatMap, includeThreat)
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
table.insert(out, "|-" .. row.style)
table.insert(out, "|-" .. row.style)
local line = string.format("| %s || %s || %s || %s || %s || %s",
row.date, row.city, row.region, 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
if includeThreat then
local threatCell = row.threat ~= "" and (row.threat .. " (" .. (row.threatUrg or "") .. ")") or "No reports / n.a."
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s || %s",
line = line .. " || " .. threatCell
row.date, row.city, row.region, row.order, row.unit, row.task, row.bank, threatCell, row.sev
))
else
table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s",
row.date, row.city, row.region, row.order, row.unit, row.task, row.bank, row.sev
))
end
end
line = line .. " || " .. row.sev
table.insert(out, line)


count = count + 1
count = count + 1
Line 707: Line 1,053:


if showLegend then
if showLegend then
table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>")
table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%%;'>")
table.insert(out, "<b>Legend:</b> ")
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:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Routine rites / civic support&nbsp; ")
Line 715: Line 1,061:
end
end


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


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 10) CITY-ONLY VIEW
-- 15) SINGLE CITY VIEW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeCity(frame)
function p.makeCity(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
local city = tostring(args.city or "")
local city = tostring(args.city or "")
if city == "" then
if city == "" then return "''Error: city parameter required.''" end
return "''Error: city parameter required.''"
if not citySet[city] then return "''Error: unknown city (must match canonical name).''" end
end
 
args.limit = 1
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
-- We reuse makeTable logic by filtering manually
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
 
local di = getDateInfo()
local di = getDateInfo()
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local econSignals = getEconSignals()


local threatMap = {}
local threatMap = {}
Line 745: Line 1,091:
end
end


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


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


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


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 11) ORDER-ONLY VIEW (shows missions for that order across the city network)
-- 16) ORDER-ONLY VIEW
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.makeOrder(frame)
function p.makeOrder(frame)
Line 780: Line 1,131:
local wanted = tostring(args.order or "")
local wanted = tostring(args.order or "")
if wanted == "" then
if wanted == "" then
return "''Error: order parameter required (use exact order name, e.g., Order of Aurora Mystica).''"
return "''Error: order parameter required (use exact order name).''"
end
end
local o = orderByName[wanted]
if not o 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 di = getDateInfo()
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local econSignals = getEconSignals()


local threatMap = {}
local threatMap = {}
Line 795: Line 1,154:
local out = {}
local out = {}
table.insert(out, "{| class='wikitable sortable'")
table.insert(out, "{| class='wikitable sortable'")
table.insert(out, "! Date !! City !! Region !! Unit !! Tasking !! Temple Bank Notes" .. (includeThreat and " !! Threat Watch" or "") .. " !! Severity")
local header = "! Date !! City !! Region !! 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
for _, c in ipairs(cityOrder) do
local row = buildCityRow(di, c.name, c.region, threatMap, includeThreat)
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
-- Compare by display name (strip wikilink wrapper crudely by checking map key)
if row.order == o.link then
local orderObj = orderByName[wanted]
if orderObj and row.order == orderObj.link then
table.insert(out, "|-" .. row.style)
table.insert(out, "|-" .. row.style)
local line = string.format("| %s || %s || %s || %s || %s",
row.date, row.city, row.region, 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
if includeThreat then
local threatCell = row.threat ~= "" and (row.threat .. " (" .. (row.threatUrg or "") .. ")") or "No reports / n.a."
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s || %s",
line = line .. " || " .. threatCell
row.date, row.city, row.region, row.unit, row.task, row.bank, threatCell, row.sev
))
else
table.insert(out, string.format("| %s || %s || %s || %s || %s || %s || %s",
row.date, row.city, row.region, row.unit, row.task, row.bank, row.sev
))
end
end
line = line .. " || " .. row.sev
table.insert(out, line)
end
end
end
end

Revision as of 03:01, 31 December 2025

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

-- Module:MissionaryDailyOpsBVR (REWRITE)
-- 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).
--
-- NEW IN THIS REWRITE:
--  • City profiles (culture/spiritual tags + per-city order bias)
--  • Economic signals pulled from General Port of Lake Morovia "Company Table"
--    (counts by sector + sample HQ companies) and used to bias order + task.
--  • Optional Threat Watch integration from Module:ReligiousEvents (opt-in).
--
-- 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=Order of Aurora Mystica}}

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

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("''", "")
	-- HTML entities are usually already decoded by MediaWiki; keep safe
	return trim(s)
end

local function containsAny(s, words)
	s = (s or ""):lower()
	for _, w in ipairs(words) do
		if s:find(w, 1, true) then return true end
	end
	return false
end

local function joinTags(tags)
	if not tags or #tags == 0 then return "" end
	local out = {}
	for _, t in ipairs(tags) do out[#out+1] = t end
	return table.concat(out, ", ")
end

--------------------------------------------------------------------------------
-- 1) CALENDAR PARSING (mirrors your HatchMinistryDailyOps approach)
--------------------------------------------------------------------------------
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 (NO bit32, NO math.random)
--------------------------------------------------------------------------------
local function hash31(str)
	local h = 0
	for i = 1, #str do
		h = (h * 131 + str:byte(i)) % 2147483647
	end
	return h
end

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

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

--------------------------------------------------------------------------------
-- 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 normalizeForFlag(name)
	return (name or "")
		:gsub("[%s%-’]", "")
		:gsub("[Áá]", "A"):gsub("[Éé]", "E")
		:gsub("[Íí]", "I"):gsub("[Óó]", "O")
		:gsub("[Úú]", "U"):gsub("[Ýý]", "Y")
end

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" spiritual 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 = {}
local cityFlag = {}
for _, c in ipairs(cityOrder) do
	citySet[c.name] = true
	cityFlag[c.name] = string.format("[[File:%sFlag.png|20px]]", normalizeForFlag(c.name))
end
-- explicit override used elsewhere on the wiki
cityFlag["Skýrophos"] = "[[File:SkyrophosFlag.png|20px]]"

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

--------------------------------------------------------------------------------
-- 5) CORRIDORS / ROLE HINTS (from the city-network page; hard-coded for stability)
--------------------------------------------------------------------------------
-- Rail corridor cities listed on the city network overview.
local RAIL = {
	["Lykopolis"]=true, ["Lewisburg"]=true, ["Bashkim"]=true, ["Somniumpolis"]=true,
	["Erythros"]=true, ["Vaeringheim"]=true, ["Symphonara"]=true, ["Skýrophos"]=true,
	["Norsolyra"]=true, ["Pelagia"]=true, ["Riddersborg"]=true, ["Sårensby"]=true,
}
-- Ferry links noted: Jogi + Ardclach
local FERRY = {["Jogi"]=true, ["Ardclach"]=true}

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

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

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

local orderByKey, orderByName, orderKeys, orderNames = {}, {}, {}, {}
for i, o in ipairs(orders) do
	orderByKey[o.key] = o
	orderByName[o.name] = o
	orderKeys[#orderKeys+1] = o.key
	orderNames[#orderNames+1] = o.name
end

--------------------------------------------------------------------------------
-- 8) CITY PROFILES (manual cultural/spiritual bias; econ signal adds more)
-- You can expand these over time. Any city not listed here still gets econ+region bias.
--------------------------------------------------------------------------------
local cityProfiles = {
	["Vaeringheim"] = {
		tags={"capital","port","admin","templebank"},
		weights={GGS=4,HSA=3,OSI=2,ASS=2,RCA=2,CIL=2},
	},
	["Luminaria"] = {
		tags={"industry","innovation","dawn"},
		weights={TIL=3,OAM=3,EOF=2,OSI=2,IGA=2},
	},
	["Somniumpolis"] = {
		tags={"finance","dream","noctic"},
		weights={REN=3,GGS=3,HSA=2,UOR=2,CIL=2,RCA=1},
	},
	["Pyralis"] = {
		tags={"volcanic","hazard","fire"},
		weights={IGA=4,ASS=2,SCV=2,EOF=1},
		baseSev="yellow",
	},
	["Lewisburg"] = {
		tags={"rail","hub","wetlands"},
		weights={TIL=3,RCA=2,ASS=2,GGS=1,SCV=1},
	},
	["Lykopolis"] = {
		tags={"rail","corridor","resource"},
		weights={TIL=3,EOF=2,RCA=2,CIC=1},
		baseSev="yellow",
	},
	["Tel-Amin"] = {
		tags={"plantation","coffee","trade"},
		weights={MVE=2,SYF=2,OAI=2,CHS=1,ASS=1},
	},
	["Jogi"] = {
		tags={"agriculture","markets","fermentation"},
		weights={MVE=3,OAI=2,SYF=2,ASS=1,GGS=1},
	},
	["Acheron"] = {
		tags={"frontier","containment","mines"},
		weights={CIC=3,EOF=2,MSV=1,UOR=1,IGA=1},
		baseSev="yellow",
	},
	["Skýrophos"] = {
		tags={"integration","civic","corridor"},
		weights={CHS=2,HSA=2,EOF=2,RCA=2,TIL=1},
	},
	["Riddersborg"] = {
		tags={"north","storm","corridor"},
		weights={EOF=3,TIL=2,ASS=1,CIC=1},
		baseSev="yellow",
	},
	["Ardclach"] = {
		tags={"north","sea","ferry"},
		weights={ASS=3,EOF=2,CIC=1},
		baseSev="yellow",
	},
}

--------------------------------------------------------------------------------
-- 9) ECONOMIC SIGNALS: parse "Company Table" on General Port page
--------------------------------------------------------------------------------
local econCache = nil

local function sectorOf(industryCell)
	local s = (industryCell or ""):lower()
	if s:find("agric", 1, true) then return "Agricultural" end
	if s:find("service", 1, true) then return "Service" end
	if s:find("manufact", 1, true) or s:find("mining", 1, true) or s:find("energy", 1, true) then
		return "Industry"
	end
	-- fallback
	return "Other"
end

local function extractCompanyName(cell)
	-- Try '''Name''' first
	local n = cell:match("'''(.-)'''")
	if n and n ~= "" then return stripWiki(n) end
	-- Try bold markdown-like **Name** (some pages render that way)
	n = cell:match("%*%*(.-)%*%*")
	if n and n ~= "" then return stripWiki(n) end
	-- fallback: strip wiki and truncate
	n = stripWiki(cell)
	if #n > 80 then n = n:sub(1, 80) .. "…" end
	return n
end

local function extractHQ(cell)
	local s = stripWiki(cell)
	if s == "" then return nil end
	-- Ignore obvious non-city HQs
	if s:lower():find("fleet", 1, true) then return nil end
	if s:lower():find("no fixed", 1, true) then return nil end
	-- Many HQ cells are just the city name after stripWiki.
	-- Some may include extra notes; take first clause.
	s = s:gsub("%s*;%s*.*$", "")
	s = s:gsub("%s*%b()%s*", " ")
	s = normalizeSpaces(s)
	s = trim(s)
	if citySet[s] then return s end
	-- Sometimes HQ may be "Vaeringheim (General Port)" etc.
	for cname, _ in pairs(citySet) do
		if s == cname then return cname end
	end
	return nil
end

local function parseCompanyTable(wikitext)
	local signals = {}
	for cname, _ in pairs(citySet) do
		signals[cname] = {Agricultural=0, Service=0, Industry=0, Other=0, samples={}}
	end

	if type(wikitext) ~= "string" then return signals end

	-- Find "Company Table" section and first wikitable after it
	local startPos = wikitext:find("====%s*Company Table%s*====")
	if not startPos then
		startPos = wikitext:find("####%s*Company Table")
	end
	if not startPos then
		-- If heading not found, bail with empty signals
		return signals
	end

	local tablePos = wikitext:find("{|", startPos)
	if not tablePos then return signals end
	local endPos = wikitext:find("|}", tablePos)
	if not endPos then return signals end

	local tableText = wikitext:sub(tablePos, endPos + 1)

	-- Split into rows by "|-"
	local rows = {}
	for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do
		rows[#rows+1] = chunk
	end
	-- Also capture last row before "|}" if not matched by above pattern
	local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}")
	if lastChunk then rows[#rows+1] = lastChunk end

	for _, row in ipairs(rows) do
		-- ignore header rows that start with "!"
		if not row:match("^%s*!") then
			-- collapse row to a single line; cells separated by "||"
			local line = normalizeSpaces(row:gsub("\n", " "))
			-- remove leading single "|"
			line = line:gsub("^%s*|%s*", "")
			local cells = {}
			for cell in line:gmatch("([^|]-)%s*||%s*") do
				cells[#cells+1] = trim(cell)
			end
			-- capture last cell after final "||"
			local tail = line:match("||%s*([^|].-)%s*$")
			if tail and tail ~= "" then
				-- only add if we didn't already (gmatch above may omit last)
				if #cells == 0 or cells[#cells] ~= trim(tail) then
					cells[#cells+1] = trim(tail)
				end
			end

			-- Expect 7 columns; be defensive
			-- Industry cell usually around 5; HQ is last.
			local industryCell = ""
			for _, c in ipairs(cells) do
				if containsAny(c, {"agric", "service", "manufact", "mining", "energy"}) then
					industryCell = c
					break
				end
			end
			local sector = sectorOf(industryCell)

			local hqCell = cells[#cells] or ""
			local hq = extractHQ(hqCell)
			if hq and signals[hq] then
				signals[hq][sector] = signals[hq][sector] + 1
				-- sample company name from the "Name" column (often 3rd cell)
				local nameCell = cells[3] or cells[2] or cells[1] or ""
				local companyName = extractCompanyName(nameCell)
				if companyName ~= "" and #signals[hq].samples < 2 then
					signals[hq].samples[#signals[hq].samples+1] = companyName
				end
			end
		end
	end

	return signals
end

local function getEconSignals()
	if econCache then return econCache end
	local content = safeTitleContent("General Port of Lake Morovia")
	econCache = parseCompanyTable(content)
	return econCache
end

local function dominantSector(sig)
	if not sig then return "Other" end
	local best, bestN = "Other", -1
	for _, k in ipairs({"Agricultural","Industry","Service","Other"}) do
		local n = sig[k] or 0
		if n > bestN then best, bestN = k, n end
	end
	return best
end

local function econTagFromSector(sec)
	if sec == "Agricultural" then return "agri" end
	if sec == "Industry" then return "industry" end
	if sec == "Service" then return "service" end
	return "mixed"
end

local function econSummary(sig)
	if not sig then return "n/a" end
	local a = sig.Agricultural or 0
	local i = sig.Industry or 0
	local s = sig.Service or 0
	local o = sig.Other or 0
	local dom = dominantSector(sig)
	local samp = (sig.samples and #sig.samples > 0) and ("; e.g., " .. table.concat(sig.samples, ", ")) or ""
	return string.format("%s (%d agri / %d ind / %d serv / %d other)%s", dom, a, i, s, o, samp)
end

--------------------------------------------------------------------------------
-- 10) OPTIONAL: Threat Watch integration from Module:ReligiousEvents
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
	local out = {}
	if type(wikitext) ~= "string" then return out end
	for line in wikitext:gmatch("[^\n]+") do
		if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then
			local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$")
			if city and urg then
				out[stripWiki(city)] = {urg=trim(urg), event=stripWiki(event or "")}
			end
		end
	end
	return out
end

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

--------------------------------------------------------------------------------
-- 11) WEIGHTED ORDER PICK + TASK PICK (matches city tags + econ tags + corridor tags)
--------------------------------------------------------------------------------
local function weightedPickKey(nextInt, weights)
	-- weights: { [orderKey]=number }
	local total = 0
	for _, ok in ipairs(orderKeys) do
		total = total + (weights[ok] or 0)
	end
	if total <= 0 then
		return pickFromList(nextInt, orderKeys, nil)
	end
	local r = (nextInt() % total) + 1
	local acc = 0
	for _, ok in ipairs(orderKeys) do
		acc = acc + (weights[ok] or 0)
		if r <= acc then return ok end
	end
	return orderKeys[1]
end

local function hasTag(tagSet, t)
	return tagSet[t] == true
end

local function buildTagSet(tags)
	local set = {}
	for _, t in ipairs(tags or {}) do set[t] = true end
	return set
end

local function pickTaskForCity(nextInt, taskList, tagSet)
	-- Prefer tasks whose tags intersect city tagset
	local candidates = {}
	for _, task in ipairs(taskList or {}) do
		local ok = false
		for _, tg in ipairs(task.tags or {}) do
			if tagSet[tg] then ok = true break end
		end
		if ok then candidates[#candidates+1] = task end
	end
	if #candidates > 0 then
		return pickFromList(nextInt, candidates, nil)
	end
	return pickFromList(nextInt, taskList, nil)
end

local function baseWeights()
	local w = {}
	for _, ok in ipairs(orderKeys) do w[ok] = 1 end
	return w
end

local function applyWeights(w, add)
	if not add then return end
	for k, v in pairs(add) do
		if orderByKey[k] then
			w[k] = (w[k] or 0) + tonumber(v or 0)
		end
	end
end

local function biasFromEcon(w, econTag)
	-- Econ bias is intentionally simple and legible
	if econTag == "agri" then
		applyWeights(w, {MVE=3,SYF=2,OAI=2,ASS=1})
	elseif econTag == "industry" then
		applyWeights(w, {TIL=3,EOF=2,IGA=2,CIC=1,RCA=1})
	elseif econTag == "service" then
		applyWeights(w, {GGS=3,HSA=2,RCA=2,CIL=1,CHS=1})
	else
		applyWeights(w, {CHS=1,HSA=1,ASS=1})
	end
end

local function biasFromCorridor(w, isRail, isFerry)
	if isRail then
		applyWeights(w, {TIL=2,RCA=2,GGS=1,EOF=1,ASS=1})
	end
	if isFerry then
		applyWeights(w, {ASS=2,GGS=1,RCA=1})
	end
end

local function biasFromTier(w, tier)
	if tier == "major" then
		-- Major cities: public order + rumor control tends to matter more
		applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1})
	end
end

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

--------------------------------------------------------------------------------
-- 13) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
local function inferSeverity(cityProfile, econTag, taskSev, threatLevel)
	local sev = taskSev or "green"

	-- baseline from profile
	if cityProfile and cityProfile.baseSev then
		sev = cityProfile.baseSev
	end

	-- econ nudge (industry + service hubs get more "yellow" oversight more often)
	if econTag == "industry" or econTag == "service" then
		if sev == "green" then sev = "yellow" end
	end

	-- threat overrides
	if threatLevel == "yellow" and sev == "green" then sev = "yellow" end
	if threatLevel == "red" and sev ~= "red" then sev = "red" end

	return sev
end

local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
	local cityName, cityRegion, tier = c.name, c.region, c.tier
	local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
	local nextInt = lcg(hash31(seedStr))

	local profile = cityProfiles[cityName] or {tags={}, weights=nil}
	local tags = {}

	-- profile tags
	for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end

	-- corridor tags
	if RAIL[cityName] then tags[#tags+1] = "rail" end
	if FERRY[cityName] then tags[#tags+1] = "ferry" end
	if tier == "major" then tags[#tags+1] = "dreamspace" end

	-- econ tags
	local sig = econSignals and econSignals[cityName] or nil
	local dom = dominantSector(sig)
	local econTag = econTagFromSector(dom)
	tags[#tags+1] = econTag

	local tagSet = buildTagSet(tags)

	-- Threat map
	local th = includeThreat and threatMap and threatMap[cityName] or nil
	local tSev = threatSev(th)

	-- Build weights
	local w = baseWeights()
	biasFromTier(w, tier)
	biasFromEcon(w, econTag)
	biasFromCorridor(w, RAIL[cityName], FERRY[cityName])
	applyWeights(w, profile.weights)

	-- Pick order + task
	local orderKey = weightedPickKey(nextInt, w)
	local o = orderByKey[orderKey] or orders[1]
	local task = pickTaskForCity(nextInt, o.tasks, tagSet)

	local sev = inferSeverity(profile, econTag, task.sev, tSev)

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

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

	-- Threat display
	local threatText, threatUrg = "", ""
	if includeThreat and th then
		threatText = th.event or ""
		threatUrg = th.urg or ""
	end

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

--------------------------------------------------------------------------------
-- 14) 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()

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

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	local header = "! style='white-space:nowrap;' | Date !! City !! Region !! 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",
				row.date, row.city, row.region, 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, "</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

--------------------------------------------------------------------------------
-- 15) 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 !! 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",
		row.date, row.city, row.region, 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

--------------------------------------------------------------------------------
-- 16) 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 o = orderByName[wanted]
	if not o 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 !! 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)
		if row.order == o.link then
			table.insert(out, "|-" .. row.style)
			local line = string.format("| %s || %s || %s || %s || %s",
				row.date, row.city, row.region, 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