@alpaca-software/40kdc-data 0.3.2 → 0.4.5

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.
Files changed (102) hide show
  1. package/README.md +12 -6
  2. package/dist/bundle-schemas.d.ts.map +1 -1
  3. package/dist/bundle-schemas.js +17 -0
  4. package/dist/bundle-schemas.js.map +1 -1
  5. package/dist/cli.js +5 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/codegen-data.js +1 -0
  8. package/dist/codegen-data.js.map +1 -1
  9. package/dist/commands/populate-base-sizes.d.ts +2 -0
  10. package/dist/commands/populate-base-sizes.d.ts.map +1 -0
  11. package/dist/commands/populate-base-sizes.js +158 -0
  12. package/dist/commands/populate-base-sizes.js.map +1 -0
  13. package/dist/convert-faction.d.ts +3 -1
  14. package/dist/convert-faction.d.ts.map +1 -1
  15. package/dist/convert-faction.js +49 -16
  16. package/dist/convert-faction.js.map +1 -1
  17. package/dist/converters/base-size-bridge.d.ts +122 -0
  18. package/dist/converters/base-size-bridge.d.ts.map +1 -0
  19. package/dist/converters/base-size-bridge.js +198 -0
  20. package/dist/converters/base-size-bridge.js.map +1 -0
  21. package/dist/converters/base-size-guide-extract.d.ts +11 -0
  22. package/dist/converters/base-size-guide-extract.d.ts.map +1 -0
  23. package/dist/converters/base-size-guide-extract.js +59 -0
  24. package/dist/converters/base-size-guide-extract.js.map +1 -0
  25. package/dist/converters/option-bridge.d.ts +36 -0
  26. package/dist/converters/option-bridge.d.ts.map +1 -0
  27. package/dist/converters/option-bridge.js +72 -0
  28. package/dist/converters/option-bridge.js.map +1 -0
  29. package/dist/converters/option-parser.d.ts +56 -0
  30. package/dist/converters/option-parser.d.ts.map +1 -0
  31. package/dist/converters/option-parser.js +209 -0
  32. package/dist/converters/option-parser.js.map +1 -0
  33. package/dist/converters/wargear-options.d.ts +55 -0
  34. package/dist/converters/wargear-options.d.ts.map +1 -0
  35. package/dist/converters/wargear-options.js +187 -0
  36. package/dist/converters/wargear-options.js.map +1 -0
  37. package/dist/data/bundle.generated.js +1 -1
  38. package/dist/data/bundle.generated.js.map +1 -1
  39. package/dist/data/dataset.d.ts +9 -1
  40. package/dist/data/dataset.d.ts.map +1 -1
  41. package/dist/data/dataset.js +14 -0
  42. package/dist/data/dataset.js.map +1 -1
  43. package/dist/data/entities.d.ts +3 -1
  44. package/dist/data/entities.d.ts.map +1 -1
  45. package/dist/data/entities.js +4 -0
  46. package/dist/data/entities.js.map +1 -1
  47. package/dist/data/index.d.ts +4 -0
  48. package/dist/data/index.d.ts.map +1 -1
  49. package/dist/data/index.js +4 -0
  50. package/dist/data/index.js.map +1 -1
  51. package/dist/data/loadout.d.ts +60 -0
  52. package/dist/data/loadout.d.ts.map +1 -0
  53. package/dist/data/loadout.js +135 -0
  54. package/dist/data/loadout.js.map +1 -0
  55. package/dist/data/types.d.ts +3 -1
  56. package/dist/data/types.d.ts.map +1 -1
  57. package/dist/data/types.js +1 -0
  58. package/dist/data/types.js.map +1 -1
  59. package/dist/export/rosterizer.js +1 -1
  60. package/dist/export/rosterizer.js.map +1 -1
  61. package/dist/gen-conformance.js +171 -0
  62. package/dist/gen-conformance.js.map +1 -1
  63. package/dist/generated.d.ts +135 -55
  64. package/dist/generated.d.ts.map +1 -1
  65. package/dist/generated.js.map +1 -1
  66. package/dist/import/rosterizer.d.ts +1 -1
  67. package/dist/import/rosterizer.js.map +1 -1
  68. package/dist/index.d.ts +3 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +3 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/runner.d.ts +16 -0
  73. package/dist/runner.d.ts.map +1 -1
  74. package/dist/runner.js +216 -0
  75. package/dist/runner.js.map +1 -1
  76. package/dist/scoring/index.d.ts +28 -6
  77. package/dist/scoring/index.d.ts.map +1 -1
  78. package/dist/scoring/index.js +31 -7
  79. package/dist/scoring/index.js.map +1 -1
  80. package/dist/terrain/index.d.ts +2 -2
  81. package/dist/terrain/index.d.ts.map +1 -1
  82. package/dist/terrain/index.js +1 -1
  83. package/dist/terrain/index.js.map +1 -1
  84. package/dist/terrain/solve.d.ts +41 -0
  85. package/dist/terrain/solve.d.ts.map +1 -1
  86. package/dist/terrain/solve.js +100 -0
  87. package/dist/terrain/solve.js.map +1 -1
  88. package/dist/translate/condition.d.ts.map +1 -1
  89. package/dist/translate/condition.js +4 -0
  90. package/dist/translate/condition.js.map +1 -1
  91. package/dist/validate.d.ts.map +1 -1
  92. package/dist/validate.js +13 -5
  93. package/dist/validate.js.map +1 -1
  94. package/package.json +5 -5
  95. package/schemas/$defs/common.schema.json +14 -0
  96. package/schemas/core/secondary-card.schema.json +10 -0
  97. package/schemas/core/terrain-layout.schema.json +18 -0
  98. package/schemas/core/unit-composition.schema.json +5 -1
  99. package/schemas/core/unit.schema.json +2 -10
  100. package/schemas/core/wargear-option.schema.json +32 -6
  101. package/schemas/core/wargear.schema.json +24 -0
  102. 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://tabletop-developer-consortium.github.io) Warhammer 40,000
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.
@@ -15,8 +15,8 @@ bundlers, and the browser.
15
15
  import { units } from "@alpaca-software/40kdc-data";
