Module:WarLeagueDivisionOpsBVR
From MicrasWiki
Jump to navigationJump to search
Documentation for this module may be created at Module:WarLeagueDivisionOpsBVR/doc
-- Module:WarLeagueDivisionOpsBVR
-- Daily, in-world War League divisional operations log.
-- Integrates Module:BassaridianDailyEvents by parsing today’s city-event/urgency table output.
-- Includes division + city flag icons with a safe fallback to the Imperial Trade Union flag.
-- Caledonian city deployments are handled EXCLUSIVELY by the Council of Kings Division (CKD),
-- and CKD’s sole purpose is Caledonian deployments.
--
-- UPDATE (Ops Alert integration):
-- - Pulls deterministic Ops Alert + Ops Impact from Module:BassaridiaForecast (if available)
-- - Adds Ops Alert + Ops Impact columns to the War League table
-- - Weather (Orange/Red) can become the event trigger if "No reports"
-- - Weather can raise urgency floor (Orange=>MEDIUM, Red=>HIGH)
-- - Sea Closed / Air No-Go forces weather delay posture and suppresses those force lines
local p = {}
--------------------------------------------------------------------------------
-- (A) PSSC DATE (same logic used by Module:BassaridianDailyEvents)
--------------------------------------------------------------------------------
local function getPSSCDate(args)
args = args or {}
local overrideYear = tonumber(args.year or args.psscyear)
local overrideDay = tonumber(args.day or args.dayofyear)
if overrideYear and overrideDay then
if overrideDay < 1 then overrideDay = 1 end
if overrideDay > 183 then overrideDay = 183 end
local month, dayOfMonth
if overrideDay <= 61 then
month = 1
dayOfMonth = overrideDay
elseif overrideDay <= 122 then
month = 2
dayOfMonth = overrideDay - 61
else
month = 3
dayOfMonth = overrideDay - 122
end
local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, overrideYear)
return {
text = dateString,
year = overrideYear,
dayOfYear = overrideDay,
month = month,
dom = dayOfMonth
}
end
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 yearFraction = totalDays / daysPerYear
local psscYear = math.floor(yearFraction)
local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1
local month, dayOfMonth
if dayOfYear <= 61 then
month = 1
dayOfMonth = dayOfYear
elseif dayOfYear <= 122 then
month = 2
dayOfMonth = dayOfYear - 61
else
month = 3
dayOfMonth = dayOfYear - 122
end
local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
return {
text = dateString,
year = psscYear,
dayOfYear = dayOfYear,
month = month,
dom = dayOfMonth
}
end
--------------------------------------------------------------------------------
-- (B) HELPERS
--------------------------------------------------------------------------------
local function pick(t) return t[math.random(#t)] end
local function pickWeighted(list)
local total = 0
for _, it in ipairs(list) do total = total + (it.weight or 1) end
local r = math.random() * total
local acc = 0
for _, it in ipairs(list) do
acc = acc + (it.weight or 1)
if r <= acc then return it end
end
return list[#list]
end
local function bulletList(items)
if not items or #items == 0 then return "" end
return "<ul><li>" .. table.concat(items, "</li><li>") .. "</li></ul>"
end
local function cityLink(cityName, anchorOverride)
local anchor = anchorOverride or cityName
return string.format("[[List_of_cities_in_Bassaridia_Vaeringheim#%s|%s]]", anchor, cityName)
end
local function opId(di, divCode, idx)
return string.format("WL-%03d.%03d-%s-%02d", di.year, di.dayOfYear, divCode, idx)
end
--------------------------------------------------------------------------------
-- (C) FLAG FALLBACK (use ITU flag when missing)
--------------------------------------------------------------------------------
local DEFAULT_FLAG = "Imperial_Trade_Union_flag.png"
local function flagIcon(fileName, sizePx)
local size = tonumber(sizePx) or 20
local f = (fileName and fileName ~= "") and fileName or DEFAULT_FLAG
return string.format("[[File:%s|%dpx]]", f, size)
end
--------------------------------------------------------------------------------
-- (D) CALEDONIAN SET (CKD ONLY)
--------------------------------------------------------------------------------
local CALEDONIAN_CITIES = {
["Fanghorn"] = true,
["Eikbu"] = true,
["Galvø"] = true,
["Skøda"] = true,
["Hamarrfell"] = true,
["Sårensby"] = true,
["Slevik"] = true,
["Sjøsborg"] = true,
["Storesund"] = true,
["Notranskja"] = true,
["Kaledonija"] = true,
["Krlsgorod"] = true,
}
--------------------------------------------------------------------------------
-- (D.1) HAIFAN BASSARIDIA / SOUTHERN STRAIT SET
--
-- CKD is NOT locked to these cities. This set only allows CKD to generate
-- special Southern Strait / former Maritime Markets operations when CKD happens
-- to appear in a Haifan Bassaridian city or node.
--------------------------------------------------------------------------------
local HAIFAN_BASSARIDIA_CITIES = {
["Keybir-Aviv"] = true,
["Tel-Amin"] = true,
["Diamandis"] = true,
["Jogi"] = true,
["Lewisburg"] = true,
["Thermosalem"] = true,
["Akróstadium"] = true,
["Sufriya"] = true,
["Lykopolis"] = true,
}
local function isHaifanBassaridiaCity(cityName)
return HAIFAN_BASSARIDIA_CITIES[tostring(cityName or "")] == true
end
local SOUTHERN_STRAIT_CITY_LIST = {
"Keybir-Aviv",
"Tel-Amin",
"Diamandis",
"Jogi",
"Lewisburg",
"Thermosalem",
"Akróstadium",
"Sufriya",
"Lykopolis"
}
local SOUTHERN_STRAIT_DIVISION_CODES = {
["HFA"] = true, -- Haifan Bassaridia Division
["HAT"] = true, -- Hatch Ministry Division
["CKD"] = true, -- Council of Kings Division, when operating in Southern Strait nodes
}
local function isSouthernStraitDivision(div)
return div and SOUTHERN_STRAIT_DIVISION_CODES[tostring(div.code or "")] == true
end
local function isSouthernStraitOnly(args)
args = args or {}
local z = mw.ustring.lower(tostring(args.zone or args.scope or args.region or args.theater or ""))
return z == "southernstrait"
or z == "southern strait"
or z == "southern straits"
or z == "haifan"
or z == "haifan bassaridia"
or z == "former maritime markets"
end
--------------------------------------------------------------------------------
-- (E) INTEGRATE DAILY EVENTS (parse Module:BassaridianDailyEvents.makeTable())
--------------------------------------------------------------------------------
local function buildEventMapForToday(di)
local ok, daily = pcall(require, "Module:BassaridianDailyEvents")
if not ok or not daily or type(daily.makeTable) ~= "function" then
return {}, false
end
local tbl
if di and di.year and di.dayOfYear then
tbl = daily.makeTable({
args = {
year = tostring(di.year),
day = tostring(di.dayOfYear)
}
})
else
tbl = daily.makeTable()
end
local eventMap = {}
for line in tostring(tbl):gmatch("[^\n]+") do
if line:match("^|%s") and not line:match("^|%-%s*$") then
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
if #parts >= 4 then
local city = parts[2]
local ev = parts[3]
local urg = parts[4]
if city and city ~= "" and not eventMap[city] then
eventMap[city] = { desc = ev, urgency = urg }
end
end
end
end
return eventMap, true
end
local function urgencyScore(urg)
if urg == "CRITICAL" then return 4 end
if urg == "HIGH" then return 3 end
if urg == "MEDIUM" then return 2 end
if urg == "LOW" then return 1 end
return 0
end
local function urgencyMax(a, b)
local sa, sb = urgencyScore(a), urgencyScore(b)
if sb > sa then return b end
return a
end
--------------------------------------------------------------------------------
-- (WX) OPTIONAL: deterministic Ops Alert from Module:BassaridiaForecast
--------------------------------------------------------------------------------
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
if not wxOk then wx = nil end
local function wxColor(level)
level = tonumber(level) or 0
if level == 3 then return "#f8d7da" end
if level == 2 then return "#ffe5cc" end
if level == 1 then return "#fff3cd" end
return "#d4edda"
end
local function wxBadge(level, label, cond)
local bg = wxColor(level)
label = tostring(label or "Green")
cond = tostring(cond or "")
return string.format(
"<span style='background:%s;padding:2px 6px;border:1px solid #ccc;border-radius:10px;white-space:nowrap;'>%s</span>%s",
bg, label, (cond ~= "" and ("<br/><span style=\"font-size:90%;\">" .. cond .. "</span>") or "")
)
end
local function wxImpactText(imp)
imp = imp or {}
return string.format("Gnd: %s; Sea: %s; Air: %s", imp.ground or "OK", imp.sea or "OK", imp.air or "OK")
end
local function getWx(cityName, di)
if not wx or type(wx.getOpsData) ~= "function" then return nil end
return wx.getOpsData(cityName, di.year, di.dayOfYear)
end
local function floorUrgencyFromWx(level)
level = tonumber(level) or 0
if level >= 3 then return "HIGH" end
if level == 2 then return "MEDIUM" end
if level == 1 then return "LOW" end
return nil
end
--------------------------------------------------------------------------------
-- (F) FORCE ORGANIZATION
--------------------------------------------------------------------------------
local GROUND_ECHELONS = {
{ name="Turmae", weight=5 },
{ name="Contubernia", weight=5 },
{ name="Ordinis", weight=4 },
{ name="Manipuli", weight=3 },
{ name="Centuriae", weight=2 },
{ name="Cohortes", weight=1 },
}
local NAVAL_ECHELONS = {
{ name="Birlik-i Gemi (Patrol Element)", weight=5 },
{ name="Tabur-i Derya (Task Unit)", weight=3 },
{ name="Ordu (Task Group)", weight=1 },
}
local AIR_ECHELONS = {
{ name="Zeygi (Element)", weight=5 },
{ name="Ptisis (Flight)", weight=3 },
{ name="Moira (Squadron)", weight=1 },
}
local function pickEchelonForUrgency(echelons, uScore)
local tuned = {}
for _, e in ipairs(echelons) do
local w = e.weight or 1
if uScore >= 3 then
if e.name:match("Cohortes") or e.name:match("Ordu %(") or e.name:match("Moira") then
w = w + 3
elseif e.name:match("Centuriae") or e.name:match("Tabur%-i Derya") or e.name:match("Ptisis") then
w = w + 2
end
elseif uScore == 2 then
if e.name:match("Centuriae") or e.name:match("Tabur%-i Derya") or e.name:match("Ptisis") then
w = w + 1
end
end
table.insert(tuned, { name = e.name, weight = w })
end
return pickWeighted(tuned).name
end
--------------------------------------------------------------------------------
-- (G) FLAGS (only known files; everything else auto-falls back to ITU)
--------------------------------------------------------------------------------
local CITY_FLAGS = {
-- Vaeringheim investor belt
["Vaeringheim"] = "VaeringheimFlag.png",
["Lunalis Sancta"] = "LunalisSanctaFlag.png",
["Luminaria"] = "LuminariaFlag.png",
["Serena"] = "SerenaFlag.png",
["Symphonara"] = "SymphonaraFlag.png",
["Aurelia"] = "AureliaFlag.png",
["Sylvapolis"] = "SylvapolisFlag.png",
["Delphica"] = "DelphicaFlag.png",
["Catonis Atrium"] = "CatonisAtriumFlag.png",
["Ferrum Citadel"] = "FerrumCitadelFlag.png",
-- Alpazkigz belt
["Pyralis"] = "PyralisFlag.png",
["Nexa"] = "NexaFlag.png",
["Saluria"] = "SaluriaFlag.png",
["Acheron"] = "AcheronFlag.png",
["Koinonía"] = "KoinoniaFlag.png",
["Aureum"] = "AureumFlag.png",
["Aetherium"] = "AetheriumFlag.png",
["Erythros"] = "ErythrosFlag.png",
-- Odiferia
["Somniumpolis"] = "SomniumpolisFlag.png",
["Odiferia"] = "OdiferiaFlag.png",
-- Hafaan / Jeseri (if they exist; otherwise fallback)
["Hafaan"] = "HafaanFlag.png",
["Jeseri"] = "JeseriFlag.png",
-- NSJ
["Skýrophos"] = "SkyrophosFlag.png",
["Bjornopolis"] = "BjornopolisFlag.png",
["Aegirheim"] = "AegirheimFlag.png",
["Norsolyra"] = "NorsolyraFlag.png",
["Thorsalon"] = "ThorsalonFlag.png",
["Myrene"] = "MyreneFlag.png",
["Ephyra"] = "EphyraFlag.png",
["Thyrea"] = "ThyreaFlag.png",
["Pelagia"] = "PelagiaFlag.png",
["Halicarn"] = "HalicarnFlag.png",
-- Haifan
["Keybir-Aviv"] = "KeybirAvivFlag.png",
["Tel-Amin"] = "TelAminFlag.png",
["Jogi"] = "JogiFlag.png",
["Lewisburg"] = "LewisburgFlag.png",
["Diamandis"] = "DiamandisFlag.png",
["Thermosalem"] = "ThermosalemFlag.png",
["Akróstadium"] = "AkrostadiumFlag.png",
["Lykopolis"] = "LykopolisFlag.png",
["Sufriya"] = "SufriyaFlag.png",
-- Normark + Ouriana
["Riddersborg"] = "RiddersborgFlag.png",
["Ardclach"] = "ArdclachFlag.png",
["Ourid"] = "Ouridflag.png",
["Bashkim"] = "BashkimFlag.png",
-- Tonar flag unknown -> fallback by omission
}
local DIVISION_FLAGS = {
["Vaeringheim Division"] = "VaeringheimDivisionFlag.png",
["Alpazkigz Division"] = "AlpazkigzDivisionFlag.png",
["Hatch Ministry Division"] = "HatchMinistryFlag.png",
["New South Jangsong Division"] = "NewSouthJangsongFlag.png",
["Haifan Bassaridia Division"] = "HaifaDivisionFlag.png",
["Bassaridian Normark Division"] = "BassaridianNormarkFlagB.png",
["Ouriana Division"] = "OurianaFlag.png",
-- Odiferia Division / CKD: omit unless you have dedicated division flags; fallback applies
}
--------------------------------------------------------------------------------
-- (H) DIVISIONS + CITY POOLS
-- NOTE: Caledonian cities removed from all divisions except CKD.
-- CKD's sole purpose: Caledonian deployments.
--------------------------------------------------------------------------------
local DIVISIONS = {
{ name="Vaeringheim Division", link="[[Vaeringheim Division]]", code="VAE", weight=5,
cities={"Vaeringheim","Luminaria","Serena","Symphonara","Aurelia","Somniumpolis","Nexa","Lunalis Sancta","Sylvapolis","Delphica","Catonis Atrium","Ferrum Citadel"} },
{ name="Alpazkigz Division", link="[[Alpazkigz Division]]", code="ALP", weight=4,
cities={"The Alpazkigz","Pyralis","Nexa","Saluria","Acheron","Koinonía","Aureum","Aetherium","Erythros"} },
{ name="Odiferia Division", link="[[Odiferia Division]]", code="ODF", weight=3,
cities={"Odiferia","Somniumpolis","Aetherium","Ferrum Citadel","Aurelia","Symphonara"} },
{ name="Hatch Ministry Division", link="[[Hatch Ministry Division]]", code="HAT", weight=3,
cities={"Lewisburg","Lykopolis","Jogi","Keybir-Aviv","Tel-Amin","Diamandis","Thermosalem","Akróstadium","Sufriya"} },
{ name="New South Jangsong Division", link="[[New South Jangsong Division]]", code="NSJ", weight=3,
cities={"Skýrophos","Bjornopolis","Aegirheim","Norsolyra","Thorsalon","Pelagia","Myrene","Thyrea","Ephyra","Halicarn"} },
{ name="Haifan Bassaridia Division", link="[[Haifan Bassaridia Division]]", code="HFA", weight=3,
cities={"Keybir-Aviv","Tel-Amin","Diamandis","Jogi","Lewisburg","Thermosalem","Akróstadium","Sufriya","Lykopolis"} },
{ name="Bassaridian Normark Division",link="[[Bassaridian Normark Division]]",code="BNO", weight=2,
cities={"Ardclach","Riddersborg"} },
{ name="Ouriana Division", link="[[Ouriana Division]]", code="OUR", weight=2,
cities={"Bashkim","Ourid","Tonar"} },
{ name="Council of Kings Division", link="[[Council of Kings Division]]", code="CKD", weight=1,
cities={
-- Eastern Caledonian Highlands / Caledonian corridor nodes
"Fanghorn",
"Eikbu",
"Galvø",
"Skøda",
"Hamarrfell",
"Sårensby",
"Slevik",
"Sjøsborg",
"Storesund",
"Notranskja",
"Kaledonija",
"Krlsgorod",
-- Haifan Bassaridia / Southern Strait nodes
"Keybir-Aviv",
"Tel-Amin",
"Diamandis",
"Jogi",
"Lewisburg",
"Thermosalem",
"Akróstadium",
"Sufriya",
"Lykopolis"
} },
}
--------------------------------------------------------------------------------
-- (I) Guardrail: if a Caledonian city ever appears elsewhere, force CKD
--------------------------------------------------------------------------------
local function forceCKDIfCaledonian(div, cityName)
-- Caledonian cities remain exclusive to CKD.
if CALEDONIAN_CITIES[cityName] and div.code ~= "CKD" then
for _, d in ipairs(DIVISIONS) do
if d.code == "CKD" then
return d
end
end
end
return div
end
local function isAllowedCKDCity(cityName)
return CALEDONIAN_CITIES[cityName] or HAIFAN_BASSARIDIA_CITIES[cityName]
end
--------------------------------------------------------------------------------
-- (J) OPERATION TYPES
--------------------------------------------------------------------------------
local OP_GROUND = {
{ name="corridor security (rail / canal / road checkpointing)", weight=4 },
{ name="urban stabilization (cordon / curfew enforcement)", weight=3 },
{ name="anti-smuggling interdiction / contraband seizure", weight=3 },
{ name="infrastructure protection (port/bridge/aqueduct)", weight=3 },
{ name="counter-insurgency sweep (cell disruption)", weight=2 },
{ name="hazard response (evacuation / debris clearance)", weight=2 },
{ name="convoy escort (supply / detainee / artifact transfer)", weight=2 },
{ name="Noctic cargo custody support (clinic / ledger / crate-seal control)", weight=1 },
}
local OP_NAVAL = {
{ name="maritime escort / lake patrol routing", weight=3 },
{ name="VBSS / interdiction / contraband boarding", weight=3 },
{ name="blockade enforcement / straits compliance sweep", weight=2 },
{ name="harbor security (lock-gates / inner basin patrol)", weight=2 },
{ name="Noctic Fleet cargo inspection / raw-leaf movement control", weight=1 },
}
local OP_AIR = {
{ name="ISR overwatch / route scanning", weight=4 },
{ name="rapid extraction / medevac lift", weight=2 },
{ name="strike deterrence package (show of force)", weight=1 },
{ name="Noctic-Rabrev cargo ISR / clinic-route overwatch", weight=1 },
}
--------------------------------------------------------------------------------
-- (J.0.1) CKD SOUTHERN STRAIT / FORMER MARITIME MARKETS OPERATIONS
--
-- These operations are used only when CKD appears in a Haifan Bassaridian
-- city/node. They are naval-led, but include inland route, mountain approach,
-- rail/canal/road, and trade-corridor enforcement.
--------------------------------------------------------------------------------
local CKD_SOUTHERN_STRAIT_NAVAL_OPS = {
{ name="CKD Southern Strait naval screen (patrol craft / boarding cordon / former Maritime Markets traffic review)", weight=6, urg="MEDIUM" },
{ name="CKD Southern Strait VBSS operation (unlit lighter inspection / broker-runner interdiction / forged flag review)", weight=5, urg="MEDIUM" },
{ name="CKD former Maritime Markets interdiction sweep (small-craft seizure / old broker quay watch / prize evidence custody)", weight=5, urg="HIGH" },
{ name="CKD straits compliance patrol (ferry lane control / bonded cargo lighter inspection / registry-seal verification)", weight=5, urg="MEDIUM" },
{ name="CKD missionary-sea escort screen (pilgrimage pier security / shrine boat routing / hostile skiff exclusion)", weight=4, urg="MEDIUM" },
{ name="CKD Southern Strait blockade-warning posture (selective cordon / lawful passage marking / suspicious craft shadowing)", weight=4, urg="HIGH" },
{ name="CKD anti-smuggling boat net (canal mouth patrol / false distress lamp check / hidden cargo hold search)", weight=4, urg="MEDIUM" },
{ name="CKD prize-court transfer escort (captured lighter convoy / detained crew protection / cargo ledger custody)", weight=3, urg="MEDIUM" },
{ name="CKD coastal radar and signal-lamp discipline sweep (false beacon suppression / night-route verification)", weight=3, urg="LOW" },
}
local CKD_SOUTHERN_STRAIT_GROUND_OPS = {
{ name="CKD Southern Strait inland enforcement line (quay-to-road checkpoints / caravan yard screening / bonded cargo transfer control)", weight=5, urg="MEDIUM" },
{ name="CKD former Maritime Markets broker-lane sweep (warehouse cordon / ledger seizure / broker-agent detention support)", weight=5, urg="HIGH" },
{ name="CKD trade-route interdiction package (rail siding check / canal-road transfer audit / counterfeit permit seizure)", weight=4, urg="MEDIUM" },
{ name="CKD shrine-and-market security cordon (missionary movement protection / crowd lanes / anti-agitator screening)", weight=4, urg="MEDIUM" },
{ name="CKD mountain approach patrol (upland pass watch / smuggler mule-track denial / overlook post rotation)", weight=3, urg="MEDIUM" },
{ name="CKD inland caravan compliance sweep (pack-train search / invoice reconciliation / suspicious escort company review)", weight=3, urg="MEDIUM" },
{ name="CKD Southern Strait depot-security operation (freight office hardening / sealed evidence movement / witness shielding)", weight=3, urg="LOW" },
{ name="CKD road-and-canal junction lockdown drill (rapid barrier placement / detainee lane setup / cargo-stack isolation)", weight=2, urg="HIGH" },
}
local CKD_SOUTHERN_STRAIT_AIR_OPS = {
{ name="CKD Southern Strait ISR grid (ferry wakes / skiff clusters / concealed cove activity / broker compound watch)", weight=5, urg="MEDIUM" },
{ name="CKD maritime-air overwatch (boarding team cover / patrol-craft route scan / false-lamp triangulation)", weight=5, urg="MEDIUM" },
{ name="CKD mountain-route ISR sweep (upland mule tracks / overlook camps / hidden caravan trails)", weight=4, urg="MEDIUM" },
{ name="CKD inland trade-route UAV scan (rail yards / caravan approaches / canal-road transfer points)", weight=4, urg="LOW" },
{ name="CKD rapid extraction standby (missionary escort recovery / detained witness lift / wounded boarding-team evacuation)", weight=3, urg="HIGH" },
{ name="CKD show-of-force pass over Southern Strait broker districts (deterrence / crowd dispersal / route discipline)", weight=2, urg="HIGH" },
}
local CKD_SOUTHERN_STRAIT_OVERSIGHT = {
{ label="Council of Kings Southern Strait liaison", weight=4 },
{ label="Straits Convention naval compliance monitor", weight=4 },
{ label="Hatch Ministry prize-court evidence observer", weight=3 },
{ label="Temple Bank missionary-route auditor", weight=3 },
{ label="General Port registry and cargo-ledger inspector", weight=3 },
{ label="Port compliance inspector (HCE)", weight=2 },
}
local CKD_SOUTHERN_STRAIT_COMPLIANCE = {
{ label="Southern Strait operation logged through CKD command channel; seizure, escort, and route-control notes filed same-day", weight=4 },
{ label="Former Maritime Markets traffic review completed; suspect ledgers, seals, and witness names preserved for review", weight=4 },
{ label="Naval screen maintained; lawful passage marked while suspicious craft were shadowed or routed to inspection", weight=4 },
{ label="Inland transfer chain stabilized; quay, rail, road, and caravan handoff records reconciled", weight=3 },
{ label="Mountain and upland approach patrol completed; suspected smuggler tracks mapped for follow-up interdiction", weight=2 },
{ label="Missionary escort corridor secured; shrine movement protected without interrupting lawful trade", weight=2 },
}
local function maybeCKDSouthernStraitOperation(cityName, divCode, primary, wxImp)
if divCode ~= "CKD" or not isHaifanBassaridiaCity(cityName) then
return nil
end
local allowNaval = not (wxImp and wxImp.sea == "Closed")
local pool
if primary == "naval" and allowNaval then
pool = CKD_SOUTHERN_STRAIT_NAVAL_OPS
elseif primary == "air" then
pool = CKD_SOUTHERN_STRAIT_AIR_OPS
else
pool = CKD_SOUTHERN_STRAIT_GROUND_OPS
end
if not pool or #pool == 0 then return nil end
return pickWeighted(pool)
end
--------------------------------------------------------------------------------
-- (J.1) NOCTIC-RABREV / NOCTIC VAMPIRISM OPERATION OVERLAY
--
-- These events are intentionally occasional. They do not replace the normal
-- War League event structure unless the city, division, or event text makes
-- a Noctic support action plausible.
--------------------------------------------------------------------------------
local NOCTIC_CITY_RISK = {
["Somniumpolis"] = 7,
["Odiferia"] = 7,
["Erythros"] = 6,
["Acheron"] = 5,
["Norsolyra"] = 5,
["Skøda"] = 5,
["Vaeringheim"] = 3,
["Jogi"] = 3,
["Aurelia"] = 3,
["Nexa"] = 2,
["Pyralis"] = 2,
["Keybir-Aviv"] = 2,
["Tel-Amin"] = 2,
["Skýrophos"] = 2,
["Thermosalem"] = 1,
["Hamarrfell"] = 1,
["Storesund"] = 1,
}
local NOCTIC_GROUND_OPS = {
{ name="Noctic-Rabrev custody support (raw-leaf cordon / Temple Bank inspection lane)", weight=4, urg="MEDIUM" },
{ name="Noctic Vampirism outbreak support (clinic queue security / patient transfer corridor)", weight=4, urg="MEDIUM" },
{ name="counterfeit nocticine sweep support (vendor rows / tincture stalls / crate-transfer corridor)", weight=3, urg="MEDIUM" },
{ name="Noctic registry protection (ledger escort / archive lockdown / witness shielding)", weight=2, urg="MEDIUM" },
{ name="raw Noctic-Rabrev seizure support (sealed evidence movement / crowd separation)", weight=3, urg="HIGH" },
{ name="Noctic panic stabilization (rumor-control cordon / shrine-district lane marking)", weight=2, urg="MEDIUM" },
{ name="Noctic medical convoy escort (licensed tinctures / blood-substitute supplies / cult-health team)", weight=2, urg="LOW" },
}
local NOCTIC_NAVAL_OPS = {
{ name="Noctic Fleet quay patrol (sealed leaf chests / flower bales / tincture crates)", weight=4, urg="MEDIUM" },
{ name="Noctic cargo interdiction support (small-craft inspection / crate-seal verification)", weight=4, urg="MEDIUM" },
{ name="wetland canal containment for raw Noctic-Rabrev cargo movement", weight=3, urg="HIGH" },
{ name="lock-gate security for Noctic-Rabrev custody audit", weight=3, urg="MEDIUM" },
{ name="marsh-side pier inspection for counterfeit nocticine consignments", weight=2, urg="MEDIUM" },
}
local NOCTIC_AIR_OPS = {
{ name="Noctic cargo ISR overwatch (lock-gates / wetland piers / suspected crate-handoff lanes)", weight=4, urg="MEDIUM" },
{ name="Noctic outbreak route scan (clinic corridors / relief queues / patient-transfer roads)", weight=3, urg="MEDIUM" },
{ name="RF/SIGINT scan for forged Noctic inspection calls and crate-handoff coordination", weight=3, urg="MEDIUM" },
{ name="storm-window UAV support for Noctic-Rabrev custody chain enforcement", weight=2, urg="HIGH" },
}
local NOCTIC_OVERSIGHT = {
{ label="Temple Bank Noctic custody clerk", weight=4 },
{ label="Temple Alabaster nocticine inspector", weight=3 },
{ label="Guild of Golden Shadows counterfeit-tincture auditor", weight=3 },
{ label="Cult-health Noctic Vampirism liaison", weight=3 },
{ label="Hatch Ministry cargo-seal observer", weight=2 },
}
local NOCTIC_COMPLIANCE = {
{ label="Noctic custody ledger updated; suspect vials, crate seals, and vendor-payment trails preserved", weight=4 },
{ label="Raw Noctic-Rabrev leaf suspension issued; Temple Bank registration and cult-health review required", weight=4 },
{ label="Clinic and relief queues stabilized; registered Noctic patients protected from rumor-driven harassment", weight=3 },
{ label="Counterfeit nocticine products isolated; affected vendors routed to Temple Alabaster inspection", weight=3 },
{ label="Noctic cargo released only after flower cargo, tinctures, and restricted raw-leaf material were separated", weight=2 },
{ label="Evidence chain sealed; duplicate ledgers forwarded to Temple Bank and port compliance offices", weight=2 },
}
local function textHasNocticSignal(txt)
local s = tostring(txt or ""):lower()
if s:find("noctic", 1, true) then return true end
if s:find("nrv", 1, true) then return true end
if s:find("raw leaf", 1, true) then return true end
if s:find("raw%-leaf") then return true end
if s:find("tincture", 1, true) then return true end
return false
end
local function nocticChanceFor(cityName, divCode, evDesc)
local risk = NOCTIC_CITY_RISK[cityName] or 0
local chance = risk * 0.018
if divCode == "ODF" then chance = chance + 0.04 end
if divCode == "HAT" then chance = chance + 0.03 end
if divCode == "ALP" then chance = chance + 0.02 end
if divCode == "NSJ" and (cityName == "Norsolyra" or cityName == "Skýrophos") then chance = chance + 0.02 end
if divCode == "CKD" and cityName == "Skøda" then chance = chance + 0.04 end
if textHasNocticSignal(evDesc) then chance = chance + 0.22 end
if chance > 0.30 then chance = 0.30 end
if chance < 0 then chance = 0 end
return chance
end
local function maybeNocticOperation(cityName, divCode, evDesc, primary, wxImp)
local chance = nocticChanceFor(cityName, divCode, evDesc)
if math.random() >= chance then return nil end
local allowNaval = not (wxImp and wxImp.sea == "Closed")
local pool
if primary == "naval" and allowNaval then
pool = NOCTIC_NAVAL_OPS
elseif primary == "air" then
pool = NOCTIC_AIR_OPS
else
pool = NOCTIC_GROUND_OPS
end
if not pool or #pool == 0 then return nil end
return pickWeighted(pool)
end
--------------------------------------------------------------------------------
-- (K) OVERSIGHT + COMPLIANCE
--------------------------------------------------------------------------------
local OVERSIGHT = {
{ label="Senate of Elders liaison", weight=3 },
{ label="Port compliance inspector (HCE)", weight=4 },
{ label="Temple Aprobelle social-harmony auditor", weight=3 },
{ label="Straits Convention monitor (civilian)", weight=3 },
{ label="No observer (weather delay)", weight=1 },
}
local COMPLIANCE = {
{ label="Deployment verified; corridor routing approved; ledger notes filed same-day", weight=6 },
{ label="Minor variance corrected; revised routing issued with escort buffer", weight=2 },
{ label="Weather delay; holding pattern under local command until visibility improves", weight=1 },
{ label="Mission deferred (no-go); export-control check triggered by false manifest", weight=1 },
{ label="Export request refused (constitutional prohibition); material retained under Council custody", weight=1 },
}
local function rowColor(obs, comp, uScore)
local o = (obs or ""):lower()
local c = (comp or ""):lower()
if o:match("weather delay") then
return ' style="background-color:#fff3cd;"'
end
if c:match("minor variance") then
return ' style="background-color:#fff3cd;"'
end
if c:match("export request refused") or c:match("mission deferred") then
return ' style="background-color:#f8d7da;"'
end
if uScore >= 3 then
return ' style="background-color:#f8d7da;"'
end
return ""
end
--------------------------------------------------------------------------------
-- (L) MAIN TABLE (flags + CKD-Caledonia enforcement + Ops Alert)
--------------------------------------------------------------------------------
function p.makeTable(frame)
local args = frame and frame.args or {}
local di = getPSSCDate(args)
local rows = tonumber(args.rows) or 36
local showLegend = tostring(args.showlegend or "yes"):lower() ~= "no"
local flagSize = tonumber(args.flagsize) or 20
local southernStraitOnly = isSouthernStraitOnly(args)
local eventMap, hasEvents = buildEventMapForToday(di)
local eventMap, hasEvents = buildEventMapForToday(di)
local seed = tonumber(args.seed) or (di.year * 1000 + di.dayOfYear)
math.randomseed(seed)
-- Southern Strait-only display settings.
-- rows = number of rows to DISPLAY.
-- scanrows = number of original War League rows to scan before filtering.
-- This preserves original Op-IDs because the original row index is still used.
local displayLimit = rows
local scanRows = rows
if southernStraitOnly then
scanRows = tonumber(args.scanrows or args.searchrows or args.sourcerows) or math.max(rows * 6, 120)
end
local displayed = 0
local divFilter = tostring(args.division or ""):lower()
local divPool = {}
-- Important:
-- Southern Strait-only mode must NOT change the division pool.
-- It should only hide non-Southern rows after the normal War League row
-- has been generated. This preserves Op-ID, row index, operation type,
-- and all deterministic row details.
if divFilter ~= "" then
for _, d in ipairs(DIVISIONS) do
if d.name:lower():find(divFilter, 1, true) or d.code:lower():find(divFilter, 1, true) then
table.insert(divPool, d)
end
end
if #divPool == 0 then divPool = DIVISIONS end
else
divPool = DIVISIONS
end
local out = {}
table.insert(out, "{| class='wikitable sortable'")
table.insert(out,
"! style='white-space:nowrap;' | Date" ..
" !! Division" ..
" !! City / Node" ..
" !! Daily event trigger" ..
" !! Urgency" ..
" !! Ops Alert" ..
" !! Ops Impact" ..
" !! Operation" ..
" !! Ground Forces" ..
" !! Naval Forces" ..
" !! Aerial Forces" ..
" !! Oversight" ..
" !! Compliance / Notes"
)
for i = 1, scanRows do
local div = pickWeighted(divPool)
local cityName = pick(div.cities)
-- Do NOT force Southern Strait cities here.
-- The row must be generated normally so Op-ID, division, operation type,
-- and force details match the ordinary War League page.
-- Guardrail: Caledonian city => CKD.
-- CKD may now operate in Caledonian cities or Haifan Bassaridian Southern Strait nodes.
div = forceCKDIfCaledonian(div, cityName)
if div.code == "CKD" and not isAllowedCKDCity(cityName) then
cityName = pick(div.cities)
end
local divFlag = flagIcon(DIVISION_FLAGS[div.name], flagSize)
local divCell = string.format("%s %s", divFlag, div.link)
local cityFlag = flagIcon(CITY_FLAGS[cityName], flagSize)
local cityCell = string.format("%s %s", cityFlag, cityLink(cityName, cityName))
local ev = eventMap[cityName]
local evDesc = ev and ev.desc or (hasEvents and "No reports" or "Events module unavailable")
local urg = ev and ev.urgency or "N/A"
local uScore = urgencyScore(urg)
-- WX integration (Ops Alert / Impacts)
local wxData = getWx(cityName, di)
local wxLevel, wxLabel, wxCond, wxHaz, wxImp = 0, "Green", "", "", nil
local opsAlertCell, opsImpactCell = "n/a", "n/a"
if wxData and wxData.alert then
wxLevel = tonumber(wxData.alert.level) or 0
wxLabel = wxData.alert.label or "Green"
wxCond = wxData.condition or ""
wxHaz = wxData.alert.hazard or ""
wxImp = wxData.impacts
opsAlertCell = wxBadge(wxLevel, wxLabel, wxCond)
opsImpactCell = wxImpactText(wxImp)
end
-- If no event is reported, allow weather to become the event driver on Orange/Red
if wxLevel >= 2 and (evDesc == "No reports" or evDesc == "Events module unavailable") then
evDesc = "Weather disruption: " .. ((wxHaz ~= "" and wxHaz) or (wxCond ~= "" and wxCond) or "elevated operational risk")
end
-- Raise urgency floor based on Wx alert (without suppressing event-driven urgency)
local floor = floorUrgencyFromWx(wxLevel)
if floor then
if urg == "N/A" then
urg = floor
else
urg = urgencyMax(urg, floor)
end
end
uScore = urgencyScore(urg)
-- Primary branch selection.
-- CKD Southern Strait deployments are naval-led, but still include ground,
-- inland, mountain, trade-route, and air-overwatch missions.
local isCKDSouthernStraitRow = (div.code == "CKD" and isHaifanBassaridiaCity(cityName))
local primary = pickWeighted({
{ name="ground", weight = isCKDSouthernStraitRow and 4 or 6 },
{ name="naval", weight = isCKDSouthernStraitRow and 8 or ((div.code == "HAT" or div.code == "HFA") and 5 or 2) },
{ name="air", weight = isCKDSouthernStraitRow and (3 + uScore) or (2 + uScore) },
}).name
local op = (primary == "naval") and pickWeighted(OP_NAVAL)
or (primary == "air") and pickWeighted(OP_AIR)
or pickWeighted(OP_GROUND)
-- CKD Southern Strait override:
-- If CKD appears in a Haifan Bassaridian city/node, replace the generic
-- operation with a Southern Strait / former Maritime Markets operation.
local ckdSouthernStraitOp = maybeCKDSouthernStraitOperation(cityName, div.code, primary, wxImp)
if ckdSouthernStraitOp then
op = { name = ckdSouthernStraitOp.name }
if ckdSouthernStraitOp.urg then
if urg == "N/A" then
urg = ckdSouthernStraitOp.urg
else
urg = urgencyMax(urg, ckdSouthernStraitOp.urg)
end
uScore = urgencyScore(urg)
end
end
-- WX: Bias operation choice when environment forces hold/hazard posture.
-- For CKD Southern Strait rows, weather delays are still framed through the
-- Southern Strait mission rather than reverting to generic local patrol text.
if wxLevel >= 2 and wxImp then
if isCKDSouthernStraitRow then
if wxImp.sea == "Closed" then
op = { name = "CKD Southern Strait storm-delay harbor screen (patrol craft held / quay cordon / inland transfer checks continue)" }
end
if wxLevel >= 3 and (wxImp.ground == "Hazard" or wxImp.air == "No-Go") then
op = { name = "CKD Southern Strait hazard response (evacuation lanes / debris clearance / missionary and cargo-route protection)" }
end
else
if wxImp.sea == "Closed" then
op = { name = "harbor security (lock-gates / inner basin patrol)" }
end
if wxLevel >= 3 and (wxImp.ground == "Hazard" or wxImp.air == "No-Go") then
op = { name = "hazard response (evacuation / debris clearance)" }
end
end
end
-- Optional Noctic overlay: occasionally turns the operation into a
-- Noctic-Rabrev / Noctic Vampirism support task when the city or event
-- makes it plausible. CKD Southern Strait rows stay Southern Strait-specific.
if not isCKDSouthernStraitRow then
local nocticOp = maybeNocticOperation(cityName, div.code, evDesc, primary, wxImp)
if nocticOp then
op = { name = nocticOp.name }
if nocticOp.urg then
if urg == "N/A" then
urg = nocticOp.urg
else
urg = urgencyMax(urg, nocticOp.urg)
end
uScore = urgencyScore(urg)
end
end
end
local ground, naval, air = "—","—","—"
-- WX: suppress air/naval allocations if outright no-go/closed
local allowNaval = not (wxImp and wxImp.sea == "Closed")
local allowAir = not (wxImp and wxImp.air == "No-Go")
if not (primary == "air" and uScore <= 1 and math.random() < 0.35) then
local gE = pickEchelonForUrgency(GROUND_ECHELONS, uScore)
ground = string.format("%s (%s-%02d)", gE, div.code, math.random(1, 24))
end
if allowNaval and (primary == "naval"
or isCKDSouthernStraitRow
or (div.code == "HAT" and math.random() < 0.55)
or (uScore >= 3 and math.random() < 0.35))
then
local nE = pickEchelonForUrgency(NAVAL_ECHELONS, uScore)
naval = string.format("%s (%s-N%02d)", nE, div.code, math.random(1, 12))
end
if allowAir and (primary == "air"
or (isCKDSouthernStraitRow and math.random() < 0.65)
or (uScore >= 2 and math.random() < 0.55))
then
local aE = pickEchelonForUrgency(AIR_ECHELONS, uScore)
air = string.format("%s (%s-A%02d)", aE, div.code, math.random(1, 18))
end
local obs = pickWeighted(OVERSIGHT).label
local comp = pickWeighted(COMPLIANCE).label
-- CKD Southern Strait rows get their own oversight/compliance language.
if isCKDSouthernStraitRow then
obs = pickWeighted(CKD_SOUTHERN_STRAIT_OVERSIGHT).label
comp = pickWeighted(CKD_SOUTHERN_STRAIT_COMPLIANCE).label
elseif textHasNocticSignal(op.name) then
-- If the operation is Noctic-related, allow Noctic-specific oversight and
-- compliance notes. This does not affect unrelated rows.
obs = pickWeighted(NOCTIC_OVERSIGHT).label
comp = pickWeighted(NOCTIC_COMPLIANCE).label
end
-- WX: force weather-delay compliance posture if Sea Closed / Air No-Go at Orange+.
-- CKD Southern Strait rows keep Southern Strait-specific delay language.
if wxLevel >= 2 and wxImp and (wxImp.sea == "Closed" or wxImp.air == "No-Go") then
if isCKDSouthernStraitRow then
obs = "Council of Kings Southern Strait weather liaison"
comp = "Southern Strait weather delay; naval screen held under local restrictions while inland transfer checks, quay cordons, and route-security notes continue"
else
obs = "No observer (weather delay)"
comp = "Weather delay; holding pattern under local command until visibility improves"
end
end
local includeRow = true
if southernStraitOnly then
includeRow = isHaifanBassaridiaCity(cityName)
end
local notes = bulletList({ "Op-ID: " .. opId(di, div.code, i) })
local style = rowColor(obs, comp, uScore)
if includeRow then
table.insert(out, "|-" .. style)
table.insert(out, string.format(
"| %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s || %s",
di.text, divCell, cityCell, evDesc, urg,
opsAlertCell, opsImpactCell,
op.name, ground, naval, air, obs, bulletList({ comp, notes })
))
displayed = displayed + 1
if displayed >= displayLimit then
break
end
end
end
if southernStraitOnly and displayed == 0 then
table.insert(out, "|-")
table.insert(out, "| colspan='13' style='text-align:center;' | No Southern Strait / Haifan Bassaridian War League rows appeared in the scanned source rows. Increase scanrows.")
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/compliant ")
table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = variance/adjustment/weather ")
table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = HIGH/CRITICAL or export-control/deferment")
table.insert(out, "<br/>Ops Alert badge reflects forecast-driven operational risk (Green/Yellow/Orange/Red); Ops Impact summarizes ground/sea/air constraints.")
table.insert(out, "</div>")
end
return table.concat(out, "\n")
end
--------------------------------------------------------------------------------
-- (M) OPTIONAL: show org ladders (for documentation / page footer)
--------------------------------------------------------------------------------
function p.showOrganization()
return table.concat({
'{| class="wikitable" style="width:100%;"',
'! Branch !! Smallest → Largest',
'|-',
'| Ground || Turmae → Contubernia → Ordinis → Manipuli → Centuriae → Cohortes',
'|-',
'| Naval || Birlik-i Gemi (Patrol Element) → Tabur-i Derya (Task Unit) → Ordu (Task Group)',
'|-',
'| Aerial || Zeygi (Element) → Ptisis (Flight) → Moira (Squadron)',
'|}',
}, "\n")
end
return p