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 1: | Line 1: | ||
-- Module:SouthernStraitCorridorOps | -- Module:SouthernStraitCorridorOps | ||
-- Nysara Velai-focused revision | |||
local p = {} | local p = {} | ||
| Line 5: | Line 6: | ||
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 currentDate = os.time() | ||
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay) | local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay) | ||
local psscYear = math.floor(totalDaysElapsed / daysPerYear) | local psscYear = math.floor(totalDaysElapsed / daysPerYear) | ||
local dayOfYear = (totalDaysElapsed % daysPerYear) + 1 | 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 | |||
out[k] = v | |||
end | |||
local y = tonumber(out.year or out.psscyear) | |||
out | local d = tonumber(out.day or out.dayofyear) | ||
if not y or not d then | |||
y, d = getCurrentPSSCDate() | |||
end | |||
if not y or not d then | |||
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 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) | |||
local ok, result = pcall(function() | |||
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 < 1 then doy = 1 end | ||
if doy > 183 then doy = 183 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" | |||
return | |||
end | 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" | |||
local | |||
table.insert(out, '<div class="' .. collapseClass .. '" style="border:1px solid #a2a9b1; margin:0.6em 0; background:#f8f9fa;">') | |||
table.insert(out, "== | 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, content) | ||
table.insert(out, '</div>') | |||
table.insert(out, '</div>') | |||
return table.concat(out, "\n") | 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 | |||
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 | local idx = (hashString(tostring(seed or "") .. ":" .. tostring(salt or "")) % #list) + 1 | ||
return list[idx] | 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 | end | ||
local function clampChance(value) | local function clampChance(value) | ||
value = tonumber(value) or 0 | value = tonumber(value) or 0 | ||
if value < 0 then return 0 end | if value < 0 then return 0 end | ||
if value > 100 then return 100 end | if value > 100 then return 100 end | ||
return value | return value | ||
end | end | ||
local function isOnOrAfterPSSCDate(year, day, startYear, startDay) | local function isOnOrAfterPSSCDate(year, day, startYear, startDay) | ||
year = tonumber(year) or 0 | year = tonumber(year) or 0 | ||
day = tonumber(day) or 0 | day = tonumber(day) or 0 | ||
startYear = tonumber(startYear) or 0 | startYear = tonumber(startYear) or 0 | ||
startDay = tonumber(startDay) or 0 | startDay = tonumber(startDay) or 0 | ||
if year > startYear then | |||
return true | |||
end | |||
if year == startYear and day >= startDay then | |||
return true | |||
end | |||
if year == startYear and day >= startDay then | |||
end | |||
return false | |||
end | end | ||
| Line 208: | Line 181: | ||
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 = "[[Nysara Velai]]", | city = "[[Nysara Velai]]", | ||
region = "Eastern foothills of the [[Snowholme Range]]", | region = "Eastern foothills of the [[Snowholme Range]]", | ||
category = "Agriculture", | category = "Agriculture", | ||
company = "[[Nysaran Terrace Farmers' | company = "[[Nysaran Terrace Farmers' Cooperative]]", | ||
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 Lenaia]]", | city = "[[Thal Lenaia]]", | ||
region = "Eastern foothills of the [[Snowholme Range]]", | region = "Eastern foothills of the [[Snowholme Range]]", | ||
category = "Agriculture", | category = "Agriculture", | ||
company = "[[Thal Lenaia Springkeepers' Cooperative]]", | company = "[[Thal Lenaia 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 = "[[Selenarion]]", | city = "[[Selenarion]]", | ||
region = "Eastern foothills of the [[Snowholme Range]]", | region = "Eastern foothills of the [[Snowholme Range]]", | ||
category = "Services", | category = "Services", | ||
company = "[[Selenarion Route Registry Office]]", | company = "[[Selenarion 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" | ||
} | } | ||
} | } | ||
-- Locality presence controls | -- Locality presence controls | ||
-- | -- AYS (Nysara Velai) is the principal focus: three rows per section by default. | ||
-- QSE is | -- QSE (Selenarion) is limited to one row per section. | ||
-- MIR, SAR, and THQ also remain limited to one row per section. | |||
local cappedPresenceLocalities = { | local cappedPresenceLocalities = { | ||
MIR = true, | MIR = true, | ||
SAR = true, | SAR = true, | ||
THQ = true | THQ = true, | ||
QSE = true | |||
} | } | ||
local function findLocalityByCode(code) | local function findLocalityByCode(code) | ||
code = tostring(code or "") | code = tostring(code or "") | ||
for _, loc in ipairs(southernTradeCities or {}) do | |||
for _, loc in ipairs(southernTradeCities or {}) do | if tostring(loc.code or "") == code then | ||
return loc | |||
end | |||
end | end | ||
return nil | |||
return nil | |||
end | end | ||
local function localityCount(state, locOrCode) | local function localityCount(state, locOrCode) | ||
if not state or not state.counts then return 0 end | if not state or not state.counts then return 0 end | ||
local code | |||
if type(locOrCode) == "table" then | |||
code = tostring(locOrCode.code or "") | |||
else | |||
code = tostring(locOrCode or "") | |||
end | |||
return tonumber(state.counts[code]) or 0 | |||
end | end | ||
return | local function incrementLocalityCount(state, loc) | ||
if not state or not loc then return end | |||
local code = tostring(loc.code or "") | |||
if code == "" then return end | |||
state.counts[code] = (tonumber(state.counts[code]) or 0) + 1 | |||
end | end | ||
local function | local function nysaraVelaiPresenceTarget(args) | ||
local v = tonumber( | |||
args.nysaravelaitarget or | |||
args.nysaraVelaiTarget or | |||
local | args.nysaratarget or | ||
args.nysaraTarget or | |||
args.aystarget or | |||
args.aysTarget | |||
) | |||
if v == nil then v = 3 end | |||
if v < 0 then v = 0 end | |||
return math.floor(v) | |||
end | end | ||
local function | local function nysaraVelaiAllowOverTarget(args) | ||
return yn( | |||
args. | args.nysaravelaioverfill or | ||
args. | args.nysaraVelaiOverfill or | ||
args. | args.nysaraoverfill or | ||
args. | args.nysaraOverfill or | ||
args. | args.aysoverfill or | ||
args.aysOverfill, | |||
false | |||
) | |||
end | end | ||
local function | local function buildPriorityLocalityPlan(args, channel) | ||
local | local plan = {} | ||
args | local target = nysaraVelaiPresenceTarget(args) | ||
) | |||
-- Nysara Velai always occupies the first priority slots. This ensures | |||
-- that even a short source table remains focused on Nysara Velai. | |||
for _ = 1, target do | |||
table.insert(plan, "AYS") | |||
end | |||
-- Selenarion receives one potential deployment only after the Nysara | |||
-- Velai target has been assigned. | |||
table.insert(plan, "QSE") | |||
return plan | |||
return | |||
end | end | ||
local function newLocalityAssignmentState(args, channel) | local function newLocalityAssignmentState(args, channel) | ||
return { | return { | ||
channel = tostring(channel or ""), | channel = tostring(channel or ""), | ||
counts = {}, | counts = {}, | ||
aysTarget = nysaraVelaiPresenceTarget(args), | |||
aysAllowOverTarget = nysaraVelaiAllowOverTarget(args), | |||
priorityPlan = buildPriorityLocalityPlan(args, channel) | |||
} | } | ||
end | end | ||
local function canAssignLocality(args, loc, state) | local function canAssignLocality(args, loc, state) | ||
if not loc then return false end | if not loc then return false end | ||
local code = tostring(loc.code or "") | |||
if cappedPresenceLocalities[code] and localityCount(state, code) >= 1 then | |||
if cappedPresenceLocalities[code] and localityCount(state, code) >= 1 | |||
return false | return false | ||
end | end | ||
if code == "AYS" and state and not state.aysAllowOverTarget then | |||
if localityCount(state, code) >= (tonumber(state.aysTarget) or 3) then | |||
return false | |||
end | |||
end | |||
return true | return true | ||
end | end | ||
local | local function priorityLocalityForRow(state, rowNumber) | ||
local | if not state or not state.priorityPlan then return nil end | ||
local | local row = tonumber(rowNumber) or 0 | ||
local code = state.priorityPlan[row] | |||
if | if not code then return nil end | ||
return findLocalityByCode(code) | |||
end | |||
return | |||
end | end | ||
| Line 452: | Line 373: | ||
local function rowLocalityData(args, channel, rowNumber, state) | local function rowLocalityData(args, channel, rowNumber, state) | ||
if not southernTradeCities or #southernTradeCities == 0 then | if not southernTradeCities or #southernTradeCities == 0 then | ||
return nil | return nil | ||
end | end | ||
state = state or newLocalityAssignmentState(args, channel) | |||
-- The first four rows, when available, always contain the default | |||
-- three Nysara Velai deployments and one Selenarion deployment. | |||
local priorityLoc = priorityLocalityForRow(state, rowNumber) | |||
if priorityLoc and canAssignLocality(args, priorityLoc, state) then | |||
incrementLocalityCount(state, priorityLoc) | |||
return priorityLoc | |||
end | |||
local | 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 baseRow = tonumber(rowNumber) or 1 | |||
local | for attempt = 0, #southernTradeCities - 1 do | ||
local | local idx = ((baseRow - 1 + offset + attempt) % #southernTradeCities) + 1 | ||
local loc = southernTradeCities[idx] | |||
if canAssignLocality(args, loc, state) then | |||
incrementLocalityCount(state, loc) | |||
return loc | |||
end | |||
end | end | ||
for _, loc in ipairs(southernTradeCities) do | -- Once all caps and the Nysara target are filled, continue with the | ||
-- genuinely uncapped secondary localities. | |||
for _, loc in ipairs(southernTradeCities) do | |||
local code = tostring(loc.code or "") | |||
if not cappedPresenceLocalities[code] and code ~= "AYS" then | |||
incrementLocalityCount(state, loc) | |||
return loc | |||
end | |||
end | end | ||
return nil | |||
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>" | |||
end | end | ||
| Line 520: | Line 435: | ||
local imperialFederationSupportLocalities = { | local imperialFederationSupportLocalities = { | ||
MIR = true, | MIR = true, | ||
SAR = true, | SAR = true, | ||
RUK = true | RUK = true | ||
} | } | ||
local function imperialSupportEnabled(args) | local function imperialSupportEnabled(args) | ||
return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true) | return yn(args.imperialsupport or args.imperialSupport or args.ifsupport or args.ifSupport, true) | ||
end | end | ||
local function imperialSupportEffective(args) | local function imperialSupportEffective(args) | ||
if not imperialSupportEnabled(args) then | if not imperialSupportEnabled(args) then | ||
return false | return false | ||
end | 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 hasImperialFederationSupport(args, loc) | ||
if not imperialSupportEffective(args) then | if not imperialSupportEffective(args) then | ||
return false | return false | ||
end | end | ||
return loc and imperialFederationSupportLocalities[tostring(loc.code or "")] == true | |||
end | end | ||
local function imperialSupportBoostedChance(args, loc, baseChance, boost) | local function imperialSupportBoostedChance(args, loc, baseChance, boost) | ||
local chance = clampChance(baseChance | local chance = clampChance(baseChance) | ||
if hasImperialFederationSupport(args, loc) then | |||
chance = chance + (tonumber(boost) or 0) | |||
end | |||
return clampChance(chance) | |||
end | end | ||
local function imperialFederationSupportNotice(args) | local function imperialFederationSupportNotice(args) | ||
if not imperialSupportEffective(args) then | if not imperialSupportEffective(args) then | ||
return "" | return "" | ||
end | 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 | end | ||
| Line 588: | Line 491: | ||
local majorBassaridianMissionLocalities = { | local majorBassaridianMissionLocalities = { | ||
SAR = true, | SAR = true, | ||
MIR = true, | MIR = true, | ||
AYS = true, | AYS = true, | ||
THQ = true, | THQ = true, | ||
QSE = true | QSE = true | ||
} | } | ||
local function bassaridianMissionsEnabled(args) | local function bassaridianMissionsEnabled(args) | ||
return yn(args.missionsupport or args.missionSupport or args.bassaridianmissions or args.bassaridianMissions, true) | return yn(args.missionsupport or args.missionSupport or args.bassaridianmissions or args.bassaridianMissions, true) | ||
end | end | ||
local function bassaridianMissionsEffective(args) | local function bassaridianMissionsEffective(args) | ||
if not bassaridianMissionsEnabled(args) then | if not bassaridianMissionsEnabled(args) then | ||
return false | return false | ||
end | 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 | end | ||
local function hasBassaridianMission(args, loc) | local function hasBassaridianMission(args, loc) | ||
if not bassaridianMissionsEffective(args) then | if not bassaridianMissionsEffective(args) then | ||
return false | return false | ||
end | end | ||
return loc ~= nil | |||
end | end | ||
local function hasMajorBassaridianMission(args, loc) | local function hasMajorBassaridianMission(args, loc) | ||
if not bassaridianMissionsEffective(args) then | if not bassaridianMissionsEffective(args) then | ||
return false | return false | ||
end | end | ||
return loc and majorBassaridianMissionLocalities[tostring(loc.code or "")] == true | |||
end | end | ||
local function | local function nysaraVelaiComplianceBoost(args) | ||
local v = tonumber( | local v = tonumber( | ||
args. | args.nysaravelaicomplianceboost or | ||
args. | args.nysaraVelaiComplianceBoost or | ||
args. | args.nysaracomplianceboost or | ||
args. | args.nysaraComplianceBoost or | ||
args. | args.ayscomplianceboost or | ||
) | args.aysComplianceBoost | ||
) | |||
if v == nil then v = 5 end | |||
return v | |||
end | end | ||
local function | local function nysaraVelaiUnrestBoost(args) | ||
local v = tonumber( | local v = tonumber( | ||
args. | args.nysaravelaiunrestboost or | ||
args. | args.nysaraVelaiUnrestBoost or | ||
args. | args.nysaraunrestboost or | ||
args. | args.nysaraUnrestBoost or | ||
args. | args.aysunrestboost or | ||
) | args.aysUnrestBoost | ||
) | |||
if v == nil then v = 4 end | |||
return v | |||
end | end | ||
local function | local function nysaraVelaiAdjustedComplianceChance(args, loc, baseChance) | ||
local chance = clampChance(baseChance) | local chance = clampChance(baseChance) | ||
if loc and tostring(loc.code or "") == "AYS" then | |||
chance = chance + nysaraVelaiComplianceBoost(args) | |||
end | |||
return clampChance(chance) | |||
end | end | ||
local function nysaraVelaiAdjustedUnrestChance(args, loc, baseChance) | |||
local chance = clampChance(baseChance) | |||
local function | |||
local chance = clampChance(baseChance | |||
if loc and tostring(loc.code or "") == "AYS" then | |||
chance = chance + nysaraVelaiUnrestBoost(args) | |||
end | |||
return clampChance(chance) | |||
end | end | ||
local function missionAdjustedComplianceChance(args, loc, baseChance) | local function missionAdjustedComplianceChance(args, loc, baseChance) | ||
local chance = clampChance(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 | |||
if hasMajorBassaridianMission(args, loc) then | |||
end | |||
return clampChance(chance) | |||
end | end | ||
local function missionAdjustedUnrestChance(args, loc, baseChance) | local function missionAdjustedUnrestChance(args, loc, baseChance) | ||
local chance = clampChance(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 | end | ||
local function bassaridianMissionsNotice(args) | local function bassaridianMissionsNotice(args) | ||
if not bassaridianMissionsEffective(args) then | if not bassaridianMissionsEffective(args) then | ||
return "" | return "" | ||
end | 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]], [[Nysara Velai]], [[Thal Lenaia]], and [[Selenarion]].<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. Nysara Velai receives an additional +4 percentage points to unrest-alert likelihood and +5 percentage points to compliance-observation likelihood, reflecting intensified contestation and intensified Bassaridian administrative presence across its terrace-farming districts, upland provisioning routes, and eastern Snowholme approaches. " .. | |||
"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 | end | ||
| Line 746: | Line 622: | ||
local recurringAdversaries = { | local recurringAdversaries = { | ||
KHADIR = { | KHADIR = { | ||
name = "[[Khadir Veyran]]", | name = "[[Khadir Veyran]]", | ||
epithet = "Keeper of the Bromian Ledger", | epithet = "Keeper of the Bromian Ledger", | ||
localities = { | localities = { | ||
QSE = true, | QSE = true, | ||
AYS = true, | AYS = true, | ||
RUK = true, | RUK = true, | ||
HAY = true | HAY = true | ||
}, | }, | ||
notes = { | notes = { | ||
"Bromian Ledger intermediaries are suspected of delaying countersignatures without openly refusing Bassaridian review.", | "Bromian Ledger intermediaries are suspected of delaying countersignatures without openly refusing Bassaridian review.", | ||
"Veyran-linked witnesses preserve older Maritime Markets custom as a rival source of commercial legitimacy.", | "Veyran-linked witnesses preserve older Maritime Markets custom as a rival source of commercial legitimacy.", | ||
"Broker-house clerks appear to be keeping disputed cargo identity ambiguous until General Port auditors withdraw.", | "Broker-house clerks appear to be keeping disputed cargo identity ambiguous until General Port auditors withdraw.", | ||
"Warehouse notaries tied to the Bromian Ledger are suspected of turning routine paperwork into a jurisdictional dispute." | "Warehouse notaries tied to the Bromian Ledger are suspected of turning routine paperwork into a jurisdictional dispute." | ||
}, | }, | ||
complianceNotes = { | complianceNotes = { | ||
"Temple Bank auditors noted that this cooperation weakens the Bromian Ledger's claim that older broker custom remains the only trusted record.", | "Temple Bank auditors noted that this cooperation weakens the Bromian Ledger's claim that older broker custom remains the only trusted record.", | ||
"Local registry compliance was treated as useful evidence against Veyran-linked delay tactics.", | "Local registry compliance was treated as useful evidence against Veyran-linked delay tactics.", | ||
"The observation was preserved as a small but meaningful challenge to Bromian Ledger influence in the pass-road economy." | "The observation was preserved as a small but meaningful challenge to Bromian Ledger influence in the pass-road economy." | ||
}, | }, | ||
tradeNotes = { | tradeNotes = { | ||
"Temple Bank clerks flagged the sign-off as a possible counterweight to Khadir Veyran's older broker-credit network.", | "Temple Bank clerks flagged the sign-off as a possible counterweight to Khadir Veyran's older broker-credit network.", | ||
"The filing gives General Port auditors a lawful route around Bromian Ledger intermediaries.", | "The filing gives General Port auditors a lawful route around Bromian Ledger intermediaries.", | ||
"The agreement was preserved as evidence that local commercial authority could be recorded without Veyran's countersignature." | "The agreement was preserved as evidence that local commercial authority could be recorded without Veyran's countersignature." | ||
} | } | ||
}, | }, | ||
SUYANA = { | SUYANA = { | ||
name = "[[Suyana Amaruq]]", | name = "[[Suyana Amaruq]]", | ||
epithet = "Black-Thread Suyana", | epithet = "Black-Thread Suyana", | ||
localities = { | localities = { | ||
QSE = true, | QSE = true, | ||
AYS = true, | AYS = true, | ||
THQ = true, | THQ = true, | ||
HAY = true | HAY = true | ||
}, | }, | ||
notes = { | notes = { | ||
"Black-Thread couriers are suspected of using ordinary household movement to mask route intelligence.", | "Black-Thread couriers are suspected of using ordinary household movement to mask route intelligence.", | ||
"Suyana-linked runners may have tested night-road inspection gaps before disappearing into ordinary market traffic.", | "Suyana-linked runners may have tested night-road inspection gaps before disappearing into ordinary market traffic.", | ||
"False pilgrims and guide-house messengers appear to have carried warning signals ahead of the audit lane.", | "False pilgrims and guide-house messengers appear to have carried warning signals ahead of the audit lane.", | ||
"Temple Bank officers treated the incident as route-intelligence obstruction rather than ordinary smuggling." | "Temple Bank officers treated the incident as route-intelligence obstruction rather than ordinary smuggling." | ||
}, | }, | ||
complianceNotes = { | complianceNotes = { | ||
"The observation was treated as a useful interruption of Black-Thread courier habits.", | "The observation was treated as a useful interruption of Black-Thread courier habits.", | ||
"Licensed guides helped separate ordinary passage from Suyana-linked courier movement.", | "Licensed guides helped separate ordinary passage from Suyana-linked courier movement.", | ||
"Temple Bank staff preserved the record as evidence that local mobility could be protected without unregistered courier screens." | "Temple Bank staff preserved the record as evidence that local mobility could be protected without unregistered courier screens." | ||
}, | }, | ||
tradeNotes = { | tradeNotes = { | ||
"Temple Bank notes warned that Black-Thread couriers may attempt to test the new trade window through false-route traffic.", | "Temple Bank notes warned that Black-Thread couriers may attempt to test the new trade window through false-route traffic.", | ||
"The sign-off was paired with route-watch instructions because Suyana-linked couriers remained active near the upland approaches.", | "The sign-off was paired with route-watch instructions because Suyana-linked couriers remained active near the upland approaches.", | ||
"Mission clerks treated the filing as useful, but not sufficient to end Black-Thread influence over informal passage." | "Mission clerks treated the filing as useful, but not sufficient to end Black-Thread influence over informal passage." | ||
} | } | ||
} | } | ||
} | } | ||
local function adversaryNodesEnabled(args) | local function adversaryNodesEnabled(args) | ||
return yn( | return yn( | ||
args.adversarynodes or | args.adversarynodes or | ||
args.adversaryNodes or | args.adversaryNodes or | ||
args.namedadversaries or | args.namedadversaries or | ||
args.namedAdversaries or | args.namedAdversaries or | ||
args.adversarynotes or | args.adversarynotes or | ||
args.adversaryNotes, | args.adversaryNotes, | ||
true | true | ||
) | ) | ||
end | end | ||
local function adversaryNodesEffective(args) | local function adversaryNodesEffective(args) | ||
if not adversaryNodesEnabled(args) then | if not adversaryNodesEnabled(args) then | ||
return false | return false | ||
end | 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.adversarystartyear or args.adversaryStartYear) or ADVERSARY_NODES_START_YEAR | |||
local startDay = tonumber(args.adversarystartday or args.adversaryStartDay) or ADVERSARY_NODES_START_DAY | |||
return isOnOrAfterPSSCDate(year, day, startYear, startDay) | |||
end | end | ||
local function adversaryNoteChance(args, kind) | local function adversaryNoteChance(args, kind) | ||
kind = tostring(kind or "") | kind = tostring(kind or "") | ||
if kind == "trade" then | |||
return clampChance(tonumber(args.tradeadversarychance or args.tradeAdversaryChance) or 22) | |||
if kind == "trade" then | elseif kind == "compliance" then | ||
return clampChance(tonumber(args.complianceadversarychance or args.complianceAdversaryChance) or 18) | |||
elseif kind == "compliance" then | end | ||
end | |||
return clampChance(tonumber(args.adversarychance or args.adversaryChance or args.unrestadversarychance or args.unrestAdversaryChance) or 35) | |||
end | end | ||
local function localityAllowsAdversary(loc, adv) | local function localityAllowsAdversary(loc, adv) | ||
if not adv then return false end | if not adv then return false end | ||
if not loc then return true end | if not loc then return true end | ||
local localities = adv.localities | |||
if not localities then return true end | |||
return localities[tostring(loc.code or "")] == true | |||
end | end | ||
local function candidateAdversaryCodes(profile, loc) | local function candidateAdversaryCodes(profile, loc) | ||
local out = {} | local out = {} | ||
if profile and type(profile.adversaries) == "table" then | |||
if profile and type(profile.adversaries) == "table" then | for _, code in ipairs(profile.adversaries) do | ||
local adv = recurringAdversaries[code] | |||
if adv and localityAllowsAdversary(loc, adv) then | |||
table.insert(out, code) | |||
end | |||
end | end | ||
end | end | ||
if #out == 0 then | if #out == 0 then | ||
for code, adv in pairs(recurringAdversaries) do | |||
if localityAllowsAdversary(loc, adv) then | |||
table.insert(out, code) | |||
end | |||
end | end | ||
end | end | ||
return out | |||
end | end | ||
local function adversaryNotePart(args, kind, seed, profile, loc) | local function adversaryNotePart(args, kind, seed, profile, loc) | ||
if not adversaryNodesEffective(args) then | if not adversaryNodesEffective(args) then | ||
return "" | return "" | ||
end | end | ||
local chance = adversaryNoteChance(args, kind) | |||
local roll = pctFromSeed(seed, "adv-note-roll-" .. tostring(kind or "general")) + 1 | |||
if roll > chance then | |||
return "" | |||
end | |||
local candidates = candidateAdversaryCodes(profile, loc) | |||
local code = pick(candidates, seed, "adv-note-code-" .. tostring(kind or "general")) | |||
local adv = recurringAdversaries[code] | |||
if not adv then | |||
return "" | |||
end | |||
local notes = adv.notes | |||
if tostring(kind or "") == "trade" and adv.tradeNotes then | |||
notes = adv.tradeNotes | |||
elseif tostring(kind or "") == "compliance" and adv.complianceNotes then | |||
local notes = adv.notes | notes = adv.complianceNotes | ||
end | |||
if tostring(kind or "") == "trade" and adv.tradeNotes then | |||
elseif tostring(kind or "") == "compliance" and adv.complianceNotes then | |||
end | |||
local note = pick(notes, seed, "adv-note-text-" .. tostring(kind or "general")) | |||
if not note then | |||
return "" | |||
end | |||
return "<br>'''Adversarial note:''' " .. adv.name .. " (''" .. adv.epithet .. "'') — " .. note | |||
end | end | ||
local function adversarialNodesNotice(args) | local function adversarialNodesNotice(args) | ||
if not adversaryNodesEffective(args) then | if not adversaryNodesEffective(args) then | ||
return "" | return "" | ||
end | end | ||
return "'''Temple Bank adversarial-node tracking active:''' from 60/2/53 PSSC onward, occasional row-level notes may identify suspected influence associated with [[Khadir Veyran]] (''Keeper of the Bromian Ledger'') or [[Suyana Amaruq]] (''Black-Thread Suyana'').<br><small>These notes are seeded and probabilistic. They do not appear in every unrest, compliance, or trade-signoff row. Use '''adversarynodes=no''' to disable them, '''adversarychance=35''' to adjust unrest-note frequency, '''complianceadversarychance=18''' to adjust compliance-note frequency, and '''tradeadversarychance=22''' to adjust Temple Bank trade-signoff note frequency.</small>" | |||
end | end | ||
| Line 937: | Line 792: | ||
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 | |||
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(" ", " ") | |||
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 | |||
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 | |||
if | for _, line in ipairs(lines) do | ||
if line:match("^%s*{|") then | |||
inTable = true | |||
cityCol = nil | |||
col = 0 | |||
table.insert(out, line) | |||
local | elseif inTable and line:match("^%s*!") then | ||
local | 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 | |||
for _, | cityCol = col | ||
end | |||
end | |||
else | |||
col = col + 1 | col = col + 1 | ||
if cityNodeHeaderLabels[cleanHeaderLabel( | if cityNodeHeaderLabels[cleanHeaderLabel(line)] then | ||
cityCol = col | cityCol = col | ||
end | end | ||
end | end | ||
table.insert(out, line) | |||
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 | |||
if | 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 | ||
end | end | ||
elseif inTable and line:match("^%s*|}") then | |||
inTable = false | |||
table.insert(out, line) | |||
else | else | ||
table.insert(out, line) | |||
end | end | ||
end | end | ||
return table.concat(out, "\n") | |||
end | end | ||
| Line 1,060: | Line 903: | ||
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 | |||
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 | |||
if | for _, line in ipairs(lines) do | ||
if line:match("^%s*{|") then | |||
inTable = true | |||
homeCol = nil | |||
stationCol = nil | |||
col = 0 | |||
table.insert(out, line) | |||
local | 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 | |||
for _, | homeCol = col | ||
elseif label == "current station" or label == "station" then | |||
stationCol = col | |||
end | |||
end | |||
else | |||
col = col + 1 | col = col + 1 | ||
local label = cleanHeaderLabel( | local label = cleanHeaderLabel(line) | ||
if label == "home city" or label == "home" then | if label == "home city" or label == "home" then | ||
homeCol = col | homeCol = col | ||
| Line 1,102: | Line 950: | ||
end | end | ||
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) | |||
table.insert(out, | |||
else | else | ||
table.insert(out, line) | table.insert(out, line) | ||
end | end | ||
end | end | ||
return table.concat(out, "\n") | |||
end | end | ||
| Line 1,150: | Line 984: | ||
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) | |||
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 | local roll = pctFromSeed(seed, "roll") + 1 | ||
if roll > chance then | |||
end | if yn(args.showTradeNoEvent or args.showtradenoevent, false) then | ||
return "No sign-off" | |||
end | |||
return "-" | |||
end | |||
local | local cult = pick(tradeSigningCults, seed, "cult") | ||
local outcome = pick(tradeOutcomes, seed, "outcome") | |||
if not cult then | |||
return "-" | |||
end | |||
local | local seq = (pctFromSeed(seed, "seq") % 20) + 1 | ||
local day | 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 | end | ||
local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq) | |||
local advNote = adversaryNotePart(args, "trade", seed, nil, loc) | |||
local signCode = string.format("%s-TA%02d", cult.code or "CUL", seq) | |||
local advNote = adversaryNotePart(args, "trade", seed, nil, loc) | |||
return "'''Trade sign-off'''<br>" .. | |||
"<small>" .. tradeId .. "</small><br>" .. | |||
(cult.label or "Cult sign-off recorded") .. "<br>" .. | |||
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. advNote .. "</small>" | |||
end | end | ||
| Line 1,231: | Line 1,055: | ||
local rebellionProfiles = { | local rebellionProfiles = { | ||
{ | { | ||
label = "Confederacy-adjacent courier screen", | label = "Confederacy-adjacent courier screen", | ||
description = "False-route couriers testing inspection gaps.", | description = "False-route couriers testing inspection gaps.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Former Maritime Markets broker faction", | label = "Former Maritime Markets broker faction", | ||
description = "Old broker-house retainers resisting General Port paperwork.", | description = "Old broker-house retainers resisting General Port paperwork.", | ||
adversaries = { "KHADIR" } | adversaries = { "KHADIR" } | ||
}, | }, | ||
{ | { | ||
label = "Anti-registry dock syndicate", | label = "Anti-registry dock syndicate", | ||
description = "Warehouse intermediaries opposing voucher and ledger discipline.", | description = "Warehouse intermediaries opposing voucher and ledger discipline.", | ||
adversaries = { "KHADIR" } | adversaries = { "KHADIR" } | ||
}, | }, | ||
{ | { | ||
label = "False-proclamation circle", | label = "False-proclamation circle", | ||
description = "Pamphlet readers circulating forged civic orders.", | description = "Pamphlet readers circulating forged civic orders.", | ||
adversaries = { "KHADIR", "SUYANA" } | adversaries = { "KHADIR", "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Route-barricade committee", | label = "Route-barricade committee", | ||
description = "Agitators obstructing lawful inspection lanes.", | description = "Agitators obstructing lawful inspection lanes.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Dispossessed-sympathetic quay cell", | label = "Dispossessed-sympathetic quay cell", | ||
description = "Waterfront cell copying Confederacy-style courier habits.", | description = "Waterfront cell copying Confederacy-style courier habits.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Illicit ritual-cargo and medical-transfer exploiters", | label = "Illicit ritual-cargo and medical-transfer exploiters", | ||
description = "Handlers using illness, mourning, ritual cargo, or shrine protection to delay inspection.", | description = "Handlers using illness, mourning, ritual cargo, or shrine protection to delay inspection.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Bromian Ledger witness screen", | label = "Bromian Ledger witness screen", | ||
description = "Broker-linked witnesses preserving conflicting older records against General Port review.", | description = "Broker-linked witnesses preserving conflicting older records against General Port review.", | ||
adversaries = { "KHADIR" } | adversaries = { "KHADIR" } | ||
} | } | ||
} | } | ||
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.", | ||
"False medical-transfer exemption.", | "False medical-transfer exemption.", | ||
"Unreconciled caravan lodging roll.", | "Unreconciled caravan lodging roll.", | ||
"Guide-house witness contradiction.", | "Guide-house witness contradiction.", | ||
"Older Maritime Markets clearance invoked." | "Older Maritime Markets clearance invoked." | ||
} | } | ||
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.", | ||
"Guide-license review opened.", | "Guide-license review opened.", | ||
"Medical-transfer custody ledger corrected.", | "Medical-transfer custody ledger corrected.", | ||
"Broker-house witnesses summoned for countersignature.", | "Broker-house witnesses summoned for countersignature.", | ||
"Night-road registry temporarily frozen." | "Night-road registry temporarily frozen." | ||
} | } | ||
local rebellionRisks = { | local rebellionRisks = { | ||
"Low", | "Low", | ||
"Moderate", | "Moderate", | ||
"High", | "High", | ||
"Contained", | "Contained", | ||
"Escalating" | "Escalating" | ||
} | } | ||
local rebellionAlertExcludedLocalities = { | local rebellionAlertExcludedLocalities = { | ||
MIR = true, | MIR = true, | ||
SAR = true, | SAR = true, | ||
THQ = true | THQ = true | ||
} | } | ||
local function isRebellionAlertExcluded(loc) | local function isRebellionAlertExcluded(loc) | ||
if not loc then | if not loc then return false end | ||
return false | return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true | ||
end | |||
return rebellionAlertExcludedLocalities[tostring(loc.code or "")] == true | |||
end | end | ||
local function rebellionAlertCell(args, channel, rowNumber, loc) | local function rebellionAlertCell(args, channel, rowNumber, loc) | ||
if not loc then | if not loc then | ||
return "-" | return "-" | ||
end | 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) | |||
chance = nysaraVelaiAdjustedUnrestChance(args, loc, chance) | |||
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 | 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 | local profile = pick(rebellionProfiles, seed, "profile") | ||
local | 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 | 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 | end | ||
local advNote = adversaryNotePart(args, "unrest", seed, profile, loc) | |||
return "'''Unrest alert'''<br>" .. | |||
"<small>" .. eventId .. "</small><br>" .. | |||
"'''Profile:''' " .. profile.label .. "<br>" .. | |||
"<small>" .. profile.description .. "<br>" .. | |||
"'''Trigger:''' " .. trigger .. "<br>" .. | |||
"'''Risk:''' " .. risk .. "<br>" .. | |||
"'''Response:''' " .. action .. advNote .. "</small>" | |||
local advNote = adversaryNotePart(args, "unrest", seed, profile, loc) | |||
return "'''Unrest alert'''<br>" .. | |||
end | end | ||
| Line 1,400: | Line 1,208: | ||
local complianceProfiles = { | local complianceProfiles = { | ||
{ | { | ||
label = "Inspection cooperation", | label = "Inspection cooperation", | ||
description = "Local carriers complied with inspection sequencing without requiring escalation.", | description = "Local carriers complied with inspection sequencing without requiring escalation.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Ledger correction", | label = "Ledger correction", | ||
description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty.", | description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty.", | ||
adversaries = { "KHADIR" } | adversaries = { "KHADIR" } | ||
}, | }, | ||
{ | { | ||
label = "Crowd dispersal", | label = "Crowd dispersal", | ||
description = "Local stewards dispersed a gathering before it became an obstruction.", | description = "Local stewards dispersed a gathering before it became an obstruction.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Guide assistance", | label = "Guide assistance", | ||
description = "Licensed guides helped separate lawful traffic from suspect courier movement.", | description = "Licensed guides helped separate lawful traffic from suspect courier movement.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Cargo separation", | label = "Cargo separation", | ||
description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage.", | description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage.", | ||
adversaries = { "KHADIR" } | adversaries = { "KHADIR" } | ||
}, | }, | ||
{ | { | ||
label = "Rumor-control compliance", | label = "Rumor-control compliance", | ||
description = "Temple notices were accepted locally and hostile rumors lost momentum.", | description = "Temple notices were accepted locally and hostile rumors lost momentum.", | ||
adversaries = { "KHADIR", "SUYANA" } | adversaries = { "KHADIR", "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Privateer signal compliance", | label = "Privateer signal compliance", | ||
description = "Vessels accepted stand-off or hold-and-release signals without resistance.", | description = "Vessels accepted stand-off or hold-and-release signals without resistance.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
}, | }, | ||
{ | { | ||
label = "Registry cooperation", | label = "Registry cooperation", | ||
description = "Local registrars preserved names, seals, and witness records for later review.", | description = "Local registrars preserved names, seals, and witness records for later review.", | ||
adversaries = { "KHADIR" } | adversaries = { "KHADIR" } | ||
}, | }, | ||
{ | { | ||
label = "Medical-transfer ledger cooperation", | label = "Medical-transfer ledger cooperation", | ||
description = "Clinic escorts preserved patient custody records without accepting false sacred exemptions.", | description = "Clinic escorts preserved patient custody records without accepting false sacred exemptions.", | ||
adversaries = { "SUYANA" } | adversaries = { "SUYANA" } | ||
} | } | ||
} | } | ||
local complianceOutcomes = { | local complianceOutcomes = { | ||
"Minor positive compliance noted.", | "Minor positive compliance noted.", | ||
"Recorded as a stabilizing local action.", | "Recorded as a stabilizing local action.", | ||
"Accepted as offsetting evidence against wider noncompliance.", | "Accepted as offsetting evidence against wider noncompliance.", | ||
"Filed as a documented good-faith response.", | "Filed as a documented good-faith response.", | ||
"Preserved for future compliance comparison.", | "Preserved for future compliance comparison.", | ||
"Marked as below trade-agreement threshold but operationally helpful." | "Marked as below trade-agreement threshold but operationally helpful." | ||
} | } | ||
local function complianceObservedCell(args, channel, rowNumber, loc) | local function complianceObservedCell(args, channel, rowNumber, loc) | ||
if not loc then | if not loc then | ||
return "-" | return "-" | ||
end | 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) | |||
chance = nysaraVelaiAdjustedComplianceChance(args, loc, chance) | |||
local | local year = tonumber(args.year or args.psscyear) or 0 | ||
local | 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 | 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 | local profile = pick(complianceProfiles, seed, "profile") | ||
local outcome = pick(complianceOutcomes, seed, "outcome") | |||
if not profile then | |||
return "-" | |||
end | |||
local | local seq = (pctFromSeed(seed, "seq") % 20) + 1 | ||
local eventId | |||
if | 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 | end | ||
local advNote = adversaryNotePart(args, "compliance", seed, profile, loc) | |||
return "'''Compliance observed'''<br>" .. | |||
"<small>" .. eventId .. "</small><br>" .. | |||
"'''Profile:''' " .. profile.label .. "<br>" .. | |||
"<small>" .. profile.description .. "<br>" .. | |||
"'''Outcome:''' " .. outcome .. advNote .. "</small>" | |||
local advNote = adversaryNotePart(args, "compliance", seed, profile, loc) | |||
return "'''Compliance observed'''<br>" .. | |||
end | end | ||
| Line 1,517: | Line 1,316: | ||
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 | |||
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 | if not showLocalityColumn and not showTradeColumn and not showComplianceColumn and not showRebellionColumn then | ||
return body | |||
end | end | ||
local | local lines = mw.text.split(body, "\n", true) | ||
local | local out = {} | ||
local | local inTable = false | ||
local | local headerInserted = false | ||
local rowNumber = 0 | |||
local localityState = newLocalityAssignmentState(args, channel) | |||
if | 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, localityState) | |||
table.insert(out, line) | |||
if showLocalityColumn then | |||
table.insert(out, "| " .. rowLocalityText(loc)) | |||
table.insert(out, " | 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 | |||
table.insert(out, | inTable = false | ||
table.insert(out, line) | |||
else | |||
table.insert(out, line) | |||
end | end | ||
end | end | ||
return table.concat(out, "\n") | |||
end | end | ||
| Line 1,606: | Line 1,392: | ||
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 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, Bassaridian Mission, capped-locality, Nysara Velai concentration, and Temple Bank adversarial-node modifiers are applied where effective.") | |||
end | |||
local | local ifSupportNotice = imperialFederationSupportNotice(args) | ||
if ifSupportNotice ~= "" then | |||
table.insert(out, ifSupportNotice) | |||
table.insert(out, "") | |||
end | |||
local out | local missionNotice = bassaridianMissionsNotice(args) | ||
if missionNotice ~= "" then | |||
table.insert(out, missionNotice) | |||
table.insert(out, "") | |||
end | |||
if | local adversaryNotice = adversarialNodesNotice(args) | ||
if adversaryNotice ~= "" then | |||
end | table.insert(out, adversaryNotice) | ||
table.insert(out, "") | |||
end | |||
if | 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 | |||
end | |||
local | 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 | end | ||
local | 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 | 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 | end | ||
local | 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 | |||
end | 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 | 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.", | |||
"* '''Nysara Velai concentration''' assigns three potential locality deployments per section/channel by default: three War League rows, three Hatch Ministry rows, and three Temple Bank rows whenever at least four source rows are available.", | |||
if showLegend then | "* '''Selenarion cap''' allows only one potential locality deployment per section/channel: one War League row, one Hatch Ministry row, and one Temple Bank row at most.", | ||
"* '''Other capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Lenaia]]. Each may appear no more than once per section/channel.", | |||
"* '''Nysara Velai chance modifiers''' add +4 percentage points to unrest-alert likelihood and +5 percentage points to compliance-observation likelihood by default.", | |||
"* '''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, ritual-cargo abuse, medical-transfer exploitation, or Dispossessed-adjacent courier behavior.", | |||
"* '''Mirovia''', '''Saruq Thyrsa''', and '''Thal Lenaia''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.", | |||
"* '''Recurring adversarial nodes''' apply from 60/2/53 PSSC by default. Eligible rows may occasionally mention [[Khadir Veyran]] or [[Suyana Amaruq]] as suspected influence, but the notes are not guaranteed to appear.", | |||
"* '''Khadir Veyran''' notes are weighted toward broker factions, Bromian Ledger witness screens, anti-registry dock activity, cargo ambiguity, ledger correction, and trade-signoff counterweight language.", | |||
"* '''Suyana Amaruq''' notes are weighted toward courier screens, route barricades, false-pilgrim or guide-house movement, medical-transfer exploitation, and Black-Thread route-intelligence behavior.", | |||
"* '''Adversarial note frequency''' defaults to 35 percent for unrest alerts, 18 percent for compliance observations, and 22 percent for Temple Bank trade sign-offs after 60/2/53 PSSC.", | |||
"* '''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, Nysara Velai, Thal Lenaia, and Selenarion receive an additional +2 percentage points to compliance observations.", | |||
"* Use '''tradechance=100''', '''compliancechance=100''', and '''rebellionchance=100''' for testing.", | |||
"* Use '''nysaravelaitarget=3''' to change the default Nysara Velai section target.", | |||
"* Use '''nysaravelaioverfill=yes''' to allow Nysara Velai to exceed its section target after the priority rows are assigned.", | |||
"* Use '''nysaravelaiunrestboost=4''' and '''nysaravelaicomplianceboost=5''' to change the default Nysara Velai chance modifiers.", | |||
"* Use '''adversarynodes=no''' to disable named adversarial notes.", | |||
"* Use '''adversarychance=35''' to change the default unrest-alert adversarial note chance.", | |||
"* Use '''complianceadversarychance=18''' to change the default compliance-observation adversarial note chance.", | |||
"* Use '''tradeadversarychance=22''' to change the default Temple Bank trade-signoff adversarial note chance.", | |||
"* Use '''adversarystartyear=53''' and '''adversarystartday=121''' to change the default 60/2/53 PSSC start date for named adversarial-node tracking.", | |||
"* 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 | end | ||
| Line 1,772: | Line 1,553: | ||
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" | args.title = args.title or "Southern Strait Corridor Daily Operations" | ||
return p.makeTable({ args = args }) | 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.hatch = "no" | ||
args.missionary = "no" | args.missionary = "no" | ||
args.war = "yes" | args.war = "yes" | ||
args.title = args.title or "War League Southern Strait Daily Operations" | args.title = args.title or "War League Southern Strait Daily Operations" | ||
return p.makeTable({ args = args }) | 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.war = "no" | ||
args.missionary = "no" | args.missionary = "no" | ||
args.hatch = "yes" | args.hatch = "yes" | ||
args.title = args.title or "Hatch Ministry Southern Strait Daily Operations" | args.title = args.title or "Hatch Ministry Southern Strait Daily Operations" | ||
return p.makeTable({ args = args }) | 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.war = "no" | ||
args.hatch = "no" | args.hatch = "no" | ||
args.missionary = "yes" | args.missionary = "yes" | ||
args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations" | args.title = args.title or "Temple Bank Southern Strait Missionary Daily Operations" | ||
return p.makeTable({ args = args }) | return p.makeTable({ args = args }) | ||
end | end | ||
return p | return p | ||
Revision as of 14:40, 13 July 2026
Documentation for this module may be created at Module:SouthernStraitCorridorOps/doc
-- Module:SouthernStraitCorridorOps
-- Nysara Velai-focused revision
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 = "[[Nysara Velai]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
company = "[[Nysaran Terrace Farmers' Cooperative]]",
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 Lenaia]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Agriculture",
company = "[[Thal Lenaia 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 = "[[Selenarion]]",
region = "Eastern foothills of the [[Snowholme Range]]",
category = "Services",
company = "[[Selenarion 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"
}
}
-- Locality presence controls
-- AYS (Nysara Velai) is the principal focus: three rows per section by default.
-- QSE (Selenarion) is limited to one row per section.
-- MIR, SAR, and THQ also remain limited to one row per section.
local cappedPresenceLocalities = {
MIR = true,
SAR = true,
THQ = true,
QSE = true
}
local function findLocalityByCode(code)
code = tostring(code or "")
for _, loc in ipairs(southernTradeCities or {}) do
if tostring(loc.code or "") == code then
return loc
end
end
return nil
end
local function localityCount(state, locOrCode)
if not state or not state.counts then return 0 end
local code
if type(locOrCode) == "table" then
code = tostring(locOrCode.code or "")
else
code = tostring(locOrCode or "")
end
return tonumber(state.counts[code]) or 0
end
local function incrementLocalityCount(state, loc)
if not state or not loc then return end
local code = tostring(loc.code or "")
if code == "" then return end
state.counts[code] = (tonumber(state.counts[code]) or 0) + 1
end
local function nysaraVelaiPresenceTarget(args)
local v = tonumber(
args.nysaravelaitarget or
args.nysaraVelaiTarget or
args.nysaratarget or
args.nysaraTarget or
args.aystarget or
args.aysTarget
)
if v == nil then v = 3 end
if v < 0 then v = 0 end
return math.floor(v)
end
local function nysaraVelaiAllowOverTarget(args)
return yn(
args.nysaravelaioverfill or
args.nysaraVelaiOverfill or
args.nysaraoverfill or
args.nysaraOverfill or
args.aysoverfill or
args.aysOverfill,
false
)
end
local function buildPriorityLocalityPlan(args, channel)
local plan = {}
local target = nysaraVelaiPresenceTarget(args)
-- Nysara Velai always occupies the first priority slots. This ensures
-- that even a short source table remains focused on Nysara Velai.
for _ = 1, target do
table.insert(plan, "AYS")
end
-- Selenarion receives one potential deployment only after the Nysara
-- Velai target has been assigned.
table.insert(plan, "QSE")
return plan
end
local function newLocalityAssignmentState(args, channel)
return {
channel = tostring(channel or ""),
counts = {},
aysTarget = nysaraVelaiPresenceTarget(args),
aysAllowOverTarget = nysaraVelaiAllowOverTarget(args),
priorityPlan = buildPriorityLocalityPlan(args, channel)
}
end
local function canAssignLocality(args, loc, state)
if not loc then return false end
local code = tostring(loc.code or "")
if cappedPresenceLocalities[code] and localityCount(state, code) >= 1 then
return false
end
if code == "AYS" and state and not state.aysAllowOverTarget then
if localityCount(state, code) >= (tonumber(state.aysTarget) or 3) then
return false
end
end
return true
end
local function priorityLocalityForRow(state, rowNumber)
if not state or not state.priorityPlan then return nil end
local row = tonumber(rowNumber) or 0
local code = state.priorityPlan[row]
if not code then return nil end
return findLocalityByCode(code)
end
-- Row-level locality focus helper
local function rowLocalityData(args, channel, rowNumber, state)
if not southernTradeCities or #southernTradeCities == 0 then
return nil
end
state = state or newLocalityAssignmentState(args, channel)
-- The first four rows, when available, always contain the default
-- three Nysara Velai deployments and one Selenarion deployment.
local priorityLoc = priorityLocalityForRow(state, rowNumber)
if priorityLoc and canAssignLocality(args, priorityLoc, state) then
incrementLocalityCount(state, priorityLoc)
return priorityLoc
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 baseRow = tonumber(rowNumber) or 1
for attempt = 0, #southernTradeCities - 1 do
local idx = ((baseRow - 1 + offset + attempt) % #southernTradeCities) + 1
local loc = southernTradeCities[idx]
if canAssignLocality(args, loc, state) then
incrementLocalityCount(state, loc)
return loc
end
end
-- Once all caps and the Nysara target are filled, continue with the
-- genuinely uncapped secondary localities.
for _, loc in ipairs(southernTradeCities) do
local code = tostring(loc.code or "")
if not cappedPresenceLocalities[code] and code ~= "AYS" then
incrementLocalityCount(state, loc)
return loc
end
end
return nil
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 nysaraVelaiComplianceBoost(args)
local v = tonumber(
args.nysaravelaicomplianceboost or
args.nysaraVelaiComplianceBoost or
args.nysaracomplianceboost or
args.nysaraComplianceBoost or
args.ayscomplianceboost or
args.aysComplianceBoost
)
if v == nil then v = 5 end
return v
end
local function nysaraVelaiUnrestBoost(args)
local v = tonumber(
args.nysaravelaiunrestboost or
args.nysaraVelaiUnrestBoost or
args.nysaraunrestboost or
args.nysaraUnrestBoost or
args.aysunrestboost or
args.aysUnrestBoost
)
if v == nil then v = 4 end
return v
end
local function nysaraVelaiAdjustedComplianceChance(args, loc, baseChance)
local chance = clampChance(baseChance)
if loc and tostring(loc.code or "") == "AYS" then
chance = chance + nysaraVelaiComplianceBoost(args)
end
return clampChance(chance)
end
local function nysaraVelaiAdjustedUnrestChance(args, loc, baseChance)
local chance = clampChance(baseChance)
if loc and tostring(loc.code or "") == "AYS" then
chance = chance + nysaraVelaiUnrestBoost(args)
end
return clampChance(chance)
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]], [[Nysara Velai]], [[Thal Lenaia]], and [[Selenarion]].<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. Nysara Velai receives an additional +4 percentage points to unrest-alert likelihood and +5 percentage points to compliance-observation likelihood, reflecting intensified contestation and intensified Bassaridian administrative presence across its terrace-farming districts, upland provisioning routes, and eastern Snowholme approaches. " ..
"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
-- Recurring adversarial-node tracking
-- Temple Bank report context begins 60/2/53 PSSC.
-- 60/2/53 PSSC = day-of-year 121.
local ADVERSARY_NODES_START_YEAR = 53
local ADVERSARY_NODES_START_DAY = 121
local recurringAdversaries = {
KHADIR = {
name = "[[Khadir Veyran]]",
epithet = "Keeper of the Bromian Ledger",
localities = {
QSE = true,
AYS = true,
RUK = true,
HAY = true
},
notes = {
"Bromian Ledger intermediaries are suspected of delaying countersignatures without openly refusing Bassaridian review.",
"Veyran-linked witnesses preserve older Maritime Markets custom as a rival source of commercial legitimacy.",
"Broker-house clerks appear to be keeping disputed cargo identity ambiguous until General Port auditors withdraw.",
"Warehouse notaries tied to the Bromian Ledger are suspected of turning routine paperwork into a jurisdictional dispute."
},
complianceNotes = {
"Temple Bank auditors noted that this cooperation weakens the Bromian Ledger's claim that older broker custom remains the only trusted record.",
"Local registry compliance was treated as useful evidence against Veyran-linked delay tactics.",
"The observation was preserved as a small but meaningful challenge to Bromian Ledger influence in the pass-road economy."
},
tradeNotes = {
"Temple Bank clerks flagged the sign-off as a possible counterweight to Khadir Veyran's older broker-credit network.",
"The filing gives General Port auditors a lawful route around Bromian Ledger intermediaries.",
"The agreement was preserved as evidence that local commercial authority could be recorded without Veyran's countersignature."
}
},
SUYANA = {
name = "[[Suyana Amaruq]]",
epithet = "Black-Thread Suyana",
localities = {
QSE = true,
AYS = true,
THQ = true,
HAY = true
},
notes = {
"Black-Thread couriers are suspected of using ordinary household movement to mask route intelligence.",
"Suyana-linked runners may have tested night-road inspection gaps before disappearing into ordinary market traffic.",
"False pilgrims and guide-house messengers appear to have carried warning signals ahead of the audit lane.",
"Temple Bank officers treated the incident as route-intelligence obstruction rather than ordinary smuggling."
},
complianceNotes = {
"The observation was treated as a useful interruption of Black-Thread courier habits.",
"Licensed guides helped separate ordinary passage from Suyana-linked courier movement.",
"Temple Bank staff preserved the record as evidence that local mobility could be protected without unregistered courier screens."
},
tradeNotes = {
"Temple Bank notes warned that Black-Thread couriers may attempt to test the new trade window through false-route traffic.",
"The sign-off was paired with route-watch instructions because Suyana-linked couriers remained active near the upland approaches.",
"Mission clerks treated the filing as useful, but not sufficient to end Black-Thread influence over informal passage."
}
}
}
local function adversaryNodesEnabled(args)
return yn(
args.adversarynodes or
args.adversaryNodes or
args.namedadversaries or
args.namedAdversaries or
args.adversarynotes or
args.adversaryNotes,
true
)
end
local function adversaryNodesEffective(args)
if not adversaryNodesEnabled(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.adversarystartyear or args.adversaryStartYear) or ADVERSARY_NODES_START_YEAR
local startDay = tonumber(args.adversarystartday or args.adversaryStartDay) or ADVERSARY_NODES_START_DAY
return isOnOrAfterPSSCDate(year, day, startYear, startDay)
end
local function adversaryNoteChance(args, kind)
kind = tostring(kind or "")
if kind == "trade" then
return clampChance(tonumber(args.tradeadversarychance or args.tradeAdversaryChance) or 22)
elseif kind == "compliance" then
return clampChance(tonumber(args.complianceadversarychance or args.complianceAdversaryChance) or 18)
end
return clampChance(tonumber(args.adversarychance or args.adversaryChance or args.unrestadversarychance or args.unrestAdversaryChance) or 35)
end
local function localityAllowsAdversary(loc, adv)
if not adv then return false end
if not loc then return true end
local localities = adv.localities
if not localities then return true end
return localities[tostring(loc.code or "")] == true
end
local function candidateAdversaryCodes(profile, loc)
local out = {}
if profile and type(profile.adversaries) == "table" then
for _, code in ipairs(profile.adversaries) do
local adv = recurringAdversaries[code]
if adv and localityAllowsAdversary(loc, adv) then
table.insert(out, code)
end
end
end
if #out == 0 then
for code, adv in pairs(recurringAdversaries) do
if localityAllowsAdversary(loc, adv) then
table.insert(out, code)
end
end
end
return out
end
local function adversaryNotePart(args, kind, seed, profile, loc)
if not adversaryNodesEffective(args) then
return ""
end
local chance = adversaryNoteChance(args, kind)
local roll = pctFromSeed(seed, "adv-note-roll-" .. tostring(kind or "general")) + 1
if roll > chance then
return ""
end
local candidates = candidateAdversaryCodes(profile, loc)
local code = pick(candidates, seed, "adv-note-code-" .. tostring(kind or "general"))
local adv = recurringAdversaries[code]
if not adv then
return ""
end
local notes = adv.notes
if tostring(kind or "") == "trade" and adv.tradeNotes then
notes = adv.tradeNotes
elseif tostring(kind or "") == "compliance" and adv.complianceNotes then
notes = adv.complianceNotes
end
local note = pick(notes, seed, "adv-note-text-" .. tostring(kind or "general"))
if not note then
return ""
end
return "<br>'''Adversarial note:''' " .. adv.name .. " (''" .. adv.epithet .. "'') — " .. note
end
local function adversarialNodesNotice(args)
if not adversaryNodesEffective(args) then
return ""
end
return "'''Temple Bank adversarial-node tracking active:''' from 60/2/53 PSSC onward, occasional row-level notes may identify suspected influence associated with [[Khadir Veyran]] (''Keeper of the Bromian Ledger'') or [[Suyana Amaruq]] (''Black-Thread Suyana'').<br><small>These notes are seeded and probabilistic. They do not appear in every unrest, compliance, or trade-signoff row. Use '''adversarynodes=no''' to disable them, '''adversarychance=35''' to adjust unrest-note frequency, '''complianceadversarychance=18''' to adjust compliance-note frequency, and '''tradeadversarychance=22''' to adjust Temple Bank trade-signoff note frequency.</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(" ", " ")
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)
local advNote = adversaryNotePart(args, "trade", seed, nil, loc)
return "'''Trade sign-off'''<br>" ..
"<small>" .. tradeId .. "</small><br>" ..
(cult.label or "Cult sign-off recorded") .. "<br>" ..
"<small>Code: '''" .. signCode .. "'''<br>" .. outcome .. advNote .. "</small>"
end
-- Rebellion / corridor unrest row column
local rebellionProfiles = {
{
label = "Confederacy-adjacent courier screen",
description = "False-route couriers testing inspection gaps.",
adversaries = { "SUYANA" }
},
{
label = "Former Maritime Markets broker faction",
description = "Old broker-house retainers resisting General Port paperwork.",
adversaries = { "KHADIR" }
},
{
label = "Anti-registry dock syndicate",
description = "Warehouse intermediaries opposing voucher and ledger discipline.",
adversaries = { "KHADIR" }
},
{
label = "False-proclamation circle",
description = "Pamphlet readers circulating forged civic orders.",
adversaries = { "KHADIR", "SUYANA" }
},
{
label = "Route-barricade committee",
description = "Agitators obstructing lawful inspection lanes.",
adversaries = { "SUYANA" }
},
{
label = "Dispossessed-sympathetic quay cell",
description = "Waterfront cell copying Confederacy-style courier habits.",
adversaries = { "SUYANA" }
},
{
label = "Illicit ritual-cargo and medical-transfer exploiters",
description = "Handlers using illness, mourning, ritual cargo, or shrine protection to delay inspection.",
adversaries = { "SUYANA" }
},
{
label = "Bromian Ledger witness screen",
description = "Broker-linked witnesses preserving conflicting older records against General Port review.",
adversaries = { "KHADIR" }
}
}
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.",
"False medical-transfer exemption.",
"Unreconciled caravan lodging roll.",
"Guide-house witness contradiction.",
"Older Maritime Markets clearance invoked."
}
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.",
"Guide-license review opened.",
"Medical-transfer custody ledger corrected.",
"Broker-house witnesses summoned for countersignature.",
"Night-road registry temporarily frozen."
}
local rebellionRisks = {
"Low",
"Moderate",
"High",
"Contained",
"Escalating"
}
local rebellionAlertExcludedLocalities = {
MIR = true,
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)
chance = nysaraVelaiAdjustedUnrestChance(args, loc, chance)
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
local advNote = adversaryNotePart(args, "unrest", seed, profile, loc)
return "'''Unrest alert'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Trigger:''' " .. trigger .. "<br>" ..
"'''Risk:''' " .. risk .. "<br>" ..
"'''Response:''' " .. action .. advNote .. "</small>"
end
-- Compliance observed / minor positive row column
local complianceProfiles = {
{
label = "Inspection cooperation",
description = "Local carriers complied with inspection sequencing without requiring escalation.",
adversaries = { "SUYANA" }
},
{
label = "Ledger correction",
description = "Route clerks corrected incomplete or inconsistent paperwork before formal penalty.",
adversaries = { "KHADIR" }
},
{
label = "Crowd dispersal",
description = "Local stewards dispersed a gathering before it became an obstruction.",
adversaries = { "SUYANA" }
},
{
label = "Guide assistance",
description = "Licensed guides helped separate lawful traffic from suspect courier movement.",
adversaries = { "SUYANA" }
},
{
label = "Cargo separation",
description = "Bonded cargo was voluntarily separated for review without disrupting ordinary passage.",
adversaries = { "KHADIR" }
},
{
label = "Rumor-control compliance",
description = "Temple notices were accepted locally and hostile rumors lost momentum.",
adversaries = { "KHADIR", "SUYANA" }
},
{
label = "Privateer signal compliance",
description = "Vessels accepted stand-off or hold-and-release signals without resistance.",
adversaries = { "SUYANA" }
},
{
label = "Registry cooperation",
description = "Local registrars preserved names, seals, and witness records for later review.",
adversaries = { "KHADIR" }
},
{
label = "Medical-transfer ledger cooperation",
description = "Clinic escorts preserved patient custody records without accepting false sacred exemptions.",
adversaries = { "SUYANA" }
}
}
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)
chance = nysaraVelaiAdjustedComplianceChance(args, loc, chance)
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
local advNote = adversaryNotePart(args, "compliance", seed, profile, loc)
return "'''Compliance observed'''<br>" ..
"<small>" .. eventId .. "</small><br>" ..
"'''Profile:''' " .. profile.label .. "<br>" ..
"<small>" .. profile.description .. "<br>" ..
"'''Outcome:''' " .. outcome .. advNote .. "</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
local localityState = newLocalityAssignmentState(args, channel)
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, localityState)
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, Bassaridian Mission, capped-locality, Nysara Velai concentration, and Temple Bank adversarial-node 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
local adversaryNotice = adversarialNodesNotice(args)
if adversaryNotice ~= "" then
table.insert(out, adversaryNotice)
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.",
"* '''Nysara Velai concentration''' assigns three potential locality deployments per section/channel by default: three War League rows, three Hatch Ministry rows, and three Temple Bank rows whenever at least four source rows are available.",
"* '''Selenarion cap''' allows only one potential locality deployment per section/channel: one War League row, one Hatch Ministry row, and one Temple Bank row at most.",
"* '''Other capped presence localities''' are [[Mirovia]], [[Saruq Thyrsa]], and [[Thal Lenaia]]. Each may appear no more than once per section/channel.",
"* '''Nysara Velai chance modifiers''' add +4 percentage points to unrest-alert likelihood and +5 percentage points to compliance-observation likelihood by default.",
"* '''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, ritual-cargo abuse, medical-transfer exploitation, or Dispossessed-adjacent courier behavior.",
"* '''Mirovia''', '''Saruq Thyrsa''', and '''Thal Lenaia''' are hard-excluded from Rebellion Alert results; their Rebellion Alert cells always display '''-''', even when '''rebellionchance=100''' is used for testing.",
"* '''Recurring adversarial nodes''' apply from 60/2/53 PSSC by default. Eligible rows may occasionally mention [[Khadir Veyran]] or [[Suyana Amaruq]] as suspected influence, but the notes are not guaranteed to appear.",
"* '''Khadir Veyran''' notes are weighted toward broker factions, Bromian Ledger witness screens, anti-registry dock activity, cargo ambiguity, ledger correction, and trade-signoff counterweight language.",
"* '''Suyana Amaruq''' notes are weighted toward courier screens, route barricades, false-pilgrim or guide-house movement, medical-transfer exploitation, and Black-Thread route-intelligence behavior.",
"* '''Adversarial note frequency''' defaults to 35 percent for unrest alerts, 18 percent for compliance observations, and 22 percent for Temple Bank trade sign-offs after 60/2/53 PSSC.",
"* '''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, Nysara Velai, Thal Lenaia, and Selenarion receive an additional +2 percentage points to compliance observations.",
"* Use '''tradechance=100''', '''compliancechance=100''', and '''rebellionchance=100''' for testing.",
"* Use '''nysaravelaitarget=3''' to change the default Nysara Velai section target.",
"* Use '''nysaravelaioverfill=yes''' to allow Nysara Velai to exceed its section target after the priority rows are assigned.",
"* Use '''nysaravelaiunrestboost=4''' and '''nysaravelaicomplianceboost=5''' to change the default Nysara Velai chance modifiers.",
"* Use '''adversarynodes=no''' to disable named adversarial notes.",
"* Use '''adversarychance=35''' to change the default unrest-alert adversarial note chance.",
"* Use '''complianceadversarychance=18''' to change the default compliance-observation adversarial note chance.",
"* Use '''tradeadversarychance=22''' to change the default Temple Bank trade-signoff adversarial note chance.",
"* Use '''adversarystartyear=53''' and '''adversarystartday=121''' to change the default 60/2/53 PSSC start date for named adversarial-node tracking.",
"* 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