Module:MissionaryDailyOpsBVR: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
Replaced content with "-------------------------------------------------------------------------------- -- WX (Ops Alert) integration from Module:BassaridiaForecast -- Used to display a city-level ops badge + bump severity + append weather-driven tasking. -------------------------------------------------------------------------------- local wxOk, wx = pcall(require, "Module:BassaridiaForecast") if not wxOk then wx = nil end local function wxColor(level) level = tonumber(level) or 0 if l..."
Tag: Replaced
NewZimiaGov (talk | contribs)
Undo revision 387695 by NewZimiaGov (talk)
Tag: Undo
Line 1: Line 1:
-- Module:MissionaryDailyOpsBVR (REWRITE + FLAG RESOLVER + CULT ICONS)
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Deterministic output (no math.random / no os.time seeding).
--
-- Features:
--  • City profiles (culture/spiritual tags + per-city order bias)
--  • Economic signals parsed 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).
--  • City flags: existence-checked; fallback to Imperial Trade Union flag when missing.
--  • Cult/Order icons: from Module:GeneralPortOperations addLogo(...) mapping.
--
-- 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("''", "")
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
--------------------------------------------------------------------------------
local function parseCalendarString(s)
if type(s) ~= "string" then return nil end
local dayOfYear, month, year = s:match("^(%d+),%s+([%a]+)%s+%b()%s*,%s*(%d+)%s+PSSC")
if dayOfYear and year then
return tonumber(year), tonumber(dayOfYear), month
end
return nil
end
local function extractEventText(s)
if type(s) ~= "string" then return "" end
local ev = s:match("PSSC%s+–%s+(.-)%s+–%s+Proverb:")
return ev or ""
end
local function doyToMonthDom(doy)
if doy <= 61 then
return 1, doy
elseif doy <= 122 then
return 2, doy - 61
else
return 3, doy - 122
end
end
local function getDateInfo()
local raw = cal.getCurrentDate()
local y, doy = parseCalendarString(raw)
if not y or not doy then
y, doy = 0, 1
raw = tostring(raw or "")
end
local m, dom = doyToMonthDom(doy)
return {
year = y,
dayOfYear = doy,
monthNum = m,
dom = dom,
text = string.format("%d/%d/%d PSSC", dom, m, y),
raw = raw,
calendarEvent = extractEventText(raw),
}
end
--------------------------------------------------------------------------------
-- 2) DETERMINISTIC RNG
--------------------------------------------------------------------------------
local function hash31(str)
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" 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 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
--------------------------------------------------------------------------------
-- 4.1) CITY FLAGS (existence-checked + fallback)
--------------------------------------------------------------------------------
local DEFAULT_CITY_FLAG_FILE = "Imperial Trade Union flag.png"
local function asciiNormalizeForFlag(name)
return (name or "")
:gsub("[%s%-’]", "")
:gsub("[ÁáÀàÂâÄäÃãÅåĀā]", "A")
:gsub("[ÉéÈèÊêËëĒē]", "E")
:gsub("[ÍíÌìÎîÏïĪī]", "I")
:gsub("[ÓóÒòÔôÖöÕõØøŌō]", "O")
:gsub("[ÚúÙùÛûÜüŪū]", "U")
:gsub("[ÝýŸÿ]", "Y")
:gsub("[Ææ]", "AE")
end
local fileExistsCache = {}
local function fileExists(fileName)
if not fileName or fileName == "" then return false end
if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
local ok, t = pcall(mw.title.new, "File:" .. fileName)
local exists = (ok and t and t.exists == true) or false
fileExistsCache[fileName] = exists
return exists
end
local citySet = {}
for _, c in ipairs(cityOrder) do citySet[c.name] = true end
-- Explicit odd filenames you noted (plus Skyrophos).
local cityFlagOverrides = {
["Skýrophos"] = "SkyrophosFlag.png",
["Sårensby"]  = "SårensbyFlag.png",
["Slevik"]    = "SlevikFlag.png.png",
["Kaledonija"]= "KaledonijaFlag.png",
}
local cityFlagMarkupCache = {}
local function getCityFlagMarkup(cityName)
if cityFlagMarkupCache[cityName] then return cityFlagMarkupCache[cityName] end
-- 1) explicit override
local override = cityFlagOverrides[cityName]
if override and fileExists(override) then
local mk = string.format("[[File:%s|20px]]", override)
cityFlagMarkupCache[cityName] = mk
return mk
end
-- 2) try normalized candidates
local norm = normalizeForFlag(cityName)
local ascii = asciiNormalizeForFlag(cityName)
local candidates = {
norm .. "Flag.png",
ascii .. "Flag.png",
}
for _, fn in ipairs(candidates) do
if fileExists(fn) then
local mk = string.format("[[File:%s|20px]]", fn)
cityFlagMarkupCache[cityName] = mk
return mk
end
end
-- 3) fallback to ITU
local mk = string.format("[[File:%s|20px]]", DEFAULT_CITY_FLAG_FILE)
cityFlagMarkupCache[cityName] = mk
return mk
end
--------------------------------------------------------------------------------
-- 5) CORRIDORS / ROLE HINTS (hard-coded)
--------------------------------------------------------------------------------
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,
}
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 (tagged)
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
end
local function T(text, sev, tags)
return {text=text, sev=sev, tags=tags or {}}
end
local orders = {
{key="OSI", name="Ordo Solis Invicti",
link=orderLink("Atos (Reformed Stripping Path)","Ordo_Solis_Invicti","Ordo Solis Invicti"),
tasks={
T("Dawn rites and public illumination drill to restore civic clarity and discourage rumor cascades in market wards.","green",{"public","education","service"}),
T("Temple-school literacy session paired with cosmology instruction and shrine maintenance inspections.","green",{"education","shrine","service"}),
T("Solar route inspection at major junctions; traffic etiquette posted for pilgrims and commuters.","green",{"transit","rail","public"}),
T("Sun-pledge renewal for civic clerks; oath sheets sealed and logged with Temple Bank scribes.","green",{"governance","audit","service"}),
T("Public lamp and beacon blessing for tunnel nodes; wayfinding clarified for night labor shifts.","green",{"infrastructure","transit","industry"}),
T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit","service"}),
T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd","public"}),
T("Counter-broadsheet sermon cycle following unsigned pamphlets; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
T("Joint patrol-litany along rail platforms to reduce theft and panic during peak transit hours.","yellow",{"rail","security","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","service"}),
T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.","green",{"omen","public","service"}),
T("Quiet home-visits to recent migrants; orientation blessings and sanctioned neighborhood guides issued.","green",{"public","service","mediation"}),
T("Rooftop shrine upkeep and dawn-banner replacement; civic lanes kept clear for morning processions.","green",{"shrine","infrastructure","public"}),
T("Pilgrim flow timing review for a dawn festival; route markers placed to prevent crush points.","green",{"festival","crowd","public"}),
T("Civic blessing for new housing blocks; routing of foot-traffic to prevent crush points and misdirection.","yellow",{"crowd","infrastructure","service"}),
T("Counter-heresy sermon cycle following illicit broadsides; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
T("Dawn mediation under escort after a workplace dispute threatens walkouts; settlement notarized.","yellow",{"mediation","industry","governance"}),
T("Omen-risk advisory for coastal weather or fog days; port stewards briefed and signage posted.","yellow",{"port","public","service"}),
T("Rapid-response pacification after arson rumor; dawn procession under guard with emergency stipend release.","red",{"unrest","arson","crowd"}),
}
},
{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","service"}),
T("Harmony clinic for stress and rumor contagion; quiet-room rotations and guided breathing rites.","green",{"health","rumor","service"}),
T("Queue-discipline coaching at relief points; volunteers trained to prevent confrontations.","green",{"order","queue","relief"}),
T("Workplace concord circle for service districts; complaint channels clarified and posted.","green",{"service","mediation","governance"}),
T("Inter-shrine courtesy visit to reduce rivalry; shared calendar commitments established.","green",{"balance","mediation","public"}),
T("Loyalty and morale audit of a civic office after complaint surge; corrective training scheduled.","yellow",{"audit","governance","service"}),
T("Containment of a cult schism meeting; supervised dialogue and sanction warnings delivered.","yellow",{"schism","containment","public"}),
T("Rumor forensics on a circulating ‘forbidden rite’ claim; witness tree compiled and forwarded.","yellow",{"rumor","investigation","public"}),
T("Crowd-stabilization drill for festival season; steward assignments and safe exits mapped.","yellow",{"festival","crowd","public"}),
T("Major crowd-stabilization operation after riot signals; staged dispersal with peace-liturgy and escorts.","red",{"riot","crowd","public"}),
}
},
{key="IGA", name="Ignis Aeternum",
link=orderLink("Pyros (Reformed Stripping Path)","Ignis_Aeternum","Ignis Aeternum"),
tasks={
T("Controlled fire blessing for workshops; safety rite doubled as inspection of fuel and storage.","green",{"industry","safety"}),
T("Kiln-and-forge consecration for a new artisan street; insurance seals verified.","green",{"industry","infrastructure","service"}),
T("Firebreak marking for dry-season hills; public notices issued to farmers and herders.","green",{"agri","fire","public"}),
T("Hearth-blessing round for crowded tenements; cooking hazards corrected with wardens.","green",{"public","safety","service"}),
T("Candle and incense quality check at shrine vendors; adulterants rejected and logged.","green",{"shrine","audit","service"}),
T("Purification burn at a compromised shrine-store; ash sealed and catalogued for Temple Bank disposal.","yellow",{"contam","shrine","service"}),
T("Investigation of incendiary threats near a gate-temple; firebreak plan issued to local wardens.","yellow",{"arson","investigation","security"}),
T("Explosive-storage compliance check near port or rail yards; seals renewed and routes revised.","yellow",{"rail","port","security","industry"}),
T("Emergency response to active arson or explosive rumor; sanctified suppression teams deployed with escorts.","red",{"arson","crisis","security"}),
T("Post-incident purification circuit across multiple oratories; public confidence restored by nightfall.","red",{"incident","public","crowd"}),
}
},
{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","service"}),
T("Star-chart instruction for apprentices; quiet observatory checks and record sealing.","green",{"education","astronomy","service"}),
T("Inter-city liaison packet prepared for corridor towns; travel permissions confirmed.","green",{"rail","service","governance"}),
T("Boundary-harmony rite at a district line; neighbor shrines exchange tokens under witness.","green",{"balance","public","mediation"}),
T("Calm briefing for merchant captains on festival timing; reduce congestion and rumor spread.","green",{"port","service","public"}),
T("Faction-balance mediation between rival shrines; written compromise placed in Temple Bank archive.","yellow",{"balance","mediation","archive"}),
T("Rumor forensics on ‘forbidden rite’ claims; witness tree compiled and forwarded to Temple authorities.","yellow",{"rumor","investigation","public"}),
T("Quiet arbitration after contract dispute between a guild and a council office.","yellow",{"service","governance","mediation"}),
T("Transit coordination for high-profile delegates; platform escorts and crowd lanes established.","yellow",{"rail","crowd","security"}),
T("High-risk stabilization after cult confrontation; negotiators embedded with escorts to prevent escalation.","red",{"confrontation","crowd","security"}),
}
},
{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","service"}),
T("Merchant vetting interviews for a sensitive consignment; anonymity rites certified.","green",{"merchant","security","service"}),
T("Port-side seal verification for sacred cargo; chain-of-custody stamped and witnessed.","green",{"port","finance","security"}),
T("Rail consignment escort paperwork checked; handover points confirmed with station stewards.","green",{"rail","service","security"}),
T("Quiet fraud-prevention briefing for shrine treasurers; common forgery patterns circulated.","green",{"finance","education","service"}),
T("Counter-counterfeit sweep targeting forged stamps and false Temple consignments; evidence boxed and logged.","yellow",{"forgery","investigation","finance"}),
T("Debt dispute arbitration for a sanctioned charity line; terms rewritten and sealed.","yellow",{"finance","mediation","service"}),
T("Watchlist update for suspected smugglers using festival crowds; surveillance plan filed.","yellow",{"security","crowd","port"}),
T("Recovery detail for stolen reliquary property; surveillance and lawful seizure under civic warrant.","red",{"theft","security","investigation"}),
T("Crisis finance stabilization after panic; emergency stipend routing and rumor suppression coordination.","red",{"panic","finance","service"}),
}
},
{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","service"}),
T("Harbor hygiene briefing for fish markets; cold-chain and wash stations checked.","green",{"port","health","service"}),
T("Flood-gate inspection rite; maintenance tickets issued and signed by civic stewards.","green",{"infrastructure","water","service"}),
T("Ferry pier blessing and rope inspection; boarding lines reorganized to reduce accidents.","green",{"ferry","port","public"}),
T("Reservoir vigil for drought days; ration etiquette posted at distribution points.","green",{"water","relief","public"}),
T("Harbor-side watch for sabotage indicators; shrine-wardens trained on lock and pier checks.","yellow",{"port","sabotage","security"}),
T("Storm or flood readiness rite; evacuation hymn routes verified with civic stewards.","yellow",{"flood","infrastructure","public"}),
T("Waterborne rumor containment bulletin; discourage panic buying and hoarding.","yellow",{"rumor","service","public"}),
T("Quarantine support for suspected waterborne illness; controlled distribution points blessed and monitored.","red",{"outbreak","water","health"}),
T("Major incident response: water infrastructure damage; emergency sanctification and ration-order enforcement.","red",{"infrastructure","crisis","public"}),
}
},
{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","service"}),
T("Nocturne counseling for insomnia cluster; soft-lit rites and chant rotation schedules.","green",{"health","sleep","service"}),
T("Public guidance sheet on lawful visions vs. sedition pamphlets; distributed at shrines.","green",{"education","public","service"}),
T("Quiet household visits after a death or displacement; stabilize grief cycles with sanctioned imagery.","green",{"service","health","public"}),
T("Festival mood stewardship: prevent illicit revel by offering sanctioned evening rites.","green",{"festival","public","morale"}),
T("Investigation of forbidden homily broadcasts; witness interviews and calm messaging drafted.","yellow",{"broadcast","investigation","service"}),
T("Rumor cascade mapping in service districts; identify the first carrier nodes.","yellow",{"rumor","investigation","service"}),
T("Containment of mass-vision panic; staged public reassurance with tightly controlled procession.","red",{"panic","crowd","public"}),
T("Stabilization after hallucination-linked violence; liaison with Harmony Sanctum for long-tail care.","red",{"violence","health","service"}),
T("Emergency night curfew-support rite; safe corridors marked to reduce shadow-fear incidents.","yellow",{"security","public","service"}),
}
},
{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","service"}),
T("Infrastructure endurance audit: roofs, walls, and bridges; damage triage list issued.","green",{"infrastructure","audit","industry"}),
T("Shelter staffing rotation planned for storm season; volunteer rosters sealed.","green",{"relief","service","governance"}),
T("Rail convoy support brief for frontier deliveries; pacing and rest points set.","green",{"rail","logistics","service"}),
T("Queue discipline at distribution points; conflict triggers reduced through signage.","green",{"order","queue","public"}),
T("Conflict de-escalation for resource disputes; binding queue rules posted at distribution points.","yellow",{"order","queue","governance"}),
T("Emergency shelter activation after storm loss; warm-hall rites and controlled entry.","red",{"storm","relief","public"}),
T("Frontier outpost morale visit; endurance rites and supply accountability review.","yellow",{"frontier","audit","service"}),
T("Bridge closure coordination after damage report; detours posted and escorted.","yellow",{"infrastructure","transit","public"}),
T("High-risk escort for frontier convoys under threat; endurance cadres embedded with wardens.","red",{"frontier","security","logistics"}),
}
},
{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","service"}),
T("Shadow-lane safety marking near canals and alleys; lantern placement logged.","green",{"public","security","transit"}),
T("Quiet counsel for a magistrate; truth-testing rite witnessed by two scribes.","green",{"governance","inquiry","service"}),
T("Missing-persons intake desk hosted at a shrine; reports standardized and forwarded.","green",{"service","investigation","public"}),
T("Nighttime rumor monitoring in taverns and stations; early interventions logged.","green",{"rumor","rail","service"}),
T("Discrete consultation for a magistrate; truth-testing rite witnessed by two scribes.","yellow",{"governance","inquiry","audit"}),
T("Shadow-route mapping after disappearances; safe-path markers installed with civic approval.","yellow",{"security","transit","public"}),
T("Evidence sealing after a midnight incident; chain-of-custody verified for Temple authorities.","yellow",{"investigation","security","service"}),
T("Emergency intervention after ‘demonic sign’ sightings; controlled crowd handling and ward placement.","red",{"demonic","panic","crowd"}),
T("High-risk extraction of a threatened cantor; night escort to secure sanctuary.","red",{"abduction","security","service"}),
}
},
{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","agri"}),
T("Garden-shrine restoration day with community labor; devotional planting aligned to calendar.","green",{"garden","shrine","agri"}),
T("Seed and tool stipend audit for smallholders; vouchers revalidated.","green",{"audit","agri","service"}),
T("Irrigation channel blessing and silt-clearing volunteer day; water etiquette posted.","green",{"water","agri","public"}),
T("Market stall purity checks for produce; vendors instructed on safe storage.","green",{"public","health","agri"}),
T("Food shortage mediation and ration arbitration; Temple Bank stipend adjustments proposed.","yellow",{"shortage","logistics","agri"}),
T("Pest or blight containment rites; field inspections with farmers’ associations.","yellow",{"blight","agriculture","agri"}),
T("Livestock procession routing for festival markets; prevent crush and theft.","yellow",{"crowd","festival","agri"}),
T("Famine-risk surge response; emergency distribution queues sanctified and monitored.","red",{"famine","relief","agri"}),
T("Emergency containment of contaminated grain rumor; sample testing coordinated with Sanctum Vitalis.","yellow",{"rumor","health","agri"}),
}
},
{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","service"}),
T("Night-sky bulletin posted to reduce panic during overcast weeks; lawful interpretations provided.","green",{"public","rumor","service"}),
T("Observatory instrument check; calibration seals signed and archived.","green",{"archive","audit","service"}),
T("Lantern route blessing for tunnels and arcades; wayfinding clarified for late shifts.","green",{"transit","public","service"}),
T("Quiet archive preservation: scroll humidity checks and secure storage review.","green",{"archive","service","audit"}),
T("Archive sealing of sensitive liturgies; access logs reviewed for anomalies.","yellow",{"archive","audit","security"}),
T("Public lantern rite to counter rumor cascades; controlled routes and timing set with wardens.","yellow",{"rumor","crowd","public"}),
T("Transit hub advisory after night panic; crowd lanes re-drawn and posted.","yellow",{"rail","crowd","transit"}),
T("Emergency omen bulletin after night panic; official statement drafted and delivered.","red",{"panic","public","service"}),
T("High-risk protection of an observatory site under threat; wards reinforced and watchers stationed.","red",{"threat","security","service"}),
}
},
{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","service"}),
T("Sanctioned healer coordination meeting; triage roles rehearsed for outbreaks.","green",{"training","health","service"}),
T("Hygiene signage deployment at markets; handwash stations verified.","green",{"public","health","service"}),
T("Nutrition counseling for ration lines; supplement vouchers distributed under Temple Bank authorization.","green",{"nutrition","relief","service"}),
T("Quiet home checks for vulnerable households; prevent panic spirals after illness rumors.","green",{"service","health","rumor"}),
T("Supplement and ration fortification distribution under Temple Bank authorization.","yellow",{"nutrition","relief","service"}),
T("Outbreak protocol rehearsal with civic healers; spacing rules posted at shrines.","yellow",{"outbreak","training","public"}),
T("Quarantine lane setup drill at a terminal or station; entry control rehearsed.","yellow",{"rail","security","health"}),
T("Active outbreak response: triage lanes established; quarantine support and calm messaging.","red",{"outbreak","crisis","public"}),
T("Mass-casualty contingency drill after violence reports; intake points and transport routes mapped.","red",{"violence","logistics","service"}),
}
},
{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","service"}),
T("Doctrinal reading circle for novices; sanctioned texts distributed with margin notes.","green",{"education","service","public"}),
T("Shrine inventory check for relic storage; locks and witness rules enforced.","green",{"security","shrine","audit"}),
T("Quiet correction of unsanctioned household rites; guidance issued without spectacle.","green",{"service","public","education"}),
T("Blessing of new white-draped oratory; entry etiquette posted to reduce disorder.","green",{"shrine","public","service"}),
T("Doctrinal correction session after unsigned broadsides; sanctioned texts distributed.","yellow",{"heresy","education","public"}),
T("Sanctuary security review for valuables; locking discipline and witness rules enforced.","yellow",{"security","shrine","service"}),
T("Investigation of tainted incense vendors; samples seized and chain-of-custody logged.","yellow",{"poison","investigation","service"}),
T("Emergency purification after poisoned incense discovery; wards placed and offenders sought.","red",{"poison","crisis","security"}),
T("High-risk protection of a central temple during unrest; entry control and calm liturgy.","red",{"unrest","security","crowd"}),
}
},
{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","service"}),
T("Escort for a threatened courier; route cleared and handover documented.","green",{"escort","security","service"}),
T("Patrol coordination with civic guards at terminals; hand signals and cordon rules standardized.","green",{"rail","security","service"}),
T("Gatehouse posture review for festival season; crowd lanes chalked and posted.","green",{"festival","crowd","security"}),
T("Weapons discipline check for attached wardens; compliance logs signed.","green",{"audit","security","service"}),
T("Guard coordination in a frontier district; watch rotations synchronized with civic militia.","yellow",{"militia","security","frontier"}),
T("Immediate reinforcement during riot signals; cordons established and safe egress opened.","red",{"riot","crowd","security"}),
T("Interdiction rehearsal for barricade incidents; safe corridors mapped.","yellow",{"barricade","security","public"}),
T("High-risk interdiction support for a seized oratory; secure perimeter until authorities arrive.","red",{"siege","security","public"}),
T("Protection detail for water or rail infrastructure after sabotage rumor; sentries posted.","yellow",{"sabotage","infrastructure","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","service"}),
T("Garden pavilion mediation for family inheritance disputes; settlement sealed.","green",{"mediation","service"}),
T("Civic cooperation workshop for rival guilds; shared calendar commitments set.","green",{"guild","coordination","service"}),
T("Inter-shrine calendar alignment meeting; procession timings standardized.","green",{"balance","public","service"}),
T("Quiet counseling for newly formed neighborhood councils; rules of debate issued.","green",{"governance","education","service"}),
T("Catacomb Harmonic Communion support: logistics, spacing, and participant verification.","yellow",{"rite","logistics","service"}),
T("Emergency reconciliation after faction clash; mediators embedded with escorts.","red",{"clash","crowd","security"}),
T("Transit-lane mediation after station dispute; queues reset and monitored.","yellow",{"rail","queue","public"}),
T("Harmonization response after rumor spike; joint bulletin with Harmony Sanctum circulated.","yellow",{"rumor","service","public"}),
T("High-risk crowd calming after panic; sound-liturgical dispersal and safe routes enforced.","red",{"panic","crowd","public"}),
}
},
{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","service"}),
T("Registry update for burials and memorial offerings; records sealed and witnessed.","green",{"archive","audit","service"}),
T("Transition counseling for displaced families; resettlement blessings recorded.","green",{"displacement","relief","service"}),
T("Cemetery perimeter inspection; prevent vandalism during festival nights.","green",{"security","public","service"}),
T("Quiet rites for recovered remains after an accident; soothe panic and rumor.","green",{"public","rumor","service"}),
T("Investigation of graveyard desecration rumor; ward placement and witness interviews.","yellow",{"desecration","investigation","security"}),
T("Relic transfer paperwork verified for a shrine move; escorts scheduled.","yellow",{"escort","security","service"}),
T("Emergency containment after death-cult provocation; controlled rites to prevent escalation.","red",{"provocation","crowd","security"}),
T("High-risk escort of relics through hostile streets; secure route planning with wardens.","red",{"escort","security","public"}),
T("Post-violence reconciliation vigil at a canal memorial; prevent retaliation cycles.","yellow",{"violence","public","mediation"}),
}
},
{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","agri"}),
T("Ecology rite with farmers: pest balance and water stewardship guidance.","green",{"ecology","agri","service"}),
T("Wildlife boundary notices posted; discourage poaching near sacred groves.","green",{"public","security","agri"}),
T("Community foraging orientation; prevent poisoning and rumor spirals.","green",{"education","health","agri"}),
T("Riverbank cleanup day; improve water flow and reduce disease risk.","green",{"water","health","public"}),
T("Containment of illegal logging or poaching rumor; community patrol coordination.","yellow",{"poaching","security","investigation"}),
T("Fire season preparedness for woodland towns; firebreak rites and evacuation path marking.","yellow",{"wildfire","public","security"}),
T("Blight liaison with Verdant Embrace; field sampling routes scheduled.","yellow",{"blight","agri","service"}),
T("Emergency response to wildfire risk; firebreak rites and evacuation path marking.","red",{"wildfire","crisis","public"}),
T("High-risk protection for a threatened grove shrine; cordon and ward reinforcement.","red",{"threat","security","public"}),
}
},
{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","service"}),
T("Festival logistics support; safe routes and water points verified with civic stewards.","green",{"festival","crowd","public"}),
T("Sanctioned tavern-chaplain circuit; reduce brawls and theft through presence.","green",{"public","security","service"}),
T("Reconciliation feast for rival crews; agreements sealed before witnesses.","green",{"mediation","festival","service"}),
T("Public song-rotation at market hours; discourage rumor panic with sanctioned refrains.","green",{"rumor","public","service"}),
T("Containment of illicit revel causing unrest; correction by sanctioned rite and fines.","yellow",{"illicit","order","security"}),
T("Crowd-density enforcement for a stadium day; steward posts assigned.","yellow",{"crowd","festival","security"}),
T("Emergency morale stabilization after violence; controlled gatherings and escorts.","red",{"violence","crowd","public"}),
T("High-risk de-escalation during riot-prone festival; staged dispersal and peace hymns.","red",{"riot","crowd","security"}),
T("Post-panic comfort rites near transit hubs; prevent stampedes and retaliatory fights.","yellow",{"panic","rail","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","public"}),
T("Courtroom courtesy briefing; witness etiquette and silence rules reinforced.","green",{"governance","public","service"}),
T("Market weights-and-measures rite; deter fraud and stabilize pricing disputes.","green",{"audit","service","public"}),
T("Transit node discipline review; platform lines repainted and stewards assigned.","green",{"rail","order","transit"}),
T("Curfew signage and quiet-hours enforcement brief; reduce night disturbances.","green",{"security","public","service"}),
T("Administrative compliance review after complaints; corrective edicts posted.","yellow",{"compliance","governance","service"}),
T("Guard liaison for a threatened civic hall; protocol rehearsal and watch rotation.","yellow",{"security","governance","service"}),
T("Emergency restoration of order after mob action; escorted dispersal and curfew-litany.","red",{"mob","crowd","security"}),
T("High-risk support to authorities during barricade incident; perimeter discipline enforced.","red",{"barricade","security","public"}),
T("Port and rail dispute arbitration; enforce lawful priority lanes for relief cargo.","yellow",{"port","rail","order","relief"}),
}
},
{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","service"}),
T("Archive and library integrity check; catalog drift corrected and access verified.","green",{"archive","audit","service"}),
T("Public instruction on lawful prophecy vs. pamphlet fraud; leaflets distributed.","green",{"education","public","service"}),
T("Scribe training session; improve recordkeeping for relief and stipend flows.","green",{"education","audit","service"}),
T("Observatory-liaison briefing with Conclave Illuminara; shared bulletins standardized.","green",{"service","archive","public"}),
T("Investigation of forged prophecy pamphlets; counterfeit sources traced.","yellow",{"forgery","investigation","service"}),
T("Archive sealing of a sensitive docket after unrest; access reduced and witnessed.","yellow",{"security","archive","service"}),
T("Emergency counter-homily after loudspeaker seizure; sanctioned address delivered.","red",{"broadcast","crisis","public"}),
T("High-risk protection for a key temple scholar; escort and secure lodging arranged.","red",{"threat","security","service"}),
T("Transit bulletin for corridor cities after omen panic; prevent stampedes and conspiracies.","yellow",{"rail","panic","public"}),
}
},
{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","service"}),
T("Food-line volunteer training; prevent cutting and fights through posted rules.","green",{"relief","queue","public"}),
T("Shelter visitation for displaced workers; job referrals routed through civic offices.","green",{"service","relief","governance"}),
T("Agri stipend review for smallholders; vouchers revalidated and extensions logged.","green",{"agri","audit","service"}),
T("Compassion circuit to hospitals and clinics; morale support and supply notes recorded.","green",{"health","service","relief"}),
T("Offering logistics for a major rite; livestock and treasure accounted and sealed.","yellow",{"offering","audit","service"}),
T("Mediation for neighborhood poverty disputes; aid allocations made transparent.","yellow",{"aid","mediation","service"}),
T("Emergency relief surge after incident; soup lines sanctified and guarded.","red",{"relief","crowd","public"}),
T("High-risk protection for a vulnerable procession route; escorts and safe shelters staged.","red",{"procession","security","public"}),
T("Port-side charity intake after a ferry accident; registries updated and families notified.","yellow",{"port","relief","service"}),
}
},
{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","service"}),
T("Infrastructure illumination audit for tunnels and transit nodes; repair notices issued.","green",{"infrastructure","transit","industry"}),
T("Rail signal blessing at key junctions; reduce accidents and rumor panic.","green",{"rail","transit","public"}),
T("Workshop visit: lantern production quality review; defective stock quarantined.","green",{"industry","audit","service"}),
T("Public lantern-route map refreshed for festival season; prevent crush points.","green",{"festival","crowd","public"}),
T("Investigation of signal tampering near a transport hub; seals replaced and watchers posted.","yellow",{"tamper","security","transit"}),
T("Emergency lighting deployment after sabotage; temporary beacons installed with escorts.","red",{"sabotage","crisis","security"}),
T("High-risk stabilization at a transport hub after panic; controlled flow and sanctuary lanes.","red",{"panic","crowd","transit"}),
T("Port navigation-light inspection for fog days; harbor masters briefed.","yellow",{"port","infrastructure","service"}),
T("Tunnel crowd-discipline drill; stewards assigned and entry metering practiced.","yellow",{"crowd","infrastructure","transit"}),
}
},
}
local orderByKey, orderByName, orderKeys, orderNames = {}, {}, {}, {}
for _, o in ipairs(orders) do
orderByKey[o.key] = o
orderByName[o.name] = o
orderKeys[#orderKeys+1] = o.key
orderNames[#orderNames+1] = o.name
end
--------------------------------------------------------------------------------
-- 7.1) ORDER ICONS (from Module:GeneralPortOperations addLogo mapping)
--------------------------------------------------------------------------------
local orderIconFile = {
["Order of Aurora Mystica"]        = "Auroramystica.png",
["Harmony Sanctum"]                = "Harmonysanctum.png",
["Ignis Aeternum"]                = "Ignis Aeternum.png",
["Celestial Harmony Sect"]        = "HarmonySect.png",
["Guild of Golden Shadows"]        = "GoldenShadows.png",
["Azure Sentinel Sect"]            = "AzureSentinel.png",
["Reverie Nebulous"]              = "RevNeb.png",
["Eon Fellowship"]                = "EonFel.png",
["Order of the Umbral Oracle"]    = "Umbral.png",
["Mystery of the Verdant Embrace"] = "Verdant.png",
["Conclave Illuminara"]            = "Conclave.png",
["Sanctum Vitalis"]                = "Vitalis.png",
["Temple Alabaster"]              = "Alabaster.png",
["Court of the Ironclad"]          = "Ironclad.png",
["Accord Concordia"]              = "Concordia.png",
["Mystery of the Stygian Veil"]    = "StygianVe.png",
["Sylvan Fellowship"]              = "Sylvan.png",
["Mystery of Red Mirth"]          = "MirthFlag.png",
["Rex Catonis"]                    = "Catonis.png",
["Sanctum Delphica"]              = "SanctumDel.png",
["Ordo Amicitia"]                  = "OrdoFl.png",
["Temple Illuminata"]              = "AureumFlag.png",
}
local function getOrderIconMarkup(orderName)
local fn = orderIconFile[orderName]
if fn and fileExists(fn) then
return string.format("[[File:%s|20px]] ", fn)
end
return ""
end
--------------------------------------------------------------------------------
-- 8) CITY PROFILES (complete)
--------------------------------------------------------------------------------
local cityProfiles = {
-- =========================
-- CORE MAJORS
-- =========================
["Vaeringheim"] = {
tags={"capital","admin","port","templebank","canals"},
weights={GGS=4,HSA=3,RCA=3,ASS=2,CIL=2,CHS=2,OSI=1},
},
["Luminaria"] = {
tags={"industry","rollingstock","innovation","dawn","infrastructure"},
weights={TIL=4,OAM=3,EOF=2,IGA=2,OSI=1,GGS=1},
},
["Serena"] = {
tags={"civic","lake","hospitality","public"},
weights={HSA=3,ACC=2,OAI=2,ASS=1,CHS=1,OSI=1},
},
["Pyralis"] = {
tags={"volcanic","hazard","fire","reconstruction"},
weights={IGA=4,ASS=2,SCV=2,EOF=1,CIC=1},
baseSev="yellow",
},
["Symphonara"] = {
tags={"civic","arts","festival","public"},
weights={MRM=3,ACC=2,OSI=2,OAM=2,HSA=1,RCA=1},
},
["Aurelia"] = {
tags={"civic","trade","public"},
weights={HSA=2,CHS=2,GGS=2,ACC=1,OSI=1},
},
["Somniumpolis"] = {
tags={"finance","dream","rumorcontrol","noctic"},
weights={REN=3,GGS=3,HSA=2,UOR=2,CIL=2,RCA=1},
baseSev="yellow",
},
["Nexa"] = {
tags={"market","trade","logistics"},
weights={GGS=3,HSA=2,CHS=2,RCA=1,ASS=1,ACC=1},
},
["Lunalis Sancta"] = {
tags={"lunar","archives","pilgrimage","omens"},
weights={CIL=4,REN=2,UOR=2,SDL=1,HSA=1},
},
["Sylvapolis"] = {
tags={"forest","garden","ecology","shrinework"},
weights={SYF=3,MVE=3,OAI=1,HSA=1,ASS=1},
},
-- =========================
-- CORE MINORS
-- =========================
["Saluria"] = {
tags={"civic","artisan","wildlife"},
weights={SYF=2,CHS=2,ACC=1,HSA=1,OAI=1},
},
["Aetherium"] = {
tags={"sky","omens","education"},
weights={CIL=2,OAM=2,CHS=2,OSI=1},
},
["Ferrum Citadel"] = {
tags={"fortress","industry","security","infrastructure"},
weights={CIC=3,RCA=2,TIL=2,EOF=1,IGA=1},
baseSev="yellow",
},
["Acheron"] = {
tags={"frontier","containment","mines","security"},
weights={CIC=3,EOF=2,MSV=1,UOR=1,IGA=1,RCA=1},
baseSev="yellow",
},
["Erythros"] = {
tags={"corridor","trade","security"},
weights={GGS=2,RCA=2,TIL=2,ASS=1,IGA=1},
},
["Catonis Atrium"] = {
tags={"civic","law","order"},
weights={RCA=4,HSA=1,CHS=1,ACC=1},
},
["Delphica"] = {
tags={"oracle","archives","scholarship"},
weights={SDL=4,CIL=2,CHS=1,HSA=1},
},
["Koinonía"] = {
tags={"harmony","mediation","civic"},
weights={ACC=3,HSA=3,CHS=1,OAI=1},
},
["Aureum"] = {
tags={"wealth","craft","trade","finance"},
weights={GGS=4,HSA=1,RCA=1,ALB=1},
},
-- =========================
-- ALPERKIN
-- =========================
["The Alpazkigz"] = {
tags={"tribal","festival","intercession","mediation"},
weights={ACC=3,HSA=2,CHS=2,OAI=1,CIC=1},
baseSev="yellow",
},
["Odiferia"] = {
tags={"noctic","rumorcontrol","dream","folk"},
weights={REN=3,HSA=2,GGS=2,UOR=2,MSV=1},
baseSev="yellow",
},
-- =========================
-- NEW SOUTH JANGSONG (MAJOR)
-- =========================
["Skýrophos"] = {
tags={"integration","civic","corridor","governance"},
weights={CHS=2,HSA=2,RCA=2,TIL=2,EOF=2,ACC=1},
},
["Bjornopolis"] = {
tags={"north","fortified","industry","civic"},
weights={EOF=3,CIC=2,TIL=2,ASS=1,RCA=1},
baseSev="yellow",
},
["Aegirheim"] = {
tags={"north","coastal","harbor","watch"},
weights={ASS=3,EOF=2,CIC=1,TIL=1},
baseSev="yellow",
},
["Norsolyra"] = {
tags={"agriculture","wetlands","river","supply"},
weights={MVE=3,ASS=2,SYF=2,OAI=1,SCV=1},
},
["Thorsalon"] = {
tags={"north","vineyards","festival","supply"},
weights={MVE=2,MRM=2,EOF=2,OAI=1,ASS=1},
baseSev="yellow",
},
-- =========================
-- NEW SOUTH JANGSONG (MINOR)
-- =========================
["Pelagia"] = {
tags={"coastal","distribution","water","relief"},
weights={ASS=3,SCV=2,HSA=1,EOF=1},
},
["Myrene"] = {
tags={"coastal","civic","trade"},
weights={ASS=2,HSA=2,CHS=1,ACC=1},
},
["Thyrea"] = {
tags={"agriculture","market","relief"},
weights={MVE=3,OAI=2,SYF=1,HSA=1},
},
["Ephyra"] = {
tags={"forest","frontier","watch","omens"},
weights={SYF=3,UOR=2,EOF=1,CIC=1},
baseSev="yellow",
},
["Halicarn"] = {
tags={"archives","scholarship","civic"},
weights={SDL=3,CIL=2,CHS=1,HSA=1},
},
-- =========================
-- HAIFAN BASSARIDIA (MAJOR)
-- =========================
["Keybir-Aviv"] = {
tags={"port","trade","gateway","security"},
weights={ASS=3,GGS=2,HSA=2,RCA=1,CIC=1},
baseSev="yellow",
},
["Tel-Amin"] = {
tags={"plantation","coffee","trade","agriculture"},
weights={MVE=3,OAI=2,SYF=2,GGS=1,ASS=1},
},
["Diamandis"] = {
tags={"luxury","trade","vaulting","security"},
weights={GGS=4,RCA=2,ALB=1,HSA=1},
baseSev="yellow",
},
["Jogi"] = {
tags={"agroprocessing","market","ferry","trade"},
weights={MVE=3,OAI=2,ASS=2,SYF=1,GGS=1},
},
["Lewisburg"] = {
tags={"rail","hub","logistics","governance"},
weights={TIL=4,RCA=3,GGS=2,HSA=1,EOF=1,ASS=1},
baseSev="yellow",
},
-- =========================
-- HAIFAN BASSARIDIA (MINOR)
-- =========================
["Thermosalem"] = {
tags={"healing","resort","water","crowd"},
weights={SCV=3,ASS=2,HSA=2,ACC=1,RCA=1},
baseSev="yellow",
},
["Akróstadium"] = {
tags={"stadium","festival","crowd","public"},
weights={MRM=3,OSI=2,ACC=2,HSA=1},
baseSev="yellow",
},
["Sufriya"] = {
tags={"terminal","security","port","logistics"},
weights={ASS=3,GGS=2,CIC=2,RCA=1,TIL=1},
baseSev="yellow",
},
["Lykopolis"] = {
tags={"rail","corridor","security","logistics"},
weights={TIL=4,RCA=2,EOF=2,CIC=1,GGS=1},
baseSev="yellow",
},
-- =========================
-- NORMARK
-- =========================
["Ardclach"] = {
tags={"north","ferry","coastal","watch"},
weights={ASS=3,EOF=2,CIC=1,GGS=1},
baseSev="yellow",
},
["Riddersborg"] = {
tags={"north","rail","corridor","storm"},
weights={EOF=3,TIL=2,CIC=2,ASS=1,RCA=1},
baseSev="yellow",
},
-- =========================
-- OURIANA
-- =========================
["Bashkim"] = {
tags={"rail","corridor","agriculture","logistics"},
weights={MVE=2,TIL=3,RCA=2,OAI=1,EOF=1},
baseSev="yellow",
},
["Ourid"] = {
tags={"river","fisheries","supply","trade"},
weights={ASS=3,MVE=2,OAI=2,HSA=1},
},
["Tonar"] = {
tags={"quarantine","doctrine","containment","security"},
weights={ALB=3,CIC=3,RCA=2,HSA=1,SDL=1},
baseSev="red",
},
-- =========================
-- CALEDONIAN DEPENDENCIES
-- =========================
["Fanghorn"] = {
tags={"frontier","forests","security","watch"},
weights={EOF=3,CIC=2,SYF=2,UOR=1,RCA=1},
baseSev="yellow",
},
["Eikbu"] = {
tags={"ranching","agriculture","trail","supply"},
weights={MVE=3,SYF=2,OAI=2,EOF=1},
},
["Galvø"] = {
tags={"island","coastal","port","storm"},
weights={ASS=3,EOF=2,CIC=1},
baseSev="yellow",
},
["Sårensby"] = {
tags={"rail","corridor","industry","civic"},
weights={TIL=3,EOF=2,RCA=2,GGS=1,ASS=1},
baseSev="yellow",
},
["Slevik"] = {
tags={"north","security","civic"},
weights={EOF=3,CIC=2,ASS=1,RCA=1},
baseSev="yellow",
},
["Sjøsborg"] = {
tags={"coastal","port","trade","watch"},
weights={ASS=3,EOF=2,GGS=1},
baseSev="yellow",
},
["Storesund"] = {
tags={"coastal","trade","storm"},
weights={ASS=3,EOF=2,CIC=1},
baseSev="yellow",
},
["Notranskja"] = {
tags={"north","frontier","watch","omens"},
weights={EOF=2,CIC=2,UOR=2,ASS=1},
baseSev="yellow",
},
["Kaledonija"] = {
tags={"regionalhub","governance","trade","civic"},
weights={RCA=3,EOF=2,GGS=2,HSA=1,ASS=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
return "Other"
end
local function extractCompanyName(cell)
local n = cell:match("'''(.-)'''")
if n and n ~= "" then return stripWiki(n) end
n = cell:match("%*%*(.-)%*%*")
if n and n ~= "" then return stripWiki(n) end
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
if s:lower():find("fleet", 1, true) then return nil end
if s:lower():find("no fixed", 1, true) then return nil end
s = s:gsub("%s*;%s*.*$", "")
s = s:gsub("%s*%b()%s*", " ")
s = normalizeSpaces(s)
s = trim(s)
if citySet[s] then return s 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
local startPos = wikitext:find("====%s*Company Table%s*====")
if not startPos then startPos = wikitext:find("####%s*Company Table") end
if not startPos then 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)
local rows = {}
for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do
rows[#rows+1] = chunk
end
local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}")
if lastChunk then rows[#rows+1] = lastChunk end
for _, row in ipairs(rows) do
if not row:match("^%s*!") then
local line = normalizeSpaces(row:gsub("\n", " "))
line = line:gsub("^%s*|%s*", "")
local cells = {}
for cell in line:gmatch("([^|]-)%s*||%s*") do
cells[#cells+1] = trim(cell)
end
local tail = line:match("||%s*([^|].-)%s*$")
if tail and tail ~= "" then
if #cells == 0 or cells[#cells] ~= trim(tail) then
cells[#cells+1] = trim(tail)
end
end
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] or 0) + 1
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
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- WX (Ops Alert) integration from Module:BassaridiaForecast
-- 10) OPTIONAL: Threat Watch integration from Module:ReligiousEvents
-- Used to display a city-level ops badge + bump severity + append weather-driven tasking.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
local function parseReligiousEventsTable(wikitext)
if not wxOk then wx = nil end
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
--------------------------------------------------------------------------------
local function weightedPickKey(nextInt, weights)
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 wxColor(level)
local function buildTagSet(tags)
level = tonumber(level) or 0
local set = {}
if level == 3 then return "#f8d7da" end
for _, t in ipairs(tags or {}) do set[t] = true end
if level == 2 then return "#ffe5cc" end
return set
if level == 1 then return "#fff3cd" end
return "#d4edda"
end
end


local function opsCell(cityName, year, dayOfYear)
local function pickTaskForCity(nextInt, taskList, tagSet)
if not wx or type(wx.getOpsData) ~= "function" then return "n/a", nil end
local candidates = {}
local op = wx.getOpsData(cityName, year, dayOfYear)
for _, task in ipairs(taskList or {}) do
if not op or not op.alert then return "n/a", nil end
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 lvl  = tonumber(op.alert.level) or 0
local function baseWeights()
local label = tostring(op.alert.label or "Green")
local w = {}
local cond  = tostring(op.condition or "")
for _, ok in ipairs(orderKeys) do w[ok] = 1 end
local bg    = wxColor(lvl)
return w
end


local cell = string.format(
local function applyWeights(w, add)
"<span style='background:%s;padding:2px 6px;border:1px solid #ccc;border-radius:10px;white-space:nowrap;'>%s</span>%s",
if not add then return end
bg, label, (cond ~= "" and ("<br/><span style='font-size:90%;'>" .. cond .. "</span>") or "")
for k, v in pairs(add) do
)
if orderByKey[k] then
return cell, op
w[k] = (w[k] or 0) + tonumber(v or 0)
end
end
end
end


local function bumpSevWeather(sev, op)
local function biasFromEcon(w, econTag)
if not op or not op.alert then return sev end
if econTag == "agri" then
local lvl = tonumber(op.alert.level) or 0
applyWeights(w, {MVE=3,SYF=2,OAI=2,ASS=1})
local imp = op.impacts or {}
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


-- Only Orange/Red force severity bumps (Yellow stays informational)
local function biasFromCorridor(w, isRail, isFerry)
if lvl == 2 and sev == "green" then sev = "yellow" end
if isRail then applyWeights(w, {TIL=2,RCA=2,GGS=1,EOF=1,ASS=1}) end
if lvl >= 3 then sev = "red" end
if isFerry then applyWeights(w, {ASS=2,GGS=1,RCA=1}) end
end


-- Hard constraints should always push to at least yellow
local function biasFromTier(w, tier)
if (imp.ground == "Hazard" or imp.sea == "Closed" or imp.air == "No-Go") and sev == "green" then
if tier == "major" then
sev = "yellow"
applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1})
end
end
end
--------------------------------------------------------------------------------
-- 12) TEMPLE BANK NOTES
--------------------------------------------------------------------------------
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"
if cityProfile and cityProfile.baseSev then
sev = cityProfile.baseSev
end
if econTag == "industry" or econTag == "service" then
if sev == "green" then sev = "yellow" end
end
if threatLevel == "yellow" and sev == "green" then sev = "yellow" end
if threatLevel == "red" and sev ~= "red" then sev = "red" end
return sev
return sev
end
end


