Module:WarLeagueOperationsPlannerBVR
From MicrasWiki
Jump to navigationJump to search
Documentation for this module may be created at Module:WarLeagueOperationsPlannerBVR/doc
-- Module:WarLeagueOperationsPlannerBVR
-- Interactive/manual-input operations planner for the Bassaridian War League.
--
-- Designed as a companion to Module:WarLeagueDivisionOpsBVR.
-- The older module remains useful as the deterministic daily operations ledger;
-- this module generates ONE deliberately configured operation at a time.
--
-- Most fields accept "auto". Editors can therefore lock only the choices they
-- care about and allow the remaining details to be generated deterministically.
--
-- Suggested wrapper:
-- Template:WarLeague Operations Planner
--
-- Main entry points:
-- {{#invoke:WarLeagueOperationsPlannerBVR|operation|...}}
-- {{#invoke:WarLeagueOperationsPlannerBVR|options}}
-- {{#invoke:WarLeagueOperationsPlannerBVR|compact|...}}
local p = {}
--------------------------------------------------------------------------------
-- A. BASIC HELPERS
--------------------------------------------------------------------------------
local function trim(v)
if v == nil then return "" end
return mw.text.trim(tostring(v))
end
local function lower(v)
return mw.ustring.lower(trim(v))
end
local function norm(v)
local s = lower(v)
s = s:gsub("[_%-]+", " ")
s = s:gsub("%s+", " ")
return s
end
local function isAuto(v)
local s = norm(v)
return s == "" or s == "auto" or s == "automatic" or s == "random"
end
local function clamp(n, lo, hi)
n = tonumber(n) or lo
if n < lo then return lo end
if n > hi then return hi end
return n
end
local function copyArray(t)
local out = {}
for i, v in ipairs(t or {}) do out[i] = v end
return out
end
local function contains(t, value)
for _, v in ipairs(t or {}) do
if v == value then return true end
end
return false
end
local function pick(t)
if not t or #t == 0 then return nil end
return t[math.random(#t)]
end
local function pickWeighted(list)
if not list or #list == 0 then return nil end
local total = 0
for _, it in ipairs(list) do total = total + (tonumber(it.weight) or 1) end
if total <= 0 then return list[1] end
local r = math.random() * total
local acc = 0
for _, it in ipairs(list) do
acc = acc + (tonumber(it.weight) or 1)
if r <= acc then return it end
end
return list[#list]
end
local function weightedKey(map)
local list = {}
for key, weight in pairs(map or {}) do
table.insert(list, { key = key, weight = weight })
end
local item = pickWeighted(list)
return item and item.key or nil
end
local function getArgs(frame)
local args = {}
if frame and frame.getParent then
local parent = frame:getParent()
if parent and parent.args then
for k, v in pairs(parent.args) do
if trim(v) ~= "" then args[k] = trim(v) end
end
end
end
if frame and frame.args then
for k, v in pairs(frame.args) do
if trim(v) ~= "" then args[k] = trim(v) end
end
end
return args
end
local function safeUserText(v)
local s = trim(v)
if s == "" then return "" end
if mw.text and mw.text.nowiki then return mw.text.nowiki(s) end
return s:gsub("|", "|"):gsub("<", "<"):gsub(">", ">")
end
local function bulletList(items)
local clean = {}
for _, item in ipairs(items or {}) do
if item and trim(item) ~= "" then table.insert(clean, item) end
end
if #clean == 0 then return "—" end
return "<ul><li>" .. table.concat(clean, "</li><li>") .. "</li></ul>"
end
local function joinComma(items)
local clean = {}
for _, item in ipairs(items or {}) do
if item and trim(item) ~= "" then table.insert(clean, item) end
end
return table.concat(clean, ", ")
end
local function yes(v)
local s = norm(v)
return s == "yes" or s == "y" or s == "true" or s == "1" or s == "on"
end
--------------------------------------------------------------------------------
-- B. PSSC DATE
--------------------------------------------------------------------------------
local function getPSSCDate(args)
args = args or {}
local overrideYear = tonumber(args.year or args.psscyear)
local overrideDay = tonumber(args.day or args.dayofyear)
if overrideYear and overrideDay then
overrideDay = clamp(overrideDay, 1, 183)
local month, dayOfMonth
if overrideDay <= 61 then
month, dayOfMonth = 1, overrideDay
elseif overrideDay <= 122 then
month, dayOfMonth = 2, overrideDay - 61
else
month, dayOfMonth = 3, overrideDay - 122
end
return {
text = string.format("%d/%d/%d PSSC", dayOfMonth, month, overrideYear),
year = overrideYear,
dayOfYear = overrideDay,
month = month,
dom = dayOfMonth,
}
end
local startDate = os.time({year = 1999, month = 8, day = 6})
local secondsInDay = 86400
local daysPerYear = 183
local now = os.time()
local totalDays = math.floor((now - startDate) / secondsInDay)
local psscYear = math.floor(totalDays / daysPerYear)
local dayOfYear = (totalDays % daysPerYear) + 1
local month, dayOfMonth
if dayOfYear <= 61 then
month, dayOfMonth = 1, dayOfYear
elseif dayOfYear <= 122 then
month, dayOfMonth = 2, dayOfYear - 61
else
month, dayOfMonth = 3, dayOfYear - 122
end
return {
text = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear),
year = psscYear,
dayOfYear = dayOfYear,
month = month,
dom = dayOfMonth,
}
end
--------------------------------------------------------------------------------
-- C. DIVISIONS
--------------------------------------------------------------------------------
local DIVISIONS = {
VAE = {
code = "VAE", name = "Vaeringheim Division", link = "[[Vaeringheim Division]]", weight = 7,
theatres = {"morovian_core", "general_port"},
doctrine = "Core corridor security: port approaches, canals, industrial continuity, infrastructure protection and high-population stability.",
},
ALP = {
code = "ALP", name = "Alpazkigz Division", link = "[[Alpazkigz Division]]", weight = 5,
theatres = {"alpazkigz", "morovian_core"},
doctrine = "Western periphery and highlands: shrine security, caravan routes, insurgent interdiction, Noctic-linked custody support and hazard response.",
},
ODF = {
code = "ODF", name = "Odiferia Division", link = "[[Odiferia Division]]", weight = 4,
theatres = {"odiferia", "morovian_core"},
doctrine = "Southern wetlands and tribal interface: swamp corridors, lowland stability, rural enforcement support and relief movement.",
},
HAT = {
code = "HAT", name = "Hatch Ministry Division", link = "[[Hatch Ministry Division]]", weight = 5,
theatres = {"haifan", "southern_strait", "general_port"},
doctrine = "Maritime corridor enforcement: escort corridors, VBSS, interdiction, convoy discipline, prize evidence and lawful straits enforcement.",
},
NSJ = {
code = "NSJ", name = "New South Jangsong Division", link = "[[New South Jangsong Division]]", weight = 5,
theatres = {"new_south_jangsong", "normark"},
doctrine = "Northern straits and frontier belt: raids, weather shocks, export-security enforcement, route defence and sustained frontier operations.",
},
HFA = {
code = "HFA", name = "Haifan Bassaridia Division", link = "[[Haifan Bassaridia Division]]", weight = 5,
theatres = {"haifan", "southern_strait"},
doctrine = "Southern straits urban and littoral integration: port-city stability, coastal route security and anti-piracy legacy zones.",
},
BNO = {
code = "BNO", name = "Bassaridian Normark Division", link = "[[Bassaridian Normark Division]]", weight = 3,
theatres = {"normark", "new_south_jangsong"},
doctrine = "Far-northern sustainment: sparse-settlement corridor security, consolidation, weather resilience and long-range logistics.",
},
OUR = {
code = "OUR", name = "Ouriana Division", link = "[[Ouriana Division]]", weight = 3,
theatres = {"ouriana"},
doctrine = "Central Keltian route governance: settlement security, containment, road and rail continuity and post-campaign civil stabilisation.",
},
CKD = {
code = "CKD", name = "Council of Kings Division", link = "[[Council of Kings Division]]", weight = 3,
theatres = {"eastern_caledonia", "southern_strait", "ouriana"},
doctrine = "Executive joint tasking: dependency stabilisation, strategic corridor integration, high-risk joint operations and direct Council missions.",
},
}
local DIVISION_ALIASES = {
["vae"] = "VAE", ["vaeringheim"] = "VAE", ["vaeringheim division"] = "VAE",
["alp"] = "ALP", ["alpazkigz"] = "ALP", ["alpazkigz division"] = "ALP",
["odf"] = "ODF", ["odiferia"] = "ODF", ["odiferia division"] = "ODF",
["hat"] = "HAT", ["hatch"] = "HAT", ["hatch ministry"] = "HAT", ["hatch ministry division"] = "HAT",
["nsj"] = "NSJ", ["new south jangsong"] = "NSJ", ["new south jangsong division"] = "NSJ",
["hfa"] = "HFA", ["haifan"] = "HFA", ["haifan bassaridia"] = "HFA", ["haifan bassaridia division"] = "HFA",
["bno"] = "BNO", ["bnm"] = "BNO", ["normark"] = "BNO", ["bassaridian normark"] = "BNO", ["bassaridian normark division"] = "BNO",
["our"] = "OUR", ["ouriana"] = "OUR", ["ouriana division"] = "OUR",
["ckd"] = "CKD", ["council of kings"] = "CKD", ["council of kings division"] = "CKD",
}
--------------------------------------------------------------------------------
-- D. THEATRES / NODES
--------------------------------------------------------------------------------
local THEATRES = {
morovian_core = {
key = "morovian_core", label = "Lake Morovia core", weight = 8,
divisions = {VAE=8, ALP=2, ODF=2},
nodes = {"Vaeringheim", "Luminaria", "Serena", "Symphonara", "Aurelia", "Sylvapolis", "Lunalis Sancta", "Delphica", "Catonis Atrium", "Ferrum Citadel", "Nexa"},
tags = {urban=true, lake=true, rail=true, commercial=true},
doctrine = "Protect throughput, canals, rail approaches, industrial continuity and densely populated civic nodes while keeping ordinary Port and municipal administration functioning.",
},
general_port = {
key = "general_port", label = "General Port of Lake Morovia", weight = 4,
divisions = {VAE=7, HAT=4, CKD=2},
nodes = {"General Port of Lake Morovia", "Vaeringheim", "Somniumpolis"},
tags = {urban=true, lake=true, port=true, rail=true, commercial=true, orbital=true},
doctrine = "Maintain port-state control, registry integrity, cargo custody, strategic infrastructure and launch/logistics continuity without unnecessarily interrupting lawful trade.",
},
alpazkigz = {
key = "alpazkigz", label = "Alpazkigz and western Morovian highlands", weight = 5,
divisions = {ALP=9, VAE=1},
nodes = {"The Alpazkigz", "Pyralis", "Saluria", "Acheron", "Koinonía", "Aureum", "Aetherium", "Erythros", "Gloom Forest of Perpetual Autumn", "Northern and Western Morovian Highlands", "Alperkin Coastal Woodlands"},
tags = {highland=true, sacred=true, tribal=true, noctic=true, wilderness=true},
doctrine = "Respect Alperkin sacred-space and customary constraints while securing caravan routes, settlements, clinics, Noctic custody chains and highland approaches.",
},
odiferia = {
key = "odiferia", label = "Odiferia and southern wetlands", weight = 4,
divisions = {ODF=9, ALP=2},
nodes = {"Odiferia", "Somniumpolis", "Southern Odiferae Wetlands", "River Noctis approaches"},
tags = {wetland=true, tribal=true, noctic=true, lake=true},
doctrine = "Use light, distributed forces suited to swamp and canal movement; separate military security from tribal, health, temple and technical authority.",
},
new_south_jangsong = {
key = "new_south_jangsong", label = "New South Jangsong", weight = 5,
divisions = {NSJ=9, BNO=2},
nodes = {"Skýrophos", "Bjornopolis", "Aegirheim", "Norsolyra", "Thorsalon", "Pelagia", "Myrene", "Thyrea", "Ephyra", "Halicarn"},
tags = {frontier=true, maritime=true, rail=true, urban=true, cold=true},
doctrine = "Hold northern-strain corridors against raids, sabotage and weather disruption while preserving export-control and civil supply routes.",
},
haifan = {
key = "haifan", label = "Haifan Bassaridia", weight = 5,
divisions = {HFA=8, HAT=5, CKD=1},
nodes = {"Keybir-Aviv", "Tel-Amin", "Diamandis", "Jogi", "Lewisburg", "Thermosalem", "Akróstadium", "Sufriya", "Lykopolis", "Mylecia"},
tags = {maritime=true, port=true, urban=true, strait=true, commercial=true},
doctrine = "Secure port cities, ferries, coastal routes and anti-piracy legacy zones while maintaining lawful civilian and commercial movement.",
},
normark = {
key = "normark", label = "Bassaridian Normark", weight = 3,
divisions = {BNO=8, NSJ=3},
nodes = {"Ardclach", "Riddersborg"},
tags = {frontier=true, cold=true, sparse=true, maritime=true},
doctrine = "Favor sustainment, route reliability and weather resilience over dense occupation; maintain protected links between sparse holdings.",
},
ouriana = {
key = "ouriana", label = "Ouriana and central Keltian corridor", weight = 3,
divisions = {OUR=8, CKD=3},
nodes = {"Bashkim", "Ourid", "Tonar"},
tags = {frontier=true, inland=true, rail=true, dependency=true},
doctrine = "Secure post-campaign settlement corridors, contain destabilising cells and preserve the distinction between security presence and ordinary civil administration.",
},
eastern_caledonia = {
key = "eastern_caledonia", label = "Eastern Caledonian Highlands", weight = 4,
divisions = {CKD=10},
nodes = {"Fanghorn", "Eikbu", "Galvø", "Skøda", "Hamarrfell", "Sårensby", "Slevik", "Sjøsborg", "Storesund", "Notranskja", "Kaledonija", "Krlsgorod"},
tags = {highland=true, cold=true, rail=true, dependency=true, fjord=true},
doctrine = "CKD-exclusive dependency and corridor tasking: protect rail/highland approaches, negotiation sites, local institutions and integration works under direct Council oversight.",
},
southern_strait = {
key = "southern_strait", label = "Southern Strait corridor / Lacaran Hodos", weight = 6,
divisions = {CKD=8, HAT=4, HFA=3},
nodes = {"Mylecia", "Nysara Velai", "Thal Lenaia", "Selenarion", "Hayr Chaskal", "Saruq Thyrsa", "Rumi-Kefaya", "Mirovia", "Lacaran Hodos", "Lacaran Hodos Divan"},
tags = {strait=true, corridor=true, rail=true, road=true, highland=true, commercial=true, dependency=true},
doctrine = "Enforce the Straits Conventions through lawful passage control, registry and evidence discipline, White-Lane protection, missionary safety and route security while preserving sacred spaces, local civic authorities and customary access.",
},
}
local THEATRE_ALIASES = {
["core"] = "morovian_core", ["morovia"] = "morovian_core", ["lake morovia"] = "morovian_core", ["lake morovia core"] = "morovian_core", ["morovian core"] = "morovian_core",
["port"] = "general_port", ["general port"] = "general_port", ["general port of lake morovia"] = "general_port",
["alpazkigz"] = "alpazkigz", ["alperkin"] = "alpazkigz", ["western highlands"] = "alpazkigz",
["odiferia"] = "odiferia", ["wetlands"] = "odiferia", ["southern wetlands"] = "odiferia",
["nsj"] = "new_south_jangsong", ["new south jangsong"] = "new_south_jangsong", ["jangsong"] = "new_south_jangsong",
["haifan"] = "haifan", ["haifan bassaridia"] = "haifan",
["normark"] = "normark", ["bassaridian normark"] = "normark",
["ouriana"] = "ouriana", ["central keltia"] = "ouriana",
["caledonia"] = "eastern_caledonia", ["eastern caledonia"] = "eastern_caledonia", ["eastern caledonian highlands"] = "eastern_caledonia",
["southern strait"] = "southern_strait", ["southern straits"] = "southern_strait", ["southernstrait"] = "southern_strait", ["lacaran hodos"] = "southern_strait", ["corridor"] = "southern_strait",
}
local NODE_TO_THEATRE = {}
for key, th in pairs(THEATRES) do
for _, node in ipairs(th.nodes) do NODE_TO_THEATRE[norm(node)] = key end
end
local function theatreFromInput(v)
if isAuto(v) then return nil end
local n = norm(v)
local key = THEATRE_ALIASES[n]
if key then return THEATRES[key] end
for k, th in pairs(THEATRES) do
if norm(k) == n or norm(th.label) == n then return th end
end
return nil
end
local function divisionFromInput(v)
if isAuto(v) then return nil end
local key = DIVISION_ALIASES[norm(v)] or trim(v):upper()
return DIVISIONS[key]
end
local function inferTheatreFromNode(node)
local key = NODE_TO_THEATRE[norm(node)]
return key and THEATRES[key] or nil
end
--------------------------------------------------------------------------------
-- E. MISSIONS, THREATS, POSTURES
--------------------------------------------------------------------------------
local MISSIONS = {
corridor_security = {label="Corridor security", primary="ground", base=2, tags={corridor=true, rail=true, road=true}},
urban_stabilization = {label="Urban stabilization", primary="ground", base=2, tags={urban=true}},
anti_smuggling = {label="Anti-smuggling interdiction / contraband seizure", primary="joint", base=2, tags={commercial=true, strait=true, noctic=true}},
infrastructure_protection = {label="Infrastructure protection", primary="ground", base=2, tags={rail=true, road=true, port=true, water=true}},
counter_insurgency = {label="Counter-insurgency / cell disruption", primary="ground", base=3, tags={frontier=true, highland=true, urban=true}},
hazard_response = {label="Hazard response / evacuation / route reopening", primary="joint", base=2, tags={wilderness=true, cold=true, wetland=true}},
convoy_escort = {label="Convoy escort / protected transfer", primary="ground", base=1, tags={road=true, rail=true, commercial=true}},
water_security = {label="Aqueduct / pump-house / water-relief security", primary="ground", base=2, tags={water=true, urban=true}},
market_security = {label="Market and ledger security", primary="ground", base=2, tags={commercial=true, urban=true, port=true}},
sacred_route = {label="Sacred-route / shrine approach protection", primary="ground", base=1, tags={sacred=true, tribal=true, corridor=true}},
noctic_custody = {label="Noctic-Rabrev custody / clinic / ledger support", primary="joint", base=2, tags={noctic=true, wetland=true, commercial=true}},
maritime_escort = {label="Maritime escort / lake or strait patrol", primary="naval", base=1, tags={maritime=true, lake=true, strait=true}},
vbss = {label="VBSS / boarding / contraband inspection", primary="naval", base=2, tags={maritime=true, port=true, strait=true}},
blockade_warning = {label="Blockade-warning / selective cordon", primary="naval", base=3, tags={strait=true, maritime=true}},
harbor_security = {label="Harbor / lock-gate security", primary="naval", base=2, tags={port=true, lake=true, maritime=true}},
white_lane = {label="White-Lane humanitarian / essential-goods protection", primary="joint", base=2, tags={strait=true, corridor=true, humanitarian=true}},
missionary_escort = {label="Missionary and pilgrimage-route escort", primary="joint", base=1, tags={sacred=true, corridor=true, strait=true}},
prize_transfer = {label="Prize-court evidence / detainee transfer escort", primary="joint", base=2, tags={strait=true, port=true, commercial=true}},
dependency_stabilization = {label="Dependency stabilization / protected integration", primary="joint", base=2, tags={dependency=true, highland=true, frontier=true}},
route_clearance = {label="Route clearance / engineering security", primary="ground", base=2, tags={road=true, rail=true, highland=true}},
lacaran_hodos = {label="Lacaran Hodos rail-road protection", primary="joint", base=2, tags={corridor=true, rail=true, road=true, highland=true}},
southern_strait_compliance = {label="Southern Strait conventions-compliance operation", primary="joint", base=2, tags={strait=true, corridor=true, commercial=true}},
confederacy_interdiction = {label="Confederacy-linked network interdiction", primary="joint", base=3, tags={frontier=true, strait=true, corridor=true}},
isr = {label="ISR / route scanning / signals overwatch", primary="air", base=1, tags={frontier=true, highland=true, maritime=true}},
medevac = {label="Rapid extraction / MEDEVAC lift", primary="air", base=2, tags={humanitarian=true, wilderness=true, urban=true}},
show_of_force = {label="Controlled show-of-force / deterrence package", primary="air", base=2, tags={frontier=true, strait=true, urban=true}},
eidolan_containment = {label="Somniant Eidolan / anomalous containment support", primary="joint", base=4, tags={anomalous=true, noctic=true, wilderness=true}},
training = {label="Readiness exercise / Games-of-the-Standards style field drill", primary="joint", base=1, tags={training=true, rail=true, road=true}},
}
local MISSION_ALIASES = {
["corridor"]="corridor_security", ["corridor security"]="corridor_security",
["urban"]="urban_stabilization", ["urban stabilization"]="urban_stabilization", ["urban stabilisation"]="urban_stabilization",
["smuggling"]="anti_smuggling", ["anti smuggling"]="anti_smuggling", ["anti-smuggling"]="anti_smuggling",
["infrastructure"]="infrastructure_protection", ["infrastructure protection"]="infrastructure_protection",
["counter insurgency"]="counter_insurgency", ["counter-insurgency"]="counter_insurgency", ["coin"]="counter_insurgency",
["hazard"]="hazard_response", ["hazard response"]="hazard_response", ["disaster"]="hazard_response",
["convoy"]="convoy_escort", ["convoy escort"]="convoy_escort",
["water"]="water_security", ["water security"]="water_security", ["aqueduct"]="water_security",
["market"]="market_security", ["market security"]="market_security", ["ledger"]="market_security",
["sacred"]="sacred_route", ["sacred route"]="sacred_route", ["shrine"]="sacred_route",
["noctic"]="noctic_custody", ["noctic custody"]="noctic_custody", ["noctic-rabrev"]="noctic_custody",
["maritime"]="maritime_escort", ["maritime escort"]="maritime_escort", ["lake patrol"]="maritime_escort",
["vbss"]="vbss", ["boarding"]="vbss",
["blockade"]="blockade_warning", ["blockade warning"]="blockade_warning",
["harbor"]="harbor_security", ["harbour"]="harbor_security", ["harbor security"]="harbor_security",
["white lane"]="white_lane", ["white-lane"]="white_lane", ["humanitarian"]="white_lane",
["missionary"]="missionary_escort", ["missionary escort"]="missionary_escort", ["pilgrimage"]="missionary_escort",
["prize"]="prize_transfer", ["prize court"]="prize_transfer", ["prize-court"]="prize_transfer",
["dependency"]="dependency_stabilization", ["dependency stabilization"]="dependency_stabilization", ["dependency stabilisation"]="dependency_stabilization",
["route clearance"]="route_clearance", ["engineering"]="route_clearance",
["lacaran hodos"]="lacaran_hodos", ["hodos"]="lacaran_hodos",
["southern strait"]="southern_strait_compliance", ["straits compliance"]="southern_strait_compliance", ["compliance"]="southern_strait_compliance",
["confederacy"]="confederacy_interdiction", ["confederacy interdiction"]="confederacy_interdiction",
["isr"]="isr", ["recon"]="isr", ["reconnaissance"]="isr",
["medevac"]="medevac", ["extraction"]="medevac",
["show of force"]="show_of_force", ["deterrence"]="show_of_force",
["eidolan"]="eidolan_containment", ["anomalous"]="eidolan_containment",
["training"]="training", ["exercise"]="training", ["war games"]="training",
}
local THREATS = {
routine = {label="Routine / no specific hostile actor", score=0},
smuggling = {label="Smuggling / cargo-identity laundering", score=1},
piracy = {label="Pirate or irregular maritime activity", score=2},
insurgency = {label="Insurgent / separatist cell", score=3},
confederacy = {label="Confederacy-linked or adjacent network", score=3},
noctic = {label="Noctic-Rabrev trafficking / counterfeit nocticine", score=2},
vampirism = {label="Noctic Vampirism outbreak / panic", score=3},
sabotage = {label="Infrastructure sabotage", score=3},
forged_docs = {label="Forged manifests / false seals / courier-paper network", score=2},
civil_unrest = {label="Civil unrest / hostile crowd pressure", score=2},
weather = {label="Severe weather / environmental disruption", score=2},
infrastructure = {label="Infrastructure failure / route collapse", score=2},
eidolan = {label="Somniant Eidolan / anomalous manifestation", score=4},
}
local THREAT_ALIASES = {
["none"]="routine", ["routine"]="routine",
["smuggling"]="smuggling", ["contraband"]="smuggling",
["piracy"]="piracy", ["pirates"]="piracy",
["insurgency"]="insurgency", ["rebellion"]="insurgency", ["separatists"]="insurgency",
["confederacy"]="confederacy", ["dispossessed"]="confederacy",
["noctic"]="noctic", ["noctic trafficking"]="noctic", ["counterfeit nocticine"]="noctic",
["vampirism"]="vampirism", ["noctic vampirism"]="vampirism",
["sabotage"]="sabotage",
["forged docs"]="forged_docs", ["forged documents"]="forged_docs", ["false seals"]="forged_docs",
["civil unrest"]="civil_unrest", ["crowd"]="civil_unrest",
["weather"]="weather", ["storm"]="weather",
["infrastructure"]="infrastructure", ["infrastructure failure"]="infrastructure",
["eidolan"]="eidolan", ["anomaly"]="eidolan", ["anomalous"]="eidolan",
}
local POSTURES = {
restrained = {label="Restrained", force=-1, risk=-1, desc="Minimum necessary force; escort, evidence preservation and de-escalation favored."},
standard = {label="Standard", force=0, risk=0, desc="Normal divisional posture under standing constitutional and corridor rules."},
assertive = {label="Assertive", force=1, risk=1, desc="Reinforced presence, broader screening and faster reserve commitment."},
emergency = {label="Emergency", force=2, risk=2, desc="Surge posture for immediate life-safety, strategic infrastructure or national-security risk."},
}
local STAGES = {
planning = "Planning / tasking",
active = "Active operation",
after_action = "After-action review",
exercise = "Training / exercise",
}
local function missionFromInput(v)
if isAuto(v) then return nil, nil end
local n = norm(v)
local key = MISSION_ALIASES[n]
if key and MISSIONS[key] then return key, MISSIONS[key] end
for k, m in pairs(MISSIONS) do
if norm(k) == n or norm(m.label) == n then return k, m end
end
return nil, nil
end
local function threatFromInput(v)
if isAuto(v) then return nil, nil end
local key = THREAT_ALIASES[norm(v)]
if key then return key, THREATS[key] end
return nil, nil
end
--------------------------------------------------------------------------------
-- F. AUTO-SELECTION LOGIC
--------------------------------------------------------------------------------
local function chooseTheatre(div)
local weighted = {}
for _, key in ipairs(div and div.theatres or {}) do
local th = THEATRES[key]
if th then table.insert(weighted, {key=key, weight=th.weight or 1}) end
end
if #weighted == 0 then
for key, th in pairs(THEATRES) do table.insert(weighted, {key=key, weight=th.weight or 1}) end
end
local item = pickWeighted(weighted)
return item and THEATRES[item.key] or THEATRES.morovian_core
end
local function chooseDivision(theatre)
local map = theatre and theatre.divisions or nil
local key = weightedKey(map)
if key and DIVISIONS[key] then return DIVISIONS[key] end
local pool = {}
for _, div in pairs(DIVISIONS) do table.insert(pool, {div=div, weight=div.weight or 1}) end
local item = pickWeighted(pool)
return item and item.div or DIVISIONS.VAE
end
local function missionWeightForTheatre(mission, theatre)
local w = 1
if not mission or not theatre then return w end
for tag, _ in pairs(mission.tags or {}) do
if theatre.tags and theatre.tags[tag] then w = w + 4 end
end
if theatre.key == "southern_strait" then
local special = {
southern_strait_compliance=10, lacaran_hodos=9, white_lane=7,
missionary_escort=5, prize_transfer=5, confederacy_interdiction=6,
route_clearance=5, dependency_stabilization=5, vbss=4,
}
w = w + (special[mission._key or ""] or 0)
elseif theatre.key == "eastern_caledonia" then
local special = {dependency_stabilization=10, route_clearance=6, corridor_security=6, hazard_response=5, isr=4}
w = w + (special[mission._key or ""] or 0)
elseif theatre.key == "alpazkigz" then
local special = {sacred_route=7, noctic_custody=6, hazard_response=6, counter_insurgency=4, convoy_escort=4}
w = w + (special[mission._key or ""] or 0)
elseif theatre.key == "odiferia" then
local special = {noctic_custody=8, hazard_response=6, corridor_security=5, water_security=4, medevac=4}
w = w + (special[mission._key or ""] or 0)
elseif theatre.key == "general_port" then
local special = {market_security=8, infrastructure_protection=8, harbor_security=7, anti_smuggling=7, water_security=5}
w = w + (special[mission._key or ""] or 0)
end
return w
end
local function chooseMission(theatre, stage)
local list = {}
for key, mission in pairs(MISSIONS) do
mission._key = key
local w = missionWeightForTheatre(mission, theatre)
if stage == "exercise" then
if key == "training" then w = w + 20 else w = math.max(1, math.floor(w / 3)) end
elseif key == "training" then
w = 1
end
table.insert(list, {key=key, weight=w})
end
local item = pickWeighted(list)
local key = item and item.key or "corridor_security"
return key, MISSIONS[key]
end
local function chooseThreat(theatre, mission)
local weights = {
routine=5, smuggling=4, piracy=2, insurgency=3, confederacy=2,
noctic=2, vampirism=1, sabotage=2, forged_docs=3, civil_unrest=2,
weather=2, infrastructure=2, eidolan=0.35,
}
if theatre then
if theatre.key == "southern_strait" then
weights.confederacy = 5; weights.forged_docs = 6; weights.smuggling = 6; weights.piracy = 3
elseif theatre.key == "alpazkigz" or theatre.key == "odiferia" then
weights.noctic = 6; weights.vampirism = 3; weights.weather = 3; weights.eidolan = 0.7
elseif theatre.key == "general_port" then
weights.sabotage = 5; weights.forged_docs = 5; weights.smuggling = 5
elseif theatre.key == "new_south_jangsong" or theatre.key == "normark" then
weights.insurgency = 5; weights.weather = 5; weights.sabotage = 4
elseif theatre.key == "eastern_caledonia" then
weights.weather = 5; weights.infrastructure = 4; weights.insurgency = 3
end
end
if mission then
local k = mission._key
if k == "hazard_response" then weights.weather=8; weights.infrastructure=6 end
if k == "noctic_custody" then weights.noctic=9; weights.vampirism=5 end
if k == "confederacy_interdiction" then weights.confederacy=10 end
if k == "eidolan_containment" then weights.eidolan=12 end
if k == "vbss" or k == "anti_smuggling" then weights.smuggling=8; weights.forged_docs=5 end
end
local key = weightedKey(weights) or "routine"
return key, THREATS[key]
end
--------------------------------------------------------------------------------
-- G. DAILY EVENT + FORECAST INTEGRATION
--------------------------------------------------------------------------------
local function buildEventMapForToday(di)
local ok, daily = pcall(require, "Module:BassaridianDailyEvents")
if not ok or not daily or type(daily.makeTable) ~= "function" then return {}, false end
local tbl = daily.makeTable({args={year=tostring(di.year), day=tostring(di.dayOfYear)}})
local eventMap = {}
for line in tostring(tbl):gmatch("[^\n]+") do
if line:match("^|%s") and not line:match("^|%-%s*$") then
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
if #parts >= 4 then
local city, ev, urg = parts[2], parts[3], parts[4]
if city and city ~= "" and not eventMap[city] then eventMap[city] = {desc=ev, urgency=urg} end
end
end
end
return eventMap, true
end
local WX_MANUAL = {
fair = {label="Green / Fair", level=0, condition="Fair", impacts={ground="OK", sea="OK", air="OK"}},
fog = {label="Yellow / Fog", level=1, condition="Fog", impacts={ground="Slow", sea="OK", air="Limited"}},
rain = {label="Yellow / Rain", level=1, condition="Rain", impacts={ground="Slow", sea="Restricted", air="Limited"}},
storm = {label="Orange / Storm", level=2, condition="Storm", impacts={ground="Slow", sea="Restricted", air="Limited"}},
severe_storm = {label="Red / Severe storm", level=3, condition="Severe storm", impacts={ground="Hazard", sea="Closed", air="No-Go"}},
blizzard = {label="Red / Blizzard", level=3, condition="Blizzard", impacts={ground="Hazard", sea="Restricted", air="No-Go"}},
flood = {label="Red / Flood", level=3, condition="Flood", impacts={ground="Hazard", sea="Restricted", air="Limited"}},
heat = {label="Yellow / Extreme heat", level=1, condition="Extreme heat", impacts={ground="Slow", sea="OK", air="OK"}},
ash = {label="Orange / Ash / smoke", level=2, condition="Ash / smoke", impacts={ground="Slow", sea="OK", air="Limited"}},
}
local WX_ALIASES = {
["fair"]="fair", ["green"]="fair", ["clear"]="fair",
["fog"]="fog", ["yellow"]="fog",
["rain"]="rain",
["storm"]="storm", ["orange"]="storm",
["severe storm"]="severe_storm", ["red"]="severe_storm",
["blizzard"]="blizzard", ["snow"]="blizzard",
["flood"]="flood", ["flooding"]="flood",
["heat"]="heat", ["extreme heat"]="heat",
["ash"]="ash", ["smoke"]="ash",
}
local function forecastFor(node, di)
local ok, wx = pcall(require, "Module:BassaridiaForecast")
if not ok or not wx or type(wx.getOpsData) ~= "function" then return nil end
local data = wx.getOpsData(node, di.year, di.dayOfYear)
if not data then return nil end
local alert = data.alert or {}
return {
label = tostring(alert.label or "Green"),
level = tonumber(alert.level) or 0,
condition = tostring(data.condition or ""),
hazard = tostring(alert.hazard or ""),
impacts = data.impacts or {ground="OK", sea="OK", air="OK"},
source = "Module:BassaridiaForecast",
}
end
local function resolveWeather(args, node, di)
local raw = args.weather or args.wx or "auto"
local n = norm(raw)
if isAuto(raw) or n == "forecast" then
local data = forecastFor(node, di)
if data then return data end
return WX_MANUAL.fair
end
local key = WX_ALIASES[n]
return key and WX_MANUAL[key] or WX_MANUAL.fair
end
local function weatherPenalty(wx)
if not wx then return 0 end
local lvl = tonumber(wx.level) or 0
return ({[0]=0,[1]=4,[2]=9,[3]=16})[lvl] or 0
end
--------------------------------------------------------------------------------
-- H. FORCE STRUCTURE + EQUIPMENT
--------------------------------------------------------------------------------
local GROUND_ECHELONS = {"Turmae", "Contubernia", "Ordinis", "Manipuli", "Centuriae", "Cohortes"}
local NAVAL_ECHELONS = {"Birlik-i Gemi (Patrol Element)", "Tabur-i Derya (Task Unit)", "Ordu (Task Group)"}
local AIR_ECHELONS = {"Zeygi (Element)", "Ptisis (Flight)", "Moira (Squadron)"}
local FORCE_LEVEL = {
none = -99,
light = -1,
standard = 0,
reinforced = 1,
heavy = 2,
surge = 3,
}
local function forceOverride(v)
if isAuto(v) then return nil end
return FORCE_LEVEL[norm(v)]
end
local GROUND_EQUIPMENT = {
{name="[[Bijarian Command Vehicle]]", tags={command=true, all=true}},
{name="[[Corythia Class Dedicated Logistics and Transport Truck]]", tags={logistics=true, all=true}},
{name="[[Abeis-Bulhanu Virelia-Class Urban Pacifier]]", tags={urban=true, crowd=true}},
{name="[[Abeis-Bulhanu Class High-Speed Skirmisher (Hoverbike)]]", tags={road=true, frontier=true}},
{name="[[Abeis-Bulhanu Khamsin-Class Desert Skirmisher]]", tags={road=true, frontier=true, strait=true}},
{name="[[Abeis-Bulhanu Hrimthurs-Class Arctic Hoverbike]]", tags={cold=true, highland=true}},
{name="[[Abeis-Ismael Class “Cathartes” Littoral Hoverbike]]", tags={strait=true, maritime=true, port=true}},
{name="[[Quadwalker “Oble-Lisea” 4189]]", tags={wilderness=true, highland=true, patrol=true}},
{name="[[Gargani Class Quadwalker]]", tags={wilderness=true, patrol=true, crowd=true}},
{name="[[Model V-99 Thunderguard Citadel Train]]", tags={rail=true, command=true, custody=true}},
{name="[[Class SZ-88 Somniant Bastion]]", tags={command=true, overwatch=true, all=true}},
{name="[[Delphis Class Coastal Surveillance Tower]]", tags={port=true, strait=true, maritime=true}},
{name="[[Abeis Class Advanced Air Defense System (Mobile Laser)]]", tags={airdefense=true, port=true, strategic=true}},
}
local NAVAL_EQUIPMENT = {
{name="[[Aetherium Class Patrol Ship]]", tags={patrol=true, port=true, strait=true, lake=true}},
{name="[[Vaeringheim Class Corvette]]", tags={escort=true, patrol=true, lake=true, strait=true}},
{name="[[Saluria Class Gunboat]]", tags={littoral=true, patrol=true, lake=true, strait=true}},
{name="[[Cetus Class Attack Craft]]", tags={intercept=true, patrol=true, lake=true, strait=true}},
{name="[[Cetomagna Class Minehunter]]", tags={hazard=true, port=true, strait=true}},
{name="[[Velkara Class Littoral Landing Ship]]", tags={lift=true, humanitarian=true, strait=true}},
{name="[[Abeis-Empress Class Littoral Battlecruiser]]", tags={command=true, deterrence=true, strait=true}},
{name="[[Ismael Class Privateer Commission]]", tags={privateer=true, interdiction=true, strait=true}},
}
local AIR_EQUIPMENT = {
{name="[[Catonis Class Unmanned Aircraft (UAV)]]", tags={isr=true, all=true}},
{name="[[Lotos Class Tactical UAV]]", tags={isr=true, all=true}},
{name="[[Aurantius Class Multi-Role UAV]]", tags={isr=true, sigint=true, all=true}},
{name="[[Noctiluna Class Medium Transport Helicopter]]", tags={medevac=true, lift=true, humanitarian=true}},
{name="[[Umbraclaw Class Heavy Transport Helicopter]]", tags={lift=true, logistics=true}},
{name="[[Fluxus Class Maritime Patrol Aircraft]]", tags={maritime=true, strait=true, lake=true, isr=true}},
{name="[[Hostica Class Maritime Patrol Aircraft]]", tags={maritime=true, strait=true, lake=true, isr=true}},
{name="[[Ventiflor Eye Class AEW&C Aircraft]]", tags={command=true, airspace=true, strategic=true}},
{name="[[Thalassa Class Attack Helicopter]]", tags={deterrence=true, combat=true}},
{name="[[Abeis-Osiríos Class CAS Gunship]]", tags={combat=true, deterrence=true}},
{name="[[Abeis-Minerva Class Combat Littoral Strike Aircraft]]", tags={littoral=true, deterrence=true, strait=true}},
}
local function missionTags(mission, theatre)
local tags = {}
for k, v in pairs((mission and mission.tags) or {}) do if v then tags[k] = true end end
for k, v in pairs((theatre and theatre.tags) or {}) do if v then tags[k] = true end end
tags.all = true
if mission then
local key = mission._key
if key == "isr" then tags.isr = true end
if key == "medevac" or key == "hazard_response" or key == "white_lane" then tags.humanitarian = true; tags.lift = true end
if key == "anti_smuggling" or key == "vbss" or key == "confederacy_interdiction" then tags.interdiction = true; tags.intercept = true end
if key == "market_security" or key == "prize_transfer" or key == "noctic_custody" then tags.custody = true end
if key == "show_of_force" or key == "blockade_warning" then tags.deterrence = true end
end
return tags
end
local function equipmentScore(eq, tags)
local score = 1
for tag, _ in pairs(eq.tags or {}) do
if tags[tag] then score = score + 4 end
end
return score
end
local function chooseEquipment(pool, tags, count)
local available = copyArray(pool)
local chosen = {}
count = clamp(count or 2, 0, 5)
for _ = 1, count do
if #available == 0 then break end
local weighted = {}
for idx, eq in ipairs(available) do table.insert(weighted, {idx=idx, weight=equipmentScore(eq, tags)}) end
local item = pickWeighted(weighted)
if not item then break end
table.insert(chosen, available[item.idx].name)
table.remove(available, item.idx)
end
return chosen
end
local function forceIndex(baseUrgency, posture, override, branchBias, maxIndex)
if override == -99 then return 0 end
local idx = 1 + math.floor((tonumber(baseUrgency) or 1) / 1.35)
idx = idx + (posture.force or 0) + (branchBias or 0)
if override ~= nil then idx = idx + override end
return clamp(idx, 1, maxIndex)
end
local function unitId(divCode, branch, n)
local prefix = branch == "ground" and "" or (branch == "naval" and "N" or "A")
return string.format("%s-%s%02d", divCode, prefix, n)
end
local function resolveBranches(args, mission, theatre, div, urgency)
local raw = norm(args.branch or args.primary or "auto")
local primary = isAuto(raw) and (mission and mission.primary or "ground") or raw
if primary ~= "ground" and primary ~= "naval" and primary ~= "air" and primary ~= "joint" then primary = mission and mission.primary or "ground" end
if primary == "joint" then
local weights = {ground=3, naval=2, air=2}
if theatre.tags.maritime or theatre.tags.strait or theatre.tags.lake then weights.naval = 4 end
if theatre.tags.highland or theatre.tags.frontier then weights.air = 3 end
local lead = weightedKey(weights) or "ground"
return "joint", lead
end
return primary, primary
end
local function makeForcePackage(args, div, theatre, mission, urgency, posture, wx)
local primary, lead = resolveBranches(args, mission, theatre, div, urgency)
local tags = missionTags(mission, theatre)
local gOverride = forceOverride(args.ground or args.groundforce)
local nOverride = forceOverride(args.naval or args.navalforce)
local aOverride = forceOverride(args.air or args.airforce)
local joint = primary == "joint"
local wantGround = joint or primary == "ground" or (urgency >= 3 and gOverride ~= -99)
local wantNaval = (joint and (theatre.tags.maritime or theatre.tags.strait or theatre.tags.lake or theatre.tags.port)) or primary == "naval"
local wantAir = joint or primary == "air" or urgency >= 2
if nOverride == -99 then wantNaval = false end
if aOverride == -99 then wantAir = false end
if gOverride == -99 then wantGround = false end
if wx and wx.impacts then
if wx.impacts.sea == "Closed" then wantNaval = false end
if wx.impacts.air == "No-Go" then wantAir = false end
end
local ground, naval, air = "—", "—", "—"
local gEq, nEq, aEq = {}, {}, {}
if wantGround then
local bias = lead == "ground" and 1 or 0
local idx = forceIndex(urgency, posture, gOverride, bias, #GROUND_ECHELONS)
ground = string.format("%s (%s)", GROUND_ECHELONS[idx], unitId(div.code, "ground", math.random(1,24)))
gEq = chooseEquipment(GROUND_EQUIPMENT, tags, idx >= 5 and 4 or (idx >= 3 and 3 or 2))
end
if wantNaval then
local bias = lead == "naval" and 1 or 0
local idx = forceIndex(urgency, posture, nOverride, bias-1, #NAVAL_ECHELONS)
naval = string.format("%s (%s)", NAVAL_ECHELONS[idx], unitId(div.code, "naval", math.random(1,12)))
nEq = chooseEquipment(NAVAL_EQUIPMENT, tags, idx >= 3 and 3 or 2)
end
if wantAir then
local bias = lead == "air" and 1 or 0
local idx = forceIndex(urgency, posture, aOverride, bias-1, #AIR_ECHELONS)
air = string.format("%s (%s)", AIR_ECHELONS[idx], unitId(div.code, "air", math.random(1,18)))
aEq = chooseEquipment(AIR_EQUIPMENT, tags, idx >= 3 and 3 or 2)
end
return {
primary = primary,
lead = lead,
ground = ground,
naval = naval,
air = air,
groundEquipment = gEq,
navalEquipment = nEq,
airEquipment = aEq,
}
end
--------------------------------------------------------------------------------
-- I. OVERSIGHT / LEGAL GUARDRAILS / SUPPORT
--------------------------------------------------------------------------------
local OVERSIGHT = {
senate = "Senate of Elders military liaison",
hce = "Port compliance inspector (HCE)",
straits = "Straits Conventions civilian monitor",
white_lane = "White-Lane Corridor Observer",
temple = "Temple Bank / cult liaison",
council = "Council of Kings command liaison",
local_authority = "Municipal / dependency / customary-authority liaison",
engineering = "Civil engineering and infrastructure authority",
health = "Civil health / clinic liaison",
}
local function oversightFromInput(v)
if isAuto(v) then return nil end
local n = norm(v)
local aliases = {
["senate"]="senate", ["senate of elders"]="senate",
["hce"]="hce", ["port"]="hce", ["port compliance"]="hce",
["straits"]="straits", ["straits convention"]="straits", ["conventions"]="straits",
["white lane"]="white_lane", ["white-lane"]="white_lane",
["temple"]="temple", ["temple bank"]="temple",
["council"]="council", ["council of kings"]="council",
["local"]="local_authority", ["customary"]="local_authority", ["municipal"]="local_authority",
["engineering"]="engineering", ["engineer"]="engineering",
["health"]="health", ["clinic"]="health",
}
local key = aliases[n]
return key and OVERSIGHT[key] or safeUserText(v)
end
local function automaticOversight(theatre, mission, threat)
local items = {}
if theatre.key == "southern_strait" then
table.insert(items, OVERSIGHT.straits)
table.insert(items, OVERSIGHT.hce)
if mission._key == "white_lane" or mission._key == "missionary_escort" or threat._key == "civil_unrest" then table.insert(items, OVERSIGHT.white_lane) end
if mission._key == "missionary_escort" or mission._key == "sacred_route" then table.insert(items, OVERSIGHT.temple) end
table.insert(items, OVERSIGHT.local_authority)
elseif theatre.key == "eastern_caledonia" or theatre.key == "ouriana" then
table.insert(items, OVERSIGHT.council)
table.insert(items, OVERSIGHT.local_authority)
table.insert(items, OVERSIGHT.senate)
elseif mission._key == "water_security" or mission._key == "infrastructure_protection" or mission._key == "route_clearance" then
table.insert(items, OVERSIGHT.engineering)
table.insert(items, OVERSIGHT.senate)
elseif mission._key == "noctic_custody" or threat._key == "vampirism" then
table.insert(items, OVERSIGHT.temple)
table.insert(items, OVERSIGHT.health)
else
table.insert(items, pick({OVERSIGHT.senate, OVERSIGHT.hce, OVERSIGHT.local_authority}))
end
return items
end
local function complianceRules(theatre, mission, threat, stage)
local rules = {}
table.insert(rules, "War League action remains subordinate to the Bassaridian constitutional chain of command and recorded legislative/executive oversight.")
table.insert(rules, "No military equipment or controlled weapons are released for foreign export; strategic materiel remains under Bassaridian custody.")
if theatre.key == "southern_strait" or theatre.tags.strait then
table.insert(rules, "Neutral and lawful passage must remain distinguishable from suspect traffic; inspections require documented cause, routing and evidence custody.")
table.insert(rules, "White-Lane humanitarian, medical and essential-goods movement is protected from avoidable interruption.")
table.insert(rules, "General Port registry, cargo-ledger and port-state procedures remain the ordinary administrative mechanism for lawful commerce.")
table.insert(rules, "Sacred-road segments, shrine approaches, caravan wells, ferry approaches and road-market nodes are treated as protected civic landscape unless an immediate safety requirement compels temporary restriction.")
end
if theatre.tags.tribal or theatre.tags.sacred then
table.insert(rules, "Tribal, customary and sacred-space authority is preserved except where a specific immediate security necessity requires a narrowly bounded intervention.")
end
if theatre.tags.dependency then
table.insert(rules, "Security presence does not by itself displace dependency or municipal civil authority; integration and public-service decisions remain civil/constitutional functions.")
end
if mission._key == "water_security" then
table.insert(rules, "War League command secures the water system and relief corridors but does not assume technical management of engineering, treatment or distribution.")
end
if mission._key == "noctic_custody" then
table.insert(rules, "Restricted raw-leaf material, tinctures, clinic transfers and evidence are separated and logged; health and Temple Bank functions remain distinct from military custody.")
end
if mission._key == "prize_transfer" or mission._key == "vbss" or mission._key == "anti_smuggling" then
table.insert(rules, "Seized manifests, seals, cargo and witness statements are preserved for prize-court or competent civil review; field seizure does not substitute for final adjudication.")
end
if mission._key == "training" or stage == "exercise" then
table.insert(rules, "Exercise activity is non-lethal and yields immediately to real emergency, civilian infrastructure, sacred-site and medical-safety requirements.")
end
if threat._key == "confederacy" then
table.insert(rules, "Affiliation is not presumed from geography or irregular paperwork alone; Confederacy-linked allegations require evidence and are recorded separately from ordinary criminality.")
end
return rules
end
local function supportPackage(theatre, mission, threat, div)
local s = {}
table.insert(s, "[[General Port of Lake Morovia]] registry / logistics coordination")
if theatre.key == "southern_strait" then
table.insert(s, "[[Temple Bank of the Reformed Stripping Path]] missionary / civic liaison")
if div.code ~= "HAT" then table.insert(s, "[[Hatch Ministry]] coastal intelligence / prize-evidence support") end
table.insert(s, "Local guides, registrars, stewards and route authorities")
elseif theatre.tags.tribal then
table.insert(s, "Recognized clan / tribal liaison and local route knowledge")
end
if mission._key == "hazard_response" or mission._key == "water_security" or mission._key == "route_clearance" then
table.insert(s, "Civil engineering / public-works element")
end
if mission._key == "medevac" or mission._key == "white_lane" or threat._key == "vampirism" then
table.insert(s, "Medical / clinic coordination element")
end
if mission._key == "noctic_custody" or threat._key == "noctic" or threat._key == "vampirism" then
table.insert(s, "Temple Bank Noctic custody and cult-health liaison")
end
if theatre.key == "eastern_caledonia" or theatre.key == "ouriana" then
table.insert(s, "Council of Kings dependency / integration liaison")
end
return s
end
--------------------------------------------------------------------------------
-- J. URGENCY + OBJECTIVE + OUTCOME
--------------------------------------------------------------------------------
local URGENCY_LABEL = {[0]="N/A", [1]="LOW", [2]="MEDIUM", [3]="HIGH", [4]="CRITICAL"}
local function computeUrgency(mission, threat, posture, wx)
local score = tonumber(mission.base) or 1
score = math.max(score, tonumber(threat.score) or 0)
score = score + math.max(0, posture.risk or 0)
if wx and tonumber(wx.level) and wx.level >= 2 then score = score + 1 end
if wx and tonumber(wx.level) and wx.level >= 3 then score = score + 1 end
return clamp(score, 1, 4)
end
local function automaticObjective(theatre, node, mission, threat)
local key = mission._key
if key == "southern_strait_compliance" then
return "Restore documented lawful passage through " .. node .. " while isolating suspect traffic, preserving White-Lane access and reconciling route, registry and cargo records."
elseif key == "lacaran_hodos" then
return "Keep the Lacaran Hodos rail-road spine open through " .. node .. ", protect transfer points and locally protected route features, and prevent disruption from becoming a corridor-wide closure."
elseif key == "dependency_stabilization" then
return "Protect civil authorities, transport links and negotiation/integration sites at " .. node .. " without converting temporary security presence into ordinary military government."
elseif key == "white_lane" then
return "Maintain a clearly marked humanitarian and essential-goods corridor through " .. node .. " while separating relief traffic from suspect or unregistered movements."
elseif key == "noctic_custody" then
return "Secure Noctic-related clinics, custody ledgers and controlled cargo at " .. node .. ", separating lawful medical or ritual movement from restricted raw-leaf or counterfeit traffic."
elseif key == "water_security" then
return "Protect water infrastructure, rationing/relief lanes and engineering access at " .. node .. " while leaving technical operation under competent civil authorities."
elseif key == "hazard_response" then
return "Reopen protected movement through " .. node .. ", support evacuation and engineering access, and prevent environmental disruption from causing a wider breakdown in provisioning or public order."
elseif key == "confederacy_interdiction" then
return "Disrupt evidence-supported Confederacy-linked or adjacent courier, cargo and support networks around " .. node .. " while maintaining lawful passage for uninvolved travelers and commerce."
elseif key == "training" then
return "Test route-holding, reconnaissance, engineering, medical and sustainment performance around " .. node .. " under non-lethal field regulations."
end
return mission.label .. " at " .. node .. " in response to " .. lower(threat.label) .. ", with emphasis on maintaining governable passage and documented handoff to civil institutions."
end
local OUTCOMES = {
{min=82, label="Controlled completion", text="Primary objective achieved; route or node remains governable and the operation transitions to ordinary civil, Port or dependency follow-up."},
{min=66, label="Objective met with variance", text="Primary objective substantially achieved, but a limited follow-up tasking remains for records, repairs, escort continuity or local reconciliation."},
{min=48, label="Partial completion", text="Immediate risk is contained but the underlying route, evidence, infrastructure or political problem remains open and requires another operational cycle."},
{min=30, label="Deferred / constrained", text="Weather, access, legal safeguards, force availability or local conditions prevent full execution; protected holding measures remain in place."},
{min=0, label="Escalation / incident remains open", text="The operation does not establish stable control of the problem; command records an escalation and requires revised tasking rather than treating the action as complete."},
}
local function outcomeOverride(v)
if isAuto(v) then return nil end
local n = norm(v)
if n == "success" or n == "controlled" or n == "complete" then return OUTCOMES[1] end
if n == "variance" or n == "mixed success" then return OUTCOMES[2] end
if n == "partial" or n == "mixed" then return OUTCOMES[3] end
if n == "deferred" or n == "no go" or n == "no-go" then return OUTCOMES[4] end
if n == "escalation" or n == "failed" or n == "failure" then return OUTCOMES[5] end
return nil
end
local function assessOutcome(args, mission, threat, posture, wx, forces, oversightCount, stage)
local forced = outcomeOverride(args.outcome or args.resolution)
if forced then return forced, nil end
if stage == "planning" then return {label="Pending execution", text="No simulated result requested at the planning stage; the operation remains an approved/taskable plan."}, nil end
local score = 68
score = score - (threat.score or 0) * 7
score = score - weatherPenalty(wx)
score = score - (posture.risk or 0) * 2
if forces.primary == "joint" then score = score + 5 end
if forces.ground ~= "—" then score = score + 3 end
if forces.naval ~= "—" and (mission.primary == "naval" or mission.primary == "joint") then score = score + 4 end
if forces.air ~= "—" then score = score + 3 end
score = score + math.min(6, (oversightCount or 1) * 2)
if mission._key == "training" then score = score + 8 end
score = score + math.random(-10, 10)
score = clamp(score, 0, 100)
for _, out in ipairs(OUTCOMES) do
if score >= out.min then return out, score end
end
return OUTCOMES[#OUTCOMES], score
end
--------------------------------------------------------------------------------
-- K. SPECIAL NODE / THEATRE GUARDRAILS
--------------------------------------------------------------------------------
local SOUTHERN_STRAIT_INTERIOR = {
[norm("Nysara Velai")]=true,
[norm("Thal Lenaia")]=true,
[norm("Selenarion")]=true,
[norm("Hayr Chaskal")]=true,
[norm("Rumi-Kefaya")]=true,
[norm("Lacaran Hodos")]=true,
[norm("Lacaran Hodos Divan")]=true,
}
local function applyGuardrails(div, theatre, node, warnings)
warnings = warnings or {}
if theatre.key == "eastern_caledonia" and div.code ~= "CKD" then
table.insert(warnings, "Eastern Caledonian deployments are reserved to the Council of Kings Division in the inherited WarLeagueDivisionOpsBVR logic; division changed to CKD.")
div = DIVISIONS.CKD
end
if theatre.key == "southern_strait" and SOUTHERN_STRAIT_INTERIOR[norm(node)] and div.code == "HAT" then
table.insert(warnings, "Hatch Ministry forces are best treated here as coastal/intelligence/prize-evidence support rather than the principal inland formation; lead division changed to CKD.")
div = DIVISIONS.CKD
end
if theatre.key == "southern_strait" and SOUTHERN_STRAIT_INTERIOR[norm(node)] and div.code == "HFA" and (node == "Nysara Velai" or node == "Lacaran Hodos") then
table.insert(warnings, "Deep Lacaran Hodos tasking is assigned to CKD for this planner; Haifan Bassaridia Division may remain in supporting coastal or staging roles.")
div = DIVISIONS.CKD
end
return div, warnings
end
--------------------------------------------------------------------------------
-- L. OPERATION ASSEMBLY
--------------------------------------------------------------------------------
local function opId(di, divCode, theatreKey)
local th = ({
morovian_core="MC", general_port="GP", alpazkigz="AL", odiferia="OD",
new_south_jangsong="NJ", haifan="HF", normark="NO", ouriana="OU",
eastern_caledonia="EC", southern_strait="SS",
})[theatreKey] or "XX"
return string.format("WLX-%03d.%03d-%s-%s-%03d", di.year, di.dayOfYear, divCode, th, math.random(1,999))
end
local function nodeLink(node)
if node == "General Port of Lake Morovia" then return "[[General Port of Lake Morovia]]" end
if node == "Lacaran Hodos" then return "[[Lacaran Hodos]]" end
if node == "Lacaran Hodos Divan" then return "[[Lacaran Hodos Divan]]" end
if node == "Gloom Forest of Perpetual Autumn" then return "[[Gloom Forest of Perpetual Autumn]]" end
if node == "Southern Odiferae Wetlands" then return "[[Alperkin#Southern Odiferae Wetlands|Southern Odiferae Wetlands]]" end
if node == "Northern and Western Morovian Highlands" then return "[[Alperkin#Northern and Western Highlands|Northern and Western Morovian Highlands]]" end
if node == "Alperkin Coastal Woodlands" then return "[[Alperkin#Coastal Woodlands|Alperkin Coastal Woodlands]]" end
return "[[" .. node .. "]]"
end
local function makeOperation(args)
local di = getPSSCDate(args)
local seed = tonumber(args.seed) or (di.year * 100000 + di.dayOfYear * 101)
math.randomseed(seed)
local warnings = {}
local manualNode = not isAuto(args.node or args.city or args.location)
local nodeInput = trim(args.node or args.city or args.location)
local inferred = manualNode and inferTheatreFromNode(nodeInput) or nil
local theatre = theatreFromInput(args.theatre or args.theater or args.region)
local div = divisionFromInput(args.division)
if not theatre and inferred then theatre = inferred end
if not theatre and div then theatre = chooseTheatre(div) end
if not theatre then
-- choose a theatre first, weighted globally
local list = {}
for key, th in pairs(THEATRES) do table.insert(list, {key=key, weight=th.weight or 1}) end
local item = pickWeighted(list)
theatre = THEATRES[item and item.key or "morovian_core"]
end
if not div then div = chooseDivision(theatre) end
local node = manualNode and nodeInput or pick(theatre.nodes)
if manualNode and inferred and inferred.key ~= theatre.key then
table.insert(warnings, "The selected node normally belongs to " .. inferred.label .. "; the manually selected theatre has been retained.")
end
div, warnings = applyGuardrails(div, theatre, node, warnings)
local stageKey = norm(args.stage or "planning"):gsub(" ", "_")
if stageKey == "after action" then stageKey = "after_action" end
if not STAGES[stageKey] then stageKey = "planning" end
local missionKey, mission = missionFromInput(args.mission)
if not mission then missionKey, mission = chooseMission(theatre, stageKey) end
mission._key = missionKey
local threatKey, threat = threatFromInput(args.threat)
if not threat then threatKey, threat = chooseThreat(theatre, mission) end
threat._key = threatKey
local postureKey = norm(args.posture or "standard")
if not POSTURES[postureKey] then postureKey = "standard" end
local posture = POSTURES[postureKey]
local wx = resolveWeather(args, node, di)
local urgency = computeUrgency(mission, threat, posture, wx)
local eventMap, hasEvents = buildEventMapForToday(di)
local eventDesc = "No linked daily-event report"
local eventUrgency = nil
if not isAuto(args.event) then
eventDesc = safeUserText(args.event)
else
local ev = eventMap[node]
if ev then
eventDesc = ev.desc
eventUrgency = ev.urgency
elseif hasEvents then
eventDesc = "No reports"
end
end
local objective = isAuto(args.objective) and automaticObjective(theatre, node, mission, threat) or safeUserText(args.objective)
if objective == "" then objective = automaticObjective(theatre, node, mission, threat) end
local forces = makeForcePackage(args, div, theatre, mission, urgency, posture, wx)
local manualObs = oversightFromInput(args.oversight)
local oversight = manualObs and {manualObs} or automaticOversight(theatre, mission, threat)
local rules = complianceRules(theatre, mission, threat, stageKey)
local support = supportPackage(theatre, mission, threat, div)
local outcome, score = assessOutcome(args, mission, threat, posture, wx, forces, #oversight, stageKey)
local id = trim(args.id)
if id == "" then id = opId(di, div.code, theatre.key) end
local customNote = safeUserText(args.note or args.notes)
return {
di=di, id=id, seed=seed, div=div, theatre=theatre, node=node,
stageKey=stageKey, stage=STAGES[stageKey], mission=mission, threat=threat,
posture=posture, wx=wx, urgency=urgency, eventDesc=eventDesc,
eventUrgency=eventUrgency, objective=objective, forces=forces,
oversight=oversight, rules=rules, support=support, outcome=outcome,
score=score, warnings=warnings, customNote=customNote,
}
end
--------------------------------------------------------------------------------
-- M. RENDERING
--------------------------------------------------------------------------------
local function wxText(wx)
if not wx then return "n/a" end
local i = wx.impacts or {}
local cond = trim(wx.condition)
local label = trim(wx.label)
local main = (label ~= "" and label or "Forecast")
if cond ~= "" and not main:lower():find(cond:lower(), 1, true) then main = main .. " — " .. cond end
return string.format("%s<br/><small>Ground: %s; Sea: %s; Air: %s</small>", main, i.ground or "OK", i.sea or "OK", i.air or "OK")
end
local function urgencyStyle(urgency)
if urgency >= 4 then return "background:#f8d7da;" end
if urgency >= 3 then return "background:#fce8e6;" end
if urgency == 2 then return "background:#fff3cd;" end
return "background:#d4edda;"
end
local function renderOperation(op, compact)
local out = {}
if compact then
table.insert(out, "{| class='wikitable' style='width:100%;'")
table.insert(out, "! Op-ID !! Date !! Division !! Theatre / Node !! Mission !! Urgency !! Forces !! Outcome")
table.insert(out, "|-")
local f = joinComma({op.forces.ground, op.forces.naval, op.forces.air})
table.insert(out, string.format("| %s || %s || %s || %s — %s || %s || style='%s' | %s || %s || %s",
op.id, op.di.text, op.div.link, op.theatre.label, nodeLink(op.node), op.mission.label,
urgencyStyle(op.urgency), URGENCY_LABEL[op.urgency], f, op.outcome.label))
table.insert(out, "|}")
return table.concat(out, "\n")
end
table.insert(out, "<div style='border:1px solid #a2a9b1; background:#f8f9fa; padding:0.8em 1em; margin:0.5em 0 1em 0;'>")
table.insert(out, "<div style='font-size:125%; font-weight:bold;'>Bassaridian War League Operations Planner</div>")
table.insert(out, "<div style='font-size:90%; color:#54595d;'>Manual-input / deterministic operation dossier · Op-ID " .. op.id .. "</div>")
table.insert(out, "</div>")
table.insert(out, "{| class='wikitable' style='width:100%;'")
table.insert(out, "! colspan='4' | Command and tasking")
table.insert(out, "|-")
table.insert(out, "! Date || " .. op.di.text .. " !! Stage || " .. op.stage)
table.insert(out, "|-")
table.insert(out, "! Division || " .. op.div.link .. " (" .. op.div.code .. ") !! Posture || " .. op.posture.label)
table.insert(out, "|-")
table.insert(out, "! Theatre || " .. op.theatre.label .. " !! Node || " .. nodeLink(op.node))
table.insert(out, "|-")
table.insert(out, "! Mission || " .. op.mission.label .. " !! Threat / driver || " .. op.threat.label)
table.insert(out, "|-")
table.insert(out, "! Urgency || style='" .. urgencyStyle(op.urgency) .. "' | '''" .. URGENCY_LABEL[op.urgency] .. "''' !! Weather / operational impact || " .. wxText(op.wx))
table.insert(out, "|-")
table.insert(out, "! Daily event link || colspan='3' | " .. op.eventDesc)
table.insert(out, "|-")
table.insert(out, "! Objective || colspan='3' | " .. op.objective)
table.insert(out, "|}")
table.insert(out, "{| class='wikitable' style='width:100%;'")
table.insert(out, "! colspan='3' | Assigned forces")
table.insert(out, "|-")
table.insert(out, "! Ground !! Naval !! Aerial")
table.insert(out, "|-")
table.insert(out, "| " .. op.forces.ground .. " || " .. op.forces.naval .. " || " .. op.forces.air)
table.insert(out, "|-")
table.insert(out, "| " .. bulletList(op.forces.groundEquipment) .. " || " .. bulletList(op.forces.navalEquipment) .. " || " .. bulletList(op.forces.airEquipment))
table.insert(out, "|}")
table.insert(out, "{| class='wikitable' style='width:100%;'")
table.insert(out, "! Oversight !! Supporting institutions / auxiliaries")
table.insert(out, "|-")
table.insert(out, "| " .. bulletList(op.oversight) .. " || " .. bulletList(op.support))
table.insert(out, "|}")
table.insert(out, "{| class='wikitable' style='width:100%;'")
table.insert(out, "! colspan='2' | Constitutional / corridor guardrails")
table.insert(out, "|-")
table.insert(out, "| colspan='2' | " .. bulletList(op.rules))
table.insert(out, "|-")
table.insert(out, "! Divisional doctrine || " .. op.div.doctrine)
table.insert(out, "|-")
table.insert(out, "! Theatre doctrine || " .. op.theatre.doctrine)
table.insert(out, "|}")
table.insert(out, "{| class='wikitable' style='width:100%;'")
table.insert(out, "! colspan='2' | Resolution / next state")
table.insert(out, "|-")
table.insert(out, "! Assessment || '''" .. op.outcome.label .. "'''" .. (op.score and (" (simulation score " .. op.score .. "/100)") or ""))
table.insert(out, "|-")
table.insert(out, "! Narrative || " .. op.outcome.text)
if op.customNote ~= "" then
table.insert(out, "|-")
table.insert(out, "! Editor note || " .. op.customNote)
end
table.insert(out, "|}")
if #op.warnings > 0 then
table.insert(out, "<div style='border-left:4px solid #f0ad4e; padding:0.4em 0.8em; margin-top:0.6em;'><b>Planner guardrail:</b> " .. table.concat(op.warnings, " ") .. "</div>")
end
table.insert(out, "<div style='font-size:88%; color:#54595d; margin-top:0.5em;'>Deterministic seed: " .. tostring(op.seed) .. ". Change <code>seed=</code> to generate another version while preserving all manually selected fields.</div>")
return table.concat(out, "\n")
end
--------------------------------------------------------------------------------
-- N. PUBLIC FUNCTIONS
--------------------------------------------------------------------------------
function p.operation(frame)
local args = getArgs(frame)
local op = makeOperation(args)
return renderOperation(op, false)
end
function p.compact(frame)
local args = getArgs(frame)
local op = makeOperation(args)
return renderOperation(op, true)
end
function p.options()
local out = {}
table.insert(out, "{| class='wikitable' style='width:100%;'")
table.insert(out, "! Parameter !! Purpose !! Recommended values")
table.insert(out, "|-")
table.insert(out, "| division || Lead War League division || auto; VAE; ALP; ODF; HAT; NSJ; HFA; BNO; OUR; CKD")
table.insert(out, "|-")
table.insert(out, "| theatre || Operational theatre || auto; Lake Morovia core; General Port; Alpazkigz; Odiferia; New South Jangsong; Haifan Bassaridia; Bassaridian Normark; Ouriana; Eastern Caledonian Highlands; Southern Strait")
table.insert(out, "|-")
table.insert(out, "| node || Exact city / route / node || auto or any listed node, including Nysara Velai, Lacaran Hodos, Mylecia, etc.")
table.insert(out, "|-")
table.insert(out, "| mission || Mission type || auto; corridor security; anti-smuggling; infrastructure protection; hazard response; water security; noctic custody; VBSS; White-Lane; missionary escort; dependency stabilization; Lacaran Hodos; Southern Strait compliance; Confederacy interdiction; ISR; MEDEVAC; training")
table.insert(out, "|-")
table.insert(out, "| threat || Incident / opponent / driver || auto; routine; smuggling; piracy; insurgency; Confederacy; Noctic; vampirism; sabotage; forged documents; civil unrest; weather; infrastructure failure; Eidolan")
table.insert(out, "|-")
table.insert(out, "| posture || Command posture || restrained; standard; assertive; emergency")
table.insert(out, "|-")
table.insert(out, "| branch || Primary branch || auto; ground; naval; air; joint")
table.insert(out, "|-")
table.insert(out, "| ground / naval / air || Branch force override || auto; none; light; standard; reinforced; heavy; surge")
table.insert(out, "|-")
table.insert(out, "| weather || Weather source / override || auto; forecast; fair; fog; rain; storm; severe storm; blizzard; flood; heat; ash")
table.insert(out, "|-")
table.insert(out, "| oversight || Oversight override || auto; Senate; HCE; Straits; White-Lane; Temple Bank; Council of Kings; local; engineering; health")
table.insert(out, "|-")
table.insert(out, "| stage || Operation state || planning; active; after-action; exercise")
table.insert(out, "|-")
table.insert(out, "| outcome || Resolution override || auto; success; variance; partial; deferred; escalation")
table.insert(out, "|-")
table.insert(out, "| objective / event / note || Manual narrative inputs || Any text; use auto to let the module fill objective/event")
table.insert(out, "|-")
table.insert(out, "| year / day || PSSC date override || year = PSSC year; day = 1–183")
table.insert(out, "|-")
table.insert(out, "| seed || Deterministic variation || Any integer")
table.insert(out, "|}")
return table.concat(out, "\n")
end
return p