Module:WarLeagueUnitUtilizationBVR: 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:WarLeagueUnitUtilizationBVR
-- Module:WarLeagueUnitUtilizationBVR
--
--
-- Purpose:
-- Displays the embedded War League inventory table and overlays a daily “used” estimate by division,
--  Displays the embedded War League inventory table and overlays a daily “used” estimate by division,
-- derived from Module:WarLeagueDivisionOpsBVR (formations + daily ops context) PLUS routine activity.
--   derived from Module:WarLeagueDivisionOpsBVR (formations + daily ops context) PLUS routine activity.
--
--
-- Guarantees:
-- Guarantees:
--  1) Table renders exactly as authored (no early break on blank lines; indentation stripped).
--  2) Robust formation parsing (naval/air echelons with parentheses + wiki markup).
--  3) “Used” NEVER exceeds availability (per cell, and per shared-division column-group).
--  4) All units are potentially activatable over time (broad background activation).
--  5) Small arms are NOT forced daily; they are selected only when appropriate to the day’s events.
--  6) Hatch Ministry always deploys 30 Ismael Class Privateers (capped to availability).
--  7) Hover platforms & quadwalkers are explicitly handled (category + activation).
--
--
-- Invoke:
-- Invoke:
Line 238: Line 244:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 2) Column order + mapping
-- 2) Column mapping
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local COL_KEYS = { "VREG","ALPK","ODFR","HAFA","JESR","HMIN","NSJ","HBAS","BNOR","OURN","CKD" }
local COL_KEYS = { "VREG","ALPK","ODFR","HAFA","JESR","HMIN","NSJ","HBAS","BNOR","OURN","CKD" }
Line 248: Line 254:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 3) Hatch Ministry always deploys these privateers
-- Hatch policy: always deploy 30 Ismael commissions (capped)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local ALWAYS_HATCH_PRIVATEERS = 30
local ALWAYS_HATCH_PRIVATEERS = 30
Line 254: Line 260:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 4) Helpers
-- Helpers
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function push(t, v) t[#t+1] = v end
local function push(t, v) t[#t+1] = v end
Line 298: Line 304:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 5) Ops parsing helpers
-- Ops parsing helpers
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function sanitizeOpsSegment(seg)
local function sanitizeOpsSegment(seg)
Line 347: Line 353:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 6) Lua 5.1-safe deterministic picker
-- Lua 5.1-safe deterministic picker
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function hash32(s)
local function hash32(s)
Line 381: Line 387:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 7) Baseline TOE templates (your canon set)
-- Baseline TOE by echelon (canon)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local TOE = {
local TOE = {
Line 447: Line 453:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 8) Coverage enforcement: ensure every unit appears in every TOE template (default 0)
-- Coverage enforcement: ensure every unit exists in each TOE template (default 0)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function collectAllUnitsFromBase(baseText)
local function collectAllUnitsFromBase(baseText)
Line 474: Line 480:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 9) Pools + categorization (includes quadwalkers + hover platforms)
-- Pools: broad activation (all units) + typed small-arms pools (event-driven)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function normName(name)
local function normName(name)
Line 487: Line 493:
local function add(r) roles[r]=true end
local function add(r) roles[r]=true end


-- Exclude aggregate personnel rows from “breadth rotation” pools
-- exclude aggregate personnel rows from rotation pools
if n=="active units" or n=="reserve units" then add("personnel"); return roles end
if n=="active units" or n=="reserve units" then
add("personnel"); return roles
end


-- Small arms
-- small arms (IMPORTANT: do NOT add to all_ground; keep event-driven)
if n:find("rifle",1,true) or n:find("pistol",1,true) or n:find("revolver",1,true)
if n:find("rifle",1,true) or n:find("pistol",1,true) or n:find("revolver",1,true)
or n:find("shotgun",1,true) or n:find("submachine",1,true) or n:find("dmr",1,true)
or n:find("shotgun",1,true) or n:find("submachine",1,true) or n:find("dmr",1,true)
or n:find("sniper",1,true) or n:find("rpg",1,true) then
or n:find("sniper",1,true) or n:find("rpg",1,true) then
add("small_arms")
add("small_arms")
add("all_ground")
end
end


-- Quadwalkers
-- quadwalkers
if n:find("quadwalker",1,true) then
if n:find("quadwalker",1,true) then
add("autonomous")
add("autonomous"); add("all_ground")
add("all_ground")
end
end


-- Hover platforms / hovercraft / hoverbikes (explicit)
-- hover platforms / hoverbikes
if n:find("hover",1,true) or n:find("hoverbike",1,true) then
if n:find("hover",1,true) or n:find("hoverbike",1,true) then
add("hover_mobility")
add("hover_mobility"); add("all_ground")
-- hoverbikes are treated as ground mobility; hover-corvette already gets sea via corvette keyword
add("all_ground")
end
end


-- Sea hints
-- sea
if n:find("ship",1,true) or n:find("corvette",1,true) or n:find("frigate",1,true) or n:find("cruiser",1,true)
if n:find("ship",1,true) or n:find("corvette",1,true) or n:find("frigate",1,true) or n:find("cruiser",1,true)
or n:find("battlecruiser",1,true) or n:find("submarine",1,true) or n:find("submersible",1,true)
or n:find("battlecruiser",1,true) or n:find("submarine",1,true) or n:find("submersible",1,true)
Line 520: Line 524:
end
end


-- Air hints
-- air
if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
or n:find("bomber",1,true) or n:find("intercept",1,true) or n:find("transport plane",1,true) then
or n:find("bomber",1,true) or n:find("intercept",1,true) or n:find("transport plane",1,true) then
Line 526: Line 530:
end
end


-- Ground vehicles/support (catch-all)
-- ground vehicles/support (non-small-arms)
if n:find("tank",1,true) or n:find("armored infantry",1,true) or n:find("recon vehicle",1,true)
if n:find("tank",1,true) or n:find("armored infantry",1,true) or n:find("recon vehicle",1,true)
or n:find("command vehicle",1,true) or n:find("recovery vehicle",1,true)
or n:find("command vehicle",1,true) or n:find("recovery vehicle",1,true)
or n:find("bridge%-laying",1,true) or n:find("rail%-laying",1,true) or n:find("citadel train",1,true)
or n:find("bridge%-laying",1,true) or n:find("rail%-laying",1,true) or n:find("citadel train",1,true)
or n:find("transport truck",1,true) or n:find("mine%-clearing",1,true) or n:find("engineering",1,true) then
or n:find("transport truck",1,true) or n:find("mine%-clearing",1,true) or n:find("engineering",1,true)
or n:find("howitzer",1,true) or n:find("field gun",1,true) or n:find("mortar",1,true)
or n:find("coastal gun",1,true) or n:find("coastal missile",1,true)
or n:find("air defense system",1,true) or n:find("static emplacement",1,true) or n:find("thaad",1,true)
or n:find("surveillance tower",1,true) then
add("all_ground")
add("all_ground")
end
end


-- If nothing matched, keep it reachable
local any=false
local any=false
for _ in pairs(roles) do any=true break end
for _ in pairs(roles) do any=true break end
Line 543: Line 550:


local function buildPools(allUnits)
local function buildPools(allUnits)
local pools = { all_any={}, all_ground={}, all_sea={}, all_air={}, misc={}, small_arms={} }
local pools = {
all_any={}, all_ground={}, all_sea={}, all_air={}, misc={},
small_arms_all={},
small_arms_rifle={}, small_arms_grenadier={}, small_arms_commando={},
small_arms_sniper={}, small_arms_pistol={}, small_arms_revolver={},
small_arms_smg={}, small_arms_shotgun={}, small_arms_dmr={}, small_arms_rpg={}
}


