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

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 (62) hide show
  1. package/dist/codegen-data.js +3 -1
  2. package/dist/codegen-data.js.map +1 -1
  3. package/dist/data/bundle.generated.js +1 -1
  4. package/dist/data/bundle.generated.js.map +1 -1
  5. package/dist/data/dataset.d.ts +17 -2
  6. package/dist/data/dataset.d.ts.map +1 -1
  7. package/dist/data/dataset.js +27 -2
  8. package/dist/data/dataset.js.map +1 -1
  9. package/dist/data/index.d.ts +5 -1
  10. package/dist/data/index.d.ts.map +1 -1
  11. package/dist/data/index.js +5 -1
  12. package/dist/data/index.js.map +1 -1
  13. package/dist/data/types.d.ts +6 -2
  14. package/dist/data/types.d.ts.map +1 -1
  15. package/dist/data/types.js +3 -1
  16. package/dist/data/types.js.map +1 -1
  17. package/dist/gen-conformance.js +163 -2
  18. package/dist/gen-conformance.js.map +1 -1
  19. package/dist/generated.d.ts +156 -36
  20. package/dist/generated.d.ts.map +1 -1
  21. package/dist/generated.js.map +1 -1
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +8 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/migrate-terrain.d.ts +2 -0
  27. package/dist/migrate-terrain.d.ts.map +1 -0
  28. package/dist/migrate-terrain.js +297 -0
  29. package/dist/migrate-terrain.js.map +1 -0
  30. package/dist/runner.d.ts.map +1 -1
  31. package/dist/runner.js +27 -1
  32. package/dist/runner.js.map +1 -1
  33. package/dist/scoring/index.d.ts +135 -0
  34. package/dist/scoring/index.d.ts.map +1 -0
  35. package/dist/scoring/index.js +195 -0
  36. package/dist/scoring/index.js.map +1 -0
  37. package/dist/terrain/index.d.ts +11 -0
  38. package/dist/terrain/index.d.ts.map +1 -0
  39. package/dist/terrain/index.js +9 -0
  40. package/dist/terrain/index.js.map +1 -0
  41. package/dist/terrain/resolve.d.ts +122 -0
  42. package/dist/terrain/resolve.d.ts.map +1 -0
  43. package/dist/terrain/resolve.js +221 -0
  44. package/dist/terrain/resolve.js.map +1 -0
  45. package/dist/terrain/solve.d.ts +56 -0
  46. package/dist/terrain/solve.d.ts.map +1 -0
  47. package/dist/terrain/solve.js +80 -0
  48. package/dist/terrain/solve.js.map +1 -0
  49. package/dist/translate/index.d.ts +1 -1
  50. package/dist/translate/index.d.ts.map +1 -1
  51. package/dist/translate/index.js.map +1 -1
  52. package/dist/translate/scoring.d.ts +6 -0
  53. package/dist/translate/scoring.d.ts.map +1 -1
  54. package/dist/translate/scoring.js.map +1 -1
  55. package/dist/validate.d.ts.map +1 -1
  56. package/dist/validate.js +1 -0
  57. package/dist/validate.js.map +1 -1
  58. package/package.json +2 -1
  59. package/schemas/$defs/common.schema.json +43 -0
  60. package/schemas/core/secondary-card.schema.json +10 -0
  61. package/schemas/core/terrain-layout.schema.json +42 -56
  62. package/schemas/core/terrain-template.schema.json +105 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Card-measurement centroid solver — the inverse of the resolver's placement.
