@alpaca-software/40kdc-data 0.5.11 → 0.5.14
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 +10 -0
- package/dist/author-input-pack.d.ts +27 -0
- package/dist/author-input-pack.d.ts.map +1 -0
- package/dist/author-input-pack.js +198 -0
- package/dist/author-input-pack.js.map +1 -0
- package/dist/codegen-data.js +1 -0
- package/dist/codegen-data.js.map +1 -1
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +3 -1
- package/dist/commands/import.js.map +1 -1
- package/dist/convert-faction.d.ts.map +1 -1
- package/dist/convert-faction.js +22 -2
- package/dist/convert-faction.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/dataset.d.ts +2 -1
- package/dist/data/dataset.d.ts.map +1 -1
- package/dist/data/dataset.js +2 -0
- package/dist/data/dataset.js.map +1 -1
- package/dist/data/entities.d.ts +22 -0
- package/dist/data/entities.d.ts.map +1 -1
- package/dist/data/entities.js +28 -0
- package/dist/data/entities.js.map +1 -1
- package/dist/data/index.d.ts +3 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +3 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/roster-resolve.d.ts +16 -1
- package/dist/data/roster-resolve.d.ts.map +1 -1
- package/dist/data/roster-resolve.js +19 -0
- package/dist/data/roster-resolve.js.map +1 -1
- package/dist/data/types.d.ts +3 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/data/types.js +1 -0
- package/dist/data/types.js.map +1 -1
- package/dist/export/newrecruit-json.d.ts.map +1 -1
- package/dist/export/newrecruit-json.js +4 -3
- package/dist/export/newrecruit-json.js.map +1 -1
- package/dist/export/newrecruit-simple.js +2 -2
- package/dist/export/newrecruit-simple.js.map +1 -1
- package/dist/export/newrecruit-wtc.d.ts.map +1 -1
- package/dist/export/newrecruit-wtc.js +3 -1
- package/dist/export/newrecruit-wtc.js.map +1 -1
- package/dist/export/rosterizer.d.ts.map +1 -1
- package/dist/export/rosterizer.js +6 -7
- package/dist/export/rosterizer.js.map +1 -1
- package/dist/extract-faction-pack.d.ts +61 -0
- package/dist/extract-faction-pack.d.ts.map +1 -0
- package/dist/extract-faction-pack.js +318 -0
- package/dist/extract-faction-pack.js.map +1 -0
- package/dist/gen-conformance.js +37 -4
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +46 -2
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/import/gw-headerless.d.ts +70 -0
- package/dist/import/gw-headerless.d.ts.map +1 -0
- package/dist/import/gw-headerless.js +343 -0
- package/dist/import/gw-headerless.js.map +1 -0
- package/dist/import/gw.js +1 -1
- package/dist/import/gw.js.map +1 -1
- package/dist/import/import-roster.d.ts.map +1 -1
- package/dist/import/import-roster.js +8 -0
- package/dist/import/import-roster.js.map +1 -1
- package/dist/import/listforge-text.js +1 -1
- package/dist/import/listforge-text.js.map +1 -1
- package/dist/import/listforge.d.ts.map +1 -1
- package/dist/import/listforge.js +18 -3
- package/dist/import/listforge.js.map +1 -1
- package/dist/import/newrecruit-json.d.ts.map +1 -1
- package/dist/import/newrecruit-json.js +21 -3
- package/dist/import/newrecruit-json.js.map +1 -1
- package/dist/import/newrecruit-simple.js +1 -1
- package/dist/import/newrecruit-simple.js.map +1 -1
- package/dist/import/newrecruit-wtc.js +1 -1
- package/dist/import/newrecruit-wtc.js.map +1 -1
- package/dist/import/resolve.d.ts.map +1 -1
- package/dist/import/resolve.js +46 -18
- package/dist/import/resolve.js.map +1 -1
- package/dist/import/roster-json.d.ts +5 -3
- package/dist/import/roster-json.d.ts.map +1 -1
- package/dist/import/roster-json.js +8 -4
- package/dist/import/roster-json.js.map +1 -1
- package/dist/import/rosterizer.js +3 -3
- package/dist/import/rosterizer.js.map +1 -1
- package/dist/import/types.d.ts +15 -4
- package/dist/import/types.d.ts.map +1 -1
- package/dist/import/types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/merge-faction-pack.d.ts +2 -0
- package/dist/merge-faction-pack.d.ts.map +1 -0
- package/dist/merge-faction-pack.js +157 -0
- package/dist/merge-faction-pack.js.map +1 -0
- package/dist/pack-blocks.d.ts +32 -0
- package/dist/pack-blocks.d.ts.map +1 -0
- package/dist/pack-blocks.js +99 -0
- package/dist/pack-blocks.js.map +1 -0
- package/dist/rube-goldberg.js +1 -1
- package/dist/rube-goldberg.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +33 -1
- package/dist/runner.js.map +1 -1
- package/dist/scope.d.ts +31 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +34 -0
- package/dist/scope.js.map +1 -0
- package/dist/seed-detachment-abilities.d.ts +2 -0
- package/dist/seed-detachment-abilities.d.ts.map +1 -0
- package/dist/seed-detachment-abilities.js +137 -0
- package/dist/seed-detachment-abilities.js.map +1 -0
- package/dist/translate/effect.d.ts +17 -3
- package/dist/translate/effect.d.ts.map +1 -1
- package/dist/translate/effect.js +23 -4
- package/dist/translate/effect.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 +1 -1
- package/dist/translate/index.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +1 -0
- package/dist/validate.js.map +1 -1
- package/package.json +3 -1
- package/schemas/core/detachment.schema.json +6 -0
- package/schemas/core/hull-shape.schema.json +30 -0
- package/schemas/core/roster.schema.json +24 -8
- package/schemas/core/stratagem.schema.json +2 -2
- package/schemas/core/unit-composition.schema.json +7 -0
- package/schemas/enrichment/ability-dsl/ability.schema.json +14 -0
package/dist/import/types.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
/** A 40kdc battle size (mirrors the shared `battle-size` def). */
|
|
20
20
|
export type BattleSize = "incursion" | "strike-force";
|
|
21
21
|
/** Diagnostic warning codes emitted during an import. */
|
|
22
|
-
export type WarningCode = "faction-unresolved" | "unit-unresolved" | "weapon-unresolved" | "enhancement-unresolved" | "detachment-unresolved" | "battle-size-unmapped" | "points-mismatch" | "leader-attachment-inferred" | "multi-force" | "unknown-field";
|
|
22
|
+
export type WarningCode = "faction-unresolved" | "unit-unresolved" | "weapon-unresolved" | "enhancement-unresolved" | "detachment-unresolved" | "detachment-points-exceeded" | "battle-size-unmapped" | "points-mismatch" | "leader-attachment-inferred" | "multi-force" | "unknown-field";
|
|
23
23
|
/** A near-match suggestion offered when resolution fails. */
|
|
24
24
|
export interface Candidate {
|
|
25
25
|
id: string;
|
|
@@ -44,6 +44,12 @@ export interface RosterWargear {
|
|
|
44
44
|
ref: ResolvedRef;
|
|
45
45
|
count: number;
|
|
46
46
|
}
|
|
47
|
+
/** One detachment on the roster, paired with its resolved DP cost. */
|
|
48
|
+
export interface RosterDetachment {
|
|
49
|
+
ref: ResolvedRef;
|
|
50
|
+
/** DP cost (1–3) from the resolved detachment entity; null when unresolved or unrecorded. */
|
|
51
|
+
dp_cost: number | null;
|
|
52
|
+
}
|
|
47
53
|
/** An inferred, always-provisional leader→bodyguard attachment. */
|
|
48
54
|
export interface RosterLeaderAttachment {
|
|
49
55
|
bodyguard_ref: ResolvedRef;
|
|
@@ -73,6 +79,8 @@ export interface RosterSource {
|
|
|
73
79
|
/** Point totals; reported and computed are kept distinct, never reconciled. */
|
|
74
80
|
export interface RosterPoints {
|
|
75
81
|
declared_limit: number | null;
|
|
82
|
+
/** 11e detachment-point budget from the battle size (strike-force 3, incursion 2); null when unknown. */
|
|
83
|
+
detachment_cap: number | null;
|
|
76
84
|
total_reported: number | null;
|
|
77
85
|
total_computed: number;
|
|
78
86
|
}
|
|
@@ -100,7 +108,7 @@ export interface Roster {
|
|
|
100
108
|
name: string;
|
|
101
109
|
source: RosterSource;
|
|
102
110
|
faction_id: string | null;
|
|
103
|
-
|
|
111
|
+
detachments: RosterDetachment[];
|
|
104
112
|
battle_size: BattleSize | null;
|
|
105
113
|
points: RosterPoints;
|
|
106
114
|
units: RosterUnit[];
|
|
@@ -136,8 +144,11 @@ export interface ParsedRoster {
|
|
|
136
144
|
generated_by: string | null;
|
|
137
145
|
/** Raw faction name from the source (e.g. "Grey Knights"). */
|
|
138
146
|
faction_raw_name: string | null;
|
|
139
|
-
/**
|
|
140
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Raw detachment names in source order (e.g. ["Gladius Task Force"]). 11e lists may
|
|
149
|
+
* carry several; most formats and pre-11e lists carry zero or one.
|
|
150
|
+
*/
|
|
151
|
+
detachment_raw_names: string[];
|
|
141
152
|
/** Raw battle-size label (e.g. "2. Strike Force (2000 Point limit)"). */
|
|
142
153
|
battle_size_raw: string | null;
|
|
143
154
|
/** Points limit parsed from the battle-size label, if any. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,kEAAkE;AAClE,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,cAAc,CAAC;AAEtD,yDAAyD;AACzD,MAAM,MAAM,WAAW,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,mBAAmB,GACnB,wBAAwB,GACxB,uBAAuB,GACvB,sBAAsB,GACtB,iBAAiB,GACjB,4BAA4B,GAC5B,aAAa,GACb,eAAe,CAAC;AAMpB,6DAA6D;AAC7D,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,8DAA8D;IAC9D,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,WAAW,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mEAAmE;AACnE,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,WAAW,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,kCAAkC;AAClC,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,WAAW,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,mFAAmF;IACnF,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,iBAAiB,EAAE,sBAAsB,GAAG,IAAI,CAAC;CAClD;AAED;uEACuE;AACvE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,gBAAgB,GAChB,iBAAiB,GACjB,wBAAwB,GACxB,qBAAqB,GACrB,mBAAmB,GACnB,YAAY,GACZ,IAAI,GACJ,aAAa,CAAC;AAElB,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,mCAAmC;AACnC,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"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"}
|
package/dist/import/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG","sourcesContent":["/**\n * Types for the army-list importer.\n *\n * Two layers live here:\n * - The **output** types ({@link Roster} and friends) mirror\n * `schemas/core/roster.schema.json` field-for-field. They are hand-authored\n * rather than generated so importer work isn't gated on the Rust→typify codegen\n * round-trip; the AJV validator (against the real schema) is the source of truth\n * for conformance.\n * - The **intermediate** type ({@link ParsedRoster}) is format-agnostic: a parser\n * adapter lowers a source payload to this shape (raw names + counts only, no\n * resolved ids), and {@link resolve} turns it into a {@link Roster}.\n *\n * Nothing here ever carries reproduced rules or ability text — only permitted\n * facts (names, counts, points, keywords, entity ids).\n *\n * @packageDocumentation\n */\n\n/** A 40kdc battle size (mirrors the shared `battle-size` def). */\nexport type BattleSize = \"incursion\" | \"strike-force\";\n\n/** Diagnostic warning codes emitted during an import. */\nexport type WarningCode =\n | \"faction-unresolved\"\n | \"unit-unresolved\"\n | \"weapon-unresolved\"\n | \"enhancement-unresolved\"\n | \"detachment-unresolved\"\n | \"battle-size-unmapped\"\n | \"points-mismatch\"\n | \"leader-attachment-inferred\"\n | \"multi-force\"\n | \"unknown-field\";\n\n// ---------------------------------------------------------------------------\n// Output types (mirror roster.schema.json)\n// ---------------------------------------------------------------------------\n\n/** A near-match suggestion offered when resolution fails. */\nexport interface Candidate {\n id: string;\n name: string;\n}\n\n/**\n * A reference to a 40kdc entity that may or may not have resolved. Retains the\n * source's raw name so the import is lossless even on a miss.\n */\nexport interface ResolvedRef {\n /** Resolved entity id, or null when no match was found. */\n id: string | null;\n /** The display name exactly as it appeared in the source payload. */\n raw_name: string;\n /** True iff {@link id} is non-null. */\n resolved: boolean;\n /** Up to 5 best-guess alternatives when resolution failed. */\n candidates: Candidate[];\n}\n\n/** A weapon/wargear selection on a unit. */\nexport interface RosterWargear {\n ref: ResolvedRef;\n count: number;\n}\n\n/** An inferred, always-provisional leader→bodyguard attachment. */\nexport interface RosterLeaderAttachment {\n bodyguard_ref: ResolvedRef;\n provisional: boolean;\n}\n\n/** One unit entry in a roster. */\nexport interface RosterUnit {\n ref: ResolvedRef;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement: ResolvedRef | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: RosterWargear[];\n leader_attachment: RosterLeaderAttachment | null;\n}\n\n/** Identifier for the adapter that produced this roster. New format adapters\n * extend this union; `roster.schema.json` keeps the canonical enum. */\nexport type RosterFormat =\n | \"listforge\"\n | \"listforge-text\"\n | \"newrecruit-json\"\n | \"newrecruit-wtc-compact\"\n | \"newrecruit-wtc-full\"\n | \"newrecruit-simple\"\n | \"rosterizer\"\n | \"gw\"\n | \"roster-json\";\n\n/** Provenance of the imported list. */\nexport interface RosterSource {\n format: RosterFormat;\n generated_by: string | null;\n}\n\n/** Point totals; reported and computed are kept distinct, never reconciled. */\nexport interface RosterPoints {\n declared_limit: number | null;\n total_reported: number | null;\n total_computed: number;\n}\n\n/** A single diagnostic warning. */\nexport interface Warning {\n code: WarningCode;\n message: string;\n raw_name: string | null;\n}\n\n/** A summary of what resolved and what did not during the import. */\nexport interface Diagnostics {\n resolved_units: number;\n unresolved_units: number;\n resolved_weapons: number;\n unresolved_weapons: number;\n warnings: Warning[];\n}\n\n/** Reference to the game edition + dataslate (mirrors game-version-ref). */\nexport interface GameVersionRef {\n edition: string;\n dataslate: string;\n}\n\n/** A fully-resolved army list. Validates against `roster.schema.json`. */\nexport interface Roster {\n name: string;\n source: RosterSource;\n faction_id: string | null;\n
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/import/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG","sourcesContent":["/**\n * Types for the army-list importer.\n *\n * Two layers live here:\n * - The **output** types ({@link Roster} and friends) mirror\n * `schemas/core/roster.schema.json` field-for-field. They are hand-authored\n * rather than generated so importer work isn't gated on the Rust→typify codegen\n * round-trip; the AJV validator (against the real schema) is the source of truth\n * for conformance.\n * - The **intermediate** type ({@link ParsedRoster}) is format-agnostic: a parser\n * adapter lowers a source payload to this shape (raw names + counts only, no\n * resolved ids), and {@link resolve} turns it into a {@link Roster}.\n *\n * Nothing here ever carries reproduced rules or ability text — only permitted\n * facts (names, counts, points, keywords, entity ids).\n *\n * @packageDocumentation\n */\n\n/** A 40kdc battle size (mirrors the shared `battle-size` def). */\nexport type BattleSize = \"incursion\" | \"strike-force\";\n\n/** Diagnostic warning codes emitted during an import. */\nexport type WarningCode =\n | \"faction-unresolved\"\n | \"unit-unresolved\"\n | \"weapon-unresolved\"\n | \"enhancement-unresolved\"\n | \"detachment-unresolved\"\n | \"detachment-points-exceeded\"\n | \"battle-size-unmapped\"\n | \"points-mismatch\"\n | \"leader-attachment-inferred\"\n | \"multi-force\"\n | \"unknown-field\";\n\n// ---------------------------------------------------------------------------\n// Output types (mirror roster.schema.json)\n// ---------------------------------------------------------------------------\n\n/** A near-match suggestion offered when resolution fails. */\nexport interface Candidate {\n id: string;\n name: string;\n}\n\n/**\n * A reference to a 40kdc entity that may or may not have resolved. Retains the\n * source's raw name so the import is lossless even on a miss.\n */\nexport interface ResolvedRef {\n /** Resolved entity id, or null when no match was found. */\n id: string | null;\n /** The display name exactly as it appeared in the source payload. */\n raw_name: string;\n /** True iff {@link id} is non-null. */\n resolved: boolean;\n /** Up to 5 best-guess alternatives when resolution failed. */\n candidates: Candidate[];\n}\n\n/** A weapon/wargear selection on a unit. */\nexport interface RosterWargear {\n ref: ResolvedRef;\n count: number;\n}\n\n/** One detachment on the roster, paired with its resolved DP cost. */\nexport interface RosterDetachment {\n ref: ResolvedRef;\n /** DP cost (1–3) from the resolved detachment entity; null when unresolved or unrecorded. */\n dp_cost: number | null;\n}\n\n/** An inferred, always-provisional leader→bodyguard attachment. */\nexport interface RosterLeaderAttachment {\n bodyguard_ref: ResolvedRef;\n provisional: boolean;\n}\n\n/** One unit entry in a roster. */\nexport interface RosterUnit {\n ref: ResolvedRef;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement: ResolvedRef | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: RosterWargear[];\n leader_attachment: RosterLeaderAttachment | null;\n}\n\n/** Identifier for the adapter that produced this roster. New format adapters\n * extend this union; `roster.schema.json` keeps the canonical enum. */\nexport type RosterFormat =\n | \"listforge\"\n | \"listforge-text\"\n | \"newrecruit-json\"\n | \"newrecruit-wtc-compact\"\n | \"newrecruit-wtc-full\"\n | \"newrecruit-simple\"\n | \"rosterizer\"\n | \"gw\"\n | \"roster-json\";\n\n/** Provenance of the imported list. */\nexport interface RosterSource {\n format: RosterFormat;\n generated_by: string | null;\n}\n\n/** Point totals; reported and computed are kept distinct, never reconciled. */\nexport interface RosterPoints {\n declared_limit: number | null;\n /** 11e detachment-point budget from the battle size (strike-force 3, incursion 2); null when unknown. */\n detachment_cap: number | null;\n total_reported: number | null;\n total_computed: number;\n}\n\n/** A single diagnostic warning. */\nexport interface Warning {\n code: WarningCode;\n message: string;\n raw_name: string | null;\n}\n\n/** A summary of what resolved and what did not during the import. */\nexport interface Diagnostics {\n resolved_units: number;\n unresolved_units: number;\n resolved_weapons: number;\n unresolved_weapons: number;\n warnings: Warning[];\n}\n\n/** Reference to the game edition + dataslate (mirrors game-version-ref). */\nexport interface GameVersionRef {\n edition: string;\n dataslate: string;\n}\n\n/** A fully-resolved army list. Validates against `roster.schema.json`. */\nexport interface Roster {\n name: string;\n source: RosterSource;\n faction_id: string | null;\n detachments: RosterDetachment[];\n battle_size: BattleSize | null;\n points: RosterPoints;\n units: RosterUnit[];\n game_version: GameVersionRef;\n diagnostics: Diagnostics;\n}\n\n// ---------------------------------------------------------------------------\n// Intermediate types (format-agnostic; produced by a parser adapter)\n// ---------------------------------------------------------------------------\n\n/** A weapon/wargear selection before id resolution. */\nexport interface ParsedWargear {\n raw_name: string;\n count: number;\n}\n\n/** A unit selection before id resolution. */\nexport interface ParsedUnit {\n raw_name: string;\n /** True when the source classifies this as a character/leader-capable model. */\n is_character: boolean;\n model_count: number;\n /** Base unit cost (without the enhancement). */\n points: number | null;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Points cost of the enhancement when the source reported one; null otherwise. */\n enhancement_points: number | null;\n wargear: ParsedWargear[];\n}\n\n/**\n * The format-agnostic intermediate. A {@link FormatAdapter} produces this from a\n * decoded source payload; {@link resolve} consumes it. Contains only raw display\n * names and counts — never reproduced rules text.\n */\nexport interface ParsedRoster {\n name: string;\n generated_by: string | null;\n /** Raw faction name from the source (e.g. \"Grey Knights\"). */\n faction_raw_name: string | null;\n /**\n * Raw detachment names in source order (e.g. [\"Gladius Task Force\"]). 11e lists may\n * carry several; most formats and pre-11e lists carry zero or one.\n */\n detachment_raw_names: string[];\n /** Raw battle-size label (e.g. \"2. Strike Force (2000 Point limit)\"). */\n battle_size_raw: string | null;\n /** Points limit parsed from the battle-size label, if any. */\n declared_limit: number | null;\n /** Total points reported by the source cost block. */\n total_reported: number | null;\n /** Points summed from every cost line in the source tree. */\n total_computed: number;\n units: ParsedUnit[];\n /** True when the source contained more than one distinct faction. */\n multi_force: boolean;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type { ResolvedPiece, ResolvedVec2, BoardEdge, FeatureRef, Keystone, Keys
|
|
|
8
8
|
export * from "./scoring/index.js";
|
|
9
9
|
export * from "./cruncher/index.js";
|
|
10
10
|
export * from "./compare.js";
|
|
11
|
+
export * from "./scope.js";
|
|
11
12
|
export { importListForge, importNewRecruit, importRoster, tryImportRoster, decodeListForge, } from "./import/index.js";
|
|
12
13
|
export { exportRoster, newRecruitJsonSerializer, newRecruitSimpleSerializer, newRecruitWtcCompactSerializer, newRecruitWtcFullSerializer, rosterJsonSerializer, rosterizerSerializer, } from "./export/index.js";
|
|
13
14
|
export type { ExportFormat, RosterSerializer } from "./export/index.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAC;AAGhC,cAAc,gBAAgB,CAAC;AAI/B,cAAc,sBAAsB,CAAC;AAKrC,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKnD,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,SAAS,EACT,UAAU,EACV,QAAQ,EACR,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAK5B,cAAc,oBAAoB,CAAC;AAKnC,cAAc,qBAAqB,CAAC;AAKpC,cAAc,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,iBAAiB,CAAC;AAGhC,cAAc,gBAAgB,CAAC;AAI/B,cAAc,sBAAsB,CAAC;AAKrC,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAIrD,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKnD,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,SAAS,EACT,UAAU,EACV,QAAQ,EACR,mBAAmB,EACnB,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,GACZ,MAAM,oBAAoB,CAAC;AAK5B,cAAc,oBAAoB,CAAC;AAKnC,cAAc,qBAAqB,CAAC;AAKpC,cAAc,cAAc,CAAC;AAK7B,cAAc,YAAY,CAAC;AAU3B,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EACV,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,MAAM,EACN,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,WAAW,EACX,SAAS,EACT,WAAW,EACX,OAAO,EACP,WAAW,EACX,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,10 @@ export * from "./cruncher/index.js";
|
|
|
21
21
|
// built on the cruncher. Mirrors `python/src/wh40kdc/compare.py`; the per-cell
|
|
22
22
|
// math is pinned against Python by the `conformance/compare` corpus.
|
|
23
23
|
export * from "./compare.js";
|
|
24
|
+
// Roster-highlighting scope: resolve which units an ability's curated
|
|
25
|
+
// `applies_to` keyword filter benefits. Mirrors `wh40kdc::scope` (Rust) and
|
|
26
|
+
// `wh40kdc.scope` (Python); pinned by the `conformance/applies-to` corpus.
|
|
27
|
+
export * from "./scope.js";
|
|
24
28
|
// Schema access + AJV validation lives behind the `./validate` subpath export
|
|
25
29
|
// (`@alpaca-software/40kdc-data/validate`), NOT the root barrel: it reads
|
|
26
30
|
// schema files from disk at module load (node:fs/node:url), which breaks
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,cAAc,iBAAiB,CAAC;AAEhC,mDAAmD;AACnD,cAAc,gBAAgB,CAAC;AAE/B,0EAA0E;AAC1E,6EAA6E;AAC7E,cAAc,sBAAsB,CAAC;AAWrC,+EAA+E;AAC/E,2EAA2E;AAC3E,8EAA8E;AAC9E,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAiB5B,6EAA6E;AAC7E,+EAA+E;AAC/E,yDAAyD;AACzD,cAAc,oBAAoB,CAAC;AAEnC,4EAA4E;AAC5E,oEAAoE;AACpE,8EAA8E;AAC9E,cAAc,qBAAqB,CAAC;AAEpC,gFAAgF;AAChF,+EAA+E;AAC/E,qEAAqE;AACrE,cAAc,cAAc,CAAC;AAE7B,8EAA8E;AAC9E,0EAA0E;AAC1E,yEAAyE;AACzE,qEAAqE;AAErE,4EAA4E;AAC5E,+EAA+E;AAC/E,uCAAuC;AACvC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC","sourcesContent":["// The linked, typed dataset — the primary entry point.\nexport * from \"./data/index.js\";\n\n// Generated types for every entity in the dataset.\nexport * from \"./generated.js\";\n\n// Plain-English translation of structured data (scoring-card awards + the\n// shared Ability-DSL condition humanizer). Cross-impl pinned by conformance.\nexport * from \"./translate/index.js\";\n\n// `ScoringTrigger` is emitted by both ./generated.js (schema-derived) and\n// ./translate (hand-authored). They are structurally identical; disambiguate the\n// two wildcard re-exports in favour of the generated, schema-canonical type.\nexport type { ScoringTrigger } from \"./generated.js\";\n\n// `AbilityScope` likewise: ./translate/effect.ts hand-authors a looser view\n// for the describer; prefer the generated, schema-canonical type.\nexport type { AbilityScope } from \"./generated.js\";\n\n// Terrain geometry resolver (template-anchored layout → board-space vertices).\n// Curated (not wildcard) so its internal type aliases don't clash with the\n// generated Footprint/TerrainTemplate/TerrainLayout types. Cross-impl pinned.\nexport {\n resolveLayout,\n polygonCentroid,\n footprintVertices,\n orientedOffsets,\n TerrainResolveError,\n solveCentroid,\n solveCentroidTriangulated,\n solveCentroidAttached,\n TerrainSolveError,\n keystoneMeasurements,\n BOARD_INCHES,\n TerrainKeystoneError,\n} from \"./terrain/index.js\";\nexport type {\n ResolvedPiece,\n ResolvedVec2,\n BoardEdge,\n FeatureRef,\n Keystone,\n KeystoneMeasurement,\n DimensionLine,\n SolveInput,\n TriangulationLine,\n TriangulateInput,\n AttachLine,\n AttachPiece,\n AttachInput,\n} from \"./terrain/index.js\";\n\n// Card-driven secondary-mission scoring: compute VP from asserted awards and\n// track per-round, per-player scoring. Mirrored by the Rust `wh40kdc::scoring`\n// module and pinned by the `conformance/scoring` corpus.\nexport * from \"./scoring/index.js\";\n\n// Damage-projection engine (expected-value math over weapon/unit profiles).\n// Mirrored by the Rust `wh40kdc::cruncher` module and pinned by the\n// `conformance/cruncher` corpus. Pure functions — universal (Node + browser).\nexport * from \"./cruncher/index.js\";\n\n// Fleet comparison (attacker set × target-profile set → expected-kills matrix),\n// built on the cruncher. Mirrors `python/src/wh40kdc/compare.py`; the per-cell\n// math is pinned against Python by the `conformance/compare` corpus.\nexport * from \"./compare.js\";\n\n// Schema access + AJV validation lives behind the `./validate` subpath export\n// (`@alpaca-software/40kdc-data/validate`), NOT the root barrel: it reads\n// schema files from disk at module load (node:fs/node:url), which breaks\n// browser bundles. The root barrel stays universal (Node + browser).\n\n// Army-list importer (ListForge → resolved 40kdc roster). Types are curated\n// rather than re-exported wholesale to avoid name clashes with generated types\n// (e.g. BattleSize, LeaderAttachment).\nexport {\n importListForge,\n importNewRecruit,\n importRoster,\n tryImportRoster,\n decodeListForge,\n} from \"./import/index.js\";\n\n// Army-list exporter (Roster → text or JSON for any of the supported formats).\nexport {\n exportRoster,\n newRecruitJsonSerializer,\n newRecruitSimpleSerializer,\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n rosterJsonSerializer,\n rosterizerSerializer,\n} from \"./export/index.js\";\nexport type { ExportFormat, RosterSerializer } from \"./export/index.js\";\nexport type { FormatAdapter } from \"./import/index.js\";\nexport type {\n ImportOptions,\n ImportResult,\n ImportFailureReason,\n AdapterTrial,\n Roster,\n RosterUnit,\n RosterWargear,\n RosterSource,\n RosterFormat,\n RosterPoints,\n RosterLeaderAttachment,\n ResolvedRef,\n Candidate,\n Diagnostics,\n Warning,\n WarningCode,\n ParsedRoster,\n ParsedUnit,\n ParsedWargear,\n} from \"./import/index.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,cAAc,iBAAiB,CAAC;AAEhC,mDAAmD;AACnD,cAAc,gBAAgB,CAAC;AAE/B,0EAA0E;AAC1E,6EAA6E;AAC7E,cAAc,sBAAsB,CAAC;AAWrC,+EAA+E;AAC/E,2EAA2E;AAC3E,8EAA8E;AAC9E,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAiB5B,6EAA6E;AAC7E,+EAA+E;AAC/E,yDAAyD;AACzD,cAAc,oBAAoB,CAAC;AAEnC,4EAA4E;AAC5E,oEAAoE;AACpE,8EAA8E;AAC9E,cAAc,qBAAqB,CAAC;AAEpC,gFAAgF;AAChF,+EAA+E;AAC/E,qEAAqE;AACrE,cAAc,cAAc,CAAC;AAE7B,sEAAsE;AACtE,4EAA4E;AAC5E,2EAA2E;AAC3E,cAAc,YAAY,CAAC;AAE3B,8EAA8E;AAC9E,0EAA0E;AAC1E,yEAAyE;AACzE,qEAAqE;AAErE,4EAA4E;AAC5E,+EAA+E;AAC/E,uCAAuC;AACvC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC","sourcesContent":["// The linked, typed dataset — the primary entry point.\nexport * from \"./data/index.js\";\n\n// Generated types for every entity in the dataset.\nexport * from \"./generated.js\";\n\n// Plain-English translation of structured data (scoring-card awards + the\n// shared Ability-DSL condition humanizer). Cross-impl pinned by conformance.\nexport * from \"./translate/index.js\";\n\n// `ScoringTrigger` is emitted by both ./generated.js (schema-derived) and\n// ./translate (hand-authored). They are structurally identical; disambiguate the\n// two wildcard re-exports in favour of the generated, schema-canonical type.\nexport type { ScoringTrigger } from \"./generated.js\";\n\n// `AbilityScope` likewise: ./translate/effect.ts hand-authors a looser view\n// for the describer; prefer the generated, schema-canonical type.\nexport type { AbilityScope } from \"./generated.js\";\n\n// Terrain geometry resolver (template-anchored layout → board-space vertices).\n// Curated (not wildcard) so its internal type aliases don't clash with the\n// generated Footprint/TerrainTemplate/TerrainLayout types. Cross-impl pinned.\nexport {\n resolveLayout,\n polygonCentroid,\n footprintVertices,\n orientedOffsets,\n TerrainResolveError,\n solveCentroid,\n solveCentroidTriangulated,\n solveCentroidAttached,\n TerrainSolveError,\n keystoneMeasurements,\n BOARD_INCHES,\n TerrainKeystoneError,\n} from \"./terrain/index.js\";\nexport type {\n ResolvedPiece,\n ResolvedVec2,\n BoardEdge,\n FeatureRef,\n Keystone,\n KeystoneMeasurement,\n DimensionLine,\n SolveInput,\n TriangulationLine,\n TriangulateInput,\n AttachLine,\n AttachPiece,\n AttachInput,\n} from \"./terrain/index.js\";\n\n// Card-driven secondary-mission scoring: compute VP from asserted awards and\n// track per-round, per-player scoring. Mirrored by the Rust `wh40kdc::scoring`\n// module and pinned by the `conformance/scoring` corpus.\nexport * from \"./scoring/index.js\";\n\n// Damage-projection engine (expected-value math over weapon/unit profiles).\n// Mirrored by the Rust `wh40kdc::cruncher` module and pinned by the\n// `conformance/cruncher` corpus. Pure functions — universal (Node + browser).\nexport * from \"./cruncher/index.js\";\n\n// Fleet comparison (attacker set × target-profile set → expected-kills matrix),\n// built on the cruncher. Mirrors `python/src/wh40kdc/compare.py`; the per-cell\n// math is pinned against Python by the `conformance/compare` corpus.\nexport * from \"./compare.js\";\n\n// Roster-highlighting scope: resolve which units an ability's curated\n// `applies_to` keyword filter benefits. Mirrors `wh40kdc::scope` (Rust) and\n// `wh40kdc.scope` (Python); pinned by the `conformance/applies-to` corpus.\nexport * from \"./scope.js\";\n\n// Schema access + AJV validation lives behind the `./validate` subpath export\n// (`@alpaca-software/40kdc-data/validate`), NOT the root barrel: it reads\n// schema files from disk at module load (node:fs/node:url), which breaks\n// browser bundles. The root barrel stays universal (Node + browser).\n\n// Army-list importer (ListForge → resolved 40kdc roster). Types are curated\n// rather than re-exported wholesale to avoid name clashes with generated types\n// (e.g. BattleSize, LeaderAttachment).\nexport {\n importListForge,\n importNewRecruit,\n importRoster,\n tryImportRoster,\n decodeListForge,\n} from \"./import/index.js\";\n\n// Army-list exporter (Roster → text or JSON for any of the supported formats).\nexport {\n exportRoster,\n newRecruitJsonSerializer,\n newRecruitSimpleSerializer,\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n rosterJsonSerializer,\n rosterizerSerializer,\n} from \"./export/index.js\";\nexport type { ExportFormat, RosterSerializer } from \"./export/index.js\";\nexport type { FormatAdapter } from \"./import/index.js\";\nexport type {\n ImportOptions,\n ImportResult,\n ImportFailureReason,\n AdapterTrial,\n Roster,\n RosterUnit,\n RosterWargear,\n RosterSource,\n RosterFormat,\n RosterPoints,\n RosterLeaderAttachment,\n ResolvedRef,\n Candidate,\n Diagnostics,\n Warning,\n WarningCode,\n ParsedRoster,\n ParsedUnit,\n ParsedWargear,\n} from \"./import/index.js\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-faction-pack.d.ts","sourceRoot":"","sources":["../src/merge-faction-pack.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge IP-safe faction-pack staging (`data/_audit/faction-pack-input/<faction>.json`,
|
|
3
|
+
* produced by `extract-faction-pack.ts`) into `data/core/<faction>/`.
|
|
4
|
+
*
|
|
5
|
+
* For every staged detachment whose *core* entry is still a bare shell — empty
|
|
6
|
+
* `stratagem_ids` AND empty `enhancement_ids` — this creates the core stratagem
|
|
7
|
+
* and enhancement entities from the staged metadata and wires their ids onto the
|
|
8
|
+
* detachment. Detachments that already carry entities (converted from army-assist,
|
|
9
|
+
* or merged earlier) are left untouched, so re-running is additive and safe.
|
|
10
|
+
*
|
|
11
|
+
* The DSL link stays open: every created entity gets `ability_id: null`. Authoring
|
|
12
|
+
* the mechanic happens separately via the `author:*` pipeline.
|
|
13
|
+
*
|
|
14
|
+
* Field provenance (mirrors the hand-merged Space Marine packs):
|
|
15
|
+
* - stratagem: name + cp_cost + phases + player_turn straight from staging; the
|
|
16
|
+
* GW `type` is omitted (faction packs don't print it for new detachments) and
|
|
17
|
+
* `timing` defaults to `once-per-phase` when the pack didn't state a frequency.
|
|
18
|
+
* - enhancement: name from staging; `cost: 0` + `points_provisional: true` until
|
|
19
|
+
* the Munitorum Field Manual lands; `keyword_restrictions` is the faction's
|
|
20
|
+
* army keyword; `upgrade_tag: true` (faction-pack enhancements are upgrades).
|
|
21
|
+
*
|
|
22
|
+
* IP firewall: staging carries names + numeric/enum metadata only — never prose —
|
|
23
|
+
* so nothing this tool writes is GW rules text.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* tsx tools/src/merge-faction-pack.ts <faction-id> [<faction-id> …]
|
|
27
|
+
* tsx tools/src/merge-faction-pack.ts --all-xenos
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { resolve } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
33
|
+
const DATA_ROOT = resolve(__dirname, "../../data");
|
|
34
|
+
/** Faction-id → the army keyword that gates its enhancements. */
|
|
35
|
+
const FACTION_KEYWORD = {
|
|
36
|
+
orks: "Orks",
|
|
37
|
+
aeldari: "Aeldari",
|
|
38
|
+
drukhari: "Drukhari",
|
|
39
|
+
tyranids: "Tyranids",
|
|
40
|
+
"genestealer-cults": "Genestealer Cults",
|
|
41
|
+
necrons: "Necrons",
|
|
42
|
+
"leagues-of-votann": "Leagues of Votann",
|
|
43
|
+
"tau-empire": "T’au Empire",
|
|
44
|
+
};
|
|
45
|
+
const XENOS = Object.keys(FACTION_KEYWORD);
|
|
46
|
+
const GAME_VERSION = { edition: "11th", dataslate: "pre-launch-provisional" };
|
|
47
|
+
function readJson(path) {
|
|
48
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
49
|
+
}
|
|
50
|
+
function writeJson(path, value) {
|
|
51
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
|
|
52
|
+
}
|
|
53
|
+
function mergeFaction(faction) {
|
|
54
|
+
const keyword = FACTION_KEYWORD[faction];
|
|
55
|
+
if (!keyword)
|
|
56
|
+
throw new Error(`no faction keyword mapping for ${faction}`);
|
|
57
|
+
const dir = resolve(DATA_ROOT, "core", faction);
|
|
58
|
+
const staging = readJson(resolve(DATA_ROOT, "_audit", "faction-pack-input", `${faction}.json`));
|
|
59
|
+
const detachments = readJson(resolve(dir, "detachments.json"));
|
|
60
|
+
const stratagems = readJson(resolve(dir, "stratagems.json"));
|
|
61
|
+
const enhancements = readJson(resolve(dir, "enhancements.json"));
|
|
62
|
+
const detById = new Map(detachments.map((d) => [d.id, d]));
|
|
63
|
+
const stratIds = new Set(stratagems.map((s) => s.id));
|
|
64
|
+
const enhIds = new Set(enhancements.map((e) => e.id));
|
|
65
|
+
let addedStrat = 0;
|
|
66
|
+
let addedEnh = 0;
|
|
67
|
+
let wiredDets = 0;
|
|
68
|
+
const skipped = [];
|
|
69
|
+
const renames = [];
|
|
70
|
+
/**
|
|
71
|
+
* A globally-unique id for a new entity. GW reuses stratagem/enhancement names
|
|
72
|
+
* across detachments (e.g. Ork "Long, Uncontrolled Bursts" is in both More Dakka!
|
|
73
|
+
* and Rollin' Deff) — distinct mechanics that collide on the bare name kebab. On
|
|
74
|
+
* collision we detachment-scope the id so existing ids stay stable and the entity
|
|
75
|
+
* collection (keyed by id) never overwrites. The staging entry is renamed to match.
|
|
76
|
+
*/
|
|
77
|
+
function uniqueId(baseId, detId, taken) {
|
|
78
|
+
if (!taken.has(baseId))
|
|
79
|
+
return baseId;
|
|
80
|
+
const scoped = `${detId}-${baseId}`;
|
|
81
|
+
if (taken.has(scoped))
|
|
82
|
+
throw new Error(`cannot disambiguate id ${baseId} for ${detId}`);
|
|
83
|
+
renames.push(`${baseId} → ${scoped}`);
|
|
84
|
+
return scoped;
|
|
85
|
+
}
|
|
86
|
+
for (const sd of staging.detachments) {
|
|
87
|
+
const det = detById.get(sd.id);
|
|
88
|
+
if (!det) {
|
|
89
|
+
skipped.push(`${sd.id} (no core detachment)`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const isBareShell = (det.stratagem_ids ?? []).length === 0 &&
|
|
93
|
+
(det.enhancement_ids ?? []).length === 0;
|
|
94
|
+
if (!isBareShell)
|
|
95
|
+
continue; // already populated — leave it.
|
|
96
|
+
for (const s of sd.stratagems) {
|
|
97
|
+
s.id = uniqueId(s.id, sd.id, stratIds); // mutate staging to stay consistent
|
|
98
|
+
stratagems.push({
|
|
99
|
+
id: s.id,
|
|
100
|
+
name: s.name,
|
|
101
|
+
category: "detachment",
|
|
102
|
+
detachment_id: sd.id,
|
|
103
|
+
cp_cost: s.cp_cost ?? 1,
|
|
104
|
+
phases: s.phases,
|
|
105
|
+
player_turn: s.player_turn ?? "either",
|
|
106
|
+
timing: s.timing ?? "once-per-phase",
|
|
107
|
+
target_restrictions: null,
|
|
108
|
+
ability_id: null,
|
|
109
|
+
game_version: { ...GAME_VERSION },
|
|
110
|
+
});
|
|
111
|
+
stratIds.add(s.id);
|
|
112
|
+
addedStrat++;
|
|
113
|
+
}
|
|
114
|
+
for (const e of sd.enhancements) {
|
|
115
|
+
e.id = uniqueId(e.id, sd.id, enhIds);
|
|
116
|
+
enhancements.push({
|
|
117
|
+
id: e.id,
|
|
118
|
+
name: e.name,
|
|
119
|
+
detachment_id: sd.id,
|
|
120
|
+
cost: e.cost ?? 0,
|
|
121
|
+
keyword_restrictions: [keyword],
|
|
122
|
+
ability_id: null,
|
|
123
|
+
is_unique: true,
|
|
124
|
+
game_version: { ...GAME_VERSION },
|
|
125
|
+
points_provisional: true,
|
|
126
|
+
upgrade_tag: true,
|
|
127
|
+
max_targets: 1,
|
|
128
|
+
});
|
|
129
|
+
enhIds.add(e.id);
|
|
130
|
+
addedEnh++;
|
|
131
|
+
}
|
|
132
|
+
det.stratagem_ids = sd.stratagems.map((s) => s.id);
|
|
133
|
+
det.enhancement_ids = sd.enhancements.map((e) => e.id);
|
|
134
|
+
wiredDets++;
|
|
135
|
+
}
|
|
136
|
+
writeJson(resolve(dir, "detachments.json"), detachments);
|
|
137
|
+
writeJson(resolve(dir, "stratagems.json"), stratagems);
|
|
138
|
+
writeJson(resolve(dir, "enhancements.json"), enhancements);
|
|
139
|
+
// Persist any id renames back to staging so it stays the source for authoring.
|
|
140
|
+
if (renames.length)
|
|
141
|
+
writeJson(resolve(DATA_ROOT, "_audit", "faction-pack-input", `${faction}.json`), staging);
|
|
142
|
+
console.log(`${faction}: wired ${wiredDets} detachment(s), +${addedStrat} stratagems, +${addedEnh} enhancements` +
|
|
143
|
+
(renames.length ? `; renamed ${renames.join("; ")}` : "") +
|
|
144
|
+
(skipped.length ? `; skipped ${skipped.join(", ")}` : ""));
|
|
145
|
+
}
|
|
146
|
+
function main() {
|
|
147
|
+
const args = process.argv.slice(2);
|
|
148
|
+
const factions = args.includes("--all-xenos") ? XENOS : args;
|
|
149
|
+
if (factions.length === 0) {
|
|
150
|
+
console.error("usage: merge-faction-pack.ts <faction-id>… | --all-xenos");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
for (const f of factions)
|
|
154
|
+
mergeFaction(f);
|
|
155
|
+
}
|
|
156
|
+
main();
|
|
157
|
+
//# sourceMappingURL=merge-faction-pack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-faction-pack.js","sourceRoot":"","sources":["../src/merge-faction-pack.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Merge IP-safe faction-pack staging (`data/_audit/faction-pack-input/<faction>.json`,\n * produced by `extract-faction-pack.ts`) into `data/core/<faction>/`.\n *\n * For every staged detachment whose *core* entry is still a bare shell — empty\n * `stratagem_ids` AND empty `enhancement_ids` — this creates the core stratagem\n * and enhancement entities from the staged metadata and wires their ids onto the\n * detachment. Detachments that already carry entities (converted from army-assist,\n * or merged earlier) are left untouched, so re-running is additive and safe.\n *\n * The DSL link stays open: every created entity gets `ability_id: null`. Authoring\n * the mechanic happens separately via the `author:*` pipeline.\n *\n * Field provenance (mirrors the hand-merged Space Marine packs):\n * - stratagem: name + cp_cost + phases + player_turn straight from staging; the\n * GW `type` is omitted (faction packs don't print it for new detachments) and\n * `timing` defaults to `once-per-phase` when the pack didn't state a frequency.\n * - enhancement: name from staging; `cost: 0` + `points_provisional: true` until\n * the Munitorum Field Manual lands; `keyword_restrictions` is the faction's\n * army keyword; `upgrade_tag: true` (faction-pack enhancements are upgrades).\n *\n * IP firewall: staging carries names + numeric/enum metadata only — never prose —\n * so nothing this tool writes is GW rules text.\n *\n * Usage:\n * tsx tools/src/merge-faction-pack.ts <faction-id> [<faction-id> …]\n * tsx tools/src/merge-faction-pack.ts --all-xenos\n */\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\n\n/** Faction-id → the army keyword that gates its enhancements. */\nconst FACTION_KEYWORD: Record<string, string> = {\n orks: \"Orks\",\n aeldari: \"Aeldari\",\n drukhari: \"Drukhari\",\n tyranids: \"Tyranids\",\n \"genestealer-cults\": \"Genestealer Cults\",\n necrons: \"Necrons\",\n \"leagues-of-votann\": \"Leagues of Votann\",\n \"tau-empire\": \"T’au Empire\",\n};\n\nconst XENOS = Object.keys(FACTION_KEYWORD);\n\nconst GAME_VERSION = { edition: \"11th\", dataslate: \"pre-launch-provisional\" } as const;\n\ninterface StagedStratagem {\n id: string;\n name: string;\n type: string | null;\n cp_cost: number | null;\n phases: string[];\n player_turn: string | null;\n timing: string | null;\n}\ninterface StagedEnhancement {\n id: string;\n name: string;\n cost: number | null;\n}\ninterface StagedDetachment {\n id: string;\n name: string;\n detachment_rule_name: string | null;\n stratagems: StagedStratagem[];\n enhancements: StagedEnhancement[];\n}\ninterface Staging {\n faction_id: string;\n detachments: StagedDetachment[];\n}\n\nfunction readJson<T>(path: string): T {\n return JSON.parse(readFileSync(path, \"utf-8\")) as T;\n}\nfunction writeJson(path: string, value: unknown): void {\n writeFileSync(path, JSON.stringify(value, null, 2) + \"\\n\");\n}\n\nfunction mergeFaction(faction: string): void {\n const keyword = FACTION_KEYWORD[faction];\n if (!keyword) throw new Error(`no faction keyword mapping for ${faction}`);\n\n const dir = resolve(DATA_ROOT, \"core\", faction);\n const staging = readJson<Staging>(\n resolve(DATA_ROOT, \"_audit\", \"faction-pack-input\", `${faction}.json`),\n );\n const detachments = readJson<Record<string, unknown>[]>(resolve(dir, \"detachments.json\"));\n const stratagems = readJson<Record<string, unknown>[]>(resolve(dir, \"stratagems.json\"));\n const enhancements = readJson<Record<string, unknown>[]>(resolve(dir, \"enhancements.json\"));\n\n const detById = new Map(detachments.map((d) => [d.id as string, d]));\n const stratIds = new Set(stratagems.map((s) => s.id as string));\n const enhIds = new Set(enhancements.map((e) => e.id as string));\n\n let addedStrat = 0;\n let addedEnh = 0;\n let wiredDets = 0;\n const skipped: string[] = [];\n const renames: string[] = [];\n\n /**\n * A globally-unique id for a new entity. GW reuses stratagem/enhancement names\n * across detachments (e.g. Ork \"Long, Uncontrolled Bursts\" is in both More Dakka!\n * and Rollin' Deff) — distinct mechanics that collide on the bare name kebab. On\n * collision we detachment-scope the id so existing ids stay stable and the entity\n * collection (keyed by id) never overwrites. The staging entry is renamed to match.\n */\n function uniqueId(baseId: string, detId: string, taken: Set<string>): string {\n if (!taken.has(baseId)) return baseId;\n const scoped = `${detId}-${baseId}`;\n if (taken.has(scoped)) throw new Error(`cannot disambiguate id ${baseId} for ${detId}`);\n renames.push(`${baseId} → ${scoped}`);\n return scoped;\n }\n\n for (const sd of staging.detachments) {\n const det = detById.get(sd.id);\n if (!det) {\n skipped.push(`${sd.id} (no core detachment)`);\n continue;\n }\n const isBareShell =\n ((det.stratagem_ids as unknown[]) ?? []).length === 0 &&\n ((det.enhancement_ids as unknown[]) ?? []).length === 0;\n if (!isBareShell) continue; // already populated — leave it.\n\n for (const s of sd.stratagems) {\n s.id = uniqueId(s.id, sd.id, stratIds); // mutate staging to stay consistent\n stratagems.push({\n id: s.id,\n name: s.name,\n category: \"detachment\",\n detachment_id: sd.id,\n cp_cost: s.cp_cost ?? 1,\n phases: s.phases,\n player_turn: s.player_turn ?? \"either\",\n timing: s.timing ?? \"once-per-phase\",\n target_restrictions: null,\n ability_id: null,\n game_version: { ...GAME_VERSION },\n });\n stratIds.add(s.id);\n addedStrat++;\n }\n for (const e of sd.enhancements) {\n e.id = uniqueId(e.id, sd.id, enhIds);\n enhancements.push({\n id: e.id,\n name: e.name,\n detachment_id: sd.id,\n cost: e.cost ?? 0,\n keyword_restrictions: [keyword],\n ability_id: null,\n is_unique: true,\n game_version: { ...GAME_VERSION },\n points_provisional: true,\n upgrade_tag: true,\n max_targets: 1,\n });\n enhIds.add(e.id);\n addedEnh++;\n }\n\n det.stratagem_ids = sd.stratagems.map((s) => s.id);\n det.enhancement_ids = sd.enhancements.map((e) => e.id);\n wiredDets++;\n }\n\n writeJson(resolve(dir, \"detachments.json\"), detachments);\n writeJson(resolve(dir, \"stratagems.json\"), stratagems);\n writeJson(resolve(dir, \"enhancements.json\"), enhancements);\n // Persist any id renames back to staging so it stays the source for authoring.\n if (renames.length)\n writeJson(\n resolve(DATA_ROOT, \"_audit\", \"faction-pack-input\", `${faction}.json`),\n staging,\n );\n\n console.log(\n `${faction}: wired ${wiredDets} detachment(s), +${addedStrat} stratagems, +${addedEnh} enhancements` +\n (renames.length ? `; renamed ${renames.join(\"; \")}` : \"\") +\n (skipped.length ? `; skipped ${skipped.join(\", \")}` : \"\"),\n );\n}\n\nfunction main(): void {\n const args = process.argv.slice(2);\n const factions = args.includes(\"--all-xenos\") ? XENOS : args;\n if (factions.length === 0) {\n console.error(\"usage: merge-faction-pack.ts <faction-id>… | --all-xenos\");\n process.exit(1);\n }\n for (const f of factions) mergeFaction(f);\n}\n\nmain();\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** A positioned text block from `pdftotext -bbox-layout`. `gy` is page-globalised y. */
|
|
2
|
+
export interface Block {
|
|
3
|
+
x: number;
|
|
4
|
+
gy: number;
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
/** Section headers (caps) that are never card/enhancement names. */
|
|
8
|
+
export declare const SECTION_WORDS: RegExp;
|
|
9
|
+
export declare function runPdftotext(args: string[]): string;
|
|
10
|
+
export declare const decodeEntities: (s: string) => string;
|
|
11
|
+
/**
|
|
12
|
+
* Parse the whole pack's bbox-layout HTML into positioned blocks. `gy` globalises
|
|
13
|
+
* y across pages (pageIndex × 100000 + y) so blocks sort in reading order without
|
|
14
|
+
* relying on printed page numbers (which drift from physical pages on later pages
|
|
15
|
+
* when full-bleed art is inserted).
|
|
16
|
+
*/
|
|
17
|
+
export declare function allBlocks(pdf: string): Block[];
|
|
18
|
+
/** Title/caps name → kebab id, matching existing entity-id conventions. */
|
|
19
|
+
export declare function slug(name: string): string;
|
|
20
|
+
/** Title-case a pack ALL-CAPS header for storage as a display name. */
|
|
21
|
+
export declare function titleCase(raw: string): string;
|
|
22
|
+
export declare const normCaps: (s: string) => string;
|
|
23
|
+
/** A card/enhancement name candidate: a real caps header, not a section word or token. */
|
|
24
|
+
export declare const isCapsHeader: (t: string) => boolean;
|
|
25
|
+
/** Two blocks share a column when their left edges are within tolerance. */
|
|
26
|
+
export declare const sameColumn: (a: number, b: number) => boolean;
|
|
27
|
+
/** Split a trailing "… N CP" off a name block. */
|
|
28
|
+
export declare const stripCp: (t: string) => {
|
|
29
|
+
name: string;
|
|
30
|
+
cp: number | null;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=pack-blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack-blocks.d.ts","sourceRoot":"","sources":["../src/pack-blocks.ts"],"names":[],"mappings":"AAsBA,wFAAwF;AACxF,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,oEAAoE;AACpE,eAAO,MAAM,aAAa,QAA4E,CAAC;AAEvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CASnD;AAED,eAAO,MAAM,cAAc,GAAI,GAAG,MAAM,KAAG,MAMf,CAAC;AAE7B;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,EAAE,CAc9C;AAED,2EAA2E;AAC3E,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOzC;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ7C;AAED,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,KAAG,MAC8B,CAAC;AAEpE,0FAA0F;AAC1F,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,KAAG,OAQhB,CAAC;AAE1B,4EAA4E;AAC5E,eAAO,MAAM,UAAU,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,OAA+B,CAAC;AAElF,kDAAkD;AAClD,eAAO,MAAM,OAAO,GAAI,GAAG,MAAM,KAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAGpE,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared bbox-coordinate primitives for parsing GW 11e Faction Pack PDFs.
|
|
3
|
+
*
|
|
4
|
+
* GW lays each card out as discrete, positioned text blocks. `pdftotext
|
|
5
|
+
* -bbox-layout` emits per-word coordinates; we group them into blocks and locate
|
|
6
|
+
* cards by coordinate (column + reading-order y) rather than by the linear text
|
|
7
|
+
* stream — `-layout` interleaves the packs' 2-column stratagem cards into garbage.
|
|
8
|
+
*
|
|
9
|
+
* These primitives are layout-independent (column positions differ across packs)
|
|
10
|
+
* and are shared by two tools:
|
|
11
|
+
* - `extract-faction-pack.ts` — emits names + numeric/enum metadata only (the
|
|
12
|
+
* committed, IP-safe staging file). It never writes prose.
|
|
13
|
+
* - `author-input-pack.ts` — captures each card's rule body (prose) into the
|
|
14
|
+
* gitignored authoring worklist. It is the only consumer that emits prose.
|
|
15
|
+
*
|
|
16
|
+
* Keeping the coordinate machinery here lets both share it without weakening the
|
|
17
|
+
* first tool's "never emits prose" property.
|
|
18
|
+
*
|
|
19
|
+
* Requires `pdftotext` (poppler) on PATH — authoring-time only, not CI.
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
/** Section headers (caps) that are never card/enhancement names. */
|
|
23
|
+
export const SECTION_WORDS = /^(DETACHMENT RULES?|ENHANCEMENTS?|STRATAGEMS?|KEYWORDS?|RESTRICTIONS?)$/;
|
|
24
|
+
export function runPdftotext(args) {
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync("pdftotext", args, { encoding: "utf-8", maxBuffer: 128 * 1024 * 1024 });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err.code === "ENOENT") {
|
|
30
|
+
throw new Error("pdftotext not found — install poppler (e.g. `brew install poppler`).");
|
|
31
|
+
}
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const decodeEntities = (s) => s
|
|
36
|
+
.replace(/&/g, "&")
|
|
37
|
+
.replace(/</g, "<")
|
|
38
|
+
.replace(/>/g, ">")
|
|
39
|
+
.replace(/'/g, "'")
|
|
40
|
+
.replace(/"/g, '"');
|
|
41
|
+
/**
|
|
42
|
+
* Parse the whole pack's bbox-layout HTML into positioned blocks. `gy` globalises
|
|
43
|
+
* y across pages (pageIndex × 100000 + y) so blocks sort in reading order without
|
|
44
|
+
* relying on printed page numbers (which drift from physical pages on later pages
|
|
45
|
+
* when full-bleed art is inserted).
|
|
46
|
+
*/
|
|
47
|
+
export function allBlocks(pdf) {
|
|
48
|
+
const html = runPdftotext(["-bbox-layout", pdf, "-"]);
|
|
49
|
+
const blocks = [];
|
|
50
|
+
html.split(/<page\b/).slice(1).forEach((pageHtml, pageIdx) => {
|
|
51
|
+
const offset = pageIdx * 100000;
|
|
52
|
+
for (const m of pageHtml.matchAll(/<block\b([^>]*)>([\s\S]*?)<\/block>/g)) {
|
|
53
|
+
const x = Number(/xMin="([\d.]+)"/.exec(m[1])?.[1] ?? "0");
|
|
54
|
+
const y = Number(/yMin="([\d.]+)"/.exec(m[1])?.[1] ?? "0");
|
|
55
|
+
const words = [...m[2].matchAll(/<word[^>]*>([^<]*)<\/word>/g)].map((w) => decodeEntities(w[1]));
|
|
56
|
+
const text = words.join(" ").replace(/\s+/g, " ").trim();
|
|
57
|
+
if (text)
|
|
58
|
+
blocks.push({ x, gy: offset + y, text });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return blocks;
|
|
62
|
+
}
|
|
63
|
+
/** Title/caps name → kebab id, matching existing entity-id conventions. */
|
|
64
|
+
export function slug(name) {
|
|
65
|
+
return name
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[’']/g, "")
|
|
68
|
+
.replace(/&/g, " and ")
|
|
69
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
70
|
+
.replace(/^-+|-+$/g, "");
|
|
71
|
+
}
|
|
72
|
+
/** Title-case a pack ALL-CAPS header for storage as a display name. */
|
|
73
|
+
export function titleCase(raw) {
|
|
74
|
+
const small = new Set(["of", "the", "and", "to", "a", "in", "for"]);
|
|
75
|
+
return raw
|
|
76
|
+
.trim()
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.split(/\s+/)
|
|
79
|
+
.map((w, i) => (i > 0 && small.has(w) ? w : w.charAt(0).toUpperCase() + w.slice(1)))
|
|
80
|
+
.join(" ");
|
|
81
|
+
}
|
|
82
|
+
export const normCaps = (s) => s.toUpperCase().replace(/[’']/g, "'").replace(/\s+/g, " ").trim();
|
|
83
|
+
/** A card/enhancement name candidate: a real caps header, not a section word or token. */
|
|
84
|
+
export const isCapsHeader = (t) => /^[A-Z0-9][A-Z0-9'’.,!&/()\- ]*$/.test(t) &&
|
|
85
|
+
/[A-Z]{3,}/.test(t) && // a real word, not a "1CP"/"D6" token
|
|
86
|
+
t.length >= 3 &&
|
|
87
|
+
t.length <= 46 &&
|
|
88
|
+
t.split(" ").length <= 7 &&
|
|
89
|
+
!SECTION_WORDS.test(t) &&
|
|
90
|
+
!/^\d+\s?CP$/.test(t) &&
|
|
91
|
+
!/\bSTRATAGEM$/.test(t);
|
|
92
|
+
/** Two blocks share a column when their left edges are within tolerance. */
|
|
93
|
+
export const sameColumn = (a, b) => Math.abs(a - b) < 46;
|
|
94
|
+
/** Split a trailing "… N CP" off a name block. */
|
|
95
|
+
export const stripCp = (t) => {
|
|
96
|
+
const m = t.match(/^(.*?)[\s]+(\d+)\s?CP$/);
|
|
97
|
+
return m ? { name: m[1].trim(), cp: Number(m[2]) } : { name: t, cp: null };
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=pack-blocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pack-blocks.js","sourceRoot":"","sources":["../src/pack-blocks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared bbox-coordinate primitives for parsing GW 11e Faction Pack PDFs.\n *\n * GW lays each card out as discrete, positioned text blocks. `pdftotext\n * -bbox-layout` emits per-word coordinates; we group them into blocks and locate\n * cards by coordinate (column + reading-order y) rather than by the linear text\n * stream — `-layout` interleaves the packs' 2-column stratagem cards into garbage.\n *\n * These primitives are layout-independent (column positions differ across packs)\n * and are shared by two tools:\n * - `extract-faction-pack.ts` — emits names + numeric/enum metadata only (the\n * committed, IP-safe staging file). It never writes prose.\n * - `author-input-pack.ts` — captures each card's rule body (prose) into the\n * gitignored authoring worklist. It is the only consumer that emits prose.\n *\n * Keeping the coordinate machinery here lets both share it without weakening the\n * first tool's \"never emits prose\" property.\n *\n * Requires `pdftotext` (poppler) on PATH — authoring-time only, not CI.\n */\nimport { execFileSync } from \"node:child_process\";\n\n/** A positioned text block from `pdftotext -bbox-layout`. `gy` is page-globalised y. */\nexport interface Block {\n x: number;\n gy: number;\n text: string;\n}\n\n/** Section headers (caps) that are never card/enhancement names. */\nexport const SECTION_WORDS = /^(DETACHMENT RULES?|ENHANCEMENTS?|STRATAGEMS?|KEYWORDS?|RESTRICTIONS?)$/;\n\nexport function runPdftotext(args: string[]): string {\n try {\n return execFileSync(\"pdftotext\", args, { encoding: \"utf-8\", maxBuffer: 128 * 1024 * 1024 });\n } catch (err: unknown) {\n if ((err as { code?: string }).code === \"ENOENT\") {\n throw new Error(\"pdftotext not found — install poppler (e.g. `brew install poppler`).\");\n }\n throw err;\n }\n}\n\nexport const decodeEntities = (s: string): string =>\n s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/'/g, \"'\")\n .replace(/"/g, '\"');\n\n/**\n * Parse the whole pack's bbox-layout HTML into positioned blocks. `gy` globalises\n * y across pages (pageIndex × 100000 + y) so blocks sort in reading order without\n * relying on printed page numbers (which drift from physical pages on later pages\n * when full-bleed art is inserted).\n */\nexport function allBlocks(pdf: string): Block[] {\n const html = runPdftotext([\"-bbox-layout\", pdf, \"-\"]);\n const blocks: Block[] = [];\n html.split(/<page\\b/).slice(1).forEach((pageHtml, pageIdx) => {\n const offset = pageIdx * 100000;\n for (const m of pageHtml.matchAll(/<block\\b([^>]*)>([\\s\\S]*?)<\\/block>/g)) {\n const x = Number(/xMin=\"([\\d.]+)\"/.exec(m[1])?.[1] ?? \"0\");\n const y = Number(/yMin=\"([\\d.]+)\"/.exec(m[1])?.[1] ?? \"0\");\n const words = [...m[2].matchAll(/<word[^>]*>([^<]*)<\\/word>/g)].map((w) => decodeEntities(w[1]));\n const text = words.join(\" \").replace(/\\s+/g, \" \").trim();\n if (text) blocks.push({ x, gy: offset + y, text });\n }\n });\n return blocks;\n}\n\n/** Title/caps name → kebab id, matching existing entity-id conventions. */\nexport function slug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[’']/g, \"\")\n .replace(/&/g, \" and \")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/** Title-case a pack ALL-CAPS header for storage as a display name. */\nexport function titleCase(raw: string): string {\n const small = new Set([\"of\", \"the\", \"and\", \"to\", \"a\", \"in\", \"for\"]);\n return raw\n .trim()\n .toLowerCase()\n .split(/\\s+/)\n .map((w, i) => (i > 0 && small.has(w) ? w : w.charAt(0).toUpperCase() + w.slice(1)))\n .join(\" \");\n}\n\nexport const normCaps = (s: string): string =>\n s.toUpperCase().replace(/[’']/g, \"'\").replace(/\\s+/g, \" \").trim();\n\n/** A card/enhancement name candidate: a real caps header, not a section word or token. */\nexport const isCapsHeader = (t: string): boolean =>\n /^[A-Z0-9][A-Z0-9'’.,!&/()\\- ]*$/.test(t) &&\n /[A-Z]{3,}/.test(t) && // a real word, not a \"1CP\"/\"D6\" token\n t.length >= 3 &&\n t.length <= 46 &&\n t.split(\" \").length <= 7 &&\n !SECTION_WORDS.test(t) &&\n !/^\\d+\\s?CP$/.test(t) &&\n !/\\bSTRATAGEM$/.test(t);\n\n/** Two blocks share a column when their left edges are within tolerance. */\nexport const sameColumn = (a: number, b: number): boolean => Math.abs(a - b) < 46;\n\n/** Split a trailing \"… N CP\" off a name block. */\nexport const stripCp = (t: string): { name: string; cp: number | null } => {\n const m = t.match(/^(.*?)[\\s]+(\\d+)\\s?CP$/);\n return m ? { name: m[1].trim(), cp: Number(m[2]) } : { name: t, cp: null };\n};\n"]}
|
package/dist/rube-goldberg.js
CHANGED
|
@@ -51,7 +51,7 @@ function walkLinkedApi(label, roster, ds) {
|
|
|
51
51
|
rule(`${label}: walking the linked Dataset API`);
|
|
52
52
|
console.log(`Roster: ${roster.name} (${roster.units.length} units)`);
|
|
53
53
|
console.log(` faction_id = ${roster.faction_id ?? "—"}`);
|
|
54
|
-
console.log(`
|
|
54
|
+
console.log(` detachments = ${roster.detachments.map((d) => d.ref.id ?? d.ref.raw_name).join(", ") || "—"}`);
|
|
55
55
|
console.log(` total_computed = ${roster.points.total_computed}`);
|
|
56
56
|
for (const u of roster.units) {
|
|
57
57
|
if (!u.ref.id) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rube-goldberg.js","sourceRoot":"","sources":["../src/rube-goldberg.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAqB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,MAAM,SAAS,GAAG,IAAI,CACpB,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EACvC,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,QAAQ,EACR,yBAAyB,EACzB,4BAA4B,CAC7B,CAAC;AAEF,MAAM,IAAI,GAAmB;IAC3B,wBAAwB;IACxB,qBAAqB;IACrB,mBAAmB;IACnB,iBAAiB;CAClB,CAAC;AAEF;uEACuE;AACvE,SAAS,MAAM,CAAC,CAAS;IACvB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAA4B,CAAC;IACnE,OAAO,CAAC,CAAC,MAAM,CAAC;IAChB,OAAO,CAAC,CAAC,WAAW,CAAC;IACrB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,IAAI,CAAC,KAAa;IACzB,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,aAAa,CAAC,KAAa,EAAE,MAAc,EAAE,EAAW;IAC/D,IAAI,CAAC,GAAG,KAAK,kCAAkC,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"rube-goldberg.js","sourceRoot":"","sources":["../src/rube-goldberg.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env tsx\n/**\n * Rube Goldberg machine — round-trips a NewRecruit list through every\n * supported format and walks the linked Dataset API at each end.\n *\n * Loop:\n * ```\n * seed (newrecruit-json) → import → Roster₀\n * → export wtc-compact → import\n * → export wtc-full → import\n * → export simple → import\n * → export newrecruit-json → import → Rosterₙ\n * ```\n *\n * Asserts `Roster₀ == Rosterₙ` at the Roster level (after stripping\n * `source` + `diagnostics`, which legitimately change across hops). Exits\n * non-zero on divergence. Walks `unit.faction.units` / `weapon.units` /\n * `ability.phases` at each end to demonstrate the cross-reference loops.\n *\n * Strict IP rule: only ids, names, and counts may be printed. No ability\n * description text, no rules text — none of that is stored in the\n * dataset, so we can't accidentally leak it, but be careful adding fields.\n *\n * @packageDocumentation\n */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Dataset } from \"./data/dataset.js\";\nimport { exportRoster, type ExportFormat } from \"./export/index.js\";\nimport { importRoster } from \"./import/import-roster.js\";\nimport type { Roster } from \"./import/types.js\";\n\nconst SEED_PATH = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"..\",\n \"conformance\",\n \"roster\",\n \"chaos-knights-houndpack\",\n \"input.newrecruit-json.json\",\n);\n\nconst HOPS: ExportFormat[] = [\n \"newrecruit-wtc-compact\",\n \"newrecruit-wtc-full\",\n \"newrecruit-simple\",\n \"newrecruit-json\",\n];\n\n/** Strip the fields that legitimately change across format hops so the\n * fixed-point comparison can focus on the structural Roster shape. */\nfunction stable(r: Roster): Record<string, unknown> {\n const x = JSON.parse(JSON.stringify(r)) as Record<string, unknown>;\n delete x.source;\n delete x.diagnostics;\n return x;\n}\n\nfunction rule(title: string): void {\n console.log(`\\n── ${title} ${\"─\".repeat(Math.max(0, 60 - title.length))}`);\n}\n\nfunction walkLinkedApi(label: string, roster: Roster, ds: Dataset): void {\n rule(`${label}: walking the linked Dataset API`);\n console.log(`Roster: ${roster.name} (${roster.units.length} units)`);\n console.log(` faction_id = ${roster.faction_id ?? \"—\"}`);\n console.log(\n ` detachments = ${\n roster.detachments.map((d) => d.ref.id ?? d.ref.raw_name).join(\", \") || \"—\"\n }`,\n );\n console.log(` total_computed = ${roster.points.total_computed}`);\n\n for (const u of roster.units) {\n if (!u.ref.id) {\n console.log(` · ${u.ref.raw_name} [unresolved]`);\n continue;\n }\n const unit = ds.units.get(u.ref.id);\n if (!unit) {\n console.log(` · ${u.ref.id} [not in dataset]`);\n continue;\n }\n const fac = unit.faction;\n const facUnits = fac?.units.length ?? 0;\n console.log(` · ${unit.id} → faction=${fac?.id ?? \"—\"} (faction has ${facUnits} units)`);\n\n for (const w of unit.weapons) {\n const carriers = w.units.length;\n console.log(` weapon=${w.id} carried by ${carriers} units`);\n }\n for (const a of unit.abilities) {\n const phaseIds = a.phases.join(\",\") || \"—\";\n const carriers = a.units.length;\n console.log(` ability=${a.id} phases=[${phaseIds}] on ${carriers} units`);\n }\n }\n}\n\nfunction main(): number {\n const ds = Dataset.embedded();\n const seedJson = JSON.parse(readFileSync(SEED_PATH, \"utf8\"));\n\n rule(\"seed\");\n console.log(`Loading: conformance/roster/chaos-knights-houndpack/input.newrecruit-json.json`);\n\n const roster0 = importRoster(seedJson, { dataset: ds });\n walkLinkedApi(\"Roster₀\", roster0, ds);\n\n let current: Roster = roster0;\n for (const [i, fmt] of HOPS.entries()) {\n const text = exportRoster(current, fmt);\n const bytes = Buffer.byteLength(text, \"utf8\");\n const isJson = fmt === \"newrecruit-json\";\n const reimported = importRoster(isJson ? JSON.parse(text) : text, { dataset: ds });\n console.log(\n `[hop ${i + 1}/${HOPS.length}] ${fmt} → ${bytes.toLocaleString()} bytes → reimport: ${reimported.units.length} units (computed ${reimported.points.total_computed} pts)`,\n );\n current = reimported;\n }\n\n walkLinkedApi(\"Rosterₙ\", current, ds);\n\n rule(\"fixed-point check\");\n const before = stable(roster0);\n const after = stable(current);\n const equal = JSON.stringify(before) === JSON.stringify(after);\n if (equal) {\n console.log(`Roster₀ == Rosterₙ after ${HOPS.length} format hops (source/diagnostics excluded).`);\n return 0;\n }\n console.error(`Roster₀ ≠ Rosterₙ — round-trip diverged.`);\n return 1;\n}\n\nprocess.exit(main());\n"]}
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":";AAsBA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAsC5C,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AA8C3B,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,QAAA,MAAM,WAAW,oJASP,CAAC;AACX,KAAK,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAgB9C,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C;AAmID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,CAAC,EACG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACrG,IAAI,GACJ,SAAS,GACZ,MAAM,GAAG,IAAI,CAQf;AA0mBD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,cAAc,CAkDhG;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9E"}
|