Module:BassaridiaOutbreaks: Difference between revisions
From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs) No edit summary |
NewZimiaGov (talk | contribs) No edit summary |
||
| Line 4: | Line 4: | ||
-- | -- | ||
-- Public functions: | -- Public functions: | ||
-- outbreaksAll(frame) -> table for | -- outbreaksAll(frame) -> table for all monitored cities | ||
-- outbreaksForCity(frame) -> table for | -- outbreaksForCity(frame) -> table for one monitored city | ||
-- diseaseGlossary(frame) -> legend | -- diseaseGlossary(frame) -> legend | ||
-- | -- | ||
-- | -- Revision: Adds Thyrsan Bloom (THB), a parasitic botanical | ||
-- | -- infection endemic to the Southern Strait Corridor. | ||
-- | -- Saruq Thyrsa and Hayr Chaskal are monitored separately | ||
-- | -- as Southern Strait localities, without treating other | ||
-- | -- nearby territories as Bassaridian dependencies. | ||
-- Existing Noctic Vampirism weighting is preserved. | |||
--------------------------------------------------------- | --------------------------------------------------------- | ||
| Line 20: | Line 21: | ||
-- Date helpers & deterministic daily seed | -- Date helpers & deterministic daily seed | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function getCurrentDateInfo() | local function getCurrentDateInfo() | ||
local startDate = os.time({year=1999, month=8, day=6}) | local startDate = os.time({year=1999, month=8, day=6}) | ||
| Line 44: | Line 44: | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- Config helpers | -- Config helpers | ||
-- freq: base chance a city has at least one reported | |||
-- freq: base chance | -- disease cluster (0..1); default 0.80. | ||
-- max: cap on concurrent diseases per city (1..3); | |||
-- max : cap concurrent diseases per city | -- default 2. | ||
-- | |||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function getConfig(frame) | local function getConfig(frame) | ||
local freq = tonumber(frame and frame.args and frame.args.freq) or 0.80 | local freq = tonumber(frame and frame.args and frame.args.freq) or 0.80 | ||
| Line 71: | Line 64: | ||
end | end | ||
return { | return { freq = freq, max = maxd } | ||
end | end | ||
| Line 80: | Line 70: | ||
-- Utility helpers | -- Utility helpers | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function clamp(n, lo, hi) | local function clamp(n, lo, hi) | ||
if n < lo then return lo end | if n < lo then return lo end | ||
| Line 119: | Line 108: | ||
["Lunalis_Sancta"] = "Lunalis Sancta", | ["Lunalis_Sancta"] = "Lunalis Sancta", | ||
["Ferrum_Citadel"] = "Ferrum Citadel", | ["Ferrum_Citadel"] = "Ferrum Citadel", | ||
["Catonis_Atrium"] = "Catonis Atrium" | ["Catonis_Atrium"] = "Catonis Atrium", | ||
["Saruq_Thyrsa"] = "Saruq Thyrsa", | |||
["Hayr_Chaskal"] = "Hayr Chaskal" | |||
} | } | ||
| Line 128: | Line 119: | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- Disease catalog | -- Disease catalog | ||
-- w: baseline selection weight. | |||
-- w | -- r: reporting pressure used in case synthesis. | ||
-- r | -- THB is an environmentally acquired parasitic condition; | ||
-- | -- its reproductive ecology is not equivalent to R_t. | ||
-- | |||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local diseases = { | local diseases = { | ||
{ | { | ||
name = "Thalassan Rot", | name = "Thalassan Rot", code = "TR", w = 10, r = 1.20, | ||
controls = "canal flush, rat-proofing, food-handler checks" | controls = "canal flush, rat-proofing, food-handler checks" | ||
}, | }, | ||
{ | { | ||
name = "Breath of Nephele", | name = "Breath of Nephele", code = "BN", w = 9, r = 1.12, | ||
controls = "ventilation drills, mask catechism, spacing" | controls = "ventilation drills, mask catechism, spacing" | ||
}, | }, | ||
{ | { | ||
name = "Haifan Salt-Sickness", | name = "Haifan Salt-Sickness", code = "HS", w = 6, r = 1.10, | ||
controls = "indoor rotations, antifungal clinics, dehumidify" | controls = "indoor rotations, antifungal clinics, dehumidify" | ||
}, | }, | ||
{ | { | ||
name = "Pilgrim’s Flux", | name = "Pilgrim’s Flux", code = "PF", w = 9, r = 1.15, | ||
controls = "boil notices, cistern audit, ORS distribution" | controls = "boil notices, cistern audit, ORS distribution" | ||
}, | }, | ||
{ | { | ||
name = "Stygian Dust", | name = "Stygian Dust", code = "SD", w = 5, r = 1.05, | ||
controls = "respirators, mist carts, guide clinic checks" | controls = "respirators, mist carts, guide clinic checks" | ||
}, | }, | ||
{ | { | ||
name = "Iylara’s Rash", | name = "Iylara’s Rash", code = "IR", w = 5, r = 1.06, | ||
controls = "drying powders, swamp barrier, glove protocols" | controls = "drying powders, swamp barrier, glove protocols" | ||
}, | }, | ||
{ | { | ||
name = " | name = "[[Public health and disease in Bassaridia Vaeringheim#Thyrsan Bloom|Thyrsan Bloom]]", | ||
code = " | code = "THB", w = 2, r = 0.42, | ||
w = 6, | controls = "skin-abrasion care, contaminated-soil precautions, irrigation-site screening, rhizostatic treatment, root-network extraction" | ||
}, | |||
{ | |||
name = "Norsolyrian Drain", code = "ND", w = 6, r = 1.18, | |||
controls = "lined ditches, praziquantel, snail abatement" | controls = "lined ditches, praziquantel, snail abatement" | ||
}, | }, | ||
{ | { | ||
name = "Somniumpolis Lung", | name = "Somniumpolis Lung", code = "SL", w = 5, r = 1.07, | ||
controls = "dust suppression, respirator fit-checks" | controls = "dust suppression, respirator fit-checks" | ||
}, | }, | ||
{ | { | ||
name = "Sea-Cow Ague", | name = "Sea-Cow Ague", code = "SCA", w = 3, r = 1.02, | ||
controls = "livestock PPE, pasteurize, vaccination drills" | controls = "livestock PPE, pasteurize, vaccination drills" | ||
}, | }, | ||
{ | { | ||
name = "Ezeri Blisters", | name = "Ezeri Blisters", code = "EB", w = 3, r = 1.00, | ||
controls = "gloves for fishers, wound care, short exclusion" | controls = "gloves for fishers, wound care, short exclusion" | ||
}, | }, | ||
{ | { | ||
name = "Agnian Lockjaw", | name = "Agnian Lockjaw", code = "AL", w = 3, r = 0.95, | ||
controls = "tetanus boosters, injury clinics" | controls = "tetanus boosters, injury clinics" | ||
}, | }, | ||
{ | { | ||
name = "Caspazani Madness", | name = "Caspazani Madness", code = "CM", w = 2, r = 1.00, | ||
controls = "PEP, bat-avoidance patrols, night rosters" | controls = "PEP, bat-avoidance patrols, night rosters" | ||
}, | }, | ||
{ | { | ||
name = "Festival Blush", | name = "Festival Blush", code = "FB", w = 8, r = 1.25, | ||
controls = "ring vaccination, crowd-spacing, dorm rotation" | controls = "ring vaccination, crowd-spacing, dorm rotation" | ||
}, | }, | ||
{ | { | ||
name = "Strip Yellowing", | name = "Strip Yellowing", code = "SY", w = 10, r = 1.10, | ||
controls = "single-use ladles, vendor relicensing, recall" | controls = "single-use ladles, vendor relicensing, recall" | ||
}, | }, | ||
{ | { | ||
name = "Haifan Crimson Fever", | name = "Haifan Crimson Fever", code = "HCF", w = 3, r = 1.30, | ||
controls = "larval control, dusk patrols, vector abatement" | controls = "larval control, dusk patrols, vector abatement" | ||
}, | }, | ||
{ | { | ||
name = "Ouridian Bite", | name = "Ouridian Bite", code = "OB", w = 4, r = 1.10, | ||
controls = "tick checks, permethrin, steppe trail advisories" | controls = "tick checks, permethrin, steppe trail advisories" | ||
}, | }, | ||
{ | { | ||
name = "Eye of Styx", | name = "Eye of Styx", code = "ES", w = 4, r = 1.00, | ||
controls = "eye-wash stations, visor compliance" | controls = "eye-wash stations, visor compliance" | ||
}, | }, | ||
{ | { | ||
name = "Kalithros’ Numbness", | name = "Kalithros’ Numbness", code = "KN", w = 2, r = 1.03, | ||
controls = "solvent substitution, exhaust capture, nitrile PPE" | controls = "solvent substitution, exhaust capture, nitrile PPE" | ||
}, | }, | ||
{ | { | ||
name = "Cato’s Curse", | name = "Cato’s Curse", code = "CC", w = 4, r = 1.05, | ||
controls = "PPE audits, metal exposure controls" | controls = "PPE audits, metal exposure controls" | ||
}, | }, | ||
{ | { | ||
name = "Eidolan Breath", | name = "Eidolan Breath", code = "EiB", w = 2, r = 1.22, | ||
controls = "airflow baffles, rite spacing, envoy oversight" | controls = "airflow baffles, rite spacing, envoy oversight" | ||
}, | }, | ||
{ | { | ||
name = "White Pestilence", | name = "White Pestilence", code = "WP", w = 2, r = 1.40, | ||
controls = "tiered quarantine, convoy assays, cordons" | controls = "tiered quarantine, convoy assays, cordons" | ||
}, | }, | ||
{ | { | ||
name = "[[Noctic Vampirism]]", | name = "[[Noctic Vampirism]]", code = "NRV", w = 4, r = 1.08, | ||
controls = "raw [[Noctic-Rabrev]] leaf suspension, [[Temple Bank of the Reformed Stripping Path|Temple Bank]] registration check, cult-health evaluation, milk and blood-substitute stabilization, Noctic custody audit" | controls = "raw [[Noctic-Rabrev]] leaf suspension, [[Temple Bank of the Reformed Stripping Path|Temple Bank]] registration check, cult-health evaluation, milk and blood-substitute stabilization, Noctic custody audit" | ||
} | } | ||
| Line 300: | Line 227: | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- Noctic-Rabrev / Noctic Vampirism risk tiers | -- Noctic-Rabrev / Noctic Vampirism risk tiers | ||
-- | -- Noctic-specific odds and outbreak-frequency bonuses. | ||
- | |||
- | |||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local nocticRisk = { | local nocticRisk = { | ||
["Odiferia"] = 8, | ["Odiferia"] = 8, | ||
| Line 324: | Line 241: | ||
["Jogi"] = 3, | ["Jogi"] = 3, | ||
["Aurelia"] = 3 | ["Aurelia"] = 3 | ||
} | |||
--------------------------------------------------------- | |||
-- Thyrsan Bloom environmental risk tiers. | |||
-- Only the two specified Southern Strait locations have | |||
-- high endemic risk. Outside the corridor, rare imported | |||
-- or environmentally linked clusters remain possible. | |||
-- No uniform countrywide frequency bonus is imposed. | |||
--------------------------------------------------------- | |||
local thyrsanRisk = { | |||
["Saruq Thyrsa"] = 12, | |||
["Hayr Chaskal"] = 10 | |||
} | } | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- Per-city disease biases | -- Per-city disease biases | ||
-- Standard city biases are boosted x3. | -- Standard city biases are boosted x3. | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local cityBias = { | local cityBias = { | ||
-- Capital, culinary Noctic usage, ports, canals, festivals | -- Capital, culinary Noctic usage, ports, canals, festivals | ||
| Line 381: | Line 305: | ||
["Fanghorn"] = {"BN", "PF", "SD"}, | ["Fanghorn"] = {"BN", "PF", "SD"}, | ||
["Notranskja"] = {"BN", "PF"}, | ["Notranskja"] = {"BN", "PF"}, | ||
["Hamarrfell"] = {"BN", "PF", "SD"} | ["Hamarrfell"] = {"BN", "PF", "SD"}, | ||
-- Southern Strait Corridor: botanical parasite and moist soils | |||
["Saruq Thyrsa"] = {"THB", "PF", "IR"}, | |||
["Hayr Chaskal"] = {"THB", "PF", "TR"} | |||
} | } | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- City | -- City / locality roster. | ||
-- Existing city positions are retained so their daily | |||
-- deterministic seeds are not shifted by new localities. | |||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local cityData = { | local cityData = { | ||
-- BVR majors/minors | -- BVR majors/minors | ||
| Line 452: | Line 381: | ||
-- Valley of Keltia | -- Valley of Keltia | ||
{city = cityLink("Ourid", "Ourid")}, | {city = cityLink("Ourid", "Ourid")}, | ||
{city = cityLink("Bashkim", "Bashkim")} | {city = cityLink("Bashkim", "Bashkim")}, | ||
-- Southern Strait Corridor monitored localities | |||
{city = "Saruq Thyrsa"}, | |||
{city = "Hayr Chaskal"} | |||
} | } | ||
| Line 458: | Line 391: | ||
-- Severity, colors, and trend arrows | -- Severity, colors, and trend arrows | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function severityFor(casesPer100k) | local function severityFor(casesPer100k) | ||
if casesPer100k >= 60 then | if casesPer100k >= 60 then | ||
| Line 482: | Line 414: | ||
local severityRank = { | local severityRank = { | ||
NONE = 0, | NONE = 0, LOW = 1, MEDIUM = 2, HIGH = 3, CRITICAL = 4 | ||
} | } | ||
| Line 501: | Line 429: | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- Outbreak-frequency adjustment | -- Outbreak-frequency adjustment | ||
-- | -- Retain existing Noctic logic; THB's environmental risk | ||
-- affects selection, not generic outbreak frequency. | |||
-- | |||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function cityOutbreakFrequency(cityName, cfg) | local function cityOutbreakFrequency(cityName, cfg) | ||
local risk = nocticRisk[cityName] or 0 | local risk = nocticRisk[cityName] or 0 | ||
| Line 517: | Line 441: | ||
-- Weighted disease picker | -- Weighted disease picker | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function boostedDiseasePool(cityName) | local function boostedDiseasePool(cityName) | ||
local biasCodes = cityBias[cityName] or {} | local biasCodes = cityBias[cityName] or {} | ||
local risk = nocticRisk[cityName] or 0 | local risk = nocticRisk[cityName] or 0 | ||
local thyrsan = thyrsanRisk[cityName] or 0 | |||
local pool = {} | local pool = {} | ||
for i, d in ipairs(diseases) do | for i, d in ipairs(diseases) do | ||
local w = d.w | local w = d.w | ||
if contains(biasCodes, d.code) then | if contains(biasCodes, d.code) then | ||
w = w * 3 | w = w * 3 | ||
end | end | ||
if d.code == "NRV" and risk > 0 then | if d.code == "NRV" and risk > 0 then | ||
w = w * (1 + risk) | w = w * (1 + risk) | ||
end | |||
if d.code == "THB" and thyrsan > 0 then | |||
w = w * (1 + thyrsan) | |||
end | end | ||
| Line 542: | Line 467: | ||
} | } | ||
end | end | ||
return pool | return pool | ||
end | end | ||
| Line 553: | Line 477: | ||
for _ = 1, n do | for _ = 1, n do | ||
local total = 0 | local total = 0 | ||
for i, d in ipairs(pool) do | for i, d in ipairs(pool) do | ||
if not used[i] then | if not used[i] then | ||
| Line 559: | Line 482: | ||
end | end | ||
end | end | ||
if total <= 0 then break end | if total <= 0 then break end | ||
| Line 581: | Line 503: | ||
end | end | ||
end | end | ||
return chosen | return chosen | ||
end | end | ||
| Line 588: | Line 509: | ||
-- Outbreak count logic | -- Outbreak count logic | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function chooseNActive(rnd, cfg, cityName) | local function chooseNActive(rnd, cfg, cityName) | ||
local freq = cityOutbreakFrequency(cityName, cfg) | local freq = cityOutbreakFrequency(cityName, cfg) | ||
if rnd() > freq then | if rnd() > freq then | ||
return 0 | return 0 | ||
| Line 597: | Line 516: | ||
local w = {0.70, 0.25, 0.05} | local w = {0.70, 0.25, 0.05} | ||
if cfg.max == 1 then | if cfg.max == 1 then | ||
w = {1.0, 0.0, 0.0} | w = {1.0, 0.0, 0.0} | ||
| Line 606: | Line 524: | ||
local roll = rnd() | local roll = rnd() | ||
if roll <= w[1] then | if roll <= w[1] then | ||
return 1 | return 1 | ||
| Line 619: | Line 536: | ||
-- City synthesis | -- City synthesis | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function synthCityRow(rnd, cityLinkText, cfg) | local function synthCityRow(rnd, cityLinkText, cfg) | ||
local cityName = canonicalCityName(plainName(cityLinkText)) | local cityName = canonicalCityName(plainName(cityLinkText)) | ||
| Line 628: | Line 544: | ||
local short = {} | local short = {} | ||
local advices = {} | local advices = {} | ||
local totalCases = 0 | local totalCases = 0 | ||
local totalPopBase = 0 | local totalPopBase = 0 | ||
| Line 640: | Line 555: | ||
local cases = math.max(0, math.floor(base + jitter)) | local cases = math.max(0, math.floor(base + jitter)) | ||
-- Noctic | -- Noctic cluster excess retained from original module. | ||
if d.code == "NRV" and (nocticRisk[cityName] or 0) > 0 then | if d.code == "NRV" and (nocticRisk[cityName] or 0) > 0 then | ||
local risk = nocticRisk[cityName] or 0 | local risk = nocticRisk[cityName] or 0 | ||
cases = cases + math.floor(rnd() * risk * 2) | cases = cases + math.floor(rnd() * risk * 2) | ||
end | |||
-- THB counts are new reports of local environmental | |||
-- infection, NOT secondary cases from person-to-person | |||
-- transmission. Lower totals suit its chronic presentation. | |||
if d.code == "THB" then | |||
if (thyrsanRisk[cityName] or 0) > 0 then | |||
cases = 2 + math.floor(rnd() * 7) | |||
else | |||
cases = 1 + math.floor(rnd() * 3) | |||
end | |||
end | end | ||
| Line 654: | Line 577: | ||
local per100k = (cases / popBase) * 100000 | local per100k = (cases / popBase) * 100000 | ||
local sev = severityFor(per100k) | local sev = severityFor(per100k) | ||
if severityRank[sev] > severityRank[maxSeverity] then | if severityRank[sev] > severityRank[maxSeverity] then | ||
maxSeverity = sev | maxSeverity = sev | ||
| Line 670: | Line 592: | ||
end | end | ||
-- R_t cannot represent the ecological reproductive process | |||
-- of Thyrsan Bloom. Use N/A if it is the sole active disease. | |||
local solelyThyrsan = (#picked == 1 and picked[1].code == "THB") | |||
local Rt = (#picked > 0) and (1.0 + rnd() * 0.8) or (0.80 + rnd() * 0.15) | local Rt = (#picked > 0) and (1.0 + rnd() * 0.8) or (0.80 + rnd() * 0.15) | ||
local RtDisplay = solelyThyrsan and "N/A" or string.format("%.2f", Rt) | |||
local trend = (rnd() * 0.6 - 0.3) | local trend = (rnd() * 0.6 - 0.3) | ||
local posRate = (#picked > 0) and (15 + math.floor(rnd() * 25)) or (4 + math.floor(rnd() * 4)) | local posRate = (#picked > 0) and (15 + math.floor(rnd() * 25)) or (4 + math.floor(rnd() * 4)) | ||
local diseasesCell = (#picked > 0) and table.concat(names, ", ") or "—" | local diseasesCell = (#picked > 0) and table.concat(names, ", ") or "—" | ||
local codesCell = (#picked > 0) and table.concat(short, "/") or "—" | local codesCell = (#picked > 0) and table.concat(short, "/") or "—" | ||
| Line 684: | Line 609: | ||
cases24 = totalCases, | cases24 = totalCases, | ||
per100k = math.floor(per100k + 0.5), | per100k = math.floor(per100k + 0.5), | ||
Rt = | Rt = RtDisplay, | ||
trend = trend, | trend = trend, | ||
trendIcon = arrow(trend), | trendIcon = arrow(trend), | ||
| Line 697: | Line 622: | ||
-- Rendering | -- Rendering | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
local function renderHeaderAll(pssc) | local function renderHeaderAll(pssc) | ||
local out = {} | local out = {} | ||
table.insert(out, "== Daily Outbreak Report ==\n") | table.insert(out, "== Daily Outbreak Report ==\n") | ||
table.insert(out, string.format("''(Day %d of Year %d PSSC)''\n\n", pssc.dayOfYear, pssc.psscYear)) | table.insert(out, string.format("''(Day %d of Year %d PSSC)''\n\n", pssc.dayOfYear, pssc.psscYear)) | ||
table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n') | table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n') | ||
table.insert(out, "! City !! Active Diseases !! Codes !! 24h Cases !! /100k !! R<sub>t</sub> !! 7d Trend !! Positivity (%) !! Severity !! Advisory\n") | table.insert(out, "! City !! Active Diseases !! Codes !! 24h Cases !! /100k !! R<sub>t</sub> !! 7d Trend !! Positivity (%) !! Severity !! Advisory\n") | ||
return out | return out | ||
end | end | ||
| Line 711: | Line 633: | ||
local function renderRowAll(row) | local function renderRowAll(row) | ||
return string.format( | return string.format( | ||
"|-\n| %s || %s || %s || %d || %d || % | "|-\n| %s || %s || %s || %d || %d || %s || %s %.0f%% || %d || style=\"background:%s\" | %s || %s\n", | ||
row.cityLink, | row.cityLink, | ||
row.diseases, | row.diseases, | ||
| Line 732: | Line 654: | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- Public: ALL CITIES | -- Public: ALL MONITORED CITIES | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
function p.outbreaksAll(frame) | function p.outbreaksAll(frame) | ||
local cfg = getConfig(frame) | local cfg = getConfig(frame) | ||
local seed = getDailySeed() | local seed = getDailySeed() | ||
math.randomseed(seed) | math.randomseed(seed) | ||
local d = getCurrentDateInfo() | local d = getCurrentDateInfo() | ||
local out = renderHeaderAll(d) | local out = renderHeaderAll(d) | ||
| Line 749: | Line 668: | ||
return math.random() | return math.random() | ||
end | end | ||
local row = synthCityRow(rnd, entry.city, cfg) | local row = synthCityRow(rnd, entry.city, cfg) | ||
table.insert(out, renderRowAll(row)) | table.insert(out, renderRowAll(row)) | ||
| Line 755: | Line 673: | ||
table.insert(out, renderFooter()) | table.insert(out, renderFooter()) | ||
return table.concat(out) | return table.concat(out) | ||
end | end | ||
| Line 762: | Line 679: | ||
-- Public: SINGLE CITY | -- Public: SINGLE CITY | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
function p.outbreaksForCity(frame) | function p.outbreaksForCity(frame) | ||
local q = frame.args.city | local q = frame.args.city | ||
if not q or q == "" then | if not q or q == "" then | ||
return "Error: Please specify a city. E.g. {{#invoke:BassaridiaOutbreaks|outbreaksForCity|city=Vaeringheim}}" | return "Error: Please specify a city. E.g. {{#invoke:BassaridiaOutbreaks|outbreaksForCity|city=Vaeringheim}}" | ||
| Line 774: | Line 689: | ||
local idx = nil | local idx = nil | ||
local link = nil | local link = nil | ||
local qCanonical = canonicalCityName(q) | local qCanonical = canonicalCityName(q) | ||
for i, entry in ipairs(cityData) do | for i, entry in ipairs(cityData) do | ||
local plain = canonicalCityName(plainName(entry.city)) | local plain = canonicalCityName(plainName(entry.city)) | ||
if entry.city == q or plain == q or plain == qCanonical then | if entry.city == q or plain == q or plain == qCanonical then | ||
idx = i | idx = i | ||
| Line 792: | Line 705: | ||
math.randomseed(seed + idx * 97) | math.randomseed(seed + idx * 97) | ||
local function rnd() | local function rnd() | ||
return math.random() | return math.random() | ||
end | end | ||
local row = synthCityRow(rnd, link, cfg) | local row = synthCityRow(rnd, link, cfg) | ||
local out = {} | local out = {} | ||
table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n') | table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n') | ||
table.insert(out, "! City !! Active Diseases !! Codes !! 24h Cases !! /100k !! R<sub>t</sub> !! 7d Trend !! Positivity (%) !! Severity !! Advisory\n") | table.insert(out, "! City !! Active Diseases !! Codes !! 24h Cases !! /100k !! R<sub>t</sub> !! 7d Trend !! Positivity (%) !! Severity !! Advisory\n") | ||
table.insert(out, renderRowAll(row)) | table.insert(out, renderRowAll(row)) | ||
table.insert(out, renderFooter()) | table.insert(out, renderFooter()) | ||
return table.concat(out) | return table.concat(out) | ||
end | end | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
-- Public: | -- Public: GLOSSARY | ||
--------------------------------------------------------- | --------------------------------------------------------- | ||
function p.diseaseGlossary(frame) | function p.diseaseGlossary(frame) | ||
local out = {} | local out = {} | ||
table.insert(out, "=== Disease Glossary & Standard Controls ===\n") | table.insert(out, "=== Disease Glossary & Standard Controls ===\n") | ||
table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n') | table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n') | ||
| Line 824: | Line 731: | ||
table.insert(out, "|}\n") | table.insert(out, "|}\n") | ||
return table.concat(out) | return table.concat(out) | ||
end | end | ||
return p | return p | ||
Latest revision as of 01:58, 10 November 2026
Documentation for this module may be created at Module:BassaridiaOutbreaks/doc
---------------------------------------------------------
-- Module: BassaridiaOutbreaks
-- Daily outbreak tracker for Bassaridia Vaeringheim.
--
-- Public functions:
-- outbreaksAll(frame) -> table for all monitored cities
-- outbreaksForCity(frame) -> table for one monitored city
-- diseaseGlossary(frame) -> legend
--
-- Revision: Adds Thyrsan Bloom (THB), a parasitic botanical
-- infection endemic to the Southern Strait Corridor.
-- Saruq Thyrsa and Hayr Chaskal are monitored separately
-- as Southern Strait localities, without treating other
-- nearby territories as Bassaridian dependencies.
-- Existing Noctic Vampirism weighting is preserved.
---------------------------------------------------------
local p = {}
---------------------------------------------------------
-- Date helpers & deterministic daily seed
---------------------------------------------------------
local function getCurrentDateInfo()
local startDate = os.time({year=1999, month=8, day=6})
local secondsInDay = 86400
local daysPerYear = 183
local now = os.time()
local totalDays = math.floor((now - startDate) / secondsInDay)
local fraction = totalDays / daysPerYear
local psscYear = math.floor(fraction)
local dayOfYear = math.floor((fraction - psscYear) * daysPerYear) + 1
return {
psscYear = psscYear,
dayOfYear = dayOfYear
}
end
local function getDailySeed()
local d = os.date("*t")
return (d.year * 1000) + d.yday
end
---------------------------------------------------------
-- Config helpers
-- freq: base chance a city has at least one reported
-- disease cluster (0..1); default 0.80.
-- max: cap on concurrent diseases per city (1..3);
-- default 2.
---------------------------------------------------------
local function getConfig(frame)
local freq = tonumber(frame and frame.args and frame.args.freq) or 0.80
if freq < 0 then
freq = 0
elseif freq > 1 then
freq = 1
end
local maxd = tonumber(frame and frame.args and frame.args.max) or 2
if maxd < 1 then
maxd = 1
elseif maxd > 3 then
maxd = 3
end
return { freq = freq, max = maxd }
end
---------------------------------------------------------
-- Utility helpers
---------------------------------------------------------
local function clamp(n, lo, hi)
if n < lo then return lo end
if n > hi then return hi end
return n
end
local function contains(list, value)
if not list then return false end
for _, v in ipairs(list) do
if v == value then return true end
end
return false
end
local function cityLink(anchor, label)
return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", anchor, label)
end
local function plainName(link)
local piped = link:match("%[%[[^|%]]+|([^%]]+)%]%]")
if piped then return piped end
local internal = link:match("%[%[([^%]]+)%]%]")
if internal then
local afterHash = internal:match("#(.+)$")
return afterHash or internal
end
local external = link:match("%[https?://[^%s%]]+%s+([^%]]+)%]")
if external then return external end
return link
end
local cityAliases = {
["Hammarfell"] = "Hamarrfell",
["Lunalis_Sancta"] = "Lunalis Sancta",
["Ferrum_Citadel"] = "Ferrum Citadel",
["Catonis_Atrium"] = "Catonis Atrium",
["Saruq_Thyrsa"] = "Saruq Thyrsa",
["Hayr_Chaskal"] = "Hayr Chaskal"
}
local function canonicalCityName(name)
return cityAliases[name] or name
end
---------------------------------------------------------
-- Disease catalog
-- w: baseline selection weight.
-- r: reporting pressure used in case synthesis.
-- THB is an environmentally acquired parasitic condition;
-- its reproductive ecology is not equivalent to R_t.
---------------------------------------------------------
local diseases = {
{
name = "Thalassan Rot", code = "TR", w = 10, r = 1.20,
controls = "canal flush, rat-proofing, food-handler checks"
},
{
name = "Breath of Nephele", code = "BN", w = 9, r = 1.12,
controls = "ventilation drills, mask catechism, spacing"
},
{
name = "Haifan Salt-Sickness", code = "HS", w = 6, r = 1.10,
controls = "indoor rotations, antifungal clinics, dehumidify"
},
{
name = "Pilgrim’s Flux", code = "PF", w = 9, r = 1.15,
controls = "boil notices, cistern audit, ORS distribution"
},
{
name = "Stygian Dust", code = "SD", w = 5, r = 1.05,
controls = "respirators, mist carts, guide clinic checks"
},
{
name = "Iylara’s Rash", code = "IR", w = 5, r = 1.06,
controls = "drying powders, swamp barrier, glove protocols"
},
{
name = "[[Public health and disease in Bassaridia Vaeringheim#Thyrsan Bloom|Thyrsan Bloom]]",
code = "THB", w = 2, r = 0.42,
controls = "skin-abrasion care, contaminated-soil precautions, irrigation-site screening, rhizostatic treatment, root-network extraction"
},
{
name = "Norsolyrian Drain", code = "ND", w = 6, r = 1.18,
controls = "lined ditches, praziquantel, snail abatement"
},
{
name = "Somniumpolis Lung", code = "SL", w = 5, r = 1.07,
controls = "dust suppression, respirator fit-checks"
},
{
name = "Sea-Cow Ague", code = "SCA", w = 3, r = 1.02,
controls = "livestock PPE, pasteurize, vaccination drills"
},
{
name = "Ezeri Blisters", code = "EB", w = 3, r = 1.00,
controls = "gloves for fishers, wound care, short exclusion"
},
{
name = "Agnian Lockjaw", code = "AL", w = 3, r = 0.95,
controls = "tetanus boosters, injury clinics"
},
{
name = "Caspazani Madness", code = "CM", w = 2, r = 1.00,
controls = "PEP, bat-avoidance patrols, night rosters"
},
{
name = "Festival Blush", code = "FB", w = 8, r = 1.25,
controls = "ring vaccination, crowd-spacing, dorm rotation"
},
{
name = "Strip Yellowing", code = "SY", w = 10, r = 1.10,
controls = "single-use ladles, vendor relicensing, recall"
},
{
name = "Haifan Crimson Fever", code = "HCF", w = 3, r = 1.30,
controls = "larval control, dusk patrols, vector abatement"
},
{
name = "Ouridian Bite", code = "OB", w = 4, r = 1.10,
controls = "tick checks, permethrin, steppe trail advisories"
},
{
name = "Eye of Styx", code = "ES", w = 4, r = 1.00,
controls = "eye-wash stations, visor compliance"
},
{
name = "Kalithros’ Numbness", code = "KN", w = 2, r = 1.03,
controls = "solvent substitution, exhaust capture, nitrile PPE"
},
{
name = "Cato’s Curse", code = "CC", w = 4, r = 1.05,
controls = "PPE audits, metal exposure controls"
},
{
name = "Eidolan Breath", code = "EiB", w = 2, r = 1.22,
controls = "airflow baffles, rite spacing, envoy oversight"
},
{
name = "White Pestilence", code = "WP", w = 2, r = 1.40,
controls = "tiered quarantine, convoy assays, cordons"
},
{
name = "[[Noctic Vampirism]]", code = "NRV", w = 4, r = 1.08,
controls = "raw [[Noctic-Rabrev]] leaf suspension, [[Temple Bank of the Reformed Stripping Path|Temple Bank]] registration check, cult-health evaluation, milk and blood-substitute stabilization, Noctic custody audit"
}
}
local diseaseIndex = {}
for i, d in ipairs(diseases) do
diseaseIndex[d.code] = i
end
---------------------------------------------------------
-- Noctic-Rabrev / Noctic Vampirism risk tiers
-- Noctic-specific odds and outbreak-frequency bonuses.
---------------------------------------------------------
local nocticRisk = {
["Odiferia"] = 8,
["Somniumpolis"] = 8,
["Erythros"] = 7,
["Acheron"] = 6,
["Norsolyra"] = 6,
["Skøda"] = 6,
["Nexa"] = 4,
["Pyralis"] = 4,
["Vaeringheim"] = 3,
["Jogi"] = 3,
["Aurelia"] = 3
}
---------------------------------------------------------
-- Thyrsan Bloom environmental risk tiers.
-- Only the two specified Southern Strait locations have
-- high endemic risk. Outside the corridor, rare imported
-- or environmentally linked clusters remain possible.
-- No uniform countrywide frequency bonus is imposed.
---------------------------------------------------------
local thyrsanRisk = {
["Saruq Thyrsa"] = 12,
["Hayr Chaskal"] = 10
}
---------------------------------------------------------
-- Per-city disease biases
-- Standard city biases are boosted x3.
---------------------------------------------------------
local cityBias = {
-- Capital, culinary Noctic usage, ports, canals, festivals
["Vaeringheim"] = {"TR", "WP", "FB", "SY", "NRV"},
-- Noctic-Rabrev / Alperkin / Gloom Forest / Odiferae centers
["Odiferia"] = {"NRV", "IR", "EiB", "ND"},
["Somniumpolis"] = {"NRV", "SL", "EiB", "ND"},
["Erythros"] = {"NRV", "SD", "ES", "FB"},
["Acheron"] = {"NRV", "SD", "ES"},
["Norsolyra"] = {"NRV", "ND", "HCF"},
["Nexa"] = {"NRV", "IR", "SY"},
["Pyralis"] = {"NRV", "SD", "AL"},
["Skøda"] = {"NRV", "BN", "PF", "SD"},
-- Ritual, culinary, and urban Noctic usage
["Jogi"] = {"NRV", "SY", "PF"},
["Aurelia"] = {"NRV", "FB", "SY"},
-- Haifan Bassaridia
["Tel-Amin"] = {"HS", "HCF", "WP"},
["Sufriya"] = {"HCF", "WP", "SY"},
["Keybir-Aviv"] = {"WP", "SY"},
["Thorsalon"] = {"WP"},
["Skýrophos"] = {"WP"},
-- Lake, marsh, forest, and wetland disease environments
["Delphica"] = {"AL"},
["Ephyra"] = {"EB"},
["Krlsgorod"] = {"EB"},
["Kaledonija"] = {"EB"},
["Slevik"] = {"EB", "WP"},
["Sårensby"] = {"WP"},
["Ardclach"] = {"WP", "EB"},
["Riddersborg"] = {"WP", "EB"},
["Saluria"] = {"IR", "SY"},
["Sylvapolis"] = {"IR", "SY"},
-- Valley of Keltia
["Bashkim"] = {"PF", "OB", "SY", "FB"},
["Ourid"] = {"PF", "OB"},
-- Inland mountainous / Caledonian dependencies
["Eikbu"] = {"BN", "PF", "SD"},
["Galvø"] = {"BN", "PF"},
["Fanghorn"] = {"BN", "PF", "SD"},
["Notranskja"] = {"BN", "PF"},
["Hamarrfell"] = {"BN", "PF", "SD"},
-- Southern Strait Corridor: botanical parasite and moist soils
["Saruq Thyrsa"] = {"THB", "PF", "IR"},
["Hayr Chaskal"] = {"THB", "PF", "TR"}
}
---------------------------------------------------------
-- City / locality roster.
-- Existing city positions are retained so their daily
-- deterministic seeds are not shifted by new localities.
---------------------------------------------------------
local cityData = {
-- BVR majors/minors
{city = cityLink("Vaeringheim", "Vaeringheim")},
{city = cityLink("Luminaria", "Luminaria")},
{city = cityLink("Serena", "Serena")},
{city = cityLink("Pyralis", "Pyralis")},
{city = cityLink("Symphonara", "Symphonara")},
{city = cityLink("Aurelia", "Aurelia")},
{city = cityLink("Somniumpolis", "Somniumpolis")},
{city = cityLink("Odiferia", "Odiferia")},
{city = cityLink("Nexa", "Nexa")},
{city = cityLink("Lunalis Sancta", "Lunalis Sancta")},
{city = cityLink("Sylvapolis", "Sylvapolis")},
{city = cityLink("Saluria", "Saluria")},
{city = cityLink("Aetherium", "Aetherium")},
{city = cityLink("Ferrum Citadel", "Ferrum Citadel")},
{city = cityLink("Acheron", "Acheron")},
{city = cityLink("Erythros", "Erythros")},
{city = cityLink("Catonis Atrium", "Catonis Atrium")},
{city = cityLink("Delphica", "Delphica")},
{city = cityLink("Koinonía", "Koinonía")},
{city = cityLink("Aureum", "Aureum")},
-- New South Jangsong
{city = cityLink("Skýrophos", "Skýrophos")},
{city = cityLink("Bjornopolis", "Bjornopolis")},
{city = cityLink("Aegirheim", "Aegirheim")},
{city = cityLink("Norsolyra", "Norsolyra")},
{city = cityLink("Thorsalon", "Thorsalon")},
{city = cityLink("Pelagia", "Pelagia")},
{city = cityLink("Myrene", "Myrene")},
{city = cityLink("Thyrea", "Thyrea")},
{city = cityLink("Ephyra", "Ephyra")},
{city = cityLink("Halicarn", "Halicarn")},
-- Haifan Bassaridia
{city = cityLink("Keybir-Aviv", "Keybir-Aviv")},
{city = cityLink("Tel-Amin", "Tel-Amin")},
{city = cityLink("Diamandis", "Diamandis")},
{city = cityLink("Jogi", "Jogi")},
{city = cityLink("Lewisburg", "Lewisburg")},
{city = cityLink("Thermosalem", "Thermosalem")},
{city = cityLink("Akróstadium", "Akróstadium")},
{city = cityLink("Sufriya", "Sufriya")},
{city = cityLink("Lykopolis", "Lykopolis")},
-- Bassaridian Normark
{city = cityLink("Ardclach", "Ardclach")},
{city = cityLink("Riddersborg", "Riddersborg")},
-- Caledonian dependencies and Eastern Caledonia
{city = cityLink("Notranskja", "Notranskja")},
{city = cityLink("Slevik", "Slevik")},
{city = cityLink("Fanghorn", "Fanghorn")},
{city = cityLink("Sårensby", "Sårensby")},
{city = cityLink("Skøda", "Skøda")},
{city = cityLink("Eikbu", "Eikbu")},
{city = cityLink("Galvø", "Galvø")},
{city = cityLink("Krlsgorod", "Krlsgorod")},
{city = cityLink("Kaledonija", "Kaledonija")},
{city = cityLink("Hamarrfell", "Hamarrfell")},
-- Valley of Keltia
{city = cityLink("Ourid", "Ourid")},
{city = cityLink("Bashkim", "Bashkim")},
-- Southern Strait Corridor monitored localities
{city = "Saruq Thyrsa"},
{city = "Hayr Chaskal"}
}
---------------------------------------------------------
-- Severity, colors, and trend arrows
---------------------------------------------------------
local function severityFor(casesPer100k)
if casesPer100k >= 60 then
return "CRITICAL"
elseif casesPer100k >= 25 then
return "HIGH"
elseif casesPer100k >= 10 then
return "MEDIUM"
elseif casesPer100k >= 1 then
return "LOW"
else
return "NONE"
end
end
local severityColors = {
CRITICAL = "#ffcccc",
HIGH = "#ffe0b2",
MEDIUM = "#fff9c4",
LOW = "#e0f2f1",
NONE = "#f2f2f2"
}
local severityRank = {
NONE = 0, LOW = 1, MEDIUM = 2, HIGH = 3, CRITICAL = 4
}
local function arrow(trend)
if trend > 0.1 then
return "▲"
elseif trend < -0.1 then
return "▼"
else
return "▬"
end
end
---------------------------------------------------------
-- Outbreak-frequency adjustment
-- Retain existing Noctic logic; THB's environmental risk
-- affects selection, not generic outbreak frequency.
---------------------------------------------------------
local function cityOutbreakFrequency(cityName, cfg)
local risk = nocticRisk[cityName] or 0
local bonus = risk * 0.02
return clamp(cfg.freq + bonus, 0, 0.98)
end
---------------------------------------------------------
-- Weighted disease picker
---------------------------------------------------------
local function boostedDiseasePool(cityName)
local biasCodes = cityBias[cityName] or {}
local risk = nocticRisk[cityName] or 0
local thyrsan = thyrsanRisk[cityName] or 0
local pool = {}
for i, d in ipairs(diseases) do
local w = d.w
if contains(biasCodes, d.code) then
w = w * 3
end
if d.code == "NRV" and risk > 0 then
w = w * (1 + risk)
end
if d.code == "THB" and thyrsan > 0 then
w = w * (1 + thyrsan)
end
pool[i] = {
name = d.name,
code = d.code,
w = w,
r = d.r,
controls = d.controls
}
end
return pool
end
local function pickNDiseasesBiased(n, rnd, cityName)
local pool = boostedDiseasePool(cityName)
local chosen = {}
local used = {}
for _ = 1, n do
local total = 0
for i, d in ipairs(pool) do
if not used[i] then
total = total + d.w
end
end
if total <= 0 then break end
local roll = rnd() * total
local acc = 0
local pick = nil
for i, d in ipairs(pool) do
if not used[i] then
acc = acc + d.w
if roll <= acc then
pick = i
break
end
end
end
if pick then
table.insert(chosen, pool[pick])
used[pick] = true
end
end
return chosen
end
---------------------------------------------------------
-- Outbreak count logic
---------------------------------------------------------
local function chooseNActive(rnd, cfg, cityName)
local freq = cityOutbreakFrequency(cityName, cfg)
if rnd() > freq then
return 0
end
local w = {0.70, 0.25, 0.05}
if cfg.max == 1 then
w = {1.0, 0.0, 0.0}
elseif cfg.max == 2 then
local s = 0.70 + 0.25
w = {0.70 / s, 0.25 / s, 0.0}
end
local roll = rnd()
if roll <= w[1] then
return 1
elseif roll <= w[1] + w[2] then
return 2
else
return 3
end
end
---------------------------------------------------------
-- City synthesis
---------------------------------------------------------
local function synthCityRow(rnd, cityLinkText, cfg)
local cityName = canonicalCityName(plainName(cityLinkText))
local nActive = chooseNActive(rnd, cfg, cityName)
local picked = pickNDiseasesBiased(nActive, rnd, cityName)
local names = {}
local short = {}
local advices = {}
local totalCases = 0
local totalPopBase = 0
local maxSeverity = "NONE"
local maxColor = severityColors.NONE
for _, d in ipairs(picked) do
local popBase = 100000
local base = d.r * 10
local jitter = math.floor(rnd() * base * 3 + 0.5)
local cases = math.max(0, math.floor(base + jitter))
-- Noctic cluster excess retained from original module.
if d.code == "NRV" and (nocticRisk[cityName] or 0) > 0 then
local risk = nocticRisk[cityName] or 0
cases = cases + math.floor(rnd() * risk * 2)
end
-- THB counts are new reports of local environmental
-- infection, NOT secondary cases from person-to-person
-- transmission. Lower totals suit its chronic presentation.
if d.code == "THB" then
if (thyrsanRisk[cityName] or 0) > 0 then
cases = 2 + math.floor(rnd() * 7)
else
cases = 1 + math.floor(rnd() * 3)
end
end
totalCases = totalCases + cases
totalPopBase = totalPopBase + popBase
local per100k = (cases / popBase) * 100000
local sev = severityFor(per100k)
if severityRank[sev] > severityRank[maxSeverity] then
maxSeverity = sev
maxColor = severityColors[sev]
end
table.insert(names, d.name)
table.insert(short, d.code)
table.insert(advices, string.format("%s: %s", d.code, d.controls))
end
local per100k = 0
if totalPopBase > 0 then
per100k = (totalCases / totalPopBase) * 100000
end
-- R_t cannot represent the ecological reproductive process
-- of Thyrsan Bloom. Use N/A if it is the sole active disease.
local solelyThyrsan = (#picked == 1 and picked[1].code == "THB")
local Rt = (#picked > 0) and (1.0 + rnd() * 0.8) or (0.80 + rnd() * 0.15)
local RtDisplay = solelyThyrsan and "N/A" or string.format("%.2f", Rt)
local trend = (rnd() * 0.6 - 0.3)
local posRate = (#picked > 0) and (15 + math.floor(rnd() * 25)) or (4 + math.floor(rnd() * 4))
local diseasesCell = (#picked > 0) and table.concat(names, ", ") or "—"
local codesCell = (#picked > 0) and table.concat(short, "/") or "—"
local advisory = (#picked > 0) and table.concat(advices, " • ") or "No active advisories"
return {
cityLink = cityLinkText,
diseases = diseasesCell,
codes = codesCell,
cases24 = totalCases,
per100k = math.floor(per100k + 0.5),
Rt = RtDisplay,
trend = trend,
trendIcon = arrow(trend),
positivity = posRate,
severity = maxSeverity,
color = maxColor,
advisory = advisory
}
end
---------------------------------------------------------
-- Rendering
---------------------------------------------------------
local function renderHeaderAll(pssc)
local out = {}
table.insert(out, "== Daily Outbreak Report ==\n")
table.insert(out, string.format("''(Day %d of Year %d PSSC)''\n\n", pssc.dayOfYear, pssc.psscYear))
table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"\n')
table.insert(out, "! City !! Active Diseases !! Codes !! 24h Cases !! /100k !! R<sub>t</sub> !! 7d Trend !! Positivity (%) !! Severity !! Advisory\n")
return out
end
local function renderRowAll(row)
return string.format(
"|-\n| %s || %s || %s || %d || %d || %s || %s %.0f%% || %d || style=\"background:%s\" | %s || %s\n",
row.cityLink,
row.diseases,
row.codes,
row.cases24,
row.per100k,
row.Rt,
row.trendIcon,
math.abs(row.trend * 100),
row.positivity,
row.color,
row.severity,
row.advisory
)
end
local function renderFooter()
return "|}\n"
end
---------------------------------------------------------
-- Public: ALL MONITORED CITIES
---------------------------------------------------------
function p.outbreaksAll(frame)
local cfg = getConfig(frame)
local seed = getDailySeed()
math.randomseed(seed)
local d = getCurrentDateInfo()
local out = renderHeaderAll(d)
for i, entry in ipairs(cityData) do
math.randomseed(seed + i * 97)
local function rnd()
return math.random()
end
local row = synthCityRow(rnd, entry.city, cfg)
table.insert(out, renderRowAll(row))
end
table.insert(out, renderFooter())
return table.concat(out)
end
---------------------------------------------------------
-- Public: SINGLE CITY
---------------------------------------------------------
function p.outbreaksForCity(frame)
local q = frame.args.city
if not q or q == "" then
return "Error: Please specify a city. E.g. {{#invoke:BassaridiaOutbreaks|outbreaksForCity|city=Vaeringheim}}"
end
local cfg = getConfig(frame)
local seed = getDailySeed()
local idx = nil
local link = nil
local qCanonical = canonicalCityName(q)
for i, entry in ipairs(cityData) do
local plain = canonicalCityName(plainName(entry.city))
if entry.city == q or plain == q or plain == qCanonical then
idx = i
link = entry.city
break
end
end
if not idx then
return "Error: City '" .. q .. "' not found."
end
math.randomseed(seed + idx * 97)
local function rnd()
return math.random()
end
local row = synthCityRow(rnd, link, cfg)
local out = {}
table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n')
table.insert(out, "! City !! Active Diseases !! Codes !! 24h Cases !! /100k !! R<sub>t</sub> !! 7d Trend !! Positivity (%) !! Severity !! Advisory\n")
table.insert(out, renderRowAll(row))
table.insert(out, renderFooter())
return table.concat(out)
end
---------------------------------------------------------
-- Public: GLOSSARY
---------------------------------------------------------
function p.diseaseGlossary(frame)
local out = {}
table.insert(out, "=== Disease Glossary & Standard Controls ===\n")
table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"\n')
table.insert(out, "! Code !! Disease !! Typical Controls\n")
for _, d in ipairs(diseases) do
table.insert(out, string.format("|-\n| %s || %s || %s\n", d.code, d.name, d.controls))
end
table.insert(out, "|}\n")
return table.concat(out)
end
return p