@alpaca-software/40kdc-data 0.3.2 → 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.
Files changed (87) hide show
  1. package/README.md +3 -3
  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 +20 -0
  62. package/dist/gen-conformance.js.map +1 -1
  63. package/dist/generated.d.ts +112 -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 +1 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/runner.d.ts +16 -0
  72. package/dist/runner.d.ts.map +1 -1
  73. package/dist/runner.js +54 -0
  74. package/dist/runner.js.map +1 -1
  75. package/dist/translate/condition.d.ts.map +1 -1
  76. package/dist/translate/condition.js +4 -0
  77. package/dist/translate/condition.js.map +1 -1
  78. package/dist/validate.d.ts.map +1 -1
  79. package/dist/validate.js +13 -5
  80. package/dist/validate.js.map +1 -1
  81. package/package.json +4 -4
  82. package/schemas/$defs/common.schema.json +14 -0
  83. package/schemas/core/unit-composition.schema.json +5 -1
  84. package/schemas/core/unit.schema.json +2 -10
  85. package/schemas/core/wargear-option.schema.json +32 -6
  86. package/schemas/core/wargear.schema.json +24 -0
  87. package/schemas/enrichment/ability-dsl/condition.schema.json +3 -2
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Build wargear-option and wargear entities for one faction from army-assist's
3
+ * prose option table. Ties together the {@link parseOption} prose parser and the
4
+ * numeric→UUID {@link bridgeOptionsToUnits} bridge, then resolves the parsed
5
+ * weapon/wargear *names* to entity ids against the faction's own weapon registry
6
+ * (a name that is not a known weapon becomes a non-weapon `wargear` item).
7
+ *
8
+ * Anything that does not resolve cleanly — an unbridged datasheet, a prose line
9
+ * the parser rejects, an ambiguous unit match — is collected in `unparsed` for
10
+ * the caller to write to a report rather than dropped silently or guessed.
11
+ */
12
+ import { nameToId } from "./id-generator.js";
13
+ import { parseOption } from "./option-parser.js";
14
+ import { bridgeOptionsToUnits, modelNameFromComposition, normModelName, } from "./option-bridge.js";
15
+ /** Guess a coarse category for a non-weapon item from its name. */
16
+ function categorize(name) {
17
+ const n = name.toLowerCase();
18
+ if (n.includes("icon"))
19
+ return "icon";
20
+ if (n.includes("standard") || n.includes("banner"))
21
+ return "standard";
22
+ if (n.includes("token"))
23
+ return "token";
24
+ return undefined;
25
+ }
26
+ /** Drop empty fields from a parsed constraint; return undefined if nothing left. */
27
+ function cleanConstraint(c) {
28
+ const out = {};
29
+ if (c.model_name)
30
+ out.model_name = c.model_name;
31
+ if (c.per_n_models)
32
+ out.per_n_models = c.per_n_models;
33
+ if (c.max_count)
34
+ out.max_count = c.max_count;
35
+ if (c.any_number)
36
+ out.any_number = true;
37
+ return Object.keys(out).length > 0 ? out : undefined;
38
+ }
39
+ export function buildWargearOptions(factionDatasheets, allModels, allOptions, allComposition, unitWeaponIds, // UUID → weapon ids on that unit
40
+ globalWeaponIds, // every weapon id in the faction
41
+ gameVersion) {
42
+ const factionUuids = factionDatasheets.map((d) => d.id);
43
+ const factionUuidSet = new Set(factionUuids);
44
+ const nameByUuid = new Map(factionDatasheets.map((d) => [d.id, d.name]));
45
+ // UUID → normalized model-name set (numeric-side bridge target).
46
+ const modelsByUuid = new Map();
47
+ for (const m of allModels) {
48
+ if (!factionUuidSet.has(m.datasheet_id))
49
+ continue;
50
+ let set = modelsByUuid.get(m.datasheet_id);
51
+ if (!set)
52
+ modelsByUuid.set(m.datasheet_id, (set = new Set()));
53
+ set.add(normModelName(m.name));
54
+ }
55
+ // numeric → normalized model-name set (from composition descriptions).
56
+ const compByNumeric = new Map();
57
+ for (const c of allComposition) {
58
+ let set = compByNumeric.get(c.datasheet_id);
59
+ if (!set)
60
+ compByNumeric.set(c.datasheet_id, (set = new Set()));
61
+ set.add(modelNameFromComposition(c.description));
62
+ }
63
+ // numeric → its option rows.
64
+ const optionsByNumeric = new Map();
65
+ for (const o of allOptions) {
66
+ let rows = optionsByNumeric.get(o.datasheet_id);
67
+ if (!rows)
68
+ optionsByNumeric.set(o.datasheet_id, (rows = []));
69
+ rows.push(o);
70
+ }
71
+ const { byNumeric, ambiguous } = bridgeOptionsToUnits(factionUuids, modelsByUuid, compByNumeric, optionsByNumeric.keys());
72
+ // Shared units appear under several numeric ids (faction "views"); merge each
73
+ // UUID's option rows, deduped by description text, so we emit options once.
74
+ const rowsByUuid = new Map();
75
+ for (const [numericId, uuid] of byNumeric) {
76
+ const seen = new Set(rowsByUuid.get(uuid)?.map((r) => r.description ?? ""));
77
+ const rows = rowsByUuid.get(uuid) ?? [];
78
+ for (const r of optionsByNumeric.get(numericId) ?? []) {
79
+ const key = r.description ?? "";
80
+ if (seen.has(key))
81
+ continue;
82
+ seen.add(key);
83
+ rows.push(r);
84
+ }
85
+ rowsByUuid.set(uuid, rows);
86
+ }
87
+ const wargearOptions = [];
88
+ const wargearById = new Map();
89
+ const unparsed = [];
90
+ const MAX_LEN = 128; // entity-id / name schema maxLength
91
+ // Resolve a name to an id, queuing any new non-weapon item into `pending`
92
+ // (committed by the caller only once the whole option resolves, so a later
93
+ // failure can't leave an orphan wargear entity behind). Throws on a name that
94
+ // can't form a valid, in-bounds entity — the caller reports it.
95
+ const resolveName = (name, weaponIds, pending) => {
96
+ if (name.length > MAX_LEN)
97
+ throw new Error(`name too long: ${name.slice(0, 40)}…`);
98
+ const id = nameToId(name); // throws on a name with no id-safe characters
99
+ if (id.length > MAX_LEN)
100
+ throw new Error(`id too long: ${id.slice(0, 40)}…`);
101
+ if (weaponIds.has(id) || globalWeaponIds.has(id))
102
+ return id;
103
+ if (!wargearById.has(id) && !pending.has(id)) {
104
+ const entity = { id, name, game_version: gameVersion };
105
+ const category = categorize(name);
106
+ if (category)
107
+ entity.category = category;
108
+ pending.set(id, entity);
109
+ }
110
+ return id;
111
+ };
112
+ for (const [uuid, rows] of rowsByUuid) {
113
+ const unitName = nameByUuid.get(uuid);
114
+ const unitId = nameToId(unitName);
115
+ const weaponIds = unitWeaponIds.get(uuid) ?? new Set();
116
+ for (const row of rows.sort((a, b) => Number(a.line) - Number(b.line))) {
117
+ const result = parseOption(row.description);
118
+ if (result.ok === "skip")
119
+ continue;
120
+ if (result.ok === false) {
121
+ unparsed.push({
122
+ unit_id: unitId,
123
+ datasheet: unitName,
124
+ line: row.line,
125
+ description: row.description,
126
+ reason: result.reason,
127
+ });
128
+ continue;
129
+ }
130
+ const o = result.option;
131
+ const entity = {
132
+ id: `${unitId}-${o.kind}-${row.line}`,
133
+ unit_id: unitId,
134
+ is_free: true,
135
+ game_version: gameVersion,
136
+ };
137
+ const constraint = cleanConstraint(o.constraint);
138
+ if (constraint)
139
+ entity.model_constraint = constraint;
140
+ const pending = new Map();
141
+ try {
142
+ if (o.replaces.length > 0) {
143
+ entity.replaces = o.replaces.map((n) => resolveName(n, weaponIds, pending));
144
+ }
145
+ if (o.replacement) {
146
+ entity.replacement = o.replacement.map((n) => resolveName(n, weaponIds, pending));
147
+ }
148
+ if (o.replacement_choice) {
149
+ entity.replacement_choice = o.replacement_choice.map((g) => g.map((n) => resolveName(n, weaponIds, pending)));
150
+ }
151
+ }
152
+ catch (err) {
153
+ // A name that can't form a valid, in-bounds entity id (stray
154
+ // punctuation, an over-captured clause): report it rather than abort the
155
+ // faction, and discard any pending wargear so no orphan is emitted.
156
+ unparsed.push({
157
+ unit_id: unitId,
158
+ datasheet: unitName,
159
+ line: row.line,
160
+ description: row.description,
161
+ reason: `name resolution failed: ${err.message}`,
162
+ });
163
+ continue;
164
+ }
165
+ for (const [id, w] of pending)
166
+ wargearById.set(id, w);
167
+ wargearOptions.push(entity);
168
+ }
169
+ }
170
+ for (const numericId of ambiguous) {
171
+ for (const row of optionsByNumeric.get(numericId) ?? []) {
172
+ unparsed.push({
173
+ unit_id: null,
174
+ datasheet: `numeric:${numericId}`,
175
+ line: row.line,
176
+ description: row.description,
177
+ reason: "ambiguous unit match (model names tie across datasheets)",
178
+ });
179
+ }
180
+ }
181
+ return {
182
+ wargearOptions,
183
+ wargear: [...wargearById.values()],
184
+ unparsed,
185
+ };
186
+ }
187
+ //# sourceMappingURL=wargear-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wargear-options.js","sourceRoot":"","sources":["../../src/converters/wargear-options.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Build wargear-option and wargear entities for one faction from army-assist's\n * prose option table. Ties together the {@link parseOption} prose parser and the\n * numeric→UUID {@link bridgeOptionsToUnits} bridge, then resolves the parsed\n * weapon/wargear *names* to entity ids against the faction's own weapon registry\n * (a name that is not a known weapon becomes a non-weapon `wargear` item).\n *\n * Anything that does not resolve cleanly — an unbridged datasheet, a prose line\n * the parser rejects, an ambiguous unit match — is collected in `unparsed` for\n * the caller to write to a report rather than dropped silently or guessed.\n */\nimport { nameToId } from \"./id-generator.js\";\nimport { parseOption, type ParsedConstraint } from \"./option-parser.js\";\nimport {\n bridgeOptionsToUnits,\n modelNameFromComposition,\n normModelName,\n} from \"./option-bridge.js\";\n\ninterface SourceOption {\n datasheet_id: string; // numeric\n line: string;\n description: string | null;\n}\ninterface SourceComposition {\n datasheet_id: string; // numeric\n line: string;\n description: string;\n}\ninterface SourceModel {\n datasheet_id: string; // UUID\n name: string;\n}\n\ninterface GameVersion {\n edition: string;\n dataslate: string;\n}\n\nexport interface WargearEntity {\n id: string;\n name: string;\n category?: string;\n game_version: GameVersion;\n}\n\nexport interface WargearOptionEntity {\n id: string;\n unit_id: string;\n model_constraint?: ParsedConstraint;\n replaces?: string[];\n replacement?: string[];\n replacement_choice?: string[][];\n is_free: boolean;\n game_version: GameVersion;\n}\n\nexport interface UnparsedOption {\n unit_id: string | null;\n datasheet: string;\n line: string;\n description: string | null;\n reason: string;\n}\n\nexport interface BuildWargearResult {\n wargearOptions: WargearOptionEntity[];\n wargear: WargearEntity[];\n unparsed: UnparsedOption[];\n}\n\n/** Guess a coarse category for a non-weapon item from its name. */\nfunction categorize(name: string): string | undefined {\n const n = name.toLowerCase();\n if (n.includes(\"icon\")) return \"icon\";\n if (n.includes(\"standard\") || n.includes(\"banner\")) return \"standard\";\n if (n.includes(\"token\")) return \"token\";\n return undefined;\n}\n\n/** Drop empty fields from a parsed constraint; return undefined if nothing left. */\nfunction cleanConstraint(c: ParsedConstraint): ParsedConstraint | undefined {\n const out: ParsedConstraint = {};\n if (c.model_name) out.model_name = c.model_name;\n if (c.per_n_models) out.per_n_models = c.per_n_models;\n if (c.max_count) out.max_count = c.max_count;\n if (c.any_number) out.any_number = true;\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\nexport function buildWargearOptions(\n factionDatasheets: readonly { id: string; name: string }[],\n allModels: readonly SourceModel[],\n allOptions: readonly SourceOption[],\n allComposition: readonly SourceComposition[],\n unitWeaponIds: Map<string, Set<string>>, // UUID → weapon ids on that unit\n globalWeaponIds: Set<string>, // every weapon id in the faction\n gameVersion: GameVersion,\n): BuildWargearResult {\n const factionUuids = factionDatasheets.map((d) => d.id);\n const factionUuidSet = new Set(factionUuids);\n const nameByUuid = new Map(factionDatasheets.map((d) => [d.id, d.name]));\n\n // UUID → normalized model-name set (numeric-side bridge target).\n const modelsByUuid = new Map<string, Set<string>>();\n for (const m of allModels) {\n if (!factionUuidSet.has(m.datasheet_id)) continue;\n let set = modelsByUuid.get(m.datasheet_id);\n if (!set) modelsByUuid.set(m.datasheet_id, (set = new Set()));\n set.add(normModelName(m.name));\n }\n\n // numeric → normalized model-name set (from composition descriptions).\n const compByNumeric = new Map<string, Set<string>>();\n for (const c of allComposition) {\n let set = compByNumeric.get(c.datasheet_id);\n if (!set) compByNumeric.set(c.datasheet_id, (set = new Set()));\n set.add(modelNameFromComposition(c.description));\n }\n\n // numeric → its option rows.\n const optionsByNumeric = new Map<string, SourceOption[]>();\n for (const o of allOptions) {\n let rows = optionsByNumeric.get(o.datasheet_id);\n if (!rows) optionsByNumeric.set(o.datasheet_id, (rows = []));\n rows.push(o);\n }\n\n const { byNumeric, ambiguous } = bridgeOptionsToUnits(\n factionUuids,\n modelsByUuid,\n compByNumeric,\n optionsByNumeric.keys(),\n );\n\n // Shared units appear under several numeric ids (faction \"views\"); merge each\n // UUID's option rows, deduped by description text, so we emit options once.\n const rowsByUuid = new Map<string, SourceOption[]>();\n for (const [numericId, uuid] of byNumeric) {\n const seen = new Set(rowsByUuid.get(uuid)?.map((r) => r.description ?? \"\"));\n const rows = rowsByUuid.get(uuid) ?? [];\n for (const r of optionsByNumeric.get(numericId) ?? []) {\n const key = r.description ?? \"\";\n if (seen.has(key)) continue;\n seen.add(key);\n rows.push(r);\n }\n rowsByUuid.set(uuid, rows);\n }\n\n const wargearOptions: WargearOptionEntity[] = [];\n const wargearById = new Map<string, WargearEntity>();\n const unparsed: UnparsedOption[] = [];\n\n const MAX_LEN = 128; // entity-id / name schema maxLength\n // Resolve a name to an id, queuing any new non-weapon item into `pending`\n // (committed by the caller only once the whole option resolves, so a later\n // failure can't leave an orphan wargear entity behind). Throws on a name that\n // can't form a valid, in-bounds entity — the caller reports it.\n const resolveName = (\n name: string,\n weaponIds: Set<string>,\n pending: Map<string, WargearEntity>,\n ): string => {\n if (name.length > MAX_LEN) throw new Error(`name too long: ${name.slice(0, 40)}…`);\n const id = nameToId(name); // throws on a name with no id-safe characters\n if (id.length > MAX_LEN) throw new Error(`id too long: ${id.slice(0, 40)}…`);\n if (weaponIds.has(id) || globalWeaponIds.has(id)) return id;\n if (!wargearById.has(id) && !pending.has(id)) {\n const entity: WargearEntity = { id, name, game_version: gameVersion };\n const category = categorize(name);\n if (category) entity.category = category;\n pending.set(id, entity);\n }\n return id;\n };\n\n for (const [uuid, rows] of rowsByUuid) {\n const unitName = nameByUuid.get(uuid)!;\n const unitId = nameToId(unitName);\n const weaponIds = unitWeaponIds.get(uuid) ?? new Set<string>();\n\n for (const row of rows.sort((a, b) => Number(a.line) - Number(b.line))) {\n const result = parseOption(row.description);\n if (result.ok === \"skip\") continue;\n if (result.ok === false) {\n unparsed.push({\n unit_id: unitId,\n datasheet: unitName,\n line: row.line,\n description: row.description,\n reason: result.reason,\n });\n continue;\n }\n const o = result.option;\n const entity: WargearOptionEntity = {\n id: `${unitId}-${o.kind}-${row.line}`,\n unit_id: unitId,\n is_free: true,\n game_version: gameVersion,\n };\n const constraint = cleanConstraint(o.constraint);\n if (constraint) entity.model_constraint = constraint;\n const pending = new Map<string, WargearEntity>();\n try {\n if (o.replaces.length > 0) {\n entity.replaces = o.replaces.map((n) => resolveName(n, weaponIds, pending));\n }\n if (o.replacement) {\n entity.replacement = o.replacement.map((n) => resolveName(n, weaponIds, pending));\n }\n if (o.replacement_choice) {\n entity.replacement_choice = o.replacement_choice.map((g) =>\n g.map((n) => resolveName(n, weaponIds, pending)),\n );\n }\n } catch (err) {\n // A name that can't form a valid, in-bounds entity id (stray\n // punctuation, an over-captured clause): report it rather than abort the\n // faction, and discard any pending wargear so no orphan is emitted.\n unparsed.push({\n unit_id: unitId,\n datasheet: unitName,\n line: row.line,\n description: row.description,\n reason: `name resolution failed: ${(err as Error).message}`,\n });\n continue;\n }\n for (const [id, w] of pending) wargearById.set(id, w);\n wargearOptions.push(entity);\n }\n }\n\n for (const numericId of ambiguous) {\n for (const row of optionsByNumeric.get(numericId) ?? []) {\n unparsed.push({\n unit_id: null,\n datasheet: `numeric:${numericId}`,\n line: row.line,\n description: row.description,\n reason: \"ambiguous unit match (model names tie across datasheets)\",\n });\n }\n }\n\n return {\n wargearOptions,\n wargear: [...wargearById.values()],\n unparsed,\n };\n}\n"]}