Module:SouthernStraitCorridorOps: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Line 3: Line 3:
local p = {}
local p = {}


--------------------------------------------------------------------------------
---
 
-- Helpers
-- Helpers
--------------------------------------------------------------------------------


local function yn(value, default)
local function yn(value, default)
local v = tostring(value or ""):lower()
local v = tostring(value or ""):lower()
if v == "" then return default end
if v == "" then return default end
if v == "no" or v == "n" or v == "0" or v == "false" then return false end
if v == "no" or v == "n" or v == "0" or v == "false" then return false end
if v == "yes" or v == "y" or v == "1" or v == "true" then return true end
if v == "yes" or v == "y" or v == "1" or v == "true" then return true end
return default
return default
end
end


local function makeFrame(args)
local function makeFrame(args)
return { args = args or {} }
return { args = args or {} }
end
end


local function getCurrentPSSCDate()
local function getCurrentPSSCDate()
local startDate = os.time({ year = 1999, month = 8, day = 6, hour = 0 })
local startDate = os.time({ year = 1999, month = 8, day = 6, hour = 0 })
local secondsInDay = 86400
local secondsInDay = 86400
local daysPerYear = 183
local daysPerYear = 183
 
 
local currentDate = os.time()
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)


local currentDate = os.time()
local psscYear = math.floor(totalDaysElapsed / daysPerYear)
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
local dayOfYear = (totalDaysElapsed % daysPerYear) + 1
if dayOfYear < 1 then dayOfYear = 1 end
if dayOfYear > 183 then dayOfYear = 183 end


local psscYear = math.floor(totalDaysElapsed / daysPerYear)
return psscYear, dayOfYear
local dayOfYear = (totalDaysElapsed % daysPerYear) + 1


if dayOfYear < 1 then dayOfYear = 1 end
if dayOfYear > 183 then dayOfYear = 183 end


return psscYear, dayOfYear
end
end


local function normalizeArgsDate(args)
local function normalizeArgsDate(args)
local out = {}
local out = {}
 


for k, v in pairs(args or {}) do
for k, v in pairs(args or {}) do
out[k] = v
out[k] = v
end
end
 
local y = tonumber(out.year or out.psscyear)
local d = tonumber(out.day or out.dayofyear)
 
if not y or not d then
y, d = getCurrentPSSCDate()
end


local y = tonumber(out.year or out.psscyear)
out.year = tostring(y)
local d = tonumber(out.day or out.dayofyear)
out.day = tostring(d)
out.psscyear = tostring(y)
out.dayofyear = tostring(d)


if not y or not d then
return out
y, d = getCurrentPSSCDate()
end


out.year = tostring(y)
out.day = tostring(d)
out.psscyear = tostring(y)
out.dayofyear = tostring(d)


return out
end
end


local function safeRequire(moduleName)
local function safeRequire(moduleName)
local ok, mod = pcall(require, moduleName)
local ok, mod = pcall(require, moduleName)
if ok and mod then
if ok and mod then
return mod, nil
return mod, nil
end
end
 
 
return nil, "Could not load " .. tostring(moduleName)
 


return nil, "Could not load " .. tostring(moduleName)
end
end


local function safeCall(moduleName, funcName, args)
local function safeCall(moduleName, funcName, args)
local mod, err = safeRequire(moduleName)
local mod, err = safeRequire(moduleName)
if not mod then
return false, "''" .. err .. ".''"
end


if not mod then
return false, "''" .. err .. ".''"
end


if type(mod[funcName]) ~= "function" then
if type(mod[funcName]) ~= "function" then
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
end
end
 
local ok, result = pcall(function()
return mod[funcName](makeFrame(args))
end)
 
if not ok then
return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
end


local ok, result = pcall(function()
return true, tostring(result or "")
return mod[funcName](makeFrame(args))
end)


if not ok then
return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
end


return true, tostring(result or "")
end
end


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


if doy < 1 then doy = 1 end
if doy > 183 then doy = 183 end


if doy <= 61 then
return 1, doy
elseif doy <= 122 then
return 2, doy - 61
else
return 3, doy - 122
end
end
end


local function dateLabel(args)
local function dateLabel(args)
local y = tonumber(args.year or args.psscyear)
local y = tonumber(args.year or args.psscyear)
local d = tonumber(args.day or args.dayofyear)
local d = tonumber(args.day or args.dayofyear)
 
 
if y and d then
local m, dom = doyToMonthDom(d)
return string.format("%d/%d/%d PSSC", dom, m, y)
end
 
return "current PSSC date"


if y and d then
local m, dom = doyToMonthDom(d)
return string.format("%d/%d/%d PSSC", dom, m, y)
end


return "current PSSC date"
end
end


local function section(title, body, collapsible, collapsed)
local function section(title, body, collapsible, collapsed)
local out = {}
local out = {}
local safeTitle = tostring(title or "Operations")
local safeTitle = tostring(title or "Operations")
local content = tostring(body or "''No output.''")
local content = tostring(body or "''No output.''")
 
 
if not collapsible then
table.insert(out, "===" .. safeTitle .. "===")
table.insert(out, content)
return table.concat(out, "\n")
end


if not collapsible then
local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"
table.insert(out, "===" .. safeTitle .. "===")
 
table.insert(out, content)
table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
return table.concat(out, "\n")
table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
end
table.insert(out, '<div style="padding:0.5em 0.6em;">')
table.insert(out, content)
table.insert(out, '</div>')
table.insert(out, '</div>')


local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"
return table.concat(out, "\n")


table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
table.insert(out, '<div style="padding:0.5em 0.6em;">')
table.insert(out, content)
table.insert(out, '</div>')
table.insert(out, '</div>')


return table.concat(out, "\n")
end
end


local function hr()
local function hr()
return "\n----\n"
return "\n----\n"
end
end


local function hashString(s)
local function hashString(s)
s = tostring(s or "")
s = tostring(s or "")
local h = 0
local h = 0
 
 
for i = 1, #s do
h = (h * 31 + string.byte(s, i)) % 1000003
end
 
return h


for i = 1, #s do
h = (h * 31 + string.byte(s, i)) % 1000003
end


return h
end
end


local function pick(list, seed, salt)
local function pick(list, seed, salt)
if not list or #list == 0 then return nil end
if not list or #list == 0 then return nil end
 
 
local idx = (hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % #list) + 1
return list[idx]
 


local idx = (hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % #list) + 1
return list[idx]
end
end


local function pctFromSeed(seed, salt)
local function pctFromSeed(seed, salt)
return hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % 100
return hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % 100
end
 
local function clampChance(value)
value = tonumber(value) or 0
if value < 0 then return 0 end
if value > 100 then return 100 end
return value
end
 
local function isOnOrAfterPSSCDate(year, day, startYear, startDay)
year = tonumber(year) or 0
day = tonumber(day) or 0
startYear = tonumber(startYear) or 0
startDay = tonumber(startDay) or 0
 
 
if year > startYear then
return true
end
 
if year == startYear and day >= startDay then
return true
end
end


--------------------------------------------------------------------------------
return false
 
 
end
 
---
 
-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list
--------------------------------------------------------------------------------


local southernTradeCities = {
local southernTradeCities = {
{
{
code = "MIR",
code = "MIR",
city = "[[Mirovia]]",
city = "[[Mirovia]]",
region = "[[Eastern Mykonos]] Desert",
region = "[[Eastern Mykonos]] Desert",
category = "Agriculture",
category = "Agriculture",
company = "[[Mirovian Cistern Growers' Syndicate]]",
company = "[[Mirovian Cistern Growers' Syndicate]]",
resource = "cistern-grown melons, dryland cucumbers, preserved desert fruit, seed oil, packed caravan rations, and water-accounted produce consignments",
resource = "cistern-grown melons, dryland cucumbers, preserved desert fruit, seed oil, packed caravan rations, and water-accounted produce consignments",
lineage = "Desert provisioning economy influenced by [[Jogi]]'s food-market role and [[Mylecia]]'s caravan-facing trade networks; adapted to cistern agriculture, water claims, and dry-route supply rather than beer, grain, or ritual crops"
lineage = "Desert provisioning economy influenced by [[Jogi]]'s food-market role and [[Mylecia]]'s caravan-facing trade networks; adapted to cistern agriculture, water claims, and dry-route supply rather than beer, grain, or ritual crops"
},
},
{
{
code = "SAR",
code = "SAR",
city = "[[Saruq Thyrsa]]",
city = "[[Saruq Thyrsa]]",
region = "Southern Strait coast",
region = "Southern Strait coast",
category = "Agriculture",
category = "Agriculture",
company = "[[Saruq Thyrsa Reedcutters' Guild]]",
company = "[[Saruq Thyrsa Reedcutters' Guild]]",
resource = "coastal reed, cane fiber, packing straw, ferry mats, cargo-padding material, rope backing, warehouse thatch, and crate-lining bundles",
resource = "coastal reed, cane fiber, packing straw, ferry mats, cargo-padding material, rope backing, warehouse thatch, and crate-lining bundles",
lineage = "Harbor-materials economy influenced by [[Blore Heath]]'s shipping brokerage and [[Jogi]]'s quay-market supply system; focused on practical cargo handling, ferry shelters, and packing materials rather than piracy or cult goods"
lineage = "Harbor-materials economy influenced by [[Blore Heath]]'s shipping brokerage and [[Jogi]]'s quay-market supply system; focused on practical cargo handling, ferry shelters, and packing materials rather than piracy or cult goods"
},
},
{
{
code = "AYS",
code = "AYS",
city = "[[Ayllu Nysara]]",
city = "[[Ayllu Nysara]]",
region = "Eastern foothills of the [[Snowholme Range]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
category = "Agriculture",
company = "[[Nysaran Terrace Farmers' Ayllu]]",
company = "[[Nysaran Terrace Farmers' Ayllu]]",
resource = "terrace beans, cold-weather tubers, sheep wool, highland fodder, pack-animal feed, preserved root stores, and mule-train provisions",
resource = "terrace beans, cold-weather tubers, sheep wool, highland fodder, pack-animal feed, preserved root stores, and mule-train provisions",
lineage = "Foothill supply economy influenced only indirectly by the Maritime Markets; tied more closely to Snowholme terrace farming, upland food storage, wool traffic, and pass-road provisioning"
lineage = "Foothill supply economy influenced only indirectly by the Maritime Markets; tied more closely to Snowholme terrace farming, upland food storage, wool traffic, and pass-road provisioning"
},
},
{
{
code = "THQ",
code = "THQ",
city = "[[Thal Qencha]]",
city = "[[Thal Qencha]]",
region = "Eastern foothills of the [[Snowholme Range]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
category = "Agriculture",
company = "[[Thal Qencha Springkeepers' Cooperative]]",
company = "[[Thal Qencha Springkeepers' Cooperative]]",
resource = "spring-fed watercress, trout ponds, smoked fish, cold-storage greens, inspected mountain food shipments, packed ice-box produce, and road-market fish consignments",
resource = "spring-fed watercress, trout ponds, smoked fish, cold-storage greens, inspected mountain food shipments, packed ice-box produce, and road-market fish consignments",
lineage = "Foothill water-and-food economy tied to Snowholme springs and pass roads; comparable to older Maritime Markets food distribution only in its role as a regulated supplier to travelers, markets, and route offices"
lineage = "Foothill water-and-food economy tied to Snowholme springs and pass roads; comparable to older Maritime Markets food distribution only in its role as a regulated supplier to travelers, markets, and route offices"
},
},
{
{
code = "RUK",
code = "RUK",
city = "[[Rumi-Kefaya]]",
city = "[[Rumi-Kefaya]]",
region = "[[Eastern Mykonos]] Desert",
region = "[[Eastern Mykonos]] Desert",
category = "Mining / Manufacturing / Energy",
category = "Mining / Manufacturing / Energy",
company = "[[Rumi-Kefaya Stone and Lime Works]]",
company = "[[Rumi-Kefaya Stone and Lime Works]]",
resource = "basalt, limestone, paving stone, railway ballast, drainage tile, lime mortar, culvert blocks, harbor repair stone, quay coping, and storm-route masonry",
resource = "basalt, limestone, paving stone, railway ballast, drainage tile, lime mortar, culvert blocks, harbor repair stone, quay coping, and storm-route masonry",
lineage = "Infrastructure economy influenced by [[Mylecia]]'s coal-and-works tradition and Southern Strait coastal preservation activity; focused on roads, quays, culverts, retaining walls, and storm-damage repair rather than extractive wealth"
lineage = "Infrastructure economy influenced by [[Mylecia]]'s coal-and-works tradition and Southern Strait coastal preservation activity; focused on roads, quays, culverts, retaining walls, and storm-damage repair rather than extractive wealth"
},
},
{
{
code = "HAY",
code = "HAY",
city = "[[Hayr Chaskal]]",
city = "[[Hayr Chaskal]]",
region = "Southern Strait coast",
region = "Southern Strait coast",
category = "Mining / Manufacturing / Energy",
category = "Mining / Manufacturing / Energy",
company = "[[Hayr Chaskal Glass and Signal Works]]",
company = "[[Hayr Chaskal Glass and Signal Works]]",
resource = "beacon glass, telegraph insulators, lighthouse lenses, lamp glass, storm panes, signal mirrors, buoy lenses, and weatherproof signal housings",
resource = "beacon glass, telegraph insulators, lighthouse lenses, lamp glass, storm panes, signal mirrors, buoy lenses, and weatherproof signal housings",
lineage = "Signal-and-navigation economy influenced by [[Blore Heath]]'s shipping control and [[Mylecia]]'s coastal works; supports ferry warnings, lighthouse maintenance, telegraph lines, and Hatch Ministry watch posts without copying older pirate or brokerage firms"
lineage = "Signal-and-navigation economy influenced by [[Blore Heath]]'s shipping control and [[Mylecia]]'s coastal works; supports ferry warnings, lighthouse maintenance, telegraph lines, and Hatch Ministry watch posts without copying older pirate or brokerage firms"
},
},
{
{
code = "QSE",
code = "QSE",
city = "[[Qhapaq Selen]]",
city = "[[Qhapaq Selen]]",
region = "Eastern foothills of the [[Snowholme Range]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Services",
category = "Services",
company = "[[Qhapaq Selen Route Registry Office]]",
company = "[[Qhapaq Selen Route Registry Office]]",
resource = "night-road registry, caravan lodging, pass permits, route insurance, guide licensing, cargo-route papers, missing-traveler filings, and bonded transit records",
resource = "night-road registry, caravan lodging, pass permits, route insurance, guide licensing, cargo-route papers, missing-traveler filings, and bonded transit records",
lineage = "Foothill route-service economy influenced by [[Mylecia]]'s caravan-facing commerce and [[Jogi]]'s intelligence-and-paperwork traditions; adapted to Snowholme pass roads, guide houses, lodging records, and insurance claims"
lineage = "Foothill route-service economy influenced by [[Mylecia]]'s caravan-facing commerce and [[Jogi]]'s intelligence-and-paperwork traditions; adapted to Snowholme pass roads, guide houses, lodging records, and insurance claims"
}
}
}
}


--------------------------------------------------------------------------------
---
 
-- Row-level locality focus helper
-- Row-level locality focus helper
--------------------------------------------------------------------------------


local function rowLocalityData(args, channel, rowNumber)
local function rowLocalityData(args, channel, rowNumber)
if not southernTradeCities or #southernTradeCities == 0 then
if not southernTradeCities or #southernTradeCities == 0 then
return nil
return nil
end
end
 
 
local year = tonumber(args.year or args.psscyear) or 0
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
":SS-ROW-LOCALITY:" ..
tostring(channel or "") ..
":" .. tostring(year) ..
":" .. tostring(day)
 
local offset = hashString(seed) % #southernTradeCities
local idx = ((tonumber(rowNumber) or 1) - 1 + offset) % #southernTradeCities + 1


local year = tonumber(args.year or args.psscyear) or 0
return southernTradeCities[idx]
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") .. ":SS-ROW-LOCALITY:" .. tostring(channel or "") .. ":" .. tostring(year) .. ":" .. tostring(day)


local offset = hashString(seed) % #southernTradeCities
local idx = ((tonumber(rowNumber) or 1) - 1 + offset) % #southernTradeCities + 1


return southernTradeCities[idx]
end
end


local function rowLocalityText(loc)
local function rowLocalityText(loc)
if not loc then
if not loc then
return "Southern Strait corridor"
return "Southern Strait corridor"
end
end
 
 
return loc.city .. "<br><small>" .. loc.region .. "</small>"
 


return loc.city .. "<br><small>" .. loc.region .. "</small>"
end
end


--------------------------------------------------------------------------------
---
 
-- Imperial Federation support marker and locality boosts
-- Imperial Federation support marker and locality boosts
-- Effective from 112/53 PSSC only
-- Effective from 112/53 PSSC only
--------------------------------------------------------------------------------
----------------------------------


local IMPERIAL_SUPPORT_START_YEAR = 53
local IMPERIAL_SUPPORT_START_YEAR = 53
Line 271: Line 333:


local imperialFederationSupportLocalities = {
local imperialFederationSupportLocalities = {
MIR = true, -- Mirovia
MIR = true,
SAR = true, -- Saruq Thyrsa
SAR = true,
RUK = true -- Rumi-Kefaya
RUK = true
}
}


local function clampChance(value)
local function imperialSupportEnabled(args)
value = tonumber(value) or 0
return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true)
end
 
local function imperialSupportEffective(args)
if not imperialSupportEnabled(args) then
return false
end
 
 
local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.imperialstartyear or args.imperialStartYear) or IMPERIAL_SUPPORT_START_YEAR
local startDay = tonumber(args.imperialstartday or args.imperialStartDay) or IMPERIAL_SUPPORT_START_DAY
 
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
 
 
end
 
