How the army lists in this builder are structured, and how to add a new supplement, nation, or community list of your own. No code required — every army list is a JSON data file.
On this page
Every army list — a nation, faction, or force within a supplement — is described entirely by a JSON file. There is no code to write. A JSON file lists the units a nation can field (infantry, cavalry, artillery, commanders, support options) and the brigades those units can be organised into, along with the min/max counts and special rules that govern each one.
The app reads these files, builds the roster-building UI from them automatically, and enforces the rules you describe (minimums, maximums, "one per X points", "requires 2 of these first", and so on) without any extra plumbing. Getting a new army list into the builder is a matter of writing the JSON correctly and pointing the app at it.
Army lists live under src/data/supplements/<supplement-id>/. Each supplement is a folder containing one supplement.json file and a nations/ folder with one JSON file per army list:
src/data/supplements/
clash-of-eagles/
supplement.json
nations/
russia.json
france.json
austria.json
...
albion-triumphant-2/
supplement.json
nations/
brunswick.json
french-army-corps.json
...
community-lists/
supplement.json
nations/
ottoman-empire.json
prussia-1806.jsonWhich supplement a nation belongs to is determined purely by which folder it sits in — a nation's own JSON file never states its supplement id. This is important: renaming a supplement folder moves every nation inside it, and copying a nation file into a different supplement folder re-parents it.
supplement.json is small — it just describes the book (or, for community lists, the collection) that a group of nations belongs to:
{
"id": "community-lists",
"name": "Community Lists",
"blurb": "Fan-created Black Powder army lists shared by the community, not part of an official Warlord Games supplement. Each list credits its author."
}| Field | What it's for |
|---|---|
id | Unique short id, kebab-case. Should match the folder name. |
name | Full title shown in the supplement picker. |
blurb | One or two sentences describing the book/collection. |
A nation file has a handful of top-level fields, then the two big lists — units and brigades — covered in their own sections below.
{
"id": "ottoman-empire",
"name": "Ottoman Empire",
"blurb": "Napoleonic-era Ottoman army: Janissary corps, Sipahi and Deli cavalry, ...",
"flagFile": "/flags/ottoman-empire.svg",
"attribution": {
"author": "Napoleonic Wargames",
"sourceUrl": "https://www.youtube.com/watch?v=..."
},
"units": [ /* ... see "Defining units" ... */ ],
"brigades": [ /* ... see "Defining brigades" ... */ ],
"pointsCaps": [ /* optional, see "Army-wide rules" */ ],
"unitRatioCaps": [ /* optional, see "Army-wide rules" */ ],
"notes": [
"Freeform bullet points shown to the player summarising the force-org rules."
],
"specialRules": [
"In-game special rules with a battlefield effect — see below."
],
"alliesRules": [
{ "maxPercent": 25, "nationIds": ["some-other-nation-id"] }
]
}| Field | Required? | What it's for |
|---|---|---|
id | Yes | Unique short id, kebab-case. Used in the URL. |
name | Yes | Display name in the nation picker and roster header. |
blurb | Yes | Short description shown on the nation's card. |
flagFile / flagFiles | No | Path under public/flags/ to an SVG flag. Use flagFiles (an array) instead when one entry covers several historical states, e.g. the Confederation of the Rhine. |
attribution | Community lists only | { author, sourceUrl? } — credits whoever wrote the list. See Community lists. |
notes | Yes (array, can be empty) | Freeform bullet points reminding the player of force-organisation rules that aren't (or can't be) structurally enforced — ratios, per-points scaling, unimplemented upgrades/downgrades. Not for in-game special rules or allied contingents — see specialRules and alliesRules below. |
specialRules | No | National special rules with an actual in-game effect — special formations, commander traits, unique unit interactions. Shown to the player in their own "Special Rules" section, separate from the force-org notes. See Special rules vs. notes. |
alliesRules | No | Structured allied-contingent allowances (max % of points, which nations). Shown in its own "Allied Contingents" section. See Allied contingents. |
Both fields are freeform string arrays on the nation, but they're shown to the player in different places and mean different things — keep them separate rather than lumping everything into notes:
notes is for force-organisation reminders — things about how the army list is built (min/max counts, "1 per N points", allied percentages, periods, upgrades this builder doesn't model yet). It's shown under a "Force organisation reference" disclaimer, because none of it is structurally checked.specialRules is for battlefield rules — things that change how a unit or the army plays once the game starts. It's shown under its own "National special rules" heading.Some real examples of what belongs in each:
| Field | Example |
|---|---|
notes | "0 to 1 Earthworks per 500 points." — a force-org scaling rule, already structurally enforced via maxPerPoints but restated for clarity. |
specialRules | "'No prisoners! No pity!': against a French army, all Prussian infantry and cavalry count as Tough Fighters." — a rule that changes how units fight. |
specialRules | "Angriffskolonne (Assault Column) and Zug Column (Column of Companies) represent Prussian attack and manoeuvre formations." — special formations available on the tabletop. |
specialRules | "The C-in-C may be the Sultan himself (+25 pts on top of Staff Rating), giving all commanders +1 to their C-in-C re-rolls. …" — a commander trait with a gameplay effect, even though it also unlocks force-org options. |
When in doubt: if it changes what happens on the tabletop, it's a special rule; if it's about which units you're allowed to buy and how many, it's a note.
Rather than a freeform "Allies: up to 25%..." note, allied allowances are structured as alliesRules — an array on the nation, since a nation can have more than one allowance (see France 1812 below, which gets a separate 25% for its Imperial Guard and a further 25% for any other ally). Each entry has a max percentage of the army's points, and either a list of specific allied nation ids or a freeform note when the book doesn't name a specific list:
"alliesRules": [
{ "maxPercent": 25, "nationIds": ["french-imperial-guard"] },
{ "maxPercent": 25, "note": "any other allied list" }
]nationIds reference other nations' id fields in this same data set (they don't have to be in the same supplement — Austria's allies span both france and saxony, from two different supplements). The app resolves these to clickable links to that nation's own list. Use note alone, with no nationIds, for a generic grant like "any one allied list" that isn't tied to a specific nation:
"alliesRules": [
{ "maxPercent": 25, "note": "any one allied list" }
]note can also carry extra conditions alongside specific nationIds, e.g. the French Army of the North Imperial Guard (1815), whose allied Corps brigades still enforce that Corps list's own minimums:
"alliesRules": [
{
"maxPercent": 25,
"nationIds": ["french-army-corps"],
"note": "any minimums from that list apply to the points spent here (e.g. taking a Guard Heavy Cavalry Brigade needs three Corps infantry brigades first)"
}
]The roster builder doesn't support actually adding allied brigades to a list yet — alliesRules is reference information only, shown in its own "Allied Contingents" section alongside the special rules and force-org notes.
Each entry in units is one troop type, commander, or support option the nation can field. It defines the stat line shown to the player and the cost(s) it can be taken for. A plain infantry unit with a single fixed cost:
{
"id": "fr-line-infantry",
"name": "French Line Infantry",
"category": "Infantry",
"type": "Regular Infantry",
"armament": "Smoothbore Musket",
"handToHand": "6",
"shooting": "3",
"morale": "4",
"stamina": "3",
"special": ["Pas de Charge"],
"cost": 38
}category must be one of Command, Infantry, Cavalry, Artillery, or Support — this is what slots a unit id can be listed in later. type and armament are free text shown on the stat line. handToHand, shooting, morale, stamina, and special are optional and omitted where they don't apply (e.g. a support option with no combat stats).
When a unit has several priced choices — a weapon option, a commander's staff rating, half vs. full battery — use variants instead of a flat cost. Each variant is one choice the player picks from a dropdown:
{
"id": "ot-cinc-commander",
"name": "(Divisional) Commander",
"category": "Command",
"type": "Command",
"armament": "-",
"variants": [
{ "label": "Staff Rating 6", "cost": 0, "staffRating": 6 },
{ "label": "Staff Rating 7", "cost": 25, "staffRating": 7 },
{ "label": "Staff Rating 8", "cost": 50, "staffRating": 8 }
]
}A variant's label is what's shown in the picker; cost is what that choice adds to the army total. staffRating is used for commanders. handToHand/shooting can also be overridden per variant when a weapon option changes a unit's stat line.
brigades is the force-organisation chart: the list of brigade types a player can add to their army, and how many of each. A brigade type has an id/name, a min/max count, an optional commander requirement, and a list of slots describing what can go inside one instance of it:
{
"id": "fac-infantry-brigade",
"name": "Infantry Brigade",
"min": 1,
"max": 6,
"commanderUnitId": "fac-brig-cmd",
"slots": [
{
"label": "Line Infantry Battalion (3-7 per brigade)",
"unitIds": ["fac-line"],
"min": 3,
"max": 7
}
]
}min/max on the brigade itself cap how many instances of this brigade type the whole army can take (e.g. "1 to 6 Infantry Brigades"). If a brigade needs its own commander (most combat brigades do), set commanderUnitId to the id of a Command-category unit; leave it out for pooled/attached support brigades (reserve artillery, earthworks) that don't carry their own commander.
The simplest slot just lists eligible unit ids and a count range, as above. From there, a slot can add per-unit and per-group limits within that same range:
unitLimits caps how many of one specific unit id can fill the slot, on top of the slot's overall min/max:
{
"label": "Heavy Cavalry Regiments (up to 2 Carabinier + 3 Cuirassier, OR up to 4 Dragoon)",
"unitIds": ["fr-carabinier", "fr-cuirassier", "fr-dragoon"],
"min": 1,
"max": 4,
"unitLimits": [
{ "unitId": "fr-carabinier", "max": 2 },
{ "unitId": "fr-cuirassier", "max": 3 },
{ "unitId": "fr-dragoon", "max": 4 }
],
"unitGroupLimits": [
{ "unitIds": ["fr-carabinier", "fr-cuirassier"], "max": 3, "label": "Carabinier/Cuirassier combined" }
],
"mutuallyExclusiveGroups": [
{ "unitIds": ["fr-carabinier", "fr-cuirassier"], "label": "Carabinier/Cuirassier" },
{ "unitIds": ["fr-dragoon"], "label": "Dragoon" }
]
}unitGroupLimits caps the combined quantity of several unit ids together — here, Carabinier + Cuirassier together can't exceed 3, even though each alone is allowed up to its own limit. mutuallyExclusiveGroups takes this further: it says only one of the listed groups may actually be used at all (you can take Carabinier/Cuirassier or Dragoon, never both in the same brigade). Set maxGroups on the first group's entry to allow "up to N of the following" instead of strictly one — see the Austrian example below, which allows up to 2 of 3 option groups:
{
"label": "Additional Troops (choose up to 2 options: 1-2 Grenadier, up to 2 Grenz, or 1 6-pdr Battery)",
"unitIds": ["at-grenadier", "at-grenz", "at-6pdr-brigade-battery"],
"min": 0,
"max": 4,
"unitLimits": [
{ "unitId": "at-grenadier", "max": 2 },
{ "unitId": "at-grenz", "max": 2 },
{ "unitId": "at-6pdr-brigade-battery", "max": 1 }
],
"mutuallyExclusiveGroups": [
{ "unitIds": ["at-grenadier"], "label": "Grenadier battalions", "maxGroups": 2 },
{ "unitIds": ["at-grenz"], "label": "Large Grenz battalions" },
{ "unitIds": ["at-6pdr-brigade-battery"], "label": "6-pdr Brigade battery" }
]
}singleUnitType: true means every unit taken in the slot must be the same id — you can fill a "Spanish, Portuguese, or Rhinbund" slot with Spanish battalions or Portuguese battalions, but not a mix of the two.
Some brigades (most Napoleonic infantry brigades) aren't just a flat list of battalions — they're organised into regiments, and each regiment has its own battalion count. For this, a slot carries a regiment block instead of a flat unitIds/min/max. The outer slot's own unitIds/min/max are ignored once regiment is present — set them to []/0/0 as placeholders:
{
"label": "Line Infantry Regiments",
"unitIds": [],
"min": 0,
"max": 0,
"regiment": {
"label": "Line Infantry Regiment",
"min": 0,
"max": 3,
"slots": [
{
"label": "Line Infantry Battalion (1-5)",
"unitIds": ["fr-line-infantry"],
"min": 1,
"max": 5
}
]
}
}This reads as: "0 to 3 Line Infantry Regiments, each containing 1 to 5 Line Infantry battalions." The regiment's own slots array uses the same shape as a brigade slot (label/unitIds/min/max, plus singleUnitType and unitLimits where needed) — this is how a "Foreign Infantry Regiment" can require one nationality only per regiment:
{
"label": "Foreign Infantry Regiment",
"min": 0,
"max": 1,
"slots": [
{
"label": "Spanish (1-2), Portuguese (1-2), or Rhinbund (1) — one type only",
"unitIds": ["fr-spanish-infantry", "fr-portuguese-infantry", "fr-rheinbund-infantry"],
"min": 1,
"max": 2,
"singleUnitType": true,
"unitLimits": [{ "unitId": "fr-rheinbund-infantry", "max": 1 }]
}
]
}A regiment can also cap itself across the whole army (not just per brigade) with armyMax — e.g. "max 1 Light Infantry Regiment in the entire army":
{
"label": "Light Infantry Regiment",
"min": 0,
"max": 1,
"armyMax": 1,
"slots": [
{ "label": "Light Infantry Battalion (1-5)", "unitIds": ["fr-light-infantry"], "min": 1, "max": 5 }
]
}A slot can be locked behind a condition, or have its available quantity scale with something else in the army, instead of using a fixed number.
requiresBrigadeCount only makes a slot available once the combined quantity of the listed unit ids within the same brigade instance reaches a minimum — counted across every slot in that brigade, flat or regiment. For example, a Small Lancer Regiment that needs 2+ heavy cavalry regiments already taken:
{
"label": "Small Lancer Regiment (0-1, requires 2+ heavy cavalry regiments)",
"unitIds": ["fr-lancer-small"],
"min": 0,
"max": 1,
"requiresBrigadeCount": {
"unitIds": ["fr-carabinier", "fr-cuirassier", "fr-dragoon"],
"min": 2
}
}requiresArmyUnitCount works the same way but counts across every brigade in the army, not just this one — used for pooled/reserve artillery that only becomes available once the army has enough infantry or cavalry:
{
"label": "Foot Artillery Battery (up to 1, if 6+ battalions in army)",
"unitIds": ["fr-foot-arty"],
"min": 0,
"max": 1,
"requiresArmyUnitCount": {
"unitIds": ["fr-line-infantry", "fr-light-infantry", "fr-combined-grenadier", "fr-voltigeurs"],
"min": 6
}
}maxPerPoints makes the slot's effective maximum grow with the army's total points, e.g. earthworks at "1 per 500 points":
{
"label": "Earthworks (1 per 500 points)",
"unitIds": ["ru-earthworks-standard", "ru-earthworks-large-1", "ru-earthworks-large-2"],
"min": 1,
"max": 1,
"maxPerPoints": { "perPoints": 500 }
}dynamicUnitLimits is for regimental-artillery-style rules where the available quantity of one unit id depends on how many battalions or regiments are already in the same brigade. Two shapes are supported: perBattalions (max = battalions counted in the given slots ÷ ratio, rounded down) and perQualifyingRegiment (max = number of regiments, among the given regiment slots, that have reached a minimum battalion count):
{
"label": "Regimental Artillery (section per 2 battalions, OR battery per regiment of 3+)",
"unitIds": ["fr-regimental-arty-section", "fr-regimental-battery"],
"min": 0,
"max": 3,
"singleUnitType": true,
"dynamicUnitLimits": [
{
"unitId": "fr-regimental-arty-section",
"perBattalions": { "countSlotIndices": [0, 1, 2], "ratio": 2 }
},
{
"unitId": "fr-regimental-battery",
"perQualifyingRegiment": { "regimentSlotIndices": [0, 1, 2], "minBattalions": 3 }
}
]
}The index arrays (countSlotIndices, regimentSlotIndices) refer to the position of slots within this same brigade's slots array — in this example, indices 0, 1, and 2 are the brigade's three infantry regiment slots.
These fields sit on the brigade type itself (alongside slots), not on an individual slot.
| Field | What it does |
|---|---|
maxBattalions | Caps the total unit count across all (or selected) slots in a single brigade instance, e.g. "max 10 battalions per Infantry Brigade" → { max: 10, countSlotIndices: [0,1,2] }. |
maxRegimentsTotal | Caps the combined number of regiments across several regiment slots, e.g. "1-3 Infantry Regiments total" across the Line/Light/Foreign slots → { slotIndices: [0,1,2], max: 3, min: 1 }. |
requires | This brigade type can't be added until other brigade types reach a combined minimum count, e.g. Heavy Cavalry requiring 2 Infantry Brigades first → { brigadeTypeIds: ["fr-infantry-brigade"], min: 2 }. |
maxRatio | Caps how many of this brigade type you can take relative to other brigade types, e.g. "max 1 Light Cavalry Brigade per 2 Infantry Brigades" → { brigadeTypeIds: ["fr-infantry-brigade"], ratio: 2 }. |
minPerPoints | Requires at least one of this brigade type per N points in the army (in addition to min), e.g. "at least 1 Line Brigade per 500 points" → { perPoints: 500 }. |
unitCaps | Per-unit caps that apply across every instance of this brigade type in the army (not just one brigade). Optional requiresBrigades gates the unit behind another brigade type being taken first; optional maxRatio makes the army-wide cap scale with another brigade type's count. |
note | Freeform text for rules that aren't (or can't easily be) structurally enforced — shown to the player as a reminder. |
A unitCaps example, from the Ottoman Deli Brigade — Mamelukes are capped at 6 across the whole army, but only once at least one Deli Brigade has been taken:
"unitCaps": [
{
"unitId": "ot-mameluke",
"armyMax": 6,
"requiresBrigades": { "brigadeTypeIds": ["ot-deli-brigade"], "min": 1 }
}
]Two optional fields sit on the nation itself, alongside units and brigades, for rules that span multiple brigade types.
pointsCaps limits a group of brigade types to a percentage of the army's total points — e.g. Imperial Guard brigades capped at 25% of the army:
"pointsCaps": [
{
"brigadeTypeIds": ["ru-guard-infantry-brigade", "ru-guard-light-cavalry-brigade", "ru-guard-heavy-cavalry-brigade"],
"maxPercent": 25,
"label": "Imperial Guard brigades"
}
]unitRatioCaps limits the army-wide quantity of one group of units to a ratio of another group, regardless of which brigades they're in — e.g. "1 Grenz battalion per 4 line infantry battalions":
"unitRatioCaps": [
{
"capUnitIds": ["at-grenz"],
"perUnitIds": ["at-infantry-german", "at-infantry-hungarian"],
"ratio": 4,
"label": "Grenz battalions (max 1 per 4 line infantry battalions)"
}
]Fan-created lists that aren't from an official Warlord Games supplement go in src/data/supplements/community-lists/nations/. The only difference from an official list is the attribution field, which credits the original author and links to their source:
{
"id": "ottoman-empire",
"name": "Ottoman Empire",
"attribution": {
"author": "Napoleonic Wargames",
"sourceUrl": "https://www.youtube.com/watch?v=BphFMuBJvtM"
},
...
}This shows a "Community list by …" credit on the nation's card, linking out to sourceUrl (a video, PDF, or forum post) when provided. Everything else — units, brigades, army-wide rules — works exactly the same as an official list.
The whole Community Lists supplement is hidden unless the NEXT_PUBLIC_SHOW_COMMUNITY_LISTS environment variable is set to true (see .env.example) — set it in whichever environments should show fan-made lists (e.g. a preview/staging deployment), and leave it unset in production until you're ready to show them there too.
JSON files aren't picked up automatically — they're imported explicitly in src/data/index.ts. To add a brand-new supplement:
src/data/supplements/<supplement-id>/supplement.json.src/data/supplements/<supplement-id>/nations/<nation-id>.json for each army list (no supplement reference needed inside the file — the folder is the source of truth).src/data/index.ts, import supplement.json and each nation file, add the supplement to the supplements array, and add a withSupplement(...) block for its nations, appended to the nations array.To add a new nation to an existing supplement, just add step 2 and 3's import (drop the file into that supplement's nations/ folder, import it in index.ts, and add it to that supplement's withSupplement(...) list).
If the nation has a flag, add the SVG under public/flags/ and reference it from flagFile (or flagFiles) in the nation JSON.
units: every troop type, with stats and cost(s).brigades: the force-org chart built from those unit ids.notes summarising anything not structurally enforced.specialRules for any in-game special rules (see Special rules vs. notes).alliesRules for any allied-contingent allowances (see Allied contingents).public/flags/ (optional).nations/ folder, import it in src/data/index.ts, and add it to that supplement's list.supplement.json.src/data/index.ts; add the supplement to supplements and its nations via withSupplement(...).community-lists/nations/.attribution field crediting the original author, with a sourceUrl where possible.Every field described on this page is defined with more detail in src/data/types.ts, which is the definitive reference if you get stuck.