local function weatherTaskOverlay(op)
local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
if not op or not op.alert then return "" end
local cityName, cityRegion, tier = c.name, c.region, c.tier
local lvl = tonumber(op.alert.level) or 0
local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
if lvl < 2 then return "" end -- only Orange/Red appends operational language
local nextInt = lcg(hash31(seedStr))
 
local profile = cityProfiles[cityName] or {tags={}, weights=nil}
local tags = {}
 
for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
if RAIL[cityName] then tags[#tags+1] = "rail" end
if FERRY[cityName] then tags[#tags+1] = "ferry" end
 
local sig = econSignals and econSignals[cityName] or nil
local dom = dominantSector(sig)
local econTag = econTagFromSector(dom)
tags[#tags+1] = econTag
 
local tagSet = buildTagSet(tags)
 
local th = includeThreat and threatMap and threatMap[cityName] or nil
local tSev = threatSev(th)
 
local w = baseWeights()
biasFromTier(w, tier)
biasFromEcon(w, econTag)
biasFromCorridor(w, RAIL[cityName], FERRY[cityName])
applyWeights(w, profile.weights)
 
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)
 
local unitName = pickOrgUnit(nextInt, sev)
local unitMeta = org[unitName]
local unitIndex = (nextInt() % 12) + 1
local unitCode = string.format("%s-%s%d", o.key, unitMeta.code, unitIndex)
local headcount = unitMeta.size
 
local bank = pickFromList(nextInt, bankNotes, nil)
 
local threatText, 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", getCityFlagMarkup(cityName), cityLink(cityName)),
order = getOrderIconMarkup(o.name) .. 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()


local haz = tostring(op.alert.hazard or "weather disruption")
local threatMap = {}
local imp = op.impacts or {}
if includeThreat then
local cond = tostring(op.condition or "")
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
threatMap = parseReligiousEventsTable(rel.makeTable())
end
end


local base = " Weather-driven tasking: " .. haz .. " (" ..
local out = {}
string.format("Gnd %s / Sea %s / Air %s", imp.ground or "OK", imp.sea or "OK", imp.air or "OK") .. ")."
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)


if cond == "Storm" or cond == "Rain" then
local count = 0
return base .. " Queue stabilization at clinics/relief points; rumor containment bulletins posted; shrine-district patrol lanes marked."
for _, c in ipairs(cityOrder) do
elseif cond == "Blizzard" or cond == "Snow" then
if regionPass(regionFilter, c.region) then
return base .. " Warming-shelter checks; fuel and medicine custody verification; travel guidance posted at shrine gates."
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
elseif cond == "Fog" then
table.insert(out, "|-" .. row.style)
return base .. " Navigation warnings and crowd guidance in canal/port corridors; panic narratives suppressed early."
 
elseif cond == "Dust" then
local line = string.format("| %s || %s || %s || %s || %s || %s",
return base .. " Mask discipline, indoor refuge advisories, and vendor hygiene enforcement prioritized."
row.date, row.city, row.region, row.order, row.unit, row.task
else
)
return base .. " Welfare checks and public messaging prioritized."
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 wantedOrder = orderByName[wanted]
if not wantedOrder then
return "''Error: unknown order name (must match module order names).''"
end
 
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
 
local di = getDateInfo()
local econSignals = getEconSignals()
 
local threatMap = {}
if includeThreat then
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
threatMap = parseReligiousEventsTable(rel.makeTable())
end
end
 
local out = {}
table.insert(out, "{| class='wikitable sortable'")
local header = "! Date !! City !! Region !! 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:find(wantedOrder.link, 1, true) 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
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
end
return p

Revision as of 21:29, 1 January 2026

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

-- Module:MissionaryDailyOpsBVR (REWRITE + FLAG RESOLVER + CULT ICONS)
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Deterministic output (no math.random / no os.time seeding).
--
-- Features:
--  • City profiles (culture/spiritual tags + per-city order bias)
--  • Economic signals parsed 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).
--  • City flags: existence-checked; fallback to Imperial Trade Union flag when missing.
--  • Cult/Order icons: from Module:GeneralPortOperations addLogo(...) mapping.
--
-- 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("''", "")
	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
