@alpaca-software/40kdc-data 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/bundle-schemas.d.ts +3 -0
- package/dist/bundle-schemas.js +137 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +31 -0
- package/dist/codegen-data.d.ts +1 -0
- package/dist/codegen-data.js +128 -0
- package/dist/commands/translate.d.ts +7 -0
- package/dist/commands/translate.js +238 -0
- package/dist/commands/validate-all.d.ts +3 -0
- package/dist/commands/validate-all.js +20 -0
- package/dist/commands/validate-core.d.ts +3 -0
- package/dist/commands/validate-core.js +12 -0
- package/dist/commands/validate-enrichment.d.ts +3 -0
- package/dist/commands/validate-enrichment.js +12 -0
- package/dist/convert-faction.d.ts +45 -0
- package/dist/convert-faction.js +479 -0
- package/dist/converters/configs/adepta-sororitas.d.ts +3 -0
- package/dist/converters/configs/adepta-sororitas.js +70 -0
- package/dist/converters/configs/adeptus-astartes.d.ts +3 -0
- package/dist/converters/configs/adeptus-astartes.js +74 -0
- package/dist/converters/configs/adeptus-custodes.d.ts +3 -0
- package/dist/converters/configs/adeptus-custodes.js +14 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts +3 -0
- package/dist/converters/configs/adeptus-mechanicus.js +51 -0
- package/dist/converters/configs/aeldari.d.ts +3 -0
- package/dist/converters/configs/aeldari.js +79 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts +3 -0
- package/dist/converters/configs/agents-of-the-imperium.js +57 -0
- package/dist/converters/configs/astra-militarum.d.ts +3 -0
- package/dist/converters/configs/astra-militarum.js +80 -0
- package/dist/converters/configs/black-templars.d.ts +3 -0
- package/dist/converters/configs/black-templars.js +16 -0
- package/dist/converters/configs/blood-angels.d.ts +3 -0
- package/dist/converters/configs/blood-angels.js +16 -0
- package/dist/converters/configs/chaos-daemons.d.ts +3 -0
- package/dist/converters/configs/chaos-daemons.js +40 -0
- package/dist/converters/configs/chaos-knights.d.ts +3 -0
- package/dist/converters/configs/chaos-knights.js +14 -0
- package/dist/converters/configs/chaos-space-marines.d.ts +3 -0
- package/dist/converters/configs/chaos-space-marines.js +95 -0
- package/dist/converters/configs/crimson-fists.d.ts +3 -0
- package/dist/converters/configs/crimson-fists.js +16 -0
- package/dist/converters/configs/dark-angels.d.ts +3 -0
- package/dist/converters/configs/dark-angels.js +16 -0
- package/dist/converters/configs/death-guard.d.ts +3 -0
- package/dist/converters/configs/death-guard.js +30 -0
- package/dist/converters/configs/deathwatch.d.ts +3 -0
- package/dist/converters/configs/deathwatch.js +16 -0
- package/dist/converters/configs/drukhari.d.ts +3 -0
- package/dist/converters/configs/drukhari.js +51 -0
- package/dist/converters/configs/emperors-children.d.ts +3 -0
- package/dist/converters/configs/emperors-children.js +38 -0
- package/dist/converters/configs/genestealer-cults.d.ts +3 -0
- package/dist/converters/configs/genestealer-cults.js +36 -0
- package/dist/converters/configs/grey-knights.d.ts +3 -0
- package/dist/converters/configs/grey-knights.js +39 -0
- package/dist/converters/configs/imperial-fists.d.ts +3 -0
- package/dist/converters/configs/imperial-fists.js +16 -0
- package/dist/converters/configs/imperial-knights.d.ts +3 -0
- package/dist/converters/configs/imperial-knights.js +14 -0
- package/dist/converters/configs/iron-hands.d.ts +3 -0
- package/dist/converters/configs/iron-hands.js +16 -0
- package/dist/converters/configs/leagues-of-votann.d.ts +3 -0
- package/dist/converters/configs/leagues-of-votann.js +32 -0
- package/dist/converters/configs/necrons.d.ts +3 -0
- package/dist/converters/configs/necrons.js +19 -0
- package/dist/converters/configs/orks.d.ts +3 -0
- package/dist/converters/configs/orks.js +71 -0
- package/dist/converters/configs/raven-guard.d.ts +3 -0
- package/dist/converters/configs/raven-guard.js +16 -0
- package/dist/converters/configs/salamanders.d.ts +3 -0
- package/dist/converters/configs/salamanders.js +16 -0
- package/dist/converters/configs/space-wolves.d.ts +3 -0
- package/dist/converters/configs/space-wolves.js +16 -0
- package/dist/converters/configs/tau-empire.d.ts +3 -0
- package/dist/converters/configs/tau-empire.js +44 -0
- package/dist/converters/configs/thousand-sons.d.ts +3 -0
- package/dist/converters/configs/thousand-sons.js +30 -0
- package/dist/converters/configs/tyranids.d.ts +3 -0
- package/dist/converters/configs/tyranids.js +27 -0
- package/dist/converters/configs/ultramarines.d.ts +3 -0
- package/dist/converters/configs/ultramarines.js +16 -0
- package/dist/converters/configs/white-scars.d.ts +3 -0
- package/dist/converters/configs/white-scars.js +16 -0
- package/dist/converters/configs/world-eaters.d.ts +3 -0
- package/dist/converters/configs/world-eaters.js +43 -0
- package/dist/converters/faction-config.d.ts +53 -0
- package/dist/converters/faction-config.js +22 -0
- package/dist/converters/id-generator.d.ts +14 -0
- package/dist/converters/id-generator.js +65 -0
- package/dist/converters/keyword-filter.d.ts +26 -0
- package/dist/converters/keyword-filter.js +78 -0
- package/dist/converters/stat-parser.d.ts +22 -0
- package/dist/converters/stat-parser.js +84 -0
- package/dist/converters/view-selector.d.ts +54 -0
- package/dist/converters/view-selector.js +96 -0
- package/dist/converters/weapon-dedup.d.ts +60 -0
- package/dist/converters/weapon-dedup.js +120 -0
- package/dist/data/bundle.generated.d.ts +3 -0
- package/dist/data/bundle.generated.js +3 -0
- package/dist/data/collection.d.ts +64 -0
- package/dist/data/collection.js +118 -0
- package/dist/data/dataset.d.ts +50 -0
- package/dist/data/dataset.js +134 -0
- package/dist/data/entities.d.ts +80 -0
- package/dist/data/entities.js +133 -0
- package/dist/data/index.d.ts +59 -0
- package/dist/data/index.js +57 -0
- package/dist/data/normalize.d.ts +29 -0
- package/dist/data/normalize.js +37 -0
- package/dist/data/types.d.ts +43 -0
- package/dist/data/types.js +25 -0
- package/dist/generated.d.ts +1084 -0
- package/dist/generated.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/known-support-10e.d.ts +31 -0
- package/dist/known-support-10e.js +113 -0
- package/dist/port-10e-faction.d.ts +52 -0
- package/dist/port-10e-faction.js +413 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +31 -0
- package/dist/schema-loader.d.ts +15 -0
- package/dist/schema-loader.js +79 -0
- package/dist/validate.d.ts +21 -0
- package/dist/validate.js +124 -0
- package/package.json +77 -0
- package/schemas/$defs/common.schema.json +86 -0
- package/schemas/$defs/game-version-ref.schema.json +11 -0
- package/schemas/core/deployment-pattern.schema.json +102 -0
- package/schemas/core/detachment.schema.json +56 -0
- package/schemas/core/enhancement.schema.json +46 -0
- package/schemas/core/faction.schema.json +29 -0
- package/schemas/core/force-disposition.schema.json +22 -0
- package/schemas/core/game-version.schema.json +20 -0
- package/schemas/core/leader-attachment.schema.json +18 -0
- package/schemas/core/mission-matchup.schema.json +25 -0
- package/schemas/core/mission.schema.json +42 -0
- package/schemas/core/roster.schema.json +203 -0
- package/schemas/core/secondary-card.schema.json +195 -0
- package/schemas/core/stratagem.schema.json +58 -0
- package/schemas/core/terrain-layout.schema.json +135 -0
- package/schemas/core/unit-composition.schema.json +38 -0
- package/schemas/core/unit.schema.json +125 -0
- package/schemas/core/wargear-option.schema.json +47 -0
- package/schemas/core/weapon.schema.json +56 -0
- package/schemas/enrichment/ability-dsl/ability.schema.json +60 -0
- package/schemas/enrichment/ability-dsl/condition.schema.json +48 -0
- package/schemas/enrichment/ability-dsl/effect.schema.json +145 -0
- package/schemas/enrichment/ability-dsl/scope.schema.json +12 -0
- package/schemas/enrichment/interaction-flag.schema.json +17 -0
- package/schemas/enrichment/phase-mapping.schema.json +14 -0
- package/schemas/enrichment/resource-pool.schema.json +36 -0
- package/schemas/enrichment/timing-flag.schema.json +28 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createValidator } from "../schema-loader.js";
|
|
2
|
+
import { validateFiles } from "../validate.js";
|
|
3
|
+
import { formatReport } from "../report.js";
|
|
4
|
+
export async function validateCoreCommand(opts) {
|
|
5
|
+
const ajv = createValidator();
|
|
6
|
+
const result = await validateFiles(ajv, "core/**/*.json");
|
|
7
|
+
const output = formatReport(result, opts.reporter);
|
|
8
|
+
console.log(output);
|
|
9
|
+
if (result.failed > 0) {
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createValidator } from "../schema-loader.js";
|
|
2
|
+
import { validateFiles } from "../validate.js";
|
|
3
|
+
import { formatReport } from "../report.js";
|
|
4
|
+
export async function validateEnrichmentCommand(opts) {
|
|
5
|
+
const ajv = createValidator();
|
|
6
|
+
const result = await validateFiles(ajv, "enrichment/**/*.json");
|
|
7
|
+
const output = formatReport(result, opts.reporter);
|
|
8
|
+
console.log(output);
|
|
9
|
+
if (result.failed > 0) {
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic faction converter: army-assist → 40kdc-data format.
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx tools/src/convert-faction.ts <faction-id>
|
|
5
|
+
* Example: npx tsx tools/src/convert-faction.ts emperors-children
|
|
6
|
+
*
|
|
7
|
+
* Faction configs are registered via side-effect imports from ./converters/configs/.
|
|
8
|
+
*/
|
|
9
|
+
import { type FactionConfig } from "./converters/faction-config.js";
|
|
10
|
+
import "./converters/configs/world-eaters.js";
|
|
11
|
+
import "./converters/configs/emperors-children.js";
|
|
12
|
+
import "./converters/configs/chaos-knights.js";
|
|
13
|
+
import "./converters/configs/imperial-knights.js";
|
|
14
|
+
import "./converters/configs/leagues-of-votann.js";
|
|
15
|
+
import "./converters/configs/drukhari.js";
|
|
16
|
+
import "./converters/configs/genestealer-cults.js";
|
|
17
|
+
import "./converters/configs/grey-knights.js";
|
|
18
|
+
import "./converters/configs/thousand-sons.js";
|
|
19
|
+
import "./converters/configs/death-guard.js";
|
|
20
|
+
import "./converters/configs/adeptus-custodes.js";
|
|
21
|
+
import "./converters/configs/adepta-sororitas.js";
|
|
22
|
+
import "./converters/configs/agents-of-the-imperium.js";
|
|
23
|
+
import "./converters/configs/adeptus-mechanicus.js";
|
|
24
|
+
import "./converters/configs/tau-empire.js";
|
|
25
|
+
import "./converters/configs/tyranids.js";
|
|
26
|
+
import "./converters/configs/necrons.js";
|
|
27
|
+
import "./converters/configs/chaos-daemons.js";
|
|
28
|
+
import "./converters/configs/orks.js";
|
|
29
|
+
import "./converters/configs/aeldari.js";
|
|
30
|
+
import "./converters/configs/chaos-space-marines.js";
|
|
31
|
+
import "./converters/configs/astra-militarum.js";
|
|
32
|
+
import "./converters/configs/adeptus-astartes.js";
|
|
33
|
+
import "./converters/configs/blood-angels.js";
|
|
34
|
+
import "./converters/configs/dark-angels.js";
|
|
35
|
+
import "./converters/configs/space-wolves.js";
|
|
36
|
+
import "./converters/configs/black-templars.js";
|
|
37
|
+
import "./converters/configs/deathwatch.js";
|
|
38
|
+
import "./converters/configs/ultramarines.js";
|
|
39
|
+
import "./converters/configs/imperial-fists.js";
|
|
40
|
+
import "./converters/configs/crimson-fists.js";
|
|
41
|
+
import "./converters/configs/iron-hands.js";
|
|
42
|
+
import "./converters/configs/raven-guard.js";
|
|
43
|
+
import "./converters/configs/salamanders.js";
|
|
44
|
+
import "./converters/configs/white-scars.js";
|
|
45
|
+
export declare function convertFaction(config: FactionConfig): void;
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic faction converter: army-assist → 40kdc-data format.
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx tsx tools/src/convert-faction.ts <faction-id>
|
|
5
|
+
* Example: npx tsx tools/src/convert-faction.ts emperors-children
|
|
6
|
+
*
|
|
7
|
+
* Faction configs are registered via side-effect imports from ./converters/configs/.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { resolve, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { nameToId, parseStratagemType, parsePlayerTurn, mapPhases } from "./converters/id-generator.js";
|
|
13
|
+
import { parseMove, parseTargetNumber, parseIntStat, parseInvuln } from "./converters/stat-parser.js";
|
|
14
|
+
import { findFactionViewIndex, getViewEntries, getPointsForView, splitIntoViews } from "./converters/view-selector.js";
|
|
15
|
+
import { buildWeaponRegistry } from "./converters/weapon-dedup.js";
|
|
16
|
+
import { getKeywordsForFaction } from "./converters/keyword-filter.js";
|
|
17
|
+
import { getFactionConfig, listFactions } from "./converters/faction-config.js";
|
|
18
|
+
// Register all faction configs
|
|
19
|
+
import "./converters/configs/world-eaters.js";
|
|
20
|
+
import "./converters/configs/emperors-children.js";
|
|
21
|
+
import "./converters/configs/chaos-knights.js";
|
|
22
|
+
import "./converters/configs/imperial-knights.js";
|
|
23
|
+
import "./converters/configs/leagues-of-votann.js";
|
|
24
|
+
import "./converters/configs/drukhari.js";
|
|
25
|
+
import "./converters/configs/genestealer-cults.js";
|
|
26
|
+
import "./converters/configs/grey-knights.js";
|
|
27
|
+
import "./converters/configs/thousand-sons.js";
|
|
28
|
+
import "./converters/configs/death-guard.js";
|
|
29
|
+
import "./converters/configs/adeptus-custodes.js";
|
|
30
|
+
import "./converters/configs/adepta-sororitas.js";
|
|
31
|
+
import "./converters/configs/agents-of-the-imperium.js";
|
|
32
|
+
import "./converters/configs/adeptus-mechanicus.js";
|
|
33
|
+
import "./converters/configs/tau-empire.js";
|
|
34
|
+
import "./converters/configs/tyranids.js";
|
|
35
|
+
import "./converters/configs/necrons.js";
|
|
36
|
+
import "./converters/configs/chaos-daemons.js";
|
|
37
|
+
import "./converters/configs/orks.js";
|
|
38
|
+
import "./converters/configs/aeldari.js";
|
|
39
|
+
import "./converters/configs/chaos-space-marines.js";
|
|
40
|
+
import "./converters/configs/astra-militarum.js";
|
|
41
|
+
import "./converters/configs/adeptus-astartes.js";
|
|
42
|
+
import "./converters/configs/blood-angels.js";
|
|
43
|
+
import "./converters/configs/dark-angels.js";
|
|
44
|
+
import "./converters/configs/space-wolves.js";
|
|
45
|
+
import "./converters/configs/black-templars.js";
|
|
46
|
+
import "./converters/configs/deathwatch.js";
|
|
47
|
+
import "./converters/configs/ultramarines.js";
|
|
48
|
+
import "./converters/configs/imperial-fists.js";
|
|
49
|
+
import "./converters/configs/crimson-fists.js";
|
|
50
|
+
import "./converters/configs/iron-hands.js";
|
|
51
|
+
import "./converters/configs/raven-guard.js";
|
|
52
|
+
import "./converters/configs/salamanders.js";
|
|
53
|
+
import "./converters/configs/white-scars.js";
|
|
54
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
const ROOT = resolve(__dirname, "../..");
|
|
56
|
+
const SOURCE = resolve(process.env.HOME, "army-assist/src/assets/json");
|
|
57
|
+
const GAME_VERSION = { edition: "10th", dataslate: "2025-q3" };
|
|
58
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
59
|
+
function readJSON(filename) {
|
|
60
|
+
return JSON.parse(readFileSync(resolve(SOURCE, filename), "utf-8"));
|
|
61
|
+
}
|
|
62
|
+
function writeOutput(relPath, data) {
|
|
63
|
+
const outPath = resolve(ROOT, relPath);
|
|
64
|
+
writeFileSync(outPath, JSON.stringify(data, null, 2) + "\n");
|
|
65
|
+
console.log(` ✓ ${relPath} (${Array.isArray(data) ? data.length : 1} entries)`);
|
|
66
|
+
}
|
|
67
|
+
/** Determine unit role from keywords and abilities. */
|
|
68
|
+
function deriveRole(keywords, abilities, name) {
|
|
69
|
+
const kw = new Set(keywords.map((k) => k.toLowerCase()));
|
|
70
|
+
if (kw.has("epic hero"))
|
|
71
|
+
return "epic-hero";
|
|
72
|
+
if (kw.has("character"))
|
|
73
|
+
return "character";
|
|
74
|
+
const hasLeader = abilities.some((a) => a.type === "Core" && a.name === "Leader");
|
|
75
|
+
if (hasLeader)
|
|
76
|
+
return "character";
|
|
77
|
+
if (kw.has("battleline"))
|
|
78
|
+
return "battleline";
|
|
79
|
+
if (name.toLowerCase().includes("rhino") ||
|
|
80
|
+
kw.has("dedicated transport"))
|
|
81
|
+
return "dedicated-transport";
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Parse base size string. "32mm" → { shape: "round", diameter: 32 } */
|
|
85
|
+
function parseBaseSize(s) {
|
|
86
|
+
if (!s || s.trim() === "")
|
|
87
|
+
return undefined;
|
|
88
|
+
const round = s.match(/(\d+)\s*mm/i);
|
|
89
|
+
if (round)
|
|
90
|
+
return { shape: "round", diameter: parseInt(round[1], 10) };
|
|
91
|
+
const oval = s.match(/(\d+)\s*x\s*(\d+)/i);
|
|
92
|
+
if (oval)
|
|
93
|
+
return {
|
|
94
|
+
shape: "oval",
|
|
95
|
+
width: parseInt(oval[1], 10),
|
|
96
|
+
length: parseInt(oval[2], 10),
|
|
97
|
+
};
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Parse transport capacity from transport text. */
|
|
101
|
+
function parseTransport(s) {
|
|
102
|
+
if (!s || s.trim() === "")
|
|
103
|
+
return undefined;
|
|
104
|
+
// Match "capacity of N" or "N <FACTION> INFANTRY"
|
|
105
|
+
const capMatch = s.match(/capacity\s*(?:of\s*)?(\d+)/i) || s.match(/(\d+)\s+\S[\S\s]*?\s+(?:infantry|model)/i);
|
|
106
|
+
if (!capMatch)
|
|
107
|
+
return undefined;
|
|
108
|
+
const capacity = parseInt(capMatch[1], 10);
|
|
109
|
+
const result = { capacity };
|
|
110
|
+
if (/jump pack/i.test(s) && /cannot/i.test(s)) {
|
|
111
|
+
result.exclusion_keywords = ["Jump Pack"];
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
// ─── Main conversion ─────────────────────────────────────────────────
|
|
116
|
+
export function convertFaction(config) {
|
|
117
|
+
const { sourceFactionId, factionId, factionName, factionAbilityName } = config;
|
|
118
|
+
console.log(`Converting ${factionName} (${sourceFactionId} → ${factionId})...`);
|
|
119
|
+
console.log("Loading source data from army-assist...");
|
|
120
|
+
const datasheets = readJSON("Datasheets.json");
|
|
121
|
+
const allModels = readJSON("Datasheets_models.json");
|
|
122
|
+
const allWargear = readJSON("Datasheets_wargear.json");
|
|
123
|
+
const allAbilities = readJSON("Datasheets_abilities.json");
|
|
124
|
+
const allKeywords = readJSON("Datasheets_keywords.json");
|
|
125
|
+
const allPoints = readJSON("Datasheets_points.json");
|
|
126
|
+
const allLeaders = readJSON("Datasheets_leader.json");
|
|
127
|
+
const enhancements = readJSON("Enhancements.json");
|
|
128
|
+
const stratagems = readJSON("Stratagems.json");
|
|
129
|
+
const detachmentAbilities = readJSON("Detachment_abilities.json");
|
|
130
|
+
// Filter to target faction, skip datasheets with no model data (metadata entries)
|
|
131
|
+
const modelDatasheetIds = new Set(allModels.map((m) => m.datasheet_id));
|
|
132
|
+
const factionDatasheets = datasheets.filter((d) => d.faction_id === sourceFactionId && modelDatasheetIds.has(d.id));
|
|
133
|
+
const factionIds = new Set(factionDatasheets.map((d) => d.id));
|
|
134
|
+
const idToName = new Map(factionDatasheets.map((d) => [d.id, d.name]));
|
|
135
|
+
console.log(`Found ${factionDatasheets.length} ${factionName} datasheets\n`);
|
|
136
|
+
// ─── Determine view indices for shared units ───
|
|
137
|
+
const viewIndices = new Map();
|
|
138
|
+
for (const ds of factionDatasheets) {
|
|
139
|
+
const dsAbilities = allAbilities.filter((a) => a.datasheet_id === ds.id);
|
|
140
|
+
viewIndices.set(ds.id, findFactionViewIndex(dsAbilities, factionAbilityName));
|
|
141
|
+
}
|
|
142
|
+
// ─── Build units ───
|
|
143
|
+
console.log("Converting units...");
|
|
144
|
+
const units = [];
|
|
145
|
+
const unitWargearMap = new Map();
|
|
146
|
+
const unitAbilityNames = new Map();
|
|
147
|
+
for (const ds of factionDatasheets) {
|
|
148
|
+
const viewIdx = viewIndices.get(ds.id);
|
|
149
|
+
// Models
|
|
150
|
+
const dsModels = allModels.filter((m) => m.datasheet_id === ds.id);
|
|
151
|
+
const viewModels = getViewEntries(dsModels, viewIdx);
|
|
152
|
+
// Abilities (for role derivation and ability name collection)
|
|
153
|
+
const dsAbilities = allAbilities.filter((a) => a.datasheet_id === ds.id);
|
|
154
|
+
const viewAbilities = getViewEntries(dsAbilities, viewIdx);
|
|
155
|
+
const abilityNames = viewAbilities
|
|
156
|
+
.filter((a) => a.type !== "Faction")
|
|
157
|
+
.map((a) => a.name);
|
|
158
|
+
unitAbilityNames.set(ds.id, [...new Set(abilityNames)]);
|
|
159
|
+
// Keywords — use faction-aware filtering for shared units
|
|
160
|
+
const dsKeywords = allKeywords.filter((k) => k.datasheet_id === ds.id);
|
|
161
|
+
const { factionKeywords, regularKeywords } = getKeywordsForFaction(dsKeywords, factionName);
|
|
162
|
+
// Points — select the correct view for shared units
|
|
163
|
+
const allDsPoints = allPoints.filter((p) => p.datasheet_id === ds.id);
|
|
164
|
+
const dsAbilityViews = splitIntoViews(allAbilities.filter((a) => a.datasheet_id === ds.id));
|
|
165
|
+
const numViews = dsAbilityViews.length;
|
|
166
|
+
const viewPoints = getPointsForView(allDsPoints, viewIdx, numViews);
|
|
167
|
+
const dsPoints = viewPoints
|
|
168
|
+
.map((p) => ({
|
|
169
|
+
models: parseInt(p.models, 10),
|
|
170
|
+
cost: parseInt(p.cost, 10),
|
|
171
|
+
}))
|
|
172
|
+
.sort((a, b) => a.models - b.models);
|
|
173
|
+
// Wargear for this unit's view
|
|
174
|
+
const dsWargear = allWargear.filter((w) => w.datasheet_id === ds.id);
|
|
175
|
+
const viewWargear = getViewEntries(dsWargear, viewIdx);
|
|
176
|
+
unitWargearMap.set(ds.id, viewWargear);
|
|
177
|
+
// Build stat profiles
|
|
178
|
+
const profiles = viewModels.map((m) => {
|
|
179
|
+
const profile = {
|
|
180
|
+
name: m.name,
|
|
181
|
+
M: parseMove(m.M),
|
|
182
|
+
T: parseIntStat(m.T),
|
|
183
|
+
W: parseIntStat(m.W),
|
|
184
|
+
Sv: parseTargetNumber(m.Sv),
|
|
185
|
+
invuln_sv: parseInvuln(m.inv_sv),
|
|
186
|
+
Ld: parseTargetNumber(m.Ld),
|
|
187
|
+
OC: parseIntStat(m.OC),
|
|
188
|
+
};
|
|
189
|
+
return profile;
|
|
190
|
+
});
|
|
191
|
+
const role = deriveRole(regularKeywords, viewAbilities, ds.name);
|
|
192
|
+
const baseSize = parseBaseSize(viewModels[0]?.base_size ?? "");
|
|
193
|
+
const transport = parseTransport(ds.transport);
|
|
194
|
+
const modelMin = dsPoints.length > 0 ? dsPoints[0].models : 1;
|
|
195
|
+
const modelMax = dsPoints.length > 0 ? dsPoints[dsPoints.length - 1].models : 1;
|
|
196
|
+
const unitId = nameToId(ds.name);
|
|
197
|
+
const unit = {
|
|
198
|
+
id: unitId,
|
|
199
|
+
name: ds.name,
|
|
200
|
+
faction_id: factionId,
|
|
201
|
+
...(role ? { role } : {}),
|
|
202
|
+
profiles,
|
|
203
|
+
points: dsPoints,
|
|
204
|
+
keywords: regularKeywords,
|
|
205
|
+
faction_keywords: factionKeywords,
|
|
206
|
+
...(baseSize ? { base_size_mm: baseSize } : {}),
|
|
207
|
+
model_count: { min: modelMin, max: modelMax },
|
|
208
|
+
weapon_ids: [],
|
|
209
|
+
ability_ids: [],
|
|
210
|
+
...(transport ? { transport_capacity: transport } : {}),
|
|
211
|
+
game_version: GAME_VERSION,
|
|
212
|
+
is_legend: false,
|
|
213
|
+
};
|
|
214
|
+
units.push(unit);
|
|
215
|
+
}
|
|
216
|
+
// ─── Build weapons ───
|
|
217
|
+
console.log("Converting weapons...");
|
|
218
|
+
const { weapons, unitWeaponIds } = buildWeaponRegistry(unitWargearMap, GAME_VERSION);
|
|
219
|
+
// Wire weapon_ids into units
|
|
220
|
+
for (const unit of units) {
|
|
221
|
+
const dsId = factionDatasheets.find((d) => d.name === unit.name).id;
|
|
222
|
+
const weaponIds = unitWeaponIds.get(dsId);
|
|
223
|
+
if (weaponIds) {
|
|
224
|
+
unit.weapon_ids = [...weaponIds].sort();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// ─── Build leader attachments ───
|
|
228
|
+
console.log("Converting leader attachments...");
|
|
229
|
+
const leaderMap = new Map();
|
|
230
|
+
for (const l of allLeaders) {
|
|
231
|
+
if (factionIds.has(l.leader_id) && factionIds.has(l.attached_id)) {
|
|
232
|
+
const leaderId = nameToId(idToName.get(l.leader_id));
|
|
233
|
+
const attachedId = nameToId(idToName.get(l.attached_id));
|
|
234
|
+
if (!leaderMap.has(leaderId)) {
|
|
235
|
+
leaderMap.set(leaderId, new Set());
|
|
236
|
+
}
|
|
237
|
+
leaderMap.get(leaderId).add(attachedId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const leaderAttachments = [...leaderMap.entries()].map(([leaderId, bodyguards]) => ({
|
|
241
|
+
leader_id: leaderId,
|
|
242
|
+
eligible_bodyguard_ids: [...bodyguards].sort(),
|
|
243
|
+
max_leaders_per_unit: 1,
|
|
244
|
+
game_version: GAME_VERSION,
|
|
245
|
+
}));
|
|
246
|
+
// ─── Build detachments ───
|
|
247
|
+
console.log("Converting detachments...");
|
|
248
|
+
const factionDetAbilities = detachmentAbilities.filter((d) => d.faction_id === sourceFactionId);
|
|
249
|
+
let factionEnhancements = enhancements.filter((e) => e.faction_id === sourceFactionId);
|
|
250
|
+
let factionStratagems = stratagems.filter((s) => s.faction_id === sourceFactionId);
|
|
251
|
+
// Deduplicate detachments (some have multiple ability entries per detachment)
|
|
252
|
+
let detachmentNames = [...new Set(factionDetAbilities.map((da) => da.detachment))];
|
|
253
|
+
// Apply detachment filter for subfactions
|
|
254
|
+
if (config.detachmentFilter) {
|
|
255
|
+
detachmentNames = detachmentNames.filter((d) => config.detachmentFilter.includes(d));
|
|
256
|
+
// Also filter enhancements and stratagems to matching detachments
|
|
257
|
+
const allowedDetachments = new Set(config.detachmentFilter);
|
|
258
|
+
factionEnhancements = factionEnhancements.filter((e) => allowedDetachments.has(e.detachment));
|
|
259
|
+
factionStratagems = factionStratagems.filter((s) => allowedDetachments.has(s.detachment));
|
|
260
|
+
}
|
|
261
|
+
const detachments = detachmentNames.map((detName) => {
|
|
262
|
+
const detId = nameToId(detName);
|
|
263
|
+
const detEnhIds = factionEnhancements
|
|
264
|
+
.filter((e) => e.detachment === detName)
|
|
265
|
+
.map((e) => nameToId(e.name));
|
|
266
|
+
const detStratIds = factionStratagems
|
|
267
|
+
.filter((s) => s.detachment === detName)
|
|
268
|
+
.map((s) => nameToId(s.name));
|
|
269
|
+
return {
|
|
270
|
+
id: detId,
|
|
271
|
+
name: detName,
|
|
272
|
+
faction_id: factionId,
|
|
273
|
+
detachment_rule_id: null,
|
|
274
|
+
enhancement_ids: detEnhIds,
|
|
275
|
+
stratagem_ids: detStratIds,
|
|
276
|
+
game_version: GAME_VERSION,
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
// ─── Build enhancements ───
|
|
280
|
+
console.log("Converting enhancements...");
|
|
281
|
+
const enhancementEntities = factionEnhancements.map((e) => ({
|
|
282
|
+
id: nameToId(e.name),
|
|
283
|
+
name: e.name,
|
|
284
|
+
detachment_id: nameToId(e.detachment),
|
|
285
|
+
cost: parseInt(e.cost, 10),
|
|
286
|
+
keyword_restrictions: [factionName],
|
|
287
|
+
ability_id: null,
|
|
288
|
+
is_unique: true,
|
|
289
|
+
game_version: GAME_VERSION,
|
|
290
|
+
}));
|
|
291
|
+
// ─── Build stratagems ───
|
|
292
|
+
console.log("Converting stratagems...");
|
|
293
|
+
const stratagemEntities = factionStratagems.map((s) => {
|
|
294
|
+
const { type } = parseStratagemType(s.type);
|
|
295
|
+
const phases = mapPhases(s.phases);
|
|
296
|
+
const playerTurn = parsePlayerTurn(s.turn);
|
|
297
|
+
return {
|
|
298
|
+
id: nameToId(s.name),
|
|
299
|
+
name: s.name,
|
|
300
|
+
category: "detachment",
|
|
301
|
+
type,
|
|
302
|
+
detachment_id: nameToId(s.detachment),
|
|
303
|
+
cp_cost: parseInt(s.cp_cost, 10),
|
|
304
|
+
phases,
|
|
305
|
+
player_turn: playerTurn,
|
|
306
|
+
timing: "once-per-phase",
|
|
307
|
+
target_restrictions: null,
|
|
308
|
+
ability_id: null,
|
|
309
|
+
game_version: GAME_VERSION,
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
// ─── Build phase mappings from source ability phases ───
|
|
313
|
+
console.log("Converting phase mappings...");
|
|
314
|
+
const phaseMappings = [];
|
|
315
|
+
// Unit abilities
|
|
316
|
+
for (const ds of factionDatasheets) {
|
|
317
|
+
const viewIdx = viewIndices.get(ds.id);
|
|
318
|
+
const dsAbilities = allAbilities.filter((a) => a.datasheet_id === ds.id);
|
|
319
|
+
const viewAbilities = getViewEntries(dsAbilities, viewIdx);
|
|
320
|
+
const seen = new Set();
|
|
321
|
+
for (const a of viewAbilities) {
|
|
322
|
+
if (a.type === "Faction")
|
|
323
|
+
continue;
|
|
324
|
+
const sourceId = nameToId(a.name);
|
|
325
|
+
if (seen.has(sourceId))
|
|
326
|
+
continue;
|
|
327
|
+
seen.add(sourceId);
|
|
328
|
+
const sourceType = a.type === "Core" ? "ability" :
|
|
329
|
+
a.type === "Wargear" ? "ability" :
|
|
330
|
+
"ability";
|
|
331
|
+
const phases = mapPhases(a.phases);
|
|
332
|
+
if (phases.length > 0) {
|
|
333
|
+
phaseMappings.push({
|
|
334
|
+
source_id: sourceId,
|
|
335
|
+
source_type: sourceType,
|
|
336
|
+
phases,
|
|
337
|
+
game_version: GAME_VERSION,
|
|
338
|
+
authored_by: "40kdc-community",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Stratagem phase mappings
|
|
344
|
+
for (const s of factionStratagems) {
|
|
345
|
+
const phases = mapPhases(s.phases);
|
|
346
|
+
if (phases.length > 0) {
|
|
347
|
+
phaseMappings.push({
|
|
348
|
+
source_id: nameToId(s.name),
|
|
349
|
+
source_type: "stratagem",
|
|
350
|
+
phases,
|
|
351
|
+
game_version: GAME_VERSION,
|
|
352
|
+
authored_by: "40kdc-community",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Enhancement phase mappings
|
|
357
|
+
for (const e of factionEnhancements) {
|
|
358
|
+
const phases = mapPhases(e.phases);
|
|
359
|
+
if (phases.length > 0) {
|
|
360
|
+
phaseMappings.push({
|
|
361
|
+
source_id: nameToId(e.name),
|
|
362
|
+
source_type: "enhancement",
|
|
363
|
+
phases,
|
|
364
|
+
game_version: GAME_VERSION,
|
|
365
|
+
authored_by: "40kdc-community",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Detachment rule phase mappings
|
|
370
|
+
for (const da of factionDetAbilities) {
|
|
371
|
+
const phases = mapPhases(da.phases);
|
|
372
|
+
if (phases.length > 0) {
|
|
373
|
+
phaseMappings.push({
|
|
374
|
+
source_id: nameToId(da.name),
|
|
375
|
+
source_type: "detachment-rule",
|
|
376
|
+
phases,
|
|
377
|
+
game_version: GAME_VERSION,
|
|
378
|
+
authored_by: "40kdc-community",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Deduplicate phase mappings
|
|
383
|
+
const dedupedPhaseMappings = [
|
|
384
|
+
...new Map(phaseMappings.map((pm) => [
|
|
385
|
+
`${pm.source_id}|${pm.source_type}`,
|
|
386
|
+
pm,
|
|
387
|
+
])).values(),
|
|
388
|
+
];
|
|
389
|
+
// ─── Build unit compositions ───
|
|
390
|
+
console.log("Generating unit compositions...");
|
|
391
|
+
const unitCompositions = units.map((u) => {
|
|
392
|
+
const unitId = u.id;
|
|
393
|
+
const modelCount = u.model_count;
|
|
394
|
+
const override = config.compositionOverrides[unitId];
|
|
395
|
+
if (override) {
|
|
396
|
+
return {
|
|
397
|
+
unit_id: unitId,
|
|
398
|
+
models: override,
|
|
399
|
+
game_version: GAME_VERSION,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// Single-model unit (vehicles, characters, monsters)
|
|
403
|
+
const profileName = u.profiles[0]?.name;
|
|
404
|
+
return {
|
|
405
|
+
unit_id: unitId,
|
|
406
|
+
models: [
|
|
407
|
+
{
|
|
408
|
+
name: profileName || u.name,
|
|
409
|
+
min: modelCount.min,
|
|
410
|
+
max: modelCount.max,
|
|
411
|
+
is_leader_model: false,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
game_version: GAME_VERSION,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
// ─── Generate factions.json ───
|
|
418
|
+
const factionEntity = [
|
|
419
|
+
{
|
|
420
|
+
id: factionId,
|
|
421
|
+
name: factionName,
|
|
422
|
+
parent_faction_id: config.parentFactionId,
|
|
423
|
+
game_version: GAME_VERSION,
|
|
424
|
+
keywords: config.factionKeywords,
|
|
425
|
+
aliases: config.aliases,
|
|
426
|
+
faction_rule_id: config.factionRuleId,
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
// ─── Write output ──────────────────────────────────────────────────
|
|
430
|
+
const coreDir = `data/core/${factionId}`;
|
|
431
|
+
const enrichDir = `data/enrichment/${factionId}`;
|
|
432
|
+
mkdirSync(resolve(ROOT, coreDir), { recursive: true });
|
|
433
|
+
mkdirSync(resolve(ROOT, enrichDir), { recursive: true });
|
|
434
|
+
console.log("\nWriting output files...");
|
|
435
|
+
writeOutput(`${coreDir}/factions.json`, factionEntity);
|
|
436
|
+
if (!config.skipUnits) {
|
|
437
|
+
writeOutput(`${coreDir}/units.json`, units);
|
|
438
|
+
writeOutput(`${coreDir}/weapons.json`, weapons);
|
|
439
|
+
writeOutput(`${coreDir}/leader-attachments.json`, leaderAttachments);
|
|
440
|
+
writeOutput(`${coreDir}/unit-compositions.json`, unitCompositions);
|
|
441
|
+
}
|
|
442
|
+
writeOutput(`${coreDir}/detachments.json`, detachments);
|
|
443
|
+
writeOutput(`${coreDir}/enhancements.json`, enhancementEntities);
|
|
444
|
+
writeOutput(`${coreDir}/stratagems.json`, stratagemEntities);
|
|
445
|
+
if (!config.skipUnits) {
|
|
446
|
+
writeOutput(`${enrichDir}/phase-mappings.json`, dedupedPhaseMappings);
|
|
447
|
+
}
|
|
448
|
+
// ─── Summary ───
|
|
449
|
+
console.log(`\n── ${factionName} Summary ──`);
|
|
450
|
+
if (!config.skipUnits) {
|
|
451
|
+
console.log(` Units: ${units.length}`);
|
|
452
|
+
console.log(` Weapons: ${weapons.length}`);
|
|
453
|
+
console.log(` Leader attachments: ${leaderAttachments.length}`);
|
|
454
|
+
console.log(` Unit compositions: ${unitCompositions.length}`);
|
|
455
|
+
}
|
|
456
|
+
console.log(` Detachments: ${detachments.length}`);
|
|
457
|
+
console.log(` Enhancements: ${enhancementEntities.length}`);
|
|
458
|
+
console.log(` Stratagems: ${stratagemEntities.length}`);
|
|
459
|
+
if (!config.skipUnits) {
|
|
460
|
+
console.log(` Phase mappings: ${dedupedPhaseMappings.length}`);
|
|
461
|
+
}
|
|
462
|
+
console.log("\nDone. Run 'npm run validate' to check output.");
|
|
463
|
+
}
|
|
464
|
+
// ─── CLI entry point ─────────────────────────────────────────────────
|
|
465
|
+
// Only run CLI when this module is the entry point (not when imported)
|
|
466
|
+
const isMain = process.argv[1] &&
|
|
467
|
+
resolve(process.argv[1]).replace(/\.\w+$/, "") ===
|
|
468
|
+
fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
|
|
469
|
+
if (isMain) {
|
|
470
|
+
const args = process.argv.slice(2);
|
|
471
|
+
if (args.length === 0 || args[0] === "--help") {
|
|
472
|
+
console.log("Usage: npx tsx tools/src/convert-faction.ts <faction-id>");
|
|
473
|
+
console.log(`Available factions: ${listFactions().join(", ")}`);
|
|
474
|
+
process.exit(args[0] === "--help" ? 0 : 1);
|
|
475
|
+
}
|
|
476
|
+
const factionIdArg = args[0];
|
|
477
|
+
const factionConfig = getFactionConfig(factionIdArg);
|
|
478
|
+
convertFaction(factionConfig);
|
|
479
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { registerFaction } from "../faction-config.js";
|
|
2
|
+
const adeptaSororitas = {
|
|
3
|
+
sourceFactionId: "AS",
|
|
4
|
+
factionId: "adepta-sororitas",
|
|
5
|
+
factionName: "Adepta Sororitas",
|
|
6
|
+
factionAbilityName: "Acts of Faith",
|
|
7
|
+
factionRuleId: "acts-of-faith",
|
|
8
|
+
factionKeywords: ["Imperium", "Adepta Sororitas"],
|
|
9
|
+
parentFactionId: null,
|
|
10
|
+
aliases: [],
|
|
11
|
+
compositionOverrides: {
|
|
12
|
+
"battle-sisters-squad": [
|
|
13
|
+
{ name: "Sister Superior", profile_name: "Battle Sister", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "power-weapon"], is_leader_model: true },
|
|
14
|
+
{ name: "Battle Sister", min: 9, max: 9, default_weapon_ids: ["boltgun", "close-combat-weapon"], is_leader_model: false },
|
|
15
|
+
],
|
|
16
|
+
"celestian-sacresants": [
|
|
17
|
+
{ name: "Sacresant Superior", profile_name: "Celestian Sacresant", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "hallowed-mace"], is_leader_model: true },
|
|
18
|
+
{ name: "Celestian Sacresant", min: 4, max: 9, default_weapon_ids: ["bolt-pistol", "hallowed-mace"], is_leader_model: false },
|
|
19
|
+
],
|
|
20
|
+
"celestian-insidiants": [
|
|
21
|
+
{ name: "Insidiant Superior", profile_name: "Celestian Insidiant", min: 1, max: 1, default_weapon_ids: ["condemnor-bolt-pistol", "blessed-sword"], is_leader_model: true },
|
|
22
|
+
{ name: "Celestian Insidiant", min: 9, max: 9, default_weapon_ids: ["condemnor-bolt-pistol", "null-mace"], is_leader_model: false },
|
|
23
|
+
],
|
|
24
|
+
"dominion-squad": [
|
|
25
|
+
{ name: "Dominion Superior", profile_name: "Dominion", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "power-weapon"], is_leader_model: true },
|
|
26
|
+
{ name: "Dominion", min: 9, max: 9, default_weapon_ids: ["boltgun", "close-combat-weapon"], is_leader_model: false },
|
|
27
|
+
],
|
|
28
|
+
"seraphim-squad": [
|
|
29
|
+
{ name: "Seraphim Superior", profile_name: "Seraphim", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "chainsword"], is_leader_model: true },
|
|
30
|
+
{ name: "Seraphim", min: 4, max: 9, default_weapon_ids: ["bolt-pistol", "close-combat-weapon"], is_leader_model: false },
|
|
31
|
+
],
|
|
32
|
+
"zephyrim-squad": [
|
|
33
|
+
{ name: "Zephyrim Superior", profile_name: "Zephyrim", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "power-weapon"], is_leader_model: true },
|
|
34
|
+
{ name: "Zephyrim", min: 4, max: 9, default_weapon_ids: ["bolt-pistol", "power-weapon"], is_leader_model: false },
|
|
35
|
+
],
|
|
36
|
+
"retributor-squad": [
|
|
37
|
+
{ name: "Retributor Superior", profile_name: "Retributor", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "power-weapon"], is_leader_model: true },
|
|
38
|
+
{ name: "Retributor", min: 4, max: 4, default_weapon_ids: ["boltgun", "close-combat-weapon"], is_leader_model: false },
|
|
39
|
+
],
|
|
40
|
+
"repentia-squad": [
|
|
41
|
+
{ name: "Repentia Superior", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "neural-whips"], is_leader_model: true },
|
|
42
|
+
{ name: "Sister Repentia", min: 4, max: 9, default_weapon_ids: ["penitent-eviscerator"], is_leader_model: false },
|
|
43
|
+
],
|
|
44
|
+
"sisters-novitiate-squad": [
|
|
45
|
+
{ name: "Novitiate Superior", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "power-weapon"], is_leader_model: true },
|
|
46
|
+
{ name: "Sister Novitiate", min: 9, max: 9, default_weapon_ids: ["autogun", "close-combat-weapon"], is_leader_model: false },
|
|
47
|
+
],
|
|
48
|
+
"sanctifiers": [
|
|
49
|
+
{ name: "Missionary", profile_name: "Missionary", min: 1, max: 1, default_weapon_ids: ["holy-fire", "close-combat-weapon"], is_leader_model: true },
|
|
50
|
+
{ name: "Miraculist", min: 2, max: 2, default_weapon_ids: ["ministorum-hand-flamer", "close-combat-weapon"], is_leader_model: false },
|
|
51
|
+
{ name: "Salvationist", min: 2, max: 2, default_weapon_ids: ["ministorum-flamer", "close-combat-weapon"], is_leader_model: false },
|
|
52
|
+
{ name: "Death Cult Assassin", min: 2, max: 2, default_weapon_ids: ["death-cult-blades"], is_leader_model: false },
|
|
53
|
+
{ name: "Sanctifier", min: 2, max: 2, default_weapon_ids: ["sanctifier-melee-weapon"], is_leader_model: false },
|
|
54
|
+
],
|
|
55
|
+
"saint-celestine": [
|
|
56
|
+
{ name: "Celestine", min: 1, max: 1, default_weapon_ids: ["the-ardent-blade-strike"], is_leader_model: true },
|
|
57
|
+
{ name: "Geminae Superia", min: 2, max: 2, default_weapon_ids: ["bolt-pistol", "power-weapon"], is_leader_model: false },
|
|
58
|
+
],
|
|
59
|
+
"aestred-thurga-and-agathae-dolan": [
|
|
60
|
+
{ name: "Aestred Thurga", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "auto-of-castigation"], is_leader_model: true },
|
|
61
|
+
{ name: "Agathae Dolan", min: 1, max: 1, default_weapon_ids: ["close-combat-weapon"], is_leader_model: false },
|
|
62
|
+
],
|
|
63
|
+
"daemonifuge": [
|
|
64
|
+
{ name: "Ephrael Stern", min: 1, max: 1, default_weapon_ids: ["bolt-pistol", "sanctity"], is_leader_model: true },
|
|
65
|
+
{ name: "Kyganil of the Bloody Tears", profile_name: "Kyganil of the Bloody Tears", min: 1, max: 1, default_weapon_ids: ["the-blades-of-the-harlequin"], is_leader_model: false },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
registerFaction(adeptaSororitas);
|
|
70
|
+
export default adeptaSororitas;
|