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 | ||
local p = {} | local p = {} | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- 1) FULL EMBEDDED TABLE | -- 1) FULL EMBEDDED TABLE | ||
-- Paste your full table into RAW_BASE_TABLE exactly as written on the wiki page. | |||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local RAW_BASE_TABLE = [=[ | local RAW_BASE_TABLE = [=[ | ||
PASTE YOUR FULL TABLE HERE (unchanged) | |||
]=] | ]=] | ||
| Line 241: | Line 14: | ||
-- 2) Column order must match the 11 investor/division columns in the table | -- 2) Column order must match the 11 investor/division columns in the table | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local COL_KEYS = { | local COL_KEYS = { "VREG","ALPK","ODFR","HAFA","JESR","HMIN","NSJ","HBAS","BNOR","OURN","CKD" } | ||
} | |||
local COL_TO_DIV = { | local COL_TO_DIV = { | ||
VREG = "VAE", | VREG="VAE", ALPK="ALP", ODFR="ODF", | ||
HAFA="HFA", JESR="HFA", HBAS="HFA", | |||
HMIN="HAT", NSJ="NSJ", BNOR="BNO", OURN="OUR", CKD="CKD", | |||
HAFA = "HFA", | |||
HMIN = "HAT", | |||
} | } | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- 3) TO&E templates (edit to match canon) | -- 3) TO&E templates (edit to match canon) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local TOE = { | local TOE = { | ||
["Turmae"] = { | ["Turmae"] = { | ||
["Active Units"] = 120, | ["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, | |||
["Syrinx Class Armored Infantry"] = 2, | |||
["Corythia Class Dedicated Logistics and Transport Truck"] = 2, | |||
}, | }, | ||
["Contubernia"] = { | ["Contubernia"] = { | ||
["Active Units"] = 60, | ["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, | |||
["Syrinx Class Armored Infantry"] = 1, | |||
}, | }, | ||
["Ordinis"] = { | ["Ordinis"] = { | ||
["Active Units"] = 240, | ["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, | |||
["Bubalus XT Class Revolver"] = 40, | |||
["Corythia Class Dedicated Logistics and Transport Truck"] = 6, | |||
}, | }, | ||
["Manipuli"] = { | ["Manipuli"] = { | ||
["Active Units"] = 900, | ["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, | |||
["Chrysos Class Commando Rifle"] = 60, | ["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, | |||
["Bubalus XT Class Revolver"] = 120, | ["Ampelos Class Armored Recovery Vehicle"]=2, | ||
["Syrinx Class Armored Infantry"] = 24, | |||
["Ephyra Class Anti-Aircraft Vehicle"] = 2, | |||
["Ampelos Class Armored Recovery Vehicle"] = 2, | |||
}, | }, | ||
["Centuriae"] = { | ["Centuriae"] = { | ||
["Active Units"] = 2400, | ["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, | |||
["Chrysos Class Commando Rifle"] = 120, | ["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, | |||
["Bubalus XT Class Revolver"] = 240, | ["Corythia Class Dedicated Logistics and Transport Truck"]=90, ["Ampelos Class Armored Recovery Vehicle"]=6, | ||
["Icaria Class Mine-Clearing / Combat Engineering Vehicle"]=4, | |||
["Syrinx Class Armored Infantry"] = 80, | |||
["Arachne Class Light Tank"] = 16, | |||
["Corythia Class Dedicated Logistics and Transport Truck"] = 90, | |||
["Icaria Class Mine-Clearing / Combat Engineering Vehicle"] = 4, | |||
}, | }, | ||
["Cohortes"] = { | ["Cohortes"] = { | ||
["Active Units"] = 8000, | ["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, | |||
["Chrysos Class Commando Rifle"] = 400, | ["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, | |||
["Bubalus XT Class Revolver"] = 900, | ["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, | |||
["Syrinx Class Armored Infantry"] = 240, | ["Icaria Class Mine-Clearing / Combat Engineering Vehicle"]=12, | ||
["Arachne Class Light Tank"] = 48, | |||
["Iynas Class Field Gun"] = 12, | |||
["Ampelos Class Armored Recovery Vehicle"] = 18, | |||
["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)"] = { | ["Tabur-i Derya (Task Unit)"] = { | ||
["Vaeringheim Class Corvette"] = 2, | ["Vaeringheim Class Corvette"]=2, ["Aetherium Class Patrol Ship"]=2, ["Cetus Class Attack Craft"]=6, | ||
["Odobenus Class Minelayer"]=1, | |||
["Odobenus Class Minelayer"] = 1, | |||
}, | }, | ||
["Ordu (Task Group)"] = { | ["Ordu (Task Group)"] = { | ||
["Sylvapolis Class Frigate"] = 2, | ["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, | |||
["Cetus Class Attack Craft"] = 12, | |||
}, | }, | ||
["Zeygi (Element)"] = { ["Catonis Class Unmanned Aircraft (UAV)"]=1, ["Lotos Class Tactical UAV"]=1 }, | |||
["Ptisis (Flight)"] = { | ["Ptisis (Flight)"] = { | ||
["Thalassa Class Attack Helicopter"] = 1, | ["Thalassa Class Attack Helicopter"]=1, ["Noctiluna Class Medium Transport Helicopter"]=1, | ||
["Catonis Class Unmanned Aircraft (UAV)"]=2, | |||
["Catonis Class Unmanned Aircraft (UAV)"] = 2, | |||
}, | }, | ||
["Moira (Squadron)"] = { | ["Moira (Squadron)"] = { | ||
["Abeis-Boreas Littoral Dominance Helicopter"] = 2, | ["Abeis-Boreas Littoral Dominance Helicopter"]=2, ["Thalassa Class Attack Helicopter"]=4, | ||
["Noctiluna Class Medium Transport Helicopter"]=2, ["Catonis Class Unmanned Aircraft (UAV)"]=6, | |||
["Noctiluna Class Medium Transport Helicopter"] = 2, | ["Aurantius Class Multi-Role UAV"]=2, | ||
["Aurantius Class Multi-Role UAV"] = 2, | |||
}, | }, | ||
} | } | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Helpers (use push() everywhere to avoid table.insert mistakes) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function push(t, v) t[#t+1] = v end | |||
local function trim(s) | local function trim(s) | ||
s = tostring(s or "") | s = tostring(s or "") | ||
| Line 400: | Line 109: | ||
local s = tostring(math.floor(n)) | local s = tostring(math.floor(n)) | ||
local out = s:reverse():gsub("(%d%d%d)", "%1,"):reverse() | local out = s:reverse():gsub("(%d%d%d)", "%1,"):reverse() | ||
return out:gsub("^,", "") | |||
end | end | ||
| Line 414: | Line 122: | ||
local function cellColor(pct) | local function cellColor(pct) | ||
if pct <= 0.02 then return "#d4edda" end | if pct <= 0.02 then return "#d4edda" end | ||
if pct <= 0.05 then return "#fff3cd" end | if pct <= 0.05 then return "#fff3cd" end | ||
| Line 422: | Line 129: | ||
local function normalizeWikitextTable(text) | local function normalizeWikitextTable(text) | ||
local out = {} | local out = {} | ||
for line in tostring(text):gmatch("([^\n]*)\n?") do | for line in tostring(text):gmatch("([^\n]*)\n?") do | ||
-- | -- do not break on blank lines | ||
push(out, line:gsub("^[ \t]+", "")) | |||
end | end | ||
return table.concat(out, "\n") | return table.concat(out, "\n") | ||
| Line 432: | Line 138: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Coverage enforcement (no unit excluded) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function collectAllUnitsFromBase(baseText) | local function collectAllUnitsFromBase(baseText) | ||
local units = {} | local units = {} | ||
for line in tostring(baseText):gmatch("[^\n]+") do | for line in tostring(baseText):gmatch("[^\n]+") do | ||
if line:match("^|%s*%[%[File:") and line:find("||", 1, true) then | if line:match("^|%s*%[%[File:") and line:find("||", 1, true) then | ||
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*") | local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*") | ||
if #parts == 14 then | if #parts == 14 then | ||
local unitName = extractDisplayText(parts[2]) | local unitName = extractDisplayText(parts[2]) | ||
if | if unitName ~= "" then units[unitName] = true end | ||
end | end | ||
end | end | ||
| Line 452: | Line 155: | ||
local function normalizeToeTemplates(allUnits) | local function normalizeToeTemplates(allUnits) | ||
for | for _, toe in pairs(TOE) do | ||
if type(toe) == "table" then | if type(toe) == "table" then | ||
for unitName, _ in pairs(allUnits) do | for unitName, _ in pairs(allUnits) do | ||
if toe[unitName] == nil then | if toe[unitName] == nil then toe[unitName] = 0 end | ||
end | end | ||
end | end | ||
| Line 465: | Line 166: | ||
local function buildToeCoverageReport(allUnits) | local function buildToeCoverageReport(allUnits) | ||
local lines = {} | local lines = {} | ||
push(lines, "''TO&E Coverage Report (any unit not explicitly counted defaults to 0):''") | |||
local echelons = {} | local echelons = {} | ||
for echelon, _ in pairs(TOE) do | for echelon, _ in pairs(TOE) do push(echelons, echelon) end | ||
table.sort(echelons) | table.sort(echelons) | ||
for _, echelon in ipairs(echelons) do | for _, echelon in ipairs(echelons) do | ||
| Line 475: | Line 176: | ||
if toe[unitName] == 0 then fallback = fallback + 1 end | if toe[unitName] == 0 then fallback = fallback + 1 end | ||
end | end | ||
push(lines, string.format("* <b>%s</b>: %d fallback rows", echelon, fallback)) | |||
end | end | ||
return table.concat(lines, "\n") | return table.concat(lines, "\n") | ||
| Line 481: | Line 182: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Parse ops module -> utilization demand by DIV and Unit | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function computeUtilization(args) | local function computeUtilization(args) | ||
local util = {} | local util = {} | ||
local debugRows = {} | local debugRows = {} | ||
local function bump(div, unitName, amount) | local function bump(div, unitName, amount) | ||
| Line 512: | Line 213: | ||
if line:match("^|%s") and line:find("||", 1, true) then | if line:match("^|%s") and line:find("||", 1, true) then | ||
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*") | local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*") | ||
if #parts >= 11 then | if #parts >= 11 then | ||
local | local r = { | ||
date = parts[1] or "", division = parts[2] or "", city = parts[3] or "", | |||
ground = parts[9] or "—", naval = parts[10] or "—", air = parts[11] or "—" | |||
} | |||
push(debugRows, r) | |||
local function handle(forceCell) | local function handle(forceCell) | ||
| Line 530: | Line 224: | ||
if forceCell == "—" or trim(forceCell) == "" then return end | if forceCell == "—" or trim(forceCell) == "" then return end | ||
-- | -- normalize <br> to newlines | ||
forceCell = forceCell:gsub("<br%s*/?>", "\n") | forceCell = forceCell:gsub("<br%s*/?>", "\n") | ||
| Line 536: | Line 230: | ||
seg = trim(seg) | seg = trim(seg) | ||
if seg ~= "" and seg ~= "—" then | if seg ~= "" and seg ~= "—" then | ||
local divCode = seg:match("%((%u%u%u)%-[%w]+%s*%)%s*$") | local divCode = seg:match("%((%u%u%u)%-[%w]+%s*%)%s*$") | ||
if divCode then | if divCode then | ||
local echelon = trim(seg:gsub("%(%u%u%u%-%w+%s*%)%s*$", "")) | local echelon = trim(seg:gsub("%(%u%u%u%-%w+%s*%)%s*$", "")) | ||
local toe = TOE[echelon] | local toe = TOE[echelon] | ||
if toe then | if toe then | ||
for unitName, amount in pairs(toe) do | for unitName, amount in pairs(toe) do | ||
if amount and amount > 0 then | if amount and amount > 0 then bump(divCode, unitName, amount) end | ||
end | end | ||
end | end | ||
| Line 554: | Line 244: | ||
end | end | ||
handle(ground) | handle(r.ground) | ||
handle(naval) | handle(r.naval) | ||
handle(air) | handle(r.air) | ||
end | end | ||
end | end | ||
| Line 565: | Line 255: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Overlay highlights (NEVER exceeding availability) | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
local function overlayTable(baseText, util) | local function overlayTable(baseText, util) | ||
local out = {} | local out = {} | ||
local | local inTable = false | ||
for line in tostring(baseText):gmatch("([^\n]*)\n?") do | for line in tostring(baseText):gmatch("([^\n]*)\n?") do | ||
if line:match("^%{%|") then | if line:match("^%{%|") then | ||
inTable = true | |||
push(out, line) | |||
elseif | elseif inTable and line:match("^|%}%s*$") then | ||
inTable = false | |||
push(out, line) | |||
elseif | elseif inTable and line:match("^|%s*%[%[File:") and line:find("||", 1, true) then | ||
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*") | local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*") | ||
if #parts == 14 then | if #parts == 14 then | ||
local unitName = extractDisplayText(parts[2]) | local unitName = extractDisplayText(parts[2]) | ||
-- | -- group columns by ops division for this row | ||
local divCols = { | local divCols, availByIdx = {}, {} | ||
for i = 4, 14 do | for i = 4, 14 do | ||
local colKey = COL_KEYS[i - 3] | local colKey = COL_KEYS[i - 3] | ||
| Line 595: | Line 282: | ||
if divCode then | if divCode then | ||
divCols[divCode] = divCols[divCode] or {} | divCols[divCode] = divCols[divCode] or {} | ||
push(divCols[divCode], { idx = i, avail = avail }) | |||
end | end | ||
end | end | ||
-- | -- allocate demand across columns, capped by availability | ||
local usedByIdx = {} | local usedByIdx = {} | ||
for divCode, cols in pairs(divCols) do | for divCode, cols in pairs(divCols) do | ||
| Line 610: | Line 297: | ||
if totalAvail > 0 and usedTotal > 0 then | if totalAvail > 0 and usedTotal > 0 then | ||
local assigned = 0 | local assigned = 0 | ||
for _, c in ipairs(cols) do | for _, c in ipairs(cols) do | ||
| Line 619: | Line 305: | ||
end | end | ||
local rem = usedTotal - assigned | local rem = usedTotal - assigned | ||
if rem > 0 then | if rem > 0 then | ||
| Line 639: | Line 324: | ||
rem = rem - 1 | rem = rem - 1 | ||
progressed = true | progressed = true | ||
end | end | ||
end | end | ||
| Line 649: | Line 333: | ||
end | end | ||
-- | -- apply styling based on allocated used (hard-capped) | ||
for i = 4, 14 do | for i = 4, 14 do | ||
local avail = availByIdx[i] or 0 | local avail = availByIdx[i] or 0 | ||
local used = usedByIdx[i] or 0 | local used = usedByIdx[i] or 0 | ||
if used > avail then used = avail end | if used > avail then used = avail end | ||
if avail > 0 and used > 0 then | if avail > 0 and used > 0 then | ||
local pct = used / avail | local pct = used / avail | ||
if pct > 1 then pct = 1 end | if pct > 1 then pct = 1 end | ||
local bg = cellColor(pct) | local bg = cellColor(pct) | ||
local usedText = string.format( | 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) | |||
parts[i] = string.format( | |||
end | end | ||
end | end | ||
push(out, "| " .. table.concat(parts, " || ")) | |||
else | else | ||
push(out, line) | |||
end | end | ||
else | else | ||
push(out, line) | |||
end | end | ||
end | end | ||
push(out, "") | |||
push(out, "<div style=\"margin-top:0.5em; font-size:90%;\">") | |||
push(out, "<b>Utilization highlight:</b> cells tint when that column’s mapped ops-division drew from that row today (TO&E-based).") | |||
push(out, "<br/><b>Hard rule:</b> used is allocated and capped so it never exceeds availability in any cell or across a shared ops-division column-group.") | |||
push(out, "</div>") | |||
return table.concat(out, "\n") | return table.concat(out, "\n") | ||
| Line 696: | Line 366: | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
-- | -- Public entrypoint | ||
-------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ||
function p.render(frame) | function p.render(frame) | ||
local args = (frame and frame.args) or {} | local args = (frame and frame.args) or {} | ||
local base = normalizeWikitextTable(RAW_BASE_TABLE) | local base = normalizeWikitextTable(RAW_BASE_TABLE) | ||
local allUnits = collectAllUnitsFromBase(base) | local allUnits = collectAllUnitsFromBase(base) | ||
normalizeToeTemplates(allUnits) | normalizeToeTemplates(allUnits) | ||
if tostring(args.report or ""):lower() == "yes" then | if tostring(args.report or ""):lower() == "yes" then | ||
return buildToeCoverageReport(allUnits) | return buildToeCoverageReport(allUnits) | ||
| Line 714: | Line 381: | ||
local util, debugRows, ok = computeUtilization(args) | local util, debugRows, ok = computeUtilization(args) | ||
if not ok then | 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>" | return base .. "\n<div style=\"margin-top:0.5em; font-size:90%;\"><b>Note:</b> Ops module unavailable; showing base table only.</div>" | ||
| Line 721: | Line 387: | ||
local rendered = overlayTable(base, util) | local rendered = overlayTable(base, util) | ||
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 | ||
local dbg = {} | local dbg = {} | ||
push(dbg, "") | |||
push(dbg, "<div style=\"margin-top:0.75em; font-size:90%;\">") | |||
push(dbg, "<b>Debug (proof of source):</b> Parsed deployments from <i>Module:WarLeagueDivisionOpsBVR</i> output used for today/seed.") | |||
if args.seed and tostring(args.seed) ~= "" then | if args.seed and tostring(args.seed) ~= "" then | ||
push(dbg, "<br/><b>Seed override:</b> " .. tostring(args.seed)) | |||
else | else | ||
push(dbg, "<br/><b>Seed:</b> default (ops module uses its internal day-based seed when not provided).") | |||
end | end | ||
push(dbg, "<ul>") | |||
for i = 1, math.min(max, #debugRows) do | for i = 1, math.min(max, #debugRows) do | ||
local r = debugRows[i] | local r = debugRows[i] | ||
push(dbg, string.format( | |||
"<li><b>%s</b> | %s | %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.ground or ""), tostring(r.naval or ""), tostring(r.air or "") | |||
)) | |||
end | end | ||
push(dbg, "</ul>") | |||
push(dbg, "</div>") | |||
rendered = rendered .. table.concat(dbg, "\n") | rendered = rendered .. table.concat(dbg, "\n") | ||
end | end | ||
Revision as of 22:44, 26 January 2026
Documentation for this module may be created at Module:WarLeagueUnitUtilizationBVR/doc
-- Module:WarLeagueUnitUtilizationBVR
local p = {}
--------------------------------------------------------------------------------
-- 1) FULL EMBEDDED TABLE
-- Paste your full table into RAW_BASE_TABLE exactly as written on the wiki page.
--------------------------------------------------------------------------------
local RAW_BASE_TABLE = [=[
PASTE YOUR FULL TABLE HERE (unchanged)
]=]
--------------------------------------------------------------------------------
-- 2) Column order must match the 11 investor/division columns in the table
--------------------------------------------------------------------------------
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",
}
--------------------------------------------------------------------------------
-- 3) TO&E templates (edit to match 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,
},
}
--------------------------------------------------------------------------------
-- Helpers (use push() everywhere to avoid table.insert mistakes)
--------------------------------------------------------------------------------
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
-- do not break on blank lines
push(out, line:gsub("^[ \t]+", ""))
end
return table.concat(out, "\n")
end
--------------------------------------------------------------------------------
-- Coverage enforcement (no unit excluded)
--------------------------------------------------------------------------------
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
local function buildToeCoverageReport(allUnits)
local lines = {}
push(lines, "''TO&E Coverage Report (any unit not explicitly counted defaults to 0):''")
local echelons = {}
for echelon, _ in pairs(TOE) do push(echelons, echelon) end
table.sort(echelons)
for _, echelon in ipairs(echelons) do
local toe = TOE[echelon]
local fallback = 0
for unitName, _ in pairs(allUnits) do
if toe[unitName] == 0 then fallback = fallback + 1 end
end
push(lines, string.format("* <b>%s</b>: %d fallback rows", echelon, fallback))
end
return table.concat(lines, "\n")
end
--------------------------------------------------------------------------------
-- Parse ops module -> utilization demand by DIV and Unit
--------------------------------------------------------------------------------
local function computeUtilization(args)
local util = {}
local debugRows = {}
local function bump(div, unitName, amount)
if not div or div == "" or not unitName or unitName == "" or not amount or amount <= 0 then return end
util[div] = util[div] or {}
util[div][unitName] = (util[div][unitName] or 0) + amount
end
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 tbl = ops.makeTable({ args = {
rows = rows,
seed = seed,
showlegend = "no",
flagsize = args.flagsize or "20",
division = args.division or "",
}})
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*")
if #parts >= 11 then
local r = {
date = parts[1] or "", division = parts[2] or "", city = parts[3] or "",
ground = parts[9] or "—", naval = parts[10] or "—", air = parts[11] or "—"
}
push(debugRows, r)
local function handle(forceCell)
forceCell = tostring(forceCell or "—")
if forceCell == "—" or trim(forceCell) == "" then return end
-- normalize <br> to newlines
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 divCode = seg:match("%((%u%u%u)%-[%w]+%s*%)%s*$")
if divCode then
local echelon = trim(seg:gsub("%(%u%u%u%-%w+%s*%)%s*$", ""))
local toe = TOE[echelon]
if toe then
for unitName, amount in pairs(toe) do
if amount and amount > 0 then bump(divCode, unitName, amount) end
end
end
end
end
end
end
handle(r.ground)
handle(r.naval)
handle(r.air)
end
end
end
return util, debugRows, true
end
--------------------------------------------------------------------------------
-- Overlay highlights (NEVER exceeding availability)
--------------------------------------------------------------------------------
local function overlayTable(baseText, util)
local out = {}
local inTable = false
for line in tostring(baseText):gmatch("([^\n]*)\n?") do
if line:match("^%{%|") then
inTable = true
push(out, line)
elseif inTable and line:match("^|%}%s*$") then
inTable = false
push(out, line)
elseif inTable 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])
-- group columns by ops division for this row
local divCols, availByIdx = {}, {}
for i = 4, 14 do
local colKey = COL_KEYS[i - 3]
local divCode = COL_TO_DIV[colKey]
local avail = toNumber(parts[i]) or 0
availByIdx[i] = avail
if divCode then
divCols[divCode] = divCols[divCode] or {}
push(divCols[divCode], { idx = i, avail = avail })
end
end
-- allocate demand across columns, capped by availability
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(a, b)
local ca = (a.avail or 0) - (usedByIdx[a.idx] or 0)
local cb = (b.avail or 0) - (usedByIdx[b.idx] or 0)
return ca > cb
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
-- apply styling based on allocated used (hard-capped)
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>Utilization highlight:</b> cells tint when that column’s mapped ops-division drew from that row today (TO&E-based).")
push(out, "<br/><b>Hard rule:</b> used is allocated and capped so it never exceeds availability in any cell or across a shared ops-division column-group.")
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)
local allUnits = collectAllUnitsFromBase(base)
normalizeToeTemplates(allUnits)
if tostring(args.report or ""):lower() == "yes" then
return buildToeCoverageReport(allUnits)
end
local util, debugRows, ok = computeUtilization(args)
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)
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 deployments from <i>Module:WarLeagueDivisionOpsBVR</i> output used for today/seed.")
if args.seed and tostring(args.seed) ~= "" then
push(dbg, "<br/><b>Seed override:</b> " .. tostring(args.seed))
else
push(dbg, "<br/><b>Seed:</b> default (ops module uses its internal day-based seed when not provided).")
end
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<br/>G: %s<br/>N: %s<br/>A: %s</li>",
tostring(r.date or ""), tostring(r.division or ""), tostring(r.city or ""),
tostring(r.ground or ""), tostring(r.naval or ""), tostring(r.air or "")
))
end
push(dbg, "</ul>")
push(dbg, "</div>")
rendered = rendered .. table.concat(dbg, "\n")
end
return rendered
end
return p