local function hasImperialFederationSupport(args, loc)
if not imperialSupportEffective(args) then
return false
end
 
 
return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true
 
 
end
 
local function imperialSupportBoostedChance(args, loc, baseChance, boost)
local chance = clampChance(baseChance)
 
 
if hasImperialFederationSupport(args, loc) then
chance = chance + (tonumber(boost) or 0)
end
 
return clampChance(chance)
 
 
end
 
local function imperialFederationSupportNotice(args)
if not imperialSupportEffective(args) then
return ""
end
 
 
return "'''Imperial Federation support present:''' [[2nd Brigade, 25th Division of the Imperial Marines]] deployed in allied support of Bassaridian Southern Strait corridor activity around [[Saruq Thyrsa]], [[Mirovia]], and [[Rumi-Kefaya]].<br><small>Mechanized, mixed-capability support of approximately 4,000 soldiers reinforces route security, inspection cordons, convoy spacing, engineering liaison, and lawful corridor stabilization. Locality rows for Saruq Thyrsa, Mirovia, and Rumi-Kefaya receive +10 percentage points to compliance observations and +3 percentage points to Temple Bank trade sign-offs from 112/53 PSSC onward.</small>"
 
 
end
 
---
 
-- Bassaridian Missions support marker and locality adjustments
-- Publicly acknowledged by the Council of Kings Division on 55/2/53 PSSC.
-- 55/2/53 PSSC = day-of-year 116.
----------------------------------
 
local BASSARIDIAN_MISSIONS_START_YEAR = 53
local BASSARIDIAN_MISSIONS_START_DAY = 116
 
local majorBassaridianMissionLocalities = {
SAR = true,
MIR = true,
AYS = true,
THQ = true,
QSE = true
}
 
local function bassaridianMissionsEnabled(args)
return yn(args.missionsupport or args.missionSupport or args.bassaridianmissions or args.bassaridianMissions, true)
end
 
local function bassaridianMissionsEffective(args)
if not bassaridianMissionsEnabled(args) then
return false
end
 
 
local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.missionstartyear or args.missionStartYear) or BASSARIDIAN_MISSIONS_START_YEAR
local startDay = tonumber(args.missionstartday or args.missionStartDay) or BASSARIDIAN_MISSIONS_START_DAY
 
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
 
 
end
 
local function hasBassaridianMission(args, loc)
if not bassaridianMissionsEffective(args) then
return false
end
 
 
return loc ~= nil
 


if value < 0 then return 0 end
end
if value > 100 then return 100 end


return value
local function hasMajorBassaridianMission(args, loc)
if not bassaridianMissionsEffective(args) then
return false
end
end


local function isOnOrAfterPSSCDate(year, day, startYear, startDay)
year = tonumber(year) or 0
day = tonumber(day) or 0
startYear = tonumber(startYear) or IMPERIAL_SUPPORT_START_YEAR
startDay = tonumber(startDay) or IMPERIAL_SUPPORT_START_DAY


if year > startYear then
return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true
return true
 
end
 
end
 
local function missionAdjustedComplianceChance(args, loc, baseChance)
local chance = clampChance(baseChance)


if year == startYear and day >= startDay then
return true
end


return false
if hasBassaridianMission(args, loc) then
chance = chance + (tonumber(args.missioncomplianceboost or args.missionComplianceBoost) or 1)
end
end


local function imperialSupportEnabled(args)
if hasMajorBassaridianMission(args, loc) then
return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true)
chance = chance + (tonumber(args.majormissioncomplianceboost or args.majorMissionComplianceBoost) or 2)
end
end


local function imperialSupportEffective(args)
return clampChance(chance)
if not imperialSupportEnabled(args) then
return false
end


local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.imperialstartyear or args.imperialStartYear) or IMPERIAL_SUPPORT_START_YEAR
local startDay = tonumber(args.imperialstartday or args.imperialStartDay) or IMPERIAL_SUPPORT_START_DAY


return isOnOrAfterPSSCDate(year, day, startYear, startDay)
end
end


local function hasImperialFederationSupport(args, loc)
local function missionAdjustedUnrestChance(args, loc, baseChance)
if not imperialSupportEffective(args) then
local chance = clampChance(baseChance)
return false
 
end


return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true
if hasBassaridianMission(args, loc) then
chance = chance - (tonumber(args.missionunrestreduction or args.missionUnrestReduction) or 1)
end
end


local function imperialSupportBoostedChance(args, loc, baseChance, boost)
return clampChance(chance)
local chance = clampChance(baseChance)
 


if hasImperialFederationSupport(args, loc) then
end
chance = chance + (tonumber(boost) or 0)
end


return clampChance(chance)
local function bassaridianMissionsNotice(args)
if not bassaridianMissionsEffective(args) then
return ""
end
end


local function imperialFederationSupportNotice(args)
if not imperialSupportEffective(args) then
return ""
end


return "'''Imperial Federation support present:''' [[2nd Brigade, 25th Division of the Imperial Marines]] deployed in allied support of Bassaridian Southern Strait corridor activity around [[Saruq Thyrsa]], [[Mirovia]], and [[Rumi-Kefaya]].<br><small>Mechanized, mixed-capability support of approximately 4,000 soldiers reinforces route security, inspection cordons, convoy spacing, engineering liaison, and lawful corridor stabilization. " ..
return "'''Bassaridian Missions acknowledged:''' [[Temple Bank of the Reformed Stripping Path|Temple Bank]]-run fortified mission compounds operate across the Southern Strait locality system, with major mission houses at [[Saruq Thyrsa]], [[Mirovia]], [[Ayllu Nysara]], [[Thal Qencha]], and [[Qhapaq Selen]].<br><small>From 55/2/53 PSSC onward, all locality rows receive -1 percentage point to unrest alerts and +1 percentage point to compliance observations. Major mission localities receive an additional +2 percentage points to compliance observations. Mission guards are drawn from local Baratar recruits and Council of Kings Division rotations, while deployed units continue to route through [[Mylecia]] before staging locally.</small>"
"Locality rows for Saruq Thyrsa, Mirovia, and Rumi-Kefaya receive +10 percentage points to compliance observations and +3 percentage points to Temple Bank trade sign-offs from 112/53 PSSC onward.</small>"
 
 
end
end


--------------------------------------------------------------------------------
---
 
-- Forced City/Node display
-- Forced City/Node display
--------------------------------------------------------------------------------


local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"
local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"


local function cleanHeaderLabel(line)
local function cleanHeaderLabel(line)
local s = tostring(line or "")
local s = tostring(line or "")
s = s:gsub("^%s*!+%s*", "")


s = s:gsub("^%s*!+%s*", "")