--------------------------------------------------------------------------------
local function parseCalendarString(s)
	if type(s) ~= "string" then return nil end
	local dayOfYear, month, year = s:match("^(%d+),%s+([%a]+)%s+%b()%s*,%s*(%d+)%s+PSSC")
	if dayOfYear and year then
		return tonumber(year), tonumber(dayOfYear), month
	end
	return nil
end

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

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

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

--------------------------------------------------------------------------------
-- 2) DETERMINISTIC RNG
--------------------------------------------------------------------------------
local function hash31(str)
	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" 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 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

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

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

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

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

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

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

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

	-- 2) try normalized candidates
	local norm = normalizeForFlag(cityName)
	local ascii = asciiNormalizeForFlag(cityName)
	local candidates = {
		norm .. "Flag.png",
		ascii .. "Flag.png",
	}

	for _, fn in ipairs(candidates) do
		if fileExists(fn) then
			local mk = string.format("[[File:%s|20px]]", fn)
			cityFlagMarkupCache[cityName] = mk
			return mk
		end
	end

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

--------------------------------------------------------------------------------
-- 5) CORRIDORS / ROLE HINTS (hard-coded)
--------------------------------------------------------------------------------
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,
}
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 (tagged)
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
	return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
end

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

local orders = {
	{key="OSI", name="Ordo Solis Invicti",
		link=orderLink("Atos (Reformed Stripping Path)","Ordo_Solis_Invicti","Ordo Solis Invicti"),
		tasks={
			T("Dawn rites and public illumination drill to restore civic clarity and discourage rumor cascades in market wards.","green",{"public","education","service"}),
			T("Temple-school literacy session paired with cosmology instruction and shrine maintenance inspections.","green",{"education","shrine","service"}),
			T("Solar route inspection at major junctions; traffic etiquette posted for pilgrims and commuters.","green",{"transit","rail","public"}),
			T("Sun-pledge renewal for civic clerks; oath sheets sealed and logged with Temple Bank scribes.","green",{"governance","audit","service"}),
			T("Public lamp and beacon blessing for tunnel nodes; wayfinding clarified for night labor shifts.","green",{"infrastructure","transit","industry"}),
			T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit","service"}),
			T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd","public"}),
			T("Counter-broadsheet sermon cycle following unsigned pamphlets; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
			T("Joint patrol-litany along rail platforms to reduce theft and panic during peak transit hours.","yellow",{"rail","security","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","service"}),
			T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.","green",{"omen","public","service"}),
			T("Quiet home-visits to recent migrants; orientation blessings and sanctioned neighborhood guides issued.","green",{"public","service","mediation"}),
			T("Rooftop shrine upkeep and dawn-banner replacement; civic lanes kept clear for morning processions.","green",{"shrine","infrastructure","public"}),
			T("Pilgrim flow timing review for a dawn festival; route markers placed to prevent crush points.","green",{"festival","crowd","public"}),
			T("Civic blessing for new housing blocks; routing of foot-traffic to prevent crush points and misdirection.","yellow",{"crowd","infrastructure","service"}),
			T("Counter-heresy sermon cycle following illicit broadsides; witness interviews conducted with local elders.","yellow",{"heresy","investigation","public"}),
			T("Dawn mediation under escort after a workplace dispute threatens walkouts; settlement notarized.","yellow",{"mediation","industry","governance"}),
			T("Omen-risk advisory for coastal weather or fog days; port stewards briefed and signage posted.","yellow",{"port","public","service"}),
			T("Rapid-response pacification after arson rumor; dawn procession under guard with emergency stipend release.","red",{"unrest","arson","crowd"}),
		}
	},

	{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","service"}),
			T("Harmony clinic for stress and rumor contagion; quiet-room rotations and guided breathing rites.","green",{"health","rumor","service"}),
			T("Queue-discipline coaching at relief points; volunteers trained to prevent confrontations.","green",{"order","queue","relief"}),
			T("Workplace concord circle for service districts; complaint channels clarified and posted.","green",{"service","mediation","governance"}),
			T("Inter-shrine courtesy visit to reduce rivalry; shared calendar commitments established.","green",{"balance","mediation","public"}),
			T("Loyalty and morale audit of a civic office after complaint surge; corrective training scheduled.","yellow",{"audit","governance","service"}),
			T("Containment of a cult schism meeting; supervised dialogue and sanction warnings delivered.","yellow",{"schism","containment","public"}),
			T("Rumor forensics on a circulating ‘forbidden rite’ claim; witness tree compiled and forwarded.","yellow",{"rumor","investigation","public"}),
			T("Crowd-stabilization drill for festival season; steward assignments and safe exits mapped.","yellow",{"festival","crowd","public"}),
			T("Major crowd-stabilization operation after riot signals; staged dispersal with peace-liturgy and escorts.","red",{"riot","crowd","public"}),
		}
	},

	{key="IGA", name="Ignis Aeternum",
		link=orderLink("Pyros (Reformed Stripping Path)","Ignis_Aeternum","Ignis Aeternum"),
		tasks={
			T("Controlled fire blessing for workshops; safety rite doubled as inspection of fuel and storage.","green",{"industry","safety"}),
			T("Kiln-and-forge consecration for a new artisan street; insurance seals verified.","green",{"industry","infrastructure","service"}),
			T("Firebreak marking for dry-season hills; public notices issued to farmers and herders.","green",{"agri","fire","public"}),
			T("Hearth-blessing round for crowded tenements; cooking hazards corrected with wardens.","green",{"public","safety","service"}),
			T("Candle and incense quality check at shrine vendors; adulterants rejected and logged.","green",{"shrine","audit","service"}),
			T("Purification burn at a compromised shrine-store; ash sealed and catalogued for Temple Bank disposal.","yellow",{"contam","shrine","service"}),
			T("Investigation of incendiary threats near a gate-temple; firebreak plan issued to local wardens.","yellow",{"arson","investigation","security"}),
			T("Explosive-storage compliance check near port or rail yards; seals renewed and routes revised.","yellow",{"rail","port","security","industry"}),
			T("Emergency response to active arson or explosive rumor; sanctified suppression teams deployed with escorts.","red",{"arson","crisis","security"}),
			T("Post-incident purification circuit across multiple oratories; public confidence restored by nightfall.","red",{"incident","public","crowd"}),
		}
	},

	{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","service"}),
			T("Star-chart instruction for apprentices; quiet observatory checks and record sealing.","green",{"education","astronomy","service"}),
			T("Inter-city liaison packet prepared for corridor towns; travel permissions confirmed.","green",{"rail","service","governance"}),
			T("Boundary-harmony rite at a district line; neighbor shrines exchange tokens under witness.","green",{"balance","public","mediation"}),
			T("Calm briefing for merchant captains on festival timing; reduce congestion and rumor spread.","green",{"port","service","public"}),
			T("Faction-balance mediation between rival shrines; written compromise placed in Temple Bank archive.","yellow",{"balance","mediation","archive"}),
			T("Rumor forensics on ‘forbidden rite’ claims; witness tree compiled and forwarded to Temple authorities.","yellow",{"rumor","investigation","public"}),
			T("Quiet arbitration after contract dispute between a guild and a council office.","yellow",{"service","governance","mediation"}),
			T("Transit coordination for high-profile delegates; platform escorts and crowd lanes established.","yellow",{"rail","crowd","security"}),
			T("High-risk stabilization after cult confrontation; negotiators embedded with escorts to prevent escalation.","red",{"confrontation","crowd","security"}),
		}
	},

	{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","service"}),
			T("Merchant vetting interviews for a sensitive consignment; anonymity rites certified.","green",{"merchant","security","service"}),
			T("Port-side seal verification for sacred cargo; chain-of-custody stamped and witnessed.","green",{"port","finance","security"}),
			T("Rail consignment escort paperwork checked; handover points confirmed with station stewards.","green",{"rail","service","security"}),
			T("Quiet fraud-prevention briefing for shrine treasurers; common forgery patterns circulated.","green",{"finance","education","service"}),
			T("Counter-counterfeit sweep targeting forged stamps and false Temple consignments; evidence boxed and logged.","yellow",{"forgery","investigation","finance"}),
			T("Debt dispute arbitration for a sanctioned charity line; terms rewritten and sealed.","yellow",{"finance","mediation","service"}),
			T("Watchlist update for suspected smugglers using festival crowds; surveillance plan filed.","yellow",{"security","crowd","port"}),
			T("Recovery detail for stolen reliquary property; surveillance and lawful seizure under civic warrant.","red",{"theft","security","investigation"}),
			T("Crisis finance stabilization after panic; emergency stipend routing and rumor suppression coordination.","red",{"panic","finance","service"}),
		}
	},

	{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","service"}),
			T("Harbor hygiene briefing for fish markets; cold-chain and wash stations checked.","green",{"port","health","service"}),
			T("Flood-gate inspection rite; maintenance tickets issued and signed by civic stewards.","green",{"infrastructure","water","service"}),
			T("Ferry pier blessing and rope inspection; boarding lines reorganized to reduce accidents.","green",{"ferry","port","public"}),
			T("Reservoir vigil for drought days; ration etiquette posted at distribution points.","green",{"water","relief","public"}),
			T("Harbor-side watch for sabotage indicators; shrine-wardens trained on lock and pier checks.","yellow",{"port","sabotage","security"}),
			T("Storm or flood readiness rite; evacuation hymn routes verified with civic stewards.","yellow",{"flood","infrastructure","public"}),
			T("Waterborne rumor containment bulletin; discourage panic buying and hoarding.","yellow",{"rumor","service","public"}),
			T("Quarantine support for suspected waterborne illness; controlled distribution points blessed and monitored.","red",{"outbreak","water","health"}),
			T("Major incident response: water infrastructure damage; emergency sanctification and ration-order enforcement.","red",{"infrastructure","crisis","public"}),
		}
	},

	{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","service"}),
			T("Nocturne counseling for insomnia cluster; soft-lit rites and chant rotation schedules.","green",{"health","sleep","service"}),
			T("Public guidance sheet on lawful visions vs. sedition pamphlets; distributed at shrines.","green",{"education","public","service"}),
			T("Quiet household visits after a death or displacement; stabilize grief cycles with sanctioned imagery.","green",{"service","health","public"}),
			T("Festival mood stewardship: prevent illicit revel by offering sanctioned evening rites.","green",{"festival","public","morale"}),
			T("Investigation of forbidden homily broadcasts; witness interviews and calm messaging drafted.","yellow",{"broadcast","investigation","service"}),
			T("Rumor cascade mapping in service districts; identify the first carrier nodes.","yellow",{"rumor","investigation","service"}),
			T("Containment of mass-vision panic; staged public reassurance with tightly controlled procession.","red",{"panic","crowd","public"}),
			T("Stabilization after hallucination-linked violence; liaison with Harmony Sanctum for long-tail care.","red",{"violence","health","service"}),
			T("Emergency night curfew-support rite; safe corridors marked to reduce shadow-fear incidents.","yellow",{"security","public","service"}),
		}
	},

	{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","service"}),
			T("Infrastructure endurance audit: roofs, walls, and bridges; damage triage list issued.","green",{"infrastructure","audit","industry"}),
			T("Shelter staffing rotation planned for storm season; volunteer rosters sealed.","green",{"relief","service","governance"}),
			T("Rail convoy support brief for frontier deliveries; pacing and rest points set.","green",{"rail","logistics","service"}),
			T("Queue discipline at distribution points; conflict triggers reduced through signage.","green",{"order","queue","public"}),
			T("Conflict de-escalation for resource disputes; binding queue rules posted at distribution points.","yellow",{"order","queue","governance"}),
			T("Emergency shelter activation after storm loss; warm-hall rites and controlled entry.","red",{"storm","relief","public"}),
			T("Frontier outpost morale visit; endurance rites and supply accountability review.","yellow",{"frontier","audit","service"}),
			T("Bridge closure coordination after damage report; detours posted and escorted.","yellow",{"infrastructure","transit","public"}),
			T("High-risk escort for frontier convoys under threat; endurance cadres embedded with wardens.","red",{"frontier","security","logistics"}),
		}
	},

	{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","service"}),
			T("Shadow-lane safety marking near canals and alleys; lantern placement logged.","green",{"public","security","transit"}),
			T("Quiet counsel for a magistrate; truth-testing rite witnessed by two scribes.","green",{"governance","inquiry","service"}),
			T("Missing-persons intake desk hosted at a shrine; reports standardized and forwarded.","green",{"service","investigation","public"}),
			T("Nighttime rumor monitoring in taverns and stations; early interventions logged.","green",{"rumor","rail","service"}),
			T("Discrete consultation for a magistrate; truth-testing rite witnessed by two scribes.","yellow",{"governance","inquiry","audit"}),
			T("Shadow-route mapping after disappearances; safe-path markers installed with civic approval.","yellow",{"security","transit","public"}),
			T("Evidence sealing after a midnight incident; chain-of-custody verified for Temple authorities.","yellow",{"investigation","security","service"}),
			T("Emergency intervention after ‘demonic sign’ sightings; controlled crowd handling and ward placement.","red",{"demonic","panic","crowd"}),
			T("High-risk extraction of a threatened cantor; night escort to secure sanctuary.","red",{"abduction","security","service"}),
		}
	},

	{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","agri"}),
			T("Garden-shrine restoration day with community labor; devotional planting aligned to calendar.","green",{"garden","shrine","agri"}),
			T("Seed and tool stipend audit for smallholders; vouchers revalidated.","green",{"audit","agri","service"}),
			T("Irrigation channel blessing and silt-clearing volunteer day; water etiquette posted.","green",{"water","agri","public"}),
			T("Market stall purity checks for produce; vendors instructed on safe storage.","green",{"public","health","agri"}),
			T("Food shortage mediation and ration arbitration; Temple Bank stipend adjustments proposed.","yellow",{"shortage","logistics","agri"}),
			T("Pest or blight containment rites; field inspections with farmers’ associations.","yellow",{"blight","agriculture","agri"}),
			T("Livestock procession routing for festival markets; prevent crush and theft.","yellow",{"crowd","festival","agri"}),
			T("Famine-risk surge response; emergency distribution queues sanctified and monitored.","red",{"famine","relief","agri"}),
			T("Emergency containment of contaminated grain rumor; sample testing coordinated with Sanctum Vitalis.","yellow",{"rumor","health","agri"}),
		}
	},

	{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","service"}),
			T("Night-sky bulletin posted to reduce panic during overcast weeks; lawful interpretations provided.","green",{"public","rumor","service"}),
			T("Observatory instrument check; calibration seals signed and archived.","green",{"archive","audit","service"}),
			T("Lantern route blessing for tunnels and arcades; wayfinding clarified for late shifts.","green",{"transit","public","service"}),
			T("Quiet archive preservation: scroll humidity checks and secure storage review.","green",{"archive","service","audit"}),
			T("Archive sealing of sensitive liturgies; access logs reviewed for anomalies.","yellow",{"archive","audit","security"}),
			T("Public lantern rite to counter rumor cascades; controlled routes and timing set with wardens.","yellow",{"rumor","crowd","public"}),
			T("Transit hub advisory after night panic; crowd lanes re-drawn and posted.","yellow",{"rail","crowd","transit"}),
			T("Emergency omen bulletin after night panic; official statement drafted and delivered.","red",{"panic","public","service"}),
			T("High-risk protection of an observatory site under threat; wards reinforced and watchers stationed.","red",{"threat","security","service"}),
		}
	},

	{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","service"}),
			T("Sanctioned healer coordination meeting; triage roles rehearsed for outbreaks.","green",{"training","health","service"}),
			T("Hygiene signage deployment at markets; handwash stations verified.","green",{"public","health","service"}),
			T("Nutrition counseling for ration lines; supplement vouchers distributed under Temple Bank authorization.","green",{"nutrition","relief","service"}),
			T("Quiet home checks for vulnerable households; prevent panic spirals after illness rumors.","green",{"service","health","rumor"}),
			T("Supplement and ration fortification distribution under Temple Bank authorization.","yellow",{"nutrition","relief","service"}),
			T("Outbreak protocol rehearsal with civic healers; spacing rules posted at shrines.","yellow",{"outbreak","training","public"}),
			T("Quarantine lane setup drill at a terminal or station; entry control rehearsed.","yellow",{"rail","security","health"}),
			T("Active outbreak response: triage lanes established; quarantine support and calm messaging.","red",{"outbreak","crisis","public"}),
			T("Mass-casualty contingency drill after violence reports; intake points and transport routes mapped.","red",{"violence","logistics","service"}),
		}
	},

	{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","service"}),
			T("Doctrinal reading circle for novices; sanctioned texts distributed with margin notes.","green",{"education","service","public"}),
			T("Shrine inventory check for relic storage; locks and witness rules enforced.","green",{"security","shrine","audit"}),
			T("Quiet correction of unsanctioned household rites; guidance issued without spectacle.","green",{"service","public","education"}),
			T("Blessing of new white-draped oratory; entry etiquette posted to reduce disorder.","green",{"shrine","public","service"}),
			T("Doctrinal correction session after unsigned broadsides; sanctioned texts distributed.","yellow",{"heresy","education","public"}),
			T("Sanctuary security review for valuables; locking discipline and witness rules enforced.","yellow",{"security","shrine","service"}),
			T("Investigation of tainted incense vendors; samples seized and chain-of-custody logged.","yellow",{"poison","investigation","service"}),
			T("Emergency purification after poisoned incense discovery; wards placed and offenders sought.","red",{"poison","crisis","security"}),
			T("High-risk protection of a central temple during unrest; entry control and calm liturgy.","red",{"unrest","security","crowd"}),
		}
	},

	{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","service"}),
			T("Escort for a threatened courier; route cleared and handover documented.","green",{"escort","security","service"}),
			T("Patrol coordination with civic guards at terminals; hand signals and cordon rules standardized.","green",{"rail","security","service"}),
			T("Gatehouse posture review for festival season; crowd lanes chalked and posted.","green",{"festival","crowd","security"}),
			T("Weapons discipline check for attached wardens; compliance logs signed.","green",{"audit","security","service"}),
			T("Guard coordination in a frontier district; watch rotations synchronized with civic militia.","yellow",{"militia","security","frontier"}),
			T("Immediate reinforcement during riot signals; cordons established and safe egress opened.","red",{"riot","crowd","security"}),
			T("Interdiction rehearsal for barricade incidents; safe corridors mapped.","yellow",{"barricade","security","public"}),
			T("High-risk interdiction support for a seized oratory; secure perimeter until authorities arrive.","red",{"siege","security","public"}),
			T("Protection detail for water or rail infrastructure after sabotage rumor; sentries posted.","yellow",{"sabotage","infrastructure","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","service"}),
			T("Garden pavilion mediation for family inheritance disputes; settlement sealed.","green",{"mediation","service"}),
			T("Civic cooperation workshop for rival guilds; shared calendar commitments set.","green",{"guild","coordination","service"}),
			T("Inter-shrine calendar alignment meeting; procession timings standardized.","green",{"balance","public","service"}),
			T("Quiet counseling for newly formed neighborhood councils; rules of debate issued.","green",{"governance","education","service"}),
			T("Catacomb Harmonic Communion support: logistics, spacing, and participant verification.","yellow",{"rite","logistics","service"}),
			T("Emergency reconciliation after faction clash; mediators embedded with escorts.","red",{"clash","crowd","security"}),
			T("Transit-lane mediation after station dispute; queues reset and monitored.","yellow",{"rail","queue","public"}),
			T("Harmonization response after rumor spike; joint bulletin with Harmony Sanctum circulated.","yellow",{"rumor","service","public"}),
			T("High-risk crowd calming after panic; sound-liturgical dispersal and safe routes enforced.","red",{"panic","crowd","public"}),
		}
	},

	{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","service"}),
			T("Registry update for burials and memorial offerings; records sealed and witnessed.","green",{"archive","audit","service"}),
			T("Transition counseling for displaced families; resettlement blessings recorded.","green",{"displacement","relief","service"}),
			T("Cemetery perimeter inspection; prevent vandalism during festival nights.","green",{"security","public","service"}),
			T("Quiet rites for recovered remains after an accident; soothe panic and rumor.","green",{"public","rumor","service"}),
			T("Investigation of graveyard desecration rumor; ward placement and witness interviews.","yellow",{"desecration","investigation","security"}),
			T("Relic transfer paperwork verified for a shrine move; escorts scheduled.","yellow",{"escort","security","service"}),
			T("Emergency containment after death-cult provocation; controlled rites to prevent escalation.","red",{"provocation","crowd","security"}),
			T("High-risk escort of relics through hostile streets; secure route planning with wardens.","red",{"escort","security","public"}),
			T("Post-violence reconciliation vigil at a canal memorial; prevent retaliation cycles.","yellow",{"violence","public","mediation"}),
		}
	},

	{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","agri"}),
			T("Ecology rite with farmers: pest balance and water stewardship guidance.","green",{"ecology","agri","service"}),
			T("Wildlife boundary notices posted; discourage poaching near sacred groves.","green",{"public","security","agri"}),
			T("Community foraging orientation; prevent poisoning and rumor spirals.","green",{"education","health","agri"}),
			T("Riverbank cleanup day; improve water flow and reduce disease risk.","green",{"water","health","public"}),
			T("Containment of illegal logging or poaching rumor; community patrol coordination.","yellow",{"poaching","security","investigation"}),
			T("Fire season preparedness for woodland towns; firebreak rites and evacuation path marking.","yellow",{"wildfire","public","security"}),
			T("Blight liaison with Verdant Embrace; field sampling routes scheduled.","yellow",{"blight","agri","service"}),
			T("Emergency response to wildfire risk; firebreak rites and evacuation path marking.","red",{"wildfire","crisis","public"}),
			T("High-risk protection for a threatened grove shrine; cordon and ward reinforcement.","red",{"threat","security","public"}),
		}
	},

	{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","service"}),
			T("Festival logistics support; safe routes and water points verified with civic stewards.","green",{"festival","crowd","public"}),
			T("Sanctioned tavern-chaplain circuit; reduce brawls and theft through presence.","green",{"public","security","service"}),
			T("Reconciliation feast for rival crews; agreements sealed before witnesses.","green",{"mediation","festival","service"}),
			T("Public song-rotation at market hours; discourage rumor panic with sanctioned refrains.","green",{"rumor","public","service"}),
			T("Containment of illicit revel causing unrest; correction by sanctioned rite and fines.","yellow",{"illicit","order","security"}),
			T("Crowd-density enforcement for a stadium day; steward posts assigned.","yellow",{"crowd","festival","security"}),
			T("Emergency morale stabilization after violence; controlled gatherings and escorts.","red",{"violence","crowd","public"}),
			T("High-risk de-escalation during riot-prone festival; staged dispersal and peace hymns.","red",{"riot","crowd","security"}),
			T("Post-panic comfort rites near transit hubs; prevent stampedes and retaliatory fights.","yellow",{"panic","rail","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","public"}),
			T("Courtroom courtesy briefing; witness etiquette and silence rules reinforced.","green",{"governance","public","service"}),
			T("Market weights-and-measures rite; deter fraud and stabilize pricing disputes.","green",{"audit","service","public"}),
			T("Transit node discipline review; platform lines repainted and stewards assigned.","green",{"rail","order","transit"}),
			T("Curfew signage and quiet-hours enforcement brief; reduce night disturbances.","green",{"security","public","service"}),
			T("Administrative compliance review after complaints; corrective edicts posted.","yellow",{"compliance","governance","service"}),
			T("Guard liaison for a threatened civic hall; protocol rehearsal and watch rotation.","yellow",{"security","governance","service"}),
			T("Emergency restoration of order after mob action; escorted dispersal and curfew-litany.","red",{"mob","crowd","security"}),
			T("High-risk support to authorities during barricade incident; perimeter discipline enforced.","red",{"barricade","security","public"}),
			T("Port and rail dispute arbitration; enforce lawful priority lanes for relief cargo.","yellow",{"port","rail","order","relief"}),
		}
	},

	{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","service"}),
			T("Archive and library integrity check; catalog drift corrected and access verified.","green",{"archive","audit","service"}),
			T("Public instruction on lawful prophecy vs. pamphlet fraud; leaflets distributed.","green",{"education","public","service"}),
			T("Scribe training session; improve recordkeeping for relief and stipend flows.","green",{"education","audit","service"}),
			T("Observatory-liaison briefing with Conclave Illuminara; shared bulletins standardized.","green",{"service","archive","public"}),
			T("Investigation of forged prophecy pamphlets; counterfeit sources traced.","yellow",{"forgery","investigation","service"}),
			T("Archive sealing of a sensitive docket after unrest; access reduced and witnessed.","yellow",{"security","archive","service"}),
			T("Emergency counter-homily after loudspeaker seizure; sanctioned address delivered.","red",{"broadcast","crisis","public"}),
			T("High-risk protection for a key temple scholar; escort and secure lodging arranged.","red",{"threat","security","service"}),
			T("Transit bulletin for corridor cities after omen panic; prevent stampedes and conspiracies.","yellow",{"rail","panic","public"}),
		}
	},

	{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","service"}),
			T("Food-line volunteer training; prevent cutting and fights through posted rules.","green",{"relief","queue","public"}),
			T("Shelter visitation for displaced workers; job referrals routed through civic offices.","green",{"service","relief","governance"}),
			T("Agri stipend review for smallholders; vouchers revalidated and extensions logged.","green",{"agri","audit","service"}),
			T("Compassion circuit to hospitals and clinics; morale support and supply notes recorded.","green",{"health","service","relief"}),
			T("Offering logistics for a major rite; livestock and treasure accounted and sealed.","yellow",{"offering","audit","service"}),
			T("Mediation for neighborhood poverty disputes; aid allocations made transparent.","yellow",{"aid","mediation","service"}),
			T("Emergency relief surge after incident; soup lines sanctified and guarded.","red",{"relief","crowd","public"}),
			T("High-risk protection for a vulnerable procession route; escorts and safe shelters staged.","red",{"procession","security","public"}),
			T("Port-side charity intake after a ferry accident; registries updated and families notified.","yellow",{"port","relief","service"}),
		}
	},

	{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","service"}),
			T("Infrastructure illumination audit for tunnels and transit nodes; repair notices issued.","green",{"infrastructure","transit","industry"}),
			T("Rail signal blessing at key junctions; reduce accidents and rumor panic.","green",{"rail","transit","public"}),
			T("Workshop visit: lantern production quality review; defective stock quarantined.","green",{"industry","audit","service"}),
			T("Public lantern-route map refreshed for festival season; prevent crush points.","green",{"festival","crowd","public"}),
			T("Investigation of signal tampering near a transport hub; seals replaced and watchers posted.","yellow",{"tamper","security","transit"}),
			T("Emergency lighting deployment after sabotage; temporary beacons installed with escorts.","red",{"sabotage","crisis","security"}),
			T("High-risk stabilization at a transport hub after panic; controlled flow and sanctuary lanes.","red",{"panic","crowd","transit"}),
			T("Port navigation-light inspection for fog days; harbor masters briefed.","yellow",{"port","infrastructure","service"}),
			T("Tunnel crowd-discipline drill; stewards assigned and entry metering practiced.","yellow",{"crowd","infrastructure","transit"}),
		}
	},
}


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

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

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