3
+ *
4
+ * Reference cards locate a terrain area by dimension lines: "this feature of the
5
+ * area is D inches from a board edge". The feature referenced varies per card
6
+ * and per piece, which is exactly why a single canonical anchor (the centroid)
7
+ * is hard to read off a card directly. This solver lets a user transcribe the
8
+ * card verbatim — pick the template, set the orientation shown, then enter one
9
+ * horizontal and one vertical dimension line against whatever feature the card
10
+ * happens to draw — and back-solves the centroid `position` the schema stores.
11
+ *
12
+ * Because the centroid is rotation- and mirror-invariant, orientation is fixed
13
+ * first; each dimension line then pins one axis of the centroid in closed form.
14
+ */
15
+ import { type Footprint, type Mirror, type Vec2 } from "./resolve.js";
16
+ /** A board edge a card dimension is measured from. left/right pin x; top/bottom pin y. */
17
+ export type BoardEdge = "left" | "right" | "top" | "bottom";
18
+ /**
19
+ * Which feature of the placed area a dimension line reaches: a specific
20
+ * footprint vertex (by index, in {@link footprintVertices} order), or one of
21
+ * the placed area's axis-aligned bounding faces ("the left face", etc.).
22
+ */
23
+ export type FeatureRef = {
24
+ kind: "vertex";
25
+ index: number;
26
+ } | {
27
+ kind: "face";
28
+ side: "min-x" | "max-x" | "min-y" | "max-y";
29
+ };
30
+ /** One card dimension line: `distance` inches from `edge` to `feature`. */
31
+ export interface DimensionLine {
32
+ edge: BoardEdge;
33
+ distance: number;
34
+ feature: FeatureRef;
35
+ }
36
+ export interface SolveInput {
37
+ footprint: Footprint;
38
+ rotation: number;
39
+ mirror: Mirror;
40
+ /** Board extents in inches (40kdc standard is 60 × 44). */
41
+ board: {
42
+ width: number;
43
+ height: number;
44
+ };
45
+ /** Two perpendicular dimension lines: exactly one must pin x, one must pin y. */
46
+ lines: [DimensionLine, DimensionLine];
47
+ }
48
+ export declare class TerrainSolveError extends Error {
49
+ }
50
+ /**
51
+ * Back-solve the centroid `position` from a template, its orientation, and two
52
+ * perpendicular card dimension lines. Closed form — one x-line and one y-line
53
+ * pin the two unknowns directly.
54
+ */
55
+ export declare function solveCentroid(input: SolveInput): Vec2;
56
+ //# sourceMappingURL=solve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solve.d.ts","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAEvF,0FAA0F;AAC1F,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAA;CAAE,CAAC;AAElE,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iFAAiF;IACjF,KAAK,EAAE,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;CACvC;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAkD/C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAYrD"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Card-measurement centroid solver — the inverse of the resolver's placement.
3
+ *
4
+ * Reference cards locate a terrain area by dimension lines: "this feature of the
5
+ * area is D inches from a board edge". The feature referenced varies per card
6
+ * and per piece, which is exactly why a single canonical anchor (the centroid)
7
+ * is hard to read off a card directly. This solver lets a user transcribe the
8
+ * card verbatim — pick the template, set the orientation shown, then enter one
9
+ * horizontal and one vertical dimension line against whatever feature the card
10
+ * happens to draw — and back-solves the centroid `position` the schema stores.
11
+ *
12
+ * Because the centroid is rotation- and mirror-invariant, orientation is fixed
13
+ * first; each dimension line then pins one axis of the centroid in closed form.
14
+ */
15
+ import { orientedOffsets } from "./resolve.js";
16
+ export class TerrainSolveError extends Error {
17
+ }
18
+ /** The signed offset (from the centroid) the given feature resolves to, on its axis. */
19
+ function featureOffset(offsets, feature, axis) {
20
+ if (feature.kind === "vertex") {
21
+ const o = offsets[feature.index];
22
+ if (!o)
23
+ throw new TerrainSolveError(`vertex index ${feature.index} out of range`);
24
+ return axis === "x" ? o.x : o.y;
25
+ }
26
+ const xs = offsets.map((o) => o.x);
27
+ const ys = offsets.map((o) => o.y);
28
+ switch (feature.side) {
29
+ case "min-x":
30
+ return Math.min(...xs);
31
+ case "max-x":
32
+ return Math.max(...xs);
33
+ case "min-y":
34
+ return Math.min(...ys);
35
+ case "max-y":
36
+ return Math.max(...ys);
37
+ }
38
+ }
39
+ function axisOfEdge(edge) {
40
+ return edge === "left" || edge === "right" ? "x" : "y";
41
+ }
42
+ /** Solve one axis of the centroid from a single dimension line. */
43
+ function solveAxis(line, offsets, board) {
44
+ const axis = axisOfEdge(line.edge);
45
+ const o = featureOffset(offsets, line.feature, axis);
46
+ // edge → centroid: near-side edges measure from 0; far-side from the extent.
47
+ let value;
48
+ switch (line.edge) {
49
+ case "left":
50
+ value = line.distance - o;
51
+ break;
52
+ case "right":
53
+ value = board.width - line.distance - o;
54
+ break;
55
+ case "top":
56
+ value = line.distance - o;
57
+ break;
58
+ case "bottom":
59
+ value = board.height - line.distance - o;
60
+ break;
61
+ }
62
+ return { axis, value };
63
+ }
64
+ /**
65
+ * Back-solve the centroid `position` from a template, its orientation, and two
66
+ * perpendicular card dimension lines. Closed form — one x-line and one y-line
67
+ * pin the two unknowns directly.
68
+ */
69
+ export function solveCentroid(input) {
70
+ const offsets = orientedOffsets(input.footprint, input.rotation, input.mirror);
71
+ const a = solveAxis(input.lines[0], offsets, input.board);
72
+ const b = solveAxis(input.lines[1], offsets, input.board);
73
+ if (a.axis === b.axis) {
74
+ throw new TerrainSolveError("the two dimension lines must pin different axes (one of left/right, one of top/bottom)");
75
+ }
76
+ const x = a.axis === "x" ? a.value : b.value;
77
+ const y = a.axis === "y" ? a.value : b.value;
78
+ return { x, y };
79
+ }
80
+ //# sourceMappingURL=solve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solve.js","sourceRoot":"","sources":["../../src/terrain/solve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Card-measurement centroid solver — the inverse of the resolver's placement.\n *\n * Reference cards locate a terrain area by dimension lines: \"this feature of the\n * area is D inches from a board edge\". The feature referenced varies per card\n * and per piece, which is exactly why a single canonical anchor (the centroid)\n * is hard to read off a card directly. This solver lets a user transcribe the\n * card verbatim — pick the template, set the orientation shown, then enter one\n * horizontal and one vertical dimension line against whatever feature the card\n * happens to draw — and back-solves the centroid `position` the schema stores.\n *\n * Because the centroid is rotation- and mirror-invariant, orientation is fixed\n * first; each dimension line then pins one axis of the centroid in closed form.\n */\nimport { orientedOffsets, type Footprint, type Mirror, type Vec2 } from \"./resolve.js\";\n\n/** A board edge a card dimension is measured from. left/right pin x; top/bottom pin y. */\nexport type BoardEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n/**\n * Which feature of the placed area a dimension line reaches: a specific\n * footprint vertex (by index, in {@link footprintVertices} order), or one of\n * the placed area's axis-aligned bounding faces (\"the left face\", etc.).\n */\nexport type FeatureRef =\n | { kind: \"vertex\"; index: number }\n | { kind: \"face\"; side: \"min-x\" | \"max-x\" | \"min-y\" | \"max-y\" };\n\n/** One card dimension line: `distance` inches from `edge` to `feature`. */\nexport interface DimensionLine {\n edge: BoardEdge;\n distance: number;\n feature: FeatureRef;\n}\n\nexport interface SolveInput {\n footprint: Footprint;\n rotation: number;\n mirror: Mirror;\n /** Board extents in inches (40kdc standard is 60 × 44). */\n board: { width: number; height: number };\n /** Two perpendicular dimension lines: exactly one must pin x, one must pin y. */\n lines: [DimensionLine, DimensionLine];\n}\n\nexport class TerrainSolveError extends Error {}\n\n/** The signed offset (from the centroid) the given feature resolves to, on its axis. */\nfunction featureOffset(offsets: Vec2[], feature: FeatureRef, axis: \"x\" | \"y\"): number {\n if (feature.kind === \"vertex\") {\n const o = offsets[feature.index];\n if (!o) throw new TerrainSolveError(`vertex index ${feature.index} out of range`);\n return axis === \"x\" ? o.x : o.y;\n }\n const xs = offsets.map((o) => o.x);\n const ys = offsets.map((o) => o.y);\n switch (feature.side) {\n case \"min-x\":\n return Math.min(...xs);\n case \"max-x\":\n return Math.max(...xs);\n case \"min-y\":\n return Math.min(...ys);\n case \"max-y\":\n return Math.max(...ys);\n }\n}\n\nfunction axisOfEdge(edge: BoardEdge): \"x\" | \"y\" {\n return edge === \"left\" || edge === \"right\" ? \"x\" : \"y\";\n}\n\n/** Solve one axis of the centroid from a single dimension line. */\nfunction solveAxis(line: DimensionLine, offsets: Vec2[], board: { width: number; height: number }): { axis: \"x\" | \"y\"; value: number } {\n const axis = axisOfEdge(line.edge);\n const o = featureOffset(offsets, line.feature, axis);\n // edge → centroid: near-side edges measure from 0; far-side from the extent.\n let value: number;\n switch (line.edge) {\n case \"left\":\n value = line.distance - o;\n break;\n case \"right\":\n value = board.width - line.distance - o;\n break;\n case \"top\":\n value = line.distance - o;\n break;\n case \"bottom\":\n value = board.height - line.distance - o;\n break;\n }\n return { axis, value };\n}\n\n/**\n * Back-solve the centroid `position` from a template, its orientation, and two\n * perpendicular card dimension lines. Closed form — one x-line and one y-line\n * pin the two unknowns directly.\n */\nexport function solveCentroid(input: SolveInput): Vec2 {\n const offsets = orientedOffsets(input.footprint, input.rotation, input.mirror);\n const a = solveAxis(input.lines[0], offsets, input.board);\n const b = solveAxis(input.lines[1], offsets, input.board);\n if (a.axis === b.axis) {\n throw new TerrainSolveError(\n \"the two dimension lines must pin different axes (one of left/right, one of top/bottom)\",\n );\n }\n const x = a.axis === \"x\" ? a.value : b.value;\n const y = a.axis === \"y\" ? a.value : b.value;\n return { x, y };\n}\n"]}
@@ -5,5 +5,5 @@
5
5
  * across language ports by the `conformance/scoring-translation` corpus.
