Module:MissionaryDailyOpsBVR: Difference between revisions
From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs) No edit summary |
NewZimiaGov (talk | contribs) No edit summary |
||
| Line 1: | Line 1: | ||
-- Module:MissionaryDailyOpsBVR | -- Module:MissionaryDailyOpsBVR (REWRITE) | ||
-- Daily (PSSC-calendar keyed) missionary tasking | -- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path. | ||
-- Deterministic | -- Deterministic output (no math.random / no os.time seeding). | ||
-- | -- | ||
-- | -- NEW IN THIS REWRITE: | ||
-- | -- • City profiles (culture/spiritual tags + per-city order bias) | ||
-- • Economic signals pulled from General Port of Lake Morovia "Company Table" | |||
-- (counts by sector + sample HQ companies) and used to bias order + task. | |||
-- • Optional Threat Watch integration from Module:ReligiousEvents (opt-in). | |||
-- | -- | ||
-- Usage: | |||
-- Usage | |||
-- {{#invoke:MissionaryDailyOpsBVR|makeTable}} | -- {{#invoke:MissionaryDailyOpsBVR|makeTable}} | ||
-- {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}} | -- {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}} | ||
-- {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}} | -- {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}} | ||
-- {{#invoke:MissionaryDailyOpsBVR|makeTable|showEcon=yes}} | |||
-- {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}} | -- {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}} | ||
-- {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}} | -- {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}} | ||
| Line 20: | Line 20: | ||
local cal = require('Module:BassaridianCalendar') | local cal = require('Module:BassaridianCalendar') | ||
local DAYS_IN_YEAR = 183 | |||
-------------------------------------------------------------------------------- | |||
-- 0) SMALL UTILS | |||
-------------------------------------------------------------------------------- | |||
local function trim(s) | |||
if s == nil then return "" end | |||
s = tostring(s) | |||
s = s:gsub("^%s+", ""):gsub("%s+$", "") | |||
return s | |||
end | |||
local function safeTitleContent(titleText) | |||
local ok, t = pcall(mw.title.new, titleText) | |||
if not ok or not t then return nil end | |||
local ok2, content = pcall(function() return t:getContent() end) | |||
if not ok2 then return nil end | |||
return content | |||
end | |||
local function normalizeSpaces(s) | |||
return (s or ""):gsub("[%s\u00A0]+", " ") | |||
end | |||
local function stripWiki(s) | |||
s = tostring(s or "") | |||
-- Remove files/templates quickly | |||
s = s:gsub("%[%[File:[^%]]+%]%]", "") | |||
s = s:gsub("%{%b{}%}", "") | |||
-- Links: [[A|B]] => B, [[A]] => A | |||
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1") | |||
s = s:gsub("%[%[([^%]]+)%]%]", "%1") | |||
-- Bold/italics | |||
s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "") | |||
-- HTML entities are usually already decoded by MediaWiki; keep safe | |||
return trim(s) | |||
end | |||
local function containsAny(s, words) | |||
s = (s or ""):lower() | |||
for _, w in ipairs(words) do | |||
if s:find(w, 1, true) then return true end | |||
end | |||
return false | |||
end | |||
local | local function joinTags(tags) | ||
if not tags or #tags == 0 then return "" end | |||
local out = {} | |||
for _, t in ipairs(tags) do out[#out+1] = t end | |||
return table.concat(out, ", ") | |||
end | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 1) CALENDAR PARSING (mirrors your HatchMinistryDailyOps approach) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function parseCalendarString(s) | local function parseCalendarString(s) | ||
| Line 71: | Line 121: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 2) DETERMINISTIC RNG (NO bit32, NO math.random) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function hash31(str) | local function hash31(str) | ||
| Line 107: | Line 157: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 3) COLOR CODING (Hatch-style) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function sevStyle(sev) | local function sevStyle(sev) | ||
| Line 119: | Line 169: | ||
end | end | ||
local function | -------------------------------------------------------------------------------- | ||
-- 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 | end | ||
local function cityLink(name) | local function cityLink(name) | ||
return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", name, name) | return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", name, name) | ||
end | end | ||
-- 54 canonical cities | -- 54 canonical cities; tier used for "major city" spiritual baseline. | ||
local cityOrder = { | local cityOrder = { | ||
-- Core majors | -- Core majors | ||
{name="Vaeringheim", region="Core"}, | {name="Vaeringheim", region="Core", tier="major"}, | ||
{name="Luminaria", region="Core"}, | {name="Luminaria", region="Core", tier="major"}, | ||
{name="Serena", region="Core"}, | {name="Serena", region="Core", tier="major"}, | ||
{name="Pyralis", region="Core"}, | {name="Pyralis", region="Core", tier="major"}, | ||
{name="Symphonara", region="Core"}, | {name="Symphonara", region="Core", tier="major"}, | ||
{name="Aurelia", region="Core"}, | {name="Aurelia", region="Core", tier="major"}, | ||
{name="Somniumpolis", region="Core"}, | {name="Somniumpolis", region="Core", tier="major"}, | ||
{name="Nexa", region="Core"}, | {name="Nexa", region="Core", tier="major"}, | ||
{name="Lunalis Sancta", region="Core"}, | {name="Lunalis Sancta", region="Core", tier="major"}, | ||
{name="Sylvapolis", region="Core"}, | {name="Sylvapolis", region="Core", tier="major"}, | ||
-- Core minors | -- Core minors | ||
{name="Saluria", region="Core"}, | {name="Saluria", region="Core", tier="minor"}, | ||
{name="Aetherium", region="Core"}, | {name="Aetherium", region="Core", tier="minor"}, | ||
{name="Ferrum Citadel", region="Core"}, | {name="Ferrum Citadel", region="Core", tier="minor"}, | ||
{name="Acheron", region="Core"}, | {name="Acheron", region="Core", tier="minor"}, | ||
{name="Erythros", region="Core"}, | {name="Erythros", region="Core", tier="minor"}, | ||
{name="Catonis Atrium", region="Core"}, | {name="Catonis Atrium", region="Core", tier="minor"}, | ||
{name="Delphica", region="Core"}, | {name="Delphica", region="Core", tier="minor"}, | ||
{name="Koinonía", region="Core"}, | {name="Koinonía", region="Core", tier="minor"}, | ||
{name="Aureum", region="Core"}, | {name="Aureum", region="Core", tier="minor"}, | ||
-- Alperkin | -- Alperkin | ||
{name="The Alpazkigz", region="Alperkin"}, | {name="The Alpazkigz", region="Alperkin", tier="minor"}, | ||
{name="Odiferia", region="Alperkin"}, | {name="Odiferia", region="Alperkin", tier="minor"}, | ||
-- New South Jangsong | -- New South Jangsong | ||
{name="Skýrophos", region="New South Jangsong"}, | {name="Skýrophos", region="New South Jangsong", tier="major"}, | ||
{name="Bjornopolis", region="New South Jangsong"}, | {name="Bjornopolis", region="New South Jangsong", tier="major"}, | ||
{name="Aegirheim", region="New South Jangsong"}, | {name="Aegirheim", region="New South Jangsong", tier="major"}, | ||
{name="Norsolyra", region="New South Jangsong"}, | {name="Norsolyra", region="New South Jangsong", tier="major"}, | ||
{name="Thorsalon", region="New South Jangsong"}, | {name="Thorsalon", region="New South Jangsong", tier="major"}, | ||
{name="Pelagia", region="New South Jangsong", tier="minor"}, | |||
{name="Myrene", region="New South Jangsong", tier="minor"}, | |||
{name="Thyrea", region="New South Jangsong", tier="minor"}, | |||
{name="Ephyra", region="New South Jangsong", tier="minor"}, | |||
{name="Halicarn", region="New South Jangsong", tier="minor"}, | |||
-- | -- Haifan Bassaridia | ||
{name=" | {name="Keybir-Aviv", region="Haifan Bassaridia", tier="major"}, | ||
{name="Tel-Amin", region="Haifan Bassaridia", tier="major"}, | |||
{name=" | {name="Diamandis", region="Haifan Bassaridia", tier="major"}, | ||
{name="Jogi", region="Haifan Bassaridia", tier="major"}, | |||
{name=" | {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=" | {name="Lykopolis", region="Haifan Bassaridia", tier="minor"}, | ||
{name=" | |||
{name="Thermosalem", region="Haifan Bassaridia"}, | |||
{name="Akróstadium", region="Haifan Bassaridia"}, | |||
{name="Sufriya", region="Haifan Bassaridia"}, | |||
{name="Lykopolis", region="Haifan Bassaridia"}, | |||
-- Normark | -- Normark | ||
{name="Ardclach", region="Normark"}, | {name="Ardclach", region="Normark", tier="major"}, | ||
{name="Riddersborg", region="Normark"}, | {name="Riddersborg", region="Normark", tier="major"}, | ||
-- Ouriana | -- Ouriana | ||
{name="Bashkim", region="Ouriana"}, | {name="Bashkim", region="Ouriana", tier="major"}, | ||
{name="Ourid", region="Ouriana"}, | {name="Ourid", region="Ouriana", tier="major"}, | ||
{name="Tonar", region="Ouriana"}, | {name="Tonar", region="Ouriana", tier="minor"}, | ||
-- | -- Caledonia | ||
{name="Fanghorn", region="Caledonia"}, | {name="Fanghorn", region="Caledonia", tier="major"}, | ||
{name="Eikbu", region="Caledonia"}, | {name="Eikbu", region="Caledonia", tier="major"}, | ||
{name="Galvø", region="Caledonia"}, | {name="Galvø", region="Caledonia", tier="major"}, | ||
{name="Sårensby", region="Caledonia"}, | {name="Sårensby", region="Caledonia", tier="minor"}, | ||
{name="Slevik", region="Caledonia"}, | {name="Slevik", region="Caledonia", tier="minor"}, | ||
{name="Sjøsborg", region="Caledonia"}, | {name="Sjøsborg", region="Caledonia", tier="minor"}, | ||
{name="Storesund", region="Caledonia"}, | {name="Storesund", region="Caledonia", tier="minor"}, | ||
{name="Notranskja", region="Caledonia"}, | {name="Notranskja", region="Caledonia", tier="minor"}, | ||
{name="Kaledonija", region="Caledonia"}, | {name="Kaledonija", region="Caledonia", tier="minor"}, | ||
} | } | ||
local citySet = {} | |||
local cityFlag = {} | |||
for _, c in ipairs(cityOrder) do | |||
citySet[c.name] = true | |||
cityFlag[c.name] = string.format("[[File:%sFlag.png|20px]]", normalizeForFlag(c.name)) | |||
end | |||
-- explicit override used elsewhere on the wiki | |||
cityFlag["Skýrophos"] = "[[File:SkyrophosFlag.png|20px]]" | |||
local function regionPass(filter, cityRegion) | local function regionPass(filter, cityRegion) | ||
if not filter or filter == "" or filter == "All" then return true end | if not filter or filter == "" or filter == "All" then return true end | ||
if filter == "Core" then return cityRegion == "Core" end | if filter == "Core" then return cityRegion == "Core" end | ||
if filter == "NSJ" then return cityRegion == "New South Jangsong" end | if filter == "NSJ" then return cityRegion == "New South Jangsong" end | ||
| Line 220: | Line 276: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 5) CORRIDORS / ROLE HINTS (from the city-network page; hard-coded for stability) | ||
-------------------------------------------------------------------------------- | |||
-- Rail corridor cities listed on the city network overview. | |||
local RAIL = { | |||
["Lykopolis"]=true, ["Lewisburg"]=true, ["Bashkim"]=true, ["Somniumpolis"]=true, | |||
["Erythros"]=true, ["Vaeringheim"]=true, ["Symphonara"]=true, ["Skýrophos"]=true, | |||
["Norsolyra"]=true, ["Pelagia"]=true, ["Riddersborg"]=true, ["Sårensby"]=true, | |||
} | |||
-- Ferry links noted: Jogi + Ardclach | |||
local FERRY = {["Jogi"]=true, ["Ardclach"]=true} | |||
-------------------------------------------------------------------------------- | |||
-- 6) MISSIONARY ORGANIZATION (sizes) | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local org = { | local org = { | ||
| Line 242: | Line 310: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 7) ORDERS + TASKS (now with tags to match city profile/economy) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function orderLink(pageTitle, anchor, label) | local function orderLink(pageTitle, anchor, label) | ||
| Line 248: | Line 316: | ||
end | end | ||
local function T(text, sev, | local function T(text, sev, tags) | ||
return {text=text, sev=sev, | return {text=text, sev=sev, tags=tags or {}} | ||
end | end | ||
local orders = { | local orders = { | ||
{ | {key="OSI", name="Ordo Solis Invicti", | ||
link=orderLink("Atos (Reformed Stripping Path)","Ordo Solis Invicti","Ordo Solis Invicti"), | |||
tasks={ | |||
T("Dawn rites and public illumination drill to restore civic clarity and discourage rumor cascades in market wards.","green",{"public","education"}), | |||
T("Temple-school literacy session paired with cosmology instruction and shrine maintenance inspections.","green",{"education","shrine"}), | |||
T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit"}), | |||
T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd"}), | |||
T("Emergency morale procession after night unrest; hymn leadership and calm restoration at transit choke points.","red",{"unrest","crowd","transit"}), | |||
}}, | |||
{key="OAM", name="Order of Aurora Mystica", | |||
link=orderLink("Eos (Reformed Stripping Path)","Order of Aurora Mystica","Order of Aurora Mystica"), | |||
tasks={ | |||
{ | T("Sunrise vigil and reconciliation rite for competing guilds; signed harmony pledges archived by Temple Bank clerks.","green",{"mediation","guild"}), | ||
T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.","green",{"omen","public"}), | |||
T("Civic blessing for new housing blocks; routing of pilgrimage foot-traffic to prevent crush points.","yellow",{"crowd","infrastructure"}), | |||
T("Counter-heresy sermon cycle following illicit broadsides; witness interviews conducted with local elders.","yellow",{"heresy","investigation"}), | |||
T("Rapid-response pacification after arson rumor; dawn procession under guard with emergency stipend release.","red",{"unrest","arson"}), | |||
}}, | |||
{key="HSA", name="Harmony Sanctum", | |||
link=orderLink("Micras (Reformed Stripping Path)","Harmony Sanctum","Harmony Sanctum"), | |||
tasks={ | |||
T("Neighborhood mediation session for family disputes and trade-feud spillover; binding accords notarized.","green",{"mediation"}), | |||
T("Harmony clinic for stress and rumor contagion; quiet-room rotations and guided breathing rites.","green",{"health","rumor"}), | |||
T("Loyalty and morale audit of a civic office after complaint surge; corrective training scheduled.","yellow",{"audit","governance"}), | |||
{ | T("Containment of a cult schism meeting; supervised dialogue and sanction warnings delivered.","yellow",{"schism","containment"}), | ||
T("Major crowd-stabilization operation after riot signals; staged dispersal with peace-liturgy and escorts.","red",{"riot","crowd"}), | |||
}}, | |||
{key="IGA", name="Ignis Aeternum", | |||
link=orderLink("Pyros (Reformed Stripping Path)","Ignis Aeternum","Ignis Aeternum"), | |||
tasks={ | |||
T("Controlled fire blessing for workshops; safety rite that doubles as an inspection of fuel and storage.","green",{"industry","safety"}), | |||
T("Purification burn at a compromised shrine-store; ash sealed and catalogued for Temple Bank disposal.","yellow",{"contam","shrine"}), | |||
T("Investigation of incendiary threats near a gate-temple; firebreak plan issued to local wardens.","yellow",{"arson","investigation"}), | |||
T("Emergency response to active arson or explosive rumor; sanctified suppression teams deployed with escorts.","red",{"arson","crisis"}), | |||
T("Post-incident purification circuit across three oratories, restoring ritual confidence by nightfall.","red",{"incident","public"}), | |||
}}, | |||
{ | {key="CHS", name="Celestial Harmony Sect", | ||
link=orderLink("Indigo (Reformed Stripping Path)","Celestial Harmony Sect","Celestial Harmony Sect"), | |||
tasks={ | |||
T("Diplomatic courtesy visit to a city council; alignments review and oath renewal with minimal spectacle.","green",{"diplomacy","governance"}), | |||
T("Star-chart instruction for apprentices; quiet observatory checks and record sealing.","green",{"education","astronomy"}), | |||
T("Faction-balance mediation between rival shrines; written compromise placed in Temple Bank archive.","yellow",{"balance","mediation"}), | |||
T("Rumor forensics on ‘forbidden rite’ claims; witness tree compiled and forwarded to Temple authorities.","yellow",{"rumor","investigation"}), | |||
T("High-risk stabilization after cult confrontation; negotiators embedded with escorts to prevent escalation.","red",{"confrontation","crowd"}), | |||
}}, | |||
{key="GGS", name="Guild of Golden Shadows", | |||
link=orderLink("Chrysos (Reformed Stripping Path)","Guild of Golden Shadows","Guild of Golden Shadows"), | |||
tasks={ | |||
{ | T("Discrete audit of offertory ledgers and voucher seals; minor variances reconciled quietly.","green",{"finance","audit"}), | ||
T("Merchant vetting interviews for a sensitive consignment; anonymity rites certified.","yellow",{"merchant","security"}), | |||
T("Counter-counterfeit sweep targeting forged stamps and false Temple consignments; evidence boxed and logged.","yellow",{"forgery","investigation"}), | |||
T("Recovery detail for stolen reliquary property; surveillance and lawful seizure under civic warrant.","red",{"theft","security"}), | |||
T("Crisis finance stabilization after panic; emergency stipend routing and rumor suppression coordination.","red",{"panic","finance"}), | |||
}}, | |||
{key="ASS", name="Azure Sentinel Sect", | |||
link=orderLink("Thalassa (Reformed Stripping Path)","Azure Sentinel Sect","Azure Sentinel Sect"), | |||
tasks={ | |||
T("Canal and fountain sanctification circuit; water-quality blessings paired with public reassurance.","green",{"water","public"}), | |||
T("Harbor-side watch for sabotage indicators; shrine-wardens trained on lock and pier checks.","yellow",{"port","sabotage"}), | |||
T("Storm or flood readiness rite; evacuation hymn routes verified with civic stewards.","yellow",{"flood","infrastructure"}), | |||
{ | T("Quarantine support for suspected waterborne illness; controlled distribution points blessed and monitored.","red",{"outbreak","water"}), | ||
T("Major incident response: water infrastructure damage; emergency sanctification and ration-order enforcement.","red",{"infrastructure","crisis"}), | |||
}}, | |||
{key="REN", name="Reverie Nebulous", | |||
link=orderLink("Nephele (Reformed Stripping Path)","Reverie Nebulous","Reverie Nebulous"), | |||
tasks={ | |||
T("Dream-interpretation clinic to reduce anxiety; rumor narratives redirected into sanctioned symbolism.","green",{"dream","rumor"}), | |||
T("Nocturne counseling for insomnia cluster; soft-lit rites and chant rotation schedules.","green",{"health","sleep"}), | |||
T("Investigation of forbidden homily broadcasts; witness interviews and calm messaging drafted.","yellow",{"broadcast","investigation"}), | |||
T("Containment of mass-vision panic; staged public reassurance with tightly controlled procession.","red",{"panic","crowd"}), | |||
T("Stabilization after hallucination-linked violence; liaison with Harmony Sanctum for long-tail care.","red",{"violence","health"}), | |||
}}, | |||
{ | {key="EOF", name="Eon Fellowship", | ||
link=orderLink("Glinos (Reformed Stripping Path)","Eon Fellowship","Eon Fellowship"), | |||
tasks={ | |||
T("Cold-weather relief checks and ration fairness review; storage seals verified.","green",{"relief","logistics"}), | |||
T("Infrastructure endurance audit: roofs, walls, and bridges; frost damage triage list issued.","yellow",{"infrastructure","audit"}), | |||
T("Conflict de-escalation for resource disputes; binding queue rules posted at distribution points.","yellow",{"order","queue"}), | |||
T("Emergency shelter activation after storm loss; warm-hall rites and controlled entry.","red",{"storm","relief"}), | |||
T("High-risk escort for frontier convoys under threat; endurance cadres embedded with wardens.","red",{"frontier","security"}), | |||
}}, | |||
{key="UOR", name="Order of the Umbral Oracle", | |||
link=orderLink("Noctis (Reformed Stripping Path)","Order of the Umbral Oracle","Order of the Umbral Oracle"), | |||
tasks={ | |||
{ | T("Night watch and omen-reading in the temple quarter; guidance slips sealed for officials.","green",{"omen","security"}), | ||
T("Discrete consultation for a magistrate; truth-testing rite witnessed by two scribes.","yellow",{"governance","inquiry"}), | |||
T("Shadow-route mapping after disappearances; safe-path markers installed with civic approval.","yellow",{"security","transit"}), | |||
T("Emergency intervention after ‘demonic sign’ sightings; controlled crowd handling and ward placement.","red",{"demonic","panic"}), | |||
T("High-risk extraction of a threatened cantor; night escort to secure sanctuary.","red",{"abduction","security"}), | |||
}}, | |||
{key="MVE", name="Mystery of the Verdant Embrace", | |||
link=orderLink("Opsithe (Reformed Stripping Path)","Mystery of the Verdant Embrace","Mystery of the Verdant Embrace"), | |||
tasks={ | |||
T("Harvest blessing and storage rite; spoilage prevention guidance issued to granaries.","green",{"agriculture","harvest"}), | |||
T("Garden-shrine restoration day with community labor; devotional planting aligned to calendar.","green",{"garden","shrine"}), | |||
T("Food shortage mediation and ration arbitration; Temple Bank stipend adjustments proposed.","yellow",{"shortage","logistics"}), | |||
{ | T("Pest or blight containment rites; field inspections with farmers’ associations.","yellow",{"blight","agriculture"}), | ||
T("Famine-risk surge response; emergency distribution queues sanctified and monitored.","red",{"famine","relief"}), | |||
}}, | |||
{key="CIL", name="Conclave Illuminara", | |||
link=orderLink("Tarsica (Reformed Stripping Path)","Conclave Illuminara","Conclave Illuminara"), | |||
tasks={ | |||
T("Moonlit instruction circle; eclipse-watch protocol rehearsed with apprentice scribes.","green",{"lunar","education"}), | |||
T("Archive sealing of sensitive liturgies; access logs reviewed for anomalies.","yellow",{"archive","audit"}), | |||
T("Public lantern rite to counter rumor cascades; controlled routes and timing set with wardens.","yellow",{"rumor","crowd"}), | |||
T("Emergency omen bulletin after night panic; official statement drafted and delivered.","red",{"panic","public"}), | |||
T("High-risk protection of an observatory site under threat; wards reinforced and watchers stationed.","red",{"threat","security"}), | |||
}}, | |||
{ | {key="SCV", name="Sanctum Vitalis", | ||
link=orderLink("Plateau (Reformed Stripping Path)","Sanctum Vitalis","Sanctum Vitalis"), | |||
tasks={ | |||
T("Clinic day: basic health screenings and hygiene instruction; water points checked.","green",{"health","public"}), | |||
T("Supplement and ration fortification distribution under Temple Bank authorization.","yellow",{"nutrition","relief"}), | |||
T("Outbreak protocol rehearsal with civic healers; spacing rules posted at shrines.","yellow",{"outbreak","training"}), | |||
T("Active outbreak response: triage lanes established; quarantine support and calm messaging.","red",{"outbreak","crisis"}), | |||
T("Mass-casualty contingency drill after violence reports; intake points and transport routes mapped.","red",{"violence","logistics"}), | |||
}}, | |||
{key="ALB", name="Temple Alabaster", | |||
link=orderLink("Ivory (Reformed Stripping Path)","Temple_Alabaster","Temple Alabaster"), | |||
tasks={ | |||
{ | T("Purity inspection of ritual vessels and incense stock; minor replacements approved.","green",{"purity","shrine"}), | ||
T("Doctrinal correction session after unsigned broadsides; sanctioned texts distributed.","yellow",{"heresy","education"}), | |||
T("Sanctuary security review for valuables; locking discipline and witness rules enforced.","yellow",{"security","shrine"}), | |||
T("Emergency purification after poisoned incense discovery; wards placed and offenders sought.","red",{"poison","crisis"}), | |||
T("High-risk protection of a central temple during unrest; entry control and calm liturgy.","red",{"unrest","security"}), | |||
}}, | |||
{key="CIC", name="Court of the Ironclad", | |||
link=orderLink("Fenrir (Reformed Stripping Path)","Court_of_the_Ironclad","Court of the Ironclad"), | |||
tasks={ | |||
T("Discipline drill for shrine wardens; non-lethal control and restraint practices.","green",{"training","security"}), | |||
T("Escort for a threatened courier; route cleared and handover documented.","yellow",{"escort","security"}), | |||
T("Guard coordination in a frontier district; watch rotations synchronized with civic militia.","yellow",{"militia","security"}), | |||
{ | T("Immediate reinforcement during riot signals; cordons established and safe egress opened.","red",{"riot","crowd"}), | ||
T("High-risk interdiction support for a seized oratory; secure perimeter until authorities arrive.","red",{"siege","security"}), | |||
}}, | |||
{key="ACC", name="Accord Concordia", | |||
link=orderLink("Lukedu (Reformed Stripping Path)","Accord_Concordia","Accord Concordia"), | |||
tasks={ | |||
T("Public harmony dialogue; disputants guided to a signed concord recorded by Temple Bank scribes.","green",{"mediation","public"}), | |||
T("Catacomb Harmonic Communion support: logistics, spacing, and participant verification.","yellow",{"rite","logistics"}), | |||
T("Civic cooperation workshop for rival guilds; shared calendar commitments set.","yellow",{"guild","coordination"}), | |||
T("Emergency reconciliation after faction clash; mediators embedded with escorts.","red",{"clash","crowd"}), | |||
T("High-risk crowd calming after panic; sound-liturgical dispersal and safe routes enforced.","red",{"panic","crowd"}), | |||
}}, | |||
{key="MSV", name="Mystery of the Stygian Veil", | |||
link=orderLink("Styx (Reformed Stripping Path)","Mystery_of_the_Stygian_Veil","Mystery of the Stygian Veil"), | |||
tasks={ | |||
T("Funerary liaison and cemetery vigil support; grief rites standardized.","green",{"funeral","public"}), | |||
T("Transition counseling for displaced families; resettlement blessings recorded.","yellow",{"displacement","relief"}), | |||
T("Investigation of graveyard desecration rumor; ward placement and witness interviews.","yellow",{"desecration","investigation"}), | |||
T("Emergency containment after death-cult provocation; controlled rites to prevent escalation.","red",{"provocation","crowd"}), | |||
T("High-risk escort of relics through hostile streets; secure route planning with wardens.","red",{"escort","security"}), | |||
}}, | |||
{key="SYF", name="Sylvan Fellowship", | |||
link=orderLink("Faun (Reformed Stripping Path)","Sylvan_Fellowship","Sylvan Fellowship"), | |||
tasks={ | |||
T("Forest-edge shrine maintenance and trail safety check; minor repairs logged.","green",{"trail","shrine"}), | |||
T("Ecology rite with farmers: pest balance and water stewardship guidance.","yellow",{"ecology","agriculture"}), | |||
T("Containment of illegal logging or poaching rumor; community patrol coordination.","yellow",{"poaching","security"}), | |||
T("Emergency response to wildfire risk; firebreak rites and evacuation path marking.","red",{"wildfire","crisis"}), | |||
T("High-risk protection for a threatened grove shrine; cordon and ward reinforcement.","red",{"threat","security"}), | |||
}}, | |||
{key="MRM", name="Mystery of Red Mirth", | |||
link=orderLink("Silenus (Reformed Stripping Path)","Myster_of_Red_Mirth","Mystery of Red Mirth"), | |||
tasks={ | |||
T("Morale rite and sanctioned celebration oversight; prevent disorder while restoring joy.","green",{"morale","public"}), | |||
T("Festival logistics support; safe routes and water points verified with civic stewards.","yellow",{"festival","crowd"}), | |||
T("Containment of illicit revel causing unrest; correction by sanctioned rite and fines.","yellow",{"illicit","order"}), | |||
T("Emergency morale stabilization after violence; controlled gatherings and escorts.","red",{"violence","crowd"}), | |||
T("High-risk de-escalation during riot-prone festival; staged dispersal and peace hymns.","red",{"riot","crowd"}), | |||
}}, | |||
{key="RCA", name="Rex Catonis", | |||
link=orderLink("Cato (Reformed Stripping Path)","Rex_Catonis","Rex Catonis"), | |||
tasks={ | |||
T("Civic order inspection: queue rules, market discipline, and shrine traffic flow.","green",{"order","queue"}), | |||
T("Administrative compliance review after complaints; corrective edicts posted.","yellow",{"compliance","governance"}), | |||
T("Guard liaison for a threatened civic hall; protocol rehearsal and watch rotation.","yellow",{"security","governance"}), | |||
T("Emergency restoration of order after mob action; escorted dispersal and curfew-litany.","red",{"mob","crowd"}), | |||
T("High-risk support to authorities during barricade incident; perimeter discipline enforced.","red",{"barricade","security"}), | |||
}}, | |||
{key="SDL", name="Sanctum Delphica", | |||
link=orderLink("Sisera (Reformed Stripping Path)","Sanctum_Delphica","Sanctum Delphica"), | |||
tasks={ | |||
T("Oracle consultation clinic for officials; questions logged and sealed.","green",{"oracle","governance"}), | |||
T("Archive and library integrity check; catalog drift corrected and access verified.","yellow",{"archive","audit"}), | |||
T("Investigation of forged prophecy pamphlets; counterfeit sources traced.","yellow",{"forgery","investigation"}), | |||
T("Emergency counter-homily after loudspeaker seizure; sanctioned address delivered.","red",{"broadcast","crisis"}), | |||
T("High-risk protection for a key temple scholar; escort and secure lodging arranged.","red",{"threat","security"}), | |||
}}, | |||
{key="OAI", name="Ordo Amicitia", | |||
link=orderLink("Erasmus (Reformed Stripping Path)","Ordo_Amicitia","Ordo Amicitia"), | |||
tasks={ | |||
T("Charity distribution and household check-ins; kindness register updated for stipend support.","green",{"charity","relief"}), | |||
T("Offering logistics for a major rite; livestock and treasure accounted and sealed.","yellow",{"offering","audit"}), | |||
T("Mediation for neighborhood poverty disputes; aid allocations made transparent.","yellow",{"aid","mediation"}), | |||
T("Emergency relief surge after incident; soup lines sanctified and guarded.","red",{"relief","crowd"}), | |||
T("High-risk protection for a vulnerable procession route; escorts and safe shelters staged.","red",{"procession","security"}), | |||
}}, | |||
{key="TIL", name="Temple Illuminata", | |||
link=orderLink("Nikolaj (Reformed Stripping Path)","Temple_Illuminata","Temple Illuminata"), | |||
tasks={ | |||
T("Lamp and beacon maintenance blessing; public wayfinding clarity restored.","green",{"beacon","transit"}), | |||
T("Infrastructure illumination audit for tunnels and transit nodes; repair notices issued.","yellow",{"infrastructure","transit"}), | |||
T("Investigation of signal tampering near a transport hub; seals replaced and watchers posted.","yellow",{"tamper","security"}), | |||
T("Emergency lighting deployment after sabotage; temporary beacons installed with escorts.","red",{"sabotage","crisis"}), | |||
T("High-risk stabilization at a transport hub after panic; controlled flow and sanctuary lanes.","red",{"panic","crowd","transit"}), | |||
}}, | |||
} | |||
local orderByKey, orderByName, orderKeys, orderNames = {}, {}, {}, {} | |||
for i, o in ipairs(orders) do | |||
orderByKey[o.key] = o | |||
orderByName[o.name] = o | |||
orderKeys[#orderKeys+1] = o.key | |||
orderNames[#orderNames+1] = o.name | |||
end | |||
-------------------------------------------------------------------------------- | |||
-- 8) CITY PROFILES (manual cultural/spiritual bias; econ signal adds more) | |||
-- You can expand these over time. Any city not listed here still gets econ+region bias. | |||
-------------------------------------------------------------------------------- | |||
local cityProfiles = { | |||
["Vaeringheim"] = { | |||
tags={"capital","port","admin","templebank"}, | |||
weights={GGS=4,HSA=3,OSI=2,ASS=2,RCA=2,CIL=2}, | |||
}, | }, | ||
["Luminaria"] = { | |||
tags={"industry","innovation","dawn"}, | |||
weights={TIL=3,OAM=3,EOF=2,OSI=2,IGA=2}, | |||
}, | }, | ||
["Somniumpolis"] = { | |||
tags={"finance","dream","noctic"}, | |||
weights={REN=3,GGS=3,HSA=2,UOR=2,CIL=2,RCA=1}, | |||
}, | }, | ||
["Pyralis"] = { | |||
tags={"volcanic","hazard","fire"}, | |||
weights={IGA=4,ASS=2,SCV=2,EOF=1}, | |||
baseSev="yellow", | |||
}, | }, | ||
["Lewisburg"] = { | |||
tags={"rail","hub","wetlands"}, | |||
weights={TIL=3,RCA=2,ASS=2,GGS=1,SCV=1}, | |||
}, | }, | ||
["Lykopolis"] = { | |||
tags={"rail","corridor","resource"}, | |||
weights={TIL=3,EOF=2,RCA=2,CIC=1}, | |||
baseSev="yellow", | |||
}, | }, | ||
["Tel-Amin"] = { | |||
tags={"plantation","coffee","trade"}, | |||
weights={MVE=2,SYF=2,OAI=2,CHS=1,ASS=1}, | |||
}, | }, | ||
["Jogi"] = { | |||
tags={"agriculture","markets","fermentation"}, | |||
weights={MVE=3,OAI=2,SYF=2,ASS=1,GGS=1}, | |||
}, | }, | ||
["Acheron"] = { | |||
tags={"frontier","containment","mines"}, | |||
weights={CIC=3,EOF=2,MSV=1,UOR=1,IGA=1}, | |||
baseSev="yellow", | |||
}, | }, | ||
["Skýrophos"] = { | |||
tags={"integration","civic","corridor"}, | |||
weights={CHS=2,HSA=2,EOF=2,RCA=2,TIL=1}, | |||
}, | }, | ||
["Riddersborg"] = { | |||
tags={"north","storm","corridor"}, | |||
weights={EOF=3,TIL=2,ASS=1,CIC=1}, | |||
baseSev="yellow", | |||
}, | }, | ||
["Ardclach"] = { | |||
tags={"north","sea","ferry"}, | |||
weights={ASS=3,EOF=2,CIC=1}, | |||
baseSev="yellow", | |||
}, | }, | ||
} | } | ||
-- | -------------------------------------------------------------------------------- | ||
local | -- 9) ECONOMIC SIGNALS: parse "Company Table" on General Port page | ||
local | -------------------------------------------------------------------------------- | ||
for | local econCache = nil | ||
local function sectorOf(industryCell) | |||
local s = (industryCell or ""):lower() | |||
if s:find("agric", 1, true) then return "Agricultural" end | |||
if s:find("service", 1, true) then return "Service" end | |||
if s:find("manufact", 1, true) or s:find("mining", 1, true) or s:find("energy", 1, true) then | |||
return "Industry" | |||
end | |||
-- fallback | |||
return "Other" | |||
end | |||
local function extractCompanyName(cell) | |||
-- Try '''Name''' first | |||
local n = cell:match("'''(.-)'''") | |||
if n and n ~= "" then return stripWiki(n) end | |||
-- Try bold markdown-like **Name** (some pages render that way) | |||
n = cell:match("%*%*(.-)%*%*") | |||
if n and n ~= "" then return stripWiki(n) end | |||
-- fallback: strip wiki and truncate | |||
n = stripWiki(cell) | |||
if #n > 80 then n = n:sub(1, 80) .. "…" end | |||
return n | |||
end | |||
local function extractHQ(cell) | |||
local s = stripWiki(cell) | |||
if s == "" then return nil end | |||
-- Ignore obvious non-city HQs | |||
if s:lower():find("fleet", 1, true) then return nil end | |||
if s:lower():find("no fixed", 1, true) then return nil end | |||
-- Many HQ cells are just the city name after stripWiki. | |||
-- Some may include extra notes; take first clause. | |||
s = s:gsub("%s*;%s*.*$", "") | |||
s = s:gsub("%s*%b()%s*", " ") | |||
s = normalizeSpaces(s) | |||
s = trim(s) | |||
if citySet[s] then return s end | |||
-- Sometimes HQ may be "Vaeringheim (General Port)" etc. | |||
for cname, _ in pairs(citySet) do | |||
if s == cname then return cname end | |||
end | |||
return nil | |||
end | |||
local function parseCompanyTable(wikitext) | |||
local signals = {} | |||
for cname, _ in pairs(citySet) do | |||
signals[cname] = {Agricultural=0, Service=0, Industry=0, Other=0, samples={}} | |||
end | |||
if type(wikitext) ~= "string" then return signals end | |||
-- Find "Company Table" section and first wikitable after it | |||
local startPos = wikitext:find("====%s*Company Table%s*====") | |||
if not startPos then | |||
startPos = wikitext:find("####%s*Company Table") | |||
end | |||
if not startPos then | |||
-- If heading not found, bail with empty signals | |||
return signals | |||
end | |||
local tablePos = wikitext:find("{|", startPos) | |||
if not tablePos then return signals end | |||
local endPos = wikitext:find("|}", tablePos) | |||
if not endPos then return signals end | |||
local tableText = wikitext:sub(tablePos, endPos + 1) | |||
-- Split into rows by "|-" | |||
local rows = {} | |||
for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do | |||
rows[#rows+1] = chunk | |||
end | |||
-- Also capture last row before "|}" if not matched by above pattern | |||
local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}") | |||
if lastChunk then rows[#rows+1] = lastChunk end | |||
for _, row in ipairs(rows) do | |||
-- ignore header rows that start with "!" | |||
if not row:match("^%s*!") then | |||
-- collapse row to a single line; cells separated by "||" | |||
local line = normalizeSpaces(row:gsub("\n", " ")) | |||
-- remove leading single "|" | |||
line = line:gsub("^%s*|%s*", "") | |||
local cells = {} | |||
for cell in line:gmatch("([^|]-)%s*||%s*") do | |||
cells[#cells+1] = trim(cell) | |||
end | |||
-- capture last cell after final "||" | |||
local tail = line:match("||%s*([^|].-)%s*$") | |||
if tail and tail ~= "" then | |||
-- only add if we didn't already (gmatch above may omit last) | |||
if #cells == 0 or cells[#cells] ~= trim(tail) then | |||
cells[#cells+1] = trim(tail) | |||
end | |||
end | |||
-- Expect 7 columns; be defensive | |||
-- Industry cell usually around 5; HQ is last. | |||
local industryCell = "" | |||
for _, c in ipairs(cells) do | |||
if containsAny(c, {"agric", "service", "manufact", "mining", "energy"}) then | |||
industryCell = c | |||
break | |||
end | |||
end | |||
local sector = sectorOf(industryCell) | |||
local hqCell = cells[#cells] or "" | |||
local hq = extractHQ(hqCell) | |||
if hq and signals[hq] then | |||
signals[hq][sector] = signals[hq][sector] + 1 | |||
-- sample company name from the "Name" column (often 3rd cell) | |||
local nameCell = cells[3] or cells[2] or cells[1] or "" | |||
local companyName = extractCompanyName(nameCell) | |||
if companyName ~= "" and #signals[hq].samples < 2 then | |||
signals[hq].samples[#signals[hq].samples+1] = companyName | |||
end | |||
end | |||
end | |||
end | |||
return signals | |||
end | |||
local function getEconSignals() | |||
if econCache then return econCache end | |||
local content = safeTitleContent("General Port of Lake Morovia") | |||
econCache = parseCompanyTable(content) | |||
return econCache | |||
end | |||
local function dominantSector(sig) | |||
if not sig then return "Other" end | |||
local best, bestN = "Other", -1 | |||
for _, k in ipairs({"Agricultural","Industry","Service","Other"}) do | |||
local n = sig[k] or 0 | |||
if n > bestN then best, bestN = k, n end | |||
end | |||
return best | |||
end | |||
local function econTagFromSector(sec) | |||
if sec == "Agricultural" then return "agri" end | |||
if sec == "Industry" then return "industry" end | |||
if sec == "Service" then return "service" end | |||
return "mixed" | |||
end | |||
local function econSummary(sig) | |||
if not sig then return "n/a" end | |||
local a = sig.Agricultural or 0 | |||
local i = sig.Industry or 0 | |||
local s = sig.Service or 0 | |||
local o = sig.Other or 0 | |||
local dom = dominantSector(sig) | |||
local samp = (sig.samples and #sig.samples > 0) and ("; e.g., " .. table.concat(sig.samples, ", ")) or "" | |||
return string.format("%s (%d agri / %d ind / %d serv / %d other)%s", dom, a, i, s, o, samp) | |||
end | end | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 10) OPTIONAL: Threat Watch integration from Module:ReligiousEvents | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function parseReligiousEventsTable(wikitext) | local function parseReligiousEventsTable(wikitext) | ||
local out = {} | local out = {} | ||
if type(wikitext) ~= "string" then return out end | if type(wikitext) ~= "string" then return out end | ||
for line in wikitext:gmatch("[^\n]+") do | for line in wikitext:gmatch("[^\n]+") do | ||
if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then | if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then | ||
local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$") | local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$") | ||
if city and urg then | if city and urg then | ||
out[city] = {urg=urg, event=event or ""} | out[stripWiki(city)] = {urg=trim(urg), event=stripWiki(event or "")} | ||
end | end | ||
end | end | ||
| Line 559: | Line 783: | ||
local function threatSev(th) | local function threatSev(th) | ||
if not th or not th.urg then return nil end | if not th or not th.urg then return nil end | ||
local u = th.urg | local u = (th.urg or ""):upper() | ||
if u == "N/A" or u == "LOW" then return "green" end | if u == "N/A" or u == "LOW" then return "green" end | ||
if u == "MEDIUM" then return "yellow" end | if u == "MEDIUM" then return "yellow" end | ||
| Line 566: | Line 790: | ||
end | end | ||
local function | -------------------------------------------------------------------------------- | ||
if | -- 11) WEIGHTED ORDER PICK + TASK PICK (matches city tags + econ tags + corridor tags) | ||
return pickFromList(nextInt, | -------------------------------------------------------------------------------- | ||
local function weightedPickKey(nextInt, weights) | |||
-- weights: { [orderKey]=number } | |||
local total = 0 | |||
for _, ok in ipairs(orderKeys) do | |||
total = total + (weights[ok] or 0) | |||
end | |||
if total <= 0 then | |||
return pickFromList(nextInt, orderKeys, nil) | |||
end | |||
local r = (nextInt() % total) + 1 | |||
local acc = 0 | |||
for _, ok in ipairs(orderKeys) do | |||
acc = acc + (weights[ok] or 0) | |||
if r <= acc then return ok end | |||
end | |||
return orderKeys[1] | |||
end | |||
local function hasTag(tagSet, t) | |||
return tagSet[t] == true | |||
end | |||
local function buildTagSet(tags) | |||
local set = {} | |||
for _, t in ipairs(tags or {}) do set[t] = true end | |||
return set | |||
end | |||
local function pickTaskForCity(nextInt, taskList, tagSet) | |||
-- Prefer tasks whose tags intersect city tagset | |||
local candidates = {} | |||
for _, task in ipairs(taskList or {}) do | |||
local ok = false | |||
for _, tg in ipairs(task.tags or {}) do | |||
if tagSet[tg] then ok = true break end | |||
end | |||
if ok then candidates[#candidates+1] = task end | |||
end | |||
if #candidates > 0 then | |||
return pickFromList(nextInt, candidates, nil) | |||
end | |||
return pickFromList(nextInt, taskList, nil) | |||
end | |||
local function baseWeights() | |||
local w = {} | |||
for _, ok in ipairs(orderKeys) do w[ok] = 1 end | |||
return w | |||
end | |||
local function applyWeights(w, add) | |||
if not add then return end | |||
for k, v in pairs(add) do | |||
if orderByKey[k] then | |||
w[k] = (w[k] or 0) + tonumber(v or 0) | |||
end | |||
end | |||
end | |||
local function biasFromEcon(w, econTag) | |||
-- Econ bias is intentionally simple and legible | |||
if econTag == "agri" then | |||
applyWeights(w, {MVE=3,SYF=2,OAI=2,ASS=1}) | |||
elseif econTag == "industry" then | |||
applyWeights(w, {TIL=3,EOF=2,IGA=2,CIC=1,RCA=1}) | |||
elseif econTag == "service" then | |||
applyWeights(w, {GGS=3,HSA=2,RCA=2,CIL=1,CHS=1}) | |||
else | |||
applyWeights(w, {CHS=1,HSA=1,ASS=1}) | |||
end | |||
end | |||
local function biasFromCorridor(w, isRail, isFerry) | |||
if isRail then | |||
applyWeights(w, {TIL=2,RCA=2,GGS=1,EOF=1,ASS=1}) | |||
end | |||
if isFerry then | |||
applyWeights(w, {ASS=2,GGS=1,RCA=1}) | |||
end | |||
end | |||
local function biasFromTier(w, tier) | |||
if tier == "major" then | |||
-- Major cities: public order + rumor control tends to matter more | |||
applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1}) | |||
end | end | ||
end | end | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 12) TEMPLE BANK NOTES (deterministic snippets) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local bankNotes = { | local bankNotes = { | ||
| Line 594: | Line 893: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 13) BUILD ONE CITY ROW | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function buildCityRow(di, | local function inferSeverity(cityProfile, econTag, taskSev, threatLevel) | ||
local sev = taskSev or "green" | |||
-- baseline from profile | |||
if cityProfile and cityProfile.baseSev then | |||
sev = cityProfile.baseSev | |||
end | |||
-- econ nudge (industry + service hubs get more "yellow" oversight more often) | |||
if econTag == "industry" or econTag == "service" then | |||
if sev == "green" then sev = "yellow" end | |||
end | |||
-- threat overrides | |||
if threatLevel == "yellow" and sev == "green" then sev = "yellow" end | |||
if threatLevel == "red" and sev ~= "red" then sev = "red" end | |||
return sev | |||
end | |||
local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon) | |||
local cityName, cityRegion, tier = c.name, c.region, c.tier | |||
local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName) | local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName) | ||
local nextInt = lcg(hash31(seedStr)) | local nextInt = lcg(hash31(seedStr)) | ||
local profile = cityProfiles[cityName] or {tags={}, weights=nil} | |||
local tags = {} | |||
-- profile tags | |||
for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end | |||
-- corridor tags | |||
if RAIL[cityName] then tags[#tags+1] = "rail" end | |||
if FERRY[cityName] then tags[#tags+1] = "ferry" end | |||
if tier == "major" then tags[#tags+1] = "dreamspace" end | |||
-- econ tags | |||
local sig = econSignals and econSignals[cityName] or nil | |||
local dom = dominantSector(sig) | |||
local econTag = econTagFromSector(dom) | |||
tags[#tags+1] = econTag | |||
local tagSet = buildTagSet(tags) | |||
-- Threat map | |||
local th = includeThreat and threatMap and threatMap[cityName] or nil | local th = includeThreat and threatMap and threatMap[cityName] or nil | ||
local | local tSev = threatSev(th) | ||
local | -- Build weights | ||
local w = baseWeights() | |||
biasFromTier(w, tier) | |||
biasFromEcon(w, econTag) | |||
biasFromCorridor(w, RAIL[cityName], FERRY[cityName]) | |||
applyWeights(w, profile.weights) | |||
-- Pick task | -- Pick order + task | ||
local task = | local orderKey = weightedPickKey(nextInt, w) | ||
local o = orderByKey[orderKey] or orders[1] | |||
local task = pickTaskForCity(nextInt, o.tasks, tagSet) | |||
local sev = task.sev | local sev = inferSeverity(profile, econTag, task.sev, tSev) | ||
-- Unit | |||
local unitName = pickOrgUnit(nextInt, sev) | local unitName = pickOrgUnit(nextInt, sev) | ||
local unitMeta = org[unitName] | local unitMeta = org[unitName] | ||
| Line 630: | Line 964: | ||
local headcount = unitMeta.size | local headcount = unitMeta.size | ||
-- Bank note | |||
local bank = pickFromList(nextInt, bankNotes, nil) | local bank = pickFromList(nextInt, bankNotes, nil) | ||
local threatText = "" | -- Threat display | ||
local threatText, threatUrg = "", "" | |||
if includeThreat and th then | if includeThreat and th then | ||
threatText = th.event or "" | threatText = th.event or "" | ||
| Line 643: | Line 978: | ||
date = di.text, | date = di.text, | ||
region = cityRegion, | region = cityRegion, | ||
city = cityLink(cityName), | city = string.format("%s %s", (cityFlag[cityName] or ""), cityLink(cityName)), | ||
order = o.link, | order = o.link, | ||
unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode), | unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode), | ||
task = task.text, | task = task.text, | ||
tags = joinTags(tags), | |||
econ = (showEcon and sig) and econSummary(sig) or "", | |||
bank = bank, | bank = bank, | ||
threat = threatText, | threat = threatText, | ||
| Line 655: | Line 992: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 14) MAIN TABLE | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
function p.makeTable(frame) | function p.makeTable(frame) | ||
| Line 662: | Line 999: | ||
local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no" | local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no" | ||
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes" | local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes" | ||
local limit = tonumber(args.limit or "") | local showEcon = tostring(args.showEcon or "no"):lower() == "yes" | ||
local limit = tonumber(args.limit or "") | |||
local di = getDateInfo() | local di = getDateInfo() | ||
local econSignals = getEconSignals() | |||
-- Optional | -- Optional threat map | ||
local threatMap = {} | local threatMap = {} | ||
if includeThreat then | if includeThreat then | ||
local ok, rel = pcall(require, "Module:ReligiousEvents") | local ok, rel = pcall(require, "Module:ReligiousEvents") | ||
if ok and rel and type(rel.makeTable) == "function" then | if ok and rel and type(rel.makeTable) == "function" then | ||
| Line 679: | Line 1,017: | ||
local out = {} | local out = {} | ||
table.insert(out, "{| class='wikitable sortable'") | table.insert(out, "{| class='wikitable sortable'") | ||
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 | local count = 0 | ||
for _, c in ipairs(cityOrder) do | for _, c in ipairs(cityOrder) do | ||
if regionPass(regionFilter, c.region) then | if regionPass(regionFilter, c.region) then | ||
local row = buildCityRow(di, c | local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon) | ||
table.insert(out, "|-" .. row.style) | table.insert(out, "|-" .. row.style) | ||
local line = string.format("| %s || %s || %s || %s || %s || %s", | |||
row.date, row.city, row.region, row.order, row.unit, row.task | |||
) | |||
if showEcon then | |||
line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") | |||
end | |||
line = line .. " || " .. row.bank | |||
if includeThreat then | if includeThreat then | ||
local threatCell = row.threat ~= "" and (row.threat .. " (" .. | local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a." | ||
line = line .. " || " .. threatCell | |||
end | end | ||
line = line .. " || " .. row.sev | |||
table.insert(out, line) | |||
count = count + 1 | count = count + 1 | ||
| Line 707: | Line 1,053: | ||
if showLegend then | if showLegend then | ||
table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>") | table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%%;'>") | ||
table.insert(out, "<b>Legend:</b> ") | table.insert(out, "<b>Legend:</b> ") | ||
table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Routine rites / civic support ") | table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Routine rites / civic support ") | ||
| Line 715: | Line 1,061: | ||
end | end | ||
if di.calendarEvent and di.calendarEvent ~= "" then | if di.calendarEvent and di.calendarEvent ~= "" then | ||
table.insert(out, string.format("\n<div style='margin-top:0.5em; font-size:90%%;'><b>Calendar note:</b> %s</div>", di.calendarEvent)) | table.insert(out, string.format("\n<div style='margin-top:0.5em; font-size:90%%;'><b>Calendar note:</b> %s</div>", di.calendarEvent)) | ||
| Line 724: | Line 1,069: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 15) SINGLE CITY VIEW | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
function p.makeCity(frame) | function p.makeCity(frame) | ||
local args = (frame and frame.args) or {} | local args = (frame and frame.args) or {} | ||
local city = tostring(args.city or "") | local city = tostring(args.city or "") | ||
if city == "" then | if city == "" then return "''Error: city parameter required.''" end | ||
if not citySet[city] then return "''Error: unknown city (must match canonical name).''" end | |||
end | |||
args. | local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes" | ||
local showEcon = tostring(args.showEcon or "no"):lower() == "yes" | |||
local di = getDateInfo() | local di = getDateInfo() | ||
local | local econSignals = getEconSignals() | ||
local threatMap = {} | local threatMap = {} | ||
| Line 745: | Line 1,091: | ||
end | end | ||
local region, tier = "Core", "minor" | |||
for _, c in ipairs(cityOrder) do | for _, c in ipairs(cityOrder) do | ||
if c.name == city then | if c.name == city then region, tier = c.region, c.tier break end | ||
end | end | ||
local row = buildCityRow(di, city, | local row = buildCityRow(di, {name=city, region=region, tier=tier}, econSignals, threatMap, includeThreat, showEcon) | ||
local out = {} | local out = {} | ||
table.insert(out, "{| class='wikitable'") | table.insert(out, "{| class='wikitable'") | ||
local header = "! Date !! City !! Region !! Cult Order !! Unit !! Tasking" | |||
if showEcon then header = header .. " !! Economic signal" end | |||
header = header .. " !! Temple Bank Notes" | |||
if includeThreat then header = header .. " !! Threat Watch" end | |||
header = header .. " !! Severity" | |||
table.insert(out, header) | |||
table.insert(out, "|-" .. row.style) | |||
local line = string.format("| %s || %s || %s || %s || %s || %s", | |||
row.date, row.city, row.region, row.order, row.unit, row.task | |||
) | |||
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end | |||
line = line .. " || " .. row.bank | |||
if includeThreat then | if includeThreat then | ||
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a." | |||
line = line .. " || " .. threatCell | |||
local threatCell = row.threat ~= "" and (row.threat .. " (" .. | |||
end | end | ||
line = line .. " || " .. row.sev | |||
table.insert(out, line) | |||
table.insert(out, "|}") | table.insert(out, "|}") | ||
return table.concat(out, "\n") | return table.concat(out, "\n") | ||
| Line 774: | Line 1,125: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- 16) ORDER-ONLY VIEW | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
function p.makeOrder(frame) | function p.makeOrder(frame) | ||
| Line 780: | Line 1,131: | ||
local wanted = tostring(args.order or "") | local wanted = tostring(args.order or "") | ||
if wanted == "" then | if wanted == "" then | ||
return "''Error: order parameter required (use exact order name | return "''Error: order parameter required (use exact order name).''" | ||
end | end | ||
local o = orderByName[wanted] | |||
if not o then | |||
return "''Error: unknown order name (must match module order names).''" | |||
end | |||
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes" | |||
local showEcon = tostring(args.showEcon or "no"):lower() == "yes" | |||
local di = getDateInfo() | local di = getDateInfo() | ||
local | local econSignals = getEconSignals() | ||
local threatMap = {} | local threatMap = {} | ||
| Line 795: | Line 1,154: | ||
local out = {} | local out = {} | ||
table.insert(out, "{| class='wikitable sortable'") | table.insert(out, "{| class='wikitable sortable'") | ||
local header = "! Date !! City !! Region !! Unit !! Tasking" | |||
if showEcon then header = header .. " !! Economic signal" end | |||
header = header .. " !! Temple Bank Notes" | |||
if includeThreat then header = header .. " !! Threat Watch" end | |||
header = header .. " !! Severity" | |||
table.insert(out, header) | |||
for _, c in ipairs(cityOrder) do | for _, c in ipairs(cityOrder) do | ||
local row = buildCityRow(di, c | local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon) | ||
if row.order == o.link then | |||
if | |||
table.insert(out, "|-" .. row.style) | table.insert(out, "|-" .. row.style) | ||
local line = string.format("| %s || %s || %s || %s || %s", | |||
row.date, row.city, row.region, row.unit, row.task | |||
) | |||
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end | |||
line = line .. " || " .. row.bank | |||
if includeThreat then | if includeThreat then | ||
local threatCell = row.threat ~= "" and (row.threat .. " (" .. | local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a." | ||
line = line .. " || " .. threatCell | |||
end | end | ||
line = line .. " || " .. row.sev | |||
table.insert(out, line) | |||
end | end | ||
end | end | ||
Revision as of 03:01, 31 December 2025
Documentation for this module may be created at Module:MissionaryDailyOpsBVR/doc
-- Module:MissionaryDailyOpsBVR (REWRITE)
-- Daily (PSSC-calendar keyed) missionary tasking ledger for the Temple Bank of the Reformed Stripping Path.
-- Deterministic output (no math.random / no os.time seeding).
--
-- NEW IN THIS REWRITE:
-- • City profiles (culture/spiritual tags + per-city order bias)
-- • Economic signals pulled from General Port of Lake Morovia "Company Table"
-- (counts by sector + sample HQ companies) and used to bias order + task.
-- • Optional Threat Watch integration from Module:ReligiousEvents (opt-in).
--
-- Usage:
-- {{#invoke:MissionaryDailyOpsBVR|makeTable}}
-- {{#invoke:MissionaryDailyOpsBVR|makeTable|region=Core}}
-- {{#invoke:MissionaryDailyOpsBVR|makeTable|includeThreat=yes}}
-- {{#invoke:MissionaryDailyOpsBVR|makeTable|showEcon=yes}}
-- {{#invoke:MissionaryDailyOpsBVR|makeCity|city=Vaeringheim}}
-- {{#invoke:MissionaryDailyOpsBVR|makeOrder|order=Order of Aurora Mystica}}
local p = {}
local cal = require('Module:BassaridianCalendar')
local DAYS_IN_YEAR = 183
--------------------------------------------------------------------------------
-- 0) SMALL UTILS
--------------------------------------------------------------------------------
local function trim(s)
if s == nil then return "" end
s = tostring(s)
s = s:gsub("^%s+", ""):gsub("%s+$", "")
return s
end
local function safeTitleContent(titleText)
local ok, t = pcall(mw.title.new, titleText)
if not ok or not t then return nil end
local ok2, content = pcall(function() return t:getContent() end)
if not ok2 then return nil end
return content
end
local function normalizeSpaces(s)
return (s or ""):gsub("[%s\u00A0]+", " ")
end
local function stripWiki(s)
s = tostring(s or "")
-- Remove files/templates quickly
s = s:gsub("%[%[File:[^%]]+%]%]", "")
s = s:gsub("%{%b{}%}", "")
-- Links: [[A|B]] => B, [[A]] => A
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
-- Bold/italics
s = s:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
-- HTML entities are usually already decoded by MediaWiki; keep safe
return trim(s)
end
local function containsAny(s, words)
s = (s or ""):lower()
for _, w in ipairs(words) do
if s:find(w, 1, true) then return true end
end
return false
end
local function joinTags(tags)
if not tags or #tags == 0 then return "" end
local out = {}
for _, t in ipairs(tags) do out[#out+1] = t end
return table.concat(out, ", ")
end
--------------------------------------------------------------------------------
-- 1) CALENDAR PARSING (mirrors your HatchMinistryDailyOps approach)
--------------------------------------------------------------------------------
local function parseCalendarString(s)
if type(s) ~= "string" then return nil end
local dayOfYear, month, year = s:match("^(%d+),%s+([%a]+)%s+%b()%s*,%s*(%d+)%s+PSSC")
if dayOfYear and year then
return tonumber(year), tonumber(dayOfYear), month
end
return nil
end
local function extractEventText(s)
if type(s) ~= "string" then return "" end
local ev = s:match("PSSC%s+–%s+(.-)%s+–%s+Proverb:")
return ev or ""
end
local function doyToMonthDom(doy)
if doy <= 61 then
return 1, doy
elseif doy <= 122 then
return 2, doy - 61
else
return 3, doy - 122
end
end
local function getDateInfo()
local raw = cal.getCurrentDate()
local y, doy = parseCalendarString(raw)
if not y or not doy then
y, doy = 0, 1
raw = tostring(raw or "")
end
local m, dom = doyToMonthDom(doy)
return {
year = y,
dayOfYear = doy,
monthNum = m,
dom = dom,
text = string.format("%d/%d/%d PSSC", dom, m, y),
raw = raw,
calendarEvent = extractEventText(raw),
}
end
--------------------------------------------------------------------------------
-- 2) DETERMINISTIC RNG (NO bit32, NO math.random)
--------------------------------------------------------------------------------
local function hash31(str)
local h = 0
for i = 1, #str do
h = (h * 131 + str:byte(i)) % 2147483647
end
return h
end
local function lcg(seed)
local s = seed % 2147483647
if s <= 0 then s = s + 2147483646 end
return function()
s = (1103515245 * s + 12345) % 2147483647
return s
end
end
local function pickFromList(nextInt, list, avoidValue)
if #list == 0 then return nil end
if #list == 1 then return list[1] end
local tries = 0
while tries < 12 do
local idx = (nextInt() % #list) + 1
local v = list[idx]
if v ~= avoidValue then return v end
tries = tries + 1
end
for _, v in ipairs(list) do
if v ~= avoidValue then return v end
end
return list[1]
end
--------------------------------------------------------------------------------
-- 3) COLOR CODING (Hatch-style)
--------------------------------------------------------------------------------
local function sevStyle(sev)
if sev == "red" then
return ' style="background-color:#f8d7da;"'
elseif sev == "yellow" then
return ' style="background-color:#fff3cd;"'
else
return ' style="background-color:#d4edda;"'
end
end
--------------------------------------------------------------------------------
-- 4) CITY NETWORK (canonical list)
--------------------------------------------------------------------------------
local function normalizeForFlag(name)
return (name or "")
:gsub("[%s%-’]", "")
:gsub("[Áá]", "A"):gsub("[Éé]", "E")
:gsub("[Íí]", "I"):gsub("[Óó]", "O")
:gsub("[Úú]", "U"):gsub("[Ýý]", "Y")
end
local function cityLink(name)
return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", name, name)
end
-- 54 canonical cities; tier used for "major city" spiritual baseline.
local cityOrder = {
-- Core majors
{name="Vaeringheim", region="Core", tier="major"},
{name="Luminaria", region="Core", tier="major"},
{name="Serena", region="Core", tier="major"},
{name="Pyralis", region="Core", tier="major"},
{name="Symphonara", region="Core", tier="major"},
{name="Aurelia", region="Core", tier="major"},
{name="Somniumpolis", region="Core", tier="major"},
{name="Nexa", region="Core", tier="major"},
{name="Lunalis Sancta", region="Core", tier="major"},
{name="Sylvapolis", region="Core", tier="major"},
-- Core minors
{name="Saluria", region="Core", tier="minor"},
{name="Aetherium", region="Core", tier="minor"},
{name="Ferrum Citadel", region="Core", tier="minor"},
{name="Acheron", region="Core", tier="minor"},
{name="Erythros", region="Core", tier="minor"},
{name="Catonis Atrium", region="Core", tier="minor"},
{name="Delphica", region="Core", tier="minor"},
{name="Koinonía", region="Core", tier="minor"},
{name="Aureum", region="Core", tier="minor"},
-- Alperkin
{name="The Alpazkigz", region="Alperkin", tier="minor"},
{name="Odiferia", region="Alperkin", tier="minor"},
-- New South Jangsong
{name="Skýrophos", region="New South Jangsong", tier="major"},
{name="Bjornopolis", region="New South Jangsong", tier="major"},
{name="Aegirheim", region="New South Jangsong", tier="major"},
{name="Norsolyra", region="New South Jangsong", tier="major"},
{name="Thorsalon", region="New South Jangsong", tier="major"},
{name="Pelagia", region="New South Jangsong", tier="minor"},
{name="Myrene", region="New South Jangsong", tier="minor"},
{name="Thyrea", region="New South Jangsong", tier="minor"},
{name="Ephyra", region="New South Jangsong", tier="minor"},
{name="Halicarn", region="New South Jangsong", tier="minor"},
-- Haifan Bassaridia
{name="Keybir-Aviv", region="Haifan Bassaridia", tier="major"},
{name="Tel-Amin", region="Haifan Bassaridia", tier="major"},
{name="Diamandis", region="Haifan Bassaridia", tier="major"},
{name="Jogi", region="Haifan Bassaridia", tier="major"},
{name="Lewisburg", region="Haifan Bassaridia", tier="major"},
{name="Thermosalem", region="Haifan Bassaridia", tier="minor"},
{name="Akróstadium", region="Haifan Bassaridia", tier="minor"},
{name="Sufriya", region="Haifan Bassaridia", tier="minor"},
{name="Lykopolis", region="Haifan Bassaridia", tier="minor"},
-- Normark
{name="Ardclach", region="Normark", tier="major"},
{name="Riddersborg", region="Normark", tier="major"},
-- Ouriana
{name="Bashkim", region="Ouriana", tier="major"},
{name="Ourid", region="Ouriana", tier="major"},
{name="Tonar", region="Ouriana", tier="minor"},
-- Caledonia
{name="Fanghorn", region="Caledonia", tier="major"},
{name="Eikbu", region="Caledonia", tier="major"},
{name="Galvø", region="Caledonia", tier="major"},
{name="Sårensby", region="Caledonia", tier="minor"},
{name="Slevik", region="Caledonia", tier="minor"},
{name="Sjøsborg", region="Caledonia", tier="minor"},
{name="Storesund", region="Caledonia", tier="minor"},
{name="Notranskja", region="Caledonia", tier="minor"},
{name="Kaledonija", region="Caledonia", tier="minor"},
}
local citySet = {}
local cityFlag = {}
for _, c in ipairs(cityOrder) do
citySet[c.name] = true
cityFlag[c.name] = string.format("[[File:%sFlag.png|20px]]", normalizeForFlag(c.name))
end
-- explicit override used elsewhere on the wiki
cityFlag["Skýrophos"] = "[[File:SkyrophosFlag.png|20px]]"
local function regionPass(filter, cityRegion)
if not filter or filter == "" or filter == "All" then return true end
if filter == "Core" then return cityRegion == "Core" end
if filter == "NSJ" then return cityRegion == "New South Jangsong" end
if filter == "Haifan" then return cityRegion == "Haifan Bassaridia" end
if filter == "Caledonian" then return cityRegion == "Caledonia" end
return cityRegion == filter
end
--------------------------------------------------------------------------------
-- 5) CORRIDORS / ROLE HINTS (from the city-network page; hard-coded for stability)
--------------------------------------------------------------------------------
-- Rail corridor cities listed on the city network overview.
local RAIL = {
["Lykopolis"]=true, ["Lewisburg"]=true, ["Bashkim"]=true, ["Somniumpolis"]=true,
["Erythros"]=true, ["Vaeringheim"]=true, ["Symphonara"]=true, ["Skýrophos"]=true,
["Norsolyra"]=true, ["Pelagia"]=true, ["Riddersborg"]=true, ["Sårensby"]=true,
}
-- Ferry links noted: Jogi + Ardclach
local FERRY = {["Jogi"]=true, ["Ardclach"]=true}
--------------------------------------------------------------------------------
-- 6) MISSIONARY ORGANIZATION (sizes)
--------------------------------------------------------------------------------
local org = {
Lampade = {size=3, code="L"},
Hetairoi = {size=6, code="H"},
Dodekade = {size=12, code="D"},
Kleisthenes = {size=25, code="K"},
Stoa = {size=50, code="S"},
Thiasos = {size=100, code="T"},
}
local function pickOrgUnit(nextInt, sev)
if sev == "green" then
return pickFromList(nextInt, {"Lampade","Hetairoi"}, nil)
elseif sev == "yellow" then
return pickFromList(nextInt, {"Hetairoi","Dodekade","Kleisthenes"}, nil)
else
return pickFromList(nextInt, {"Kleisthenes","Stoa","Thiasos"}, nil)
end
end
--------------------------------------------------------------------------------
-- 7) ORDERS + TASKS (now with tags to match city profile/economy)
--------------------------------------------------------------------------------
local function orderLink(pageTitle, anchor, label)
return string.format("[[%s#%s|%s]]", pageTitle, anchor, label)
end
local function T(text, sev, tags)
return {text=text, sev=sev, tags=tags or {}}
end
local orders = {
{key="OSI", name="Ordo Solis Invicti",
link=orderLink("Atos (Reformed Stripping Path)","Ordo Solis Invicti","Ordo Solis Invicti"),
tasks={
T("Dawn rites and public illumination drill to restore civic clarity and discourage rumor cascades in market wards.","green",{"public","education"}),
T("Temple-school literacy session paired with cosmology instruction and shrine maintenance inspections.","green",{"education","shrine"}),
T("Radiance audit of public lamps, mirrored courts, and shrine sightlines; repair orders issued under Temple Bank stipend.","yellow",{"infrastructure","audit"}),
T("Solar Circle consecration for a new neighborhood shrine, with crowd choreography and compliance wardens.","yellow",{"shrine","crowd"}),
T("Emergency morale procession after night unrest; hymn leadership and calm restoration at transit choke points.","red",{"unrest","crowd","transit"}),
}},
{key="OAM", name="Order of Aurora Mystica",
link=orderLink("Eos (Reformed Stripping Path)","Order of Aurora Mystica","Order of Aurora Mystica"),
tasks={
T("Sunrise vigil and reconciliation rite for competing guilds; signed harmony pledges archived by Temple Bank clerks.","green",{"mediation","guild"}),
T("Dawn-watch observatory check and omen bulletin issued to reduce panic and stabilize labor shifts.","green",{"omen","public"}),
T("Civic blessing for new housing blocks; routing of pilgrimage foot-traffic to prevent crush points.","yellow",{"crowd","infrastructure"}),
T("Counter-heresy sermon cycle following illicit broadsides; witness interviews conducted with local elders.","yellow",{"heresy","investigation"}),
T("Rapid-response pacification after arson rumor; dawn procession under guard with emergency stipend release.","red",{"unrest","arson"}),
}},
{key="HSA", name="Harmony Sanctum",
link=orderLink("Micras (Reformed Stripping Path)","Harmony Sanctum","Harmony Sanctum"),
tasks={
T("Neighborhood mediation session for family disputes and trade-feud spillover; binding accords notarized.","green",{"mediation"}),
T("Harmony clinic for stress and rumor contagion; quiet-room rotations and guided breathing rites.","green",{"health","rumor"}),
T("Loyalty and morale audit of a civic office after complaint surge; corrective training scheduled.","yellow",{"audit","governance"}),
T("Containment of a cult schism meeting; supervised dialogue and sanction warnings delivered.","yellow",{"schism","containment"}),
T("Major crowd-stabilization operation after riot signals; staged dispersal with peace-liturgy and escorts.","red",{"riot","crowd"}),
}},
{key="IGA", name="Ignis Aeternum",
link=orderLink("Pyros (Reformed Stripping Path)","Ignis Aeternum","Ignis Aeternum"),
tasks={
T("Controlled fire blessing for workshops; safety rite that doubles as an inspection of fuel and storage.","green",{"industry","safety"}),
T("Purification burn at a compromised shrine-store; ash sealed and catalogued for Temple Bank disposal.","yellow",{"contam","shrine"}),
T("Investigation of incendiary threats near a gate-temple; firebreak plan issued to local wardens.","yellow",{"arson","investigation"}),
T("Emergency response to active arson or explosive rumor; sanctified suppression teams deployed with escorts.","red",{"arson","crisis"}),
T("Post-incident purification circuit across three oratories, restoring ritual confidence by nightfall.","red",{"incident","public"}),
}},
{key="CHS", name="Celestial Harmony Sect",
link=orderLink("Indigo (Reformed Stripping Path)","Celestial Harmony Sect","Celestial Harmony Sect"),
tasks={
T("Diplomatic courtesy visit to a city council; alignments review and oath renewal with minimal spectacle.","green",{"diplomacy","governance"}),
T("Star-chart instruction for apprentices; quiet observatory checks and record sealing.","green",{"education","astronomy"}),
T("Faction-balance mediation between rival shrines; written compromise placed in Temple Bank archive.","yellow",{"balance","mediation"}),
T("Rumor forensics on ‘forbidden rite’ claims; witness tree compiled and forwarded to Temple authorities.","yellow",{"rumor","investigation"}),
T("High-risk stabilization after cult confrontation; negotiators embedded with escorts to prevent escalation.","red",{"confrontation","crowd"}),
}},
{key="GGS", name="Guild of Golden Shadows",
link=orderLink("Chrysos (Reformed Stripping Path)","Guild of Golden Shadows","Guild of Golden Shadows"),
tasks={
T("Discrete audit of offertory ledgers and voucher seals; minor variances reconciled quietly.","green",{"finance","audit"}),
T("Merchant vetting interviews for a sensitive consignment; anonymity rites certified.","yellow",{"merchant","security"}),
T("Counter-counterfeit sweep targeting forged stamps and false Temple consignments; evidence boxed and logged.","yellow",{"forgery","investigation"}),
T("Recovery detail for stolen reliquary property; surveillance and lawful seizure under civic warrant.","red",{"theft","security"}),
T("Crisis finance stabilization after panic; emergency stipend routing and rumor suppression coordination.","red",{"panic","finance"}),
}},
{key="ASS", name="Azure Sentinel Sect",
link=orderLink("Thalassa (Reformed Stripping Path)","Azure Sentinel Sect","Azure Sentinel Sect"),
tasks={
T("Canal and fountain sanctification circuit; water-quality blessings paired with public reassurance.","green",{"water","public"}),
T("Harbor-side watch for sabotage indicators; shrine-wardens trained on lock and pier checks.","yellow",{"port","sabotage"}),
T("Storm or flood readiness rite; evacuation hymn routes verified with civic stewards.","yellow",{"flood","infrastructure"}),
T("Quarantine support for suspected waterborne illness; controlled distribution points blessed and monitored.","red",{"outbreak","water"}),
T("Major incident response: water infrastructure damage; emergency sanctification and ration-order enforcement.","red",{"infrastructure","crisis"}),
}},
{key="REN", name="Reverie Nebulous",
link=orderLink("Nephele (Reformed Stripping Path)","Reverie Nebulous","Reverie Nebulous"),
tasks={
T("Dream-interpretation clinic to reduce anxiety; rumor narratives redirected into sanctioned symbolism.","green",{"dream","rumor"}),
T("Nocturne counseling for insomnia cluster; soft-lit rites and chant rotation schedules.","green",{"health","sleep"}),
T("Investigation of forbidden homily broadcasts; witness interviews and calm messaging drafted.","yellow",{"broadcast","investigation"}),
T("Containment of mass-vision panic; staged public reassurance with tightly controlled procession.","red",{"panic","crowd"}),
T("Stabilization after hallucination-linked violence; liaison with Harmony Sanctum for long-tail care.","red",{"violence","health"}),
}},
{key="EOF", name="Eon Fellowship",
link=orderLink("Glinos (Reformed Stripping Path)","Eon Fellowship","Eon Fellowship"),
tasks={
T("Cold-weather relief checks and ration fairness review; storage seals verified.","green",{"relief","logistics"}),
T("Infrastructure endurance audit: roofs, walls, and bridges; frost damage triage list issued.","yellow",{"infrastructure","audit"}),
T("Conflict de-escalation for resource disputes; binding queue rules posted at distribution points.","yellow",{"order","queue"}),
T("Emergency shelter activation after storm loss; warm-hall rites and controlled entry.","red",{"storm","relief"}),
T("High-risk escort for frontier convoys under threat; endurance cadres embedded with wardens.","red",{"frontier","security"}),
}},
{key="UOR", name="Order of the Umbral Oracle",
link=orderLink("Noctis (Reformed Stripping Path)","Order of the Umbral Oracle","Order of the Umbral Oracle"),
tasks={
T("Night watch and omen-reading in the temple quarter; guidance slips sealed for officials.","green",{"omen","security"}),
T("Discrete consultation for a magistrate; truth-testing rite witnessed by two scribes.","yellow",{"governance","inquiry"}),
T("Shadow-route mapping after disappearances; safe-path markers installed with civic approval.","yellow",{"security","transit"}),
T("Emergency intervention after ‘demonic sign’ sightings; controlled crowd handling and ward placement.","red",{"demonic","panic"}),
T("High-risk extraction of a threatened cantor; night escort to secure sanctuary.","red",{"abduction","security"}),
}},
{key="MVE", name="Mystery of the Verdant Embrace",
link=orderLink("Opsithe (Reformed Stripping Path)","Mystery of the Verdant Embrace","Mystery of the Verdant Embrace"),
tasks={
T("Harvest blessing and storage rite; spoilage prevention guidance issued to granaries.","green",{"agriculture","harvest"}),
T("Garden-shrine restoration day with community labor; devotional planting aligned to calendar.","green",{"garden","shrine"}),
T("Food shortage mediation and ration arbitration; Temple Bank stipend adjustments proposed.","yellow",{"shortage","logistics"}),
T("Pest or blight containment rites; field inspections with farmers’ associations.","yellow",{"blight","agriculture"}),
T("Famine-risk surge response; emergency distribution queues sanctified and monitored.","red",{"famine","relief"}),
}},
{key="CIL", name="Conclave Illuminara",
link=orderLink("Tarsica (Reformed Stripping Path)","Conclave Illuminara","Conclave Illuminara"),
tasks={
T("Moonlit instruction circle; eclipse-watch protocol rehearsed with apprentice scribes.","green",{"lunar","education"}),
T("Archive sealing of sensitive liturgies; access logs reviewed for anomalies.","yellow",{"archive","audit"}),
T("Public lantern rite to counter rumor cascades; controlled routes and timing set with wardens.","yellow",{"rumor","crowd"}),
T("Emergency omen bulletin after night panic; official statement drafted and delivered.","red",{"panic","public"}),
T("High-risk protection of an observatory site under threat; wards reinforced and watchers stationed.","red",{"threat","security"}),
}},
{key="SCV", name="Sanctum Vitalis",
link=orderLink("Plateau (Reformed Stripping Path)","Sanctum Vitalis","Sanctum Vitalis"),
tasks={
T("Clinic day: basic health screenings and hygiene instruction; water points checked.","green",{"health","public"}),
T("Supplement and ration fortification distribution under Temple Bank authorization.","yellow",{"nutrition","relief"}),
T("Outbreak protocol rehearsal with civic healers; spacing rules posted at shrines.","yellow",{"outbreak","training"}),
T("Active outbreak response: triage lanes established; quarantine support and calm messaging.","red",{"outbreak","crisis"}),
T("Mass-casualty contingency drill after violence reports; intake points and transport routes mapped.","red",{"violence","logistics"}),
}},
{key="ALB", name="Temple Alabaster",
link=orderLink("Ivory (Reformed Stripping Path)","Temple_Alabaster","Temple Alabaster"),
tasks={
T("Purity inspection of ritual vessels and incense stock; minor replacements approved.","green",{"purity","shrine"}),
T("Doctrinal correction session after unsigned broadsides; sanctioned texts distributed.","yellow",{"heresy","education"}),
T("Sanctuary security review for valuables; locking discipline and witness rules enforced.","yellow",{"security","shrine"}),
T("Emergency purification after poisoned incense discovery; wards placed and offenders sought.","red",{"poison","crisis"}),
T("High-risk protection of a central temple during unrest; entry control and calm liturgy.","red",{"unrest","security"}),
}},
{key="CIC", name="Court of the Ironclad",
link=orderLink("Fenrir (Reformed Stripping Path)","Court_of_the_Ironclad","Court of the Ironclad"),
tasks={
T("Discipline drill for shrine wardens; non-lethal control and restraint practices.","green",{"training","security"}),
T("Escort for a threatened courier; route cleared and handover documented.","yellow",{"escort","security"}),
T("Guard coordination in a frontier district; watch rotations synchronized with civic militia.","yellow",{"militia","security"}),
T("Immediate reinforcement during riot signals; cordons established and safe egress opened.","red",{"riot","crowd"}),
T("High-risk interdiction support for a seized oratory; secure perimeter until authorities arrive.","red",{"siege","security"}),
}},
{key="ACC", name="Accord Concordia",
link=orderLink("Lukedu (Reformed Stripping Path)","Accord_Concordia","Accord Concordia"),
tasks={
T("Public harmony dialogue; disputants guided to a signed concord recorded by Temple Bank scribes.","green",{"mediation","public"}),
T("Catacomb Harmonic Communion support: logistics, spacing, and participant verification.","yellow",{"rite","logistics"}),
T("Civic cooperation workshop for rival guilds; shared calendar commitments set.","yellow",{"guild","coordination"}),
T("Emergency reconciliation after faction clash; mediators embedded with escorts.","red",{"clash","crowd"}),
T("High-risk crowd calming after panic; sound-liturgical dispersal and safe routes enforced.","red",{"panic","crowd"}),
}},
{key="MSV", name="Mystery of the Stygian Veil",
link=orderLink("Styx (Reformed Stripping Path)","Mystery_of_the_Stygian_Veil","Mystery of the Stygian Veil"),
tasks={
T("Funerary liaison and cemetery vigil support; grief rites standardized.","green",{"funeral","public"}),
T("Transition counseling for displaced families; resettlement blessings recorded.","yellow",{"displacement","relief"}),
T("Investigation of graveyard desecration rumor; ward placement and witness interviews.","yellow",{"desecration","investigation"}),
T("Emergency containment after death-cult provocation; controlled rites to prevent escalation.","red",{"provocation","crowd"}),
T("High-risk escort of relics through hostile streets; secure route planning with wardens.","red",{"escort","security"}),
}},
{key="SYF", name="Sylvan Fellowship",
link=orderLink("Faun (Reformed Stripping Path)","Sylvan_Fellowship","Sylvan Fellowship"),
tasks={
T("Forest-edge shrine maintenance and trail safety check; minor repairs logged.","green",{"trail","shrine"}),
T("Ecology rite with farmers: pest balance and water stewardship guidance.","yellow",{"ecology","agriculture"}),
T("Containment of illegal logging or poaching rumor; community patrol coordination.","yellow",{"poaching","security"}),
T("Emergency response to wildfire risk; firebreak rites and evacuation path marking.","red",{"wildfire","crisis"}),
T("High-risk protection for a threatened grove shrine; cordon and ward reinforcement.","red",{"threat","security"}),
}},
{key="MRM", name="Mystery of Red Mirth",
link=orderLink("Silenus (Reformed Stripping Path)","Myster_of_Red_Mirth","Mystery of Red Mirth"),
tasks={
T("Morale rite and sanctioned celebration oversight; prevent disorder while restoring joy.","green",{"morale","public"}),
T("Festival logistics support; safe routes and water points verified with civic stewards.","yellow",{"festival","crowd"}),
T("Containment of illicit revel causing unrest; correction by sanctioned rite and fines.","yellow",{"illicit","order"}),
T("Emergency morale stabilization after violence; controlled gatherings and escorts.","red",{"violence","crowd"}),
T("High-risk de-escalation during riot-prone festival; staged dispersal and peace hymns.","red",{"riot","crowd"}),
}},
{key="RCA", name="Rex Catonis",
link=orderLink("Cato (Reformed Stripping Path)","Rex_Catonis","Rex Catonis"),
tasks={
T("Civic order inspection: queue rules, market discipline, and shrine traffic flow.","green",{"order","queue"}),
T("Administrative compliance review after complaints; corrective edicts posted.","yellow",{"compliance","governance"}),
T("Guard liaison for a threatened civic hall; protocol rehearsal and watch rotation.","yellow",{"security","governance"}),
T("Emergency restoration of order after mob action; escorted dispersal and curfew-litany.","red",{"mob","crowd"}),
T("High-risk support to authorities during barricade incident; perimeter discipline enforced.","red",{"barricade","security"}),
}},
{key="SDL", name="Sanctum Delphica",
link=orderLink("Sisera (Reformed Stripping Path)","Sanctum_Delphica","Sanctum Delphica"),
tasks={
T("Oracle consultation clinic for officials; questions logged and sealed.","green",{"oracle","governance"}),
T("Archive and library integrity check; catalog drift corrected and access verified.","yellow",{"archive","audit"}),
T("Investigation of forged prophecy pamphlets; counterfeit sources traced.","yellow",{"forgery","investigation"}),
T("Emergency counter-homily after loudspeaker seizure; sanctioned address delivered.","red",{"broadcast","crisis"}),
T("High-risk protection for a key temple scholar; escort and secure lodging arranged.","red",{"threat","security"}),
}},
{key="OAI", name="Ordo Amicitia",
link=orderLink("Erasmus (Reformed Stripping Path)","Ordo_Amicitia","Ordo Amicitia"),
tasks={
T("Charity distribution and household check-ins; kindness register updated for stipend support.","green",{"charity","relief"}),
T("Offering logistics for a major rite; livestock and treasure accounted and sealed.","yellow",{"offering","audit"}),
T("Mediation for neighborhood poverty disputes; aid allocations made transparent.","yellow",{"aid","mediation"}),
T("Emergency relief surge after incident; soup lines sanctified and guarded.","red",{"relief","crowd"}),
T("High-risk protection for a vulnerable procession route; escorts and safe shelters staged.","red",{"procession","security"}),
}},
{key="TIL", name="Temple Illuminata",
link=orderLink("Nikolaj (Reformed Stripping Path)","Temple_Illuminata","Temple Illuminata"),
tasks={
T("Lamp and beacon maintenance blessing; public wayfinding clarity restored.","green",{"beacon","transit"}),
T("Infrastructure illumination audit for tunnels and transit nodes; repair notices issued.","yellow",{"infrastructure","transit"}),
T("Investigation of signal tampering near a transport hub; seals replaced and watchers posted.","yellow",{"tamper","security"}),
T("Emergency lighting deployment after sabotage; temporary beacons installed with escorts.","red",{"sabotage","crisis"}),
T("High-risk stabilization at a transport hub after panic; controlled flow and sanctuary lanes.","red",{"panic","crowd","transit"}),
}},
}
local orderByKey, orderByName, orderKeys, orderNames = {}, {}, {}, {}
for i, o in ipairs(orders) do
orderByKey[o.key] = o
orderByName[o.name] = o
orderKeys[#orderKeys+1] = o.key
orderNames[#orderNames+1] = o.name
end
--------------------------------------------------------------------------------
-- 8) CITY PROFILES (manual cultural/spiritual bias; econ signal adds more)
-- You can expand these over time. Any city not listed here still gets econ+region bias.
--------------------------------------------------------------------------------
local cityProfiles = {
["Vaeringheim"] = {
tags={"capital","port","admin","templebank"},
weights={GGS=4,HSA=3,OSI=2,ASS=2,RCA=2,CIL=2},
},
["Luminaria"] = {
tags={"industry","innovation","dawn"},
weights={TIL=3,OAM=3,EOF=2,OSI=2,IGA=2},
},
["Somniumpolis"] = {
tags={"finance","dream","noctic"},
weights={REN=3,GGS=3,HSA=2,UOR=2,CIL=2,RCA=1},
},
["Pyralis"] = {
tags={"volcanic","hazard","fire"},
weights={IGA=4,ASS=2,SCV=2,EOF=1},
baseSev="yellow",
},
["Lewisburg"] = {
tags={"rail","hub","wetlands"},
weights={TIL=3,RCA=2,ASS=2,GGS=1,SCV=1},
},
["Lykopolis"] = {
tags={"rail","corridor","resource"},
weights={TIL=3,EOF=2,RCA=2,CIC=1},
baseSev="yellow",
},
["Tel-Amin"] = {
tags={"plantation","coffee","trade"},
weights={MVE=2,SYF=2,OAI=2,CHS=1,ASS=1},
},
["Jogi"] = {
tags={"agriculture","markets","fermentation"},
weights={MVE=3,OAI=2,SYF=2,ASS=1,GGS=1},
},
["Acheron"] = {
tags={"frontier","containment","mines"},
weights={CIC=3,EOF=2,MSV=1,UOR=1,IGA=1},
baseSev="yellow",
},
["Skýrophos"] = {
tags={"integration","civic","corridor"},
weights={CHS=2,HSA=2,EOF=2,RCA=2,TIL=1},
},
["Riddersborg"] = {
tags={"north","storm","corridor"},
weights={EOF=3,TIL=2,ASS=1,CIC=1},
baseSev="yellow",
},
["Ardclach"] = {
tags={"north","sea","ferry"},
weights={ASS=3,EOF=2,CIC=1},
baseSev="yellow",
},
}
--------------------------------------------------------------------------------
-- 9) ECONOMIC SIGNALS: parse "Company Table" on General Port page
--------------------------------------------------------------------------------
local econCache = nil
local function sectorOf(industryCell)
local s = (industryCell or ""):lower()
if s:find("agric", 1, true) then return "Agricultural" end
if s:find("service", 1, true) then return "Service" end
if s:find("manufact", 1, true) or s:find("mining", 1, true) or s:find("energy", 1, true) then
return "Industry"
end
-- fallback
return "Other"
end
local function extractCompanyName(cell)
-- Try '''Name''' first
local n = cell:match("'''(.-)'''")
if n and n ~= "" then return stripWiki(n) end
-- Try bold markdown-like **Name** (some pages render that way)
n = cell:match("%*%*(.-)%*%*")
if n and n ~= "" then return stripWiki(n) end
-- fallback: strip wiki and truncate
n = stripWiki(cell)
if #n > 80 then n = n:sub(1, 80) .. "…" end
return n
end
local function extractHQ(cell)
local s = stripWiki(cell)
if s == "" then return nil end
-- Ignore obvious non-city HQs
if s:lower():find("fleet", 1, true) then return nil end
if s:lower():find("no fixed", 1, true) then return nil end
-- Many HQ cells are just the city name after stripWiki.
-- Some may include extra notes; take first clause.
s = s:gsub("%s*;%s*.*$", "")
s = s:gsub("%s*%b()%s*", " ")
s = normalizeSpaces(s)
s = trim(s)
if citySet[s] then return s end
-- Sometimes HQ may be "Vaeringheim (General Port)" etc.
for cname, _ in pairs(citySet) do
if s == cname then return cname end
end
return nil
end
local function parseCompanyTable(wikitext)
local signals = {}
for cname, _ in pairs(citySet) do
signals[cname] = {Agricultural=0, Service=0, Industry=0, Other=0, samples={}}
end
if type(wikitext) ~= "string" then return signals end
-- Find "Company Table" section and first wikitable after it
local startPos = wikitext:find("====%s*Company Table%s*====")
if not startPos then
startPos = wikitext:find("####%s*Company Table")
end
if not startPos then
-- If heading not found, bail with empty signals
return signals
end
local tablePos = wikitext:find("{|", startPos)
if not tablePos then return signals end
local endPos = wikitext:find("|}", tablePos)
if not endPos then return signals end
local tableText = wikitext:sub(tablePos, endPos + 1)
-- Split into rows by "|-"
local rows = {}
for chunk in tableText:gmatch("\n|%-%s*\n(.-)\n|%-%s*\n") do
rows[#rows+1] = chunk
end
-- Also capture last row before "|}" if not matched by above pattern
local lastChunk = tableText:match("\n|%-%s*\n(.-)\n|%}")
if lastChunk then rows[#rows+1] = lastChunk end
for _, row in ipairs(rows) do
-- ignore header rows that start with "!"
if not row:match("^%s*!") then
-- collapse row to a single line; cells separated by "||"
local line = normalizeSpaces(row:gsub("\n", " "))
-- remove leading single "|"
line = line:gsub("^%s*|%s*", "")
local cells = {}
for cell in line:gmatch("([^|]-)%s*||%s*") do
cells[#cells+1] = trim(cell)
end
-- capture last cell after final "||"
local tail = line:match("||%s*([^|].-)%s*$")
if tail and tail ~= "" then
-- only add if we didn't already (gmatch above may omit last)
if #cells == 0 or cells[#cells] ~= trim(tail) then
cells[#cells+1] = trim(tail)
end
end
-- Expect 7 columns; be defensive
-- Industry cell usually around 5; HQ is last.
local industryCell = ""
for _, c in ipairs(cells) do
if containsAny(c, {"agric", "service", "manufact", "mining", "energy"}) then
industryCell = c
break
end
end
local sector = sectorOf(industryCell)
local hqCell = cells[#cells] or ""
local hq = extractHQ(hqCell)
if hq and signals[hq] then
signals[hq][sector] = signals[hq][sector] + 1
-- sample company name from the "Name" column (often 3rd cell)
local nameCell = cells[3] or cells[2] or cells[1] or ""
local companyName = extractCompanyName(nameCell)
if companyName ~= "" and #signals[hq].samples < 2 then
signals[hq].samples[#signals[hq].samples+1] = companyName
end
end
end
end
return signals
end
local function getEconSignals()
if econCache then return econCache end
local content = safeTitleContent("General Port of Lake Morovia")
econCache = parseCompanyTable(content)
return econCache
end
local function dominantSector(sig)
if not sig then return "Other" end
local best, bestN = "Other", -1
for _, k in ipairs({"Agricultural","Industry","Service","Other"}) do
local n = sig[k] or 0
if n > bestN then best, bestN = k, n end
end
return best
end
local function econTagFromSector(sec)
if sec == "Agricultural" then return "agri" end
if sec == "Industry" then return "industry" end
if sec == "Service" then return "service" end
return "mixed"
end
local function econSummary(sig)
if not sig then return "n/a" end
local a = sig.Agricultural or 0
local i = sig.Industry or 0
local s = sig.Service or 0
local o = sig.Other or 0
local dom = dominantSector(sig)
local samp = (sig.samples and #sig.samples > 0) and ("; e.g., " .. table.concat(sig.samples, ", ")) or ""
return string.format("%s (%d agri / %d ind / %d serv / %d other)%s", dom, a, i, s, o, samp)
end
--------------------------------------------------------------------------------
-- 10) OPTIONAL: Threat Watch integration from Module:ReligiousEvents
--------------------------------------------------------------------------------
local function parseReligiousEventsTable(wikitext)
local out = {}
if type(wikitext) ~= "string" then return out end
for line in wikitext:gmatch("[^\n]+") do
if line:match("^|%s*%d+/%d+/%d+%s+PSSC") then
local date, city, event, urg = line:match("^|%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*||%s*(.-)%s*$")
if city and urg then
out[stripWiki(city)] = {urg=trim(urg), event=stripWiki(event or "")}
end
end
end
return out
end
local function threatSev(th)
if not th or not th.urg then return nil end
local u = (th.urg or ""):upper()
if u == "N/A" or u == "LOW" then return "green" end
if u == "MEDIUM" then return "yellow" end
if u == "HIGH" or u == "CRITICAL" then return "red" end
return "yellow"
end
--------------------------------------------------------------------------------
-- 11) WEIGHTED ORDER PICK + TASK PICK (matches city tags + econ tags + corridor tags)
--------------------------------------------------------------------------------
local function weightedPickKey(nextInt, weights)
-- weights: { [orderKey]=number }
local total = 0
for _, ok in ipairs(orderKeys) do
total = total + (weights[ok] or 0)
end
if total <= 0 then
return pickFromList(nextInt, orderKeys, nil)
end
local r = (nextInt() % total) + 1
local acc = 0
for _, ok in ipairs(orderKeys) do
acc = acc + (weights[ok] or 0)
if r <= acc then return ok end
end
return orderKeys[1]
end
local function hasTag(tagSet, t)
return tagSet[t] == true
end
local function buildTagSet(tags)
local set = {}
for _, t in ipairs(tags or {}) do set[t] = true end
return set
end
local function pickTaskForCity(nextInt, taskList, tagSet)
-- Prefer tasks whose tags intersect city tagset
local candidates = {}
for _, task in ipairs(taskList or {}) do
local ok = false
for _, tg in ipairs(task.tags or {}) do
if tagSet[tg] then ok = true break end
end
if ok then candidates[#candidates+1] = task end
end
if #candidates > 0 then
return pickFromList(nextInt, candidates, nil)
end
return pickFromList(nextInt, taskList, nil)
end
local function baseWeights()
local w = {}
for _, ok in ipairs(orderKeys) do w[ok] = 1 end
return w
end
local function applyWeights(w, add)
if not add then return end
for k, v in pairs(add) do
if orderByKey[k] then
w[k] = (w[k] or 0) + tonumber(v or 0)
end
end
end
local function biasFromEcon(w, econTag)
-- Econ bias is intentionally simple and legible
if econTag == "agri" then
applyWeights(w, {MVE=3,SYF=2,OAI=2,ASS=1})
elseif econTag == "industry" then
applyWeights(w, {TIL=3,EOF=2,IGA=2,CIC=1,RCA=1})
elseif econTag == "service" then
applyWeights(w, {GGS=3,HSA=2,RCA=2,CIL=1,CHS=1})
else
applyWeights(w, {CHS=1,HSA=1,ASS=1})
end
end
local function biasFromCorridor(w, isRail, isFerry)
if isRail then
applyWeights(w, {TIL=2,RCA=2,GGS=1,EOF=1,ASS=1})
end
if isFerry then
applyWeights(w, {ASS=2,GGS=1,RCA=1})
end
end
local function biasFromTier(w, tier)
if tier == "major" then
-- Major cities: public order + rumor control tends to matter more
applyWeights(w, {HSA=1,RCA=1,CIL=1,CHS=1})
end
end
--------------------------------------------------------------------------------
-- 12) TEMPLE BANK NOTES (deterministic snippets)
--------------------------------------------------------------------------------
local bankNotes = {
"Temple Bank stipend verified; local shrine receipts reconciled; next audit window scheduled.",
"Voucher authorizations issued for ritual materials; chain-of-custody notes logged for any seized items.",
"Investor liaison note filed; payroll routing confirmed for the deployed unit; compliance clerk assigned.",
"Donation flow stabilized; offertory seals inspected; variance thresholds set for the next three days.",
"Emergency stipend line opened for crowd-control and relief logistics; disbursement witnessed by two scribes.",
}
--------------------------------------------------------------------------------
-- 13) BUILD ONE CITY ROW
--------------------------------------------------------------------------------
local function inferSeverity(cityProfile, econTag, taskSev, threatLevel)
local sev = taskSev or "green"
-- baseline from profile
if cityProfile and cityProfile.baseSev then
sev = cityProfile.baseSev
end
-- econ nudge (industry + service hubs get more "yellow" oversight more often)
if econTag == "industry" or econTag == "service" then
if sev == "green" then sev = "yellow" end
end
-- threat overrides
if threatLevel == "yellow" and sev == "green" then sev = "yellow" end
if threatLevel == "red" and sev ~= "red" then sev = "red" end
return sev
end
local function buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
local cityName, cityRegion, tier = c.name, c.region, c.tier
local seedStr = string.format("%d:%03d:%s", di.year, di.dayOfYear, cityName)
local nextInt = lcg(hash31(seedStr))
local profile = cityProfiles[cityName] or {tags={}, weights=nil}
local tags = {}
-- profile tags
for _, t in ipairs(profile.tags or {}) do tags[#tags+1] = t end
-- corridor tags
if RAIL[cityName] then tags[#tags+1] = "rail" end
if FERRY[cityName] then tags[#tags+1] = "ferry" end
if tier == "major" then tags[#tags+1] = "dreamspace" end
-- econ tags
local sig = econSignals and econSignals[cityName] or nil
local dom = dominantSector(sig)
local econTag = econTagFromSector(dom)
tags[#tags+1] = econTag
local tagSet = buildTagSet(tags)
-- Threat map
local th = includeThreat and threatMap and threatMap[cityName] or nil
local tSev = threatSev(th)
-- Build weights
local w = baseWeights()
biasFromTier(w, tier)
biasFromEcon(w, econTag)
biasFromCorridor(w, RAIL[cityName], FERRY[cityName])
applyWeights(w, profile.weights)
-- Pick order + task
local orderKey = weightedPickKey(nextInt, w)
local o = orderByKey[orderKey] or orders[1]
local task = pickTaskForCity(nextInt, o.tasks, tagSet)
local sev = inferSeverity(profile, econTag, task.sev, tSev)
-- Unit
local unitName = pickOrgUnit(nextInt, sev)
local unitMeta = org[unitName]
local unitIndex = (nextInt() % 12) + 1
local unitCode = string.format("%s-%s%d", o.key, unitMeta.code, unitIndex)
local headcount = unitMeta.size
-- Bank note
local bank = pickFromList(nextInt, bankNotes, nil)
-- Threat display
local threatText, threatUrg = "", ""
if includeThreat and th then
threatText = th.event or ""
threatUrg = th.urg or ""
end
return {
style = sevStyle(sev),
date = di.text,
region = cityRegion,
city = string.format("%s %s", (cityFlag[cityName] or ""), cityLink(cityName)),
order = o.link,
unit = string.format("%s (%d) [%s]", unitName, headcount, unitCode),
task = task.text,
tags = joinTags(tags),
econ = (showEcon and sig) and econSummary(sig) or "",
bank = bank,
threat = threatText,
threatUrg = threatUrg,
sev = sev,
}
end
--------------------------------------------------------------------------------
-- 14) MAIN TABLE
--------------------------------------------------------------------------------
function p.makeTable(frame)
local args = (frame and frame.args) or {}
local regionFilter = tostring(args.region or "All")
local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
local limit = tonumber(args.limit or "")
local di = getDateInfo()
local econSignals = getEconSignals()
-- Optional threat map
local threatMap = {}
if includeThreat then
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
local t = rel.makeTable()
threatMap = parseReligiousEventsTable(t)
end
end
local out = {}
table.insert(out, "{| class='wikitable sortable'")
local header = "! style='white-space:nowrap;' | Date !! City !! Region !! Cult Order !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
if includeThreat then header = header .. " !! Threat Watch" end
header = header .. " !! Severity"
table.insert(out, header)
local count = 0
for _, c in ipairs(cityOrder) do
if regionPass(regionFilter, c.region) then
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
table.insert(out, "|-" .. row.style)
local line = string.format("| %s || %s || %s || %s || %s || %s",
row.date, row.city, row.region, row.order, row.unit, row.task
)
if showEcon then
line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a")
end
line = line .. " || " .. row.bank
if includeThreat then
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
line = line .. " || " .. threatCell
end
line = line .. " || " .. row.sev
table.insert(out, line)
count = count + 1
if limit and count >= limit then break end
end
end
table.insert(out, "|}")
if showLegend then
table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%%;'>")
table.insert(out, "<b>Legend:</b> ")
table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Routine rites / civic support ")
table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Heightened oversight / rumor control / targeted intervention ")
table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Crisis response / unrest / outbreak / high-risk operations")
table.insert(out, "</div>")
end
if di.calendarEvent and di.calendarEvent ~= "" then
table.insert(out, string.format("\n<div style='margin-top:0.5em; font-size:90%%;'><b>Calendar note:</b> %s</div>", di.calendarEvent))
end
return table.concat(out, "\n")
end
--------------------------------------------------------------------------------
-- 15) SINGLE CITY VIEW
--------------------------------------------------------------------------------
function p.makeCity(frame)
local args = (frame and frame.args) or {}
local city = tostring(args.city or "")
if city == "" then return "''Error: city parameter required.''" end
if not citySet[city] then return "''Error: unknown city (must match canonical name).''" end
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
local di = getDateInfo()
local econSignals = getEconSignals()
local threatMap = {}
if includeThreat then
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
threatMap = parseReligiousEventsTable(rel.makeTable())
end
end
local region, tier = "Core", "minor"
for _, c in ipairs(cityOrder) do
if c.name == city then region, tier = c.region, c.tier break end
end
local row = buildCityRow(di, {name=city, region=region, tier=tier}, econSignals, threatMap, includeThreat, showEcon)
local out = {}
table.insert(out, "{| class='wikitable'")
local header = "! Date !! City !! Region !! Cult Order !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
if includeThreat then header = header .. " !! Threat Watch" end
header = header .. " !! Severity"
table.insert(out, header)
table.insert(out, "|-" .. row.style)
local line = string.format("| %s || %s || %s || %s || %s || %s",
row.date, row.city, row.region, row.order, row.unit, row.task
)
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
line = line .. " || " .. row.bank
if includeThreat then
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
line = line .. " || " .. threatCell
end
line = line .. " || " .. row.sev
table.insert(out, line)
table.insert(out, "|}")
return table.concat(out, "\n")
end
--------------------------------------------------------------------------------
-- 16) ORDER-ONLY VIEW
--------------------------------------------------------------------------------
function p.makeOrder(frame)
local args = (frame and frame.args) or {}
local wanted = tostring(args.order or "")
if wanted == "" then
return "''Error: order parameter required (use exact order name).''"
end
local o = orderByName[wanted]
if not o then
return "''Error: unknown order name (must match module order names).''"
end
local includeThreat = tostring(args.includeThreat or "no"):lower() == "yes"
local showEcon = tostring(args.showEcon or "no"):lower() == "yes"
local di = getDateInfo()
local econSignals = getEconSignals()
local threatMap = {}
if includeThreat then
local ok, rel = pcall(require, "Module:ReligiousEvents")
if ok and rel and type(rel.makeTable) == "function" then
threatMap = parseReligiousEventsTable(rel.makeTable())
end
end
local out = {}
table.insert(out, "{| class='wikitable sortable'")
local header = "! Date !! City !! Region !! Unit !! Tasking"
if showEcon then header = header .. " !! Economic signal" end
header = header .. " !! Temple Bank Notes"
if includeThreat then header = header .. " !! Threat Watch" end
header = header .. " !! Severity"
table.insert(out, header)
for _, c in ipairs(cityOrder) do
local row = buildCityRow(di, c, econSignals, threatMap, includeThreat, showEcon)
if row.order == o.link then
table.insert(out, "|-" .. row.style)
local line = string.format("| %s || %s || %s || %s || %s",
row.date, row.city, row.region, row.unit, row.task
)
if showEcon then line = line .. " || " .. (row.econ ~= "" and row.econ or "n/a") end
line = line .. " || " .. row.bank
if includeThreat then
local threatCell = (row.threat ~= "" and (row.threat .. (row.threatUrg ~= "" and (" ("..row.threatUrg..")") or ""))) or "No reports / n.a."
line = line .. " || " .. threatCell
end
line = line .. " || " .. row.sev
table.insert(out, line)
end
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
return p