--------------------------------------------------------------------------------
-- 8) CITY PROFILES (complete)
--------------------------------------------------------------------------------
local cityProfiles = {
	-- =========================
	-- CORE MAJORS
	-- =========================
	["Vaeringheim"] = {
		tags={"capital","admin","port","templebank","canals"},
		weights={GGS=4,HSA=3,RCA=3,ASS=2,CIL=2,CHS=2,OSI=1},
	},
	["Luminaria"] = {
		tags={"industry","rollingstock","innovation","dawn","infrastructure"},
		weights={TIL=4,OAM=3,EOF=2,IGA=2,OSI=1,GGS=1},
	},
	["Serena"] = {
		tags={"civic","lake","hospitality","public"},
		weights={HSA=3,ACC=2,OAI=2,ASS=1,CHS=1,OSI=1},
	},
	["Pyralis"] = {
		tags={"volcanic","hazard","fire","reconstruction"},
		weights={IGA=4,ASS=2,SCV=2,EOF=1,CIC=1},
		baseSev="yellow",
	},
	["Symphonara"] = {
		tags={"civic","arts","festival","public"},
		weights={MRM=3,ACC=2,OSI=2,OAM=2,HSA=1,RCA=1},
	},
	["Aurelia"] = {
		tags={"civic","trade","public"},
		weights={HSA=2,CHS=2,GGS=2,ACC=1,OSI=1},
	},
	["Somniumpolis"] = {
		tags={"finance","dream","rumorcontrol","noctic"},
		weights={REN=3,GGS=3,HSA=2,UOR=2,CIL=2,RCA=1},
		baseSev="yellow",
	},
	["Nexa"] = {
		tags={"market","trade","logistics"},
		weights={GGS=3,HSA=2,CHS=2,RCA=1,ASS=1,ACC=1},
	},
	["Lunalis Sancta"] = {
		tags={"lunar","archives","pilgrimage","omens"},
		weights={CIL=4,REN=2,UOR=2,SDL=1,HSA=1},
	},
	["Sylvapolis"] = {
		tags={"forest","garden","ecology","shrinework"},
		weights={SYF=3,MVE=3,OAI=1,HSA=1,ASS=1},
	},

	-- =========================
	-- CORE MINORS
	-- =========================
	["Saluria"] = {
		tags={"civic","artisan","wildlife"},
		weights={SYF=2,CHS=2,ACC=1,HSA=1,OAI=1},
	},
	["Aetherium"] = {
		tags={"sky","omens","education"},
		weights={CIL=2,OAM=2,CHS=2,OSI=1},
	},
	["Ferrum Citadel"] = {
		tags={"fortress","industry","security","infrastructure"},
		weights={CIC=3,RCA=2,TIL=2,EOF=1,IGA=1},
		baseSev="yellow",
	},
	["Acheron"] = {
		tags={"frontier","containment","mines","security"},
		weights={CIC=3,EOF=2,MSV=1,UOR=1,IGA=1,RCA=1},
		baseSev="yellow",
	},
	["Erythros"] = {
		tags={"corridor","trade","security"},
		weights={GGS=2,RCA=2,TIL=2,ASS=1,IGA=1},
	},
	["Catonis Atrium"] = {
		tags={"civic","law","order"},
		weights={RCA=4,HSA=1,CHS=1,ACC=1},
	},
	["Delphica"] = {
		tags={"oracle","archives","scholarship"},
		weights={SDL=4,CIL=2,CHS=1,HSA=1},
	},
	["Koinonía"] = {
		tags={"harmony","mediation","civic"},
		weights={ACC=3,HSA=3,CHS=1,OAI=1},
	},
	["Aureum"] = {
		tags={"wealth","craft","trade","finance"},
		weights={GGS=4,HSA=1,RCA=1,ALB=1},
	},

	-- =========================
	-- ALPERKIN
	-- =========================
	["The Alpazkigz"] = {
		tags={"tribal","festival","intercession","mediation"},
		weights={ACC=3,HSA=2,CHS=2,OAI=1,CIC=1},
		baseSev="yellow",
	},
	["Odiferia"] = {
		tags={"noctic","rumorcontrol","dream","folk"},
		weights={REN=3,HSA=2,GGS=2,UOR=2,MSV=1},
		baseSev="yellow",
	},

	-- =========================
	-- NEW SOUTH JANGSONG (MAJOR)
	-- =========================
	["Skýrophos"] = {
		tags={"integration","civic","corridor","governance"},
		weights={CHS=2,HSA=2,RCA=2,TIL=2,EOF=2,ACC=1},
	},
	["Bjornopolis"] = {
		tags={"north","fortified","industry","civic"},
		weights={EOF=3,CIC=2,TIL=2,ASS=1,RCA=1},
		baseSev="yellow",
	},
	["Aegirheim"] = {
		tags={"north","coastal","harbor","watch"},
		weights={ASS=3,EOF=2,CIC=1,TIL=1},
		baseSev="yellow",
	},
	["Norsolyra"] = {
		tags={"agriculture","wetlands","river","supply"},
		weights={MVE=3,ASS=2,SYF=2,OAI=1,SCV=1},
	},
	["Thorsalon"] = {
		tags={"north","vineyards","festival","supply"},
		weights={MVE=2,MRM=2,EOF=2,OAI=1,ASS=1},
		baseSev="yellow",
	},

	-- =========================
	-- NEW SOUTH JANGSONG (MINOR)
	-- =========================
	["Pelagia"] = {
		tags={"coastal","distribution","water","relief"},
		weights={ASS=3,SCV=2,HSA=1,EOF=1},
	},
	["Myrene"] = {
		tags={"coastal","civic","trade"},
		weights={ASS=2,HSA=2,CHS=1,ACC=1},
	},
	["Thyrea"] = {
		tags={"agriculture","market","relief"},
		weights={MVE=3,OAI=2,SYF=1,HSA=1},
	},
	["Ephyra"] = {
		tags={"forest","frontier","watch","omens"},
		weights={SYF=3,UOR=2,EOF=1,CIC=1},
		baseSev="yellow",
	},
	["Halicarn"] = {
		tags={"archives","scholarship","civic"},
		weights={SDL=3,CIL=2,CHS=1,HSA=1},
	},

	-- =========================
	-- HAIFAN BASSARIDIA (MAJOR)
	-- =========================
	["Keybir-Aviv"] = {
		tags={"port","trade","gateway","security"},
		weights={ASS=3,GGS=2,HSA=2,RCA=1,CIC=1},
		baseSev="yellow",
	},
	["Tel-Amin"] = {
		tags={"plantation","coffee","trade","agriculture"},
		weights={MVE=3,OAI=2,SYF=2,GGS=1,ASS=1},
	},
	["Diamandis"] = {
		tags={"luxury","trade","vaulting","security"},
		weights={GGS=4,RCA=2,ALB=1,HSA=1},
		baseSev="yellow",
	},
	["Jogi"] = {
		tags={"agroprocessing","market","ferry","trade"},
		weights={MVE=3,OAI=2,ASS=2,SYF=1,GGS=1},
	},
	["Lewisburg"] = {
		tags={"rail","hub","logistics","governance"},
		weights={TIL=4,RCA=3,GGS=2,HSA=1,EOF=1,ASS=1},
		baseSev="yellow",
	},

	-- =========================
	-- HAIFAN BASSARIDIA (MINOR)
	-- =========================
	["Thermosalem"] = {
		tags={"healing","resort","water","crowd"},
		weights={SCV=3,ASS=2,HSA=2,ACC=1,RCA=1},
		baseSev="yellow",
	},
	["Akróstadium"] = {
		tags={"stadium","festival","crowd","public"},
		weights={MRM=3,OSI=2,ACC=2,HSA=1},
		baseSev="yellow",
	},
	["Sufriya"] = {
		tags={"terminal","security","port","logistics"},
		weights={ASS=3,GGS=2,CIC=2,RCA=1,TIL=1},
		baseSev="yellow",
	},
	["Lykopolis"] = {
		tags={"rail","corridor","security","logistics"},
		weights={TIL=4,RCA=2,EOF=2,CIC=1,GGS=1},
		baseSev="yellow",
	},

	-- =========================
	-- NORMARK
	-- =========================
	["Ardclach"] = {
		tags={"north","ferry","coastal","watch"},
		weights={ASS=3,EOF=2,CIC=1,GGS=1},
		baseSev="yellow",
	},
	["Riddersborg"] = {
		tags={"north","rail","corridor","storm"},
		weights={EOF=3,TIL=2,CIC=2,ASS=1,RCA=1},
		baseSev="yellow",
	},

	-- =========================
	-- OURIANA
	-- =========================
	["Bashkim"] = {
		tags={"rail","corridor","agriculture","logistics"},
		weights={MVE=2,TIL=3,RCA=2,OAI=1,EOF=1},
		baseSev="yellow",
	},
	["Ourid"] = {
		tags={"river","fisheries","supply","trade"},
		weights={ASS=3,MVE=2,OAI=2,HSA=1},
	},
	["Tonar"] = {
		tags={"quarantine","doctrine","containment","security"},
		weights={ALB=3,CIC=3,RCA=2,HSA=1,SDL=1},
		baseSev="red",
	},

	-- =========================
	-- CALEDONIAN DEPENDENCIES
	-- =========================
	["Fanghorn"] = {
		tags={"frontier","forests","security","watch"},
		weights={EOF=3,CIC=2,SYF=2,UOR=1,RCA=1},
		baseSev="yellow",
	},
	["Eikbu"] = {
		tags={"ranching","agriculture","trail","supply"},
		weights={MVE=3,SYF=2,OAI=2,EOF=1},
	},
	["Galvø"] = {
		tags={"island","coastal","port","storm"},
		weights={ASS=3,EOF=2,CIC=1},
		baseSev="yellow",
	},
	["Sårensby"] = {
		tags={"rail","corridor","industry","civic"},
		weights={TIL=3,EOF=2,RCA=2,GGS=1,ASS=1},
		baseSev="yellow",
	},
	["Slevik"] = {
		tags={"north","security","civic"},
		weights={EOF=3,CIC=2,ASS=1,RCA=1},
		baseSev="yellow",
	},
	["Sjøsborg"] = {
		tags={"coastal","port","trade","watch"},
		weights={ASS=3,EOF=2,GGS=1},
		baseSev="yellow",
	},
	["Storesund"] = {
		tags={"coastal","trade","storm"},
		weights={ASS=3,EOF=2,CIC=1},
		baseSev="yellow",
	},
	["Notranskja"] = {
		tags={"north","frontier","watch","omens"},
		weights={EOF=2,CIC=2,UOR=2,ASS=1},
		baseSev="yellow",
	},
	["Kaledonija"] = {
		tags={"regionalhub","governance","trade","civic"},
		weights={RCA=3,EOF=2,GGS=2,HSA=1,ASS=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
	return "Other"
end

local function extractCompanyName(cell)
	local n = cell:match("'''(.-)'''")
	if n and n ~= "" then return stripWiki(n) end
	n = cell:match("%*%*(.-)%*%*")
	if n and n ~= "" then return stripWiki(n) end
	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
	if s:lower():find("fleet", 1, true) then return nil end
	if s:lower():find("no fixed", 1, true) then return nil end
	s = s:gsub("%s*;%s*.*$", "")
	s = s:gsub("%s*%b()%s*", " ")
	s = normalizeSpaces(s)
	s = trim(s)
	if citySet[s] then return s 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

	local startPos = wikitext:find("====%s*Company Table%s*====")
	if not startPos then startPos = wikitext:find("####%s*Company Table") end
	if not startPos then 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)

	local rows = {}
	for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do
		rows[#rows+1] = chunk
	end
	local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}")
	if lastChunk then rows[#rows+1] = lastChunk end

	for _, row in ipairs(rows) do
		if not row:match("^%s*!") then
			local line = normalizeSpaces(row:gsub("\n", " "))
			line = line:gsub("^%s*|%s*", "")

			local cells = {}
			for cell in line:gmatch("([^|]-)%s*||%s*") do
				cells[#cells+1] = trim(cell)
			end
			local tail = line:match("||%s*([^|].-)%s*$")
			if tail and tail ~= "" then
				if #cells == 0 or cells[#cells] ~= trim(tail) then
					cells[#cells+1] = trim(tail)
				end
			end

			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] or 0) + 1
				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
--------------------------------------------------------------------------------
local function weightedPickKey(nextInt, weights)
	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 buildTagSet(tags)
	local set = {}
	for _, t in ipairs(tags or {}) do set[t] = true end
	return set
end

local function pickTaskForCity(nextInt, taskList, 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)
	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
		applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1})
	end
end

--------------------------------------------------------------------------------
-- 12) TEMPLE BANK NOTES
--------------------------------------------------------------------------------
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"

	if cityProfile and cityProfile.baseSev then
		sev = cityProfile.baseSev
	end

	if econTag == "industry" or econTag == "service" then
		if sev == "green" then sev = "yellow" end
	end

	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 = {}

	for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
	if RAIL[cityName] then tags[#tags+1] = "rail" end
	if FERRY[cityName] then tags[#tags+1] = "ferry" end

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

	local tagSet = buildTagSet(tags)

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

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

	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)

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

	local bank = pickFromList(nextInt, bankNotes, nil)

	local threatText, 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", getCityFlagMarkup(cityName), cityLink(cityName)),
		order = getOrderIconMarkup(o.name) .. 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()

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

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	local header = "! style='white-space:nowrap;' | Date !! City !! Region !! 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 wantedOrder = orderByName[wanted]
	if not wantedOrder then
		return "''Error: unknown order name (must match module order names).''"
	end

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

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

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

	local out = {}
	table.insert(out, "{| class='wikitable sortable'")
	local header = "! Date !! City !! Region !! 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:find(wantedOrder.link, 1, true) 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