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
-- Displays the Unit Numbers table exactly as authored, with per-cell color coding based on
-- "today's" War League deployments pulled from Module:WarLeagueDivisionOpsBVR.makeTable().
--
-- Key guarantees:
--  (A) Table renders identically to the original markup (no early truncation; no indentation-preformat issues)
--  (B) Utilization is derived from the ops module output for the day/seed
--  (C) NEVER uses more of an item than is available in a given cell or across the mapped column-group
--  (D) No unit row is excluded from TO&E coverage (missing keys default to 0, not skipped)
--
-- Usage:
--  {{#invoke:WarLeagueUnitUtilizationBVR|render}}
--  {{#invoke:WarLeagueUnitUtilizationBVR|render|seed=520154|opsrows=36}}
--  {{#invoke:WarLeagueUnitUtilizationBVR|render|debug=yes}}
--  {{#invoke:WarLeagueUnitUtilizationBVR|render|report=yes}}


local p = {}
local p = {}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 1) FULL EMBEDDED TABLE (exact markup, no extra indentation)
-- 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 = [=[
{| class="wikitable sortable" style="width:100%;"
PASTE YOUR FULL TABLE HERE (unchanged)
|+ '''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
|}
]=]
]=]


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" }
"VREG","ALPK","ODFR","HAFA","JESR","HMIN",
"NSJ","HBAS","BNOR","OURN","CKD"
}


-- Column -> ops division code (the code embedded in unit IDs like (ODF-01), (HFA-N07), etc.)
local COL_TO_DIV = {
local COL_TO_DIV = {
VREG = "VAE",
VREG="VAE", ALPK="ALP", ODFR="ODF",
ALPK = "ALP",
HAFA="HFA", JESR="HFA", HBAS="HFA",
ODFR = "ODF",
HMIN="HAT", NSJ="NSJ", BNOR="BNO", OURN="OUR", CKD="CKD",
HAFA = "HFA",
JESR = "HFA", -- if you add a Jeseri ops division later, change this mapping
HMIN = "HAT",
NSJ = "NSJ",
HBAS = "HFA",
BNOR = "BNO",
OURN = "OUR",
CKD = "CKD",
}
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 3) TO&E templates (edit to match canon)
-- 3) TO&E templates (edit to match canon)
-- Coverage is enforced: every unit in the table exists in every TOE[echelon] (default 0).
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local TOE = {
local TOE = {
["Turmae"] = {
["Turmae"] = {
["Active Units"] = 120,
["Active Units"]=120, ["Kalithros Class Rifle"]=120, ["Bubalus XT Class Revolver"]=20,
["Kalithros Class Rifle"] = 120,
["Syrinx Class Armored Infantry"]=2, ["Onceanic Recon Vehicle"]=1,
["Bubalus XT Class Revolver"] = 20,
["Corythia Class Dedicated Logistics and Transport Truck"]=2,
["Syrinx Class Armored Infantry"] = 2,
["Onceanic Recon Vehicle"] = 1,
["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,
["Kalithros Class Rifle"] = 60,
["Syrinx Class Armored Infantry"]=1, ["Corythia Class Dedicated Logistics and Transport Truck"]=1,
["Bubalus XT Class Revolver"] = 10,
["Syrinx Class Armored Infantry"] = 1,
["Corythia Class Dedicated Logistics and Transport Truck"] = 1,
},
},
["Ordinis"] = {
["Ordinis"] = {
["Active Units"] = 240,
["Active Units"]=240, ["Kalithros Class Rifle"]=240, ["Delphica Class Grenadier Rifle"]=24,
["Kalithros Class Rifle"] = 240,
["Bubalus XT Class Revolver"]=40, ["Syrinx Class Armored Infantry"]=6, ["Onceanic Recon Vehicle"]=2,
["Delphica Class Grenadier Rifle"] = 24,
["Corythia Class Dedicated Logistics and Transport Truck"]=6,
["Bubalus XT Class Revolver"] = 40,
["Syrinx Class Armored Infantry"] = 6,
["Onceanic Recon Vehicle"] = 2,
["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,
["Kalithros Class Rifle"] = 900,
["Chrysos Class Commando Rifle"]=60, ["Lothaya Class Sniper Rifle"]=18,
["Delphica Class Grenadier Rifle"] = 120,
["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,
["Lothaya Class Sniper Rifle"] = 18,
["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,
["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"] = {
["Centuriae"] = {
["Active Units"] = 2400,
["Active Units"]=2400, ["Kalithros Class Rifle"]=2400, ["Delphica Class Grenadier Rifle"]=360,
["Kalithros Class Rifle"] = 2400,
["Chrysos Class Commando Rifle"]=120, ["Lothaya Class Sniper Rifle"]=36,
["Delphica Class Grenadier Rifle"] = 360,
["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,
["Lothaya Class Sniper Rifle"] = 36,
["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,
["Cathartes Class RPG"] = 36,
["Icaria Class Mine-Clearing / Combat Engineering Vehicle"]=4,
["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"] = {
["Cohortes"] = {
["Active Units"] = 8000,
["Active Units"]=8000, ["Kalithros Class Rifle"]=8000, ["Delphica Class Grenadier Rifle"]=1200,
["Kalithros Class Rifle"] = 8000,
["Chrysos Class Commando Rifle"]=400, ["Lothaya Class Sniper Rifle"]=120,
["Delphica Class Grenadier Rifle"] = 1200,
["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,
["Lothaya Class Sniper Rifle"] = 120,
["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,
["Cathartes Class RPG"] = 120,
["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,
["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,
},
},
["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,
["Aetherium Class Patrol Ship"] = 2,
["Odobenus Class Minelayer"]=1,
["Cetus Class Attack Craft"] = 6,
["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,
["Vaeringheim Class Corvette"] = 4,
["Cetus Class Attack Craft"]=12, ["Cetomagna Class Minehunter"]=1, ["Odobenus Class Minelayer"]=1,
["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,
},
},
["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,
["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,
["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,
["Catonis Class Unmanned Aircraft (UAV)"] = 6,
["Aurantius Class Multi-Role UAV"] = 2,
},
},
}
}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 4) Helpers
-- 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()
out = out:gsub("^,", "")
return out:gsub("^,", "")
return out
end
end


Line 414: Line 122:


local function cellColor(pct)
local function cellColor(pct)
-- pct in [0..1]
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)
-- Ensure no accidental leading indentation breaks table parsing (preformatted blocks).
local out = {}
local out = {}
for line in tostring(text):gmatch("([^\n]*)\n?") do
for line in tostring(text):gmatch("([^\n]*)\n?") do
-- keep even blank lines (do NOT early-break)
-- do not break on blank lines
table.insert(out, line:gsub("^[ \t]+", ""))
push(out, line:gsub("^[ \t]+", ""))
end
end
return table.concat(out, "\n")
return table.concat(out, "\n")
Line 432: Line 138:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 5) Coverage enforcement: ensure every unit in the embedded table exists in every TOE template
-- 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
-- Only rows with 14 cells: image || unit || total || 11 columns
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 unitName and unitName ~= "" then
if unitName ~= "" then units[unitName] = true end
units[unitName] = true
end
end
end
end
end
Line 452: Line 155:


local function normalizeToeTemplates(allUnits)
local function normalizeToeTemplates(allUnits)
for echelon, toe in pairs(TOE) do
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
toe[unitName] = 0
end
end
end
end
end
Line 465: Line 166:
local function buildToeCoverageReport(allUnits)
local function buildToeCoverageReport(allUnits)
local lines = {}
local lines = {}
table.insert(lines, "''TO&E Coverage Report (any unit not explicitly counted defaults to 0):''")
push(lines, "''TO&E Coverage Report (any unit not explicitly counted defaults to 0):''")
local echelons = {}
local echelons = {}
for echelon, _ in pairs(TOE) do table.insert(echelons, echelon) end
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
table.insert(lines, string.format("* <b>%s</b>: %d fallback rows", echelon, fallback))
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:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 6) Parse ops module -> utilization demand by ops DIVCODE and UnitName
-- Parse ops module -> utilization demand by DIV and Unit
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function computeUtilization(args)
local function computeUtilization(args)
local util = {}     -- util[DIV][UnitName] = demand
local util = {}
local debugRows = {} -- proof of what we read from ops table
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*")
-- indices per ops header: 1 Date, 2 Division, 3 City/Node, ... 9 Ground, 10 Naval, 11 Air
if #parts >= 11 then
if #parts >= 11 then
local dateCell  = parts[1] or ""
local r = {
local divCell  = parts[2] or ""
date = parts[1] or "", division = parts[2] or "", city = parts[3] or "",
local cityCell  = parts[3] or ""
ground = parts[9] or "—", naval = parts[10] or "—", air = parts[11] or "—"
local ground   = parts[9] or "—"
}
local naval     = parts[10] or "—"
push(debugRows, r)
local air       = parts[11] or "—"
 
table.insert(debugRows, {
date = dateCell, division = divCell, city = cityCell,
ground = ground, naval = naval, air = air
})


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


-- Convert all <br>, <br/>, <br /> to newlines so split works reliably
-- 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
-- Expect div code at END like (ODF-01) or (ODF-N07) or (ODF-A12)
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
-- Remove trailing (DIV-ID) to get echelon name, preserving inner parentheses like "(Patrol Element)"
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
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:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 7) Overlay highlights onto the embedded table (NEVER exceeding availability)
-- Overlay highlights (NEVER exceeding availability)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function overlayTable(baseText, util)
local function overlayTable(baseText, util)
local out = {}
local out = {}
local inMainTable = false
local inTable = false


for line in tostring(baseText):gmatch("([^\n]*)\n?") do
for line in tostring(baseText):gmatch("([^\n]*)\n?") do
-- Do NOT break on blank lines (keeps the table identical)
if line:match("^%{%|") then
if line:match("^%{%|") then
inMainTable = true
inTable = true
table.insert(out, line)
push(out, line)
elseif inMainTable and line:match("^|%}%s*$") then
elseif inTable and line:match("^|%}%s*$") then
inMainTable = false
inTable = false
table.insert(out, line)
push(out, line)
elseif inMainTable and line:match("^|%s*%[%[File:") and line:find("||", 1, true) then
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])


-- Build per-division column groups for this row
-- group columns by ops division for this row
local divCols = {}    -- divCols[DIV] = { {idx=i, avail=n}, ... }
local divCols, availByIdx = {}, {}
local availByIdx = {} -- availByIdx[i] = n
 
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 {}
table.insert(divCols[divCode], { idx = i, avail = avail })
push(divCols[divCode], { idx = i, avail = avail })
end
end
end
end


-- Allocate/cap demand across columns so we never exceed availability
-- allocate demand across columns, capped by availability
local usedByIdx = {} -- usedByIdx[i] = allocated used (0..avail)
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
-- Pass 1: proportional floor
local assigned = 0
local assigned = 0
for _, c in ipairs(cols) do
for _, c in ipairs(cols) do
Line 619: Line 305:
end
end


-- Pass 2: distribute remainder one-by-one into remaining capacity
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
if rem <= 0 then break end
end
end
end
end
Line 649: Line 333:
end
end


-- Apply styling based on allocated used (hard-capped by avail)
-- 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 -- hard safety
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))
"<br/><span style=\"font-size:85%%;\"><b>used:</b> %s</span>",
parts[i] = string.format("style=\"background:%s; text-align:center;\" | %s%s", bg, parts[i], usedText)
fmt(used)
)
parts[i] = string.format(
"style=\"background:%s; text-align:center;\" | %s%s",
bg,
parts[i],
usedText
)
end
end
end
end


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


-- Legend (kept outside the table; does not alter table structure)
push(out, "")
table.insert(out, "")
push(out, "<div style=\"margin-top:0.5em; font-size:90%;\">")
table.insert(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).")
table.insert(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.")
table.insert(out, "<br/><span style=\"background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;\">Low</span> ")
push(out, "</div>")
table.insert(out, "<span style=\"background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;\">Moderate</span> ")
table.insert(out, "<span style=\"background:#ffe5cc;padding:2px 6px;border:1px solid #ffddaa;\">High</span> ")
table.insert(out, "<span style=\"background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;\">Very high</span>")
table.insert(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.")
table.insert(out, "</div>")


return table.concat(out, "\n")
return table.concat(out, "\n")
Line 696: Line 366:


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


-- Normalize table wikitext to avoid preformatted rendering from indentation
local base = normalizeWikitextTable(RAW_BASE_TABLE)
local base = normalizeWikitextTable(RAW_BASE_TABLE)


-- Enforce TO&E coverage (no unit excluded)
local allUnits = collectAllUnitsFromBase(base)
local allUnits = collectAllUnitsFromBase(base)
normalizeToeTemplates(allUnits)
normalizeToeTemplates(allUnits)


-- Optional maintenance report
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)


-- Optional debug: show what we actually read from the ops module table
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 = {}
table.insert(dbg, "")
push(dbg, "")
table.insert(dbg, "<div style=\"margin-top:0.75em; font-size:90%;\">")
push(dbg, "<div style=\"margin-top:0.75em; font-size:90%;\">")
table.insert(dbg, "<b>Debug (proof of source):</b> Parsed deployments from <i>Module:WarLeagueDivisionOpsBVR</i> output used for today/seed.")
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
table.insert(dbg, "<br/><b>Seed override:</b> " .. tostring(args.seed))
push(dbg, "<br/><b>Seed override:</b> " .. tostring(args.seed))
else
else
table.insert(dbg, "<br/><b>Seed:</b> default (ops module uses day-based seed when not provided).")
push(dbg, "<br/><b>Seed:</b> default (ops module uses its internal day-based seed when not provided).")
end
end
table.insert(dbg, "<ul>")
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]
table.insert(dbg,
push(dbg, string.format(
string.format(
"<li><b>%s</b> | %s | %s<br/>G: %s<br/>N: %s<br/>A: %s</li>",
"<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.date or ""),
tostring(r.ground or ""), tostring(r.naval or ""), tostring(r.air or "")
tostring(r.division or ""),
))
tostring(r.city or ""),
tostring(r.ground or ""),
tostring(r.naval or ""),
tostring(r.air or "")
)
)
end
end
table.insert(dbg, "</ul>")
push(dbg, "</ul>")
table.insert(dbg, "</div>")
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