6
6
  */
7
7
  export { describeCondition, dekebab, type Condition } from "./condition.js";
8
- export { describeTrigger, describeAward, describeScoringCard, type ScoringTrigger, type ScoringAward, } from "./scoring.js";
8
+ export { describeTrigger, describeAward, describeScoringCard, type ScoringTrigger, type ScoringAward, type ScoringMode, } from "./scoring.js";
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,GAGpB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Plain-English translation of structured game data — currently the\n * `secondary-card` scoring `awards` (mission \"how to play\" readouts) and the\n * shared Ability-DSL condition humanizer. Output is ASCII-only and pinned\n * across language ports by the `conformance/scoring-translation` corpus.\n */\nexport { describeCondition, dekebab, type Condition } from \"./condition.js\";\nexport {\n describeTrigger,\n describeAward,\n describeScoringCard,\n type ScoringTrigger,\n type ScoringAward,\n} from \"./scoring.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/translate/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,GAIpB,MAAM,cAAc,CAAC","sourcesContent":["/**\n * Plain-English translation of structured game data — currently the\n * `secondary-card` scoring `awards` (mission \"how to play\" readouts) and the\n * shared Ability-DSL condition humanizer. Output is ASCII-only and pinned\n * across language ports by the `conformance/scoring-translation` corpus.\n */\nexport { describeCondition, dekebab, type Condition } from \"./condition.js\";\nexport {\n describeTrigger,\n describeAward,\n describeScoringCard,\n type ScoringTrigger,\n type ScoringAward,\n type ScoringMode,\n} from \"./scoring.js\";\n"]}
@@ -18,6 +18,8 @@ export interface ScoringTrigger {
18
18
  max?: number;
19
19
  };
20
20
  }
21
+ /** The scoring approach a card is played under (cards that print both). */
22
+ export type ScoringMode = "fixed" | "tactical";
21
23
  /** One VP-award block on a scoring card. */
