@alpaca-software/40kdc-data 0.3.1 → 0.4.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 +3 -3
- package/dist/bundle-schemas.d.ts.map +1 -1
- package/dist/bundle-schemas.js +17 -0
- package/dist/bundle-schemas.js.map +1 -1
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/codegen-data.js +2 -1
- package/dist/codegen-data.js.map +1 -1
- package/dist/commands/populate-base-sizes.d.ts +2 -0
- package/dist/commands/populate-base-sizes.d.ts.map +1 -0
- package/dist/commands/populate-base-sizes.js +158 -0
- package/dist/commands/populate-base-sizes.js.map +1 -0
- package/dist/convert-faction.d.ts +3 -1
- package/dist/convert-faction.d.ts.map +1 -1
- package/dist/convert-faction.js +49 -16
- package/dist/convert-faction.js.map +1 -1
- package/dist/converters/base-size-bridge.d.ts +122 -0
- package/dist/converters/base-size-bridge.d.ts.map +1 -0
- package/dist/converters/base-size-bridge.js +198 -0
- package/dist/converters/base-size-bridge.js.map +1 -0
- package/dist/converters/base-size-guide-extract.d.ts +11 -0
- package/dist/converters/base-size-guide-extract.d.ts.map +1 -0
- package/dist/converters/base-size-guide-extract.js +59 -0
- package/dist/converters/base-size-guide-extract.js.map +1 -0
- package/dist/converters/option-bridge.d.ts +36 -0
- package/dist/converters/option-bridge.d.ts.map +1 -0
- package/dist/converters/option-bridge.js +72 -0
- package/dist/converters/option-bridge.js.map +1 -0
- package/dist/converters/option-parser.d.ts +56 -0
- package/dist/converters/option-parser.d.ts.map +1 -0
- package/dist/converters/option-parser.js +209 -0
- package/dist/converters/option-parser.js.map +1 -0
- package/dist/converters/wargear-options.d.ts +55 -0
- package/dist/converters/wargear-options.d.ts.map +1 -0
- package/dist/converters/wargear-options.js +187 -0
- package/dist/converters/wargear-options.js.map +1 -0
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/dataset.d.ts +10 -2
- package/dist/data/dataset.d.ts.map +1 -1
- package/dist/data/dataset.js +16 -2
- package/dist/data/dataset.js.map +1 -1
- package/dist/data/entities.d.ts +3 -1
- package/dist/data/entities.d.ts.map +1 -1
- package/dist/data/entities.js +4 -0
- package/dist/data/entities.js.map +1 -1
- package/dist/data/index.d.ts +5 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +5 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/loadout.d.ts +60 -0
- package/dist/data/loadout.d.ts.map +1 -0
- package/dist/data/loadout.js +135 -0
- package/dist/data/loadout.js.map +1 -0
- package/dist/data/types.d.ts +4 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/data/types.js +2 -1
- package/dist/data/types.js.map +1 -1
- package/dist/export/rosterizer.js +1 -1
- package/dist/export/rosterizer.js.map +1 -1
- package/dist/gen-conformance.js +25 -2
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +112 -55
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/import/rosterizer.d.ts +1 -1
- package/dist/import/rosterizer.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +55 -1
- package/dist/runner.js.map +1 -1
- package/dist/scoring/index.d.ts +135 -0
- package/dist/scoring/index.d.ts.map +1 -0
- package/dist/scoring/index.js +195 -0
- package/dist/scoring/index.js.map +1 -0
- package/dist/translate/condition.d.ts.map +1 -1
- package/dist/translate/condition.js +4 -0
- package/dist/translate/condition.js.map +1 -1
- package/dist/translate/index.d.ts +1 -1
- package/dist/translate/index.d.ts.map +1 -1
- package/dist/translate/index.js.map +1 -1
- package/dist/translate/scoring.d.ts +6 -0
- package/dist/translate/scoring.d.ts.map +1 -1
- package/dist/translate/scoring.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +13 -5
- package/dist/validate.js.map +1 -1
- package/package.json +4 -4
- package/schemas/$defs/common.schema.json +14 -0
- package/schemas/core/secondary-card.schema.json +10 -0
- package/schemas/core/unit-composition.schema.json +5 -1
- package/schemas/core/unit.schema.json +2 -10
- package/schemas/core/wargear-option.schema.json +32 -6
- package/schemas/core/wargear.schema.json +24 -0
- package/schemas/enrichment/ability-dsl/condition.schema.json +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Published by [Alpaca Software](https://alpacasoft.dev).
|
|
4
4
|
|
|
5
|
-
The [40kdc](https://
|
|
5
|
+
The [40kdc](https://40kdc.alpacasoft.dev) Warhammer 40,000
|
|
6
6
|
dataset behind a **linked, typed API**. Find a unit, then walk straight to its
|
|
7
7
|
weapons, abilities, the game phases those abilities act in, and its faction —
|
|
8
8
|
all strongly typed, all resolved for you.
|
|
@@ -71,8 +71,8 @@ data against them. See the repository root for schema details.
|
|
|
71
71
|
|
|
72
72
|
- Code (`tools/`): **MIT**.
|
|
73
73
|
- Embedded enrichment data (`data/enrichment/`): **CC BY 4.0** —
|
|
74
|
-
attribution: *40kdc community contributors*
|
|
75
|
-
(<https://github.com/
|
|
74
|
+
attribution: *Alpaca Software and the 40kdc community contributors*
|
|
75
|
+
(<https://github.com/wn-mitch/40kdc-data>).
|
|
76
76
|
- JSON Schemas: **CC0**.
|
|
77
77
|
|
|
78
78
|
Stat lines and points are numerical facts. Ability and rules text are never
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-schemas.d.ts","sourceRoot":"","sources":["../src/bundle-schemas.ts"],"names":[],"mappings":"AA2CA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"bundle-schemas.d.ts","sourceRoot":"","sources":["../src/bundle-schemas.ts"],"names":[],"mappings":"AA2CA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA+F1C,wBAAgB,MAAM,IAAI,UAAU,CAsCnC"}
|
package/dist/bundle-schemas.js
CHANGED
|
@@ -78,6 +78,23 @@ function stripConditionals(node) {
|
|
|
78
78
|
continue;
|
|
79
79
|
out[key] = stripConditionals(value);
|
|
80
80
|
}
|
|
81
|
+
// Stripping `if`/`then`/`else` can empty out `allOf` members that existed
|
|
82
|
+
// only to express a conditional (e.g. exactly-one-of cross-field rules). An
|
|
83
|
+
// empty subschema is a no-op constraint, so drop those members; if none
|
|
84
|
+
// remain, drop the `allOf` entirely. Without this the bundle keeps
|
|
85
|
+
// `allOf: [{}, {}]`, which makes typify/json2ts emit a degenerate type and
|
|
86
|
+
// lose the whole entity. The real constraint is still enforced by ajv
|
|
87
|
+
// against the un-stripped source schemas.
|
|
88
|
+
if (Array.isArray(out.allOf)) {
|
|
89
|
+
const kept = out.allOf.filter((m) => !(m &&
|
|
90
|
+
typeof m === "object" &&
|
|
91
|
+
!Array.isArray(m) &&
|
|
92
|
+
Object.keys(m).length === 0));
|
|
93
|
+
if (kept.length === 0)
|
|
94
|
+
delete out.allOf;
|
|
95
|
+
else
|
|
96
|
+
out.allOf = kept;
|
|
97
|
+
}
|
|
81
98
|
return out;
|
|
82
99
|
}
|
|
83
100
|
return node;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-schemas.js","sourceRoot":"","sources":["../src/bundle-schemas.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { findSchemaFiles, SCHEMAS_ROOT } from \"./schema-loader.js\";\n\n/**\n * Flattens the multi-file schema set into a single self-contained\n * draft-2020-12 document, written to crates/wh40kdc/schemas/bundled.schema.json.\n *\n * Why a bespoke flattener rather than a ref-parser bundle: the Rust codegen\n * (typify) wants one document where every type lives in a single flat `$defs`\n * map — it resolves `$ref`s as `#/$defs/<name>` and does NOT traverse nested\n * `$defs` paths. Generic bundlers also anchor a reused subschema to its first-use\n * location (e.g. `#/$defs/faction/properties/id`), yielding junk Rust type names.\n *\n * So this pass hoists EVERY definition — `common.schema.json`'s shared defs, each\n * entity schema (keyed by its filename stem), and every entity's local `$defs` —\n * flat into one top-level `$defs`. All such names are globally unique across the\n * schema set (asserted at build time), so no prefixing is needed and type names\n * track the names authors actually chose.\n *\n * Refs are resolved against each schema's `$id` URL — not its filesystem path —\n * because that is how the refs are authored (e.g. `../defs/common.schema.json`\n * targets the `$id` `.../schemas/defs/...`, while the file lives in `$defs/`).\n */\n\nconst COMMON_ID = \"https://40kdc.dev/schemas/defs/common.schema.json\";\n\n/**\n * Schemas excluded from the codegen bundle (still loaded for AJV validation).\n * The roster schema describes importer *output* — a tool-side artifact, not a\n * dataset entity the Rust crate serves — so it is intentionally kept out of the\n * generated types. Its TS types are hand-authored in `src/import/types.ts`.\n */\nconst CODEGEN_EXCLUDED_IDS = new Set([\n \"https://40kdc.dev/schemas/core/roster.schema.json\",\n]);\nconst OUTPUT_PATH = resolve(\n SCHEMAS_ROOT,\n \"../crates/wh40kdc/schemas/bundled.schema.json\",\n);\nconst BUNDLE_ID = \"https://40kdc.dev/schemas/bundled.schema.json\";\n\ntype JsonObject = Record<string, unknown>;\n\nfunction stemOfId(id: string): string {\n return basename(new URL(id).pathname).replace(/\\.schema\\.json$/, \"\");\n}\n\n/**\n * Rewrite a single `$ref` (resolved against the `$id` of the file it appears in)\n * to a flat pointer into the bundle's top-level `$defs`.\n *\n * - any `#/$defs/<name>` pointer (file-local, common, or cross-file into a hoisted\n * local def) stays `#/$defs/<name>` — every such name is now top-level.\n * - a whole-file ref (`effect.schema.json`) maps to that file's stem: `#/$defs/effect`.\n */\nfunction rewriteRef(ref: string, sourceId: string): string {\n const hashIndex = ref.indexOf(\"#\");\n const filePart = hashIndex === -1 ? ref : ref.slice(0, hashIndex);\n const pointer = hashIndex === -1 ? \"\" : ref.slice(hashIndex + 1); // e.g. \"/$defs/entity-id\"\n\n if (pointer) {\n if (!pointer.startsWith(\"/$defs/\")) {\n throw new Error(\n `unexpected non-$defs JSON pointer in $ref ${JSON.stringify(ref)} (source ${sourceId})`,\n );\n }\n return `#${pointer}`;\n }\n\n // Whole-file ref: resolve to the target's $id and key by its stem.\n const targetId = filePart ? new URL(filePart, sourceId).href : sourceId;\n return `#/$defs/${stemOfId(targetId)}`;\n}\n\n/**\n * Strip JSON Schema conditional applicators (`if`/`then`/`else`) from the codegen\n * bundle. typify cannot model them — they express \"field X is required when field\n * Y has value Z\", which has no Rust-type representation. The constraints are still\n * enforced at data-validation time by ajv against the real (un-stripped) schemas;\n * dropping them here only makes the affected fields optional in the generated Rust\n * types, which is correct for deserializing any valid document.\n */\nfunction stripConditionals(node: unknown): unknown {\n if (Array.isArray(node)) {\n return node.map(stripConditionals);\n }\n if (node && typeof node === \"object\") {\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(node as JsonObject)) {\n if (key === \"if\" || key === \"then\" || key === \"else\") continue;\n out[key] = stripConditionals(value);\n }\n return out;\n }\n return node;\n}\n\n/** Recursively rewrite every `$ref` in `node`, knowing the source schema's `$id`. */\nfunction rewriteRefs(node: unknown, sourceId: string): unknown {\n if (Array.isArray(node)) {\n return node.map((item) => rewriteRefs(item, sourceId));\n }\n if (node && typeof node === \"object\") {\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(node as JsonObject)) {\n if (key === \"$ref\" && typeof value === \"string\") {\n out[key] = rewriteRef(value, sourceId);\n } else {\n out[key] = rewriteRefs(value, sourceId);\n }\n }\n return out;\n }\n return node;\n}\n\nexport function bundle(): JsonObject {\n const files = findSchemaFiles(SCHEMAS_ROOT).sort();\n const defs: JsonObject = {};\n\n const place = (name: string, def: unknown, sourceId: string): void => {\n if (name in defs) {\n throw new Error(`definition name collision: ${name} (from ${sourceId})`);\n }\n defs[name] = stripConditionals(rewriteRefs(def, sourceId));\n };\n\n for (const file of files) {\n const raw = JSON.parse(readFileSync(file, \"utf-8\")) as JsonObject;\n const id = raw.$id as string;\n if (!id) throw new Error(`schema missing $id: ${file}`);\n if (CODEGEN_EXCLUDED_IDS.has(id)) continue;\n const { $id: _id, $schema: _schema, $defs: localDefs, ...body } = raw;\n\n // Hoist this file's local $defs flat to the top level (names are globally\n // unique across the schema set; collisions throw above).\n for (const [name, def] of Object.entries((localDefs ?? {}) as JsonObject)) {\n place(name, def, id);\n }\n\n // common is purely a $defs bag — it contributes no stem-keyed entity.\n if (id !== COMMON_ID) {\n place(stemOfId(id), body, id);\n }\n }\n\n return {\n $schema: \"https://json-schema.org/draft/2020-12/schema\",\n $id: BUNDLE_ID,\n title: \"40kdc Bundled Schemas\",\n description:\n \"Auto-generated by tools/src/bundle-schemas.ts. Single self-contained schema for Rust codegen — do not edit by hand.\",\n $defs: defs,\n };\n}\n\nfunction main(): void {\n const result = bundle();\n mkdirSync(dirname(OUTPUT_PATH), { recursive: true });\n writeFileSync(OUTPUT_PATH, JSON.stringify(result, null, 2) + \"\\n\", \"utf-8\");\n const count = Object.keys(result.$defs as JsonObject).length;\n console.log(`Bundled ${count} definitions → ${OUTPUT_PATH}`);\n}\n\nif (import.meta.url === pathToFileURL(process.argv[1]).href) {\n main();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bundle-schemas.js","sourceRoot":"","sources":["../src/bundle-schemas.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { findSchemaFiles, SCHEMAS_ROOT } from \"./schema-loader.js\";\n\n/**\n * Flattens the multi-file schema set into a single self-contained\n * draft-2020-12 document, written to crates/wh40kdc/schemas/bundled.schema.json.\n *\n * Why a bespoke flattener rather than a ref-parser bundle: the Rust codegen\n * (typify) wants one document where every type lives in a single flat `$defs`\n * map — it resolves `$ref`s as `#/$defs/<name>` and does NOT traverse nested\n * `$defs` paths. Generic bundlers also anchor a reused subschema to its first-use\n * location (e.g. `#/$defs/faction/properties/id`), yielding junk Rust type names.\n *\n * So this pass hoists EVERY definition — `common.schema.json`'s shared defs, each\n * entity schema (keyed by its filename stem), and every entity's local `$defs` —\n * flat into one top-level `$defs`. All such names are globally unique across the\n * schema set (asserted at build time), so no prefixing is needed and type names\n * track the names authors actually chose.\n *\n * Refs are resolved against each schema's `$id` URL — not its filesystem path —\n * because that is how the refs are authored (e.g. `../defs/common.schema.json`\n * targets the `$id` `.../schemas/defs/...`, while the file lives in `$defs/`).\n */\n\nconst COMMON_ID = \"https://40kdc.dev/schemas/defs/common.schema.json\";\n\n/**\n * Schemas excluded from the codegen bundle (still loaded for AJV validation).\n * The roster schema describes importer *output* — a tool-side artifact, not a\n * dataset entity the Rust crate serves — so it is intentionally kept out of the\n * generated types. Its TS types are hand-authored in `src/import/types.ts`.\n */\nconst CODEGEN_EXCLUDED_IDS = new Set([\n \"https://40kdc.dev/schemas/core/roster.schema.json\",\n]);\nconst OUTPUT_PATH = resolve(\n SCHEMAS_ROOT,\n \"../crates/wh40kdc/schemas/bundled.schema.json\",\n);\nconst BUNDLE_ID = \"https://40kdc.dev/schemas/bundled.schema.json\";\n\ntype JsonObject = Record<string, unknown>;\n\nfunction stemOfId(id: string): string {\n return basename(new URL(id).pathname).replace(/\\.schema\\.json$/, \"\");\n}\n\n/**\n * Rewrite a single `$ref` (resolved against the `$id` of the file it appears in)\n * to a flat pointer into the bundle's top-level `$defs`.\n *\n * - any `#/$defs/<name>` pointer (file-local, common, or cross-file into a hoisted\n * local def) stays `#/$defs/<name>` — every such name is now top-level.\n * - a whole-file ref (`effect.schema.json`) maps to that file's stem: `#/$defs/effect`.\n */\nfunction rewriteRef(ref: string, sourceId: string): string {\n const hashIndex = ref.indexOf(\"#\");\n const filePart = hashIndex === -1 ? ref : ref.slice(0, hashIndex);\n const pointer = hashIndex === -1 ? \"\" : ref.slice(hashIndex + 1); // e.g. \"/$defs/entity-id\"\n\n if (pointer) {\n if (!pointer.startsWith(\"/$defs/\")) {\n throw new Error(\n `unexpected non-$defs JSON pointer in $ref ${JSON.stringify(ref)} (source ${sourceId})`,\n );\n }\n return `#${pointer}`;\n }\n\n // Whole-file ref: resolve to the target's $id and key by its stem.\n const targetId = filePart ? new URL(filePart, sourceId).href : sourceId;\n return `#/$defs/${stemOfId(targetId)}`;\n}\n\n/**\n * Strip JSON Schema conditional applicators (`if`/`then`/`else`) from the codegen\n * bundle. typify cannot model them — they express \"field X is required when field\n * Y has value Z\", which has no Rust-type representation. The constraints are still\n * enforced at data-validation time by ajv against the real (un-stripped) schemas;\n * dropping them here only makes the affected fields optional in the generated Rust\n * types, which is correct for deserializing any valid document.\n */\nfunction stripConditionals(node: unknown): unknown {\n if (Array.isArray(node)) {\n return node.map(stripConditionals);\n }\n if (node && typeof node === \"object\") {\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(node as JsonObject)) {\n if (key === \"if\" || key === \"then\" || key === \"else\") continue;\n out[key] = stripConditionals(value);\n }\n // Stripping `if`/`then`/`else` can empty out `allOf` members that existed\n // only to express a conditional (e.g. exactly-one-of cross-field rules). An\n // empty subschema is a no-op constraint, so drop those members; if none\n // remain, drop the `allOf` entirely. Without this the bundle keeps\n // `allOf: [{}, {}]`, which makes typify/json2ts emit a degenerate type and\n // lose the whole entity. The real constraint is still enforced by ajv\n // against the un-stripped source schemas.\n if (Array.isArray(out.allOf)) {\n const kept = (out.allOf as unknown[]).filter(\n (m) =>\n !(\n m &&\n typeof m === \"object\" &&\n !Array.isArray(m) &&\n Object.keys(m as JsonObject).length === 0\n ),\n );\n if (kept.length === 0) delete out.allOf;\n else out.allOf = kept;\n }\n return out;\n }\n return node;\n}\n\n/** Recursively rewrite every `$ref` in `node`, knowing the source schema's `$id`. */\nfunction rewriteRefs(node: unknown, sourceId: string): unknown {\n if (Array.isArray(node)) {\n return node.map((item) => rewriteRefs(item, sourceId));\n }\n if (node && typeof node === \"object\") {\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(node as JsonObject)) {\n if (key === \"$ref\" && typeof value === \"string\") {\n out[key] = rewriteRef(value, sourceId);\n } else {\n out[key] = rewriteRefs(value, sourceId);\n }\n }\n return out;\n }\n return node;\n}\n\nexport function bundle(): JsonObject {\n const files = findSchemaFiles(SCHEMAS_ROOT).sort();\n const defs: JsonObject = {};\n\n const place = (name: string, def: unknown, sourceId: string): void => {\n if (name in defs) {\n throw new Error(`definition name collision: ${name} (from ${sourceId})`);\n }\n defs[name] = stripConditionals(rewriteRefs(def, sourceId));\n };\n\n for (const file of files) {\n const raw = JSON.parse(readFileSync(file, \"utf-8\")) as JsonObject;\n const id = raw.$id as string;\n if (!id) throw new Error(`schema missing $id: ${file}`);\n if (CODEGEN_EXCLUDED_IDS.has(id)) continue;\n const { $id: _id, $schema: _schema, $defs: localDefs, ...body } = raw;\n\n // Hoist this file's local $defs flat to the top level (names are globally\n // unique across the schema set; collisions throw above).\n for (const [name, def] of Object.entries((localDefs ?? {}) as JsonObject)) {\n place(name, def, id);\n }\n\n // common is purely a $defs bag — it contributes no stem-keyed entity.\n if (id !== COMMON_ID) {\n place(stemOfId(id), body, id);\n }\n }\n\n return {\n $schema: \"https://json-schema.org/draft/2020-12/schema\",\n $id: BUNDLE_ID,\n title: \"40kdc Bundled Schemas\",\n description:\n \"Auto-generated by tools/src/bundle-schemas.ts. Single self-contained schema for Rust codegen — do not edit by hand.\",\n $defs: defs,\n };\n}\n\nfunction main(): void {\n const result = bundle();\n mkdirSync(dirname(OUTPUT_PATH), { recursive: true });\n writeFileSync(OUTPUT_PATH, JSON.stringify(result, null, 2) + \"\\n\", \"utf-8\");\n const count = Object.keys(result.$defs as JsonObject).length;\n console.log(`Bundled ${count} definitions → ${OUTPUT_PATH}`);\n}\n\nif (import.meta.url === pathToFileURL(process.argv[1]).href) {\n main();\n}\n"]}
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { validateAllCommand } from "./commands/validate-all.js";
|
|
|
6
6
|
import { translateCommand } from "./commands/translate.js";
|
|
7
7
|
import { importCommand } from "./commands/import.js";
|
|
8
8
|
import { auditCoverageCommand } from "./audit-coverage.js";
|
|
9
|
+
import { populateBaseSizesCommand } from "./commands/populate-base-sizes.js";
|
|
9
10
|
const program = new Command();
|
|
10
11
|
program
|
|
11
12
|
.name("40kdc-validate")
|
|
@@ -44,5 +45,9 @@ program
|
|
|
44
45
|
.option("--reporter <mode>", "Output format: json or pretty", "json")
|
|
45
46
|
.option("--out <file>", "Write roster JSON to a file instead of stdout")
|
|
46
47
|
.action(importCommand);
|
|
48
|
+
program
|
|
49
|
+
.command("populate-base-sizes")
|
|
50
|
+
.description("Populate base_size_mm on units + composition models from the GW base-size guide (+ bevy fallback)")
|
|
51
|
+
.action(populateBaseSizesCommand);
|
|
47
52
|
program.parse();
|
|
48
53
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAE7E,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,gBAAgB,CAAC;KACtB,WAAW,CAAC,2CAA2C,CAAC;KACxD,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,eAAe,CAAC;KACxB,WAAW,CAAC,0BAA0B,CAAC;KACvC,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAE/B,OAAO;KACJ,OAAO,CAAC,qBAAqB,CAAC;KAC9B,WAAW,CAAC,gCAAgC,CAAC;KAC7C,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAErC,OAAO;KACJ,OAAO,CAAC,cAAc,CAAC;KACvB,WAAW,CAAC,yBAAyB,CAAC;KACtC,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,kBAAkB,CAAC,CAAC;AAE9B,OAAO;KACJ,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,wCAAwC,CAAC;KACrD,QAAQ,CAAC,QAAQ,EAAE,6BAA6B,CAAC;KACjD,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAE5B,OAAO;KACJ,OAAO,CAAC,gBAAgB,CAAC;KACzB,WAAW,CAAC,yEAAyE,CAAC;KACtF,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,QAAQ,CAAC;KACtE,MAAM,CAAC,SAAS,EAAE,mDAAmD,EAAE,KAAK,CAAC;KAC7E,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,oBAAoB,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAE1F,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,yDAAyD,CAAC;KACtE,QAAQ,CAAC,SAAS,EAAE,wEAAwE,CAAC;KAC7F,MAAM,CAAC,mBAAmB,EAAE,+BAA+B,EAAE,MAAM,CAAC;KACpE,MAAM,CAAC,cAAc,EAAE,+CAA+C,CAAC;KACvE,MAAM,CAAC,aAAa,CAAC,CAAC;AAEzB,OAAO;KACJ,OAAO,CAAC,qBAAqB,CAAC;KAC9B,WAAW,CAAC,mGAAmG,CAAC;KAChH,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAEpC,OAAO,CAAC,KAAK,EAAE,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport { validateCoreCommand } from \"./commands/validate-core.js\";\nimport { validateEnrichmentCommand } from \"./commands/validate-enrichment.js\";\nimport { validateAllCommand } from \"./commands/validate-all.js\";\nimport { translateCommand } from \"./commands/translate.js\";\nimport { importCommand } from \"./commands/import.js\";\nimport { auditCoverageCommand } from \"./audit-coverage.js\";\nimport { populateBaseSizesCommand } from \"./commands/populate-base-sizes.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"40kdc-validate\")\n .description(\"Validate 40kdc data files against schemas\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"validate-core\")\n .description(\"Validate core data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateCoreCommand);\n\nprogram\n .command(\"validate-enrichment\")\n .description(\"Validate enrichment data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateEnrichmentCommand);\n\nprogram\n .command(\"validate-all\")\n .description(\"Validate all data files\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .action(validateAllCommand);\n\nprogram\n .command(\"translate\")\n .description(\"Translate ability DSL to plain English\")\n .argument(\"[path]\", \"Path to abilities.json file\")\n .action(translateCommand);\n\nprogram\n .command(\"audit-coverage\")\n .description(\"Audit how much ability data translates into cruncher buffs, per faction\")\n .option(\"--reporter <mode>\", \"Output format: pretty or json\", \"pretty\")\n .option(\"--write\", \"Also write data/_audit/coverage.json + summary.md\", false)\n .action((opts) => auditCoverageCommand({ reporter: opts.reporter, write: opts.write }));\n\nprogram\n .command(\"import\")\n .description(\"Import a ListForge army-list export into a 40kdc roster\")\n .argument(\"[input]\", \"ListForge URL, base64 segment, JSON, or file path (omit/'-' for stdin)\")\n .option(\"--reporter <mode>\", \"Output format: json or pretty\", \"json\")\n .option(\"--out <file>\", \"Write roster JSON to a file instead of stdout\")\n .action(importCommand);\n\nprogram\n .command(\"populate-base-sizes\")\n .description(\"Populate base_size_mm on units + composition models from the GW base-size guide (+ bevy fallback)\")\n .action(populateBaseSizesCommand);\n\nprogram.parse();\n"]}
|
package/dist/codegen-data.js
CHANGED
|
@@ -35,10 +35,11 @@ const FILE_TO_COLLECTION = {
|
|
|
35
35
|
"leader-attachments": "leaderAttachments",
|
|
36
36
|
"unit-compositions": "unitCompositions",
|
|
37
37
|
"wargear-options": "wargearOptions",
|
|
38
|
+
wargear: "wargear",
|
|
38
39
|
"game-versions": "gameVersions",
|
|
39
40
|
missions: "missions",
|
|
40
41
|
"mission-matchups": "missionMatchups",
|
|
41
|
-
"secondary-cards": "
|
|
42
|
+
"secondary-cards": "missionCards",
|
|
42
43
|
"deployment-patterns": "deploymentPatterns",
|
|
43
44
|
"force-dispositions": "forceDispositions",
|
|
44
45
|
"terrain-templates": "terrainTemplates",
|
package/dist/codegen-data.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codegen-data.js","sourceRoot":"","sources":["../src/codegen-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAgB,MAAM,iBAAiB,CAAC;AAE7D,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAC9C,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;AAC5F,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAC;AAEhE,iFAAiF;AACjF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,CAAC;AAE3D,kFAAkF;AAClF,MAAM,kBAAkB,GAAkC;IACxD,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,SAAS;IAClB,iBAAiB,EAAE,gBAAgB;IACnC,QAAQ,EAAE,UAAU;IACpB,SAAS,EAAE,WAAW;IACtB,gBAAgB,EAAE,eAAe;IACjC,WAAW,EAAE,aAAa;IAC1B,UAAU,EAAE,YAAY;IACxB,YAAY,EAAE,cAAc;IAC5B,oBAAoB,EAAE,mBAAmB;IACzC,mBAAmB,EAAE,kBAAkB;IACvC,iBAAiB,EAAE,gBAAgB;IACnC,eAAe,EAAE,cAAc;IAC/B,QAAQ,EAAE,UAAU;IACpB,kBAAkB,EAAE,iBAAiB;IACrC,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"codegen-data.js","sourceRoot":"","sources":["../src/codegen-data.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bundles every authored data file under `data/` into a single embedded module,\n * `src/data/bundle.generated.ts`.\n *\n * The bundle is inlined as an escaped JSON *string* that is `JSON.parse`d at load\n * time (mirroring the Rust crate's `include_str!`): tsc typechecks it instantly\n * (it is just a string), it parses once at import, and it compiles into `dist`\n * with no runtime filesystem access — so the published package works in Node,\n * bundlers, and browsers alike, where `data/` is not shipped.\n *\n * Run via `npm run codegen:data`. The output is gitignored and regenerated on\n * build/test/pack.\n */\nimport { readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { emptyRawData, type RawData } from \"./data/types.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REPO_ROOT = resolve(__dirname, \"../..\");\nconst DATA_ROOTS = [join(REPO_ROOT, \"data\", \"core\"), join(REPO_ROOT, \"data\", \"enrichment\")];\nconst OUT_FILE = join(__dirname, \"data\", \"bundle.generated.ts\");\n\n/** Directory names that hold examples/scratch data and must never be bundled. */\nconst EXCLUDED_DIRS = new Set([\"_example\", \"_port-audit\"]);\n\n/** Map a data file's base name (sans `.json`) to its `RawData` collection key. */\nconst FILE_TO_COLLECTION: Record<string, keyof RawData> = {\n units: \"units\",\n weapons: \"weapons\",\n \"weapon-keywords\": \"weaponKeywords\",\n factions: \"factions\",\n abilities: \"abilities\",\n \"phase-mappings\": \"phaseMappings\",\n detachments: \"detachments\",\n stratagems: \"stratagems\",\n enhancements: \"enhancements\",\n \"leader-attachments\": \"leaderAttachments\",\n \"unit-compositions\": \"unitCompositions\",\n \"wargear-options\": \"wargearOptions\",\n wargear: \"wargear\",\n \"game-versions\": \"gameVersions\",\n missions: \"missions\",\n \"mission-matchups\": \"missionMatchups\",\n \"secondary-cards\": \"missionCards\",\n \"deployment-patterns\": \"deploymentPatterns\",\n \"force-dispositions\": \"forceDispositions\",\n \"terrain-templates\": \"terrainTemplates\",\n \"terrain-layouts\": \"terrainLayouts\",\n \"resource-pools\": \"resourcePools\",\n \"timing-flags\": \"timingFlags\",\n \"interaction-flags\": \"interactionFlags\",\n};\n\n/** The id-bearing key for a collection, used only for duplicate-id reporting. */\nconst ID_KEY: Partial<Record<keyof RawData, string>> = {\n abilities: \"ability_id\",\n};\n\n/** Recursively collect bundleable `.json` files, skipping excluded dirs/examples. */\nfunction collectFiles(dir: string): string[] {\n const out: string[] = [];\n for (const entry of readdirSync(dir)) {\n if (EXCLUDED_DIRS.has(entry)) continue;\n const full = join(dir, entry);\n if (statSync(full).isDirectory()) {\n out.push(...collectFiles(full));\n } else if (entry.endsWith(\".json\") && !entry.endsWith(\".example.json\")) {\n out.push(full);\n }\n }\n return out;\n}\n\nfunction baseName(file: string): string {\n return file.slice(file.lastIndexOf(\"/\") + 1, -\".json\".length);\n}\n\nfunction build(): RawData {\n const data = emptyRawData();\n for (const root of DATA_ROOTS) {\n for (const file of collectFiles(root)) {\n const collection = FILE_TO_COLLECTION[baseName(file)];\n if (!collection) continue; // schema/scratch json we don't bundle\n const parsed = JSON.parse(readFileSync(file, \"utf-8\")) as unknown;\n if (!Array.isArray(parsed)) {\n throw new Error(`expected a JSON array in ${file}`);\n }\n (data[collection] as unknown[]).push(...parsed);\n }\n }\n return data;\n}\n\n/** Warn (do not fail) on duplicate primary ids — a data-hygiene signal. */\nfunction reportDuplicateIds(data: RawData): void {\n for (const [collection, key] of Object.entries(ID_KEY) as [keyof RawData, string][]) {\n const seen = new Set<string>();\n const dupes = new Set<string>();\n for (const item of data[collection] as Record<string, unknown>[]) {\n const id = item[key] as string | undefined;\n if (id === undefined) continue;\n if (seen.has(id)) dupes.add(id);\n else seen.add(id);\n }\n if (dupes.size > 0) {\n console.warn(` ⚠ ${collection}: ${dupes.size} duplicate ${key}(s), e.g. ${[...dupes].slice(0, 3).join(\", \")}`);\n }\n }\n}\n\nfunction emit(data: RawData): string {\n // JSON.stringify of the JSON text yields a valid, fully-escaped JS string\n // literal — safe to drop straight into the generated source.\n const jsonText = JSON.stringify(data);\n const literal = JSON.stringify(jsonText);\n return `/* GENERATED by 'npm run codegen:data' from the repository's data/ tree. DO NOT EDIT BY HAND. */\nimport type { RawData } from \"./types.js\";\n\nconst JSON_TEXT = ${literal};\n\n/** The full 40kdc dataset, embedded at build time and parsed once at load. */\nexport const RAW_DATA: RawData = JSON.parse(JSON_TEXT) as RawData;\n`;\n}\n\nfunction main(): void {\n const data = build();\n reportDuplicateIds(data);\n writeFileSync(OUT_FILE, emit(data));\n const counts = (Object.keys(data) as (keyof RawData)[])\n .map((k) => `${k}=${data[k].length}`)\n .join(\", \");\n console.log(`Wrote ${OUT_FILE}\\n ${counts}`);\n}\n\nmain();\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"populate-base-sizes.d.ts","sourceRoot":"","sources":["../../src/commands/populate-base-sizes.ts"],"names":[],"mappings":"AA0EA,wBAAgB,wBAAwB,IAAI,IAAI,CA+G/C"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Populate `base_size_mm` on units and their composition models from the GW
|
|
3
|
+
* Tournament Companion base-size guide (primary) and bevy-deploy-helper (fallback).
|
|
4
|
+
*
|
|
5
|
+
* Additive only: this patches the existing committed `units.json` and
|
|
6
|
+
* `unit-compositions.json` in place, preserving every other field and key order.
|
|
7
|
+
* It deliberately does NOT re-run `convert-faction` (which regenerates and would
|
|
8
|
+
* regress unrelated committed data — see CONTRIBUTING.md / project memory).
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { resolve, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { buildGuideIndex, buildBevyIndex, assignBaseSizes, } from "../converters/base-size-bridge.js";
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = resolve(__dirname, "../../.."); // tools/src/commands → repo root
|
|
16
|
+
const CORE_DIR = resolve(ROOT, "data/core");
|
|
17
|
+
const GUIDE_PATH = resolve(__dirname, "../converters/data/base-size-guide.json");
|
|
18
|
+
const BEVY_DIR = resolve(ROOT, "../bevy-deploy-helper/assets");
|
|
19
|
+
const REPORT_PATH = resolve(CORE_DIR, "_reports/_base-sizes.unresolved.json");
|
|
20
|
+
function readJson(path) {
|
|
21
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
function writeJson(path, data) {
|
|
24
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
25
|
+
}
|
|
26
|
+
/** Rebuild an object, dropping any existing `base_size_mm`, then inserting it after
|
|
27
|
+
* the first present anchor key (or appending if none/undefined). Preserves order. */
|
|
28
|
+
function withBaseSize(obj, value, anchors) {
|
|
29
|
+
const anchor = anchors.find((k) => k in obj);
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
32
|
+
if (k === "base_size_mm")
|
|
33
|
+
continue; // re-inserted below (idempotent re-runs)
|
|
34
|
+
out[k] = v;
|
|
35
|
+
if (k === anchor && value)
|
|
36
|
+
out.base_size_mm = value;
|
|
37
|
+
}
|
|
38
|
+
if (value && !anchor)
|
|
39
|
+
out.base_size_mm = value;
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Vehicle/Aircraft units are expected to be hull-based; tracked separately in the summary. */
|
|
43
|
+
function isVehicle(u) {
|
|
44
|
+
return (u.keywords ?? []).some((k) => /vehicle|aircraft/i.test(k));
|
|
45
|
+
}
|
|
46
|
+
export function populateBaseSizesCommand() {
|
|
47
|
+
// ── Sources ──────────────────────────────────────────────────────
|
|
48
|
+
const guideRows = readJson(GUIDE_PATH);
|
|
49
|
+
const guide = buildGuideIndex(guideRows);
|
|
50
|
+
let bevy = new Map();
|
|
51
|
+
if (existsSync(resolve(BEVY_DIR, "Datasheets.json"))) {
|
|
52
|
+
const src = {
|
|
53
|
+
datasheets: readJson(resolve(BEVY_DIR, "Datasheets.json")),
|
|
54
|
+
models: readJson(resolve(BEVY_DIR, "Datasheets_models.json")),
|
|
55
|
+
};
|
|
56
|
+
bevy = buildBevyIndex(src);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.warn(` ! bevy fallback not found at ${BEVY_DIR} — proceeding guide-only`);
|
|
60
|
+
}
|
|
61
|
+
// ── Load every faction's units + compositions ────────────────────
|
|
62
|
+
const factions = readdirSync(CORE_DIR, { withFileTypes: true })
|
|
63
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("_"))
|
|
64
|
+
.map((d) => d.name)
|
|
65
|
+
.filter((f) => existsSync(resolve(CORE_DIR, f, "units.json")));
|
|
66
|
+
const compByUnit = new Map();
|
|
67
|
+
const compFiles = new Map();
|
|
68
|
+
for (const f of factions) {
|
|
69
|
+
const cpath = resolve(CORE_DIR, f, "unit-compositions.json");
|
|
70
|
+
if (!existsSync(cpath))
|
|
71
|
+
continue;
|
|
72
|
+
const records = readJson(cpath);
|
|
73
|
+
compFiles.set(f, { path: cpath, records });
|
|
74
|
+
for (const c of records)
|
|
75
|
+
compByUnit.set(c.unit_id, c.models);
|
|
76
|
+
}
|
|
77
|
+
const unitFiles = new Map();
|
|
78
|
+
const unitInputs = [];
|
|
79
|
+
for (const f of factions) {
|
|
80
|
+
const upath = resolve(CORE_DIR, f, "units.json");
|
|
81
|
+
const records = readJson(upath);
|
|
82
|
+
unitFiles.set(f, { path: upath, records });
|
|
83
|
+
for (const u of records) {
|
|
84
|
+
unitInputs.push({ id: u.id, models: compByUnit.get(u.id) ?? [] });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ── Resolve ──────────────────────────────────────────────────────
|
|
88
|
+
const { assignments, report } = assignBaseSizes(unitInputs, guide, bevy);
|
|
89
|
+
// ── Patch units.json (representative base) ───────────────────────
|
|
90
|
+
const stats = { unitsTotal: 0, unitsPopulated: 0, nvTotal: 0, nvPopulated: 0, draftUnits: 0 };
|
|
91
|
+
for (const [f, { path, records }] of unitFiles) {
|
|
92
|
+
const patched = records.map((u) => {
|
|
93
|
+
stats.unitsTotal++;
|
|
94
|
+
const vehicle = isVehicle(u);
|
|
95
|
+
if (!vehicle)
|
|
96
|
+
stats.nvTotal++;
|
|
97
|
+
const base = assignments.get(u.id)?.unitBase;
|
|
98
|
+
if (base) {
|
|
99
|
+
stats.unitsPopulated++;
|
|
100
|
+
if (!vehicle)
|
|
101
|
+
stats.nvPopulated++;
|
|
102
|
+
if (base.draft)
|
|
103
|
+
stats.draftUnits++;
|
|
104
|
+
}
|
|
105
|
+
return withBaseSize(u, base, ["faction_keywords", "keywords", "profiles"]);
|
|
106
|
+
});
|
|
107
|
+
writeJson(path, patched);
|
|
108
|
+
}
|
|
109
|
+
// ── Patch unit-compositions.json (per-model bases) ───────────────
|
|
110
|
+
let modelEntries = 0;
|
|
111
|
+
let modelPopulated = 0;
|
|
112
|
+
for (const [f, { path, records }] of compFiles) {
|
|
113
|
+
const patched = records.map((c) => {
|
|
114
|
+
const modelBases = assignments.get(c.unit_id)?.modelBases ?? new Map();
|
|
115
|
+
const models = c.models.map((m) => {
|
|
116
|
+
modelEntries++;
|
|
117
|
+
const base = modelBases.get(m.name);
|
|
118
|
+
if (base)
|
|
119
|
+
modelPopulated++;
|
|
120
|
+
return withBaseSize(m, base, ["is_leader_model", "default_weapon_ids", "max"]);
|
|
121
|
+
});
|
|
122
|
+
return { ...c, models };
|
|
123
|
+
});
|
|
124
|
+
writeJson(path, patched);
|
|
125
|
+
}
|
|
126
|
+
// ── Report ───────────────────────────────────────────────────────
|
|
127
|
+
mkdirSync(dirname(REPORT_PATH), { recursive: true });
|
|
128
|
+
writeJson(REPORT_PATH, {
|
|
129
|
+
generated_from: "GW Chapter Approved Tournament Companion — Base Size Guide (primary) + bevy-deploy-helper (fallback)",
|
|
130
|
+
summary: {
|
|
131
|
+
units_total: stats.unitsTotal,
|
|
132
|
+
units_populated: stats.unitsPopulated,
|
|
133
|
+
non_vehicle_total: stats.nvTotal,
|
|
134
|
+
non_vehicle_populated: stats.nvPopulated,
|
|
135
|
+
draft_units: stats.draftUnits,
|
|
136
|
+
bevy_fallback: report.bevyFallback.length,
|
|
137
|
+
unmatched: report.unmatched.length,
|
|
138
|
+
unresolved_models: report.unresolvedModels.length,
|
|
139
|
+
guide_unparsed: report.guideUnparsed.length,
|
|
140
|
+
},
|
|
141
|
+
unmatched_units: report.unmatched.sort(),
|
|
142
|
+
bevy_fallback_units: report.bevyFallback.sort(),
|
|
143
|
+
unresolved_models: report.unresolvedModels.sort(),
|
|
144
|
+
guide_unparsed_rows: report.guideUnparsed,
|
|
145
|
+
});
|
|
146
|
+
// ── Summary ──────────────────────────────────────────────────────
|
|
147
|
+
const pct = (n, d) => (d === 0 ? "0" : ((100 * n) / d).toFixed(1));
|
|
148
|
+
console.log(` ✓ base sizes populated`);
|
|
149
|
+
console.log(` units: ${stats.unitsPopulated}/${stats.unitsTotal} (${pct(stats.unitsPopulated, stats.unitsTotal)}%)`);
|
|
150
|
+
console.log(` non-vehicle: ${stats.nvPopulated}/${stats.nvTotal} (${pct(stats.nvPopulated, stats.nvTotal)}%)`);
|
|
151
|
+
console.log(` per-model: ${modelPopulated}/${modelEntries}`);
|
|
152
|
+
console.log(` draft units: ${stats.draftUnits} bevy fallback: ${report.bevyFallback.length}`);
|
|
153
|
+
console.log(` unmatched: ${report.unmatched.length} unresolved models: ${report.unresolvedModels.length}`);
|
|
154
|
+
console.log(` report → ${REPORT_PATH.replace(ROOT + "/", "")}`);
|
|
155
|
+
}
|
|
156
|
+
if (import.meta.url === `file://${process.argv[1]}`)
|
|
157
|
+
populateBaseSizesCommand();
|
|
158
|
+
//# sourceMappingURL=populate-base-sizes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"populate-base-sizes.js","sourceRoot":"","sources":["../../src/commands/populate-base-sizes.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Populate `base_size_mm` on units and their composition models from the GW\n * Tournament Companion base-size guide (primary) and bevy-deploy-helper (fallback).\n *\n * Additive only: this patches the existing committed `units.json` and\n * `unit-compositions.json` in place, preserving every other field and key order.\n * It deliberately does NOT re-run `convert-faction` (which regenerates and would\n * regress unrelated committed data — see CONTRIBUTING.md / project memory).\n */\nimport { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport {\n buildGuideIndex,\n buildBevyIndex,\n assignBaseSizes,\n type GuideRow,\n type BaseSize,\n type BevySources,\n type UnitInput,\n type CompositionModel,\n} from \"../converters/base-size-bridge.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst ROOT = resolve(__dirname, \"../../..\"); // tools/src/commands → repo root\nconst CORE_DIR = resolve(ROOT, \"data/core\");\nconst GUIDE_PATH = resolve(__dirname, \"../converters/data/base-size-guide.json\");\nconst BEVY_DIR = resolve(ROOT, \"../bevy-deploy-helper/assets\");\nconst REPORT_PATH = resolve(CORE_DIR, \"_reports/_base-sizes.unresolved.json\");\n\ninterface UnitRecord {\n id: string;\n keywords?: string[];\n faction_keywords?: string[];\n base_size_mm?: BaseSize | null;\n [k: string]: unknown;\n}\ninterface CompositionRecord {\n unit_id: string;\n models: Array<CompositionModel & { base_size_mm?: BaseSize } & Record<string, unknown>>;\n [k: string]: unknown;\n}\n\nfunction readJson<T>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf8\")) as T;\n}\nfunction writeJson(path: string, data: unknown): void {\n writeFileSync(path, JSON.stringify(data, null, 2) + \"\\n\");\n}\n\n/** Rebuild an object, dropping any existing `base_size_mm`, then inserting it after\n * the first present anchor key (or appending if none/undefined). Preserves order. */\nfunction withBaseSize<T extends Record<string, unknown>>(\n obj: T,\n value: BaseSize | undefined,\n anchors: string[],\n): T {\n const anchor = anchors.find((k) => k in obj);\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (k === \"base_size_mm\") continue; // re-inserted below (idempotent re-runs)\n out[k] = v;\n if (k === anchor && value) out.base_size_mm = value;\n }\n if (value && !anchor) out.base_size_mm = value;\n return out as T;\n}\n\n/** Vehicle/Aircraft units are expected to be hull-based; tracked separately in the summary. */\nfunction isVehicle(u: UnitRecord): boolean {\n return (u.keywords ?? []).some((k) => /vehicle|aircraft/i.test(k));\n}\n\nexport function populateBaseSizesCommand(): void {\n // ── Sources ──────────────────────────────────────────────────────\n const guideRows = readJson<GuideRow[]>(GUIDE_PATH);\n const guide = buildGuideIndex(guideRows);\n\n let bevy = new Map<string, BaseSize>();\n if (existsSync(resolve(BEVY_DIR, \"Datasheets.json\"))) {\n const src: BevySources = {\n datasheets: readJson(resolve(BEVY_DIR, \"Datasheets.json\")),\n models: readJson(resolve(BEVY_DIR, \"Datasheets_models.json\")),\n };\n bevy = buildBevyIndex(src);\n } else {\n console.warn(` ! bevy fallback not found at ${BEVY_DIR} — proceeding guide-only`);\n }\n\n // ── Load every faction's units + compositions ────────────────────\n const factions = readdirSync(CORE_DIR, { withFileTypes: true })\n .filter((d) => d.isDirectory() && !d.name.startsWith(\"_\"))\n .map((d) => d.name)\n .filter((f) => existsSync(resolve(CORE_DIR, f, \"units.json\")));\n\n const compByUnit = new Map<string, CompositionModel[]>();\n const compFiles = new Map<string, { path: string; records: CompositionRecord[] }>();\n for (const f of factions) {\n const cpath = resolve(CORE_DIR, f, \"unit-compositions.json\");\n if (!existsSync(cpath)) continue;\n const records = readJson<CompositionRecord[]>(cpath);\n compFiles.set(f, { path: cpath, records });\n for (const c of records) compByUnit.set(c.unit_id, c.models);\n }\n\n const unitFiles = new Map<string, { path: string; records: UnitRecord[] }>();\n const unitInputs: UnitInput[] = [];\n for (const f of factions) {\n const upath = resolve(CORE_DIR, f, \"units.json\");\n const records = readJson<UnitRecord[]>(upath);\n unitFiles.set(f, { path: upath, records });\n for (const u of records) {\n unitInputs.push({ id: u.id, models: compByUnit.get(u.id) ?? [] });\n }\n }\n\n // ── Resolve ──────────────────────────────────────────────────────\n const { assignments, report } = assignBaseSizes(unitInputs, guide, bevy);\n\n // ── Patch units.json (representative base) ───────────────────────\n const stats = { unitsTotal: 0, unitsPopulated: 0, nvTotal: 0, nvPopulated: 0, draftUnits: 0 };\n for (const [f, { path, records }] of unitFiles) {\n const patched = records.map((u) => {\n stats.unitsTotal++;\n const vehicle = isVehicle(u);\n if (!vehicle) stats.nvTotal++;\n const base = assignments.get(u.id)?.unitBase;\n if (base) {\n stats.unitsPopulated++;\n if (!vehicle) stats.nvPopulated++;\n if (base.draft) stats.draftUnits++;\n }\n return withBaseSize(u, base, [\"faction_keywords\", \"keywords\", \"profiles\"]);\n });\n writeJson(path, patched);\n }\n\n // ── Patch unit-compositions.json (per-model bases) ───────────────\n let modelEntries = 0;\n let modelPopulated = 0;\n for (const [f, { path, records }] of compFiles) {\n const patched = records.map((c) => {\n const modelBases = assignments.get(c.unit_id)?.modelBases ?? new Map<string, BaseSize>();\n const models = c.models.map((m) => {\n modelEntries++;\n const base = modelBases.get(m.name);\n if (base) modelPopulated++;\n return withBaseSize(m, base, [\"is_leader_model\", \"default_weapon_ids\", \"max\"]);\n });\n return { ...c, models };\n });\n writeJson(path, patched);\n }\n\n // ── Report ───────────────────────────────────────────────────────\n mkdirSync(dirname(REPORT_PATH), { recursive: true });\n writeJson(REPORT_PATH, {\n generated_from: \"GW Chapter Approved Tournament Companion — Base Size Guide (primary) + bevy-deploy-helper (fallback)\",\n summary: {\n units_total: stats.unitsTotal,\n units_populated: stats.unitsPopulated,\n non_vehicle_total: stats.nvTotal,\n non_vehicle_populated: stats.nvPopulated,\n draft_units: stats.draftUnits,\n bevy_fallback: report.bevyFallback.length,\n unmatched: report.unmatched.length,\n unresolved_models: report.unresolvedModels.length,\n guide_unparsed: report.guideUnparsed.length,\n },\n unmatched_units: report.unmatched.sort(),\n bevy_fallback_units: report.bevyFallback.sort(),\n unresolved_models: report.unresolvedModels.sort(),\n guide_unparsed_rows: report.guideUnparsed,\n });\n\n // ── Summary ──────────────────────────────────────────────────────\n const pct = (n: number, d: number) => (d === 0 ? \"0\" : ((100 * n) / d).toFixed(1));\n console.log(` ✓ base sizes populated`);\n console.log(` units: ${stats.unitsPopulated}/${stats.unitsTotal} (${pct(stats.unitsPopulated, stats.unitsTotal)}%)`);\n console.log(` non-vehicle: ${stats.nvPopulated}/${stats.nvTotal} (${pct(stats.nvPopulated, stats.nvTotal)}%)`);\n console.log(` per-model: ${modelPopulated}/${modelEntries}`);\n console.log(` draft units: ${stats.draftUnits} bevy fallback: ${report.bevyFallback.length}`);\n console.log(` unmatched: ${report.unmatched.length} unresolved models: ${report.unresolvedModels.length}`);\n console.log(` report → ${REPORT_PATH.replace(ROOT + \"/\", \"\")}`);\n}\n\nif (import.meta.url === `file://${process.argv[1]}`) populateBaseSizesCommand();\n"]}
|
|
@@ -42,5 +42,7 @@ import "./converters/configs/iron-hands.js";
|
|
|
42
42
|
import "./converters/configs/raven-guard.js";
|
|
43
43
|
import "./converters/configs/salamanders.js";
|
|
44
44
|
import "./converters/configs/white-scars.js";
|
|
45
|
-
export declare function convertFaction(config: FactionConfig
|
|
45
|
+
export declare function convertFaction(config: FactionConfig, options?: {
|
|
46
|
+
wargearOnly?: boolean;
|
|
47
|
+
}): void;
|
|
46
48
|
//# sourceMappingURL=convert-faction.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert-faction.d.ts","sourceRoot":"","sources":["../src/convert-faction.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"convert-faction.d.ts","sourceRoot":"","sources":["../src/convert-faction.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,OAAO,EAAE,KAAK,aAAa,EAAkC,MAAM,gCAAgC,CAAC;AAGpG,OAAO,sCAAsC,CAAC;AAC9C,OAAO,2CAA2C,CAAC;AACnD,OAAO,uCAAuC,CAAC;AAC/C,OAAO,0CAA0C,CAAC;AAClD,OAAO,2CAA2C,CAAC;AACnD,OAAO,kCAAkC,CAAC;AAC1C,OAAO,2CAA2C,CAAC;AACnD,OAAO,sCAAsC,CAAC;AAC9C,OAAO,uCAAuC,CAAC;AAC/C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,0CAA0C,CAAC;AAClD,OAAO,0CAA0C,CAAC;AAClD,OAAO,gDAAgD,CAAC;AACxD,OAAO,4CAA4C,CAAC;AACpD,OAAO,oCAAoC,CAAC;AAC5C,OAAO,kCAAkC,CAAC;AAC1C,OAAO,iCAAiC,CAAC;AACzC,OAAO,uCAAuC,CAAC;AAC/C,OAAO,8BAA8B,CAAC;AACtC,OAAO,iCAAiC,CAAC;AACzC,OAAO,6CAA6C,CAAC;AACrD,OAAO,yCAAyC,CAAC;AACjD,OAAO,0CAA0C,CAAC;AAClD,OAAO,sCAAsC,CAAC;AAC9C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,sCAAsC,CAAC;AAC9C,OAAO,wCAAwC,CAAC;AAChD,OAAO,oCAAoC,CAAC;AAC5C,OAAO,sCAAsC,CAAC;AAC9C,OAAO,wCAAwC,CAAC;AAChD,OAAO,uCAAuC,CAAC;AAC/C,OAAO,oCAAoC,CAAC;AAC5C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qCAAqC,CAAC;AAqJ7C,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,IAAI,CA6dN"}
|
package/dist/convert-faction.js
CHANGED
|
@@ -13,6 +13,7 @@ import { nameToId, parseStratagemType, parsePlayerTurn, mapPhases } from "./conv
|
|
|
13
13
|
import { parseMove, parseTargetNumber, parseIntStat, parseInvuln } from "./converters/stat-parser.js";
|
|
14
14
|
import { findFactionViewIndex, getViewEntries, getPointsForView, splitIntoViews } from "./converters/view-selector.js";
|
|
15
15
|
import { buildWeaponRegistry } from "./converters/weapon-dedup.js";
|
|
16
|
+
import { buildWargearOptions } from "./converters/wargear-options.js";
|
|
16
17
|
import { getKeywordsForFaction } from "./converters/keyword-filter.js";
|
|
17
18
|
import { getFactionConfig, listFactions } from "./converters/faction-config.js";
|
|
18
19
|
// Register all faction configs
|
|
@@ -113,13 +114,15 @@ function parseTransport(s) {
|
|
|
113
114
|
return result;
|
|
114
115
|
}
|
|
115
116
|
// ─── Main conversion ─────────────────────────────────────────────────
|
|
116
|
-
export function convertFaction(config) {
|
|
117
|
+
export function convertFaction(config, options = {}) {
|
|
117
118
|
const { sourceFactionId, factionId, factionName, factionAbilityName } = config;
|
|
118
119
|
console.log(`Converting ${factionName} (${sourceFactionId} → ${factionId})...`);
|
|
119
120
|
console.log("Loading source data from army-assist...");
|
|
120
121
|
const datasheets = readJSON("Datasheets.json");
|
|
121
122
|
const allModels = readJSON("Datasheets_models.json");
|
|
122
123
|
const allWargear = readJSON("Datasheets_wargear.json");
|
|
124
|
+
const allOptions = readJSON("Datasheets_options.json");
|
|
125
|
+
const allComposition = readJSON("Datasheets_unit_composition.json");
|
|
123
126
|
const allAbilities = readJSON("Datasheets_abilities.json");
|
|
124
127
|
const allKeywords = readJSON("Datasheets_keywords.json");
|
|
125
128
|
const allPoints = readJSON("Datasheets_points.json");
|
|
@@ -224,6 +227,10 @@ export function convertFaction(config) {
|
|
|
224
227
|
unit.weapon_ids = [...weaponIds].sort();
|
|
225
228
|
}
|
|
226
229
|
}
|
|
230
|
+
// ─── Build wargear options + non-weapon wargear ───
|
|
231
|
+
console.log("Converting wargear options...");
|
|
232
|
+
const globalWeaponIds = new Set(weapons.map((w) => w.id));
|
|
233
|
+
const { wargearOptions, wargear: wargearItems, unparsed: unparsedOptions, } = buildWargearOptions(factionDatasheets.map((d) => ({ id: d.id, name: d.name })), allModels, allOptions, allComposition, unitWeaponIds, globalWeaponIds, GAME_VERSION);
|
|
227
234
|
// ─── Build leader attachments ───
|
|
228
235
|
console.log("Converting leader attachments...");
|
|
229
236
|
const leaderMap = new Map();
|
|
@@ -432,18 +439,37 @@ export function convertFaction(config) {
|
|
|
432
439
|
mkdirSync(resolve(ROOT, coreDir), { recursive: true });
|
|
433
440
|
mkdirSync(resolve(ROOT, enrichDir), { recursive: true });
|
|
434
441
|
console.log("\nWriting output files...");
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
writeOutput(`${coreDir}/
|
|
442
|
+
// `wargearOnly` adds just the wargear data: the rest of the converter's output
|
|
443
|
+
// has drifted from the committed dataset (e.g. weapon keywords are object refs
|
|
444
|
+
// there, strings here), so a full rewrite would regress those files. Adding
|
|
445
|
+
// wargear must not touch them.
|
|
446
|
+
if (!options.wargearOnly) {
|
|
447
|
+
writeOutput(`${coreDir}/factions.json`, factionEntity);
|
|
441
448
|
}
|
|
442
|
-
writeOutput(`${coreDir}/detachments.json`, detachments);
|
|
443
|
-
writeOutput(`${coreDir}/enhancements.json`, enhancementEntities);
|
|
444
|
-
writeOutput(`${coreDir}/stratagems.json`, stratagemEntities);
|
|
445
449
|
if (!config.skipUnits) {
|
|
446
|
-
|
|
450
|
+
if (!options.wargearOnly) {
|
|
451
|
+
writeOutput(`${coreDir}/units.json`, units);
|
|
452
|
+
writeOutput(`${coreDir}/weapons.json`, weapons);
|
|
453
|
+
writeOutput(`${coreDir}/leader-attachments.json`, leaderAttachments);
|
|
454
|
+
writeOutput(`${coreDir}/unit-compositions.json`, unitCompositions);
|
|
455
|
+
}
|
|
456
|
+
writeOutput(`${coreDir}/wargear-options.json`, wargearOptions);
|
|
457
|
+
if (wargearItems.length > 0) {
|
|
458
|
+
writeOutput(`${coreDir}/wargear.json`, wargearItems);
|
|
459
|
+
}
|
|
460
|
+
if (unparsedOptions.length > 0) {
|
|
461
|
+
// Underscore-prefixed: a report for manual review, skipped by validation
|
|
462
|
+
// and not bundled into the dataset.
|
|
463
|
+
writeOutput(`${coreDir}/_wargear-options.unparsed.json`, unparsedOptions);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (!options.wargearOnly) {
|
|
467
|
+
writeOutput(`${coreDir}/detachments.json`, detachments);
|
|
468
|
+
writeOutput(`${coreDir}/enhancements.json`, enhancementEntities);
|
|
469
|
+
writeOutput(`${coreDir}/stratagems.json`, stratagemEntities);
|
|
470
|
+
if (!config.skipUnits) {
|
|
471
|
+
writeOutput(`${enrichDir}/phase-mappings.json`, dedupedPhaseMappings);
|
|
472
|
+
}
|
|
447
473
|
}
|
|
448
474
|
// ─── Summary ───
|
|
449
475
|
console.log(`\n── ${factionName} Summary ──`);
|
|
@@ -452,6 +478,9 @@ export function convertFaction(config) {
|
|
|
452
478
|
console.log(` Weapons: ${weapons.length}`);
|
|
453
479
|
console.log(` Leader attachments: ${leaderAttachments.length}`);
|
|
454
480
|
console.log(` Unit compositions: ${unitCompositions.length}`);
|
|
481
|
+
console.log(` Wargear options: ${wargearOptions.length}`);
|
|
482
|
+
console.log(` Wargear items: ${wargearItems.length}`);
|
|
483
|
+
console.log(` Unparsed options: ${unparsedOptions.length}`);
|
|
455
484
|
}
|
|
456
485
|
console.log(` Detachments: ${detachments.length}`);
|
|
457
486
|
console.log(` Enhancements: ${enhancementEntities.length}`);
|
|
@@ -468,13 +497,17 @@ const isMain = process.argv[1] &&
|
|
|
468
497
|
fileURLToPath(import.meta.url).replace(/\.\w+$/, "");
|
|
469
498
|
if (isMain) {
|
|
470
499
|
const args = process.argv.slice(2);
|
|
471
|
-
|
|
472
|
-
|
|
500
|
+
const wargearOnly = args.includes("--wargear-only");
|
|
501
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
502
|
+
if (positional.length === 0 || args[0] === "--help") {
|
|
503
|
+
console.log("Usage: npx tsx tools/src/convert-faction.ts <faction-id|all> [--wargear-only]");
|
|
473
504
|
console.log(`Available factions: ${listFactions().join(", ")}`);
|
|
474
505
|
process.exit(args[0] === "--help" ? 0 : 1);
|
|
475
506
|
}
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
507
|
+
const target = positional[0];
|
|
508
|
+
const factionIds = target === "all" ? listFactions() : [target];
|
|
509
|
+
for (const id of factionIds) {
|
|
510
|
+
convertFaction(getFactionConfig(id), { wargearOnly });
|
|
511
|
+
}
|
|
479
512
|
}
|
|
480
513
|
//# sourceMappingURL=convert-faction.js.map
|