for unitName,_ in pairs(allUnits) do
for unitName,_ in pairs(allUnits) do
Line 554: Line 567:
if roles.all_air then pools.all_air[#pools.all_air+1] = unitName end
if roles.all_air then pools.all_air[#pools.all_air+1] = unitName end
if roles.misc then pools.misc[#pools.misc+1] = unitName end
if roles.misc then pools.misc[#pools.misc+1] = unitName end
if roles.small_arms then pools.small_arms[#pools.small_arms+1] = unitName end
 
if roles.small_arms then
pools.small_arms_all[#pools.small_arms_all+1] = unitName
local nn = normName(unitName)
if nn:find("grenadier",1,true) then pools.small_arms_grenadier[#pools.small_arms_grenadier+1]=unitName
elseif nn:find("commando",1,true) then pools.small_arms_commando[#pools.small_arms_commando+1]=unitName
elseif nn:find("sniper",1,true) then pools.small_arms_sniper[#pools.small_arms_sniper+1]=unitName
elseif nn:find("pistol",1,true) then pools.small_arms_pistol[#pools.small_arms_pistol+1]=unitName
elseif nn:find("revolver",1,true) then pools.small_arms_revolver[#pools.small_arms_revolver+1]=unitName
elseif nn:find("submachine",1,true) then pools.small_arms_smg[#pools.small_arms_smg+1]=unitName
elseif nn:find("shotgun",1,true) then pools.small_arms_shotgun[#pools.small_arms_shotgun+1]=unitName
elseif nn:find("dmr",1,true) then pools.small_arms_dmr[#pools.small_arms_dmr+1]=unitName
elseif nn:find("rpg",1,true) then pools.small_arms_rpg[#pools.small_arms_rpg+1]=unitName
elseif nn:find("rifle",1,true) then pools.small_arms_rifle[#pools.small_arms_rifle+1]=unitName
end
end
end
end
end
end
Line 563: Line 591:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 10) Category-scaled breadth amounts (big enough to be meaningful)
-- Category-scaled background amounts (big enough to show meaningful “used”)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function unitCategory(unitName)
local function unitCategory(unitName)
local n = normName(unitName)
local n = normName(unitName)
if n == "active units" or n == "reserve units" then return "personnel" end
if n:find("rifle",1,true) or n:find("pistol",1,true) or n:find("revolver",1,true)
or n:find("shotgun",1,true) or n:find("submachine",1,true) or n:find("dmr",1,true)
or n:find("sniper",1,true) or n:find("rpg",1,true) then
return "small_arms"
end
if n:find("quadwalker",1,true) then return "autonomous" end
if n:find("quadwalker",1,true) then return "autonomous" end
if n:find("hover",1,true) or n:find("hoverbike",1,true) then return "hover_mobility" end
if n:find("hover",1,true) or n:find("hoverbike",1,true) then return "hover_mobility" end
Line 598: Line 617:


if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
or n:find("bomber",1,true) or n:find("intercept",1,true) then
or n:find("bomber",1,true) or n:find("intercept",1,true) or n:find("transport plane",1,true) then
return "air"
return "air"
end
end
Line 611: Line 630:


return "misc"
return "misc"
end
local function breadthLevel(args)
local b=tostring((args and args.breadth) or "standard"):lower()
if b=="low" then return 1 end
if b=="high" then return 3 end
return 2
end
end


Line 625: Line 651:
local cat = unitCategory(unitName)
local cat = unitCategory(unitName)
local s = scaleFactor(uScore, bLevel)
local s = scaleFactor(uScore, bLevel)
if cat == "small_arms" then return 150 * s end
if cat == "personnel" then return 500 * s end
if cat == "ground_vehicle" then return math.max(1, math.floor(2 * s)) end
if cat == "ground_vehicle" then return math.max(1, math.floor(2 * s)) end
if cat == "autonomous" then return math.max(1, math.floor(2 * s)) end
if cat == "autonomous" then return math.max(1, math.floor(2 * s)) end
if cat == "hover_mobility" then return math.max(1, math.floor(3 * s)) end
if cat == "hover_mobility" then return math.max(1, math.floor(3 * s)) end
if cat == "fires" then return math.max(1, math.floor(1 * s)) end
if cat == "fires" then return math.max(1, math.floor(1 * s)) end
if cat == "fixed_defense" then return math.max(1, math.floor(1 * s)) end
if cat == "fixed_defense" then return math.max(1, math.floor(1 * s)) end
if cat == "air" then return math.max(1, math.floor(1 * s)) end
if cat == "air" then return math.max(1, math.floor(1 * s)) end
if cat == "sea" then return math.max(1, math.floor(1 * s)) end
if cat == "sea" then return math.max(1, math.floor(1 * s)) end
return 1
return 1
end
local function breadthLevel(args)
local b=tostring((args and args.breadth) or "standard"):lower()
if b=="low" then return 1 end
if b=="high" then return 3 end
return 2
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 11) Demand accumulation
-- Demand accumulation
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function addDemand(util, divCode, unitName, amount)
local function addDemand(util, divCode, unitName, amount)
Line 659: Line 671:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 12) Ensure small arms never excluded + broad activation for all units
-- Event-driven small arms selection (no longer forced daily)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function applyBreadthActivation(util, pools, divCode, opText, uScore, impact, salt, bLevel)
local function addSmallArmsForOp(util, pools, divCode, opText, uScore, bLevel, salt)
-- 12.1 Guarantee: some small arms every day
local op = normName(opText)
local kSA = (bLevel==1 and 2) or (bLevel==2 and 3) or 5
local wants = {}
if uScore >= 3 then kSA = kSA + 1 end
 
if uScore >= 4 then kSA = kSA + 1 end
local function want(poolName, k)
if kSA > 6 then kSA = 6 end
wants[#wants+1] = { pool = poolName, k = k }
end
 
-- scale
local s = 1
if bLevel == 2 then s = 2 end
if bLevel == 3 then s = 3 end
if uScore >= 3 then s = s + 1 end
if uScore >= 4 then s = s + 1 end
 
-- Does this op plausibly call for infantry weapons today?
local infantryRelevant =
  op:find("vbss",1,true) or op:find("boarding",1,true) or op:find("interdiction",1,true)
or op:find("contraband",1,true) or op:find("raid",1,true) or op:find("security",1,true)
or op:find("riot",1,true) or op:find("stabil",1,true) or op:find("search",1,true)
or op:find("cordon",1,true) or op:find("checkpoint",1,true) or op:find("corridor",1,true)
or op:find("hostage",1,true) or op:find("sabotage",1,true)
 
-- Hazard response is usually engineering-first; only include small arms if there’s unrest/looting/security language.
if op:find("hazard",1,true) or op:find("evac",1,true) or op:find("debris",1,true) or op:find("clearance",1,true) then
infantryRelevant = infantryRelevant or op:find("looting",1,true) or op:find("unrest",1,true) or op:find("security",1,true)
end
 
if not infantryRelevant then return end
 
-- Choose an appropriate mix
if op:find("vbss",1,true) or op:find("boarding",1,true) or op:find("interdiction",1,true) then
want("small_arms_commando", 1 + (uScore>=3 and 1 or 0))
want("small_arms_rifle", 1 + (uScore>=3 and 1 or 0))
want("small_arms_pistol", 1)
want("small_arms_dmr", (uScore>=3 and 1 or 0))
want("small_arms_smg", (uScore>=3 and 1 or 0))
elseif op:find("checkpoint",1,true) or op:find("corridor",1,true) or op:find("security",1,true) then
want("small_arms_rifle", 2)
want("small_arms_smg", 1)
want("small_arms_dmr", (uScore>=3 and 1 or 0))
want("small_arms_shotgun", (uScore>=3 and 1 or 0))
elseif op:find("riot",1,true) or op:find("stabil",1,true) or op:find("urban",1,true) then
want("small_arms_rifle", 2)
want("small_arms_smg", 1)
want("small_arms_commando", (uScore>=3 and 1 or 0))
want("small_arms_pistol", 1)
else
want("small_arms_rifle", 2)
want("small_arms_grenadier", (uScore>=3 and 1 or 0))
want("small_arms_dmr", (uScore>=3 and 1 or 0))
end


local saPicks = pickDeterministic(pools.small_arms, kSA, salt.."|guarantee_small_arms")
-- Apply picks
for _, u in ipairs(saPicks) do
for _, w in ipairs(wants) do
local base = 150
local poolList = pools[w.pool]
if bLevel == 2 then base = 250 end
if poolList and #poolList > 0 then
if bLevel == 3 then base = 450 end
local picks = pickDeterministic(poolList, w.k, salt.."|"..w.pool)
if uScore >= 3 then base = base + 150 end
for _, u in ipairs(picks) do
if uScore >= 4 then base = base + 250 end
local qty = 150 * s
addDemand(util, divCode, u, base)
local nn = normName(u)
if nn:find("commando",1,true) then qty = math.floor(qty * 0.40) end
if nn:find("grenadier",1,true) then qty = math.floor(qty * 0.35) end
if nn:find("sniper",1,true) then qty = math.floor(qty * 0.15) end
if nn:find("pistol",1,true) or nn:find("revolver",1,true) then qty = math.floor(qty * 0.25) end
if qty < 25 then qty = 25 end
addDemand(util, divCode, u, qty)
end
end
end
end
end


-- 12.2 Background activation: rotate through broad pools with category-scaled amounts
--------------------------------------------------------------------------------
-- Broad activation for all units (excluding small arms; those are event-driven)
--------------------------------------------------------------------------------
local function applyBreadthActivation(util, pools, divCode, opText, uScore, impact, salt, bLevel)
-- Background activation: training, maintenance, readiness checks, unpublished activities.
local baseK = (bLevel==1 and 2) or (bLevel==2 and 4) or 7
local baseK = (bLevel==1 and 2) or (bLevel==2 and 4) or 7
if uScore>=3 then baseK = baseK + 2 end
if uScore>=3 then baseK = baseK + 2 end
Line 684: Line 755:
if baseK > 12 then baseK = 12 end
if baseK > 12 then baseK = 12 end


-- Ground
-- Ground (vehicles, drones, defenses; NOT small arms)
local gPicks = pickDeterministic(pools.all_ground, baseK, salt.."|bg_ground")
local gPicks = pickDeterministic(pools.all_ground, baseK, salt.."|bg_ground")
for _, u in ipairs(gPicks) do
for _, u in ipairs(gPicks) do
Line 708: Line 779:
end
end


-- Misc (ensures odd items stay reachable)
-- Misc
local mPicks = pickDeterministic(pools.misc, (bLevel>=2 and 2 or 1), salt.."|bg_misc")
local mPicks = pickDeterministic(pools.misc, (bLevel>=2 and 2 or 1), salt.."|bg_misc")
for _, u in ipairs(mPicks) do
for _, u in ipairs(mPicks) do
addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
end
end
-- Event-driven small arms (only when appropriate)
addSmallArmsForOp(util, pools, divCode, opText, uScore, bLevel, salt.."|event_small_arms")
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 13) Hatch privateer commissions: div override for this unit in overlay
-- Ismael commissions: treat NSJ column availability as HAT for this unit only
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function divOverrideForUnit(unitName, colKey, defaultDiv)
local function divOverrideForUnit(unitName, colKey, defaultDiv)
if unitName == ISMAEL_UNIT_NAME then
if unitName == ISMAEL_UNIT_NAME then
-- The 50 commissions live under NSJ in the stock table; Hatch deploys 30 daily.
-- Treat that column's availability as HAT for this unit only.
if colKey == "NSJ" then return "HAT" end
if colKey == "NSJ" then return "HAT" end
end
end
Line 728: Line 800:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 14) Compute utilization from DailyOps (TOE + breadth activation + hatch privateers)
-- Compute utilization from DailyOps (TOE + breadth activation + hatch privateers)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function computeUtilization(args, pools)
local function computeUtilization(args, pools)
Line 749: Line 821:
}})
}})


-- prevent multiplying breadth activation many times per row/div
-- avoid applying breadth multiple times per same (div, op-row, domain)
local activated = {}
local activated = {}


Line 776: Line 848:
end
end
end
end
-- breadth activation once per div per ops-row domain
-- breadth once
activateOnce(divCode, opText, uScore, impact, saltBase.."|"..divCode)
activateOnce(divCode, opText, uScore, impact, saltBase.."|"..divCode)
end
end
Line 812: Line 884:
end
end


-- Always-on Hatch Ministry privateer deployment (capped by availability in overlay)
-- Hatch always deploys 30 Ismael commissions (capped in overlay)
addDemand(util, "HAT", ISMAEL_UNIT_NAME, ALWAYS_HATCH_PRIVATEERS)
addDemand(util, "HAT", ISMAEL_UNIT_NAME, ALWAYS_HATCH_PRIVATEERS)


Line 819: Line 891:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 15) Overlay highlights (NEVER exceeding availability)
-- Overlay highlights (NEVER exceed availability)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function overlayTable(baseText, util)
local function overlayTable(baseText, util)
Line 919: Line 991:
push(out,"")
push(out,"")
push(out,"<div style=\"margin-top:0.5em; font-size:90%;\">")
push(out,"<div style=\"margin-top:0.5em; font-size:90%;\">")
push(out,"<b>Daily overview:</b> usage is a capped, per-day estimate derived from War League daily ops plus routine training/maintenance/unpublished activity.")
push(out,"<b>Daily overview:</b> usage is a capped estimate derived from War League daily ops plus routine training/maintenance/unpublished activity.")
push(out,"<br/><b>Hard rule:</b> used never exceeds availability in any cell or across a shared ops-division group.")
push(out,"<br/><b>Hard rule:</b> used never exceeds availability in any cell or across a shared ops-division group.")
push(out,"<br/><b>Hatch Ministry:</b> always attempts to deploy 30 <i>Ismael Class Privateer</i> commissions (capped to what exists).")
push(out,"<br/><b>Hatch Ministry:</b> always attempts to deploy 30 <i>Ismael Class Privateer</i> commissions (capped to what exists).")
push(out,"<br/><b>Small arms:</b> always includes a daily mix (never excluded).")
push(out,"<br/><b>Small arms:</b> selected only when appropriate to the day’s operations (VBSS, security, unrest, etc.).")
push(out,"</div>")
push(out,"</div>")


Line 929: Line 1,001:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 16) Public entrypoint
-- Public entrypoint
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function p.render(frame)
function p.render(frame)
Line 936: Line 1,008:
local base = normalizeWikitextTable(RAW_BASE_TABLE)
local base = normalizeWikitextTable(RAW_BASE_TABLE)


-- ensure TOE coverage for all inventory rows
-- TOE coverage
local allUnits = collectAllUnitsFromBase(base)
local allUnits = collectAllUnitsFromBase(base)
normalizeToeTemplates(allUnits)
normalizeToeTemplates(allUnits)


-- build pools from the embedded table so all units are reachable
-- pools from embedded inventory (so all units can be reached)
local pools = buildPools(allUnits)
local pools = buildPools(allUnits)


-- compute utilization from ops + breadth + hatch privateers
-- compute usage
local util, debugRows, ok = computeUtilization(args, pools)
local util, debugRows, ok = computeUtilization(args, pools)
if not ok then
if not ok then
Line 951: Line 1,023:
local rendered = overlayTable(base, util)
local rendered = overlayTable(base, util)


-- optional debug
-- debug
if tostring(args.debug or ""):lower() == "yes" then
if tostring(args.debug or ""):lower() == "yes" then
local max = tonumber(args.debugrows) or 8
local max = tonumber(args.debugrows) or 8

Revision as of 16:01, 27 January 2026

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

-- Module:WarLeagueUnitUtilizationBVR
--
-- Displays the embedded War League inventory table and overlays a daily “used” estimate by division,
-- derived from Module:WarLeagueDivisionOpsBVR (formations + daily ops context) PLUS routine activity.
--
-- Guarantees:
--   1) Table renders exactly as authored (no early break on blank lines; indentation stripped).
--   2) Robust formation parsing (naval/air echelons with parentheses + wiki markup).
--   3) “Used” NEVER exceeds availability (per cell, and per shared-division column-group).
--   4) All units are potentially activatable over time (broad background activation).
--   5) Small arms are NOT forced daily; they are selected only when appropriate to the day’s events.
--   6) Hatch Ministry always deploys 30 Ismael Class Privateers (capped to availability).
--   7) Hover platforms & quadwalkers are explicitly handled (category + activation).
--
-- Invoke:
--   {{#invoke:WarLeagueUnitUtilizationBVR|render}}
--   {{#invoke:WarLeagueUnitUtilizationBVR|render|breadth=low}}        -- low|standard|high
--   {{#invoke:WarLeagueUnitUtilizationBVR|render|breadth=high}}
--   {{#invoke:WarLeagueUnitUtilizationBVR|render|seed=520154|opsrows=40}}
--   {{#invoke:WarLeagueUnitUtilizationBVR|render|debug=yes|debugrows=8}}

local p = {}

--------------------------------------------------------------------------------
-- 1) FULL EMBEDDED TABLE (embedded exactly as provided)
--------------------------------------------------------------------------------
local RAW_BASE_TABLE = [=[
{| class="wikitable sortable" style="width:100%;"
|+ '''Unit Numbers (Activated 32/3/52 PSSC)'''
! rowspan="2" | Image
! rowspan="2" | Unit
! rowspan="2" | Total
! colspan="6" | Regional investors
! colspan="5" | Service divisions / national allocations
|-
! [[File:VaeringheimDivisionFlag.png|25px]] Vaeringheim Regional Investor
! [[File:AlpazkigzDivisionFlag.png|25px]] Alpazkigz Regional Investor
! [[File:OdiferiaFlag.png|25px]] Odiferia Regional Investor
! [[File:HafaanFlag.png|25px]] Hafaan Regional Investor
! [[File:JeseriFlag.png|25px]] Jeseri Regional Investor
! [[File:HatchMinistryFlag.png|25px]] Hatch Ministry Regional Investor
! [[File:NewSouthJangsongFlag.png|25px]] New South Jangsong Division
! [[File:HaifaDivisionFlag.png|25px]] Haifan Bassaridia Division
! [[File:BassaridianNormarkFlagB.png|25px]] Bassaridian Normark
! [[File:OurianaFlag.png|25px]] Ouriana Division
! [[File:CouncilofKingsFlag.png|25px]] Council of Kings Division

|- 
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Personnel & readiness'''
|-
| [[File:ActiveUnit1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Active_Units|Active Units]] || 249,973 || 25,866 || 20,847 || 25,042 || 1,749 || 1,008 || 7,246 || 10,187 || 15,385 || 8,354 || 21,206 || 113,083
|-
| [[File:ReserveSoldier1226.jpeg|150px|border]]  || [[Somniant_Stock_Fund_Military_Hardware#Reserve_Units|Reserve Units]] || 305,525 || 31,615 || 25,480 || 30,608 || 2,138 || 1,232 || 8,857 || 12,451 || 18,803 || 10,211 || 25,918 || 138,212

|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Small arms & infantry weapons'''
|-
| [[File:KalithrosClass1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Kalithros_Class_Rifle|Kalithros Class Rifle]] || 169,837 || 17,563 || 14,156 || 17,006 || 1,192 || 688 || 4,931 || 6,916 || 10,466 || 5,684 || 14,402 || 76,832
|-
| [[File:DelphicaClass1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Delphica_Class_Grenadier_Rifle|Delphica Class Grenadier Rifle]] || 44,520 || 4,604 || 3,711 || 4,458 || 312 || 180 || 1,293 || 1,813 || 2,744 || 1,490 || 3,775 || 20,140
|-
| [[File:ChrysosClass1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Chrysos_Class_Commando_Rifle|Chrysos Class Commando Rifle]] || 19,821 || 2,050 || 1,652 || 1,985 || 139 || 80 || 575 || 807 || 1,221 || 663 || 1,681 || 8,967
|-
| [[File:Lothaya1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Lothaya_Class_Sniper_Rifle|Lothaya Class Sniper Rifle]] || 6,099 || 631 || 509 || 611 || 43 || 25 || 177 || 248 || 375 || 204 || 517 || 2,556
|-
| [[File:ErythrosPist1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Erythros_Class_Pistol|Erythros Class Pistol]] || 28,540 || 2,952 || 2,379 || 2,858 || 200 || 116 || 828 || 1,162 || 1,758 || 955 || 2,421 || 12,911
|-
| [[File:Bubalus1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Bubalus_XT_Class_Revolver|Bubalus XT Class Revolver]] || 150,285 || 15,541 || 12,526 || 15,049 || 1,055 || 609 || 4,363 || 6,120 || 9,261 || 5,030 || 12,744 || 67,987
|-
| [[File:Cathartes1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Cathartes_Class_RPG|Cathartes Class RPG]] || 6,182 || 640 || 516 || 620 || 43 || 25 || 179 || 251 || 380 || 206 || 525 || 2,797
|-
| [[File:Hapyriya1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Harpyia_Class_Submachine_Gun|Harpyia Class Submachine Gun]] || 22,349 || 2,312 || 1,863 || 2,238 || 157 || 91 || 649 || 910 || 1,377 || 748 || 1,896 || 10,110
|-
| [[File:StrixShotgun1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Strix_Class_Combat_Shotgun|Strix Class Combat Shotgun]] || 35,555 || 3,677 || 2,964 || 3,561 || 249 || 144 || 1,032 || 1,448 || 2,190 || 1,190 || 3,015 || 15,346
|-
| [[File:Regavis1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Regavis_Class_DMR|Regavis Class DMR]] || 43,178 || 4,465 || 3,599 || 4,323 || 303 || 175 || 1,254 || 1,759 || 2,661 || 1,445 || 3,661 || 19,534

|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Armored vehicles & ground support'''
|-
| [[File:Makra1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Makra_Class_Battle_Tank|Makra Class Battle Tank]] || 1,290 || 133 || 107 || 129 || 9 || 5 || 37 || 53 || 79 || 43 || 109 || 584
|-
| [[File:LayaClass1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Laya_Class_Heavy_Tank|Laya Class Heavy Tank]] || 444 || 46 || 37 || 45 || 3 || 2 || 13 || 18 || 27 || 15 || 38 || 201
|-
| [[File:Arachne1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Arachne_Class_Light_Tank|Arachne Class Light Tank]] || 1,560 || 162 || 130 || 157 || 11 || 6 || 45 || 63 || 95 || 52 || 133 || 705
|-
| [[File:ThalassaMBT1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Thalassa_Class_Main_Battle_Tank|Thalassa Class Main Battle Tank]] || 765 || 80 || 64 || 77 || 5 || 3 || 22 || 30 || 46 || 25 || 66 || 346
|-
| [[File:Syrinx1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Syrinx_Class_Armored_Infantry|Syrinx Class Armored Infantry]] || 7,067 || 731 || 589 || 708 || 50 || 29 || 205 || 288 || 436 || 237 || 599 || 3,198
|-
| [[File:Bijarian1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Bijarian_Command_Vehicle|Bijarian Command Vehicle]] || 115 || 12 || 10 || 12 || 1 || 0 || 3 || 4 || 6 || 3 || 10 || 52
|-
| [[File:Onceanic1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Onceanic_Recon_Vehicle|Onceanic Recon Vehicle]] || 495 || 52 || 42 || 50 || 3 || 2 || 14 || 20 || 30 || 16 || 43 || 223
|-
| [[File:Ephyra1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Ephyra_Class_Anti-Aircraft_Vehicle|Ephyra Class Anti-Aircraft Vehicle]] || 218 || 23 || 19 || 22 || 1 || 1 || 6 || 8 || 13 || 7 || 19 || 99
|-
| [[File:AmpelosARV1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Ampelos_Class_Armored_Recovery_Vehicle|Ampelos Class Armored Recovery Vehicle]] || 495 || 52 || 42 || 50 || 3 || 2 || 14 || 20 || 30 || 16 || 43 || 223
|-
| [[File:Penthia1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Penthia_Class_Bridge-Laying_Vehicle|Penthia Class Bridge-Laying Vehicle]] || 146 || 16 || 13 || 15 || 1 || 0 || 4 || 5 || 8 || 4 || 13 || 66
|-
| [[File:Ampelostr1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Ampelos_Class_Rail-Laying_Engineering_Vehicle|Ampelos Class Rail-Laying Engineering Vehicle]] || 301 || 32 || 26 || 31 || 2 || 1 || 8 || 12 || 17 || 9 || 26 || 137
|-
| [[File:Corythia1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Corythia_Class_Dedicated_Logistics_and_Transport_Truck|Corythia Class Dedicated Logistics and Transport Truck]] || 301 || 32 || 26 || 31 || 2 || 1 || 8 || 12 || 17 || 9 || 26 || 137
|-
| [[File:Icaria1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Icaria_Class_Mine-Clearing_/Combat_Engineering_Vehicle|Icaria Class Mine-Clearing / Combat Engineering Vehicle]] || 134 || 14 || 11 || 14 || 1 || 0 || 4 || 5 || 8 || 4 || 11 || 61

|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Artillery, mortars & coastal fires'''
|-
| [[File:Aithra1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aithra_Class_Howitzer|Aithra Class Howitzer]] || 321 || 34 || 27 || 33 || 2 || 1 || 9 || 13 || 19 || 10 || 28 || 145
|-
| [[File:Aetheris1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aetheris_Class_Towed_Howitzer|Aetheris Class Towed Howitzer]] || 941 || 98 || 79 || 95 || 7 || 4 || 27 || 38 || 57 || 31 || 80 || 426
|-
| [[File:Iynas1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Iynas_Class_Field_Gun|Iynas Class Field Gun]] || 753 || 78 || 63 || 76 || 5 || 3 || 22 || 30 || 46 || 25 || 64 || 341
|-
| [[File:Secutor1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#300mm_Secutor_Class_Coastal_Gun|300mm Secutor Class Coastal Gun]] || 516 || 53 || 43 || 52 || 4 || 2 || 15 || 21 || 32 || 17 || 43 || 234
|-
| [[File:Halicarn1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#155mm_Halicarn_Class_Self-Propelled_Howitzer|155mm Halicarn Class Self-Propelled Howitzer]] || 373 || 39 || 32 || 38 || 3 || 1 || 10 || 15 || 22 || 12 || 32 || 169
|-
| [[File:Odiferian1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#120mm_Odiferian_Class_Mortar_Carrier|120mm Odiferian Class Mortar Carrier]] || 218 || 23 || 19 || 22 || 1 || 1 || 6 || 8 || 13 || 7 || 19 || 99

|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Autonomous systems, prototypes & fixed defenses'''
|-
| [[File:Houndbot231227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Quadwalker_“Horehound”_4170|Quadwalker “Horehound” 4170]] || 495 || 52 || 42 || 50 || 3 || 2 || 14 || 20 || 30 || 16 || 43 || 223
|-
| [[File:Dungbeetle1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Quadwalker_“Dungbeetle”_4172|Quadwalker “Dungbeetle” 4172]] || 361 || 37 || 30 || 36 || 3 || 1 || 10 || 15 || 22 || 12 || 30 || 164
|-
| [[File:KillBot1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Quadwalker_“Killbot”_4200|Quadwalker “Killbot” 4200]] || 734 || 76 || 62 || 74 || 5 || 3 || 21 || 29 || 45 || 24 || 62 || 333
|-
| [[File:Mechaoble1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Quadwalker_“Oble-Lisea”_4189|Quadwalker “Oble-Lisea” 4189]] || 528 || 55 || 44 || 53 || 4 || 2 || 15 || 21 || 32 || 17 || 45 || 239
|-
| [[File:Nexa1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Nexa_Class_Advanced_Prototype|Nexa Class Advanced Prototype]] || 1,095 || 114 || 92 || 110 || 8 || 4 || 31 || 44 || 67 || 36 || 93 || 496
|-
| [[File:GarganiClass1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Gargani_Class_Quadwalker|Gargani Class Quadwalker]] || 683 || 71 || 57 || 69 || 5 || 3 || 19 || 27 || 41 || 22 || 58 || 309
|-
| [[File:AbeisMobile1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis_Class_Advanced_Air_Defense_System_(Mobile_Laser)|Abeis Class Advanced Air Defense System (Mobile Laser)]] || 146 || 16 || 13 || 15 || 1 || 0 || 4 || 5 || 8 || 4 || 13 || 66
|-
| [[File:AbeisStatic1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis_Class_Static_Emplacement_(Fixed_Laser)|Abeis Class Static Emplacement (Fixed Laser)]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Fenruvian1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Fenruvian_THAAD_System_Facility|Fenruvian THAAD System Facility]] || 12 || 1 || 1 || 1 || 0 || 0 || 0 || 1 || 1 || 1 || 1 || 5
|-
| [[File:DelphisTower1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Delphis_Class_Coastal_Surveillance_Tower|Delphis Class Coastal Surveillance Tower]] || 28 || 2 || 2 || 2 || 2 || 2 || 2 || 2 || 2 || 2 || 2 || 8
|-
| [[File:AnguillaClass1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Anguilla_Class_Coastal_Missile_Battery|Anguilla Class Coastal Missile Battery]] || 373 || 39 || 32 || 38 || 3 || 1 || 10 || 15 || 22 || 12 || 32 || 169
|-
| [[File:V991227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Model_V-99_Thunderguard_Citadel_Train|Model V-99 Thunderguard Citadel Train]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14

|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Naval forces & maritime platforms'''
|-
| [[File:Erigone1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Erigone_Class_Littoral_Orbital_&_Missile_Intercept_Catamaran|Abeis-Erigone Class Littoral Orbital & Missile Intercept Catamaran]] || 12 || 2 || 1 || 2 || 0 || 0 || 0 || 0 || 0 || 0 || 2 || 5
|-
| [[File:GenportManf1225.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Empress_Class_Littoral_Battlecruiser|Abeis-Empress Class Littoral Battlecruiser]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Aetherion1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Aetherion Class Multidomain Hover-Corvette|Abeis-Aetherion Class Multidomain Hover-Corvette]]<br> ('''active 1/2/52 [[PSSC]]''') || 4 || 0 || 0 || 0 || 0 || 0 || 0 || 0 || 0 || 0 || 0 || 4
|-
| [[File:KalithrosBarge1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Kalithros-Class_Amphibious_Airstrip_Barge|Kalithros-Class Amphibious Airstrip Barge]] || 12 || 2 || 1 || 2 || 0 || 0 || 0 || 0 || 0 || 0 || 2 || 5
|-
| [[File:Aiji1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aiji_Class_Air-Caravan|Aiji Class Air-Caravan]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Seidolon1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Sideron_Class_Coastal_Battlecruiser|Sideron Class Coastal Battlecruiser]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Eidolon1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Eidolon_Class_Cruiser|Eidolon Class Cruiser]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Acheron1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Acheron_Class_Submersible_Destroyer|Acheron Class Submersible Destroyer]] || 12 || 2 || 1 || 2 || 0 || 0 || 0 || 0 || 0 || 0 || 2 || 5
|-
| [[File:Xylanda1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Xylanda_Class_Attack_Submarine|Xylanda Class Attack Submarine]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Bijarian1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Bijarian_Ballistic_Missile_Submarine|Bijarian Ballistic Missile Submarine]] || 12 || 2 || 1 || 2 || 0 || 0 || 0 || 0 || 0 || 0 || 2 || 5
|-
| [[File:Vaeringheim1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Vaeringheim_Class_Corvette|Vaeringheim Class Corvette]] || 83 || 9 || 7 || 9 || 0 || 0 || 2 || 3 || 5 || 3 || 7 || 38
|-
| [[File:Sylvapolis1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Sylvapolis_Class_Frigate|Sylvapolis Class Frigate]] || 43 || 5 || 4 || 5 || 0 || 0 || 1 || 1 || 2 || 1 || 4 || 20
|-
| [[File:Atteran1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Atterian_Class_Combat_Ship|Atterian Class Combat Ship]] || 12 || 2 || 1 || 2 || 0 || 0 || 0 || 0 || 0 || 0 || 2 || 5
|-
| [[File:Aetherium1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aetherium_Class_Patrol_Ship|Aetherium Class Patrol Ship]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Velkara1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Velkara_Class_Littoral_Landing_Ship|Velkara Class Littoral Landing Ship]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Saluria1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Saluria_Class_Gunboat|Saluria Class Gunboat]] || 43 || 5 || 4 || 5 || 0 || 0 || 1 || 1 || 2 || 1 || 4 || 20
|-
| [[File:Riceship.jpg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Cetus_Class_Attack_Craft|Cetus Class Attack Craft]] || 64 || 7 || 6 || 7 || 0 || 0 || 1 || 2 || 3 || 2 || 6 || 29
|-
| [[File:Tartarian1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Tartarian_Amphibious_Assault_Craft|Tartarian Amphibious Assault Craft]] || 83 || 9 || 7 || 9 || 0 || 0 || 2 || 3 || 5 || 3 || 7 || 38
|-
| [[File:Ismael1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Ismael_Class_Privateer_Commission|Ismael Class Privateer Commission]] || 50 || 0 || 0 || 0 || 0 || 0 || 0 || 50 || 0 || 0 || 0 || 0
|-
| [[File:Odobenus1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Odobenus_Class_Minelayer|Odobenus Class Minelayer]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Cetomagna1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Cetomagna_Class_Minehunter|Cetomagna Class Minehunter]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14

|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Mobile platforms, UAVs & aviation'''
|-
| [[File:Bulhanu1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Bulhanu_Class_High-Speed_Skirmisher_(Hoverbike)|Abeis-Bulhanu Class High-Speed Skirmisher (Hoverbike)]] || 1,290 || 133 || 107 || 129 || 9 || 5 || 37 || 53 || 79 || 43 || 109 || 584
|-
| [[File:Khamsin1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Bulhanu_Khamsin-Class_Desert_Skirmisher|Abeis-Bulhanu Khamsin-Class Desert Skirmisher]] || 1,063 || 110 || 89 || 107 || 7 || 4 || 31 || 43 || 65 || 35 || 90 || 481
|-
| [[File:Virelia1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Bulhanu_Virelia-Class_Urban_Pacifier|Abeis-Bulhanu Virelia-Class Urban Pacifier]] || 671 || 69 || 56 || 67 || 5 || 3 || 19 || 27 || 41 || 22 || 57 || 303
|-
| [[File:Hrimthurs1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Bulhanu_Hrimthurs-Class_Arctic_Hoverbike|Abeis-Bulhanu Hrimthurs-Class Arctic Hoverbike]] || 805 || 84 || 67 || 81 || 5 || 3 || 23 || 33 || 49 || 27 || 69 || 364
|-
| [[File:IsmaelHover1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Ismael_Class_“Cathartes”_Littoral_Hoverbike|Abeis-Ismael Class “Cathartes” Littoral Hoverbike]] || 857 || 89 || 72 || 86 || 6 || 3 || 25 || 35 || 52 || 28 || 73 || 387
|-
| [[File:Strix1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Empress_Class_“Strix-E”_Swarm-Interdictor_Littoral_Hoverbike|Abeis-Empress Class “Strix-E” Swarm-Interdictor Littoral Hoverbike]] || 683 || 71 || 57 || 69 || 5 || 3 || 19 || 27 || 41 || 22 || 58 || 309
|-
| [[File:Minerva1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Minerva_Class_Combat_Littoral_Strike_Aircraft|Abeis-Minerva Class Combat Littoral Strike Aircraft]] || 83 || 9 || 7 || 9 || 0 || 0 || 2 || 3 || 5 || 3 || 7 || 38
|-
| [[File:Boreas1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Boreas_Littoral_Dominance_Helicopter|Abeis-Boreas Littoral Dominance Helicopter]] || 134 || 14 || 11 || 14 || 1 || 0 || 4 || 5 || 8 || 4 || 11 || 61
|-
| [[File:AithraFighter1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aithra_Class_Proximity_Aircraft|Aithra Class Proximity Aircraft]] || 146 || 16 || 13 || 15 || 1 || 0 || 4 || 5 || 8 || 4 || 13 || 66
|-
| [[File:Intercept1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Symphonara_Class_Compound_Intercept|Symphonara Class Compound Intercept]] || 83 || 9 || 7 || 9 || 0 || 0 || 2 || 3 || 5 || 3 || 7 || 38
|-
| [[File:ThalassaCopt1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Thalassa_Class_Attack_Helicopter|Thalassa Class Attack Helicopter]] || 198 || 21 || 17 || 21 || 1 || 1 || 5 || 7 || 11 || 6 || 17 || 90
|-
| [[File:Catonisuav1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Catonis_Class_Unmanned_Aircraft_(UAV)|Catonis Class Unmanned Aircraft (UAV)]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Lotos1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Lotos_Class_Tactical_UAV|Lotos Class Tactical UAV]] || 167 || 18 || 14 || 17 || 1 || 1 || 4 || 6 || 10 || 5 || 15 || 75
|-
| [[File:Aurantius1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aurantius_Class_Multi-Role_UAV|Aurantius Class Multi-Role UAV]] || 167 || 18 || 14 || 17 || 1 || 1 || 4 || 6 || 10 || 5 || 15 || 75
|-
| [[File:SeaBomber1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Chrysos_Class_Sea_Bomber|Chrysos Class Sea Bomber]] || 218 || 23 || 19 || 22 || 1 || 1 || 6 || 8 || 13 || 7 || 19 || 99
|-
| [[File:MisttalonBomber1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Misttalon_Class_Heavy_Bomber|Misttalon Class Heavy Bomber]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Change into a 202512291428.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Orphali_Class_Tanker_Aircraft|Orphali Class Tanker Aircraft]] || 64 || 7 || 6 || 7 || 0 || 0 || 1 || 2 || 3 || 2 || 6 || 29
|-
| [[File:Ventiflor1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Ventiflor_Eye_Class_AEW&C_Aircraft|Ventiflor Eye Class AEW&C Aircraft]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Fluxus1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Fluxus_Class_Maritime_Patrol_Aircraft|Fluxus Class Maritime Patrol Aircraft]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Hostica1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Hostica_Class_Maritime_Patrol_Aircraft|Hostica Class Maritime Patrol Aircraft]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:NefelianTransport1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Nefelian_Transport_Plane|Nefelian Transport Plane]] || 115 || 12 || 10 || 12 || 1 || 0 || 3 || 4 || 6 || 3 || 10 || 52
|-
| [[File:Noctiluna1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Noctiluna_Class_Medium_Transport_Helicopter|Noctiluna Class Medium Transport Helicopter]] || 134 || 14 || 11 || 14 || 1 || 0 || 4 || 5 || 8 || 4 || 11 || 61
|-
| [[File:Umbraclaw1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Umbraclaw_Class_Heavy_Transport_Helicopter|Umbraclaw Class Heavy Transport Helicopter]] || 115 || 12 || 10 || 12 || 1 || 0 || 3 || 4 || 6 || 3 || 10 || 52
|}
]=]

--------------------------------------------------------------------------------
-- 2) Column mapping
--------------------------------------------------------------------------------
local COL_KEYS = { "VREG","ALPK","ODFR","HAFA","JESR","HMIN","NSJ","HBAS","BNOR","OURN","CKD" }
local COL_TO_DIV = {
	VREG="VAE", ALPK="ALP", ODFR="ODF",
	HAFA="HFA", JESR="HFA", HBAS="HFA",
	HMIN="HAT", NSJ="NSJ", BNOR="BNO", OURN="OUR", CKD="CKD",
}

--------------------------------------------------------------------------------
-- Hatch policy: always deploy 30 Ismael commissions (capped)
--------------------------------------------------------------------------------
local ALWAYS_HATCH_PRIVATEERS = 30
local ISMAEL_UNIT_NAME = "Ismael Class Privateer Commission"

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function push(t, v) t[#t+1] = v end
local function trim(s) s=tostring(s or ""); return (s:gsub("^%s+",""):gsub("%s+$","")) end

local function toNumber(s)
	s=tostring(s or "")
	local n=s:gsub("[^0-9]","")
	if n=="" then return nil end
	return tonumber(n)
end

local function fmt(n)
	if not n then return "" end
	local s=tostring(math.floor(n))
	local out=s:reverse():gsub("(%d%d%d)","%1,"):reverse()
	return out:gsub("^,","")
end

local function extractDisplayText(wikilinkCell)
	local c=tostring(wikilinkCell or "")
	local display=c:match("%[%[[^%]]-%|([^%]]-)%]%]")
	if display and display~="" then return display end
	display=c:match("%[%[([^%]]-)%]%]")
	if display and display~="" then return display end
	return trim(c:gsub("[%[%]]",""))
end

local function cellColor(pct)
	if pct <= 0.02 then return "#d4edda" end
	if pct <= 0.05 then return "#fff3cd" end
	if pct <= 0.10 then return "#ffe5cc" end
	return "#f8d7da"
end

local function normalizeWikitextTable(text)
	local out={}
	for line in tostring(text):gmatch("([^\n]*)\n?") do
		push(out, line:gsub("^[ \t]+",""))
	end
	return table.concat(out,"\n")
end

--------------------------------------------------------------------------------
-- Ops parsing helpers
--------------------------------------------------------------------------------
local function sanitizeOpsSegment(seg)
	seg=tostring(seg or "")
	seg=seg:gsub("<br%s*/?>","\n")
	seg=seg:gsub("<[^>]+>","")
	seg=seg:gsub("''+","")
	seg=seg:gsub("%[%[[^%]|]-|([^%]]-)%]%]","%1")
	seg=seg:gsub("%[%[([^%]]-)%]%]","%1")
	seg=seg:gsub("&nbsp;"," ")
	seg=seg:gsub("%s+"," ")
	return trim(seg)
end

local function parseFormation(seg)
	seg = sanitizeOpsSegment(seg)
	local divCode = seg:match("%((%u%u%u)%-[%w]+%s*%)%s*$")
	if not divCode then return nil,nil end
	local echelon = trim(seg:gsub("%(%u%u%u%-%w+%s*%)%s*$",""))
	return echelon, divCode
end

local function urgencyScore(urg)
	urg=tostring(urg or "")
	if urg=="CRITICAL" then return 4 end
	if urg=="HIGH" then return 3 end
	if urg=="MEDIUM" then return 2 end
	if urg=="LOW" then return 1 end
	return 0
end

local function parseImpact(impactCell)
	local s=tostring(impactCell or "")
	local g=s:match("Gnd:%s*([^;]+)") or "OK"
	local sea=s:match("Sea:%s*([^;]+)") or "OK"
	local air=s:match("Air:%s*([^;]+)") or "OK"
	return { ground=trim(g), sea=trim(sea), air=trim(air) }
end

local function impactAllows(imp, domain)
	if not imp then return true end
	local v = (domain=="sea" and imp.sea) or (domain=="air" and imp.air) or imp.ground
	v=tostring(v or "OK"):lower()
	if v:find("no%-go",1,true) then return false end
	if v:find("closed",1,true) then return false end
	return true
end

--------------------------------------------------------------------------------
-- Lua 5.1-safe deterministic picker
--------------------------------------------------------------------------------
local function hash32(s)
	s=tostring(s or "")
	local h=0
	for i=1,#s do
		h = (h*131 + s:byte(i)) % 4294967296
	end
	return h
end

local function pickDeterministic(list, k, salt)
	if not list or #list==0 or k<=0 then return {} end
	local picked, used = {}, {}
	local h = hash32(salt)
	for _=1,k do
		if #picked >= #list then break end
		h = (1103515245*h + 12345) % 4294967296
		local idx = (h % #list) + 1
		local guard=0
		while used[idx] and guard<100 do
			h = (1103515245*h + 12345) % 4294967296
			idx = (h % #list) + 1
			guard=guard+1
		end
		if not used[idx] then
			used[idx]=true
			picked[#picked+1]=list[idx]
		end
	end
	return picked
end

--------------------------------------------------------------------------------
-- Baseline TOE by echelon (canon)
--------------------------------------------------------------------------------
local TOE = {
	["Turmae"] = {
		["Active Units"]=120, ["Kalithros Class Rifle"]=120, ["Bubalus XT Class Revolver"]=20,
		["Syrinx Class Armored Infantry"]=2, ["Onceanic Recon Vehicle"]=1,
		["Corythia Class Dedicated Logistics and Transport Truck"]=2,
	},
	["Contubernia"] = {
		["Active Units"]=60, ["Kalithros Class Rifle"]=60, ["Bubalus XT Class Revolver"]=10,
		["Syrinx Class Armored Infantry"]=1, ["Corythia Class Dedicated Logistics and Transport Truck"]=1,
	},
	["Ordinis"] = {
		["Active Units"]=240, ["Kalithros Class Rifle"]=240, ["Delphica Class Grenadier Rifle"]=24,
		["Bubalus XT Class Revolver"]=40, ["Syrinx Class Armored Infantry"]=6, ["Onceanic Recon Vehicle"]=2,
		["Corythia Class Dedicated Logistics and Transport Truck"]=6,
	},
	["Manipuli"] = {
		["Active Units"]=900, ["Kalithros Class Rifle"]=900, ["Delphica Class Grenadier Rifle"]=120,
		["Chrysos Class Commando Rifle"]=60, ["Lothaya Class Sniper Rifle"]=18,
		["Bubalus XT Class Revolver"]=120, ["Cathartes Class RPG"]=18,
		["Syrinx Class Armored Infantry"]=24, ["Makra Class Battle Tank"]=2, ["Arachne Class Light Tank"]=4,
		["Ephyra Class Anti-Aircraft Vehicle"]=2, ["Corythia Class Dedicated Logistics and Transport Truck"]=30,
		["Ampelos Class Armored Recovery Vehicle"]=2,
	},
	["Centuriae"] = {
		["Active Units"]=2400, ["Kalithros Class Rifle"]=2400, ["Delphica Class Grenadier Rifle"]=360,
		["Chrysos Class Commando Rifle"]=120, ["Lothaya Class Sniper Rifle"]=36,
		["Bubalus XT Class Revolver"]=240, ["Cathartes Class RPG"]=36,
		["Syrinx Class Armored Infantry"]=80, ["Makra Class Battle Tank"]=8, ["Thalassa Class Main Battle Tank"]=4,
		["Arachne Class Light Tank"]=16, ["Aithra Class Howitzer"]=6, ["Aetheris Class Towed Howitzer"]=12,
		["Corythia Class Dedicated Logistics and Transport Truck"]=90, ["Ampelos Class Armored Recovery Vehicle"]=6,
		["Icaria Class Mine-Clearing / Combat Engineering Vehicle"]=4,
	},
	["Cohortes"] = {
		["Active Units"]=8000, ["Kalithros Class Rifle"]=8000, ["Delphica Class Grenadier Rifle"]=1200,
		["Chrysos Class Commando Rifle"]=400, ["Lothaya Class Sniper Rifle"]=120,
		["Bubalus XT Class Revolver"]=900, ["Cathartes Class RPG"]=120,
		["Syrinx Class Armored Infantry"]=240, ["Makra Class Battle Tank"]=24, ["Thalassa Class Main Battle Tank"]=12,
		["Arachne Class Light Tank"]=48, ["Aithra Class Howitzer"]=18, ["Aetheris Class Towed Howitzer"]=36,
		["Iynas Class Field Gun"]=12, ["Corythia Class Dedicated Logistics and Transport Truck"]=300,
		["Ampelos Class Armored Recovery Vehicle"]=18, ["Penthia Class Bridge-Laying Vehicle"]=4,
		["Icaria Class Mine-Clearing / Combat Engineering Vehicle"]=12,
	},
	["Birlik-i Gemi (Patrol Element)"] = { ["Aetherium Class Patrol Ship"]=1, ["Cetus Class Attack Craft"]=2 },
	["Tabur-i Derya (Task Unit)"] = {
		["Vaeringheim Class Corvette"]=2, ["Aetherium Class Patrol Ship"]=2, ["Cetus Class Attack Craft"]=6,
		["Odobenus Class Minelayer"]=1,
	},
	["Ordu (Task Group)"] = {
		["Sylvapolis Class Frigate"]=2, ["Vaeringheim Class Corvette"]=4, ["Aetherium Class Patrol Ship"]=6,
		["Cetus Class Attack Craft"]=12, ["Cetomagna Class Minehunter"]=1, ["Odobenus Class Minelayer"]=1,
	},
	["Zeygi (Element)"] = { ["Catonis Class Unmanned Aircraft (UAV)"]=1, ["Lotos Class Tactical UAV"]=1 },
	["Ptisis (Flight)"] = {
		["Thalassa Class Attack Helicopter"]=1, ["Noctiluna Class Medium Transport Helicopter"]=1,
		["Catonis Class Unmanned Aircraft (UAV)"]=2,
	},
	["Moira (Squadron)"] = {
		["Abeis-Boreas Littoral Dominance Helicopter"]=2, ["Thalassa Class Attack Helicopter"]=4,
		["Noctiluna Class Medium Transport Helicopter"]=2, ["Catonis Class Unmanned Aircraft (UAV)"]=6,
		["Aurantius Class Multi-Role UAV"]=2,
	},
}

--------------------------------------------------------------------------------
-- Coverage enforcement: ensure every unit exists in each TOE template (default 0)
--------------------------------------------------------------------------------
local function collectAllUnitsFromBase(baseText)
	local units={}
	for line in tostring(baseText):gmatch("[^\n]+") do
		if line:match("^|%s*%[%[File:") and line:find("||",1,true) then
			local parts = mw.text.split(line:gsub("^|%s*",""), "%s*||%s*")
			if #parts == 14 then
				local unitName = extractDisplayText(parts[2])
				if unitName ~= "" then units[unitName]=true end
			end
		end
	end
	return units
end

local function normalizeToeTemplates(allUnits)
	for _, toe in pairs(TOE) do
		if type(toe)=="table" then
			for unitName,_ in pairs(allUnits) do
				if toe[unitName]==nil then toe[unitName]=0 end
			end
		end
	end
end

--------------------------------------------------------------------------------
-- Pools: broad activation (all units) + typed small-arms pools (event-driven)
--------------------------------------------------------------------------------
local function normName(name)
	name=tostring(name or ""):lower()
	name=name:gsub("[^a-z0-9%-%s/()]", " "):gsub("%s+"," ")
	return trim(name)
end

local function rolesForUnit(unitName)
	local n=normName(unitName)
	local roles={}
	local function add(r) roles[r]=true end

	-- exclude aggregate personnel rows from rotation pools
	if n=="active units" or n=="reserve units" then
		add("personnel"); return roles
	end

	-- small arms (IMPORTANT: do NOT add to all_ground; keep event-driven)
	if n:find("rifle",1,true) or n:find("pistol",1,true) or n:find("revolver",1,true)
		or n:find("shotgun",1,true) or n:find("submachine",1,true) or n:find("dmr",1,true)
		or n:find("sniper",1,true) or n:find("rpg",1,true) then
		add("small_arms")
	end

	-- quadwalkers
	if n:find("quadwalker",1,true) then
		add("autonomous"); add("all_ground")
	end

	-- hover platforms / hoverbikes
	if n:find("hover",1,true) or n:find("hoverbike",1,true) then
		add("hover_mobility"); add("all_ground")
	end

	-- sea
	if n:find("ship",1,true) or n:find("corvette",1,true) or n:find("frigate",1,true) or n:find("cruiser",1,true)
		or n:find("battlecruiser",1,true) or n:find("submarine",1,true) or n:find("submersible",1,true)
		or n:find("gunboat",1,true) or n:find("minelayer",1,true) or n:find("minehunter",1,true)
		or n:find("landing ship",1,true) or n:find("airstrip barge",1,true) or n:find("privateer",1,true)
		or n:find("air%-caravan",1,true) then
		add("all_sea")
	end

	-- air
	if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
		or n:find("bomber",1,true) or n:find("intercept",1,true) or n:find("transport plane",1,true) then
		add("all_air")
	end

	-- ground vehicles/support (non-small-arms)
	if n:find("tank",1,true) or n:find("armored infantry",1,true) or n:find("recon vehicle",1,true)
		or n:find("command vehicle",1,true) or n:find("recovery vehicle",1,true)
		or n:find("bridge%-laying",1,true) or n:find("rail%-laying",1,true) or n:find("citadel train",1,true)
		or n:find("transport truck",1,true) or n:find("mine%-clearing",1,true) or n:find("engineering",1,true)
		or n:find("howitzer",1,true) or n:find("field gun",1,true) or n:find("mortar",1,true)
		or n:find("coastal gun",1,true) or n:find("coastal missile",1,true)
		or n:find("air defense system",1,true) or n:find("static emplacement",1,true) or n:find("thaad",1,true)
		or n:find("surveillance tower",1,true) then
		add("all_ground")
	end

	local any=false
	for _ in pairs(roles) do any=true break end
	if not any then add("misc") end

	return roles
end

local function buildPools(allUnits)
	local pools = {
		all_any={}, all_ground={}, all_sea={}, all_air={}, misc={},
		small_arms_all={},
		small_arms_rifle={}, small_arms_grenadier={}, small_arms_commando={},
		small_arms_sniper={}, small_arms_pistol={}, small_arms_revolver={},
		small_arms_smg={}, small_arms_shotgun={}, small_arms_dmr={}, small_arms_rpg={}
	}

	for unitName,_ in pairs(allUnits) do
		local n = normName(unitName)
		if n ~= "active units" and n ~= "reserve units" then
			local roles = rolesForUnit(unitName)
			pools.all_any[#pools.all_any+1] = unitName
			if roles.all_ground then pools.all_ground[#pools.all_ground+1] = unitName end
			if roles.all_sea then pools.all_sea[#pools.all_sea+1] = unitName end
			if roles.all_air then pools.all_air[#pools.all_air+1] = unitName end
			if roles.misc then pools.misc[#pools.misc+1] = unitName end

			if roles.small_arms then
				pools.small_arms_all[#pools.small_arms_all+1] = unitName
				local nn = normName(unitName)
				if nn:find("grenadier",1,true) then pools.small_arms_grenadier[#pools.small_arms_grenadier+1]=unitName
				elseif nn:find("commando",1,true) then pools.small_arms_commando[#pools.small_arms_commando+1]=unitName
				elseif nn:find("sniper",1,true) then pools.small_arms_sniper[#pools.small_arms_sniper+1]=unitName
				elseif nn:find("pistol",1,true) then pools.small_arms_pistol[#pools.small_arms_pistol+1]=unitName
				elseif nn:find("revolver",1,true) then pools.small_arms_revolver[#pools.small_arms_revolver+1]=unitName
				elseif nn:find("submachine",1,true) then pools.small_arms_smg[#pools.small_arms_smg+1]=unitName
				elseif nn:find("shotgun",1,true) then pools.small_arms_shotgun[#pools.small_arms_shotgun+1]=unitName
				elseif nn:find("dmr",1,true) then pools.small_arms_dmr[#pools.small_arms_dmr+1]=unitName
				elseif nn:find("rpg",1,true) then pools.small_arms_rpg[#pools.small_arms_rpg+1]=unitName
				elseif nn:find("rifle",1,true) then pools.small_arms_rifle[#pools.small_arms_rifle+1]=unitName
				end
			end
		end
	end

	for _,list in pairs(pools) do table.sort(list) end
	return pools
end

--------------------------------------------------------------------------------
-- Category-scaled background amounts (big enough to show meaningful “used”)
--------------------------------------------------------------------------------
local function unitCategory(unitName)
	local n = normName(unitName)
	if n:find("quadwalker",1,true) then return "autonomous" end
	if n:find("hover",1,true) or n:find("hoverbike",1,true) then return "hover_mobility" end

	if n:find("howitzer",1,true) or n:find("field gun",1,true) or n:find("mortar",1,true)
		or n:find("coastal gun",1,true) or n:find("coastal missile",1,true) then
		return "fires"
	end

	if n:find("air defense system",1,true) or n:find("static emplacement",1,true) or n:find("thaad",1,true)
		or n:find("surveillance tower",1,true) then
		return "fixed_defense"
	end

	if n:find("tank",1,true) or n:find("armored infantry",1,true) or n:find("recon vehicle",1,true)
		or n:find("command vehicle",1,true) or n:find("recovery vehicle",1,true)
		or n:find("bridge%-laying",1,true) or n:find("rail%-laying",1,true)
		or n:find("transport truck",1,true) or n:find("mine%-clearing",1,true) or n:find("engineering",1,true)
		or n:find("citadel train",1,true) then
		return "ground_vehicle"
	end

	if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
		or n:find("bomber",1,true) or n:find("intercept",1,true) or n:find("transport plane",1,true) then
		return "air"
	end

	if n:find("ship",1,true) or n:find("corvette",1,true) or n:find("frigate",1,true) or n:find("cruiser",1,true)
		or n:find("battlecruiser",1,true) or n:find("submarine",1,true) or n:find("submersible",1,true)
		or n:find("gunboat",1,true) or n:find("minelayer",1,true) or n:find("minehunter",1,true)
		or n:find("landing ship",1,true) or n:find("airstrip barge",1,true) or n:find("privateer",1,true)
		or n:find("air%-caravan",1,true) then
		return "sea"
	end

	return "misc"
end

local function breadthLevel(args)
	local b=tostring((args and args.breadth) or "standard"):lower()
	if b=="low" then return 1 end
	if b=="high" then return 3 end
	return 2
end

local function scaleFactor(uScore, bLevel)
	local s = 1
	if bLevel == 2 then s = 2 end
	if bLevel == 3 then s = 4 end
	if uScore >= 3 then s = s + 1 end
	if uScore >= 4 then s = s + 2 end
	return s
end

local function breadthAmountFor(unitName, uScore, bLevel)
	local cat = unitCategory(unitName)
	local s = scaleFactor(uScore, bLevel)
	if cat == "ground_vehicle" then return math.max(1, math.floor(2 * s)) end
	if cat == "autonomous" then return math.max(1, math.floor(2 * s)) end
	if cat == "hover_mobility" then return math.max(1, math.floor(3 * s)) end
	if cat == "fires" then return math.max(1, math.floor(1 * s)) end
	if cat == "fixed_defense" then return math.max(1, math.floor(1 * s)) end
	if cat == "air" then return math.max(1, math.floor(1 * s)) end
	if cat == "sea" then return math.max(1, math.floor(1 * s)) end
	return 1
end

--------------------------------------------------------------------------------
-- Demand accumulation
--------------------------------------------------------------------------------
local function addDemand(util, divCode, unitName, amount)
	if not divCode or divCode=="" or not unitName or unitName=="" or not amount or amount<=0 then return end
	util[divCode]=util[divCode] or {}
	util[divCode][unitName]=(util[divCode][unitName] or 0) + amount
end

--------------------------------------------------------------------------------
-- Event-driven small arms selection (no longer forced daily)
--------------------------------------------------------------------------------
local function addSmallArmsForOp(util, pools, divCode, opText, uScore, bLevel, salt)
	local op = normName(opText)
	local wants = {}

	local function want(poolName, k)
		wants[#wants+1] = { pool = poolName, k = k }
	end

	-- scale
	local s = 1
	if bLevel == 2 then s = 2 end
	if bLevel == 3 then s = 3 end
	if uScore >= 3 then s = s + 1 end
	if uScore >= 4 then s = s + 1 end

	-- Does this op plausibly call for infantry weapons today?
	local infantryRelevant =
		   op:find("vbss",1,true) or op:find("boarding",1,true) or op:find("interdiction",1,true)
		or op:find("contraband",1,true) or op:find("raid",1,true) or op:find("security",1,true)
		or op:find("riot",1,true) or op:find("stabil",1,true) or op:find("search",1,true)
		or op:find("cordon",1,true) or op:find("checkpoint",1,true) or op:find("corridor",1,true)
		or op:find("hostage",1,true) or op:find("sabotage",1,true)

	-- Hazard response is usually engineering-first; only include small arms if there’s unrest/looting/security language.
	if op:find("hazard",1,true) or op:find("evac",1,true) or op:find("debris",1,true) or op:find("clearance",1,true) then
		infantryRelevant = infantryRelevant or op:find("looting",1,true) or op:find("unrest",1,true) or op:find("security",1,true)
	end

	if not infantryRelevant then return end

	-- Choose an appropriate mix
	if op:find("vbss",1,true) or op:find("boarding",1,true) or op:find("interdiction",1,true) then
		want("small_arms_commando", 1 + (uScore>=3 and 1 or 0))
		want("small_arms_rifle", 1 + (uScore>=3 and 1 or 0))
		want("small_arms_pistol", 1)
		want("small_arms_dmr", (uScore>=3 and 1 or 0))
		want("small_arms_smg", (uScore>=3 and 1 or 0))
	elseif op:find("checkpoint",1,true) or op:find("corridor",1,true) or op:find("security",1,true) then
		want("small_arms_rifle", 2)
		want("small_arms_smg", 1)
		want("small_arms_dmr", (uScore>=3 and 1 or 0))
		want("small_arms_shotgun", (uScore>=3 and 1 or 0))
	elseif op:find("riot",1,true) or op:find("stabil",1,true) or op:find("urban",1,true) then
		want("small_arms_rifle", 2)
		want("small_arms_smg", 1)
		want("small_arms_commando", (uScore>=3 and 1 or 0))
		want("small_arms_pistol", 1)
	else
		want("small_arms_rifle", 2)
		want("small_arms_grenadier", (uScore>=3 and 1 or 0))
		want("small_arms_dmr", (uScore>=3 and 1 or 0))
	end

	-- Apply picks
	for _, w in ipairs(wants) do
		local poolList = pools[w.pool]
		if poolList and #poolList > 0 then
			local picks = pickDeterministic(poolList, w.k, salt.."|"..w.pool)
			for _, u in ipairs(picks) do
				local qty = 150 * s
				local nn = normName(u)
				if nn:find("commando",1,true) then qty = math.floor(qty * 0.40) end
				if nn:find("grenadier",1,true) then qty = math.floor(qty * 0.35) end
				if nn:find("sniper",1,true) then qty = math.floor(qty * 0.15) end
				if nn:find("pistol",1,true) or nn:find("revolver",1,true) then qty = math.floor(qty * 0.25) end
				if qty < 25 then qty = 25 end
				addDemand(util, divCode, u, qty)
			end
		end
	end
end

--------------------------------------------------------------------------------
-- Broad activation for all units (excluding small arms; those are event-driven)
--------------------------------------------------------------------------------
local function applyBreadthActivation(util, pools, divCode, opText, uScore, impact, salt, bLevel)
	-- Background activation: training, maintenance, readiness checks, unpublished activities.
	local baseK = (bLevel==1 and 2) or (bLevel==2 and 4) or 7
	if uScore>=3 then baseK = baseK + 2 end
	if uScore>=4 then baseK = baseK + 3 end
	if baseK > 12 then baseK = 12 end

	-- Ground (vehicles, drones, defenses; NOT small arms)
	local gPicks = pickDeterministic(pools.all_ground, baseK, salt.."|bg_ground")
	for _, u in ipairs(gPicks) do
		addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
	end

	-- Sea
	if impactAllows(impact,"sea") then
		local kSea = math.max(2, math.floor(baseK/2))
		local sPicks = pickDeterministic(pools.all_sea, kSea, salt.."|bg_sea")
		for _, u in ipairs(sPicks) do
			addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
		end
	end

	-- Air
	if impactAllows(impact,"air") then
		local kAir = math.max(2, math.floor(baseK/2))
		local aPicks = pickDeterministic(pools.all_air, kAir, salt.."|bg_air")
		for _, u in ipairs(aPicks) do
			addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
		end
	end

	-- Misc
	local mPicks = pickDeterministic(pools.misc, (bLevel>=2 and 2 or 1), salt.."|bg_misc")
	for _, u in ipairs(mPicks) do
		addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
	end

	-- Event-driven small arms (only when appropriate)
	addSmallArmsForOp(util, pools, divCode, opText, uScore, bLevel, salt.."|event_small_arms")
end

--------------------------------------------------------------------------------
-- Ismael commissions: treat NSJ column availability as HAT for this unit only
--------------------------------------------------------------------------------
local function divOverrideForUnit(unitName, colKey, defaultDiv)
	if unitName == ISMAEL_UNIT_NAME then
		if colKey == "NSJ" then return "HAT" end
	end
	return defaultDiv
end

--------------------------------------------------------------------------------
-- Compute utilization from DailyOps (TOE + breadth activation + hatch privateers)
--------------------------------------------------------------------------------
local function computeUtilization(args, pools)
	local util, debugRows = {}, {}
	local ok, ops = pcall(require, "Module:WarLeagueDivisionOpsBVR")
	if not ok or not ops or type(ops.makeTable) ~= "function" then
		return util, debugRows, false
	end

	local rows = tonumber(args.opsrows) or tonumber(args.rows) or 36
	local seed = args.seed
	local bLevel = breadthLevel(args)

	local tbl = ops.makeTable({ args = {
		rows = rows,
		seed = seed,
		showlegend = "no",
		flagsize = args.flagsize or "20",
		division = args.division or "",
	}})

	-- avoid applying breadth multiple times per same (div, op-row, domain)
	local activated = {}

	local function activateOnce(divCode, opText, uScore, impact, salt)
		activated[divCode] = activated[divCode] or {}
		if activated[divCode][salt] then return end
		activated[divCode][salt] = true
		applyBreadthActivation(util, pools, divCode, opText, uScore, impact, salt, bLevel)
	end

	local function handleCell(forceCell, opText, uScore, impact, saltBase)
		forceCell=tostring(forceCell or "—")
		if forceCell=="—" or trim(forceCell)=="" then return end
		forceCell = forceCell:gsub("<br%s*/?>","\n")

		for _,seg in ipairs(mw.text.split(forceCell,"\n")) do
			seg=trim(seg)
			if seg~="" and seg~="—" then
				local echelon, divCode = parseFormation(seg)
				if echelon and divCode then
					-- baseline TOE demand
					local toe = TOE[echelon]
					if toe then
						for unitName, amount in pairs(toe) do
							if amount and amount>0 then addDemand(util, divCode, unitName, amount) end
						end
					end
					-- breadth once
					activateOnce(divCode, opText, uScore, impact, saltBase.."|"..divCode)
				end
			end
		end
	end

	for line in tostring(tbl):gmatch("[^\n]+") do
		if line:match("^|%s") and line:find("||",1,true) then
			local parts = mw.text.split(line:gsub("^|%s*",""), "%s*||%s*")
			-- expected: date, division, city, daily event, urgency, ops alert, ops impact, operation, ground, naval, air, ...
			if #parts >= 11 then
				local dateCell = parts[1] or ""
				local urg = parts[5] or ""
				local uScore = urgencyScore(urg)
				local impact = parseImpact(parts[7] or "")
				local opText = parts[8] or ""
				local ground = parts[9] or "—"
				local naval  = parts[10] or "—"
				local air    = parts[11] or "—"

				local salt = tostring(seed or "").."|"..tostring(dateCell).."|"..tostring(parts[2] or "").."|"..tostring(parts[3] or "").."|"..tostring(opText)

				push(debugRows, {
					date=dateCell, division=parts[2] or "", city=parts[3] or "",
					urgency=urg, impact=parts[7] or "", op=opText,
					ground=ground, naval=naval, air=air
				})

				if impactAllows(impact,"ground") then handleCell(ground, opText, uScore, impact, salt.."|G") end
				if impactAllows(impact,"sea") then handleCell(naval, opText, uScore, impact, salt.."|N") end
				if impactAllows(impact,"air") then handleCell(air, opText, uScore, impact, salt.."|A") end
			end
		end
	end

	-- Hatch always deploys 30 Ismael commissions (capped in overlay)
	addDemand(util, "HAT", ISMAEL_UNIT_NAME, ALWAYS_HATCH_PRIVATEERS)

	return util, debugRows, true
end

--------------------------------------------------------------------------------
-- Overlay highlights (NEVER exceed availability)
--------------------------------------------------------------------------------
local function overlayTable(baseText, util)
	local out={}
	local inMain=false

	for line in tostring(baseText):gmatch("([^\n]*)\n?") do
		if line:match("^%{%|") then
			inMain=true
			push(out,line)
		elseif inMain and line:match("^|%}%s*$") then
			inMain=false
			push(out,line)
		elseif inMain and line:match("^|%s*%[%[File:") and line:find("||",1,true) then
			local parts = mw.text.split(line:gsub("^|%s*",""), "%s*||%s*")
			if #parts == 14 then
				local unitName = extractDisplayText(parts[2])

				local divCols, availByIdx = {}, {}
				for i=4,14 do
					local colKey = COL_KEYS[i-3]
					local defaultDiv = COL_TO_DIV[colKey]
					local divCode = divOverrideForUnit(unitName, colKey, defaultDiv)

					local avail = toNumber(parts[i]) or 0
					availByIdx[i]=avail
					if divCode then
						divCols[divCode]=divCols[divCode] or {}
						divCols[divCode][#divCols[divCode]+1] = { idx=i, avail=avail }
					end
				end

				local usedByIdx = {}

				for divCode, cols in pairs(divCols) do
					local demand = (util[divCode] and util[divCode][unitName]) or 0
					if demand > 0 then
						local totalAvail = 0
						for _,c in ipairs(cols) do totalAvail = totalAvail + (c.avail or 0) end
						local usedTotal = math.min(demand, totalAvail)

						if totalAvail>0 and usedTotal>0 then
							local assigned=0
							for _,c in ipairs(cols) do
								local share = math.floor(usedTotal * c.avail / totalAvail)
								if share > c.avail then share = c.avail end
								usedByIdx[c.idx] = share
								assigned = assigned + share
							end

							local rem = usedTotal - assigned
							if rem > 0 then
								table.sort(cols, function(x,y)
									local cx=(x.avail or 0) - (usedByIdx[x.idx] or 0)
									local cy=(y.avail or 0) - (usedByIdx[y.idx] or 0)
									return cx > cy
								end)
								local safety=0
								while rem>0 and safety<2000 do
									safety=safety+1
									local progressed=false
									for _,c in ipairs(cols) do
										if rem<=0 then break end
										local cap=(c.avail or 0) - (usedByIdx[c.idx] or 0)
										if cap>0 then
											usedByIdx[c.idx]=(usedByIdx[c.idx] or 0)+1
											rem=rem-1
											progressed=true
										end
									end
									if not progressed then break end
								end
							end
						end
					end
				end

				for i=4,14 do
					local avail=availByIdx[i] or 0
					local used=usedByIdx[i] or 0
					if used>avail then used=avail end
					if avail>0 and used>0 then
						local pct=used/avail; if pct>1 then pct=1 end
						local bg=cellColor(pct)
						local usedText=string.format("<br/><span style=\"font-size:85%%;\"><b>used:</b> %s</span>", fmt(used))
						parts[i]=string.format("style=\"background:%s; text-align:center;\" | %s%s", bg, parts[i], usedText)
					end
				end

				push(out, "| " .. table.concat(parts, " || "))
			else
				push(out,line)
			end
		else
			push(out,line)
		end
	end

	push(out,"")
	push(out,"<div style=\"margin-top:0.5em; font-size:90%;\">")
	push(out,"<b>Daily overview:</b> usage is a capped estimate derived from War League daily ops plus routine training/maintenance/unpublished activity.")
	push(out,"<br/><b>Hard rule:</b> used never exceeds availability in any cell or across a shared ops-division group.")
	push(out,"<br/><b>Hatch Ministry:</b> always attempts to deploy 30 <i>Ismael Class Privateer</i> commissions (capped to what exists).")
	push(out,"<br/><b>Small arms:</b> selected only when appropriate to the day’s operations (VBSS, security, unrest, etc.).")
	push(out,"</div>")

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

--------------------------------------------------------------------------------
-- Public entrypoint
--------------------------------------------------------------------------------
function p.render(frame)
	local args = (frame and frame.args) or {}

	local base = normalizeWikitextTable(RAW_BASE_TABLE)

	-- TOE coverage
	local allUnits = collectAllUnitsFromBase(base)
	normalizeToeTemplates(allUnits)

	-- pools from embedded inventory (so all units can be reached)
	local pools = buildPools(allUnits)

	-- compute usage
	local util, debugRows, ok = computeUtilization(args, pools)
	if not ok then
		return base .. "\n<div style=\"margin-top:0.5em; font-size:90%;\"><b>Note:</b> Ops module unavailable; showing base table only.</div>"
	end

	local rendered = overlayTable(base, util)

	-- debug
	if tostring(args.debug or ""):lower() == "yes" then
		local max = tonumber(args.debugrows) or 8
		local dbg={}
		push(dbg,"")
		push(dbg,"<div style=\"margin-top:0.75em; font-size:90%;\">")
		push(dbg,"<b>Debug (proof of source):</b> Parsed rows from <i>Module:WarLeagueDivisionOpsBVR</i> (Operation/Urgency/Impact + force cells).")
		push(dbg,"<ul>")
		for i=1, math.min(max, #debugRows) do
			local r=debugRows[i]
			push(dbg, string.format(
				"<li><b>%s</b> | %s | %s | <b>%s</b><br/>Impact: %s<br/>Op: %s<br/>G: %s<br/>N: %s<br/>A: %s</li>",
				tostring(r.date or ""), tostring(r.division or ""), tostring(r.city or ""),
				tostring(r.urgency or ""), tostring(r.impact or ""), tostring(r.op or ""),
				tostring(r.ground or ""), tostring(r.naval or ""), tostring(r.air or "")
			))
		end
		push(dbg,"</ul></div>")
		rendered = rendered .. table.concat(dbg,"\n")
	end

	return rendered
end

return p