22
24
  export interface ScoringAward {
23
25
  trigger?: ScoringTrigger;
@@ -26,8 +28,12 @@ export interface ScoringAward {
26
28
  vp_per?: number;
27
29
  per?: string;
28
30
  per_max?: number;
31
+ /** Per-game VP ceiling for this award (the card's "UP TO N VP"). */
32
+ vp_max?: number;
29
33
  cumulative?: boolean;
30
34
  exclusive_group?: string;
35
+ /** Which scoring track this award belongs to, on cards that print both. */
36
+ mode?: ScoringMode;
31
37
  }
32
38
  /** "End of your Command phase (round 2+)" and friends. */
33
39
  export declare function describeTrigger(t: ScoringTrigger): string;
@@ -1 +1 @@
1
- {"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5E,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,GAAG,eAAe,CAAC;IAC/F,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC;IACvD,YAAY,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAyCzD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAiBrD;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAGjE"}
1
+ {"version":3,"file":"scoring.d.ts","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE5E,sEAAsE;AACtE,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,GAAG,eAAe,CAAC;IAC/F,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjE,WAAW,CAAC,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC;IACvD,YAAY,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,CAAC;AAE/C,4CAA4C;AAC5C,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAMD,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAyCzD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAiBrD;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,EAAE,CAGjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Humanize a `secondary-card` scoring `award` into plain English.\n *\n * Output is **ASCII-only** with a fixed clause order, pinned byte-for-byte\n * across the TS and Rust ports by the `conformance/scoring-translation` corpus.\n * The community `text` summary and the `actions` list are verbatim data, not\n * translation, so they are not produced here — only the structured `awards`.\n */\n\nimport type { SecondaryCard } from \"../generated.js\";\nimport { describeCondition, dekebab, type Condition } from \"./condition.js\";\n\n/** When a VP award is evaluated (the `trigger` block on an award). */\nexport interface ScoringTrigger {\n timing?: \"start-of-turn\" | \"end-of-turn\" | \"start-of-phase\" | \"end-of-phase\" | \"end-of-battle\";\n phase?: \"command\" | \"movement\" | \"shooting\" | \"charge\" | \"fight\";\n player_turn?: \"your-turn\" | \"opponent-turn\" | \"either\";\n battle_round?: { min?: number; max?: number };\n}\n\n/** One VP-award block on a scoring card. */\nexport interface ScoringAward {\n trigger?: ScoringTrigger;\n when?: Condition;\n vp?: number;\n vp_per?: number;\n per?: string;\n per_max?: number;\n cumulative?: boolean;\n exclusive_group?: string;\n}\n\nfunction capitalize(s: string): string {\n return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);\n}\n\n/** \"End of your Command phase (round 2+)\" and friends. */\nexport function describeTrigger(t: ScoringTrigger): string {\n const turn =\n t.player_turn === \"opponent-turn\"\n ? \"the opponent's\"\n : t.player_turn === \"either\"\n ? \"any\"\n : \"your\";\n\n let base: string;\n switch (t.timing) {\n case \"start-of-turn\":\n base = `Start of ${turn} turn`;\n break;\n case \"end-of-turn\":\n base = `End of ${turn} turn`;\n break;\n case \"start-of-phase\":\n base = `Start of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-phase\":\n base = `End of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-battle\":\n base = \"End of the battle\";\n break;\n default:\n base = t.phase ? `During ${turn} ${capitalize(t.phase)} phase` : \"Any time\";\n }\n\n const br = t.battle_round;\n if (br) {\n const { min, max } = br;\n if (min != null && max != null) {\n base += min === max ? ` (round ${min})` : ` (rounds ${min}-${max})`;\n } else if (min != null) {\n base += ` (round ${min}+)`;\n } else if (max != null) {\n base += ` (rounds 1-${max})`;\n }\n }\n return base;\n}\n\n/** \"End of your Command phase (round 2+): 3 VP per controlled objective when ...\" */\nexport function describeAward(a: ScoringAward): string {\n const trigger = a.trigger ? describeTrigger(a.trigger) : \"Any time\";\n\n let amount: string;\n if (a.vp != null) {\n amount = `${a.vp} VP`;\n } else if (a.vp_per != null) {\n amount = `${a.vp_per} VP per ${a.per ? dekebab(a.per) : \"instance\"}`;\n if (a.per_max != null) amount += ` (max ${a.per_max})`;\n } else {\n amount = \"no VP\";\n }\n\n const prefix = a.cumulative ? \"+ \" : \"\";\n const when = a.when ? ` when ${describeCondition(a.when)}` : \"\";\n const tier = a.exclusive_group ? \" [highest tier]\" : \"\";\n return `${prefix}${trigger}: ${amount}${when}${tier}`;\n}\n\n/** Humanize every award on a card, in array order (the order is load-bearing). */\nexport function describeScoringCard(card: SecondaryCard): string[] {\n const awards = (card.awards ?? []) as unknown as ScoringAward[];\n return awards.map(describeAward);\n}\n"]}
1
+ {"version":3,"file":"scoring.js","sourceRoot":"","sources":["../../src/translate/scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAkB,MAAM,gBAAgB,CAAC;AA6B5E,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,eAAe,CAAC,CAAiB;IAC/C,MAAM,IAAI,GACR,CAAC,CAAC,WAAW,KAAK,eAAe;QAC/B,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,CAAC,CAAC,WAAW,KAAK,QAAQ;YAC1B,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,MAAM,CAAC;IAEf,IAAI,IAAY,CAAC;IACjB,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;QACjB,KAAK,eAAe;YAClB,IAAI,GAAG,YAAY,IAAI,OAAO,CAAC;YAC/B,MAAM;QACR,KAAK,aAAa;YAChB,IAAI,GAAG,UAAU,IAAI,OAAO,CAAC;YAC7B,MAAM;QACR,KAAK,gBAAgB;YACnB,IAAI,GAAG,YAAY,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,QAAQ,CAAC;YAC7D,MAAM;QACR,KAAK,cAAc;YACjB,IAAI,GAAG,UAAU,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,QAAQ,CAAC;YAC3D,MAAM;QACR,KAAK,eAAe;YAClB,IAAI,GAAG,mBAAmB,CAAC;YAC3B,MAAM;QACR;YACE,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC;IAChF,CAAC;IAED,MAAM,EAAE,GAAG,CAAC,CAAC,YAAY,CAAC;IAC1B,IAAI,EAAE,EAAE,CAAC;QACP,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;QACxB,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YAC/B,IAAI,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,GAAG,CAAC,CAAC,CAAC,YAAY,GAAG,IAAI,GAAG,GAAG,CAAC;QACtE,CAAC;aAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,IAAI,WAAW,GAAG,IAAI,CAAC;QAC7B,CAAC;aAAM,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,IAAI,cAAc,GAAG,GAAG,CAAC;QAC/B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,aAAa,CAAC,CAAe;IAC3C,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IAEpE,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC;IACxB,CAAC;SAAM,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;QACrE,IAAI,CAAC,CAAC,OAAO,IAAI,IAAI;YAAE,MAAM,IAAI,SAAS,CAAC,CAAC,OAAO,GAAG,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,OAAO,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,MAAM,IAAI,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,OAAO,GAAG,MAAM,GAAG,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;AACxD,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,IAAmB;IACrD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAA8B,CAAC;IAChE,OAAO,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Humanize a `secondary-card` scoring `award` into plain English.\n *\n * Output is **ASCII-only** with a fixed clause order, pinned byte-for-byte\n * across the TS and Rust ports by the `conformance/scoring-translation` corpus.\n * The community `text` summary and the `actions` list are verbatim data, not\n * translation, so they are not produced here — only the structured `awards`.\n */\n\nimport type { SecondaryCard } from \"../generated.js\";\nimport { describeCondition, dekebab, type Condition } from \"./condition.js\";\n\n/** When a VP award is evaluated (the `trigger` block on an award). */\nexport interface ScoringTrigger {\n timing?: \"start-of-turn\" | \"end-of-turn\" | \"start-of-phase\" | \"end-of-phase\" | \"end-of-battle\";\n phase?: \"command\" | \"movement\" | \"shooting\" | \"charge\" | \"fight\";\n player_turn?: \"your-turn\" | \"opponent-turn\" | \"either\";\n battle_round?: { min?: number; max?: number };\n}\n\n/** The scoring approach a card is played under (cards that print both). */\nexport type ScoringMode = \"fixed\" | \"tactical\";\n\n/** One VP-award block on a scoring card. */\nexport interface ScoringAward {\n trigger?: ScoringTrigger;\n when?: Condition;\n vp?: number;\n vp_per?: number;\n per?: string;\n per_max?: number;\n /** Per-game VP ceiling for this award (the card's \"UP TO N VP\"). */\n vp_max?: number;\n cumulative?: boolean;\n exclusive_group?: string;\n /** Which scoring track this award belongs to, on cards that print both. */\n mode?: ScoringMode;\n}\n\nfunction capitalize(s: string): string {\n return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);\n}\n\n/** \"End of your Command phase (round 2+)\" and friends. */\nexport function describeTrigger(t: ScoringTrigger): string {\n const turn =\n t.player_turn === \"opponent-turn\"\n ? \"the opponent's\"\n : t.player_turn === \"either\"\n ? \"any\"\n : \"your\";\n\n let base: string;\n switch (t.timing) {\n case \"start-of-turn\":\n base = `Start of ${turn} turn`;\n break;\n case \"end-of-turn\":\n base = `End of ${turn} turn`;\n break;\n case \"start-of-phase\":\n base = `Start of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-phase\":\n base = `End of ${turn} ${capitalize(t.phase ?? \"\")} phase`;\n break;\n case \"end-of-battle\":\n base = \"End of the battle\";\n break;\n default:\n base = t.phase ? `During ${turn} ${capitalize(t.phase)} phase` : \"Any time\";\n }\n\n const br = t.battle_round;\n if (br) {\n const { min, max } = br;\n if (min != null && max != null) {\n base += min === max ? ` (round ${min})` : ` (rounds ${min}-${max})`;\n } else if (min != null) {\n base += ` (round ${min}+)`;\n } else if (max != null) {\n base += ` (rounds 1-${max})`;\n }\n }\n return base;\n}\n\n/** \"End of your Command phase (round 2+): 3 VP per controlled objective when ...\" */\nexport function describeAward(a: ScoringAward): string {\n const trigger = a.trigger ? describeTrigger(a.trigger) : \"Any time\";\n\n let amount: string;\n if (a.vp != null) {\n amount = `${a.vp} VP`;\n } else if (a.vp_per != null) {\n amount = `${a.vp_per} VP per ${a.per ? dekebab(a.per) : \"instance\"}`;\n if (a.per_max != null) amount += ` (max ${a.per_max})`;\n } else {\n amount = \"no VP\";\n }\n\n const prefix = a.cumulative ? \"+ \" : \"\";\n const when = a.when ? ` when ${describeCondition(a.when)}` : \"\";\n const tier = a.exclusive_group ? \" [highest tier]\" : \"\";\n return `${prefix}${trigger}: ${amount}${when}${tier}`;\n}\n\n/** Humanize every award on a card, in array order (the order is load-bearing). */\nexport function describeScoringCard(card: SecondaryCard): string[] {\n const awards = (card.awards ?? []) as unknown as ScoringAward[];\n return awards.map(describeAward);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AA4CD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CA+E3B"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAS3B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AA6CD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,CAAC,CA+E3B"}
package/dist/validate.js CHANGED
@@ -24,6 +24,7 @@ const SCHEMA_MAP = {
24
24
  "mission-matchups": "https://40kdc.dev/schemas/core/mission-matchup.schema.json",
25
25
  missions: "https://40kdc.dev/schemas/core/mission.schema.json",
26
26
  "secondary-cards": "https://40kdc.dev/schemas/core/secondary-card.schema.json",
27
+ "terrain-templates": "https://40kdc.dev/schemas/core/terrain-template.schema.json",
27
28
  "terrain-layouts": "https://40kdc.dev/schemas/core/terrain-layout.schema.json",
28
29
  "phase-mappings": "https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json",
29
30
  "timing-flags": "https://40kdc.dev/schemas/enrichment/timing-flag.schema.json",
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type Ajv from \"ajv\";\nimport { readFileSync } from \"node:fs\";\nimport { glob } from \"glob\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\n\nexport interface ValidationError {\n file: string;\n index: number;\n errors: Array<{ path: string; message: string }>;\n}\n\nexport interface ValidationResult {\n totalFiles: number;\n totalItems: number;\n passed: number;\n failed: number;\n errors: ValidationError[];\n}\n\n/**\n * Map from data file base-name prefix to schema $id.\n */\nconst SCHEMA_MAP: Record<string, string> = {\n factions: \"https://40kdc.dev/schemas/core/faction.schema.json\",\n units: \"https://40kdc.dev/schemas/core/unit.schema.json\",\n weapons: \"https://40kdc.dev/schemas/core/weapon.schema.json\",\n \"weapon-keywords\": \"https://40kdc.dev/schemas/core/weapon-keyword.schema.json\",\n \"game-versions\": \"https://40kdc.dev/schemas/core/game-version.schema.json\",\n detachments: \"https://40kdc.dev/schemas/core/detachment.schema.json\",\n enhancements: \"https://40kdc.dev/schemas/core/enhancement.schema.json\",\n stratagems: \"https://40kdc.dev/schemas/core/stratagem.schema.json\",\n \"wargear-options\": \"https://40kdc.dev/schemas/core/wargear-option.schema.json\",\n \"leader-attachments\": \"https://40kdc.dev/schemas/core/leader-attachment.schema.json\",\n \"unit-compositions\": \"https://40kdc.dev/schemas/core/unit-composition.schema.json\",\n \"force-dispositions\": \"https://40kdc.dev/schemas/core/force-disposition.schema.json\",\n \"deployment-patterns\": \"https://40kdc.dev/schemas/core/deployment-pattern.schema.json\",\n \"mission-matchups\": \"https://40kdc.dev/schemas/core/mission-matchup.schema.json\",\n missions: \"https://40kdc.dev/schemas/core/mission.schema.json\",\n \"secondary-cards\": \"https://40kdc.dev/schemas/core/secondary-card.schema.json\",\n \"terrain-layouts\": \"https://40kdc.dev/schemas/core/terrain-layout.schema.json\",\n \"phase-mappings\": \"https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json\",\n \"timing-flags\": \"https://40kdc.dev/schemas/enrichment/timing-flag.schema.json\",\n \"interaction-flags\": \"https://40kdc.dev/schemas/enrichment/interaction-flag.schema.json\",\n abilities: \"https://40kdc.dev/schemas/enrichment/ability-dsl/ability.schema.json\",\n \"resource-pools\": \"https://40kdc.dev/schemas/enrichment/resource-pool.schema.json\",\n};\n\n/**\n * Determine which schema $id to use for a given data file path.\n * Convention: the file's base name prefix (before the first dot) maps to a schema.\n */\nfunction resolveSchemaId(filePath: string): string | null {\n const base = basename(filePath);\n for (const [prefix, schemaId] of Object.entries(SCHEMA_MAP)) {\n if (base.startsWith(prefix)) {\n return schemaId;\n }\n }\n return null;\n}\n\n/**\n * Validate all data files matching the given glob pattern.\n * Each data file is expected to be a JSON array; each element is validated individually.\n */\nexport async function validateFiles(\n ajv: Ajv,\n pattern: string,\n cwd?: string,\n): Promise<ValidationResult> {\n const root = cwd ?? DATA_ROOT;\n const files = await glob(pattern, { cwd: root, absolute: true });\n\n const result: ValidationResult = {\n totalFiles: files.length,\n totalItems: 0,\n passed: 0,\n failed: 0,\n errors: [],\n };\n\n for (const file of files) {\n const schemaId = resolveSchemaId(file);\n if (!schemaId) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `No schema mapping found for file: ${basename(file)}` }],\n });\n result.failed++;\n continue;\n }\n\n const validate = ajv.getSchema(schemaId);\n if (!validate) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Schema not found: ${schemaId}` }],\n });\n result.failed++;\n continue;\n }\n\n let data: unknown;\n try {\n const raw = readFileSync(file, \"utf-8\");\n data = JSON.parse(raw);\n } catch (err) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Failed to parse JSON: ${(err as Error).message}` }],\n });\n result.failed++;\n continue;\n }\n\n if (!Array.isArray(data)) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: \"Data file must be a JSON array\" }],\n });\n result.failed++;\n continue;\n }\n\n for (let i = 0; i < data.length; i++) {\n result.totalItems++;\n const valid = validate(data[i]);\n if (valid) {\n result.passed++;\n } else {\n result.failed++;\n result.errors.push({\n file,\n index: i,\n errors: (validate.errors ?? []).map((e) => ({\n path: e.instancePath || \"/\",\n message: e.message ?? \"Unknown validation error\",\n })),\n });\n }\n }\n }\n\n return result;\n}\n"]}
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type Ajv from \"ajv\";\nimport { readFileSync } from \"node:fs\";\nimport { glob } from \"glob\";\nimport { resolve, basename } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = fileURLToPath(new URL(\".\", import.meta.url));\nconst DATA_ROOT = resolve(__dirname, \"../../data\");\n\nexport interface ValidationError {\n file: string;\n index: number;\n errors: Array<{ path: string; message: string }>;\n}\n\nexport interface ValidationResult {\n totalFiles: number;\n totalItems: number;\n passed: number;\n failed: number;\n errors: ValidationError[];\n}\n\n/**\n * Map from data file base-name prefix to schema $id.\n */\nconst SCHEMA_MAP: Record<string, string> = {\n factions: \"https://40kdc.dev/schemas/core/faction.schema.json\",\n units: \"https://40kdc.dev/schemas/core/unit.schema.json\",\n weapons: \"https://40kdc.dev/schemas/core/weapon.schema.json\",\n \"weapon-keywords\": \"https://40kdc.dev/schemas/core/weapon-keyword.schema.json\",\n \"game-versions\": \"https://40kdc.dev/schemas/core/game-version.schema.json\",\n detachments: \"https://40kdc.dev/schemas/core/detachment.schema.json\",\n enhancements: \"https://40kdc.dev/schemas/core/enhancement.schema.json\",\n stratagems: \"https://40kdc.dev/schemas/core/stratagem.schema.json\",\n \"wargear-options\": \"https://40kdc.dev/schemas/core/wargear-option.schema.json\",\n \"leader-attachments\": \"https://40kdc.dev/schemas/core/leader-attachment.schema.json\",\n \"unit-compositions\": \"https://40kdc.dev/schemas/core/unit-composition.schema.json\",\n \"force-dispositions\": \"https://40kdc.dev/schemas/core/force-disposition.schema.json\",\n \"deployment-patterns\": \"https://40kdc.dev/schemas/core/deployment-pattern.schema.json\",\n \"mission-matchups\": \"https://40kdc.dev/schemas/core/mission-matchup.schema.json\",\n missions: \"https://40kdc.dev/schemas/core/mission.schema.json\",\n \"secondary-cards\": \"https://40kdc.dev/schemas/core/secondary-card.schema.json\",\n \"terrain-templates\": \"https://40kdc.dev/schemas/core/terrain-template.schema.json\",\n \"terrain-layouts\": \"https://40kdc.dev/schemas/core/terrain-layout.schema.json\",\n \"phase-mappings\": \"https://40kdc.dev/schemas/enrichment/phase-mapping.schema.json\",\n \"timing-flags\": \"https://40kdc.dev/schemas/enrichment/timing-flag.schema.json\",\n \"interaction-flags\": \"https://40kdc.dev/schemas/enrichment/interaction-flag.schema.json\",\n abilities: \"https://40kdc.dev/schemas/enrichment/ability-dsl/ability.schema.json\",\n \"resource-pools\": \"https://40kdc.dev/schemas/enrichment/resource-pool.schema.json\",\n};\n\n/**\n * Determine which schema $id to use for a given data file path.\n * Convention: the file's base name prefix (before the first dot) maps to a schema.\n */\nfunction resolveSchemaId(filePath: string): string | null {\n const base = basename(filePath);\n for (const [prefix, schemaId] of Object.entries(SCHEMA_MAP)) {\n if (base.startsWith(prefix)) {\n return schemaId;\n }\n }\n return null;\n}\n\n/**\n * Validate all data files matching the given glob pattern.\n * Each data file is expected to be a JSON array; each element is validated individually.\n */\nexport async function validateFiles(\n ajv: Ajv,\n pattern: string,\n cwd?: string,\n): Promise<ValidationResult> {\n const root = cwd ?? DATA_ROOT;\n const files = await glob(pattern, { cwd: root, absolute: true });\n\n const result: ValidationResult = {\n totalFiles: files.length,\n totalItems: 0,\n passed: 0,\n failed: 0,\n errors: [],\n };\n\n for (const file of files) {\n const schemaId = resolveSchemaId(file);\n if (!schemaId) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `No schema mapping found for file: ${basename(file)}` }],\n });\n result.failed++;\n continue;\n }\n\n const validate = ajv.getSchema(schemaId);\n if (!validate) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Schema not found: ${schemaId}` }],\n });\n result.failed++;\n continue;\n }\n\n let data: unknown;\n try {\n const raw = readFileSync(file, \"utf-8\");\n data = JSON.parse(raw);\n } catch (err) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: `Failed to parse JSON: ${(err as Error).message}` }],\n });\n result.failed++;\n continue;\n }\n\n if (!Array.isArray(data)) {\n result.errors.push({\n file,\n index: -1,\n errors: [{ path: \"\", message: \"Data file must be a JSON array\" }],\n });\n result.failed++;\n continue;\n }\n\n for (let i = 0; i < data.length; i++) {\n result.totalItems++;\n const valid = validate(data[i]);\n if (valid) {\n result.passed++;\n } else {\n result.failed++;\n result.errors.push({\n file,\n index: i,\n errors: (validate.errors ?? []).map((e) => ({\n path: e.instancePath || \"/\",\n message: e.message ?? \"Unknown validation error\",\n })),\n });\n }\n }\n }\n\n return result;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpaca-software/40kdc-data",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "The 40kdc Warhammer 40K dataset behind a linked, typed API — find units, follow them to their weapons, abilities, phases, and factions. Also validates data against the canonical JSON Schemas.",
6
6
  "keywords": [
@@ -52,6 +52,7 @@
52
52
  "codegen:data": "tsx src/codegen-data.ts",
53
53
  "gen:conformance": "tsx src/gen-conformance.ts",
54
54
  "docs:api": "typedoc",
55
+ "docs:api:html": "typedoc --options typedoc.html.json",
55
56
  "link:abilities": "tsx src/link-abilities.ts",
56
57
  "validate": "tsx src/cli.ts validate-all",
57
58
  "validate:core": "tsx src/cli.ts validate-core",
@@ -81,6 +81,49 @@
81
81
  },
82
82
  "required": ["x", "y"],
83
83
  "additionalProperties": false
84
+ },
85
+ "footprint": {
86
+ "description": "A terrain piece's 2D footprint in local inches (y-down): an axis-aligned rectangle with its min corner at the local origin, a right triangle with the right angle at the local origin and legs along +x/+y, or an explicit polygon (>= 3 points). The placement resolver re-centers the footprint on its polygon area centroid, so the local-origin convention does not affect where the piece lands — only its shape matters.",
87
+ "oneOf": [
88
+ {
89
+ "type": "object",
90
+ "properties": {
91
+ "type": { "const": "rectangle" },
92
+ "width": { "type": "number", "exclusiveMinimum": 0 },
93
+ "height": { "type": "number", "exclusiveMinimum": 0 }
94
+ },
95
+ "required": ["type", "width", "height"],
96
+ "additionalProperties": false
97
+ },
98
+ {
99
+ "type": "object",
100
+ "properties": {
101
+ "type": { "const": "right-triangle" },
102
+ "width": { "type": "number", "exclusiveMinimum": 0 },
103
+ "height": { "type": "number", "exclusiveMinimum": 0 }
104
+ },
105
+ "required": ["type", "width", "height"],
106
+ "additionalProperties": false
107
+ },
108
+ {
109
+ "type": "object",
110
+ "properties": {
111
+ "type": { "const": "polygon" },
112
+ "points": {
113
+ "type": "array",
114
+ "items": { "$ref": "#/$defs/vec2" },
115
+ "minItems": 3
116
+ }
117
+ },
118
+ "required": ["type", "points"],
119
+ "additionalProperties": false
120
+ }
121
+ ]
122
+ },
123
+ "terrain-area-keyword": {
124
+ "type": "string",
125
+ "enum": ["obscuring", "hidden", "plunging-fire"],
126
+ "description": "An 11e terrain-area keyword. Confirmed launch set; extend as further keywords publish on dataslate."
84
127
  }