if s:find("|", 1, true) then
if s:find("|", 1, true) then
local parts = mw.text.split(s, "|", true)
local parts = mw.text.split(s, "|", true)
s = parts[#parts] or s
s = parts[#parts] or s
end
end
 
s = s:gsub("<.->", "")
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
s = s:gsub("&nbsp;", " ")
s = s:gsub("%s*/%s*", "/")
s = s:gsub("^%s+", ""):gsub("%s+$", "")
 
return s:lower()


s = s:gsub("<.->", "")
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
s = s:gsub("&nbsp;", " ")
s = s:gsub("%s*/%s*", "/")
s = s:gsub("^%s+", ""):gsub("%s+$", "")


return s:lower()
end
end


local cityNodeHeaderLabels = {
local cityNodeHeaderLabels = {
["city"] = true,
["city"] = true,
["node"] = true,
["node"] = true,
["city/node"] = true,
["city/node"] = true,
["city / node"] = true,
["city / node"] = true,
["city or node"] = true,
["city or node"] = true,
["home port"] = true,
["home port"] = true,
["homeport"] = true,
["homeport"] = true,
["home city"] = true,
["home city"] = true,
["base city"] = true,
["base city"] = true,
["staging city"] = true
["staging city"] = true
}
}


local function forceMyleciaCityNodeColumn(body, args)
local function forceMyleciaCityNodeColumn(body, args)
body = tostring(body or "")
body = tostring(body or "")


if yn(args.forcemylecia or args.forceMylecia or args.myleciacity or args.myleciaCity, true) == false then
return body
end


if not body:find("{|", 1, true) then
if yn(args.forcemylecia or args.forceMylecia or args.myleciacity or args.myleciaCity, true) == false then
return body
return body
end
end


local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
if not body:find("{|", 1, true) then
local lines = mw.text.split(body, "\n", true)
return body
local out = {}
end


local inTable = false
local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
local cityCol = nil
local lines = mw.text.split(body, "\n", true)
local col = 0
local out = {}


for _, line in ipairs(lines) do
local inTable = false
if line:match("^%s*{|") then
local cityCol = nil
inTable = true
local col = 0
cityCol = nil
col = 0
table.insert(out, line)


elseif inTable and line:match("^%s*!") then
for _, line in ipairs(lines) do
local raw = line:gsub("^%s*!+%s*", "")
if line:match("^%s*{|") then
local headers = mw.text.split(raw, "!!", true)
inTable = true
cityCol = nil
col = 0
table.insert(out, line)


if #headers > 1 then
elseif inTable and line:match("^%s*!") then
for _, h in ipairs(headers) do
local raw = line:gsub("^%s*!+%s*", "")
col = col + 1
local headers = mw.text.split(raw, "!!", true)


if cityNodeHeaderLabels[cleanHeaderLabel("! " .. h)] then
if #headers > 1 then
cityCol = col
for _, h in ipairs(headers) do
end
end
else
col = col + 1
col = col + 1
 
if cityNodeHeaderLabels[cleanHeaderLabel("! " .. h)] then
if cityNodeHeaderLabels[cleanHeaderLabel(line)] then
cityCol = col
cityCol = col
end
end
end
end
else
col = col + 1
if cityNodeHeaderLabels[cleanHeaderLabel(line)] then
cityCol = col
end
end


table.insert(out, line)
table.insert(out, line)


elseif inTable and line:match("^%s*|%-") then
elseif inTable and line:match("^%s*|%-") then
col = 0
col = 0
table.insert(out, line)
table.insert(out, line)


elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
local raw = line:gsub("^%s*|%s*", "")
local raw = line:gsub("^%s*|%s*", "")
local cells = mw.text.split(raw, "||", true)
local cells = mw.text.split(raw, "||", true)


if #cells > 1 then
if #cells > 1 then
for i = 1, #cells do
for i = 1, #cells do
if i == cityCol then
if i == cityCol then
cells[i] = " " .. forcedText .. " "
cells[i] = " " .. forcedText .. " "
end
end
end
end
table.insert(out, "| " .. table.concat(cells, " || "))
else
col = col + 1


table.insert(out, "| " .. table.concat(cells, " || "))
if col == cityCol then
table.insert(out, "| " .. forcedText)
else
else
col = col + 1
table.insert(out, line)
 
if col == cityCol then
table.insert(out, "| " .. forcedText)
else
table.insert(out, line)
end
end
end
end


elseif inTable and line:match("^%s*|}") then
elseif inTable and line:match("^%s*|}") then
inTable = false
inTable = false
table.insert(out, line)
table.insert(out, line)


else
else
table.insert(out, line)
table.insert(out, line)
end
end
end
end
return table.concat(out, "\n")


return table.concat(out, "\n")
end
end


--------------------------------------------------------------------------------
---
 
-- Hatch Ministry Home city / Current station swap
-- Hatch Ministry Home city / Current station swap
--------------------------------------------------------------------------------


local function swapHatchHomeCityCurrentStation(body, args)
local function swapHatchHomeCityCurrentStation(body, args)
body = tostring(body or "")
body = tostring(body or "")


if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
return body
end


if not body:find("{|", 1, true) then
if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
return body
return body
end
end


local lines = mw.text.split(body, "\n", true)
if not body:find("{|", 1, true) then
local out = {}
return body
end


local inTable = false
local lines = mw.text.split(body, "\n", true)
local homeCol = nil
local out = {}
local stationCol = nil
local col = 0


for _, line in ipairs(lines) do
local inTable = false
if line:match("^%s*{|") then
local homeCol = nil
inTable = true
local stationCol = nil
homeCol = nil
local col = 0
stationCol = nil
col = 0
table.insert(out, line)


elseif inTable and line:match("^%s*!") then
for _, line in ipairs(lines) do
local raw = line:gsub("^%s*!+%s*", "")
if line:match("^%s*{|") then
local headers = mw.text.split(raw, "!!", true)
inTable = true
homeCol = nil
stationCol = nil
col = 0
table.insert(out, line)


if #headers > 1 then
elseif inTable and line:match("^%s*!") then
for _, h in ipairs(headers) do
local raw = line:gsub("^%s*!+%s*", "")
col = col + 1
local headers = mw.text.split(raw, "!!", true)
local label = cleanHeaderLabel("! " .. h)


if label == "home city" or label == "home" then
if #headers > 1 then
homeCol = col
for _, h in ipairs(headers) do
elseif label == "current station" or label == "station" then
stationCol = col
end
end
else
col = col + 1
col = col + 1
local label = cleanHeaderLabel(line)
local label = cleanHeaderLabel("! " .. h)


if label == "home city" or label == "home" then
if label == "home city" or label == "home" then
Line 527: Line 669:
end
end
end
end
else
col = col + 1
local label = cleanHeaderLabel(line)


table.insert(out, line)
if label == "home city" or label == "home" then
homeCol = col
elseif label == "current station" or label == "station" then
stationCol = col
end
end


elseif inTable and line:match("^%s*|%-") then
table.insert(out, line)
col = 0
table.insert(out, line)


elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
elseif inTable and line:match("^%s*|%-") then
local raw = line:gsub("^%s*|%s*", "")
col = 0
local cells = mw.text.split(raw, "||", true)
table.insert(out, line)


if #cells > 1 then
elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
if cells[homeCol] and cells[stationCol] then
local raw = line:gsub("^%s*|%s*", "")
cells[homeCol], cells[stationCol] = cells[stationCol], cells[homeCol]
local cells = mw.text.split(raw, "||", true)
end


table.insert(out, "| " .. table.concat(cells, " || "))
if #cells > 1 then
else
if cells[homeCol] and cells[stationCol] then
col = col + 1
cells[homeCol], cells[stationCol] = cells[stationCol], cells[homeCol]
table.insert(out, line)
end
end


elseif inTable and line:match("^%s*|}") then
table.insert(out, "| " .. table.concat(cells, " || "))
inTable = false
table.insert(out, line)
 
else
else
col = col + 1
table.insert(out, line)
table.insert(out, line)
end
end
elseif inTable and line:match("^%s*|}") then
inTable = false
table.insert(out, line)
else
table.insert(out, line)
end
end
end
return table.concat(out, "\n")


return table.concat(out, "\n")
end
end


--------------------------------------------------------------------------------
---
 
-- Cult sign-off list for Temple Bank trade agreements
-- Cult sign-off list for Temple Bank trade agreements
--------------------------------------------------------------------------------


local tradeSigningCults = {
local tradeSigningCults = {
{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
{ code = "AZS", label = "[[Thalassa (Reformed Stripping Path)#Azure Sentinel Sect|Azure Sentinel Sect]]" },
{ code = "AZS", label = "[[Thalassa (Reformed Stripping Path)#Azure Sentinel Sect|Azure Sentinel Sect]]" },
{ code = "HAR", label = "[[Micras (Reformed Stripping Path)#Harmony Sanctum|Harmony Sanctum]]" },
{ code = "HAR", label = "[[Micras (Reformed Stripping Path)#Harmony Sanctum|Harmony Sanctum]]" },
{ code = "VIT", label = "[[Plateau (Reformed Stripping Path)#Sanctum Vitalis|Sanctum Vitalis]]" },
{ code = "VIT", label = "[[Plateau (Reformed Stripping Path)#Sanctum Vitalis|Sanctum Vitalis]]" },
{ code = "AUR", label = "[[Eos (Reformed Stripping Path)#Order of Aurora Mystica|Order of Aurora Mystica]]" },
{ code = "AUR", label = "[[Eos (Reformed Stripping Path)#Order of Aurora Mystica|Order of Aurora Mystica]]" },
{ code = "DEL", label = "[[Sisera (Reformed Stripping Path)#Sanctum Delphica|Sanctum Delphica]]" },
{ code = "DEL", label = "[[Sisera (Reformed Stripping Path)#Sanctum Delphica|Sanctum Delphica]]" },
{ code = "CAT", label = "[[Cato (Reformed Stripping Path)#Rex Catonis|Rex Catonis]]" },
{ code = "CAT", label = "[[Cato (Reformed Stripping Path)#Rex Catonis|Rex Catonis]]" },
{ code = "CON", label = "[[Lukedu (Reformed Stripping Path)#Accord Concordia|Accord Concordia]]" },
{ code = "CON", label = "[[Lukedu (Reformed Stripping Path)#Accord Concordia|Accord Concordia]]" },
{ code = "STY", label = "[[Styx (Reformed Stripping Path)#Mystery of the Stygian Veil|Mystery of the Stygian Veil]]" }
{ code = "STY", label = "[[Styx (Reformed Stripping Path)#Mystery of the Stygian Veil|Mystery of the Stygian Veil]]" }
}
}


local tradeOutcomes = {
local tradeOutcomes = {
"Cleared for General Port ledger review.",
"Cleared for General Port ledger review.",
"Provisional admission recommended.",
"Provisional admission recommended.",
"Conditional trade window approved.",
"Conditional trade window approved.",
"Company charter accepted for corridor listing.",
"Company charter accepted for corridor listing.",
"Trade agreement filed through ordinary registry channels."
"Trade agreement filed through ordinary registry channels."
}
}


local function tradeAgreementCell(args, channel, rowNumber, loc)
local function tradeAgreementCell(args, channel, rowNumber, loc)
if tostring(channel or "") ~= "missionary" then
if tostring(channel or "") ~= "missionary" then
return "-"
return "-"
end
end


if not loc then
return "-"
end


local chance = imperialSupportBoostedChance(args, loc, tonumber(args.tradechance or args.tradeChance) or 3, 3)
if not loc then
return "-"
end


local year = tonumber(args.year or args.psscyear) or 0
local chance = imperialSupportBoostedChance(args, loc, tonumber(args.tradechance or args.tradeChance) or 3, 3)
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local roll = pctFromSeed(seed, "roll") + 1
local year = tonumber(args.year or args.psscyear) or 0
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


if roll > chance then
local roll = pctFromSeed(seed, "roll") + 1
if yn(args.showTradeNoEvent or args.showtradenoevent, false) then
return "No sign-off"
end


return "-"
if roll > chance then
if yn(args.showTradeNoEvent or args.showtradenoevent, false) then
return "No sign-off"
end
end


local cult = pick(tradeSigningCults, seed, "cult")
return "-"
local outcome = pick(tradeOutcomes, seed, "outcome")
end
 
local cult = pick(tradeSigningCults, seed, "cult")
local outcome = pick(tradeOutcomes, seed, "outcome")
 
if not cult then
return "-"
end
 
local seq = (pctFromSeed(seed, "seq") % 20) + 1
local tradeId


if not cult then
if year > 0 and day > 0 then
return "-"
tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
end
else
tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)
local tradeId


if year > 0 and day > 0 then
return "'''Trade sign-off'''<br>" ..
tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
"<small>" .. tradeId .. "</small><br>" ..
else
(cult.label or "Cult sign-off recorded") .. "<br>" ..
tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. "</small>"
end


local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)


return "'''Trade sign-off'''<br>" ..
"<small>" .. tradeId .. "</small><br>" ..
(cult.label or "Cult sign-off recorded") .. "<br>" ..
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. "</small>"
end
end


--------------------------------------------------------------------------------
---
 
-- Rebellion / corridor unrest row column
-- Rebellion / corridor unrest row column
--------------------------------------------------------------------------------


local rebellionProfiles = {
local rebellionProfiles = {
{ label = "Confederacy-adjacent courier screen", description = "False-route couriers testing inspection gaps." },
{ label = "Confederacy-adjacent courier screen", description = "False-route couriers testing inspection gaps." },
{ label = "Former Maritime Markets broker faction", description = "Old broker-house retainers resisting General Port paperwork." },
{ label = "Former Maritime Markets broker faction", description = "Old broker-house retainers resisting General Port paperwork." },
{ label = "Anti-registry dock syndicate", description = "Warehouse intermediaries opposing voucher and ledger discipline." },
{ label = "Anti-registry dock syndicate", description = "Warehouse intermediaries opposing voucher and ledger discipline." },
{ label = "False-proclamation circle", description = "Pamphlet readers circulating forged civic orders." },
{ label = "False-proclamation circle", description = "Pamphlet readers circulating forged civic orders." },
{ label = "Route-barricade committee", description = "Agitators obstructing lawful inspection lanes." },
{ label = "Route-barricade committee", description = "Agitators obstructing lawful inspection lanes." },
{ label = "Dispossessed-sympathetic quay cell", description = "Waterfront cell copying Confederacy-style courier habits." }
{ label = "Dispossessed-sympathetic quay cell", description = "Waterfront cell copying Confederacy-style courier habits." }
}
}


local rebellionTriggers = {
local rebellionTriggers = {
"Forged levy notice.",
"Forged levy notice.",
"Masked speakers redirected traffic.",
"Masked speakers redirected traffic.",
"Missing seals and altered registry slips.",
"Missing seals and altered registry slips.",
"Crowd pressure over bonded cargo.",
"Crowd pressure over bonded cargo.",
"Tampered signal schedule.",
"Tampered signal schedule.",
"Conflicting route orders.",
"Conflicting route orders.",
"Coordinated paper strike.",
"Coordinated paper strike.",
"Anti-Bassaridian chant movement.",
"Anti-Bassaridian chant movement.",
"Repeated courier handoffs.",
"Repeated courier handoffs.",
"Matched false manifests."
"Matched false manifests."
}
}


local rebellionActions = {
local rebellionActions = {
"CKD cordon and evidence hold.",
"CKD cordon and evidence hold.",
"Temple rumor-control desk opened.",
"Temple rumor-control desk opened.",
"Ledgers and copied seals sealed.",
"Ledgers and copied seals sealed.",
"Privateer stand-off posture.",
"Privateer stand-off posture.",
"Pass papers reissued.",
"Pass papers reissued.",
"Lawful cargo separated.",
"Lawful cargo separated.",
"Missionary mediation opened.",
"Missionary mediation opened.",
"Corrected signal notice issued.",
"Corrected signal notice issued.",
"Temporary General Port audit lane opened.",
"Temporary General Port audit lane opened.",
"Incident classified as contained unrest."
"Incident classified as contained unrest."
}
}


local rebellionRisks = {
local rebellionRisks = {
"Low",
"Low",
"Moderate",
"Moderate",
"High",
"High",
"Contained",
"Contained",
"Escalating"
"Escalating"
}
}


-- Hard exclusion: these locality codes must never produce a Rebellion Alert result.
-- This returns "-" before the chance roll, even when rebellionchance=100 or showRebellionNoEvent=yes.
local rebellionAlertExcludedLocalities = {
local rebellionAlertExcludedLocalities = {
SAR = true, -- Saruq Thyrsa
SAR = true,
THQ = true -- Thal Qencha
THQ = true
}
}


local function isRebellionAlertExcluded(loc)
local function isRebellionAlertExcluded(loc)
if not loc then
if not loc then
return false
return false
end
end
 
 
return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true
 
 
end
 
local function rebellionAlertCell(args, channel, rowNumber, loc)
if not loc then
return "-"
end
 


return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true
if isRebellionAlertExcluded(loc) then
return "-"
end
end


--------------------------------------------------------------------------------
local baseChance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
-- Compliance observed / minor positive row column
local chance = missionAdjustedUnrestChance(args, loc, baseChance)
--------------------------------------------------------------------------------


local complianceProfiles = {
local year = tonumber(args.year or args.psscyear) or 0
{ label = "Inspection cooperation", description = "Local carriers complied with inspection sequencing without requiring escalation." },
local day  = tonumber(args.day or args.dayofyear) or 0
{ label = "Ledger correction", description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty." },
local seed = tostring(args.seed or "") ..
{ label = "Crowd dispersal", description = "Local stewards dispersed a gathering before it became an obstruction." },
":SS-REBELLION-COL:" .. tostring(channel or "") ..
{ label = "Guide assistance", description = "Licensed guides helped separate lawful traffic from suspect courier movement." },
":" .. tostring(rowNumber or 0) ..
{ label = "Cargo separation", description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage." },
":" .. tostring(loc.code or "GEN") ..
{ label = "Rumor-control compliance", description = "Temple notices were accepted locally and hostile rumors lost momentum." },
":" .. tostring(year) ..
{ label = "Privateer signal compliance", description = "Vessels accepted stand-off or hold-and-release signals without resistance." },
":" .. tostring(day)
{ label = "Registry cooperation", description = "Local registrars preserved names, seals, and witness records for later review." }
}


local complianceOutcomes = {
local roll = pctFromSeed(seed, "roll") + 1
"Minor positive compliance noted.",
"Recorded as a stabilizing local action.",
"Accepted as offsetting evidence against wider noncompliance.",
"Filed as a documented good-faith response.",
"Preserved for future compliance comparison.",
"Marked as below trade-agreement threshold but operationally helpful."
}


local function complianceObservedCell(args, channel, rowNumber, loc)
if roll > chance then
if not loc then
if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
return "-"
return "No unrest"
end
end


local chance = imperialSupportBoostedChance(args, loc, tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8, 10)
return "-"
end
 
local profile = pick(rebellionProfiles, seed, "profile")
local trigger = pick(rebellionTriggers, seed, "trigger")
local action = pick(rebellionActions, seed, "action")
local risk = pick(rebellionRisks, seed, "risk")
 
if not profile then
return "-"
end
 
local seq = (pctFromSeed(seed, "seq") % 20) + 1
local eventId


local year = tonumber(args.year or args.psscyear) or 0
if year > 0 and day > 0 then
local day  = tonumber(args.day or args.dayofyear) or 0
eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
local seed = tostring(args.seed or "") ..
else
":SS-COMPLIANCE-COL:" .. tostring(channel or "") ..
eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
":" .. tostring(rowNumber or 0) ..
end
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local roll = pctFromSeed(seed, "roll") + 1
return "'''Unrest alert'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Trigger:''' " .. trigger .. "<br>" ..
"'''Risk:''' " .. risk .. "<br>" ..
"'''Response:''' " .. action .. "</small>"


if roll > chance then
if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then
return "No observation"
end


return "-"
end
end


local profile = pick(complianceProfiles, seed, "profile")
---
local outcome = pick(complianceOutcomes, seed, "outcome")


if not profile then
-- Compliance observed / minor positive row column
return "-"
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
local complianceProfiles = {
local eventId
{ label = "Inspection cooperation", description = "Local carriers complied with inspection sequencing without requiring escalation." },
{ label = "Ledger correction", description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty." },
{ label = "Crowd dispersal", description = "Local stewards dispersed a gathering before it became an obstruction." },
{ label = "Guide assistance", description = "Licensed guides helped separate lawful traffic from suspect courier movement." },
{ label = "Cargo separation", description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage." },
{ label = "Rumor-control compliance", description = "Temple notices were accepted locally and hostile rumors lost momentum." },
{ label = "Privateer signal compliance", description = "Vessels accepted stand-off or hold-and-release signals without resistance." },
{ label = "Registry cooperation", description = "Local registrars preserved names, seals, and witness records for later review." }
}


if year > 0 and day > 0 then
local complianceOutcomes = {
eventId = string.format("SS-CO-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
"Minor positive compliance noted.",
else
"Recorded as a stabilizing local action.",
eventId = string.format("SS-CO-%s-%02d", loc.code or "GEN", seq)
"Accepted as offsetting evidence against wider noncompliance.",
end
"Filed as a documented good-faith response.",
"Preserved for future compliance comparison.",
"Marked as below trade-agreement threshold but operationally helpful."
}


return "'''Compliance observed'''<br>" ..
local function complianceObservedCell(args, channel, rowNumber, loc)
"<small>" .. eventId .. "</small><br>" ..
if not loc then
"'''Profile:''' " .. profile.label .. "<br>" ..
return "-"
"<small>" .. profile.description .. "<br>" ..
"'''Outcome:''' " .. outcome .. "</small>"
end
end


local function rebellionAlertCell(args, channel, rowNumber, loc)
if not loc then
return "-"
end


if isRebellionAlertExcluded(loc) then
local baseChance = tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8
return "-"
local chance = missionAdjustedComplianceChance(args, loc, baseChance)
end
chance = imperialSupportBoostedChance(args, loc, chance, 10)


local chance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
local year = tonumber(args.year or args.psscyear) or 0
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
":SS-COMPLIANCE-COL:" .. tostring(channel or "") ..
":" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


if chance < 0 then chance = 0 end
local roll = pctFromSeed(seed, "roll") + 1
if chance > 100 then chance = 100 end


local year = tonumber(args.year or args.psscyear) or 0
if roll > chance then
local day  = tonumber(args.day or args.dayofyear) or 0
if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then
local seed = tostring(args.seed or "") ..
return "No observation"
":SS-REBELLION-COL:" .. tostring(channel or "") ..
end
":" .. tostring(rowNumber or 0) ..
":" .. tostring(loc.code or "GEN") ..
":" .. tostring(year) ..
":" .. tostring(day)


local roll = pctFromSeed(seed, "roll") + 1
return "-"
end


if roll > chance then
local profile = pick(complianceProfiles, seed, "profile")
if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
local outcome = pick(complianceOutcomes, seed, "outcome")
return "No unrest"
end


return "-"
if not profile then
end
return "-"
end


local profile = pick(rebellionProfiles, seed, "profile")
local seq = (pctFromSeed(seed, "seq") % 20) + 1
local trigger = pick(rebellionTriggers, seed, "trigger")
local eventId
local action = pick(rebellionActions, seed, "action")
local risk = pick(rebellionRisks, seed, "risk")


if not profile then
if year > 0 and day > 0 then
return "-"
eventId = string.format("SS-CO-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
end
else
eventId = string.format("SS-CO-%s-%02d", loc.code or "GEN", seq)
end


local seq = (pctFromSeed(seed, "seq") % 20) + 1
return "'''Compliance observed'''<br>" ..
local eventId
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Outcome:''' " .. outcome .. "</small>"


if year > 0 and day > 0 then
eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
else
eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
end


return "'''Unrest alert'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Trigger:''' " .. trigger .. "<br>" ..
"'''Risk:''' " .. risk .. "<br>" ..
"'''Response:''' " .. action .. "</small>"
end
end


--------------------------------------------------------------------------------
---
 
-- Row-column insertion helper
-- Row-column insertion helper
--------------------------------------------------------------------------------


local function addCorridorColumns(body, args, channel)
local function addCorridorColumns(body, args, channel)
body = tostring(body or "")
body = tostring(body or "")
 


if not body:find("{|", 1, true) then
if not body:find("{|", 1, true) then
return body
return body
end
end


local showLocalityColumn = yn(args.rowlocalities or args.localities or args.showlocalities, true)
local showLocalityColumn = yn(args.rowlocalities or args.localities or args.showlocalities, true)
local showTradeColumn = yn(args.tradecolumn or args.tradecolumns or args.showtradecolumn, true)
local showTradeColumn = yn(args.tradecolumn or args.tradecolumns or args.showtradecolumn, true)
local showComplianceColumn = yn(args.compliancecolumn or args.compliancecolumns or args.observedcolumn or args.showcompliancecolumn, true)
local showComplianceColumn = yn(args.compliancecolumn or args.compliancecolumns or args.observedcolumn or args.showcompliancecolumn, true)
local showRebellionColumn = yn(args.rebellioncolumn or args.rebellioncolumns or args.unrestcolumn or args.showrebellioncolumn, true)
local showRebellionColumn = yn(args.rebellioncolumn or args.rebellioncolumns or args.unrestcolumn or args.showrebellioncolumn, true)


if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then
if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then
return body
return body
end
end


local lines = mw.text.split(body, "\n", true)
local lines = mw.text.split(body, "\n", true)
local out = {}
local out = {}


local inTable = false
local inTable = false
local headerInserted = false
local headerInserted = false
local rowNumber = 0
local rowNumber = 0


for _, line in ipairs(lines) do
for _, line in ipairs(lines) do
if line:match("^%s*{|") then
if line:match("^%s*{|") then
inTable = true
inTable = true
headerInserted = false
headerInserted = false
table.insert(out, line)
table.insert(out, line)


elseif inTable and not headerInserted and line:match("^%s*!") then
elseif inTable and not headerInserted and line:match("^%s*!") then
if showLocalityColumn then
if showLocalityColumn then
table.insert(out, "! Locality focus")
table.insert(out, "! Locality focus")
end
end


if showTradeColumn then
if showTradeColumn then
table.insert(out, "! Trade agreement")
table.insert(out, "! Trade agreement")
end
end


if showComplianceColumn then
if showComplianceColumn then
table.insert(out, "! Compliance observed")
table.insert(out, "! Compliance observed")
end
end


if showRebellionColumn then
if showRebellionColumn then
table.insert(out, "! Rebellion alert")
table.insert(out, "! Rebellion alert")
end
end


headerInserted = true
headerInserted = true
table.insert(out, line)
table.insert(out, line)


elseif inTable and line:match("^%s*|%-") then
elseif inTable and line:match("^%s*|%-") then
rowNumber = rowNumber + 1
rowNumber = rowNumber + 1
local loc = rowLocalityData(args, channel, rowNumber)
local loc = rowLocalityData(args, channel, rowNumber)


table.insert(out, line)
table.insert(out, line)


if showLocalityColumn then
if showLocalityColumn then
table.insert(out, "| " .. rowLocalityText(loc))
table.insert(out, "| " .. rowLocalityText(loc))
end
end


if showTradeColumn then
if showTradeColumn then
table.insert(out, "| " .. tradeAgreementCell(args, channel, rowNumber, loc))
table.insert(out, "| " .. tradeAgreementCell(args, channel, rowNumber, loc))
end
end


if showComplianceColumn then
if showComplianceColumn then
table.insert(out, "| " .. complianceObservedCell(args, channel, rowNumber, loc))
table.insert(out, "| " .. complianceObservedCell(args, channel, rowNumber, loc))
end
end


if showRebellionColumn then
if showRebellionColumn then
table.insert(out, "| " .. rebellionAlertCell(args, channel, rowNumber, loc))
table.insert(out, "| " .. rebellionAlertCell(args, channel, rowNumber, loc))
end
end


elseif inTable and line:match("^%s*|}") then
elseif inTable and line:match("^%s*|}") then
inTable = false
inTable = false
table.insert(out, line)
table.insert(out, line)


else
else
table.insert(out, line)
table.insert(out, line)
end
end
end
end
return table.concat(out, "\n")


return table.concat(out, "\n")
end
end


--------------------------------------------------------------------------------
---
 
-- Main Southern Strait-only view
-- Main Southern Strait-only view
--------------------------------------------------------------------------------


function p.makeTable(frame)
function p.makeTable(frame)
local args = normalizeArgsDate((frame and frame.args) or {})
local args = normalizeArgsDate((frame and frame.args) or {})
 


local showIntro  = yn(args.intro, false)
local showIntro  = yn(args.intro, false)
local showTitle  = yn(args.showtitle or args.titleline, false)
local showTitle  = yn(args.showtitle or args.titleline, false)
local showLegend = yn(args.legend or args.showlegend, false)
local showLegend = yn(args.legend or args.showlegend, false)
local collapseSections = yn(args.collapsible, true)
local startCollapsed = yn(args.collapsed, true)


local collapseSections = yn(args.collapsible, true)
local includeWar    = yn(args.war or args.warleague, true)
local startCollapsed = yn(args.collapsed, true)
local includeHatch  = yn(args.hatch or args.hatchministry, true)
local includeMission = yn(args.missionary or args.templebank, true)


local includeWar    = yn(args.war or args.warleague, true)
local year = args.year or args.psscyear
local includeHatch  = yn(args.hatch or args.hatchministry, true)
local day  = args.day or args.dayofyear
local includeMission = yn(args.missionary or args.templebank, true)
local title = args.title or "Southern Strait Corridor Daily Operations"
local label = dateLabel(args)


local year = args.year or args.psscyear
local out = {}
local day  = args.day or args.dayofyear


local title = args.title or "Southern Strait Corridor Daily Operations"
if showTitle then
local label = dateLabel(args)
table.insert(out, "==" .. title .. "==")
end


local out = {}
if showIntro then
table.insert(out, string.format(
"''Southern Strait-only operational display for %s, limited to Mylecian Southern Strait corridor entries.''",
label
))
table.insert(out, "")
table.insert(out, "This display combines Council of Kings deployments, Hatch Ministry privateer activity, Temple Bank missionary operations, row-level Southern Strait locality focus columns, Temple Bank-only trade-agreement sign-offs, row-level compliance observations, and row-level rebellion-type unrest alerts while displaying the operational City/Node as Mylecia. Imperial Federation and Bassaridian Mission modifiers are applied where effective.")
end


if showTitle then
local ifSupportNotice = imperialFederationSupportNotice(args)
table.insert(out, "==" .. title .. "==")
if ifSupportNotice ~= "" then
end
table.insert(out, ifSupportNotice)
table.insert(out, "")
end


if showIntro then
local missionNotice = bassaridianMissionsNotice(args)
table.insert(out, string.format(
if missionNotice ~= "" then
"''Southern Strait-only operational display for %s, limited to Mylecian Southern Strait corridor entries.''",
table.insert(out, missionNotice)
label
table.insert(out, "")
))
end
table.insert(out, "")
table.insert(out, "This display combines Council of Kings deployments, Hatch Ministry privateer activity, Temple Bank missionary operations, row-level Southern Strait locality focus columns, Temple Bank-only trade-agreement sign-offs, row-level compliance observations, and row-level rebellion-type unrest alerts while displaying the operational City/Node as Mylecia.")
end


local ifSupportNotice = imperialFederationSupportNotice(args)
if includeWar then
local warArgs = {
year = year,
day = day,
rows = args.warrows or args.rows or "24",
scanrows = args.warscanrows or args.scanrows or "160",
showlegend = showLegend and "yes" or "no",
flagsize = args.flagsize or "20",
zone = "SouthernStrait",
division = "Council of Kings Division",
}


if ifSupportNotice ~= "" then
if args.seed and tostring(args.seed) ~= "" then
table.insert(out, ifSupportNotice)
warArgs.seed = args.seed
table.insert(out, "")
end
end


if includeWar then
local ok, body = safeCall("Module:WarLeagueDivisionOpsBVR", "makeTable", warArgs)
local warArgs = {
body = forceMyleciaCityNodeColumn(body, args)
year = year,
body = addCorridorColumns(body, args, "war")
day = day,
rows = args.warrows or args.rows or "24",
scanrows = args.warscanrows or args.scanrows or "160",
showlegend = showLegend and "yes" or "no",
flagsize = args.flagsize or "20",
zone = "SouthernStrait",
division = "Council of Kings Division",
}


if args.seed and tostring(args.seed) ~= "" then
table.insert(out, hr())
warArgs.seed = args.seed
table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
end
end


local ok, body = safeCall("Module:WarLeagueDivisionOpsBVR", "makeTable", warArgs)
if includeHatch then
body = forceMyleciaCityNodeColumn(body, args)
local hatchArgs = {
body = addCorridorColumns(body, args, "war")
year = year,
day = day,
legend = showLegend and "yes" or "no",
zone = "SouthernStrait",
}


table.insert(out, hr())
if args.captain and tostring(args.captain) ~= "" then
table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
hatchArgs.captain = args.captain
end
end


if includeHatch then
local ok, body = safeCall("Module:HatchMinistryDailyOps", "dailyReport", hatchArgs)
local hatchArgs = {
body = forceMyleciaCityNodeColumn(body, args)
year = year,
body = swapHatchHomeCityCurrentStation(body, args)
day = day,
body = addCorridorColumns(body, args, "hatch")
legend = showLegend and "yes" or "no",
zone = "SouthernStrait",
}


if args.captain and tostring(args.captain) ~= "" then
table.insert(out, hr())
hatchArgs.captain = args.captain
table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
end
end


local ok, body = safeCall("Module:HatchMinistryDailyOps", "dailyReport", hatchArgs)
if includeMission then
body = forceMyleciaCityNodeColumn(body, args)
local missionaryArgs = {
body = swapHatchHomeCityCurrentStation(body, args)
year = year,
body = addCorridorColumns(body, args, "hatch")
day = day,
region = "Haifan",
showlegend = showLegend and "yes" or "no",
includeThreat = args.includeThreat or args.includethreat or "no",
showEcon = args.showEcon or args.showecon or "no",
}


table.insert(out, hr())
if args.missionarylimit and tostring(args.missionarylimit) ~= "" then
table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
missionaryArgs.limit = args.missionarylimit
elseif args.limit and tostring(args.limit) ~= "" then
missionaryArgs.limit = args.limit
end
end


if includeMission then
local ok, body = safeCall("Module:MissionaryDailyOpsBVR", "makeTable", missionaryArgs)
local missionaryArgs = {
body = forceMyleciaCityNodeColumn(body, args)
year = year,
body = addCorridorColumns(body, args, "missionary")
day = day,
region = "Haifan",
showlegend = showLegend and "yes" or "no",
includeThreat = args.includeThreat or args.includethreat or "no",
showEcon = args.showEcon or args.showecon or "no",
}


if args.missionarylimit and tostring(args.missionarylimit) ~= "" then
table.insert(out, hr())
missionaryArgs.limit = args.missionarylimit
table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
elseif args.limit and tostring(args.limit) ~= "" then
end
missionaryArgs.limit = args.limit
end


local ok, body = safeCall("Module:MissionaryDailyOpsBVR", "makeTable", missionaryArgs)
if showLegend then
body = forceMyleciaCityNodeColumn(body, args)
local legendBody = table.concat({
body = addCorridorColumns(body, args, "missionary")
"* '''Council of Kings Southern Strait deployments''' show only Council of Kings Division War League rows for the Southern Strait corridor.",
"* '''Hatch Ministry Southern Strait privateer operations''' show Southern Strait privateer rows.",
"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows, but the displayed City/Node is forced to Mylecia in this wrapper.",
"* '''City/Node display''' is forced to [[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]] for War League, Hatch Ministry, and Temple Bank rows.",
"* '''Locality focus''' columns assign one extended Southern Strait, Eastern Mykonos Desert, or eastern Snowholme foothill settlement to each generated row when row-level localities are enabled.",
"* '''Trade agreement''' columns generate actual trade sign-offs only in the Temple Bank section; War League and Hatch Ministry rows display '''-''' in that column.",
"* '''Compliance observed''' columns show minor positive local behavior below the level of a formal trade agreement.",
"* '''Rebellion alert''' columns show rare row-level contained unrest events such as forged proclamations, broker-cell activity, route barricades, false orders, or Dispossessed-adjacent courier behavior.",
"* '''Saruq Thyrsa''' and '''Thal Qencha''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
"* '''Imperial Federation support''' applies from 112/53 PSSC by default. It adds +10 percentage points to compliance observations and +3 percentage points to Temple Bank trade sign-offs for Saruq Thyrsa, Mirovia, and Rumi-Kefaya.",
"* '''Bassaridian Missions support''' applies from 55/2/53 PSSC by default. It reduces unrest-alert chance by 1 percentage point in all locality rows and increases compliance-observation chance by 1 percentage point in all locality rows.",
"* '''Major Bassaridian Missions''' at Saruq Thyrsa, Mirovia, Ayllu Nysara, Thal Qencha, and Qhapaq Selen receive an additional +2 percentage points to compliance observations.",
"* Use '''tradechance=100''', '''compliancechance=100''', and '''rebellionchance=100''' for testing.",
"* Use '''missionsupport=no''' to disable Bassaridian Missions modifiers for testing.",
"* Use '''imperialsupport=no''' to disable Imperial Federation support modifiers for testing.",
"* Use '''forcemylecia=no''' only if the wrapper should temporarily show the original source-module City/Node values."
}, "\n")


table.insert(out, hr())
table.insert(out, hr())
table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
end
end


if showLegend then
return table.concat(out, "\n")
local legendBody = table.concat({
"* '''Council of Kings Southern Strait deployments''' show only Council of Kings Division War League rows for the Southern Strait corridor.",
"* '''Hatch Ministry Southern Strait privateer operations''' show Southern Strait privateer rows.",
"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows, but the displayed City/Node is forced to Mylecia in this wrapper.",
"* '''City/Node display''' is forced to [[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]] for War League, Hatch Ministry, and Temple Bank rows.",
"* '''Locality focus''' columns assign one extended Southern Strait, Eastern Mykonos Desert, or eastern Snowholme foothill settlement to each generated row when row-level localities are enabled.",
"* '''Trade agreement''' columns generate actual trade sign-offs only in the Temple Bank section; War League and Hatch Ministry rows display '''-''' in that column.",
"* '''Compliance observed''' columns show minor positive local behavior below the level of a formal trade agreement.",
"* '''Rebellion alert''' columns show rare row-level contained unrest events such as forged proclamations, broker-cell activity, route barricades, false orders, or Dispossessed-adjacent courier behavior.",
"* '''Saruq Thyrsa''' and '''Thal Qencha''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
"* Use '''tradechance=100''', '''compliancechance=100''', and '''rebellionchance=100''' for testing.",
"* Use '''forcemylecia=no''' only if the wrapper should temporarily show the original source-module City/Node values."
}, "\n")


table.insert(out, hr())
table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
end


return table.concat(out, "\n")
end
end


--------------------------------------------------------------------------------
---
 
-- Shortcut views
-- Shortcut views
--------------------------------------------------------------------------------


function p.southernStrait(frame)
function p.southernStrait(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
args.title = args.title or "Southern Strait Corridor Daily Operations"
 
 
return p.makeTable({ args = args })


args.title = args.title or "Southern Strait Corridor Daily Operations"


return p.makeTable({ args = args })
end
end


function p.warOnly(frame)
function p.warOnly(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
 
 
args.hatch = "no"
args.missionary = "no"
args.war = "yes"
args.title = args.title or "War League Southern Strait Daily Operations"
 
return p.makeTable({ args = args })


args.hatch = "no"
args.missionary = "no"
args.war = "yes"
args.title = args.title or "War League Southern Strait Daily Operations"


return p.makeTable({ args = args })
end
end


function p.hatchOnly(frame)
function p.hatchOnly(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
 
 
args.war = "no"
args.missionary = "no"
args.hatch = "yes"
args.title = args.title or "Hatch Ministry Southern Strait Daily Operations"
 
return p.makeTable({ args = args })


args.war = "no"
args.missionary = "no"
args.hatch = "yes"
args.title = args.title or "Hatch Ministry Southern Strait Daily Operations"


return p.makeTable({ args = args })
end
end


function p.missionaryOnly(frame)
function p.missionaryOnly(frame)
local args = (frame and frame.args) or {}
local args = (frame and frame.args) or {}
 
 
args.war = "no"
args.hatch = "no"
args.missionary = "yes"
args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations"
 
return p.makeTable({ args = args })


args.war = "no"
args.hatch = "no"
args.missionary = "yes"
args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations"


return p.makeTable({ args = args })
end
end


return p
return p

Revision as of 23:37, 18 June 2026

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

-- Module:SouthernStraitCorridorOps

local p = {}

---

-- Helpers

local function yn(value, default)
local v = tostring(value or ""):lower()
if v == "" then return default end
if v == "no" or v == "n" or v == "0" or v == "false" then return false end
if v == "yes" or v == "y" or v == "1" or v == "true" then return true end
return default
end

local function makeFrame(args)
return { args = args or {} }
end

local function getCurrentPSSCDate()
local startDate = os.time({ year = 1999, month = 8, day = 6, hour = 0 })
local secondsInDay = 86400
local daysPerYear = 183


local currentDate = os.time()
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)

local psscYear = math.floor(totalDaysElapsed / daysPerYear)
local dayOfYear = (totalDaysElapsed % daysPerYear) + 1
if dayOfYear < 1 then dayOfYear = 1 end
if dayOfYear > 183 then dayOfYear = 183 end

return psscYear, dayOfYear


end

local function normalizeArgsDate(args)
local out = {}


for k, v in pairs(args or {}) do
	out[k] = v
end

local y = tonumber(out.year or out.psscyear)
local d = tonumber(out.day or out.dayofyear)

if not y or not d then
	y, d = getCurrentPSSCDate()
end

out.year = tostring(y)
out.day = tostring(d)
out.psscyear = tostring(y)
out.dayofyear = tostring(d)

return out


end

local function safeRequire(moduleName)
local ok, mod = pcall(require, moduleName)
if ok and mod then
return mod, nil
end


return nil, "Could not load " .. tostring(moduleName)


end

local function safeCall(moduleName, funcName, args)
local mod, err = safeRequire(moduleName)
if not mod then
return false, "''" .. err .. ".''"
end


if type(mod[funcName]) ~= "function" then
	return false, "''" .. tostring(moduleName) .. " has no public function named " .. tostring(funcName) .. ".''"
end

local ok, result = pcall(function()
	return mod[funcName](makeFrame(args))
end)

if not ok then
	return false, "''Error while invoking " .. tostring(moduleName) .. "." .. tostring(funcName) .. ": " .. tostring(result) .. "''"
end

return true, tostring(result or "")


end

local function doyToMonthDom(doy)
doy = tonumber(doy) or 1
if doy < 1 then doy = 1 end
if doy > 183 then doy = 183 end


if doy <= 61 then
	return 1, doy
elseif doy <= 122 then
	return 2, doy - 61
else
	return 3, doy - 122
end


end

local function dateLabel(args)
local y = tonumber(args.year or args.psscyear)
local d = tonumber(args.day or args.dayofyear)


if y and d then
	local m, dom = doyToMonthDom(d)
	return string.format("%d/%d/%d PSSC", dom, m, y)
end

return "current PSSC date"


end

local function section(title, body, collapsible, collapsed)
local out = {}
local safeTitle = tostring(title or "Operations")
local content = tostring(body or "''No output.''")


if not collapsible then
	table.insert(out, "===" .. safeTitle .. "===")
	table.insert(out, content)
	return table.concat(out, "\n")
end

local collapseClass = collapsed and "mw-collapsible mw-collapsed" or "mw-collapsible"

table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">')
table.insert(out, '<div style="font-weight:bold; padding:0.35em 0.6em; background:#eaecf0;">' .. safeTitle .. '</div>')
table.insert(out, '<div style="padding:0.5em 0.6em;">')
table.insert(out, content)
table.insert(out, '</div>')
table.insert(out, '</div>')

return table.concat(out, "\n")


end

local function hr()
return "\n----\n"
end

local function hashString(s)
s = tostring(s or "")
local h = 0


for i = 1, #s do
	h = (h * 31 + string.byte(s, i)) % 1000003
end

return h


end

local function pick(list, seed, salt)
if not list or #list == 0 then return nil end


local idx = (hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % #list) + 1
return list[idx]


end

local function pctFromSeed(seed, salt)
return hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % 100
end

local function clampChance(value)
value = tonumber(value) or 0
if value < 0 then return 0 end
if value > 100 then return 100 end
return value
end

local function isOnOrAfterPSSCDate(year, day, startYear, startDay)
year = tonumber(year) or 0
day = tonumber(day) or 0
startYear = tonumber(startYear) or 0
startDay = tonumber(startDay) or 0


if year > startYear then
	return true
end

if year == startYear and day >= startDay then
	return true
end

return false


end

---

-- Extended Southern Strait / Eastern Mykonos / Snowholme locality list

local southernTradeCities = {
{
code = "MIR",
city = "[[Mirovia]]",
region = "[[Eastern Mykonos]] Desert",
category = "Agriculture",
company = "[[Mirovian Cistern Growers' Syndicate]]",
resource = "cistern-grown melons, dryland cucumbers, preserved desert fruit, seed oil, packed caravan rations, and water-accounted produce consignments",
lineage = "Desert provisioning economy influenced by [[Jogi]]'s food-market role and [[Mylecia]]'s caravan-facing trade networks; adapted to cistern agriculture, water claims, and dry-route supply rather than beer, grain, or ritual crops"
},
{
code = "SAR",
city = "[[Saruq Thyrsa]]",
region = "Southern Strait coast",
category = "Agriculture",
company = "[[Saruq Thyrsa Reedcutters' Guild]]",
resource = "coastal reed, cane fiber, packing straw, ferry mats, cargo-padding material, rope backing, warehouse thatch, and crate-lining bundles",
lineage = "Harbor-materials economy influenced by [[Blore Heath]]'s shipping brokerage and [[Jogi]]'s quay-market supply system; focused on practical cargo handling, ferry shelters, and packing materials rather than piracy or cult goods"
},
{
code = "AYS",
city = "[[Ayllu Nysara]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
company = "[[Nysaran Terrace Farmers' Ayllu]]",
resource = "terrace beans, cold-weather tubers, sheep wool, highland fodder, pack-animal feed, preserved root stores, and mule-train provisions",
lineage = "Foothill supply economy influenced only indirectly by the Maritime Markets; tied more closely to Snowholme terrace farming, upland food storage, wool traffic, and pass-road provisioning"
},
{
code = "THQ",
city = "[[Thal Qencha]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
company = "[[Thal Qencha Springkeepers' Cooperative]]",
resource = "spring-fed watercress, trout ponds, smoked fish, cold-storage greens, inspected mountain food shipments, packed ice-box produce, and road-market fish consignments",
lineage = "Foothill water-and-food economy tied to Snowholme springs and pass roads; comparable to older Maritime Markets food distribution only in its role as a regulated supplier to travelers, markets, and route offices"
},
{
code = "RUK",
city = "[[Rumi-Kefaya]]",
region = "[[Eastern Mykonos]] Desert",
category = "Mining / Manufacturing / Energy",
company = "[[Rumi-Kefaya Stone and Lime Works]]",
resource = "basalt, limestone, paving stone, railway ballast, drainage tile, lime mortar, culvert blocks, harbor repair stone, quay coping, and storm-route masonry",
lineage = "Infrastructure economy influenced by [[Mylecia]]'s coal-and-works tradition and Southern Strait coastal preservation activity; focused on roads, quays, culverts, retaining walls, and storm-damage repair rather than extractive wealth"
},
{
code = "HAY",
city = "[[Hayr Chaskal]]",
region = "Southern Strait coast",
category = "Mining / Manufacturing / Energy",
company = "[[Hayr Chaskal Glass and Signal Works]]",
resource = "beacon glass, telegraph insulators, lighthouse lenses, lamp glass, storm panes, signal mirrors, buoy lenses, and weatherproof signal housings",
lineage = "Signal-and-navigation economy influenced by [[Blore Heath]]'s shipping control and [[Mylecia]]'s coastal works; supports ferry warnings, lighthouse maintenance, telegraph lines, and Hatch Ministry watch posts without copying older pirate or brokerage firms"
},
{
code = "QSE",
city = "[[Qhapaq Selen]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Services",
company = "[[Qhapaq Selen Route Registry Office]]",
resource = "night-road registry, caravan lodging, pass permits, route insurance, guide licensing, cargo-route papers, missing-traveler filings, and bonded transit records",
lineage = "Foothill route-service economy influenced by [[Mylecia]]'s caravan-facing commerce and [[Jogi]]'s intelligence-and-paperwork traditions; adapted to Snowholme pass roads, guide houses, lodging records, and insurance claims"
}
}

---

-- Row-level locality focus helper

local function rowLocalityData(args, channel, rowNumber)
if not southernTradeCities or #southernTradeCities == 0 then
return nil
end


local year = tonumber(args.year or args.psscyear) or 0
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
	":SS-ROW-LOCALITY:" ..
	tostring(channel or "") ..
	":" .. tostring(year) ..
	":" .. tostring(day)

local offset = hashString(seed) % #southernTradeCities
local idx = ((tonumber(rowNumber) or 1) - 1 + offset) % #southernTradeCities + 1

return southernTradeCities[idx]


end

local function rowLocalityText(loc)
if not loc then
return "Southern Strait corridor"
end


return loc.city .. "<br><small>" .. loc.region .. "</small>"


end

---

-- Imperial Federation support marker and locality boosts
-- Effective from 112/53 PSSC only
----------------------------------

local IMPERIAL_SUPPORT_START_YEAR = 53
local IMPERIAL_SUPPORT_START_DAY = 112

local imperialFederationSupportLocalities = {
MIR = true,
SAR = true,
RUK = true
}

local function imperialSupportEnabled(args)
return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true)
end

local function imperialSupportEffective(args)
if not imperialSupportEnabled(args) then
return false
end


local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.imperialstartyear or args.imperialStartYear) or IMPERIAL_SUPPORT_START_YEAR
local startDay = tonumber(args.imperialstartday or args.imperialStartDay) or IMPERIAL_SUPPORT_START_DAY

return isOnOrAfterPSSCDate(year, day, startYear, startDay)


end

local function hasImperialFederationSupport(args, loc)
if not imperialSupportEffective(args) then
return false
end


return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true


end

local function imperialSupportBoostedChance(args, loc, baseChance, boost)
local chance = clampChance(baseChance)


if hasImperialFederationSupport(args, loc) then
	chance = chance + (tonumber(boost) or 0)
end

return clampChance(chance)


end

local function imperialFederationSupportNotice(args)
if not imperialSupportEffective(args) then
return ""
end


return "'''Imperial Federation support present:''' [[2nd Brigade, 25th Division of the Imperial Marines]] deployed in allied support of Bassaridian Southern Strait corridor activity around [[Saruq Thyrsa]], [[Mirovia]], and [[Rumi-Kefaya]].<br><small>Mechanized, mixed-capability support of approximately 4,000 soldiers reinforces route security, inspection cordons, convoy spacing, engineering liaison, and lawful corridor stabilization. Locality rows for Saruq Thyrsa, Mirovia, and Rumi-Kefaya receive +10 percentage points to compliance observations and +3 percentage points to Temple Bank trade sign-offs from 112/53 PSSC onward.</small>"


end

---

-- Bassaridian Missions support marker and locality adjustments
-- Publicly acknowledged by the Council of Kings Division on 55/2/53 PSSC.
-- 55/2/53 PSSC = day-of-year 116.
----------------------------------

local BASSARIDIAN_MISSIONS_START_YEAR = 53
local BASSARIDIAN_MISSIONS_START_DAY = 116

local majorBassaridianMissionLocalities = {
SAR = true,
MIR = true,
AYS = true,
THQ = true,
QSE = true
}

local function bassaridianMissionsEnabled(args)
return yn(args.missionsupport or args.missionSupport or args.bassaridianmissions or args.bassaridianMissions, true)
end

local function bassaridianMissionsEffective(args)
if not bassaridianMissionsEnabled(args) then
return false
end


local year = tonumber(args.year or args.psscyear) or 0
local day = tonumber(args.day or args.dayofyear) or 0
local startYear = tonumber(args.missionstartyear or args.missionStartYear) or BASSARIDIAN_MISSIONS_START_YEAR
local startDay = tonumber(args.missionstartday or args.missionStartDay) or BASSARIDIAN_MISSIONS_START_DAY

return isOnOrAfterPSSCDate(year, day, startYear, startDay)


end

local function hasBassaridianMission(args, loc)
if not bassaridianMissionsEffective(args) then
return false
end


return loc ~= nil


end

local function hasMajorBassaridianMission(args, loc)
if not bassaridianMissionsEffective(args) then
return false
end


return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true


end

local function missionAdjustedComplianceChance(args, loc, baseChance)
local chance = clampChance(baseChance)


if hasBassaridianMission(args, loc) then
	chance = chance + (tonumber(args.missioncomplianceboost or args.missionComplianceBoost) or 1)
end

if hasMajorBassaridianMission(args, loc) then
	chance = chance + (tonumber(args.majormissioncomplianceboost or args.majorMissionComplianceBoost) or 2)
end

return clampChance(chance)


end

local function missionAdjustedUnrestChance(args, loc, baseChance)
local chance = clampChance(baseChance)


if hasBassaridianMission(args, loc) then
	chance = chance - (tonumber(args.missionunrestreduction or args.missionUnrestReduction) or 1)
end

return clampChance(chance)


end

local function bassaridianMissionsNotice(args)
if not bassaridianMissionsEffective(args) then
return ""
end


return "'''Bassaridian Missions acknowledged:''' [[Temple Bank of the Reformed Stripping Path|Temple Bank]]-run fortified mission compounds operate across the Southern Strait locality system, with major mission houses at [[Saruq Thyrsa]], [[Mirovia]], [[Ayllu Nysara]], [[Thal Qencha]], and [[Qhapaq Selen]].<br><small>From 55/2/53 PSSC onward, all locality rows receive -1 percentage point to unrest alerts and +1 percentage point to compliance observations. Major mission localities receive an additional +2 percentage points to compliance observations. Mission guards are drawn from local Baratar recruits and Council of Kings Division rotations, while deployed units continue to route through [[Mylecia]] before staging locally.</small>"


end

---

-- Forced City/Node display

local forcedCityNodeText = "[[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]]"

local function cleanHeaderLabel(line)
local s = tostring(line or "")
s = s:gsub("^%s*!+%s*", "")


if s:find("|", 1, true) then
	local parts = mw.text.split(s, "|", true)
	s = parts[#parts] or s
end

s = s:gsub("<.->", "")
s = s:gsub("%[%[[^%]|]+|([^%]]+)%]%]", "%1")
s = s:gsub("%[%[([^%]]+)%]%]", "%1")
s = s:gsub("&nbsp;", " ")
s = s:gsub("%s*/%s*", "/")
s = s:gsub("^%s+", ""):gsub("%s+$", "")

return s:lower()


end

local cityNodeHeaderLabels = {
["city"] = true,
["node"] = true,
["city/node"] = true,
["city / node"] = true,
["city or node"] = true,
["home port"] = true,
["homeport"] = true,
["home city"] = true,
["base city"] = true,
["staging city"] = true
}

local function forceMyleciaCityNodeColumn(body, args)
body = tostring(body or "")


if yn(args.forcemylecia or args.forceMylecia or args.myleciacity or args.myleciaCity, true) == false then
	return body
end

if not body:find("{|", 1, true) then
	return body
end

local forcedText = args.forcecitytext or args.forceCityText or forcedCityNodeText
local lines = mw.text.split(body, "\n", true)
local out = {}

local inTable = false
local cityCol = nil
local col = 0

for _, line in ipairs(lines) do
	if line:match("^%s*{|") then
		inTable = true
		cityCol = nil
		col = 0
		table.insert(out, line)

	elseif inTable and line:match("^%s*!") then
		local raw = line:gsub("^%s*!+%s*", "")
		local headers = mw.text.split(raw, "!!", true)

		if #headers > 1 then
			for _, h in ipairs(headers) do
				col = col + 1
				if cityNodeHeaderLabels[cleanHeaderLabel("! " .. h)] then
					cityCol = col
				end
			end
		else
			col = col + 1
			if cityNodeHeaderLabels[cleanHeaderLabel(line)] then
				cityCol = col
			end
		end

		table.insert(out, line)

	elseif inTable and line:match("^%s*|%-") then
		col = 0
		table.insert(out, line)

	elseif inTable and cityCol and line:match("^%s*|[^}%-%+]") then
		local raw = line:gsub("^%s*|%s*", "")
		local cells = mw.text.split(raw, "||", true)

		if #cells > 1 then
			for i = 1, #cells do
				if i == cityCol then
					cells[i] = " " .. forcedText .. " "
				end
			end

			table.insert(out, "| " .. table.concat(cells, " || "))
		else
			col = col + 1

			if col == cityCol then
				table.insert(out, "| " .. forcedText)
			else
				table.insert(out, line)
			end
		end

	elseif inTable and line:match("^%s*|}") then
		inTable = false
		table.insert(out, line)

	else
		table.insert(out, line)
	end
end

return table.concat(out, "\n")


end

---

-- Hatch Ministry Home city / Current station swap

local function swapHatchHomeCityCurrentStation(body, args)
body = tostring(body or "")


if yn(args.swaphatchstation or args.swapHatchStation or args.swaphatchhomecity or args.swapHatchHomeCity, true) == false then
	return body
end

if not body:find("{|", 1, true) then
	return body
end

local lines = mw.text.split(body, "\n", true)
local out = {}

local inTable = false
local homeCol = nil
local stationCol = nil
local col = 0

for _, line in ipairs(lines) do
	if line:match("^%s*{|") then
		inTable = true
		homeCol = nil
		stationCol = nil
		col = 0
		table.insert(out, line)

	elseif inTable and line:match("^%s*!") then
		local raw = line:gsub("^%s*!+%s*", "")
		local headers = mw.text.split(raw, "!!", true)

		if #headers > 1 then
			for _, h in ipairs(headers) do
				col = col + 1
				local label = cleanHeaderLabel("! " .. h)

				if label == "home city" or label == "home" then
					homeCol = col
				elseif label == "current station" or label == "station" then
					stationCol = col
				end
			end
		else
			col = col + 1
			local label = cleanHeaderLabel(line)

			if label == "home city" or label == "home" then
				homeCol = col
			elseif label == "current station" or label == "station" then
				stationCol = col
			end
		end

		table.insert(out, line)

	elseif inTable and line:match("^%s*|%-") then
		col = 0
		table.insert(out, line)

	elseif inTable and homeCol and stationCol and line:match("^%s*|[^}%-%+]") then
		local raw = line:gsub("^%s*|%s*", "")
		local cells = mw.text.split(raw, "||", true)

		if #cells > 1 then
			if cells[homeCol] and cells[stationCol] then
				cells[homeCol], cells[stationCol] = cells[stationCol], cells[homeCol]
			end

			table.insert(out, "| " .. table.concat(cells, " || "))
		else
			col = col + 1
			table.insert(out, line)
		end

	elseif inTable and line:match("^%s*|}") then
		inTable = false
		table.insert(out, line)

	else
		table.insert(out, line)
	end
end

return table.concat(out, "\n")


end

---

-- Cult sign-off list for Temple Bank trade agreements

local tradeSigningCults = {
{ code = "UMB", label = "[[Noctis (Reformed Stripping Path)#Order of the Umbral Oracle|Order of the Umbral Oracle]]" },
{ code = "GGS", label = "[[Chrysos (Reformed Stripping Path)#Guild of Golden Shadows|Guild of Golden Shadows]]" },
{ code = "AZS", label = "[[Thalassa (Reformed Stripping Path)#Azure Sentinel Sect|Azure Sentinel Sect]]" },
{ code = "HAR", label = "[[Micras (Reformed Stripping Path)#Harmony Sanctum|Harmony Sanctum]]" },
{ code = "VIT", label = "[[Plateau (Reformed Stripping Path)#Sanctum Vitalis|Sanctum Vitalis]]" },
{ code = "AUR", label = "[[Eos (Reformed Stripping Path)#Order of Aurora Mystica|Order of Aurora Mystica]]" },
{ code = "DEL", label = "[[Sisera (Reformed Stripping Path)#Sanctum Delphica|Sanctum Delphica]]" },
{ code = "CAT", label = "[[Cato (Reformed Stripping Path)#Rex Catonis|Rex Catonis]]" },
{ code = "CON", label = "[[Lukedu (Reformed Stripping Path)#Accord Concordia|Accord Concordia]]" },
{ code = "STY", label = "[[Styx (Reformed Stripping Path)#Mystery of the Stygian Veil|Mystery of the Stygian Veil]]" }
}

local tradeOutcomes = {
"Cleared for General Port ledger review.",
"Provisional admission recommended.",
"Conditional trade window approved.",
"Company charter accepted for corridor listing.",
"Trade agreement filed through ordinary registry channels."
}

local function tradeAgreementCell(args, channel, rowNumber, loc)
if tostring(channel or "") ~= "missionary" then
return "-"
end


if not loc then
	return "-"
end

local chance = imperialSupportBoostedChance(args, loc, tonumber(args.tradechance or args.tradeChance) or 3, 3)

local year = tonumber(args.year or args.psscyear) or 0
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
	":SS-TEMPLE-TRADE-COL:" .. tostring(rowNumber or 0) ..
	":" .. tostring(loc.code or "GEN") ..
	":" .. tostring(year) ..
	":" .. tostring(day)

local roll = pctFromSeed(seed, "roll") + 1

if roll > chance then
	if yn(args.showTradeNoEvent or args.showtradenoevent, false) then
		return "No sign-off"
	end

	return "-"
end

local cult = pick(tradeSigningCults, seed, "cult")
local outcome = pick(tradeOutcomes, seed, "outcome")

if not cult then
	return "-"
end

local seq = (pctFromSeed(seed, "seq") % 20) + 1
local tradeId

if year > 0 and day > 0 then
	tradeId = string.format("GP-SS-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
else
	tradeId = string.format("GP-SS-%s-%02d", loc.code or "GEN", seq)
end

local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq)

return "'''Trade sign-off'''<br>" ..
	"<small>" .. tradeId .. "</small><br>" ..
	(cult.label or "Cult sign-off recorded") .. "<br>" ..
	"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. "</small>"


end

---

-- Rebellion / corridor unrest row column

local rebellionProfiles = {
{ label = "Confederacy-adjacent courier screen", description = "False-route couriers testing inspection gaps." },
{ label = "Former Maritime Markets broker faction", description = "Old broker-house retainers resisting General Port paperwork." },
{ label = "Anti-registry dock syndicate", description = "Warehouse intermediaries opposing voucher and ledger discipline." },
{ label = "False-proclamation circle", description = "Pamphlet readers circulating forged civic orders." },
{ label = "Route-barricade committee", description = "Agitators obstructing lawful inspection lanes." },
{ label = "Dispossessed-sympathetic quay cell", description = "Waterfront cell copying Confederacy-style courier habits." }
}

local rebellionTriggers = {
"Forged levy notice.",
"Masked speakers redirected traffic.",
"Missing seals and altered registry slips.",
"Crowd pressure over bonded cargo.",
"Tampered signal schedule.",
"Conflicting route orders.",
"Coordinated paper strike.",
"Anti-Bassaridian chant movement.",
"Repeated courier handoffs.",
"Matched false manifests."
}

local rebellionActions = {
"CKD cordon and evidence hold.",
"Temple rumor-control desk opened.",
"Ledgers and copied seals sealed.",
"Privateer stand-off posture.",
"Pass papers reissued.",
"Lawful cargo separated.",
"Missionary mediation opened.",
"Corrected signal notice issued.",
"Temporary General Port audit lane opened.",
"Incident classified as contained unrest."
}

local rebellionRisks = {
"Low",
"Moderate",
"High",
"Contained",
"Escalating"
}

local rebellionAlertExcludedLocalities = {
SAR = true,
THQ = true
}

local function isRebellionAlertExcluded(loc)
if not loc then
return false
end


return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true


end

local function rebellionAlertCell(args, channel, rowNumber, loc)
if not loc then
return "-"
end


if isRebellionAlertExcluded(loc) then
	return "-"
end

local baseChance = tonumber(args.rebellionchance or args.rebellionChance or args.unrestchance or args.unrestChance) or 6
local chance = missionAdjustedUnrestChance(args, loc, baseChance)

local year = tonumber(args.year or args.psscyear) or 0
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
	":SS-REBELLION-COL:" .. tostring(channel or "") ..
	":" .. tostring(rowNumber or 0) ..
	":" .. tostring(loc.code or "GEN") ..
	":" .. tostring(year) ..
	":" .. tostring(day)

local roll = pctFromSeed(seed, "roll") + 1

if roll > chance then
	if yn(args.showRebellionNoEvent or args.showrebellionnoevent or args.showUnrestNoEvent or args.showunrestnoevent, false) then
		return "No unrest"
	end

	return "-"
end

local profile = pick(rebellionProfiles, seed, "profile")
local trigger = pick(rebellionTriggers, seed, "trigger")
local action = pick(rebellionActions, seed, "action")
local risk = pick(rebellionRisks, seed, "risk")

if not profile then
	return "-"
end

local seq = (pctFromSeed(seed, "seq") % 20) + 1
local eventId

if year > 0 and day > 0 then
	eventId = string.format("SS-RB-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
else
	eventId = string.format("SS-RB-%s-%02d", loc.code or "GEN", seq)
end

return "'''Unrest alert'''<br>" ..
	"<small>" .. eventId .. "</small><br>" ..
	"'''Profile:''' " .. profile.label .. "<br>" ..
	"<small>" .. profile.description .. "<br>" ..
	"'''Trigger:''' " .. trigger .. "<br>" ..
	"'''Risk:''' " .. risk .. "<br>" ..
	"'''Response:''' " .. action .. "</small>"


end

---

-- Compliance observed / minor positive row column

local complianceProfiles = {
{ label = "Inspection cooperation", description = "Local carriers complied with inspection sequencing without requiring escalation." },
{ label = "Ledger correction", description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty." },
{ label = "Crowd dispersal", description = "Local stewards dispersed a gathering before it became an obstruction." },
{ label = "Guide assistance", description = "Licensed guides helped separate lawful traffic from suspect courier movement." },
{ label = "Cargo separation", description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage." },
{ label = "Rumor-control compliance", description = "Temple notices were accepted locally and hostile rumors lost momentum." },
{ label = "Privateer signal compliance", description = "Vessels accepted stand-off or hold-and-release signals without resistance." },
{ label = "Registry cooperation", description = "Local registrars preserved names, seals, and witness records for later review." }
}

local complianceOutcomes = {
"Minor positive compliance noted.",
"Recorded as a stabilizing local action.",
"Accepted as offsetting evidence against wider noncompliance.",
"Filed as a documented good-faith response.",
"Preserved for future compliance comparison.",
"Marked as below trade-agreement threshold but operationally helpful."
}

local function complianceObservedCell(args, channel, rowNumber, loc)
if not loc then
return "-"
end


local baseChance = tonumber(args.compliancechance or args.complianceChance or args.observedchance or args.observedChance) or 8
local chance = missionAdjustedComplianceChance(args, loc, baseChance)
chance = imperialSupportBoostedChance(args, loc, chance, 10)

local year = tonumber(args.year or args.psscyear) or 0
local day  = tonumber(args.day or args.dayofyear) or 0
local seed = tostring(args.seed or "") ..
	":SS-COMPLIANCE-COL:" .. tostring(channel or "") ..
	":" .. tostring(rowNumber or 0) ..
	":" .. tostring(loc.code or "GEN") ..
	":" .. tostring(year) ..
	":" .. tostring(day)

local roll = pctFromSeed(seed, "roll") + 1

if roll > chance then
	if yn(args.showComplianceNoEvent or args.showcompliancenoevent, false) then
		return "No observation"
	end

	return "-"
end

local profile = pick(complianceProfiles, seed, "profile")
local outcome = pick(complianceOutcomes, seed, "outcome")

if not profile then
	return "-"
end

local seq = (pctFromSeed(seed, "seq") % 20) + 1
local eventId

if year > 0 and day > 0 then
	eventId = string.format("SS-CO-%03d.%03d-%s-%02d", year, day, loc.code or "GEN", seq)
else
	eventId = string.format("SS-CO-%s-%02d", loc.code or "GEN", seq)
end

return "'''Compliance observed'''<br>" ..
	"<small>" .. eventId .. "</small><br>" ..
	"'''Profile:''' " .. profile.label .. "<br>" ..
	"<small>" .. profile.description .. "<br>" ..
	"'''Outcome:''' " .. outcome .. "</small>"


end

---

-- Row-column insertion helper

local function addCorridorColumns(body, args, channel)
body = tostring(body or "")


if not body:find("{|", 1, true) then
	return body
end

local showLocalityColumn = yn(args.rowlocalities or args.localities or args.showlocalities, true)
local showTradeColumn = yn(args.tradecolumn or args.tradecolumns or args.showtradecolumn, true)
local showComplianceColumn = yn(args.compliancecolumn or args.compliancecolumns or args.observedcolumn or args.showcompliancecolumn, true)
local showRebellionColumn = yn(args.rebellioncolumn or args.rebellioncolumns or args.unrestcolumn or args.showrebellioncolumn, true)

if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then
	return body
end

local lines = mw.text.split(body, "\n", true)
local out = {}

local inTable = false
local headerInserted = false
local rowNumber = 0

for _, line in ipairs(lines) do
	if line:match("^%s*{|") then
		inTable = true
		headerInserted = false
		table.insert(out, line)

	elseif inTable and not headerInserted and line:match("^%s*!") then
		if showLocalityColumn then
			table.insert(out, "! Locality focus")
		end

		if showTradeColumn then
			table.insert(out, "! Trade agreement")
		end

		if showComplianceColumn then
			table.insert(out, "! Compliance observed")
		end

		if showRebellionColumn then
			table.insert(out, "! Rebellion alert")
		end

		headerInserted = true
		table.insert(out, line)

	elseif inTable and line:match("^%s*|%-") then
		rowNumber = rowNumber + 1
		local loc = rowLocalityData(args, channel, rowNumber)

		table.insert(out, line)

		if showLocalityColumn then
			table.insert(out, "| " .. rowLocalityText(loc))
		end

		if showTradeColumn then
			table.insert(out, "| " .. tradeAgreementCell(args, channel, rowNumber, loc))
		end

		if showComplianceColumn then
			table.insert(out, "| " .. complianceObservedCell(args, channel, rowNumber, loc))
		end

		if showRebellionColumn then
			table.insert(out, "| " .. rebellionAlertCell(args, channel, rowNumber, loc))
		end

	elseif inTable and line:match("^%s*|}") then
		inTable = false
		table.insert(out, line)

	else
		table.insert(out, line)
	end
end

return table.concat(out, "\n")


end

---

-- Main Southern Strait-only view

function p.makeTable(frame)
local args = normalizeArgsDate((frame and frame.args) or {})


local showIntro  = yn(args.intro, false)
local showTitle  = yn(args.showtitle or args.titleline, false)
local showLegend = yn(args.legend or args.showlegend, false)
local collapseSections = yn(args.collapsible, true)
local startCollapsed = yn(args.collapsed, true)

local includeWar     = yn(args.war or args.warleague, true)
local includeHatch   = yn(args.hatch or args.hatchministry, true)
local includeMission = yn(args.missionary or args.templebank, true)

local year = args.year or args.psscyear
local day  = args.day or args.dayofyear
local title = args.title or "Southern Strait Corridor Daily Operations"
local label = dateLabel(args)

local out = {}

if showTitle then
	table.insert(out, "==" .. title .. "==")
end

if showIntro then
	table.insert(out, string.format(
		"''Southern Strait-only operational display for %s, limited to Mylecian Southern Strait corridor entries.''",
		label
	))
	table.insert(out, "")
	table.insert(out, "This display combines Council of Kings deployments, Hatch Ministry privateer activity, Temple Bank missionary operations, row-level Southern Strait locality focus columns, Temple Bank-only trade-agreement sign-offs, row-level compliance observations, and row-level rebellion-type unrest alerts while displaying the operational City/Node as Mylecia. Imperial Federation and Bassaridian Mission modifiers are applied where effective.")
end

local ifSupportNotice = imperialFederationSupportNotice(args)
if ifSupportNotice ~= "" then
	table.insert(out, ifSupportNotice)
	table.insert(out, "")
end

local missionNotice = bassaridianMissionsNotice(args)
if missionNotice ~= "" then
	table.insert(out, missionNotice)
	table.insert(out, "")
end

if includeWar then
	local warArgs = {
		year = year,
		day = day,
		rows = args.warrows or args.rows or "24",
		scanrows = args.warscanrows or args.scanrows or "160",
		showlegend = showLegend and "yes" or "no",
		flagsize = args.flagsize or "20",
		zone = "SouthernStrait",
		division = "Council of Kings Division",
	}

	if args.seed and tostring(args.seed) ~= "" then
		warArgs.seed = args.seed
	end

	local ok, body = safeCall("Module:WarLeagueDivisionOpsBVR", "makeTable", warArgs)
	body = forceMyleciaCityNodeColumn(body, args)
	body = addCorridorColumns(body, args, "war")

	table.insert(out, hr())
	table.insert(out, section("Council of Kings Southern Strait deployments", body, collapseSections, startCollapsed))
end

if includeHatch then
	local hatchArgs = {
		year = year,
		day = day,
		legend = showLegend and "yes" or "no",
		zone = "SouthernStrait",
	}

	if args.captain and tostring(args.captain) ~= "" then
		hatchArgs.captain = args.captain
	end

	local ok, body = safeCall("Module:HatchMinistryDailyOps", "dailyReport", hatchArgs)
	body = forceMyleciaCityNodeColumn(body, args)
	body = swapHatchHomeCityCurrentStation(body, args)
	body = addCorridorColumns(body, args, "hatch")

	table.insert(out, hr())
	table.insert(out, section("Hatch Ministry Southern Strait privateer operations", body, collapseSections, startCollapsed))
end

if includeMission then
	local missionaryArgs = {
		year = year,
		day = day,
		region = "Haifan",
		showlegend = showLegend and "yes" or "no",
		includeThreat = args.includeThreat or args.includethreat or "no",
		showEcon = args.showEcon or args.showecon or "no",
	}

	if args.missionarylimit and tostring(args.missionarylimit) ~= "" then
		missionaryArgs.limit = args.missionarylimit
	elseif args.limit and tostring(args.limit) ~= "" then
		missionaryArgs.limit = args.limit
	end

	local ok, body = safeCall("Module:MissionaryDailyOpsBVR", "makeTable", missionaryArgs)
	body = forceMyleciaCityNodeColumn(body, args)
	body = addCorridorColumns(body, args, "missionary")

	table.insert(out, hr())
	table.insert(out, section("Temple Bank Southern Strait missionary operations", body, collapseSections, startCollapsed))
end

if showLegend then
	local legendBody = table.concat({
		"* '''Council of Kings Southern Strait deployments''' show only Council of Kings Division War League rows for the Southern Strait corridor.",
		"* '''Hatch Ministry Southern Strait privateer operations''' show Southern Strait privateer rows.",
		"* '''Temple Bank Southern Strait missionary operations''' show Haifan Bassaridian missionary rows, but the displayed City/Node is forced to Mylecia in this wrapper.",
		"* '''City/Node display''' is forced to [[File:MyleciaFlabB.jpeg|20px]] [[Mylecia]] for War League, Hatch Ministry, and Temple Bank rows.",
		"* '''Locality focus''' columns assign one extended Southern Strait, Eastern Mykonos Desert, or eastern Snowholme foothill settlement to each generated row when row-level localities are enabled.",
		"* '''Trade agreement''' columns generate actual trade sign-offs only in the Temple Bank section; War League and Hatch Ministry rows display '''-''' in that column.",
		"* '''Compliance observed''' columns show minor positive local behavior below the level of a formal trade agreement.",
		"* '''Rebellion alert''' columns show rare row-level contained unrest events such as forged proclamations, broker-cell activity, route barricades, false orders, or Dispossessed-adjacent courier behavior.",
		"* '''Saruq Thyrsa''' and '''Thal Qencha''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
		"* '''Imperial Federation support''' applies from 112/53 PSSC by default. It adds +10 percentage points to compliance observations and +3 percentage points to Temple Bank trade sign-offs for Saruq Thyrsa, Mirovia, and Rumi-Kefaya.",
		"* '''Bassaridian Missions support''' applies from 55/2/53 PSSC by default. It reduces unrest-alert chance by 1 percentage point in all locality rows and increases compliance-observation chance by 1 percentage point in all locality rows.",
		"* '''Major Bassaridian Missions''' at Saruq Thyrsa, Mirovia, Ayllu Nysara, Thal Qencha, and Qhapaq Selen receive an additional +2 percentage points to compliance observations.",
		"* Use '''tradechance=100''', '''compliancechance=100''', and '''rebellionchance=100''' for testing.",
		"* Use '''missionsupport=no''' to disable Bassaridian Missions modifiers for testing.",
		"* Use '''imperialsupport=no''' to disable Imperial Federation support modifiers for testing.",
		"* Use '''forcemylecia=no''' only if the wrapper should temporarily show the original source-module City/Node values."
	}, "\n")

	table.insert(out, hr())
	table.insert(out, section("Combined legend", legendBody, collapseSections, startCollapsed))
end

return table.concat(out, "\n")


end

---

-- Shortcut views

function p.southernStrait(frame)
local args = (frame and frame.args) or {}
args.title = args.title or "Southern Strait Corridor Daily Operations"


return p.makeTable({ args = args })


end

function p.warOnly(frame)
local args = (frame and frame.args) or {}


args.hatch = "no"
args.missionary = "no"
args.war = "yes"
args.title = args.title or "War League Southern Strait Daily Operations"

return p.makeTable({ args = args })


end

function p.hatchOnly(frame)
local args = (frame and frame.args) or {}


args.war = "no"
args.missionary = "no"
args.hatch = "yes"
args.title = args.title or "Hatch Ministry Southern Strait Daily Operations"

return p.makeTable({ args = args })


end

function p.missionaryOnly(frame)
local args = (frame and frame.args) or {}


args.war = "no"
args.hatch = "no"
args.missionary = "yes"
args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations"

return p.makeTable({ args = args })


end

return p