Module:WarLeagueUnitUtilizationBVR: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
(41 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:WarLeagueUnitUtilizationBVR
-- Module:WarLeagueUnitUtilizationBVR
--
-- Displays the embedded War League inventory table and overlays a daily “used” estimate by division,
-- derived from Module:WarLeagueDivisionOpsBVR (formations + daily ops context) PLUS routine activity.
--
-- Civilian militia (Baratar) integration:
--  • Baratar personnel rows are inventory-only (never consumed by War League TOE demand)
--  • Baratar weapons are deployable ONLY for internal/domestic ops (riot, security, checkpoints, unrest, etc.)
--  • Leading Baratar icons are stripped for internal matching
--  • Grenades are classified as event-driven munitions (high-intensity internal ops)
--
-- Guarantees:
--  1) Table renders exactly as authored (no early break on blank lines; indentation stripped).
--  2) Robust formation parsing (naval/air echelons with parentheses + wiki markup).
--  3) “Used” NEVER exceeds availability (per cell, and per shared-division column-group).
--  4) All units are potentially activatable over time (broad background activation).
--  5) Small arms are NOT forced daily; they are selected only when appropriate to the day’s events.
--  6) Hatch Ministry always deploys 30 Ismael Class Privateers (capped to availability).
--  7) Hover platforms & quadwalkers are explicitly handled (category + activation).
--
-- Invoke:
--  {{#invoke:WarLeagueUnitUtilizationBVR|render}}
--  {{#invoke:WarLeagueUnitUtilizationBVR|render|breadth=low}}        -- low|standard|high
--  {{#invoke:WarLeagueUnitUtilizationBVR|render|breadth=high}}
--  {{#invoke:WarLeagueUnitUtilizationBVR|render|seed=520154|opsrows=40}}
--  {{#invoke:WarLeagueUnitUtilizationBVR|render|debug=yes|debugrows=8}}


local p = {}
local p = {}


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 1) FULL EMBEDDED TABLE
-- 1) FULL EMBEDDED TABLE (embedded exactly as provided)
-- 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)
{| class="wikitable sortable" style="width:100%;"
|+ '''Unit Numbers (Activated 32/3/52 PSSC)'''
! rowspan="2" | Image
! rowspan="2" | Unit
! rowspan="2" | Total
! colspan="6" | Regional investors
! colspan="5" | Service divisions / national allocations
|-
! [[File:VaeringheimDivisionFlag.png|25px]] Vaeringheim Regional Investor
! [[File:AlpazkigzDivisionFlag.png|25px]] Alpazkigz Regional Investor
! [[File:OdiferiaFlag.png|25px]] Odiferia Regional Investor
! [[File:HafaanFlag.png|25px]] Hafaan Regional Investor
! [[File:JeseriFlag.png|25px]] Jeseri Regional Investor
! [[File:HatchMinistryFlag.png|25px]] Hatch Ministry Regional Investor
! [[File:NewSouthJangsongFlag.png|25px]] New South Jangsong Division
! [[File:HaifaDivisionFlag.png|25px]] Haifan Bassaridia Division
! [[File:BassaridianNormarkFlagB.png|25px]] Bassaridian Normark
! [[File:OurianaFlag.png|25px]] Ouriana Division
! [[File:CouncilofKingsFlag.png|25px]] Council of Kings Division
 
|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Personnel & readiness'''
|-
| [[File:ActiveUnit1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Active_Units|Active Units]] || 249,973 || 25,866 || 20,847 || 25,042 || 1,749 || 1,008 || 7,246 || 10,187 || 15,385 || 8,354 || 21,206 || 113,083
|-
| [[File:ReserveSoldier1226.jpeg|150px|border]]  || [[Somniant_Stock_Fund_Military_Hardware#Reserve_Units|Reserve Units]] || 305,525 || 31,615 || 25,480 || 30,608 || 2,138 || 1,232 || 8,857 || 12,451 || 18,803 || 10,211 || 25,918 || 138,212
|-
| [[File:BaratarActive2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Active Units''' || 1,316,449 || 148,005 || 165,738 || 136,255 || 11,394 || 5,002 || 48,964 || 69,264 || 73,785 || 45,868 || 143,225 || 468,949
|-
| [[File:BaratarReserve2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Reserve Units''' || 1,608,990 || 180,894 || 202,568 || 166,534 || 13,926 || 6,114 || 59,844 || 84,656 || 90,182 || 56,061 || 175,052 || 573,159
 
|-
! 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:Doryon2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Doryon Rifle 7.62×39mm''' || 574,077 || 64,542 || 72,275 || 59,418 || 4,969 || 2,181 || 21,352 || 30,205 || 32,176 || 20,002 || 62,458 || 204,499
|-
|[[File:Aurean2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Aurean Carbine 6.5mm Compact''' || 511,166 || 57,469 || 64,355 || 52,907 || 4,424 || 1,942 || 19,012 || 26,895 || 28,650 || 17,810 || 55,613 || 182,089
|-
| [[File:Kleisthenes.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Kleisthenes Carbine 5.45mm Folding Stock''' || 425,972 || 47,891 || 53,629 || 44,089 || 3,687 || 1,619 || 15,843 || 22,412 || 23,875 || 14,842 || 46,344 || 151,741
|-
| [[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:Skopion2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Skopion Sniper Rifle 8.6mm with 5x Scope''' || 78,043 || 8,807 || 9,863 || 8,113 || 678 || 297 || 2,915 || 4,123 || 4,390 || 2,731 || 8,516 || 28,610
|-
| [[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:Lykastos2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Lykastos Pistol 9mm Swampproof''' || 738,354 || 83,011 || 92,957 || 76,421 || 6,391 || 2,806 || 27,462 || 38,848 || 41,384 || 25,726 || 80,330 || 263,018
|-
| [[File:Phokion2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Phokion Sidearm 10mm Officer Issue''' || 331,477 || 37,249 || 41,711 || 34,291 || 2,868 || 1,259 || 12,323 || 17,432 || 18,570 || 11,544 || 36,046 || 118,184
|-
| [[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: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:Sphex2026 (2).jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Sphex SMG 5.7mm Short Barrel''' || 60,831 || 6,515 || 7,297 || 6,000 || 502 || 220 || 2,157 || 3,048 || 3,245 || 2,018 || 6,313 || 23,516
|-
| [[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:Orontes2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Orontes Shotgun 12-Gauge Semi-Auto''' || 207,519 || 23,413 || 26,219 || 21,555 || 1,802 || 791 || 7,746 || 10,957 || 11,672 || 7,256 || 22,657 || 73,451
|-
| [[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
|-
| [[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:Myrrhex2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Myrrhex Grenade Mk.IV Incense Compound''' || 375,794 || 42,256 || 47,325 || 38,920 || 3,256 || 1,430 || 13,975 || 19,770 || 21,060 || 13,080 || 40,948 || 134,774
 
|-
|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Hellhound detachments (Hellhound Breeders of Dragevik)'''
|-
|[[File:HellhoundScout1.jpeg|150px|border]] || Hellhound (scout) || 510 || 510 || 491 || 468 || 35 || 35 || 167 || 192 || 203 || 187 || 213 || 2,719
|-
|[[File:Hellhoundsoldier1.jpeg|150px|border]]|| Hellhound (soldier) || 680 || 680 || 655 || 624 || 35 || 35 || 167 || 192 || 203 || 187 || 213 || 3,422
|-
| [[File:HellhoundSpy1.jpeg|150px|border]] || Hellhound (spy) || 2,380 || 2,380 || 2,294 || 2,186 || 140 || 140 || 749 || 866 || 911 || 842 || 960 || 12,561
 
|-
! 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
|-
| [[File:SomniantBastion2026.jpeg|150px|border]] || [[Eosphorus_Motor_Company#Class_SZ-88_Somniant_Bastion|Class SZ-88 Somniant Bastion]] || 158 || 30 || 30 || 26 || 2 || 1 || 8 || 12 || 12 || 9 || 22 || 6
|-
! 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;" | '''Maritime security services (Nefelean Seas Armory)'''
|-
| [[File:ReconDolphin.jpeg|150px|border]] || Reconnaissance Patrol (armored dolphin pair) || 62,657 || 6,120 || 5,898 || 5,620 || 421 || 421 || 1,998 || 2,310 || 2,431 || 2,245 || 2,561 || 32,636
|-
| [[File:EscortDolphin.jpeg|150px|border]] || Escort Run (convoy / VIP transit) || 65,799 || 6,460 || 6,226 || 5,933 || 421 || 421 || 2,082 || 2,406 || 2,532 || 2,339 || 2,667 || 34,317
|-
| [[File:Defensedolphin.jpeg|150px|border]] || Harbor Defense Shift (night watch) || 95,178 || 9,350 || 9,011 || 8,587 || 631 || 631 || 2,997 || 3,464 || 3,646 || 3,368 || 3,841 || 49,656
|-
| [[File:Hydrophonedolphin.jpeg|150px|border]]  || Hydrophone Sweep & Echolocation Mapping || 46,993 || 4,590 || 4,424 || 4,215 || 316 || 316 || 1,499 || 1,732 || 1,823 || 1,684 || 1,921 || 24,477
|-
| [[File:Interceptdolphin.jpeg|150px|border]]  || Rapid Intercept / Deterrence Callout || 116,064 || 11,390 || 10,977 || 10,460 || 772 || 772 || 3,663 || 4,234 || 4,456 || 4,116 || 4,694 || 60,534
|-
 
! 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:Osirios1227.jpeg|149px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Osiríos_Class_CAS_Gunship|Abeis-Osiríos Class CAS Gunship]] || 185 || 18 || 18 || 15 || 1 || 1 || 5 || 6 || 7 || 5 || 18 || 92
|-
| [[File:Boreas1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Boreas_Littoral_Dominance_Helicopter|Abeis-Boreas Littoral Dominance Helicopter]] || 134 || 14 || 11 || 14 || 1 || 0 || 4 || 5 || 8 || 4 || 11 || 61
|-
| [[File:AithraFighter1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aithra_Class_Proximity_Aircraft|Aithra Class Proximity Aircraft]] || 146 || 16 || 13 || 15 || 1 || 0 || 4 || 5 || 8 || 4 || 13 || 66
|-
| [[File:Intercept1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Symphonara_Class_Compound_Intercept|Symphonara Class Compound Intercept]] || 83 || 9 || 7 || 9 || 0 || 0 || 2 || 3 || 5 || 3 || 7 || 38
|-
| [[File:ThalassaCopt1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Thalassa_Class_Attack_Helicopter|Thalassa Class Attack Helicopter]] || 198 || 21 || 17 || 21 || 1 || 1 || 5 || 7 || 11 || 6 || 17 || 90
|-
| [[File:Catonisuav1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Catonis_Class_Unmanned_Aircraft_(UAV)|Catonis Class Unmanned Aircraft (UAV)]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Lotos1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Lotos_Class_Tactical_UAV|Lotos Class Tactical UAV]] || 167 || 18 || 14 || 17 || 1 || 1 || 4 || 6 || 10 || 5 || 15 || 75
|-
| [[File:Aurantius1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aurantius_Class_Multi-Role_UAV|Aurantius Class Multi-Role UAV]] || 167 || 18 || 14 || 17 || 1 || 1 || 4 || 6 || 10 || 5 || 15 || 75
|-
| [[File:SeaBomber1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Chrysos_Class_Sea_Bomber|Chrysos Class Sea Bomber]] || 218 || 23 || 19 || 22 || 1 || 1 || 6 || 8 || 13 || 7 || 19 || 99
|-
| [[File:MisttalonBomber1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Misttalon_Class_Heavy_Bomber|Misttalon Class Heavy Bomber]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Change into a 202512291428.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Orphali_Class_Tanker_Aircraft|Orphali Class Tanker Aircraft]] || 64 || 7 || 6 || 7 || 0 || 0 || 1 || 2 || 3 || 2 || 6 || 29
|-
| [[File:Ventiflor1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Ventiflor_Eye_Class_AEW&C_Aircraft|Ventiflor Eye Class AEW&C Aircraft]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Fluxus1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Fluxus_Class_Maritime_Patrol_Aircraft|Fluxus Class Maritime Patrol Aircraft]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Hostica1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Hostica_Class_Maritime_Patrol_Aircraft|Hostica Class Maritime Patrol Aircraft]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:NefelianTransport1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Nefelian_Transport_Plane|Nefelian Transport Plane]] || 115 || 12 || 10 || 12 || 1 || 0 || 3 || 4 || 6 || 3 || 10 || 52
|-
| [[File:Noctiluna1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Noctiluna_Class_Medium_Transport_Helicopter|Noctiluna Class Medium Transport Helicopter]] || 134 || 14 || 11 || 14 || 1 || 0 || 4 || 5 || 8 || 4 || 11 || 61
|-
| [[File:Umbraclaw1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Umbraclaw_Class_Heavy_Transport_Helicopter|Umbraclaw Class Heavy Transport Helicopter]] || 115 || 12 || 10 || 12 || 1 || 0 || 3 || 4 || 6 || 3 || 10 || 52
|}
 
]=]
]=]


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 2) Column order must match the 11 investor/division columns in the table
-- 2) Column mapping
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local COL_KEYS = { "VREG","ALPK","ODFR","HAFA","JESR","HMIN","NSJ","HBAS","BNOR","OURN","CKD" }
local COL_KEYS = { "VREG","ALPK","ODFR","HAFA","JESR","HMIN","NSJ","HBAS","BNOR","OURN","CKD" }
local COL_TO_DIV = {
local COL_TO_DIV = {
VREG="VAE", ALPK="ALP", ODFR="ODF",
VREG="VAE", ALPK="ALP", ODFR="ODF",
Line 23: Line 308:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- 3) TO&E templates (edit to match canon)
-- Hatch policy: always deploy 30 Ismael commissions (capped)
--------------------------------------------------------------------------------
local ALWAYS_HATCH_PRIVATEERS = 30
local ISMAEL_UNIT_NAME = "Ismael Class Privateer Commission"
--------------------------------------------------------------------------------
-- Hatch policy: always deploy its single V-99 train (capped)
--------------------------------------------------------------------------------
local ALWAYS_HATCH_V99 = 1
local HATCH_V99_UNIT_NAME = "Model V-99 Thunderguard Citadel Train"
 
--------------------------------------------------------------------------------
local ALWAYS_SZ88_MIN = {
VAE = 18,
ALP = 18,
ODF = 15,
HFA = 1,
JES = 1,
HAT = 5,
NSJ = 7,
HBD = 7,
BNO = 5,
OUR = 7,
CKD = 2,
}
local SZ88_UNIT_NAME = "Class SZ-88 Somniant Bastion"
 
--------------------------------------------------------------------------------
-- Council of Kings policy: always use at least 20% of available CKD forces
--------------------------------------------------------------------------------
local CKD_MIN_FORCE_USE_PCT = 0.20
 
--------------------------------------------------------------------------------
-- Hellhound policy: always deploy a minimum daily presence (capped in overlay)
--------------------------------------------------------------------------------
-- IMPORTANT: these strings MUST exactly match the unit names in RAW_BASE_TABLE.
-- They already exist in the table at lines 111–117.
local HELLHOUND_SCOUT_KEY  = "Hellhound (scout)"
local HELLHOUND_SOLDIER_KEY = "Hellhound (soldier)"
local HELLHOUND_SPY_KEY    = "Hellhound (spy)"
 
-- Minimum DAILY demand added every day. Overlay later caps by availability.
local ALWAYS_HELLHOUNDS_LOW  = { scout = 8,  soldier = 12, spy = 3 }
local ALWAYS_HELLHOUNDS_STD  = { scout = 15, soldier = 25, spy = 6 }
local ALWAYS_HELLHOUNDS_HIGH = { scout = 25, soldier = 40, spy = 10 }
 
-- Divisions that should always show Hellhound presence
-- NOTE: These are DIV CODES (same as used elsewhere: VAE/ALP/ODF/HFA/HAT/NSJ/BNO/OUR/CKD)
local HELLHOUND_DUTY_DIVS = { "VAE","ALP","ODF","HFA","HAT","NSJ","BNO","OUR","CKD" }
 
--------------------------------------------------------------------------------
-- Hellhound surge policy: occasionally “exercise the full kennel”
-- (overlay caps to availability, so large demand is safe)
--------------------------------------------------------------------------------
-- How often a FULL surge occurs (lower = more frequent).
local HELLHOUND_FULL_SURGE_EVERY_LOW  = 21  -- about ~1-in-21 days
local HELLHOUND_FULL_SURGE_EVERY_STD  = 14  -- about ~1-in-14 days
local HELLHOUND_FULL_SURGE_EVERY_HIGH = 7    -- about ~1-in-7 days
 
-- If ops are high urgency, make surges more likely.
local HELLHOUND_SURGE_URGENCY_BONUS = true
 
-- Demand used for a “use everything” surge.
-- Any huge number works; overlay hard-caps it to availability.
local HELLHOUND_FULL_SURGE_DEMAND = 1000000000
 
 
--------------------------------------------------------------------------------
-- Baratar personnel activation: percent-based + hard cap
--------------------------------------------------------------------------------
local BARATAR_FORCE_CAP_PCT = 0.20          -- hard cap: never use >20% of Baratar forces
local BARATAR_DUTY_PCT_LOW  = 0.10          -- tweak as desired
local BARATAR_DUTY_PCT_STD  = 0.15
local BARATAR_DUTY_PCT_HIGH = 0.20          -- can equal cap
 
-- must match the key produced by unitKeyFromCell() for the Baratar Active Units row
local BARATAR_ACTIVE_KEY  = "Baratar Active Units"
local BARATAR_RESERVE_KEY = "Baratar Reserve Units" -- optional, only if you ever decide to use it
 
 
--------------------------------------------------------------------------------
-- Baratar policy: always keep some Baratar Active Units on duty (baseline internal security)
--------------------------------------------------------------------------------
local ALWAYS_BARATAR_ACTIVE_DUTY_LOW  = 5000
local ALWAYS_BARATAR_ACTIVE_DUTY_STD  = 12000
local ALWAYS_BARATAR_ACTIVE_DUTY_HIGH = 25000
 
-- IMPORTANT: must match the row key produced by unitKeyFromCell() for the Baratar "Active Units" row
local BARATAR_ACTIVE_KEY = "Baratar Active Units"
 
-- Div codes used by your overlay grouping (matches COL_TO_DIV outputs)
local BARATAR_DUTY_DIVS = { "VAE","ALP","ODF","HFA","HAT","NSJ","BNO","OUR","CKD" }
--------------------------------------------------------------------------------
-- Baratar weapons baseline: % of EACH gun type used daily + hard cap
--------------------------------------------------------------------------------
local BARATAR_WEAPON_DUTY_PCT = 0.05  -- 5% of EACH Baratar gun type (set to 0.20 if you truly want ~20%)
local BARATAR_WEAPON_CAP_PCT  = 0.20  -- never exceed 20% of any Baratar gun type
 
-- Apply baseline usage ONLY to these specific Baratar weapon rows (exact names as in the table)
local BARATAR_WEAPON_WHITELIST = {
["Doryon Rifle 7.62×39mm"] = true,
["Aurean Carbine 6.5mm Compact"] = true,
["Kleisthenes Carbine 5.45mm Folding Stock"] = true,
["Skopion Sniper Rifle 8.6mm with 5x Scope"] = true,
["Lykastos Pistol 9mm Swampproof"] = true,
["Phokion Sidearm 10mm Officer Issue"] = true,
["Sphex SMG 5.7mm Short Barrel"] = true,
["Orontes Shotgun 12-Gauge Semi-Auto"] = true,
["Myrrhex Grenade Mk.IV Incense Compound"] = true,
}
 
 
 
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function push(t, v) t[#t+1] = v end
local function trim(s) s=tostring(s or ""); return (s:gsub("^%s+",""):gsub("%s+$","")) end
 
local function toNumber(s)
s = tostring(s or "")
-- Keep decimals; strip commas/spaces; ignore non-numeric cells like "—"
s = s:gsub(",", ""):gsub("&nbsp;", " "):gsub("%s+", "")
local num = s:match("[-]?%d+%.?%d*")
if not num or num == "" then return nil end
return tonumber(num)
end
 
local function _commaInt(intStr)
local out = intStr:reverse():gsub("(%d%d%d)", "%1,"):reverse()
return out:gsub("^,", "")
end
 
local function fmt(n)
if n == nil then return "" end
if math.floor(n) == n then
return _commaInt(tostring(n))
end
-- 2dp for service-style rows
local s = string.format("%.2f", n)
local intPart, frac = s:match("^(%-?%d+)%.(%d%d)$")
return _commaInt(intPart) .. "." .. frac
end
 
 
local function cellColor(pct)
if pct <= 0.02 then return "#d4edda" end
if pct <= 0.05 then return "#fff3cd" end
if pct <= 0.10 then return "#ffe5cc" end
return "#f8d7da"
end
 
local function normalizeWikitextTable(text)
local out={}
for line in tostring(text):gmatch("([^\n]*)\n?") do
push(out, line:gsub("^[ \t]+",""))
end
return table.concat(out,"\n")
end
 
--------------------------------------------------------------------------------
-- Baratar / Civilian Militia helpers
--------------------------------------------------------------------------------
local BARATAR_ICON_PATTERN = "File:Baratar420%.png"
 
local function isBaratarCell(cell)
return tostring(cell or ""):find(BARATAR_ICON_PATTERN, 1, false) ~= nil
end
 
local function stripLeadingIcons(s)
s = tostring(s or "")
-- remove italics/bold markers so file icons can be truly "leading"
s = s:gsub("''+","")
-- strip one or more leading file icons
s = s:gsub("^%s*(%[%[File:[^%]]-%]%]%s*)+", "")
return trim(s)
end
 
local function extractDisplayText(wikilinkCell)
local c = tostring(wikilinkCell or "")
-- never treat file-links as display text (prevents capturing "23px")
c = c:gsub("%[%[File:[^%]]-%]%]%s*", "")
local display = c:match("%[%[[^%]|]-|([^%]]-)%]%]")
if display and display~="" then return trim(display) end
display = c:match("%[%[([^%]]-)%]%]")
if display and display~="" then return trim(display) end
-- strip remaining wiki/HTML formatting
c = c:gsub("<[^>]+>","")
c = c:gsub("''+","")
return trim(c:gsub("[%[%]]",""))
end
 
local function extractUnitName(cell)
-- strip Baratar/other leading icons + formatting, then extract display text
return extractDisplayText(stripLeadingIcons(cell))
end
 
local function normName(name)
name=tostring(name or ""):lower()
name=name:gsub("[^a-z0-9%-%s/()]", " "):gsub("%s+"," ")
return trim(name)
end
 
local function isPersonnelAggregate(unitName)
local n = normName(unitName)
return (n == "active units" or n == "reserve units"
or n == "baratar active units" or n == "baratar reserve units")
end
 
-- Internal key for a row: Baratar personnel are uniquely keyed so TOE demand never hits them.
local function unitKeyFromCell(cell)
local raw = tostring(cell or "")
local name = extractUnitName(raw)
if isBaratarCell(raw) and (normName(name)=="active units" or normName(name)=="reserve units") then
return "Baratar " .. name
end
return name
end
 
-- Heuristic Baratar weapon detection (used for restriction filtering)
local function isBaratarUnitName(unitName)
local nn = normName(unitName)
return nn:find("lykastos",1,true)
or nn:find("phokion",1,true)
or nn:find("doryon",1,true)
or nn:find("aurean",1,true)
or nn:find("kleisthenes",1,true)
or nn:find("orontes",1,true)
or nn:find("skopion",1,true)
or nn:find("sphex",1,true)
or nn:find("myrrhex",1,true)
end
 
--------------------------------------------------------------------------------
-- Roles (includes grenades as event-driven munitions)
--------------------------------------------------------------------------------
local function rolesForUnit(unitName)
local n=normName(unitName)
local roles={}
local function add(r) roles[r]=true end
 
if isPersonnelAggregate(unitName) then
add("personnel"); return roles
end
 
-- small arms & munitions (event-driven only)
if n:find("rifle",1,true) or n:find("pistol",1,true) or n:find("revolver",1,true)
or n:find("shotgun",1,true) or n:find("submachine",1,true) or n:find("dmr",1,true)
or n:find("sniper",1,true) or n:find("rpg",1,true)
or n:find("grenade",1,true) then
add("small_arms")
end
 
if n:find("quadwalker",1,true) then add("autonomous"); add("all_ground") end
if n:find("hover",1,true) or n:find("hoverbike",1,true) then add("hover_mobility"); add("all_ground") end
 
if n:find("ship",1,true) or n:find("corvette",1,true) or n:find("frigate",1,true)
or n:find("cruiser",1,true) or n:find("battlecruiser",1,true)
or n:find("submarine",1,true) or n:find("submersible",1,true)
or n:find("gunboat",1,true) or n:find("minelayer",1,true) or n:find("minehunter",1,true)
or n:find("landing ship",1,true) or n:find("airstrip barge",1,true)
or n:find("privateer",1,true) or n:find("air%-caravan",1,true) then
add("all_sea")
end
 
if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
or n:find("gunship",1,true)
or n:find("bomber",1,true) or n:find("intercept",1,true) or n:find("transport plane",1,true)
or n:find("zeppelin",1,true) or n:find("airship",1,true) or n:find("bastion",1,true) then
return "air"
end
 
if n:find("tank",1,true) or n:find("armored infantry",1,true) or n:find("recon vehicle",1,true)
or n:find("command vehicle",1,true) or n:find("recovery vehicle",1,true)
or n:find("bridge%-laying",1,true) or n:find("rail%-laying",1,true) or n:find("citadel train",1,true)
or n:find("transport truck",1,true) or n:find("mine%-clearing",1,true) or n:find("engineering",1,true)
or n:find("howitzer",1,true) or n:find("field gun",1,true) or n:find("mortar",1,true)
or n:find("coastal gun",1,true) or n:find("coastal missile",1,true)
or n:find("air defense system",1,true) or n:find("static emplacement",1,true) or n:find("thaad",1,true)
or n:find("surveillance tower",1,true) then
add("all_ground")
end
 
-- Maritime service units (dolphin teams, harbor shifts, hydrophone sweeps)
if n:find("dolphin",1,true)
or n:find("hydrophone",1,true)
or n:find("harbor defense",1,true)
or n:find("escort run",1,true)
or n:find("handler team",1,true)
or n:find("intercept / deterrence",1,true)
or n:find("reconnaissance patrol",1,true) then
add("all_sea")
-- Harbor defense often has shore-side posture too
if n:find("harbor defense",1,true) then add("all_ground") end
end
 
-- Hellhound units (scout / soldier / spy)
if n:find("hellhound", 1, true) then
add("all_ground")
if n:find("(spy)", 1, true) or n:find(" spy", 1, true) then
add("specops")
add("recon")
elseif n:find("(scout)", 1, true) or n:find(" scout", 1, true) then
add("recon")
else
add("infantry")
end
end
 
 
 
if next(roles)==nil then add("misc") end
return roles
end
 
--------------------------------------------------------------------------------
-- Ops parsing helpers
--------------------------------------------------------------------------------
local function sanitizeOpsSegment(seg)
seg=tostring(seg or "")
seg=seg:gsub("<br%s*/?>","\n")
seg=seg:gsub("<[^>]+>","")
seg=seg:gsub("''+","")
seg=seg:gsub("%[%[[^%]|]-|([^%]]-)%]%]","%1")
seg=seg:gsub("%[%[([^%]]-)%]%]","%1")
seg=seg:gsub("&nbsp;"," ")
seg=seg:gsub("%s+"," ")
return trim(seg)
end
 
local function parseFormation(seg)
seg = sanitizeOpsSegment(seg)
local divCode = seg:match("%((%u%u%u)%-[%w]+%s*%)%s*$")
if not divCode then return nil,nil end
local echelon = trim(seg:gsub("%(%u%u%u%-%w+%s*%)%s*$",""))
return echelon, divCode
end
 
local function urgencyScore(urg)
urg=tostring(urg or "")
if urg=="CRITICAL" then return 4 end
if urg=="HIGH" then return 3 end
if urg=="MEDIUM" then return 2 end
if urg=="LOW" then return 1 end
return 0
end
 
local function parseImpact(impactCell)
local s=tostring(impactCell or "")
local g=s:match("Gnd:%s*([^;]+)") or "OK"
local sea=s:match("Sea:%s*([^;]+)") or "OK"
local air=s:match("Air:%s*([^;]+)") or "OK"
return { ground=trim(g), sea=trim(sea), air=trim(air) }
end
 
local function impactAllows(imp, domain)
if not imp then return true end
local v = (domain=="sea" and imp.sea) or (domain=="air" and imp.air) or imp.ground
v=tostring(v or "OK"):lower()
if v:find("no%-go",1,true) then return false end
if v:find("closed",1,true) then return false end
return true
end
 
--------------------------------------------------------------------------------
-- Lua 5.1-safe deterministic picker
--------------------------------------------------------------------------------
local function hash32(s)
s=tostring(s or "")
local h=0
for i=1,#s do
h = (h*131 + s:byte(i)) % 4294967296
end
return h
end
 
local function pickDeterministic(list, k, salt)
if not list or #list==0 or k<=0 then return {} end
local picked, used = {}, {}
local h = hash32(salt)
for _=1,k do
if #picked >= #list then break end
h = (1103515245*h + 12345) % 4294967296
local idx = (h % #list) + 1
local guard=0
while used[idx] and guard<100 do
h = (1103515245*h + 12345) % 4294967296
idx = (h % #list) + 1
guard=guard+1
end
if not used[idx] then
used[idx]=true
picked[#picked+1]=list[idx]
end
end
return picked
end
 
--------------------------------------------------------------------------------
-- Baseline TOE by echelon (canon)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local TOE = {
local TOE = {
Line 78: Line 761:
["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, ["Noctiluna Class Medium Transport Helicopter"]=1,
["Abeis-Osiríos Class CAS Gunship"]=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, ["Thalassa Class Attack Helicopter"]=4,
["Abeis-Osiríos Class CAS Gunship"]=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, ["Catonis Class Unmanned Aircraft (UAV)"]=6,
["Aurantius Class Multi-Role UAV"]=2,
["Aurantius Class Multi-Role UAV"]=2,
Line 89: Line 772:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Helpers (use push() everywhere to avoid table.insert mistakes)
-- Coverage enforcement: ensure every unit exists in each TOE template (default 0)
--------------------------------------------------------------------------------
local function collectAllUnitsFromBase(baseText)
local units={}
for line in tostring(baseText):gmatch("[^\n]+") do
if line:match("^|%s") and line:find("||",1,true) then
local parts = mw.text.split(line:gsub("^|%s*",""), "%s*||%s*")
if #parts == 14 then
local unitName = unitKeyFromCell(parts[2])
if unitName ~= "" then units[unitName]=true end
end
end
end
return units
end
 
local function normalizeToeTemplates(allUnits)
for _, toe in pairs(TOE) do
if type(toe)=="table" then
for unitName,_ in pairs(allUnits) do
if toe[unitName]==nil then toe[unitName]=0 end
end
end
end
end
 
--------------------------------------------------------------------------------
-- Pools: broad activation (all units) + typed small-arms pools (event-driven)
--------------------------------------------------------------------------------
local function buildPools(allUnits)
local pools = {
all_any={}, all_ground={}, all_sea={}, all_air={}, misc={},
small_arms_all={},
small_arms_rifle={}, small_arms_grenadier={}, small_arms_commando={},
small_arms_sniper={}, small_arms_pistol={}, small_arms_revolver={},
small_arms_smg={}, small_arms_shotgun={}, small_arms_dmr={}, small_arms_rpg={},
small_arms_grenade={}
}
 
for unitName,_ in pairs(allUnits) do
if not isPersonnelAggregate(unitName) then
local roles = rolesForUnit(unitName)
 
pools.all_any[#pools.all_any+1] = unitName
if roles.all_ground then pools.all_ground[#pools.all_ground+1] = unitName end
if roles.all_sea then pools.all_sea[#pools.all_sea+1] = unitName end
if roles.all_air then pools.all_air[#pools.all_air+1] = unitName end
if roles.misc then pools.misc[#pools.misc+1] = unitName end
 
if roles.small_arms then
pools.small_arms_all[#pools.small_arms_all+1] = unitName
local nn = normName(unitName)
if nn:find("grenade",1,true) then pools.small_arms_grenade[#pools.small_arms_grenade+1]=unitName
elseif nn:find("grenadier",1,true) then pools.small_arms_grenadier[#pools.small_arms_grenadier+1]=unitName
elseif nn:find("commando",1,true) then pools.small_arms_commando[#pools.small_arms_commando+1]=unitName
elseif nn:find("sniper",1,true) then pools.small_arms_sniper[#pools.small_arms_sniper+1]=unitName
elseif nn:find("pistol",1,true) then pools.small_arms_pistol[#pools.small_arms_pistol+1]=unitName
elseif nn:find("revolver",1,true) then pools.small_arms_revolver[#pools.small_arms_revolver+1]=unitName
elseif nn:find("submachine",1,true) then pools.small_arms_smg[#pools.small_arms_smg+1]=unitName
elseif nn:find("shotgun",1,true) then pools.small_arms_shotgun[#pools.small_arms_shotgun+1]=unitName
elseif nn:find("dmr",1,true) then pools.small_arms_dmr[#pools.small_arms_dmr+1]=unitName
elseif nn:find("rpg",1,true) then pools.small_arms_rpg[#pools.small_arms_rpg+1]=unitName
elseif nn:find("rifle",1,true) then pools.small_arms_rifle[#pools.small_arms_rifle+1]=unitName
end
end
end
end
 
for _,list in pairs(pools) do
if type(list)=="table" then table.sort(list) end
end
return pools
end
 
--------------------------------------------------------------------------------
-- Category-scaled background amounts (big enough to show meaningful “used”)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function push(t, v) t[#t+1] = v end
local function unitCategory(unitName)
local n = normName(unitName)
if n:find("quadwalker",1,true) then return "autonomous" end
if n:find("hover",1,true) or n:find("hoverbike",1,true) then return "hover_mobility" end
 
if n:find("howitzer",1,true) or n:find("field gun",1,true) or n:find("mortar",1,true)
or n:find("coastal gun",1,true) or n:find("coastal missile",1,true) then
return "fires"
end
 
if n:find("air defense system",1,true) or n:find("static emplacement",1,true) or n:find("thaad",1,true)
or n:find("surveillance tower",1,true) then
return "fixed_defense"
end
 
if n:find("tank",1,true) or n:find("armored infantry",1,true) or n:find("recon vehicle",1,true)
or n:find("command vehicle",1,true) or n:find("recovery vehicle",1,true)
or n:find("bridge%-laying",1,true) or n:find("rail%-laying",1,true)
or n:find("transport truck",1,true) or n:find("mine%-clearing",1,true) or n:find("engineering",1,true)
or n:find("citadel train",1,true) then
return "ground_vehicle"
end
 
if n:find("uav",1,true) or n:find("aircraft",1,true) or n:find("helicopter",1,true)
or n:find("gunship",1,true)
or n:find("bomber",1,true) or n:find("intercept",1,true) or n:find("transport plane",1,true)
or n:find("zeppelin",1,true) or n:find("airship",1,true) or n:find("bastion",1,true) then
return "air"
end
 
if n:find("ship",1,true) or n:find("corvette",1,true) or n:find("frigate",1,true) or n:find("cruiser",1,true)
or n:find("battlecruiser",1,true) or n:find("submarine",1,true) or n:find("submersible",1,true)
or n:find("gunboat",1,true) or n:find("minelayer",1,true) or n:find("minehunter",1,true)
or n:find("landing ship",1,true) or n:find("airstrip barge",1,true) or n:find("privateer",1,true)
or n:find("air%-caravan",1,true) then
return "sea"
end
 
if n:find("dolphin",1,true)
or n:find("hydrophone",1,true)
or n:find("harbor defense",1,true)
or n:find("escort run",1,true)
or n:find("handler team",1,true)
or n:find("intercept / deterrence",1,true)
or n:find("reconnaissance patrol",1,true) then
return "maritime_service"
end
 
if n:find("hellhound", 1, true) then
return "hellhound_unit"
end
 
 
return "misc"
end
 
local function breadthLevel(args)
local b=tostring((args and args.breadth) or "standard"):lower()
if b=="low" then return 1 end
if b=="high" then return 3 end
return 2
end
 
local function scaleFactor(uScore, bLevel)
local s = 1
if bLevel == 2 then s = 2 end
if bLevel == 3 then s = 4 end
if uScore >= 3 then s = s + 1 end
if uScore >= 4 then s = s + 2 end
return s
end
 
local function breadthAmountFor(unitName, uScore, bLevel)
local cat = unitCategory(unitName)
local s = scaleFactor(uScore, bLevel)
if cat == "ground_vehicle" then return math.max(1, math.floor(2 * s)) end
if cat == "autonomous" then return math.max(1, math.floor(2 * s)) end
if cat == "hover_mobility" then return math.max(1, math.floor(3 * s)) end
if cat == "fires" then return math.max(1, math.floor(1 * s)) end
if cat == "fixed_defense" then return math.max(1, math.floor(1 * s)) end
if cat == "air" then return math.max(1, math.floor(1 * s)) end
if cat == "sea" then return math.max(1, math.floor(1 * s)) end
if cat == "maritime_service" then return math.max(25, math.floor(40 * s)) end
if cat == "hellhound_unit" then
return math.max(10, math.floor(25 * s))
end
return 1
end


local function trim(s)
--------------------------------------------------------------------------------
s = tostring(s or "")
-- Demand accumulation
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
--------------------------------------------------------------------------------
local function addDemand(util, divCode, unitName, amount)
if not divCode or divCode=="" or not unitName or unitName=="" or not amount or amount<=0 then return end
util[divCode]=util[divCode] or {}
util[divCode][unitName]=(util[divCode][unitName] or 0) + amount
end
end


local function toNumber(s)
--------------------------------------------------------------------------------
s = tostring(s or "")
-- Event-driven small arms selection (Baratar weapons only for domestic/internal)
local n = s:gsub("[^0-9]", "")
--------------------------------------------------------------------------------
if n == "" then return nil end
local function isDomesticOp(op)
return tonumber(n)
op = normName(op)
return op:find("riot",1,true)
or op:find("unrest",1,true)
or op:find("checkpoint",1,true)
or op:find("cordon",1,true)
or op:find("curfew",1,true)
or op:find("security",1,true)
or op:find("stabil",1,true)
or op:find("crowd",1,true)
or op:find("polic",1,true)
or op:find("domestic",1,true)
or op:find("internal",1,true)
end
end


local function fmt(n)
local function filteredPool(list, allowBaratar)
if not n then return "" end
if allowBaratar then return list end
local s = tostring(math.floor(n))
if not list or #list==0 then return list end
local out = s:reverse():gsub("(%d%d%d)", "%1,"):reverse()
local out={}
return out:gsub("^,", "")
for _,u in ipairs(list) do
if not isBaratarUnitName(u) then out[#out+1]=u end
end
return out
end
end


local function extractDisplayText(wikilinkCell)
local function addSmallArmsForOp(util, pools, divCode, opText, uScore, bLevel, salt)
local c = tostring(wikilinkCell or "")
local op = normName(opText)
local display = c:match("%[%[[^%]]-%|([^%]]-)%]%]")
local wants = {}
if display and display ~= "" then return display end
 
display = c:match("%[%[([^%]]-)%]%]")
local function want(poolName, k)
if display and display ~= "" then return display end
if k and k > 0 then wants[#wants+1] = { pool = poolName, k = k } end
return trim(c:gsub("[%[%]]", ""))
end
 
-- scale
local s = 1
if bLevel == 2 then s = 2 end
if bLevel == 3 then s = 3 end
if uScore >= 3 then s = s + 1 end
if uScore >= 4 then s = s + 1 end
 
-- Does this op plausibly call for infantry weapons today?
local infantryRelevant =
  op:find("vbss",1,true) or op:find("boarding",1,true) or op:find("interdiction",1,true)
or op:find("contraband",1,true) or op:find("raid",1,true) or op:find("security",1,true)
or op:find("riot",1,true) or op:find("stabil",1,true) or op:find("search",1,true)
or op:find("cordon",1,true) or op:find("checkpoint",1,true) or op:find("corridor",1,true)
or op:find("hostage",1,true) or op:find("sabotage",1,true)
 
-- Hazard response is usually engineering-first; only include small arms if there’s unrest/looting/security language.
if op:find("hazard",1,true) or op:find("evac",1,true) or op:find("debris",1,true) or op:find("clearance",1,true) then
infantryRelevant = infantryRelevant or op:find("looting",1,true) or op:find("unrest",1,true) or op:find("security",1,true)
end
 
if not infantryRelevant then return end
 
local domestic = isDomesticOp(opText)
local allowBaratar = domestic  -- hard restriction
 
-- Choose an appropriate mix
if op:find("vbss",1,true) or op:find("boarding",1,true) or op:find("interdiction",1,true) then
want("small_arms_commando", 1 + (uScore>=3 and 1 or 0))
want("small_arms_rifle", 1 + (uScore>=3 and 1 or 0))
want("small_arms_pistol", 1)
want("small_arms_dmr", (uScore>=3 and 1 or 0))
want("small_arms_smg", (uScore>=3 and 1 or 0))
elseif op:find("checkpoint",1,true) or op:find("corridor",1,true) or op:find("security",1,true) then
want("small_arms_rifle", 2)
want("small_arms_smg", 1)
want("small_arms_dmr", (uScore>=3 and 1 or 0))
want("small_arms_shotgun", (uScore>=3 and 1 or 0))
elseif op:find("riot",1,true) or op:find("stabil",1,true) or op:find("urban",1,true) or op:find("unrest",1,true) then
want("small_arms_rifle", 2)
want("small_arms_smg", 1)
want("small_arms_commando", (uScore>=3 and 1 or 0))
want("small_arms_pistol", 1)
else
want("small_arms_rifle", 2)
want("small_arms_grenadier", (uScore>=3 and 1 or 0))
want("small_arms_dmr", (uScore>=3 and 1 or 0))
end
 
-- Grenades: event-driven munitions for high-intensity domestic situations only
local highIntensityDomestic = domestic and (uScore >= 3) and (
  op:find("riot",1,true) or op:find("unrest",1,true) or op:find("violent",1,true)
or op:find("crackdown",1,true) or op:find("insurg",1,true) or op:find("siege",1,true)
or op:find("raid",1,true)
)
if highIntensityDomestic then
want("small_arms_grenade", (uScore>=4 and 2 or 1))
end
 
-- Apply picks
for _, w in ipairs(wants) do
local poolList = pools[w.pool]
poolList = filteredPool(poolList, allowBaratar)
if poolList and #poolList > 0 then
local picks = pickDeterministic(poolList, w.k, salt.."|"..w.pool.."|"..tostring(allowBaratar))
for _, u in ipairs(picks) do
local qty = 150 * s
local nn = normName(u)
 
-- scale by weapon type
if nn:find("commando",1,true) then qty = math.floor(qty * 0.40) end
if nn:find("grenadier",1,true) then qty = math.floor(qty * 0.35) end
if nn:find("sniper",1,true) then qty = math.floor(qty * 0.15) end
if nn:find("pistol",1,true) or nn:find("revolver",1,true) then qty = math.floor(qty * 0.25) end
if nn:find("grenade",1,true) then
-- grenades are lower-volume but can spike under CRITICAL
qty = math.floor(60 * s)
if uScore >= 4 then qty = qty * 2 end
end
 
if qty < 25 then qty = 25 end
addDemand(util, divCode, u, qty)
end
end
end
end
end


local function cellColor(pct)
--------------------------------------------------------------------------------
if pct <= 0.02 then return "#d4edda" end
-- Broad activation for all units (excluding small arms; those are event-driven)
if pct <= 0.05 then return "#fff3cd" end
--------------------------------------------------------------------------------
if pct <= 0.10 then return "#ffe5cc" end
local function applyBreadthActivation(util, pools, divCode, opText, uScore, impact, salt, bLevel)
return "#f8d7da"
-- Background activation: training, maintenance, readiness checks, unpublished activities.
local baseK = (bLevel==1 and 2) or (bLevel==2 and 4) or 7
if uScore>=3 then baseK = baseK + 2 end
if uScore>=4 then baseK = baseK + 3 end
if baseK > 12 then baseK = 12 end
 
-- Ground (vehicles, drones, defenses; NOT small arms)
local gPicks = pickDeterministic(pools.all_ground, baseK, salt.."|bg_ground")
for _, u in ipairs(gPicks) do
addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
end
 
-- Sea
if impactAllows(impact,"sea") then
local kSea = math.max(2, math.floor(baseK/2))
local sPicks = pickDeterministic(pools.all_sea, kSea, salt.."|bg_sea")
for _, u in ipairs(sPicks) do
addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
end
end
 
-- Air
if impactAllows(impact,"air") then
local kAir = math.max(2, math.floor(baseK/2))
local aPicks = pickDeterministic(pools.all_air, kAir, salt.."|bg_air")
for _, u in ipairs(aPicks) do
addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
end
end
 
-- Misc
local mPicks = pickDeterministic(pools.misc, (bLevel>=2 and 2 or 1), salt.."|bg_misc")
for _, u in ipairs(mPicks) do
addDemand(util, divCode, u, breadthAmountFor(u, uScore, bLevel))
end
 
-- Event-driven small arms (only when appropriate)
addSmallArmsForOp(util, pools, divCode, opText, uScore, bLevel, salt.."|event_small_arms")
end
end


local function normalizeWikitextTable(text)
--------------------------------------------------------------------------------
local out = {}
-- Ismael commissions: treat NSJ column availability as HAT for this unit only
for line in tostring(text):gmatch("([^\n]*)\n?") do
--------------------------------------------------------------------------------
-- do not break on blank lines
local function divOverrideForUnit(unitName, colKey, defaultDiv)
push(out, line:gsub("^[ \t]+", ""))
if unitName == ISMAEL_UNIT_NAME then
if colKey == "NSJ" then return "HAT" end
end
end
return table.concat(out, "\n")
return defaultDiv
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Coverage enforcement (no unit excluded)
-- Compute utilization from DailyOps (TOE + breadth activation + hatch privateers)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Read availability from the embedded inventory table for a specific unit-key
-- Returns totals grouped by ops-division codes (VAE/ALP/ODF/HFA/HAT/NSJ/BNO/OUR/CKD).
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function collectAllUnitsFromBase(baseText)
local function availByDivForUnitKey(baseText, targetKey)
local units = {}
local avail = {}
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") 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 key = unitKeyFromCell(parts[2])
if unitName ~= "" then units[unitName] = true end
if key == targetKey then
for i = 4, 14 do
local colKey = COL_KEYS[i-3]
local defaultDiv = COL_TO_DIV[colKey]
local divCode = divOverrideForUnit(targetKey, colKey, defaultDiv)
local n = toNumber(parts[i]) or 0
if divCode and n > 0 then
avail[divCode] = (avail[divCode] or 0) + n
end
end
return avail
end
end
end
end
end
end
end
return units
return avail
end
end


local function normalizeToeTemplates(allUnits)
--------------------------------------------------------------------------------
for _, toe in pairs(TOE) do
-- Minimum utilization floor for one division across all inventory rows.
if type(toe) == "table" then
-- Adds enough demand so the division reaches at least floorPct of each available row.
for unitName, _ in pairs(allUnits) do
-- Existing overlay/list logic still caps final "used" values to availability.
if toe[unitName] == nil then toe[unitName] = 0 end
--------------------------------------------------------------------------------
local function applyDivisionMinimumUseFloor(baseText, util, targetDiv, floorPct)
for line in tostring(baseText):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 == 14 then
local unitKey = unitKeyFromCell(parts[2])
 
for i = 4, 14 do
local colKey = COL_KEYS[i-3]
local defaultDiv = COL_TO_DIV[colKey]
local divCode = divOverrideForUnit(unitKey, colKey, defaultDiv)
 
if divCode == targetDiv then
local avail = toNumber(parts[i]) or 0
if avail > 0 then
local floorUse = math.floor(avail * floorPct)
if floorUse < 1 then floorUse = 1 end
 
local alreadyDemanded = (util[targetDiv] and util[targetDiv][unitKey]) or 0
if alreadyDemanded < floorUse then
addDemand(util, targetDiv, unitKey, floorUse - alreadyDemanded)
end
end
end
end
end
end
end
end
end
end
end
end
--------------------------------------------------------------------------------
-- Availability for every Baratar small-arms row in the embedded table.
-- Returns: out[unitName][divCode] = totalAvail
--------------------------------------------------------------------------------
local function baratarSmallArmsAvailByDiv(baseText)
local out = {}
for line in tostring(baseText):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 == 14 then
local unitName = unitKeyFromCell(parts[2])


local function buildToeCoverageReport(allUnits)
-- Baratar weapon rows only (exclude personnel), and only small arms/munitions
local lines = {}
if BARATAR_WEAPON_WHITELIST[unitName] then
push(lines, "''TO&E Coverage Report (any unit not explicitly counted defaults to 0):''")
local roles = rolesForUnit(unitName)
local echelons = {}
if roles and roles.small_arms then
for echelon, _ in pairs(TOE) do push(echelons, echelon) end
out[unitName] = out[unitName] or {}
table.sort(echelons)
for i = 4, 14 do
for _, echelon in ipairs(echelons) do
local colKey = COL_KEYS[i-3]
local toe = TOE[echelon]
local defaultDiv = COL_TO_DIV[colKey]
local fallback = 0
local divCode = divOverrideForUnit(unitName, colKey, defaultDiv)
for unitName, _ in pairs(allUnits) do
local n = toNumber(parts[i]) or 0
if toe[unitName] == 0 then fallback = fallback + 1 end
if divCode and n > 0 then
out[unitName][divCode] = (out[unitName][divCode] or 0) + n
end
end
end
end
end
end
end
push(lines, string.format("* <b>%s</b>: %d fallback rows", echelon, fallback))
end
end
return table.concat(lines, "\n")
 
return out
end
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 function computeUtilization(args, pools)
local util, debugRows = {}, {}
local ok, ops = pcall(require, "Module:WarLeagueDivisionOpsBVR")
local ok, ops = pcall(require, "Module:WarLeagueDivisionOpsBVR")
if not ok or not ops or type(ops.makeTable) ~= "function" then
if not ok or not ops or type(ops.makeTable) ~= "function" then
Line 201: Line 1,235:
local rows = tonumber(args.opsrows) or tonumber(args.rows) or 36
local rows = tonumber(args.opsrows) or tonumber(args.rows) or 36
local seed = args.seed
local seed = args.seed
local bLevel = breadthLevel(args)
-- Always keep some Baratar active units on duty (internal security baseline)
local duty = (bLevel==1 and ALWAYS_BARATAR_ACTIVE_DUTY_LOW)
or (bLevel==3 and ALWAYS_BARATAR_ACTIVE_DUTY_HIGH)
or ALWAYS_BARATAR_ACTIVE_DUTY_STD
for _,div in ipairs(BARATAR_DUTY_DIVS) do
addDemand(util, div, BARATAR_ACTIVE_KEY, duty)
end
-- Baratar baseline activation: % of available, hard-capped at 20%
local baseText = normalizeWikitextTable(RAW_BASE_TABLE)
local barAvail = availByDivForUnitKey(baseText, BARATAR_ACTIVE_KEY)
local pct = (bLevel==1 and BARATAR_DUTY_PCT_LOW)
or (bLevel==3 and BARATAR_DUTY_PCT_HIGH)
or BARATAR_DUTY_PCT_STD
for divCode, totalAvail in pairs(barAvail) do
local cap = math.floor(totalAvail * BARATAR_FORCE_CAP_PCT)
if cap > 0 then
local duty = math.floor(totalAvail * pct)
if duty < 1 then duty = 1 end
if duty > cap then duty = cap end
addDemand(util, divCode, BARATAR_ACTIVE_KEY, duty)
end
end
-- Baseline Baratar weapons usage: 5% of EACH gun type, capped at 20%
local barGuns = baratarSmallArmsAvailByDiv(baseText)
for unitName, divMap in pairs(barGuns) do
for divCode, totalAvail in pairs(divMap) do
local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
if cap > 0 then
local duty = math.floor(totalAvail * BARATAR_WEAPON_DUTY_PCT)
if duty < 1 then duty = 1 end
if duty > cap then duty = cap end
if duty > 0 then
addDemand(util, divCode, unitName, duty)
end
end
end
end


local tbl = ops.makeTable({ args = {
local tbl = ops.makeTable({ args = {
Line 210: Line 1,291:
}})
}})


for line in tostring(tbl):gmatch("[^\n]+") do
-- avoid applying breadth multiple times per same (div, op-row, domain)
if line:match("^|%s") and line:find("||", 1, true) then
local activated = {}
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)
local function activateOnce(divCode, opText, uScore, impact, salt)
forceCell = tostring(forceCell or "—")
activated[divCode] = activated[divCode] or {}
if forceCell == "—" or trim(forceCell) == "" then return end
if activated[divCode][salt] then return end
activated[divCode][salt] = true
applyBreadthActivation(util, pools, divCode, opText, uScore, impact, salt, bLevel)
end


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


for _, seg in ipairs(mw.text.split(forceCell, "\n")) do
for _,seg in ipairs(mw.text.split(forceCell,"\n")) do
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 echelon, divCode = parseFormation(seg)
if divCode then
if echelon and divCode then
local echelon = trim(seg:gsub("%(%u%u%u%-%w+%s*%)%s*$", ""))
-- baseline TOE demand
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 bump(divCode, unitName, amount) end
if amount and amount>0 then addDemand(util, divCode, unitName, amount) end
end
end
end
end
end
end
end
-- breadth once
activateOnce(divCode, opText, uScore, impact, saltBase.."|"..divCode)
end
end
end
end
end
for line in tostring(tbl):gmatch("[^\n]+") do
if line:match("^|%s") and line:find("||",1,true) then
local parts = mw.text.split(line:gsub("^|%s*",""), "%s*||%s*")
-- expected: date, division, city, daily event, urgency, ops alert, ops impact, operation, ground, naval, air, ...
if #parts >= 11 then
local dateCell = parts[1] or ""
local urg = parts[5] or ""
local uScore = urgencyScore(urg)
local impact = parseImpact(parts[7] or "")
local opText = parts[8] or ""
local ground = parts[9] or "—"
local naval  = parts[10] or "—"
local air    = parts[11] or "—"
local salt = tostring(seed or "").."|"..tostring(dateCell).."|"..tostring(parts[2] or "").."|"..tostring(parts[3] or "").."|"..tostring(opText)
push(debugRows, {
date=dateCell, division=parts[2] or "", city=parts[3] or "",
urgency=urg, impact=parts[7] or "", op=opText,
ground=ground, naval=naval, air=air
})


handle(r.ground)
if impactAllows(impact,"ground") then handleCell(ground, opText, uScore, impact, salt.."|G") end
handle(r.naval)
if impactAllows(impact,"sea") then handleCell(naval, opText, uScore, impact, salt.."|N") end
handle(r.air)
if impactAllows(impact,"air") then handleCell(air, opText, uScore, impact, salt.."|A") end
end
end
end
end
end
end
-- Hatch always deploys 30 Ismael commissions (capped in overlay)
addDemand(util, "HAT", ISMAEL_UNIT_NAME, ALWAYS_HATCH_PRIVATEERS)
-- Hatch always deploys its single V-99 Thunderguard train (capped in overlay)
addDemand(util, "HAT", HATCH_V99_UNIT_NAME, ALWAYS_HATCH_V99)
-- Always deploy some SZ-88 Somniant Bastions where available (capped in overlay)
for divCode, n in pairs(ALWAYS_SZ88_MIN) do
addDemand(util, divCode, SZ88_UNIT_NAME, n)
end
-- Hellhounds: baseline minimum presence daily (capped in overlay)
local hh = ALWAYS_HELLHOUNDS_STD
if bLevel <= 1 then hh = ALWAYS_HELLHOUNDS_LOW end
if bLevel >= 3 then hh = ALWAYS_HELLHOUNDS_HIGH end
for _, divCode in ipairs(HELLHOUND_DUTY_DIVS) do
addDemand(util, divCode, HELLHOUND_SCOUT_KEY,  hh.scout)
addDemand(util, divCode, HELLHOUND_SOLDIER_KEY, hh.soldier)
addDemand(util, divCode, HELLHOUND_SPY_KEY,    hh.spy)
end
--------------------------------------------------------------------
-- Hellhounds: occasional FULL surge (so all units get used sometimes)
-- Overlay will cap the “used” value to availability automatically.
--------------------------------------------------------------------
local maxUScore = 0
local lastDate = ""
if #debugRows > 0 then
lastDate = tostring(debugRows[1].date or "")
for i=1, #debugRows do
local u = urgencyScore(debugRows[i].urgency or "")
if u > maxUScore then maxUScore = u end
end
end
local d, m, y = lastDate:match("(%d+)%s*/%s*(%d+)%s*/%s*(%d+)")
d, m, y = tonumber(d), tonumber(m), tonumber(y)
local dayKey = 0
if d and m and y then
dayKey = (y * 10000) + (m * 100) + d
end
local every = HELLHOUND_FULL_SURGE_EVERY_STD
if bLevel <= 1 then every = HELLHOUND_FULL_SURGE_EVERY_LOW end
if bLevel >= 3 then every = HELLHOUND_FULL_SURGE_EVERY_HIGH end
if HELLHOUND_SURGE_URGENCY_BONUS and maxUScore >= 3 then
every = math.max(2, math.floor(every / 2))
end
if dayKey > 0 and every > 0 and (dayKey % every == 0) then
for _, divCode in ipairs(HELLHOUND_DUTY_DIVS) do
addDemand(util, divCode, HELLHOUND_SCOUT_KEY,  HELLHOUND_FULL_SURGE_DEMAND)
addDemand(util, divCode, HELLHOUND_SOLDIER_KEY, HELLHOUND_FULL_SURGE_DEMAND)
addDemand(util, divCode, HELLHOUND_SPY_KEY,    HELLHOUND_FULL_SURGE_DEMAND)
end
end
-- Council of Kings Division: always use at least 20% of available CKD forces.
applyDivisionMinimumUseFloor(baseText, util, "CKD", CKD_MIN_FORCE_USE_PCT)


return util, debugRows, true
return util, debugRows, true
Line 255: Line 1,417:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Overlay highlights (NEVER exceeding availability)
-- Overlay highlights (NEVER exceed availability)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function overlayTable(baseText, util)
local function overlayTable(baseText, util)
local out = {}
local out={}
local inTable = false
local inMain=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
inMain=true
push(out, line)
push(out,line)
elseif inTable and line:match("^|%}%s*$") then
elseif inMain and line:match("^|%}%s*$") then
inTable = false
inMain=false
push(out, line)
push(out,line)
elseif inTable and line:match("^|%s*%[%[File:") and line:find("||", 1, true) then
elseif inMain and 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 == 14 then
if #parts == 14 then
local unitName = extractDisplayText(parts[2])
local unitName = unitKeyFromCell(parts[2])


-- group columns by ops division for this row
local divCols, availByIdx = {}, {}
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]
local divCode = COL_TO_DIV[colKey]
local defaultDiv = COL_TO_DIV[colKey]
local divCode = divOverrideForUnit(unitName, colKey, defaultDiv)
 
local avail = toNumber(parts[i]) or 0
local avail = toNumber(parts[i]) or 0
availByIdx[i] = avail
availByIdx[i]=avail
if divCode then
if divCode then
divCols[divCode] = divCols[divCode] or {}
divCols[divCode]=divCols[divCode] or {}
push(divCols[divCode], { idx = i, avail = avail })
divCols[divCode][#divCols[divCode]+1] = { idx=i, avail=avail }
end
end
end
end


-- allocate demand across columns, capped by availability
local usedByIdx = {}
local usedByIdx = {}


Line 293: Line 1,455:
if demand > 0 then
if demand > 0 then
local totalAvail = 0
local totalAvail = 0
for _, c in ipairs(cols) do totalAvail = totalAvail + (c.avail or 0) end
for _,c in ipairs(cols) do totalAvail = totalAvail + (c.avail or 0) end
local usedTotal = math.min(demand, totalAvail)
local usedTotal = math.min(demand, totalAvail)
-- Hard cap the whitelisted Baratar weapons at 20% (baseline + event-driven combined)
if BARATAR_WEAPON_WHITELIST[unitName] then
local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
if usedTotal > cap then usedTotal = cap end
end
-- Hard cap Baratar personnel usage at 20% (future-proof)
if unitName == BARATAR_ACTIVE_KEY or unitName == BARATAR_RESERVE_KEY then
local cap = math.floor(totalAvail * BARATAR_FORCE_CAP_PCT)
if usedTotal > cap then usedTotal = cap end
end
-- Hard cap Baratar small arms/munitions usage at 20% (baseline + event-driven combined)
if isBaratarUnitName(unitName) then
local roles = rolesForUnit(unitName)
if roles and roles.small_arms then
local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
if usedTotal > cap then usedTotal = cap end
end
end


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
local share = math.floor(usedTotal * c.avail / totalAvail)
local share = math.floor(usedTotal * c.avail / totalAvail)
if share > c.avail then share = c.avail end
if share > c.avail then share = c.avail end
Line 307: Line 1,490:
local rem = usedTotal - assigned
local rem = usedTotal - assigned
if rem > 0 then
if rem > 0 then
table.sort(cols, function(a, b)
table.sort(cols, function(x,y)
local ca = (a.avail or 0) - (usedByIdx[a.idx] or 0)
local cx=(x.avail or 0) - (usedByIdx[x.idx] or 0)
local cb = (b.avail or 0) - (usedByIdx[b.idx] or 0)
local cy=(y.avail or 0) - (usedByIdx[y.idx] or 0)
return ca > cb
return cx > cy
end)
end)
 
local safety=0
local safety = 0
while rem>0 and safety<2000 do
while rem > 0 and safety < 2000 do
safety=safety+1
safety = safety + 1
local progressed=false
local progressed = false
for _,c in ipairs(cols) do
for _, c in ipairs(cols) do
if rem<=0 then break end
if rem <= 0 then break end
local cap=(c.avail or 0) - (usedByIdx[c.idx] or 0)
local cap = (c.avail or 0) - (usedByIdx[c.idx] or 0)
if cap>0 then
if cap > 0 then
usedByIdx[c.idx]=(usedByIdx[c.idx] or 0)+1
usedByIdx[c.idx] = (usedByIdx[c.idx] or 0) + 1
rem=rem-1
rem = rem - 1
progressed=true
progressed = true
end
end
end
end
Line 333: Line 1,515:
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; if pct>1 then pct=1 end
local pct = used / avail
local bg=cellColor(pct)
if pct > 1 then pct = 1 end
local usedText=string.format("<br/><span style=\"font-size:85%%;\"><b>used:</b> %s</span>", fmt(used))
local bg = cellColor(pct)
parts[i]=string.format("style=\"background:%s; text-align:center;\" | %s%s", bg, parts[i], usedText)
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
end
end
Line 349: Line 1,529:
push(out, "| " .. table.concat(parts, " || "))
push(out, "| " .. table.concat(parts, " || "))
else
else
push(out, line)
push(out,line)
end
end
else
else
push(out, line)
push(out,line)
end
end
end
end


push(out, "")
push(out,"")
push(out, "<div style=\"margin-top:0.5em; font-size:90%;\">")
push(out,"<div style=\"margin-top:0.5em; font-size:90%;\">")
push(out, "<b>Utilization highlight:</b> cells tint when that column’s mapped ops-division drew from that row today (TO&E-based).")
push(out,"<b>Daily overview:</b> usage is a capped estimate derived from War League daily ops plus routine training/maintenance/unpublished activity.")
push(out, "<br/><b>Hard rule:</b> used is allocated and capped so it never exceeds availability in any cell or across a shared ops-division column-group.")
push(out,"<br/><b>Hard rule:</b> used never exceeds availability in any cell or across a shared ops-division group.")
push(out, "</div>")
push(out,"<br/><b>Hatch Ministry:</b> always attempts to deploy 30 <i>Ismael Class Privateer</i> commissions (capped to what exists).")
push(out,"<br/><b>Council of Kings Division:</b> always maintains at least 20% utilization of available CKD forces, capped to inventory.")
push(out,"<br/><b>Small arms:</b> selected only when appropriate to the day’s operations (VBSS, security, unrest, etc.).")
push(out,"<br/><b>Baratar militia:</b> Baratar personnel rows are inventory-only; Baratar weapons (including grenades) are estimated as used only during internal/domestic operations.")
push(out,"</div>")
 
return table.concat(out,"\n")
end
 
 
--------------------------------------------------------------------------------
-- Division unit list output (per-division used totals)
--------------------------------------------------------------------------------
 
-- Order you want divisions to print in (matches your overlay div codes)
local DIV_ORDER = { "VAE","ALP","ODF","HFA","HAT","NSJ","BNO","OUR","CKD" }
 
-- Display labels (plain text to avoid broken links)
local DIV_LABEL = {
VAE = "Vaeringheim",
ALP = "Alpazkigz",
ODF = "Odiferia",
HFA = "Haifan Bassaridia",
HAT = "Hatch Ministry",
NSJ = "New South Jangsong",
BNO = "Bassaridian Normark",
OUR = "Ouriana",
CKD = "Council of Kings",
}
 
local function listLabelForRow(unitKey, rawUnitCell)
-- Baratar personnel keys are normalized to "Baratar Active Units"/"Baratar Reserve Units"
if tostring(unitKey):match("^Baratar%s+") then
local base = tostring(unitKey):gsub("^Baratar%s+","")
return base .. " (Baratar)"
end
 
-- For everything else, use the human display name from the unit cell
local name = extractUnitName(rawUnitCell)
 
-- If the unit cell itself contains the Baratar icon, label it as (Baratar)
-- (covers Baratar weapons rows)
if isBaratarCell(rawUnitCell) then
return name .. " (Baratar)"
end
 
return name
end
 
-- Compute USED TOTAL per division for each row, using the same capping rules as overlayTable().
-- Returns: usedTotals[divCode][unitKey] = usedTotal
local function computeUsedTotalsByDivision(baseText, util)
local usedTotals = {}
for _,div in ipairs(DIV_ORDER) do usedTotals[div] = {} end
 
for line in tostring(baseText):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 == 14 then
local unitKey = unitKeyFromCell(parts[2])
 
-- Build availability groups by divCode (same grouping logic as overlay)
local divAvail = {}
for i = 4, 14 do
local colKey = COL_KEYS[i-3]
local defaultDiv = COL_TO_DIV[colKey]
local divCode = divOverrideForUnit(unitKey, colKey, defaultDiv)
local avail = toNumber(parts[i]) or 0
if divCode then
divAvail[divCode] = (divAvail[divCode] or 0) + avail
end
end
 
-- For each divCode, cap demand to availability (and apply Baratar caps)
for divCode, totalAvail in pairs(divAvail) do
local demand = (util[divCode] and util[divCode][unitKey]) or 0
if demand > 0 and totalAvail > 0 then
local usedTotal = math.min(demand, totalAvail)
 
-- Hard cap whitelisted Baratar weapons at 20% (baseline + event-driven combined)
if BARATAR_WEAPON_WHITELIST[unitKey] then
local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
if usedTotal > cap then usedTotal = cap end
end
 
-- Hard cap Baratar personnel usage at 20%
if unitKey == BARATAR_ACTIVE_KEY or unitKey == BARATAR_RESERVE_KEY then
local cap = math.floor(totalAvail * BARATAR_FORCE_CAP_PCT)
if usedTotal > cap then usedTotal = cap end
end
 
-- Future-proof: cap any Baratar small arms/munitions at 20%
if isBaratarUnitName(unitKey) then
local roles = rolesForUnit(unitKey)
if roles and roles.small_arms then
local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
if usedTotal > cap then usedTotal = cap end
end
end
 
if usedTotal > 0 then
usedTotals[divCode] = usedTotals[divCode] or {}
usedTotals[divCode][unitKey] = (usedTotals[divCode][unitKey] or 0) + usedTotal
end
end
end
end
end
end
 
return usedTotals
end
 
local function buildDivisionLists(baseText, usedTotals, filterDiv)
local out = {}
local currentCategory = nil
 
-- We will store, per category, the units that appear in table order:
-- catOrder[#] = categoryName
-- catUnits[categoryName] = { {unitKey=..., label=...}, ... }
local catOrder, catUnits = {}, {}
 
for line in tostring(baseText):gmatch("[^\n]+") do
-- Category header rows look like: ! colspan="14" ... | '''Personnel & readiness'''
if line:match("^!%s*colspan") and line:find("'''", 1, true) then
local cat = line:match("'''(.-)'''")
cat = trim(cat or "")
if cat ~= "" then
currentCategory = cat
if not catUnits[currentCategory] then
catUnits[currentCategory] = {}
catOrder[#catOrder+1] = currentCategory
end
end
 
-- Unit rows start with "| " and have 14 parts
elseif line:match("^|%s") and line:find("||", 1, true) then
local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
if #parts == 14 and currentCategory then
local unitKey = unitKeyFromCell(parts[2])
local label = listLabelForRow(unitKey, parts[2])
 
-- Keep the row in category order once (the usedTotals decide whether it prints)
catUnits[currentCategory][#catUnits[currentCategory]+1] = {
unitKey = unitKey,
label = label
}
end
end
end
 
-- Helper to emit one division
local function emitDivision(divCode)
local divTitle = (DIV_LABEL[divCode] or divCode) .. " (" .. divCode .. ")"
out[#out+1] = "=== " .. divTitle .. " ==="
out[#out+1] = ""
 
for _,cat in ipairs(catOrder) do
local any = false
for _,row in ipairs(catUnits[cat]) do
local used = usedTotals[divCode] and usedTotals[divCode][row.unitKey] or 0
if used and used > 0 then
any = true
break
end
end
 
if any then
out[#out+1] = "==== " .. cat .. " ===="
out[#out+1] = ""
for _,row in ipairs(catUnits[cat]) do
local used = usedTotals[divCode] and usedTotals[divCode][row.unitKey] or 0
if used and used > 0 then
out[#out+1] = string.format("* %s — '''used: %s'''", row.label, fmt(used))
end
end
out[#out+1] = ""
end
end
end
 
if filterDiv and filterDiv ~= "" then
emitDivision(filterDiv)
else
for _,divCode in ipairs(DIV_ORDER) do
emitDivision(divCode)
end
end


return table.concat(out, "\n")
return table.concat(out, "\n")
end
-- Public: prints a list like your example, for one division or all divisions.
-- Invoke:
--  {{#invoke:WarLeagueUnitUtilizationBVR|divisionList}}                -> all divisions
--  {{#invoke:WarLeagueUnitUtilizationBVR|divisionList|div=NSJ}}        -> one division
--  {{#invoke:WarLeagueUnitUtilizationBVR|divisionList|div=HFA}}        -> one division
function p.divisionList(frame)
local args = (frame and frame.args) or {}
local base = normalizeWikitextTable(RAW_BASE_TABLE)
-- Build pools + utilization exactly like render() does
local allUnits = collectAllUnitsFromBase(base)
normalizeToeTemplates(allUnits)
local pools = buildPools(allUnits)
local util = select(1, computeUtilization(args, pools))
local usedTotals = computeUsedTotalsByDivision(base, util)
local filterDiv = trim(args.div or args.division or "")
return buildDivisionLists(base, usedTotals, filterDiv)
end
end


Line 373: Line 1,762:
local base = normalizeWikitextTable(RAW_BASE_TABLE)
local base = normalizeWikitextTable(RAW_BASE_TABLE)


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


if tostring(args.report or ""):lower() == "yes" then
-- pools from embedded inventory (so all units can be reached)
return buildToeCoverageReport(allUnits)
local pools = buildPools(allUnits)
end


local util, debugRows, ok = computeUtilization(args)
-- compute usage
local util, debugRows, ok = computeUtilization(args, pools)
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 387: Line 1,777:
local rendered = overlayTable(base, util)
local rendered = overlayTable(base, util)


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



Revision as of 14:32, 9 June 2026

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

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

local p = {}

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

|- 
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Personnel & readiness'''
|-
| [[File:ActiveUnit1226.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Active_Units|Active Units]] || 249,973 || 25,866 || 20,847 || 25,042 || 1,749 || 1,008 || 7,246 || 10,187 || 15,385 || 8,354 || 21,206 || 113,083
|-
| [[File:ReserveSoldier1226.jpeg|150px|border]]  || [[Somniant_Stock_Fund_Military_Hardware#Reserve_Units|Reserve Units]] || 305,525 || 31,615 || 25,480 || 30,608 || 2,138 || 1,232 || 8,857 || 12,451 || 18,803 || 10,211 || 25,918 || 138,212
|-
| [[File:BaratarActive2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Active Units''' || 1,316,449 || 148,005 || 165,738 || 136,255 || 11,394 || 5,002 || 48,964 || 69,264 || 73,785 || 45,868 || 143,225 || 468,949
|-
| [[File:BaratarReserve2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Reserve Units''' || 1,608,990 || 180,894 || 202,568 || 166,534 || 13,926 || 6,114 || 59,844 || 84,656 || 90,182 || 56,061 || 175,052 || 573,159

|-
! 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:Doryon2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Doryon Rifle 7.62×39mm''' || 574,077 || 64,542 || 72,275 || 59,418 || 4,969 || 2,181 || 21,352 || 30,205 || 32,176 || 20,002 || 62,458 || 204,499
|-
|[[File:Aurean2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Aurean Carbine 6.5mm Compact''' || 511,166 || 57,469 || 64,355 || 52,907 || 4,424 || 1,942 || 19,012 || 26,895 || 28,650 || 17,810 || 55,613 || 182,089
|-
| [[File:Kleisthenes.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Kleisthenes Carbine 5.45mm Folding Stock''' || 425,972 || 47,891 || 53,629 || 44,089 || 3,687 || 1,619 || 15,843 || 22,412 || 23,875 || 14,842 || 46,344 || 151,741
|-
| [[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:Skopion2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Skopion Sniper Rifle 8.6mm with 5x Scope''' || 78,043 || 8,807 || 9,863 || 8,113 || 678 || 297 || 2,915 || 4,123 || 4,390 || 2,731 || 8,516 || 28,610
|-
| [[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:Lykastos2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Lykastos Pistol 9mm Swampproof''' || 738,354 || 83,011 || 92,957 || 76,421 || 6,391 || 2,806 || 27,462 || 38,848 || 41,384 || 25,726 || 80,330 || 263,018
|-
| [[File:Phokion2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Phokion Sidearm 10mm Officer Issue''' || 331,477 || 37,249 || 41,711 || 34,291 || 2,868 || 1,259 || 12,323 || 17,432 || 18,570 || 11,544 || 36,046 || 118,184
|-
| [[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: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:Sphex2026 (2).jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Sphex SMG 5.7mm Short Barrel''' || 60,831 || 6,515 || 7,297 || 6,000 || 502 || 220 || 2,157 || 3,048 || 3,245 || 2,018 || 6,313 || 23,516
|-
| [[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:Orontes2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Orontes Shotgun 12-Gauge Semi-Auto''' || 207,519 || 23,413 || 26,219 || 21,555 || 1,802 || 791 || 7,746 || 10,957 || 11,672 || 7,256 || 22,657 || 73,451
|-
| [[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
|-
| [[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:Myrrhex2026.jpeg|150px|border]] || '''[[File:Baratar420.png|23px]] Myrrhex Grenade Mk.IV Incense Compound''' || 375,794 || 42,256 || 47,325 || 38,920 || 3,256 || 1,430 || 13,975 || 19,770 || 21,060 || 13,080 || 40,948 || 134,774

|-
|-
! colspan="14" style="background:#f2f2f2; text-align:left;" | '''Hellhound detachments (Hellhound Breeders of Dragevik)'''
|-
|[[File:HellhoundScout1.jpeg|150px|border]] || Hellhound (scout) || 510 || 510 || 491 || 468 || 35 || 35 || 167 || 192 || 203 || 187 || 213 || 2,719
|-
|[[File:Hellhoundsoldier1.jpeg|150px|border]]|| Hellhound (soldier) || 680 || 680 || 655 || 624 || 35 || 35 || 167 || 192 || 203 || 187 || 213 || 3,422
|-
| [[File:HellhoundSpy1.jpeg|150px|border]] || Hellhound (spy) || 2,380 || 2,380 || 2,294 || 2,186 || 140 || 140 || 749 || 866 || 911 || 842 || 960 || 12,561

|-
! 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
|-
| [[File:SomniantBastion2026.jpeg|150px|border]] || [[Eosphorus_Motor_Company#Class_SZ-88_Somniant_Bastion|Class SZ-88 Somniant Bastion]] || 158 || 30 || 30 || 26 || 2 || 1 || 8 || 12 || 12 || 9 || 22 || 6
|-
! 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;" | '''Maritime security services (Nefelean Seas Armory)'''
|-
| [[File:ReconDolphin.jpeg|150px|border]] || Reconnaissance Patrol (armored dolphin pair) || 62,657 || 6,120 || 5,898 || 5,620 || 421 || 421 || 1,998 || 2,310 || 2,431 || 2,245 || 2,561 || 32,636
|-
| [[File:EscortDolphin.jpeg|150px|border]] || Escort Run (convoy / VIP transit) || 65,799 || 6,460 || 6,226 || 5,933 || 421 || 421 || 2,082 || 2,406 || 2,532 || 2,339 || 2,667 || 34,317
|-
| [[File:Defensedolphin.jpeg|150px|border]] || Harbor Defense Shift (night watch) || 95,178 || 9,350 || 9,011 || 8,587 || 631 || 631 || 2,997 || 3,464 || 3,646 || 3,368 || 3,841 || 49,656
|-
| [[File:Hydrophonedolphin.jpeg|150px|border]]  || Hydrophone Sweep & Echolocation Mapping || 46,993 || 4,590 || 4,424 || 4,215 || 316 || 316 || 1,499 || 1,732 || 1,823 || 1,684 || 1,921 || 24,477
|-
| [[File:Interceptdolphin.jpeg|150px|border]]  || Rapid Intercept / Deterrence Callout || 116,064 || 11,390 || 10,977 || 10,460 || 772 || 772 || 3,663 || 4,234 || 4,456 || 4,116 || 4,694 || 60,534
|-

! 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:Osirios1227.jpeg|149px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Osiríos_Class_CAS_Gunship|Abeis-Osiríos Class CAS Gunship]] || 185 || 18 || 18 || 15 || 1 || 1 || 5 || 6 || 7 || 5 || 18 || 92
|-
| [[File:Boreas1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Abeis-Boreas_Littoral_Dominance_Helicopter|Abeis-Boreas Littoral Dominance Helicopter]] || 134 || 14 || 11 || 14 || 1 || 0 || 4 || 5 || 8 || 4 || 11 || 61
|-
| [[File:AithraFighter1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aithra_Class_Proximity_Aircraft|Aithra Class Proximity Aircraft]] || 146 || 16 || 13 || 15 || 1 || 0 || 4 || 5 || 8 || 4 || 13 || 66
|-
| [[File:Intercept1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Symphonara_Class_Compound_Intercept|Symphonara Class Compound Intercept]] || 83 || 9 || 7 || 9 || 0 || 0 || 2 || 3 || 5 || 3 || 7 || 38
|-
| [[File:ThalassaCopt1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Thalassa_Class_Attack_Helicopter|Thalassa Class Attack Helicopter]] || 198 || 21 || 17 || 21 || 1 || 1 || 5 || 7 || 11 || 6 || 17 || 90
|-
| [[File:Catonisuav1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Catonis_Class_Unmanned_Aircraft_(UAV)|Catonis Class Unmanned Aircraft (UAV)]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Lotos1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Lotos_Class_Tactical_UAV|Lotos Class Tactical UAV]] || 167 || 18 || 14 || 17 || 1 || 1 || 4 || 6 || 10 || 5 || 15 || 75
|-
| [[File:Aurantius1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Aurantius_Class_Multi-Role_UAV|Aurantius Class Multi-Role UAV]] || 167 || 18 || 14 || 17 || 1 || 1 || 4 || 6 || 10 || 5 || 15 || 75
|-
| [[File:SeaBomber1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Chrysos_Class_Sea_Bomber|Chrysos Class Sea Bomber]] || 218 || 23 || 19 || 22 || 1 || 1 || 6 || 8 || 13 || 7 || 19 || 99
|-
| [[File:MisttalonBomber1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Misttalon_Class_Heavy_Bomber|Misttalon Class Heavy Bomber]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Change into a 202512291428.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Orphali_Class_Tanker_Aircraft|Orphali Class Tanker Aircraft]] || 64 || 7 || 6 || 7 || 0 || 0 || 1 || 2 || 3 || 2 || 6 || 29
|-
| [[File:Ventiflor1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Ventiflor_Eye_Class_AEW&C_Aircraft|Ventiflor Eye Class AEW&C Aircraft]] || 31 || 4 || 3 || 3 || 0 || 0 || 1 || 1 || 2 || 1 || 3 || 14
|-
| [[File:Fluxus1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Fluxus_Class_Maritime_Patrol_Aircraft|Fluxus Class Maritime Patrol Aircraft]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:Hostica1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Hostica_Class_Maritime_Patrol_Aircraft|Hostica Class Maritime Patrol Aircraft]] || 95 || 11 || 9 || 10 || 0 || 0 || 2 || 3 || 5 || 3 || 9 || 43
|-
| [[File:NefelianTransport1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Nefelian_Transport_Plane|Nefelian Transport Plane]] || 115 || 12 || 10 || 12 || 1 || 0 || 3 || 4 || 6 || 3 || 10 || 52
|-
| [[File:Noctiluna1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Noctiluna_Class_Medium_Transport_Helicopter|Noctiluna Class Medium Transport Helicopter]] || 134 || 14 || 11 || 14 || 1 || 0 || 4 || 5 || 8 || 4 || 11 || 61
|-
| [[File:Umbraclaw1227.jpeg|150px|border]] || [[Somniant_Stock_Fund_Military_Hardware#Umbraclaw_Class_Heavy_Transport_Helicopter|Umbraclaw Class Heavy Transport Helicopter]] || 115 || 12 || 10 || 12 || 1 || 0 || 3 || 4 || 6 || 3 || 10 || 52
|}

]=]

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

--------------------------------------------------------------------------------
-- Hatch policy: always deploy 30 Ismael commissions (capped)
--------------------------------------------------------------------------------
local ALWAYS_HATCH_PRIVATEERS = 30
local ISMAEL_UNIT_NAME = "Ismael Class Privateer Commission"
--------------------------------------------------------------------------------
-- Hatch policy: always deploy its single V-99 train (capped)
--------------------------------------------------------------------------------
local ALWAYS_HATCH_V99 = 1
local HATCH_V99_UNIT_NAME = "Model V-99 Thunderguard Citadel Train"

--------------------------------------------------------------------------------
local ALWAYS_SZ88_MIN = {
	VAE = 18,
	ALP = 18,
	ODF = 15,
	HFA = 1,
	JES = 1,
	HAT = 5,
	NSJ = 7,
	HBD = 7,
	BNO = 5,
	OUR = 7,
	CKD = 2,
}
local SZ88_UNIT_NAME = "Class SZ-88 Somniant Bastion"

--------------------------------------------------------------------------------
-- Council of Kings policy: always use at least 20% of available CKD forces
--------------------------------------------------------------------------------
local CKD_MIN_FORCE_USE_PCT = 0.20

--------------------------------------------------------------------------------
-- Hellhound policy: always deploy a minimum daily presence (capped in overlay)
--------------------------------------------------------------------------------
-- IMPORTANT: these strings MUST exactly match the unit names in RAW_BASE_TABLE.
-- They already exist in the table at lines 111–117.
local HELLHOUND_SCOUT_KEY   = "Hellhound (scout)"
local HELLHOUND_SOLDIER_KEY = "Hellhound (soldier)"
local HELLHOUND_SPY_KEY     = "Hellhound (spy)"

-- Minimum DAILY demand added every day. Overlay later caps by availability.
local ALWAYS_HELLHOUNDS_LOW  = { scout = 8,  soldier = 12, spy = 3 }
local ALWAYS_HELLHOUNDS_STD  = { scout = 15, soldier = 25, spy = 6 }
local ALWAYS_HELLHOUNDS_HIGH = { scout = 25, soldier = 40, spy = 10 }

-- Divisions that should always show Hellhound presence
-- NOTE: These are DIV CODES (same as used elsewhere: VAE/ALP/ODF/HFA/HAT/NSJ/BNO/OUR/CKD)
local HELLHOUND_DUTY_DIVS = { "VAE","ALP","ODF","HFA","HAT","NSJ","BNO","OUR","CKD" }

--------------------------------------------------------------------------------
-- Hellhound surge policy: occasionally “exercise the full kennel”
-- (overlay caps to availability, so large demand is safe)
--------------------------------------------------------------------------------
-- How often a FULL surge occurs (lower = more frequent).
local HELLHOUND_FULL_SURGE_EVERY_LOW  = 21   -- about ~1-in-21 days
local HELLHOUND_FULL_SURGE_EVERY_STD  = 14   -- about ~1-in-14 days
local HELLHOUND_FULL_SURGE_EVERY_HIGH = 7    -- about ~1-in-7 days

-- If ops are high urgency, make surges more likely.
local HELLHOUND_SURGE_URGENCY_BONUS = true

-- Demand used for a “use everything” surge.
-- Any huge number works; overlay hard-caps it to availability.
local HELLHOUND_FULL_SURGE_DEMAND = 1000000000


--------------------------------------------------------------------------------
-- Baratar personnel activation: percent-based + hard cap
--------------------------------------------------------------------------------
local BARATAR_FORCE_CAP_PCT = 0.20          -- hard cap: never use >20% of Baratar forces
local BARATAR_DUTY_PCT_LOW  = 0.10          -- tweak as desired
local BARATAR_DUTY_PCT_STD  = 0.15
local BARATAR_DUTY_PCT_HIGH = 0.20          -- can equal cap

-- must match the key produced by unitKeyFromCell() for the Baratar Active Units row
local BARATAR_ACTIVE_KEY  = "Baratar Active Units"
local BARATAR_RESERVE_KEY = "Baratar Reserve Units" -- optional, only if you ever decide to use it


--------------------------------------------------------------------------------
-- Baratar policy: always keep some Baratar Active Units on duty (baseline internal security)
--------------------------------------------------------------------------------
local ALWAYS_BARATAR_ACTIVE_DUTY_LOW  = 5000
local ALWAYS_BARATAR_ACTIVE_DUTY_STD  = 12000
local ALWAYS_BARATAR_ACTIVE_DUTY_HIGH = 25000

-- IMPORTANT: must match the row key produced by unitKeyFromCell() for the Baratar "Active Units" row
local BARATAR_ACTIVE_KEY = "Baratar Active Units"

-- Div codes used by your overlay grouping (matches COL_TO_DIV outputs)
local BARATAR_DUTY_DIVS = { "VAE","ALP","ODF","HFA","HAT","NSJ","BNO","OUR","CKD" }
--------------------------------------------------------------------------------
-- Baratar weapons baseline: % of EACH gun type used daily + hard cap
--------------------------------------------------------------------------------
local BARATAR_WEAPON_DUTY_PCT = 0.05   -- 5% of EACH Baratar gun type (set to 0.20 if you truly want ~20%)
local BARATAR_WEAPON_CAP_PCT  = 0.20   -- never exceed 20% of any Baratar gun type

-- Apply baseline usage ONLY to these specific Baratar weapon rows (exact names as in the table)
local BARATAR_WEAPON_WHITELIST = {
	["Doryon Rifle 7.62×39mm"] = true,
	["Aurean Carbine 6.5mm Compact"] = true,
	["Kleisthenes Carbine 5.45mm Folding Stock"] = true,
	["Skopion Sniper Rifle 8.6mm with 5x Scope"] = true,
	["Lykastos Pistol 9mm Swampproof"] = true,
	["Phokion Sidearm 10mm Officer Issue"] = true,
	["Sphex SMG 5.7mm Short Barrel"] = true,
	["Orontes Shotgun 12-Gauge Semi-Auto"] = true,
	["Myrrhex Grenade Mk.IV Incense Compound"] = true,
}



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

local function toNumber(s)
	s = tostring(s or "")
	-- Keep decimals; strip commas/spaces; ignore non-numeric cells like "—"
	s = s:gsub(",", ""):gsub("&nbsp;", " "):gsub("%s+", "")
	local num = s:match("[-]?%d+%.?%d*")
	if not num or num == "" then return nil end
	return tonumber(num)
end

local function _commaInt(intStr)
	local out = intStr:reverse():gsub("(%d%d%d)", "%1,"):reverse()
	return out:gsub("^,", "")
end

local function fmt(n)
	if n == nil then return "" end
	if math.floor(n) == n then
		return _commaInt(tostring(n))
	end
	-- 2dp for service-style rows
	local s = string.format("%.2f", n)
	local intPart, frac = s:match("^(%-?%d+)%.(%d%d)$")
	return _commaInt(intPart) .. "." .. frac
end


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

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

--------------------------------------------------------------------------------
-- Baratar / Civilian Militia helpers
--------------------------------------------------------------------------------
local BARATAR_ICON_PATTERN = "File:Baratar420%.png"

local function isBaratarCell(cell)
	return tostring(cell or ""):find(BARATAR_ICON_PATTERN, 1, false) ~= nil
end

local function stripLeadingIcons(s)
	s = tostring(s or "")
	-- remove italics/bold markers so file icons can be truly "leading"
	s = s:gsub("''+","")
	-- strip one or more leading file icons
	s = s:gsub("^%s*(%[%[File:[^%]]-%]%]%s*)+", "")
	return trim(s)
end

local function extractDisplayText(wikilinkCell)
	local c = tostring(wikilinkCell or "")
	-- never treat file-links as display text (prevents capturing "23px")
	c = c:gsub("%[%[File:[^%]]-%]%]%s*", "")
	local display = c:match("%[%[[^%]|]-|([^%]]-)%]%]")
	if display and display~="" then return trim(display) end
	display = c:match("%[%[([^%]]-)%]%]")
	if display and display~="" then return trim(display) end
	-- strip remaining wiki/HTML formatting
	c = c:gsub("<[^>]+>","")
	c = c:gsub("''+","")
	return trim(c:gsub("[%[%]]",""))
end

local function extractUnitName(cell)
	-- strip Baratar/other leading icons + formatting, then extract display text
	return extractDisplayText(stripLeadingIcons(cell))
end

local function normName(name)
	name=tostring(name or ""):lower()
	name=name:gsub("[^a-z0-9%-%s/()]", " "):gsub("%s+"," ")
	return trim(name)
end

local function isPersonnelAggregate(unitName)
	local n = normName(unitName)
	return (n == "active units" or n == "reserve units"
		or n == "baratar active units" or n == "baratar reserve units")
end

-- Internal key for a row: Baratar personnel are uniquely keyed so TOE demand never hits them.
local function unitKeyFromCell(cell)
	local raw = tostring(cell or "")
	local name = extractUnitName(raw)
	if isBaratarCell(raw) and (normName(name)=="active units" or normName(name)=="reserve units") then
		return "Baratar " .. name
	end
	return name
end

-- Heuristic Baratar weapon detection (used for restriction filtering)
local function isBaratarUnitName(unitName)
	local nn = normName(unitName)
	return nn:find("lykastos",1,true)
		or nn:find("phokion",1,true)
		or nn:find("doryon",1,true)
		or nn:find("aurean",1,true)
		or nn:find("kleisthenes",1,true)
		or nn:find("orontes",1,true)
		or nn:find("skopion",1,true)
		or nn:find("sphex",1,true)
		or nn:find("myrrhex",1,true)
end

--------------------------------------------------------------------------------
-- Roles (includes grenades as event-driven munitions)
--------------------------------------------------------------------------------
local function rolesForUnit(unitName)
	local n=normName(unitName)
	local roles={}
	local function add(r) roles[r]=true end

	if isPersonnelAggregate(unitName) then
		add("personnel"); return roles
	end

	-- small arms & munitions (event-driven only)
	if n:find("rifle",1,true) or n:find("pistol",1,true) or n:find("revolver",1,true)
		or n:find("shotgun",1,true) or n:find("submachine",1,true) or n:find("dmr",1,true)
		or n:find("sniper",1,true) or n:find("rpg",1,true)
		or n:find("grenade",1,true) then
		add("small_arms")
	end

	if n:find("quadwalker",1,true) then add("autonomous"); add("all_ground") end
	if n:find("hover",1,true) or n:find("hoverbike",1,true) then add("hover_mobility"); add("all_ground") end

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

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

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

	-- Maritime service units (dolphin teams, harbor shifts, hydrophone sweeps)
	if n:find("dolphin",1,true)
		or n:find("hydrophone",1,true)
		or n:find("harbor defense",1,true)
		or n:find("escort run",1,true)
		or n:find("handler team",1,true)
		or n:find("intercept / deterrence",1,true)
		or n:find("reconnaissance patrol",1,true) then
		add("all_sea")
		-- Harbor defense often has shore-side posture too
		if n:find("harbor defense",1,true) then add("all_ground") end
	end

	-- Hellhound units (scout / soldier / spy)
	if n:find("hellhound", 1, true) then
		add("all_ground")
		if n:find("(spy)", 1, true) or n:find(" spy", 1, true) then
			add("specops")
			add("recon")
		elseif n:find("(scout)", 1, true) or n:find(" scout", 1, true) then
			add("recon")
		else
			add("infantry")
		end
	end



	if next(roles)==nil then add("misc") end
	return roles
end

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

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

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

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

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

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

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

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

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

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

--------------------------------------------------------------------------------
-- Pools: broad activation (all units) + typed small-arms pools (event-driven)
--------------------------------------------------------------------------------
local function buildPools(allUnits)
	local pools = {
		all_any={}, all_ground={}, all_sea={}, all_air={}, misc={},
		small_arms_all={},
		small_arms_rifle={}, small_arms_grenadier={}, small_arms_commando={},
		small_arms_sniper={}, small_arms_pistol={}, small_arms_revolver={},
		small_arms_smg={}, small_arms_shotgun={}, small_arms_dmr={}, small_arms_rpg={},
		small_arms_grenade={}
	}

	for unitName,_ in pairs(allUnits) do
		if not isPersonnelAggregate(unitName) then
			local roles = rolesForUnit(unitName)

			pools.all_any[#pools.all_any+1] = unitName
			if roles.all_ground then pools.all_ground[#pools.all_ground+1] = unitName end
			if roles.all_sea then pools.all_sea[#pools.all_sea+1] = unitName end
			if roles.all_air then pools.all_air[#pools.all_air+1] = unitName end
			if roles.misc then pools.misc[#pools.misc+1] = unitName end

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

	for _,list in pairs(pools) do
		if type(list)=="table" then table.sort(list) end
	end
	return pools
end

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

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

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

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

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

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

	if n:find("dolphin",1,true)
		or n:find("hydrophone",1,true)
		or n:find("harbor defense",1,true)
		or n:find("escort run",1,true)
		or n:find("handler team",1,true)
		or n:find("intercept / deterrence",1,true)
		or n:find("reconnaissance patrol",1,true) then
		return "maritime_service"
	end

	if n:find("hellhound", 1, true) then
		return "hellhound_unit"
	end


	return "misc"
end

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

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

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

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

--------------------------------------------------------------------------------
-- Event-driven small arms selection (Baratar weapons only for domestic/internal)
--------------------------------------------------------------------------------
local function isDomesticOp(op)
	op = normName(op)
	return op:find("riot",1,true)
		or op:find("unrest",1,true)
		or op:find("checkpoint",1,true)
		or op:find("cordon",1,true)
		or op:find("curfew",1,true)
		or op:find("security",1,true)
		or op:find("stabil",1,true)
		or op:find("crowd",1,true)
		or op:find("polic",1,true)
		or op:find("domestic",1,true)
		or op:find("internal",1,true)
end

local function filteredPool(list, allowBaratar)
	if allowBaratar then return list end
	if not list or #list==0 then return list end
	local out={}
	for _,u in ipairs(list) do
		if not isBaratarUnitName(u) then out[#out+1]=u end
	end
	return out
end

local function addSmallArmsForOp(util, pools, divCode, opText, uScore, bLevel, salt)
	local op = normName(opText)
	local wants = {}

	local function want(poolName, k)
		if k and k > 0 then wants[#wants+1] = { pool = poolName, k = k } end
	end

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

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

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

	if not infantryRelevant then return end

	local domestic = isDomesticOp(opText)
	local allowBaratar = domestic  -- hard restriction

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

	-- Grenades: event-driven munitions for high-intensity domestic situations only
	local highIntensityDomestic = domestic and (uScore >= 3) and (
		   op:find("riot",1,true) or op:find("unrest",1,true) or op:find("violent",1,true)
		or op:find("crackdown",1,true) or op:find("insurg",1,true) or op:find("siege",1,true)
		or op:find("raid",1,true)
	)
	if highIntensityDomestic then
		want("small_arms_grenade", (uScore>=4 and 2 or 1))
	end

	-- Apply picks
	for _, w in ipairs(wants) do
		local poolList = pools[w.pool]
		poolList = filteredPool(poolList, allowBaratar)
		if poolList and #poolList > 0 then
			local picks = pickDeterministic(poolList, w.k, salt.."|"..w.pool.."|"..tostring(allowBaratar))
			for _, u in ipairs(picks) do
				local qty = 150 * s
				local nn = normName(u)

				-- scale by weapon type
				if nn:find("commando",1,true) then qty = math.floor(qty * 0.40) end
				if nn:find("grenadier",1,true) then qty = math.floor(qty * 0.35) end
				if nn:find("sniper",1,true) then qty = math.floor(qty * 0.15) end
				if nn:find("pistol",1,true) or nn:find("revolver",1,true) then qty = math.floor(qty * 0.25) end
				if nn:find("grenade",1,true) then
					-- grenades are lower-volume but can spike under CRITICAL
					qty = math.floor(60 * s)
					if uScore >= 4 then qty = qty * 2 end
				end

				if qty < 25 then qty = 25 end
				addDemand(util, divCode, u, qty)
			end
		end
	end
end

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

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

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

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

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

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

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

--------------------------------------------------------------------------------
-- Compute utilization from DailyOps (TOE + breadth activation + hatch privateers)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Read availability from the embedded inventory table for a specific unit-key
-- Returns totals grouped by ops-division codes (VAE/ALP/ODF/HFA/HAT/NSJ/BNO/OUR/CKD).
--------------------------------------------------------------------------------
local function availByDivForUnitKey(baseText, targetKey)
	local avail = {}
	for line in tostring(baseText):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 == 14 then
				local key = unitKeyFromCell(parts[2])
				if key == targetKey then
					for i = 4, 14 do
						local colKey = COL_KEYS[i-3]
						local defaultDiv = COL_TO_DIV[colKey]
						local divCode = divOverrideForUnit(targetKey, colKey, defaultDiv)
						local n = toNumber(parts[i]) or 0
						if divCode and n > 0 then
							avail[divCode] = (avail[divCode] or 0) + n
						end
					end
					return avail
				end
			end
		end
	end
	return avail
end

--------------------------------------------------------------------------------
-- Minimum utilization floor for one division across all inventory rows.
-- Adds enough demand so the division reaches at least floorPct of each available row.
-- Existing overlay/list logic still caps final "used" values to availability.
--------------------------------------------------------------------------------
local function applyDivisionMinimumUseFloor(baseText, util, targetDiv, floorPct)
	for line in tostring(baseText):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 == 14 then
				local unitKey = unitKeyFromCell(parts[2])

				for i = 4, 14 do
					local colKey = COL_KEYS[i-3]
					local defaultDiv = COL_TO_DIV[colKey]
					local divCode = divOverrideForUnit(unitKey, colKey, defaultDiv)

					if divCode == targetDiv then
						local avail = toNumber(parts[i]) or 0
						if avail > 0 then
							local floorUse = math.floor(avail * floorPct)
							if floorUse < 1 then floorUse = 1 end

							local alreadyDemanded = (util[targetDiv] and util[targetDiv][unitKey]) or 0
							if alreadyDemanded < floorUse then
								addDemand(util, targetDiv, unitKey, floorUse - alreadyDemanded)
							end
						end
					end
				end
			end
		end
	end
end
--------------------------------------------------------------------------------
-- Availability for every Baratar small-arms row in the embedded table.
-- Returns: out[unitName][divCode] = totalAvail
--------------------------------------------------------------------------------
local function baratarSmallArmsAvailByDiv(baseText)
	local out = {}

	for line in tostring(baseText):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 == 14 then
				local unitName = unitKeyFromCell(parts[2])

				-- Baratar weapon rows only (exclude personnel), and only small arms/munitions
				if BARATAR_WEAPON_WHITELIST[unitName] then
					local roles = rolesForUnit(unitName)
					if roles and roles.small_arms then
						out[unitName] = out[unitName] or {}
						for i = 4, 14 do
							local colKey = COL_KEYS[i-3]
							local defaultDiv = COL_TO_DIV[colKey]
							local divCode = divOverrideForUnit(unitName, colKey, defaultDiv)
							local n = toNumber(parts[i]) or 0
							if divCode and n > 0 then
								out[unitName][divCode] = (out[unitName][divCode] or 0) + n
							end
						end
					end
				end
			end
		end
	end

	return out
end


local function computeUtilization(args, pools)
	local util, debugRows = {}, {}
	local ok, ops = pcall(require, "Module:WarLeagueDivisionOpsBVR")
	if not ok or not ops or type(ops.makeTable) ~= "function" then
		return util, debugRows, false
	end

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

	-- Always keep some Baratar active units on duty (internal security baseline)
	local duty = (bLevel==1 and ALWAYS_BARATAR_ACTIVE_DUTY_LOW)
		or (bLevel==3 and ALWAYS_BARATAR_ACTIVE_DUTY_HIGH)
		or ALWAYS_BARATAR_ACTIVE_DUTY_STD

	for _,div in ipairs(BARATAR_DUTY_DIVS) do
		addDemand(util, div, BARATAR_ACTIVE_KEY, duty)
	end

	-- Baratar baseline activation: % of available, hard-capped at 20%
	local baseText = normalizeWikitextTable(RAW_BASE_TABLE)
	local barAvail = availByDivForUnitKey(baseText, BARATAR_ACTIVE_KEY)

	local pct = (bLevel==1 and BARATAR_DUTY_PCT_LOW)
		or (bLevel==3 and BARATAR_DUTY_PCT_HIGH)
		or BARATAR_DUTY_PCT_STD

	for divCode, totalAvail in pairs(barAvail) do
		local cap = math.floor(totalAvail * BARATAR_FORCE_CAP_PCT)
		if cap > 0 then
			local duty = math.floor(totalAvail * pct)
			if duty < 1 then duty = 1 end
			if duty > cap then duty = cap end
			addDemand(util, divCode, BARATAR_ACTIVE_KEY, duty)
		end
	end

	-- Baseline Baratar weapons usage: 5% of EACH gun type, capped at 20%
	local barGuns = baratarSmallArmsAvailByDiv(baseText)

	for unitName, divMap in pairs(barGuns) do
		for divCode, totalAvail in pairs(divMap) do
			local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
			if cap > 0 then
				local duty = math.floor(totalAvail * BARATAR_WEAPON_DUTY_PCT)
				if duty < 1 then duty = 1 end
				if duty > cap then duty = cap end
				if duty > 0 then
					addDemand(util, divCode, unitName, duty)
				end
			end
		end
	end



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

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

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

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

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

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

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

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

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

	-- Hatch always deploys 30 Ismael commissions (capped in overlay)
	addDemand(util, "HAT", ISMAEL_UNIT_NAME, ALWAYS_HATCH_PRIVATEERS)
-- Hatch always deploys its single V-99 Thunderguard train (capped in overlay)
addDemand(util, "HAT", HATCH_V99_UNIT_NAME, ALWAYS_HATCH_V99)
-- Always deploy some SZ-88 Somniant Bastions where available (capped in overlay)
for divCode, n in pairs(ALWAYS_SZ88_MIN) do
	addDemand(util, divCode, SZ88_UNIT_NAME, n)
end
	-- Hellhounds: baseline minimum presence daily (capped in overlay)
	local hh = ALWAYS_HELLHOUNDS_STD
	if bLevel <= 1 then hh = ALWAYS_HELLHOUNDS_LOW end
	if bLevel >= 3 then hh = ALWAYS_HELLHOUNDS_HIGH end

	for _, divCode in ipairs(HELLHOUND_DUTY_DIVS) do
		addDemand(util, divCode, HELLHOUND_SCOUT_KEY,   hh.scout)
		addDemand(util, divCode, HELLHOUND_SOLDIER_KEY, hh.soldier)
		addDemand(util, divCode, HELLHOUND_SPY_KEY,     hh.spy)
	end

	--------------------------------------------------------------------
	-- Hellhounds: occasional FULL surge (so all units get used sometimes)
	-- Overlay will cap the “used” value to availability automatically.
	--------------------------------------------------------------------
	local maxUScore = 0
	local lastDate = ""
	if #debugRows > 0 then
		lastDate = tostring(debugRows[1].date or "")
		for i=1, #debugRows do
			local u = urgencyScore(debugRows[i].urgency or "")
			if u > maxUScore then maxUScore = u end
		end
	end

	local d, m, y = lastDate:match("(%d+)%s*/%s*(%d+)%s*/%s*(%d+)")
	d, m, y = tonumber(d), tonumber(m), tonumber(y)
	local dayKey = 0
	if d and m and y then
		dayKey = (y * 10000) + (m * 100) + d
	end

	local every = HELLHOUND_FULL_SURGE_EVERY_STD
	if bLevel <= 1 then every = HELLHOUND_FULL_SURGE_EVERY_LOW end
	if bLevel >= 3 then every = HELLHOUND_FULL_SURGE_EVERY_HIGH end

	if HELLHOUND_SURGE_URGENCY_BONUS and maxUScore >= 3 then
		every = math.max(2, math.floor(every / 2))
	end

	if dayKey > 0 and every > 0 and (dayKey % every == 0) then
		for _, divCode in ipairs(HELLHOUND_DUTY_DIVS) do
			addDemand(util, divCode, HELLHOUND_SCOUT_KEY,   HELLHOUND_FULL_SURGE_DEMAND)
			addDemand(util, divCode, HELLHOUND_SOLDIER_KEY, HELLHOUND_FULL_SURGE_DEMAND)
			addDemand(util, divCode, HELLHOUND_SPY_KEY,     HELLHOUND_FULL_SURGE_DEMAND)
		end
	end

	-- Council of Kings Division: always use at least 20% of available CKD forces.
	applyDivisionMinimumUseFloor(baseText, util, "CKD", CKD_MIN_FORCE_USE_PCT)

	return util, debugRows, true
end

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

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

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

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

				local usedByIdx = {}

				for divCode, cols in pairs(divCols) do
					local demand = (util[divCode] and util[divCode][unitName]) or 0
					if demand > 0 then
						local totalAvail = 0
						for _,c in ipairs(cols) do totalAvail = totalAvail + (c.avail or 0) end
						local usedTotal = math.min(demand, totalAvail)
						-- Hard cap the whitelisted Baratar weapons at 20% (baseline + event-driven combined)
						if BARATAR_WEAPON_WHITELIST[unitName] then
							local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
							if usedTotal > cap then usedTotal = cap end
						end

						-- Hard cap Baratar personnel usage at 20% (future-proof)
						if unitName == BARATAR_ACTIVE_KEY or unitName == BARATAR_RESERVE_KEY then
							local cap = math.floor(totalAvail * BARATAR_FORCE_CAP_PCT)
							if usedTotal > cap then usedTotal = cap end
						end
						-- Hard cap Baratar small arms/munitions usage at 20% (baseline + event-driven combined)
						if isBaratarUnitName(unitName) then
							local roles = rolesForUnit(unitName)
							if roles and roles.small_arms then
								local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
								if usedTotal > cap then usedTotal = cap end
							end
						end



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

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

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

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

	push(out,"")
	push(out,"<div style=\"margin-top:0.5em; font-size:90%;\">")
	push(out,"<b>Daily overview:</b> usage is a capped estimate derived from War League daily ops plus routine training/maintenance/unpublished activity.")
	push(out,"<br/><b>Hard rule:</b> used never exceeds availability in any cell or across a shared ops-division group.")
	push(out,"<br/><b>Hatch Ministry:</b> always attempts to deploy 30 <i>Ismael Class Privateer</i> commissions (capped to what exists).")
push(out,"<br/><b>Council of Kings Division:</b> always maintains at least 20% utilization of available CKD forces, capped to inventory.")
	push(out,"<br/><b>Small arms:</b> selected only when appropriate to the day’s operations (VBSS, security, unrest, etc.).")
	push(out,"<br/><b>Baratar militia:</b> Baratar personnel rows are inventory-only; Baratar weapons (including grenades) are estimated as used only during internal/domestic operations.")
	push(out,"</div>")

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


--------------------------------------------------------------------------------
-- Division unit list output (per-division used totals)
--------------------------------------------------------------------------------

-- Order you want divisions to print in (matches your overlay div codes)
local DIV_ORDER = { "VAE","ALP","ODF","HFA","HAT","NSJ","BNO","OUR","CKD" }

-- Display labels (plain text to avoid broken links)
local DIV_LABEL = {
	VAE = "Vaeringheim",
	ALP = "Alpazkigz",
	ODF = "Odiferia",
	HFA = "Haifan Bassaridia",
	HAT = "Hatch Ministry",
	NSJ = "New South Jangsong",
	BNO = "Bassaridian Normark",
	OUR = "Ouriana",
	CKD = "Council of Kings",
}

local function listLabelForRow(unitKey, rawUnitCell)
	-- Baratar personnel keys are normalized to "Baratar Active Units"/"Baratar Reserve Units"
	if tostring(unitKey):match("^Baratar%s+") then
		local base = tostring(unitKey):gsub("^Baratar%s+","")
		return base .. " (Baratar)"
	end

	-- For everything else, use the human display name from the unit cell
	local name = extractUnitName(rawUnitCell)

	-- If the unit cell itself contains the Baratar icon, label it as (Baratar)
	-- (covers Baratar weapons rows)
	if isBaratarCell(rawUnitCell) then
		return name .. " (Baratar)"
	end

	return name
end

-- Compute USED TOTAL per division for each row, using the same capping rules as overlayTable().
-- Returns: usedTotals[divCode][unitKey] = usedTotal
local function computeUsedTotalsByDivision(baseText, util)
	local usedTotals = {}
	for _,div in ipairs(DIV_ORDER) do usedTotals[div] = {} end

	for line in tostring(baseText):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 == 14 then
				local unitKey = unitKeyFromCell(parts[2])

				-- Build availability groups by divCode (same grouping logic as overlay)
				local divAvail = {}
				for i = 4, 14 do
					local colKey = COL_KEYS[i-3]
					local defaultDiv = COL_TO_DIV[colKey]
					local divCode = divOverrideForUnit(unitKey, colKey, defaultDiv)
					local avail = toNumber(parts[i]) or 0
					if divCode then
						divAvail[divCode] = (divAvail[divCode] or 0) + avail
					end
				end

				-- For each divCode, cap demand to availability (and apply Baratar caps)
				for divCode, totalAvail in pairs(divAvail) do
					local demand = (util[divCode] and util[divCode][unitKey]) or 0
					if demand > 0 and totalAvail > 0 then
						local usedTotal = math.min(demand, totalAvail)

						-- Hard cap whitelisted Baratar weapons at 20% (baseline + event-driven combined)
						if BARATAR_WEAPON_WHITELIST[unitKey] then
							local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
							if usedTotal > cap then usedTotal = cap end
						end

						-- Hard cap Baratar personnel usage at 20%
						if unitKey == BARATAR_ACTIVE_KEY or unitKey == BARATAR_RESERVE_KEY then
							local cap = math.floor(totalAvail * BARATAR_FORCE_CAP_PCT)
							if usedTotal > cap then usedTotal = cap end
						end

						-- Future-proof: cap any Baratar small arms/munitions at 20%
						if isBaratarUnitName(unitKey) then
							local roles = rolesForUnit(unitKey)
							if roles and roles.small_arms then
								local cap = math.floor(totalAvail * BARATAR_WEAPON_CAP_PCT)
								if usedTotal > cap then usedTotal = cap end
							end
						end

						if usedTotal > 0 then
							usedTotals[divCode] = usedTotals[divCode] or {}
							usedTotals[divCode][unitKey] = (usedTotals[divCode][unitKey] or 0) + usedTotal
						end
					end
				end
			end
		end
	end

	return usedTotals
end

local function buildDivisionLists(baseText, usedTotals, filterDiv)
	local out = {}
	local currentCategory = nil

	-- We will store, per category, the units that appear in table order:
	-- catOrder[#] = categoryName
	-- catUnits[categoryName] = { {unitKey=..., label=...}, ... }
	local catOrder, catUnits = {}, {}

	for line in tostring(baseText):gmatch("[^\n]+") do
		-- Category header rows look like: ! colspan="14" ... | '''Personnel & readiness'''
		if line:match("^!%s*colspan") and line:find("'''", 1, true) then
			local cat = line:match("'''(.-)'''")
			cat = trim(cat or "")
			if cat ~= "" then
				currentCategory = cat
				if not catUnits[currentCategory] then
					catUnits[currentCategory] = {}
					catOrder[#catOrder+1] = currentCategory
				end
			end

		-- Unit rows start with "| " and have 14 parts
		elseif line:match("^|%s") and line:find("||", 1, true) then
			local parts = mw.text.split(line:gsub("^|%s*", ""), "%s*||%s*")
			if #parts == 14 and currentCategory then
				local unitKey = unitKeyFromCell(parts[2])
				local label = listLabelForRow(unitKey, parts[2])

				-- Keep the row in category order once (the usedTotals decide whether it prints)
				catUnits[currentCategory][#catUnits[currentCategory]+1] = {
					unitKey = unitKey,
					label = label
				}
			end
		end
	end

	-- Helper to emit one division
	local function emitDivision(divCode)
		local divTitle = (DIV_LABEL[divCode] or divCode) .. " (" .. divCode .. ")"
		out[#out+1] = "=== " .. divTitle .. " ==="
		out[#out+1] = ""

		for _,cat in ipairs(catOrder) do
			local any = false
			for _,row in ipairs(catUnits[cat]) do
				local used = usedTotals[divCode] and usedTotals[divCode][row.unitKey] or 0
				if used and used > 0 then
					any = true
					break
				end
			end

			if any then
				out[#out+1] = "==== " .. cat .. " ===="
				out[#out+1] = ""
				for _,row in ipairs(catUnits[cat]) do
					local used = usedTotals[divCode] and usedTotals[divCode][row.unitKey] or 0
					if used and used > 0 then
						out[#out+1] = string.format("* %s — '''used: %s'''", row.label, fmt(used))
					end
				end
				out[#out+1] = ""
			end
		end
	end

	if filterDiv and filterDiv ~= "" then
		emitDivision(filterDiv)
	else
		for _,divCode in ipairs(DIV_ORDER) do
			emitDivision(divCode)
		end
	end

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

-- Public: prints a list like your example, for one division or all divisions.
-- Invoke:
--   {{#invoke:WarLeagueUnitUtilizationBVR|divisionList}}                -> all divisions
--   {{#invoke:WarLeagueUnitUtilizationBVR|divisionList|div=NSJ}}        -> one division
--   {{#invoke:WarLeagueUnitUtilizationBVR|divisionList|div=HFA}}        -> one division
function p.divisionList(frame)
	local args = (frame and frame.args) or {}

	local base = normalizeWikitextTable(RAW_BASE_TABLE)

	-- Build pools + utilization exactly like render() does
	local allUnits = collectAllUnitsFromBase(base)
	normalizeToeTemplates(allUnits)
	local pools = buildPools(allUnits)
	local util = select(1, computeUtilization(args, pools))

	local usedTotals = computeUsedTotalsByDivision(base, util)
	local filterDiv = trim(args.div or args.division or "")

	return buildDivisionLists(base, usedTotals, filterDiv)
end

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

	local base = normalizeWikitextTable(RAW_BASE_TABLE)

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

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

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

	local rendered = overlayTable(base, util)

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

	return rendered
end

return p