85
128
  }
86
129
  }
@@ -186,6 +186,16 @@
186
186
  "minimum": 1,
187
187
  "description": "Optional cap on how many instances `vp_per` counts."
188
188
  },
189
+ "vp_max": {
190
+ "type": "integer",
191
+ "minimum": 1,
192
+ "description": "Optional cap on the total VP this award can contribute over the game — the card's 'UP TO N VP' ceiling. Distinct from `per_max`, which caps the instance count; use `vp_max` when the printed ceiling is not a multiple of `vp_per` (e.g. Burden of Trust's '2VP per guarded objective, up to 9VP')."
193
+ },
194
+ "mode": {
195
+ "type": "string",
196
+ "enum": ["fixed", "tactical"],
197
+ "description": "Which scoring track this award belongs to on cards that print both. Fixed missions are chosen for the whole game and score the (usually lower) `fixed` values; Tactical missions are drawn each turn and score the `tactical` values. Omitted on cards that score the same regardless of approach. A card may carry parallel `fixed` and `tactical` awards for the same condition; a consumer scores only the awards matching the player's chosen approach."
198
+ },
189
199
  "cumulative": {
190
200
  "type": "boolean",
191
201
  "default": false,
@@ -2,84 +2,68 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://40kdc.dev/schemas/core/terrain-layout.schema.json",
4
4
  "title": "Terrain Layout",
5
- "description": "A recommended arrangement of terrain pieces on the board, independent of the deployment map (a deployment-pattern references the layouts it recommends via recommended_terrain_layout_ids). Geometry is the source of truth; the GW standard piece templates are expressed as explicit footprints, with an optional descriptive `template` label. Footprints are deliberately open (not enum-locked) the launch catalog and its size are unconfirmed, so this models any shape rather than a fixed set. No layout data is authored yet.",
5
+ "description": "A recommended arrangement of terrain pieces on the board, independent of the deployment map (a deployment-pattern references the layouts it recommends via recommended_terrain_layout_ids). Each piece draws its geometry from a catalog `template` (a terrain-template entity) or an inline `footprint`; geometry is the source of truth. Placement is template-centroid-anchored: `position` is the piece's centroid, which is invariant under rotation and mirror, so orientation and location are decoupled. Resolved board-space vertices are derived by the shared terrain resolver (pinned by the conformance corpus), never stored here. No layout data is authored yet beyond migrated examples.",
6
6
  "type": "object",
7
7
  "$defs": {
8
- "footprint": {
9
- "description": "A terrain piece's 2D footprint, relative to the piece's `position`. Axis-aligned rectangle, right triangle (right angle at the local origin, legs along +x/+y), or an explicit polygon. GW's standard templates (e.g. 7\"×11.5\" rectangles, 8\"×11.5\" right triangles, 6\"×4\" rectangles, 10\"×2.5\" and 6\"×2\" lines) are all expressible here; lines are thin rectangles.",
10
- "oneOf": [
11
- {
12
- "type": "object",
13
- "properties": {
14
- "type": { "const": "rectangle" },
15
- "width": { "type": "number", "exclusiveMinimum": 0 },
16
- "height": { "type": "number", "exclusiveMinimum": 0 }
17
- },
18
- "required": ["type", "width", "height"],
19
- "additionalProperties": false
20
- },
21
- {
22
- "type": "object",
23
- "properties": {
24
- "type": { "const": "right-triangle" },
25
- "width": { "type": "number", "exclusiveMinimum": 0 },
26
- "height": { "type": "number", "exclusiveMinimum": 0 }
27
- },
28
- "required": ["type", "width", "height"],
29
- "additionalProperties": false
30
- },
31
- {
32
- "type": "object",
33
- "properties": {
34
- "type": { "const": "polygon" },
35
- "points": {
36
- "type": "array",
37
- "items": { "$ref": "../defs/common.schema.json#/$defs/vec2" },
38
- "minItems": 3
39
- }
40
- },
41
- "required": ["type", "points"],
42
- "additionalProperties": false
43
- }
44
- ]
45
- },
46
- "terrain-area-keyword": {
47
- "type": "string",
48
- "enum": ["obscuring", "hidden", "plunging-fire"],
49
- "description": "An 11e terrain-area keyword. Confirmed launch set; extend as further keywords publish on dataslate."
50
- },
51
8
  "piece": {
52
9
  "type": "object",
53
- "description": "One terrain feature placed on the board.",
10
+ "description": "One terrain piece placed on the board. Geometry comes from a catalog `template` or an inline `footprint` (if both are present, `footprint` is authoritative and `template` is provenance).",
54
11
  "properties": {
12
+ "id": {
13
+ "$ref": "../defs/common.schema.json#/$defs/entity-id",
14
+ "description": "Layout-local id. Required for any piece referenced by another piece's `parent_area_id`."
15
+ },
55
16
  "name": { "type": "string", "minLength": 1, "maxLength": 128 },
56
- "footprint": { "$ref": "#/$defs/footprint" },
17
+ "piece_type": {
18
+ "type": "string",
19
+ "enum": ["area", "feature"],
20
+ "default": "area",
21
+ "description": "An `area` is a gameplay terrain zone (the 11e 'terrain area'); a `feature` is physical scenery (walls, containers, pipes) placed on an area."
22
+ },
23
+ "template": {
24
+ "$ref": "../defs/common.schema.json#/$defs/entity-id",
25
+ "description": "Id of the terrain-template this piece instances. Footprint and defaults (height, blocking, keywords) are taken from that template unless overridden here."
26
+ },
27
+ "footprint": {
28
+ "$ref": "../defs/common.schema.json#/$defs/footprint",
29
+ "description": "Inline geometry, standing in for or overriding a template footprint. Authoritative when present."
30
+ },
57
31
  "position": {
58
32
  "$ref": "../defs/common.schema.json#/$defs/vec2",
59
- "description": "Board-inch placement of the footprint's local origin."
33
+ "description": "Placement of the piece's CENTROID (the polygon area centroid of its footprint). Rotation- and mirror-invariant: changing `rotation_degrees` or `mirror` never moves this point. In board inches, unless the piece is a feature with `parent_area_id`, in which case it is in the parent area's centroid-local frame."
60
34
  },
61
35
  "rotation_degrees": {
62
36
  "type": "number",
63
37
  "minimum": 0,
64
38
  "exclusiveMaximum": 360,
65
- "description": "Clockwise rotation of the footprint about `position`. Absent or 0 means axis-aligned."
39
+ "description": "Clockwise rotation about the centroid in the y-down board frame. Absent or 0 means the template's natural orientation."
66
40
  },
67
- "template": {
41
+ "mirror": {
68
42
  "type": "string",
69
- "minLength": 1,
70
- "maxLength": 64,
71
- "description": "Optional descriptive label for the GW standard template this piece uses (e.g. 'large-ruin', 'long-wall'). Free-form, not enum-locked — the geometry in `footprint` is authoritative."
43
+ "enum": ["none", "horizontal", "vertical"],
44
+ "default": "none",
45
+ "description": "Reflection applied in the centroid-local frame before rotation: `horizontal` negates local x (left-right flip), `vertical` negates local y."
46
+ },
47
+ "parent_area_id": {
48
+ "$ref": "../defs/common.schema.json#/$defs/entity-id",
49
+ "description": "For a feature: the layout-local id of the area it sits on. The feature's `position`/`rotation_degrees`/`mirror` are composed with the parent area's placement, so moving, rotating, or mirroring the area carries the feature with it."
50
+ },
51
+ "floor": {
52
+ "type": "integer",
53
+ "minimum": 0,
54
+ "default": 0,
55
+ "description": "Ruin floor this piece occupies (0 = ground level)."
72
56
  },
73
57
  "height_inches": {
74
58
  "type": "number",
75
59
  "minimum": 0,
76
- "description": "Height of the piece in inches. Gates Plunging Fire (a piece 3\" or taller confers +1 BS on ground-level targets)."
60
+ "description": "Height of the piece in inches; overrides the template default. Gates Plunging Fire (a piece 3\" or taller confers +1 BS on ground-level targets)."
77
61
  },
78
62
  "terrain_area_keywords": {
79
63
  "type": "array",
80
64
  "uniqueItems": true,
81
- "description": "Terrain-area keywords this piece's area carries.",
82
- "items": { "$ref": "#/$defs/terrain-area-keyword" }
65
+ "description": "Terrain-area keywords this piece's area carries; overrides the template default.",
66
+ "items": { "$ref": "../defs/common.schema.json#/$defs/terrain-area-keyword" }
83
67
  },
84
68
  "link_group": {
85
69
  "type": "string",
@@ -109,7 +93,9 @@
109
93
  "additionalProperties": false
110
94
  }
111
95
  },
112
- "required": ["footprint", "position"],
96
+ "required": ["position"],
97
+ "if": { "not": { "required": ["template"] } },
98
+ "then": { "required": ["footprint"] },
113
99
  "additionalProperties": false
114
100
  }
115
101
  },