16
16
 
17
17
  units.find("Kharn")!.abilities
18
- .filter(a => a.phases.includes("shooting"))
19
- .map(a => a.id); // ["berzerker-frenzy"]
18
+ .filter(a => a.phases.includes("fight"))
19
+ .map(a => a.id); // ["legendary-killer", "berzerker-frenzy"]
20
20
  ```
21
21
 
22
22
  ## Install
@@ -69,12 +69,18 @@ data against them. See the repository root for schema details.
69
69
 
70
70
  ## Licensing & attribution
71
71
 
72
- - Code (`tools/`): **MIT**.
72
+ - Code (`tools/`): **MIT + attribution requirement** — see [LICENSE-TOOLS](../LICENSE-TOOLS).
73
73
  - Embedded enrichment data (`data/enrichment/`): **CC BY 4.0** —
74
- attribution: *40kdc community contributors*
75
- (<https://github.com/Tabletop-Developer-Consortium/40kdc-data>).
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
+ **Public deployment requirement:** Any publicly accessible application or
79
+ service that ships this package as part of its end-user product must display a
80
+ visible credit containing the text **"Powered by 40kdc-data"** and a link to
81
+ <https://40kdc.alpacasoft.dev> in a user-accessible location (footer, about
82
+ page, or credits section). Private use and library redistribution are exempt.
83
+
78
84
  Stat lines and points are numerical facts. Ability and rules text are never
79
85
  stored — abilities are community-authored structured mechanics (the Ability
80
86
  DSL), not reproductions of copyrighted text.
@@ -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;AA2E1C,wBAAgB,MAAM,IAAI,UAAU,CAsCnC"}
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"}
@@ -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;AAE3D,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,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\";\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.parse();\n"]}
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"]}
@@ -35,6 +35,7 @@ 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",
@@ -1 +1 @@
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 \"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"]}
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,2 @@
1
+ export declare function populateBaseSizesCommand(): void;
2
+ //# sourceMappingURL=populate-base-sizes.d.ts.map
@@ -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): void;
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;AAUH,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,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAgb1D"}
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"}
@@ -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
- 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);
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
- writeOutput(`${enrichDir}/phase-mappings.json`, dedupedPhaseMappings);
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
- if (args.length === 0 || args[0] === "--help") {
472
- console.log("Usage: npx tsx tools/src/convert-faction.ts <faction-id>");
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 factionIdArg = args[0];
477
- const factionConfig = getFactionConfig(factionIdArg);
478
- convertFaction(factionConfig);
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